lsmodules tweaks

New features which should be implemented in Porteus; suggestions are welcome. All questions or problems with testing releases (alpha, beta, or rc) should go in their relevant thread here, rather than the Bug Reports section.
pterid
Contributor
Contributor
Posts: 110
Joined: 01 Feb 2025, 20:13
Distribution: Porteus 5.01 Xfce on ext4 USB

lsmodules tweaks

Post#1 by pterid » 03 May 2026, 13:30

I had been meaning to play with some changes to lsmodules (Porteus Modules app) for a while.

I made two small changes so far which I would hope are quite uncontroversial:
  • A small spinner at the top that appears during activation/deactivation, and goes away when activation is complete and the module list has been refreshed.
  • A refresh button that forces re-indexing of the modules folders - useful if you have manually moved some modules around.
Image

The diff from the latest 5.01 version is quite small. The only "trick" is the addition of a new function run_module_activator called via a very short GLib timeout (100 milliseconds), so that the on_row_activated function can finish up, and the spinner can become visible, before the subprocess is run.

Code: Select all

114a115
>         self.spinner = Gtk.Spinner()
117c118,119
<         self.add_button = Gtk.Button.new_from_icon_name("folder-add", Gtk.IconSize.BUTTON)
---
>         self.hb_top.pack_start(self.spinner, False, False, 0)
>         self.add_button = Gtk.Button.new_from_icon_name("folder-new", Gtk.IconSize.BUTTON)
124a127,130
>         self.refresh_button = Gtk.Button.new_from_icon_name("view-refresh", Gtk.IconSize.BUTTON)
>         self.refresh_button.connect("clicked", self.on_refresh_button_clicked)
>         self.hb_top.pack_end(self.refresh_button, False, False, 0)
> 
165a172,173
>     def on_refresh_button_clicked(self, button):
>         self.set_modules_model()
236a245
>         self.spinner.start()
246c255,256
<                 run([cmd_string, row[3] + "/" + row[1]])
---
>                 mod_path = row[3] + "/" + row[1]
>                 GLib.timeout_add(100, self.run_module_activator, cmd_string, mod_path)
248a259
>                 self.spinner.stop()
249a261,263
>     def run_module_activator(self, cmd_string, mod_path):
>         run([cmd_string, mod_path])
> 
358c372
<         
---
>         self.spinner.stop()

pterid
Contributor
Contributor
Posts: 110
Joined: 01 Feb 2025, 20:13
Distribution: Porteus 5.01 Xfce on ext4 USB

lsmodules tweaks

Post#2 by pterid » 04 May 2026, 16:41

I've done what I wanted this weekend, which is to integrate lsmodules with upgrade-mods 0.0.3 (if you have it installed of course).

1. If upgrade-mods is in your PATH and you right-click a module, youwill see a right-click context menu:

Image

2. If you left-click the menu, a terminal window opens and runs upgrade-mods (it's interactive, type y then y then Y to actually replace the old module)

Image

3. Click the new refresh button, and the list will show your new upgraded module in place.

Image

Full source of the modified lsmodules:

Code: Select all

#!/usr/bin/python3

## Porteus Module Activation Status
## Author: jssouza

import os
import shutil
import glob
import gi
gi.require_version('Gtk', '3.0')
gi.require_version('Vte', '2.91')
from gi.repository import Gtk, Vte, Gdk, GdkPixbuf, Gio, GLib
from subprocess import run

import signal
signal.signal(signal.SIGINT, signal.SIG_DFL)


class TermWindow(Gtk.Window):

    def __init__(self, command_args):
        Gtk.Window.__init__(self, title = "Update Module", border_width = 5, height_request = 500, width_request = 550, icon_name = "system-settings")

        self.vb = Gtk.Box(spacing = 5, orientation = Gtk.Orientation.VERTICAL)
        self.vb.set_homogeneous(False)

        terminal = Vte.Terminal()
        pty = Vte.Pty.new_sync(Vte.PtyFlags.DEFAULT)
        terminal.set_pty(pty)
        pty.spawn_async(None, command_args, None, GLib.SpawnFlags.DO_NOT_REAP_CHILD, None, None, -1, None, self.ready)

        self.connect("delete-event", self.on_window_close)

        scrolledwindow = Gtk.ScrolledWindow()
        scrolledwindow.add(terminal)

        self.vb.pack_start(scrolledwindow, True, True, 0)

        self.hb_bottom = Gtk.Box(spacing = 5)
        self.hb_bottom.set_homogeneous(False)
        self.ok_button = Gtk.Button.new_with_label("Quit")
        self.ok_button.connect("clicked", self.on_quit_button_clicked)
        self.hb_bottom.pack_end(self.ok_button, False, False, 6)

        self.vb.pack_start(self.hb_bottom, False, False, 6)
        self.add(self.vb)

    def ready(self, pty, task):
        # print('ready')
        None

    def on_window_close(self):
        self.destroy()

    def on_quit_button_clicked(self, button):
        self.destroy()


class PortModules:
    '''Port Modules Class'''

    backing_files = []
    extramod_dirs = []
    extramod_paths = {}
    is_copy_2_ram = False	

    def __init__(self):
        self.set_extra_mods()
        self.check_copy2ram()

    def check_copy2ram(self):
        with open('/etc/bootcmd.cfg', encoding = 'utf-8') as fd:
            self.is_copy_2_ram = "copy2ram" in fd.read()

    def set_extra_mods(self):
        with open('/etc/bootcmd.cfg', encoding = 'utf-8') as fd:
            for fline in fd:
                if fline.startswith('extramod='):
                    fline = fline[9:-1]
                    self.extramod_dirs = fline.split(';')
                    i = len(self.extramod_dirs)
                    while i > 0:
                        if self.extramod_dirs[i - 1].startswith("UUID") or self.extramod_dirs[i - 1].startswith("LABEL"):
                            index = self.extramod_dirs[i - 1].find("/")
                            self.extramod_dirs[i - 1] = self.extramod_dirs[i - 1][index + 1:]
                        i -= 1
                    # print(self.extramod_dirs)

    def set_extra_mod_paths(self):
        self.extramod_paths.clear()
        for extramod_dir in self.extramod_dirs:
            for backing_file in self.backing_files:
                if extramod_dir + "/" in backing_file:
                    index = backing_file.rfind("/")
                    self.extramod_paths[extramod_dir] = backing_file[:index]
                    break
        return self.extramod_paths

    def set_backing_files(self):
        del self.backing_files[:]
        os.chdir('/sys/block/')
        loop_devices = glob.glob('loop*')
        for loop_device in loop_devices:
            if os.path.exists('/sys/block/' + loop_device + '/loop/'):
                with open('/sys/block/' + loop_device + '/loop/backing_file', encoding = 'utf-8') as fd:
                    fline = fd.read()
                    if fline[:-1].endswith(".xzm"):
                        self.backing_files.append(fline[:-1])
        return len(self.backing_files)
        # print(self.backing_files)

    def populate_modules(self, path):
        module_list = {}
        if os.path.exists(path):     
            os.chdir(path)
            modules = glob.glob('*.xzm')
            modules.sort()
            for module in modules:
                if os.path.realpath(module) in self.backing_files:
                    is_activated = True
                    self.backing_files.remove(os.path.realpath(module))
                else:
                    is_activated = False            
                module_list[os.path.realpath(module)] = is_activated
        return module_list

    def populate_remaining_modules(self):
        module_list = {}
        for backing_file in self.backing_files:
            module_list[backing_file] = True
        return module_list


class MainWindow(Gtk.Window):
    portdir_env_dir = os.environ["PORTDIR"]
    bootdev_env_dir = os.environ["BOOTDEV"]
    extra_mod_paths = {}
    port_modules = PortModules()
    theme = Gtk.IconTheme.get_default()
    activated_icon = Gtk.IconTheme.load_icon(theme, "gtk-yes", 24, Gtk.IconLookupFlags.USE_BUILTIN)
    not_activated_icon = Gtk.IconTheme.load_icon(theme, "gtk-no", 24, Gtk.IconLookupFlags.USE_BUILTIN)
    #add_icon = Gtk.IconTheme.load_icon(theme, "gtk-add", 24, Gtk.IconLookupFlags.USE_BUILTIN)
    update_enabled = shutil.which('upgrade-mods')

    def __init__(self):

        Gtk.Window.__init__(self, title = "Porteus Modules", border_width = 5, height_request = 550, width_request = 500, icon_name = "cdr")

        self.vb = Gtk.Box(spacing = 5, orientation = Gtk.Orientation.VERTICAL)
        self.vb.set_homogeneous(False)

        self.hb_top = Gtk.Box(spacing = 5)
        self.hb_top.set_homogeneous(False)
        self.l_num_mods_txt = Gtk.Label(label = "Modules Activated: ")
        self.l_num_mods_txt.set_justify(Gtk.Justification.RIGHT)
        self.l_num_mods = Gtk.Label(label = "")
        self.l_num_mods.set_justify(Gtk.Justification.LEFT)
        self.spinner = Gtk.Spinner()
        self.hb_top.pack_start(self.l_num_mods_txt, False, False, 0)
        self.hb_top.pack_start(self.l_num_mods, False, False, 0)
        self.hb_top.pack_start(self.spinner, False, False, 0)
        self.add_button = Gtk.Button.new_from_icon_name("folder-new", Gtk.IconSize.BUTTON)
        #self.add_button.set_relief(Gtk.ReliefStyle.NONE)
        self.add_button.connect("clicked", self.on_add_button_clicked)
        self.hb_top.pack_end(self.add_button, False, False, 0)
        self.mod_add_button = Gtk.Button.new_from_icon_name("cdr", Gtk.IconSize.BUTTON)
        #self.mod_add_button.set_relief(Gtk.ReliefStyle.NONE)
        self.mod_add_button.connect("clicked", self.on_mod_add_button_clicked)
        self.hb_top.pack_end(self.mod_add_button, False, False, 0)
        self.refresh_button = Gtk.Button.new_from_icon_name("view-refresh", Gtk.IconSize.BUTTON)
        self.refresh_button.connect("clicked", self.on_refresh_button_clicked)
        self.hb_top.pack_end(self.refresh_button, False, False, 0)


        self.vb.pack_start(self.hb_top, False, False, 5)

        self.ts_modules_model = Gtk.TreeStore(GdkPixbuf.Pixbuf, str, bool, str)

        self.tree_view = Gtk.TreeView(model = self.ts_modules_model)
        self.setup_tree_view()
        self.tree_view.connect("row-activated", self.on_row_activated)

        self.right_click_menu = Gtk.Menu()
        self.tree_view.connect("button-press-event", self.on_tree_button_press)


        self.scrolled_win = Gtk.ScrolledWindow()
        self.scrolled_win.set_policy(Gtk.PolicyType.AUTOMATIC, Gtk.PolicyType.AUTOMATIC)
        self.scrolled_win.add(self.tree_view)
        self.vb.pack_start(self.scrolled_win, True, True, 0)


        self.hb_bottom = Gtk.Box(spacing = 5)
        self.hb_bottom.set_homogeneous(False)
        self.l_mod_path = Gtk.Label(label = "")
        self.hb_bottom.pack_start(self.l_mod_path, False, False, 6)

        self.vb.pack_start(self.hb_bottom, False, False, 6)
        self.add(self.vb)

        self.select = self.tree_view.get_selection()
        self.select.connect("changed", self.on_tree_selection_changed)

        self.gio_file = Gio.File.new_for_path("/mnt/live/memory/images")
        self.monitor = self.gio_file.monitor_directory(Gio.FileMonitorFlags.NONE, None)
        self.monitor.connect("changed", self.on_mods_dir_changed)

        if self.bootdev_env_dir.startswith("/mnt/isoloop"):
            self.bootdev_env_dir = "/mnt/live" + self.portdir_env_dir    

        self.set_modules_model()

    def msg_dialog(self, msg):
        dialog = Gtk.MessageDialog(self, 0, Gtk.MessageType.INFO,
        Gtk.ButtonsType.OK, msg)
        dialog.run()
        dialog.destroy()
    
    def on_refresh_button_clicked(self, button):
        self.set_modules_model()

    def on_add_button_clicked(self, button):
        dialog = Gtk.FileChooserDialog(title = "Choose a modules directory", parent = self,
            action = Gtk.FileChooserAction.SELECT_FOLDER)
        dialog.add_buttons(Gtk.STOCK_CANCEL, Gtk.ResponseType.CANCEL,
             "Select", Gtk.ResponseType.OK)
        dialog.set_default_size(800, 400)

        response = dialog.run()
        if Gtk.ResponseType.OK == response:
            self.add_custom_modules_path(dialog.get_filename())

        dialog.destroy()

    def on_mod_add_button_clicked(self, button):
        dialog = Gtk.FileChooserDialog(title = "Choose a module to activate/deactivate", parent = self,
            action = Gtk.FileChooserAction.OPEN)
        dialog.add_buttons(Gtk.STOCK_CANCEL, Gtk.ResponseType.CANCEL,
             "Select", Gtk.ResponseType.OK)
        dialog.set_default_size(800, 400)

        response = dialog.run()
        if Gtk.ResponseType.OK == response:
            run(["/usr/bin/activate", dialog.get_filename()])

        dialog.destroy()

    def add_custom_modules_path(self, dir_path):
        if os.path.exists(dir_path):
            if dir_path.startswith(self.portdir_env_dir + "/base") or \
               dir_path.startswith(self.portdir_env_dir + "/modules") or \
               dir_path.startswith(self.portdir_env_dir + "/optional"):
                self.msg_dialog("Porteus directories already added")
                return 

            for extra_mod_path in self.extra_mod_paths:
                if dir_path == self.extra_mod_paths[extra_mod_path]: 
                    self.msg_dialog("Directory already added as part of extramod= cheatcode")
                    return


        if os.path.exists(os.environ["HOME"] + "/.config/lsmodules"):
            with open(os.environ["HOME"] + "/.config/lsmodules", mode = "r", encoding = "utf-8") as fd: 
                for fline in fd:
                    if fline.startswith("#"):
                        fline = fline[1:]
                    fline = fline.lstrip()
                    if fline.startswith("$PORTDIR"):
                        fline = fline.replace("$PORTDIR", self.portdir_env_dir) 
                    if fline.startswith("$BOOTDEV"):
                        fline = fline.replace("$BOOTDEV", self.bootdev_env_dir)
                    if dir_path == fline[:-1]:
                        self.msg_dialog("Directory already added")
                        return
            with open(os.environ["HOME"] + "/.config/lsmodules", mode = "a", encoding = "utf-8") as fd:
                fd.write(dir_path + "\n")                                
        else:
            with open(os.environ["HOME"] + "/.config/lsmodules", mode = "a+", encoding = "utf-8") as fd:
                fd.write(dir_path + "\n")
        self.set_modules_model()                        
        
            
    def on_tree_selection_changed(self, selection):
            model, iter = selection.get_selected()
            if iter is not None:
                if model[iter][0] is not None:
                    self.l_mod_path.set_markup("<i>" + model[iter][3] + "/" + model[iter][1] + "</i>")
                else:
                    self.l_mod_path.set_text("")                    

    def on_row_activated(self, tree_view, path, column):
        self.spinner.start()
        model = tree_view.get_model()
        iter = model.get_iter(path)
        row = model.get(iter, 0, 1, 2, 3)
        if row[0] is not None:
            if row[3] not in self.portdir_env_dir + "/base/" and row[3] not in "/mnt/live/memory/copy2ram":
                if row[2]:
                    cmd_string = "deactivate"
                else:
                    cmd_string = "activate"
                mod_path = row[3] + "/" + row[1]
                GLib.timeout_add(100, self.run_module_activator, cmd_string, mod_path)
            else:
                self.msg_dialog("Base Modules should not be Activated/Deactivated")
                self.spinner.stop()

    def run_module_activator(self, cmd_string, mod_path):
        run([cmd_string, mod_path])

    def on_tree_button_press(self, treeview, event):
        clicked_path, clicked_column, cell_x, cell_y = treeview.get_path_at_pos(event.x, event.y)
        if event.type == Gdk.EventType.BUTTON_PRESS and event.button == 3 and clicked_path and self.update_enabled:
            clicked_data = treeview.get_model()[clicked_path]
            clicked_mod_path = f"{clicked_data[3]}/{clicked_data[1]}"
            self.build_and_show_right_click_menu(clicked_mod_path)

    def build_and_show_right_click_menu(self, mod_path):
        menu = self.right_click_menu
        if menu.is_visible:
            menu.hide()
        for mi in menu.get_children():
            menu.remove(mi)
        update_item = Gtk.MenuItem(label="Update")
        update_item.connect("button-press-event", self.do_update, mod_path)
        menu.append(update_item)
        menu.show_all()
        menu.popup(None, None, None, None, 0, Gtk.get_current_event_time())

    def do_update(self, menu_item, event, mod_path):
        if self.update_enabled:
            term_window = TermWindow([self.update_enabled, "-Uv", mod_path])
            # term_window = TermWindow(["echo", "hi"])
            term_window.show_all()


    def cell_data_func(self, column, renderer, model, iter, data):
        row = model.get(iter, 0, 1)
        if row[0] is None:
            markup_txt = "<b>" + row[1] + "</b>"
            renderer.set_property("markup", markup_txt)

    def setup_tree_view(self):
        column = Gtk.TreeViewColumn("Module Name")
        renderer_pixbuf = Gtk.CellRendererPixbuf()
        renderer_text = Gtk.CellRendererText()
        column.pack_start(renderer_pixbuf, False)
        column.pack_start(renderer_text, False)

        column.add_attribute(renderer_pixbuf, "pixbuf", 0)
        column.add_attribute(renderer_text, "text", 1)
        column.set_cell_data_func(renderer_text, self.cell_data_func, None)
        # column.set_sort_column_id(1)

        self.tree_view.append_column(column)

    def insert_modules_in_model(self, module_list, iter):
        for module in module_list:
            (dirname, filename) = os.path.split(module)     
            if True == module_list[module]:
                icon = self.activated_icon
            else:
                icon = self.not_activated_icon 
            self.ts_modules_model.append(iter, [icon, filename, module_list[module], dirname])

    def on_mods_dir_changed(self, monitor, file1, file2, event_type):
        if event_type == Gio.FileMonitorEvent.CREATED or event_type == Gio.FileMonitorEvent.DELETED:
            GLib.timeout_add_seconds(1, self.set_modules_model)

    def set_modules_model(self):
        self.ts_modules_model.clear()
        self.extra_mod_paths.clear()
        num_modules = 0
        num_activated_modules = self.port_modules.set_backing_files()
        self.extra_mod_paths = self.port_modules.set_extra_mod_paths()

        if self.port_modules.is_copy_2_ram:
            iter = self.ts_modules_model.append(None, [None, "Copied to RAM", None, None])
            module_list = self.port_modules.populate_modules("/mnt/live/memory/copy2ram/")
            num_modules += len(module_list)
            self.insert_modules_in_model(module_list, iter)

        elif os.path.exists(self.portdir_env_dir):
            iter = self.ts_modules_model.append(None, [None, "Base", None, None])
            module_list = self.port_modules.populate_modules(self.portdir_env_dir + "/base/")
            num_modules += len(module_list)
            self.insert_modules_in_model(module_list, iter)

            iter = self.ts_modules_model.append(None, [None, "Modules", None, None])
            module_list = self.port_modules.populate_modules(self.portdir_env_dir + "/modules/")
            num_modules += len(module_list)
            self.insert_modules_in_model(module_list, iter)

            if os.path.exists(self.portdir_env_dir + "/optional/"):
                iter = self.ts_modules_model.append(None, [None, "Optional", None, None])
                module_list = self.port_modules.populate_modules(self.portdir_env_dir + "/optional/")
                num_modules += len(module_list)
                sub_dirs = next(os.walk(self.portdir_env_dir + "/optional/"))[1]
                self.insert_modules_in_model(module_list, iter)
                if sub_dirs:
                    for sub_dir in sub_dirs:
                        module_list = self.port_modules.populate_modules(self.portdir_env_dir + "/optional/" + sub_dir + "/")
                        num_modules += len(module_list)
                        self.insert_modules_in_model(module_list, iter) 
        
        ''' Extramod Dirs '''
        for extra_mod_path in self.extra_mod_paths:
            # print(extra_mod_path, self.extra_mod_paths[extra_mod_path])
            iter = self.ts_modules_model.append(None, [None, "Extramod Dir (" + extra_mod_path + ")", None, None])
            module_list = self.port_modules.populate_modules(self.extra_mod_paths[extra_mod_path] + "/")
            num_modules += len(module_list)
            self.insert_modules_in_model(module_list, iter)
    
        ''' User defined Dirs '''
        if os.path.exists(os.environ["HOME"] + "/.config/lsmodules"):
            with open(os.environ["HOME"] + "/.config/lsmodules", mode = "r", encoding = "utf-8") as fd: 
                for fline in fd:
                    if fline.startswith("#"):
                        continue
                    fline = fline.lstrip()
                    fline = fline[:-1]
                    if fline.startswith("$PORTDIR"):
                        fline = fline.replace("$PORTDIR", self.portdir_env_dir) 
                    elif fline.startswith("$BOOTDEV"):
                        fline = fline.replace("$BOOTDEV", self.bootdev_env_dir)
                    if os.path.exists(fline):                    
                        iter = self.ts_modules_model.append(None, [None, "Custom Dir (" + fline + ")", None, None])
                        module_list = self.port_modules.populate_modules(fline + "/")
                        num_modules += len(module_list)
                        self.insert_modules_in_model(module_list, iter)

        ''' Remaining Modules '''                    
        module_list = self.port_modules.populate_remaining_modules()
        if module_list:
            iter = self.ts_modules_model.append(None, [None, "Other", None, None])
            self.insert_modules_in_model(module_list, iter)
            num_modules += len(module_list)

        self.l_num_mods.set_text(str(num_activated_modules) + "/" + str(num_modules))
        self.tree_view.expand_all()

        selection = self.tree_view.get_selection()        
        path = Gtk.TreePath([0, 0])
        selection.select_path(path)
        self.spinner.stop()

win = MainWindow()
win.connect("delete-event", Gtk.main_quit)
win.connect("destroy", Gtk.main_quit) 
win.show_all()
Gtk.main()

#widget = Gtk.TreeViewColumn()
#print(dir(widget.props))
Doing this has made me notice that upgrade-mods doesn't send enough status messages by default, so you can be left wondering whether the process has finished yet in the terminal window. I can fix that another day.... (Edit: added the verbose flag to the command)
Last edited by pterid on 06 May 2026, 23:09, edited 1 time in total.

pterid
Contributor
Contributor
Posts: 110
Joined: 01 Feb 2025, 20:13
Distribution: Porteus 5.01 Xfce on ext4 USB

lsmodules tweaks

Post#3 by pterid » 04 May 2026, 23:24

One more feature before bed: move modules around the folders. This feature should be available regardless of whether you have upgrade-mods installed.

1. Right-click a module and choose Move...

Image

2. In the modal, choose which folder to move it to. (You can't choose an arbitrary folder; add the folder as a custom module folder first.) Click Move.

Image

3. After the modal disappears, click the new refresh button. (I haven't automated the refresh yet.) The module will be visible in its new location.

Image

The code has some basic sanity checks (won't move an activated module; won't move if a file already exists at the new path) but testing would be very welcome.

Code: Select all

#!/usr/bin/python3

## Porteus Module Activation Status
## Author: jssouza

import os
import shutil
import glob
import gi
gi.require_version('Gtk', '3.0')
gi.require_version('Vte', '2.91')
from gi.repository import Gtk, Vte, Gdk, GdkPixbuf, Gio, GLib
from subprocess import run

import signal
signal.signal(signal.SIGINT, signal.SIG_DFL)


class TermWindow(Gtk.Window):

    def __init__(self, command_args):
        Gtk.Window.__init__(self, title = "Update Module", border_width = 5, height_request = 500, width_request = 550, icon_name = "system-settings")

        self.vb = Gtk.Box(spacing = 5, orientation = Gtk.Orientation.VERTICAL)
        self.vb.set_homogeneous(False)

        terminal = Vte.Terminal()
        pty = Vte.Pty.new_sync(Vte.PtyFlags.DEFAULT)
        terminal.set_pty(pty)
        pty.spawn_async(None, command_args, None, GLib.SpawnFlags.DO_NOT_REAP_CHILD, None, None, -1, None, self.ready)

        self.connect("delete-event", self.on_window_close)

        scrolledwindow = Gtk.ScrolledWindow()
        scrolledwindow.add(terminal)

        self.vb.pack_start(scrolledwindow, True, True, 0)

        self.hb_bottom = Gtk.Box(spacing = 5)
        self.hb_bottom.set_homogeneous(False)
        self.ok_button = Gtk.Button.new_with_label("Quit")
        self.ok_button.connect("clicked", self.on_quit_button_clicked)
        self.hb_bottom.pack_end(self.ok_button, False, False, 6)

        self.vb.pack_start(self.hb_bottom, False, False, 6)
        self.add(self.vb)

    def ready(self, pty, task):
        # print('ready')
        None

    def on_window_close(self):
        self.destroy()

    def on_quit_button_clicked(self, button):
        self.destroy()


class PortModules:
    '''Port Modules Class'''

    backing_files = []
    extramod_dirs = []
    extramod_paths = {}
    is_copy_2_ram = False	

    def __init__(self):
        self.set_extra_mods()
        self.check_copy2ram()

    def check_copy2ram(self):
        with open('/etc/bootcmd.cfg', encoding = 'utf-8') as fd:
            self.is_copy_2_ram = "copy2ram" in fd.read()

    def set_extra_mods(self):
        with open('/etc/bootcmd.cfg', encoding = 'utf-8') as fd:
            for fline in fd:
                if fline.startswith('extramod='):
                    fline = fline[9:-1]
                    self.extramod_dirs = fline.split(';')
                    i = len(self.extramod_dirs)
                    while i > 0:
                        if self.extramod_dirs[i - 1].startswith("UUID") or self.extramod_dirs[i - 1].startswith("LABEL"):
                            index = self.extramod_dirs[i - 1].find("/")
                            self.extramod_dirs[i - 1] = self.extramod_dirs[i - 1][index + 1:]
                        i -= 1
                    # print(self.extramod_dirs)

    def set_extra_mod_paths(self):
        self.extramod_paths.clear()
        for extramod_dir in self.extramod_dirs:
            for backing_file in self.backing_files:
                if extramod_dir + "/" in backing_file:
                    index = backing_file.rfind("/")
                    self.extramod_paths[extramod_dir] = backing_file[:index]
                    break
        return self.extramod_paths

    def set_backing_files(self):
        del self.backing_files[:]
        os.chdir('/sys/block/')
        loop_devices = glob.glob('loop*')
        for loop_device in loop_devices:
            if os.path.exists('/sys/block/' + loop_device + '/loop/'):
                with open('/sys/block/' + loop_device + '/loop/backing_file', encoding = 'utf-8') as fd:
                    fline = fd.read()
                    if fline[:-1].endswith(".xzm"):
                        self.backing_files.append(fline[:-1])
        return len(self.backing_files)
        # print(self.backing_files)

    def populate_modules(self, path):
        module_list = {}
        if os.path.exists(path):     
            os.chdir(path)
            modules = glob.glob('*.xzm')
            modules.sort()
            for module in modules:
                if os.path.realpath(module) in self.backing_files:
                    is_activated = True
                    self.backing_files.remove(os.path.realpath(module))
                else:
                    is_activated = False            
                module_list[os.path.realpath(module)] = is_activated
        return module_list

    def populate_remaining_modules(self):
        module_list = {}
        for backing_file in self.backing_files:
            module_list[backing_file] = True
        return module_list


class MoveWindow(Gtk.Window):
    '''dialog for moving modules between different folders'''

    def __init__(self, parent: Gtk.Window, folder_list: list[str], mod_path: str):

        Gtk.Window.__init__(self, title = "Move Module", border_width = 10, height_request = 300, width_request = 350, icon_name = "folder-move")

        self.parent = parent
        self.mod_path = mod_path
        self.mod_name = os.path.basename(mod_path)

        self.connect("delete-event", self.on_window_close)

        self.vb = Gtk.Box(spacing = 10, orientation = Gtk.Orientation.VERTICAL)
        self.vb.set_homogeneous(False)

        self.hb1 = Gtk.Box(spacing = 5)
        self.l_intro = Gtk.Label(label = "Select a new folder for the module")
        self.l_intro.set_justify(Gtk.Justification.LEFT)
        self.hb1.pack_start(self.l_intro, False, False, 0)
        self.vb.pack_start(self.hb1, False, False, 0)

        self.hb2 = Gtk.Box(spacing = 5)
        self.l_mod_name = Gtk.Label()
        self.l_mod_name.set_markup(f"<b>{self.mod_name}</b>")
        self.l_mod_name.set_justify(Gtk.Justification.CENTER)
        self.hb2.pack_start(self.l_mod_name, True, True, 0)
        self.vb.pack_start(self.hb2, False, False, 0)

        self.folder_store = Gtk.ListStore(str)
        for fp in folder_list:
            self.folder_store.append([fp])
        self.folder_view = Gtk.TreeView(model = self.folder_store)
        column = Gtk.TreeViewColumn("Folders")
        folder_renderer_text = Gtk.CellRendererText()
        column.pack_start(folder_renderer_text, False)
        column.add_attribute(folder_renderer_text, "text", 0)
        self.folder_view.append_column(column)

        self.vb.pack_start(self.folder_view, False, False, 0)

        self.hb4 = Gtk.Box(spacing = 5)
        self.cancel_button = Gtk.Button.new_with_label("Cancel")
        self.cancel_button.connect("clicked", self.on_window_close)
        self.hb4.pack_start(self.cancel_button, True, False, 0)
        self.move_button = Gtk.Button.new_with_label("Move")
        self.move_button.connect("clicked", self.move_module_file)
        self.hb4.pack_end(self.move_button, True, False, 0)

        self.vb.pack_end(self.hb4, False, False, 0)

        self.add(self.vb)

    def on_window_close(self, _arg):
        self.destroy()

    def move_module_file(self, _arg):
        model, treeiter = self.folder_view.get_selection().get_selected()
        new_folder = model[treeiter][0]

        new_path = os.path.join(new_folder, self.mod_name)
        if os.path.exists(self.mod_path) and not os.path.exists(new_path):
            os.rename(self.mod_path, new_path)
            print(f'Moved {self.mod_path} to {new_path}')
        else:
            self.parent.msg_dialog("Destination file already exists!")
            print(f'Not OK to move {self.mod_path} to {new_path}')

        self.on_window_close(None)

class MainWindow(Gtk.Window):
    portdir_env_dir = os.environ["PORTDIR"]
    bootdev_env_dir = os.environ["BOOTDEV"]
    extra_mod_paths = {}
    port_modules = PortModules()
    theme = Gtk.IconTheme.get_default()
    activated_icon = Gtk.IconTheme.load_icon(theme, "gtk-yes", 24, Gtk.IconLookupFlags.USE_BUILTIN)
    not_activated_icon = Gtk.IconTheme.load_icon(theme, "gtk-no", 24, Gtk.IconLookupFlags.USE_BUILTIN)
    #add_icon = Gtk.IconTheme.load_icon(theme, "gtk-add", 24, Gtk.IconLookupFlags.USE_BUILTIN)
    update_enabled = shutil.which('upgrade-mods')

    def __init__(self):

        Gtk.Window.__init__(self, title = "Porteus Modules", border_width = 5, height_request = 550, width_request = 500, icon_name = "cdr")

        self.vb = Gtk.Box(spacing = 5, orientation = Gtk.Orientation.VERTICAL)
        self.vb.set_homogeneous(False)

        self.hb_top = Gtk.Box(spacing = 5)
        self.hb_top.set_homogeneous(False)
        self.l_num_mods_txt = Gtk.Label(label = "Modules Activated: ")
        self.l_num_mods_txt.set_justify(Gtk.Justification.RIGHT)
        self.l_num_mods = Gtk.Label(label = "")
        self.l_num_mods.set_justify(Gtk.Justification.LEFT)
        self.spinner = Gtk.Spinner()
        self.hb_top.pack_start(self.l_num_mods_txt, False, False, 0)
        self.hb_top.pack_start(self.l_num_mods, False, False, 0)
        self.hb_top.pack_start(self.spinner, False, False, 0)
        self.add_button = Gtk.Button.new_from_icon_name("folder-new", Gtk.IconSize.BUTTON)
        #self.add_button.set_relief(Gtk.ReliefStyle.NONE)
        self.add_button.connect("clicked", self.on_add_button_clicked)
        self.hb_top.pack_end(self.add_button, False, False, 0)
        self.mod_add_button = Gtk.Button.new_from_icon_name("cdr", Gtk.IconSize.BUTTON)
        #self.mod_add_button.set_relief(Gtk.ReliefStyle.NONE)
        self.mod_add_button.connect("clicked", self.on_mod_add_button_clicked)
        self.hb_top.pack_end(self.mod_add_button, False, False, 0)
        self.refresh_button = Gtk.Button.new_from_icon_name("view-refresh", Gtk.IconSize.BUTTON)
        self.refresh_button.connect("clicked", self.on_refresh_button_clicked)
        self.hb_top.pack_end(self.refresh_button, False, False, 0)


        self.vb.pack_start(self.hb_top, False, False, 5)

        self.ts_modules_model = Gtk.TreeStore(GdkPixbuf.Pixbuf, str, bool, str)

        self.tree_view = Gtk.TreeView(model = self.ts_modules_model)
        self.setup_tree_view()
        self.tree_view.connect("row-activated", self.on_row_activated)

        self.right_click_menu = Gtk.Menu()
        self.tree_view.connect("button-press-event", self.on_tree_button_press)


        self.scrolled_win = Gtk.ScrolledWindow()
        self.scrolled_win.set_policy(Gtk.PolicyType.AUTOMATIC, Gtk.PolicyType.AUTOMATIC)
        self.scrolled_win.add(self.tree_view)
        self.vb.pack_start(self.scrolled_win, True, True, 0)


        self.hb_bottom = Gtk.Box(spacing = 5)
        self.hb_bottom.set_homogeneous(False)
        self.l_mod_path = Gtk.Label(label = "")
        self.hb_bottom.pack_start(self.l_mod_path, False, False, 6)

        self.vb.pack_start(self.hb_bottom, False, False, 6)
        self.add(self.vb)

        self.select = self.tree_view.get_selection()
        self.select.connect("changed", self.on_tree_selection_changed)

        self.gio_file = Gio.File.new_for_path("/mnt/live/memory/images")
        self.monitor = self.gio_file.monitor_directory(Gio.FileMonitorFlags.NONE, None)
        self.monitor.connect("changed", self.on_mods_dir_changed)

        if self.bootdev_env_dir.startswith("/mnt/isoloop"):
            self.bootdev_env_dir = "/mnt/live" + self.portdir_env_dir    

        self.set_modules_model()

    def msg_dialog(self, msg):
        dialog = Gtk.MessageDialog(self, 0, Gtk.MessageType.INFO,
        Gtk.ButtonsType.OK, msg)
        dialog.run()
        dialog.destroy()
    
    def on_refresh_button_clicked(self, button):
        self.set_modules_model()

    def on_add_button_clicked(self, button):
        dialog = Gtk.FileChooserDialog(title = "Choose a modules directory", parent = self,
            action = Gtk.FileChooserAction.SELECT_FOLDER)
        dialog.add_buttons(Gtk.STOCK_CANCEL, Gtk.ResponseType.CANCEL,
             "Select", Gtk.ResponseType.OK)
        dialog.set_default_size(800, 400)

        response = dialog.run()
        if Gtk.ResponseType.OK == response:
            self.add_custom_modules_path(dialog.get_filename())

        dialog.destroy()

    def on_mod_add_button_clicked(self, button):
        dialog = Gtk.FileChooserDialog(title = "Choose a module to activate/deactivate", parent = self,
            action = Gtk.FileChooserAction.OPEN)
        dialog.add_buttons(Gtk.STOCK_CANCEL, Gtk.ResponseType.CANCEL,
             "Select", Gtk.ResponseType.OK)
        dialog.set_default_size(800, 400)

        response = dialog.run()
        if Gtk.ResponseType.OK == response:
            run(["/usr/bin/activate", dialog.get_filename()])

        dialog.destroy()

    def add_custom_modules_path(self, dir_path):
        if os.path.exists(dir_path):
            if dir_path.startswith(self.portdir_env_dir + "/base") or \
               dir_path.startswith(self.portdir_env_dir + "/modules") or \
               dir_path.startswith(self.portdir_env_dir + "/optional"):
                self.msg_dialog("Porteus directories already added")
                return 

            for extra_mod_path in self.extra_mod_paths:
                if dir_path == self.extra_mod_paths[extra_mod_path]: 
                    self.msg_dialog("Directory already added as part of extramod= cheatcode")
                    return


        if os.path.exists(os.environ["HOME"] + "/.config/lsmodules"):
            with open(os.environ["HOME"] + "/.config/lsmodules", mode = "r", encoding = "utf-8") as fd: 
                for fline in fd:
                    if fline.startswith("#"):
                        fline = fline[1:]
                    fline = fline.lstrip()
                    if fline.startswith("$PORTDIR"):
                        fline = fline.replace("$PORTDIR", self.portdir_env_dir) 
                    if fline.startswith("$BOOTDEV"):
                        fline = fline.replace("$BOOTDEV", self.bootdev_env_dir)
                    if dir_path == fline[:-1]:
                        self.msg_dialog("Directory already added")
                        return
            with open(os.environ["HOME"] + "/.config/lsmodules", mode = "a", encoding = "utf-8") as fd:
                fd.write(dir_path + "\n")                                
        else:
            with open(os.environ["HOME"] + "/.config/lsmodules", mode = "a+", encoding = "utf-8") as fd:
                fd.write(dir_path + "\n")
        self.set_modules_model()                        
        
            
    def on_tree_selection_changed(self, selection):
            model, iter = selection.get_selected()
            if iter is not None:
                if model[iter][0] is not None:
                    self.l_mod_path.set_markup("<i>" + model[iter][3] + "/" + model[iter][1] + "</i>")
                else:
                    self.l_mod_path.set_text("")                    

    def on_row_activated(self, tree_view, path, column):
        self.spinner.start()
        model = tree_view.get_model()
        iter = model.get_iter(path)
        row = model.get(iter, 0, 1, 2, 3)
        if row[0] is not None:
            if row[3] not in self.portdir_env_dir + "/base/" and row[3] not in "/mnt/live/memory/copy2ram":
                if row[2]:
                    cmd_string = "deactivate"
                else:
                    cmd_string = "activate"
                mod_path = row[3] + "/" + row[1]
                GLib.timeout_add(100, self.run_module_activator, cmd_string, mod_path)
            else:
                self.msg_dialog("Base Modules should not be Activated/Deactivated")
                self.spinner.stop()

    def run_module_activator(self, cmd_string, mod_path):
        run([cmd_string, mod_path])

    def on_tree_button_press(self, treeview, event):
        clicked_path, clicked_column, cell_x, cell_y = treeview.get_path_at_pos(event.x, event.y)
        if event.type == Gdk.EventType.BUTTON_PRESS and event.button == 3 and clicked_path:
            clicked_data = treeview.get_model()[clicked_path]
            clicked_mod_path = f"{clicked_data[3]}/{clicked_data[1]}"
            is_activated = clicked_data[2]
            self.build_and_show_right_click_menu(clicked_mod_path, is_activated)

    def build_and_show_right_click_menu(self, mod_path, is_activated):
        menu = self.right_click_menu
        if menu.is_visible:
            menu.hide()
        for mi in menu.get_children():
            menu.remove(mi)
        move_item = Gtk.MenuItem(label="Move...")
        move_item.connect("button-press-event", self.do_move, mod_path, is_activated)
        menu.append(move_item)
        if self.update_enabled:
            update_item = Gtk.MenuItem(label="Update...")
            update_item.connect("button-press-event", self.do_update, mod_path)
            menu.append(update_item)
        menu.show_all()
        menu.popup(None, None, None, None, 0, Gtk.get_current_event_time())

    def do_move(self, menu_item, event, mod_path, is_activated):
        if is_activated:
            self.msg_dialog("You cannot move an activated module. Deactivate it first!")
        else:
            # assemble folder list
            # this would be easier if we had done it at init time
            default_folder_names = ['base', 'modules', 'optional']
            folder_set = {self.portdir_env_dir + '/' + fname for fname in default_folder_names if os.path.exists(self.portdir_env_dir + '/' + fname)}
            for emp in self.port_modules.extramod_dirs:
                if os.path.exists(emp):
                    folder_set.add(emp)
            for row in self.ts_modules_model:
                folder_title = row[1]
                if 'Custom Dir (' in folder_title: 
                    folder_title_stripped = folder_title.replace('Custom Dir (', '')
                    folder_title_stripped = folder_title_stripped[:-1]
                    if os.path.exists(folder_title_stripped):
                        folder_set.add(folder_title_stripped)
                if 'Extramod Dir (' in folder_title:
                    folder_title_stripped = folder_title.replace('Extramod Dir (', '')
                    folder_title_stripped = folder_title_stripped[:-1]
                    if os.path.exists(folder_title_stripped):
                        folder_set.add(folder_title_stripped)
            def folder_add_fn(store, treepath, treeiter):
                if store[treeiter][3]:
                    folder_set.add(store[treeiter][3])
            self.ts_modules_model.foreach(folder_add_fn)
            print(folder_set)
            print(mod_path)

            move_window = MoveWindow(self, list(folder_set), mod_path)
            move_window.set_modal(True)
            move_window.show_all()

    def do_update(self, menu_item, event, mod_path):
        if self.update_enabled:
            term_window = TermWindow([self.update_enabled, "-Uv", mod_path])
            # term_window = TermWindow(["echo", "hi"])
            term_window.show_all()


    def cell_data_func(self, column, renderer, model, iter, data):
        row = model.get(iter, 0, 1)
        if row[0] is None:
            markup_txt = "<b>" + row[1] + "</b>"
            renderer.set_property("markup", markup_txt)

    def setup_tree_view(self):
        column = Gtk.TreeViewColumn("Module Name")
        renderer_pixbuf = Gtk.CellRendererPixbuf()
        renderer_text = Gtk.CellRendererText()
        column.pack_start(renderer_pixbuf, False)
        column.pack_start(renderer_text, False)

        column.add_attribute(renderer_pixbuf, "pixbuf", 0)
        column.add_attribute(renderer_text, "text", 1)
        column.set_cell_data_func(renderer_text, self.cell_data_func, None)
        # column.set_sort_column_id(1)

        self.tree_view.append_column(column)

    def insert_modules_in_model(self, module_list, iter):
        for module in module_list:
            (dirname, filename) = os.path.split(module)     
            if True == module_list[module]:
                icon = self.activated_icon
            else:
                icon = self.not_activated_icon 
            self.ts_modules_model.append(iter, [icon, filename, module_list[module], dirname])

    def on_mods_dir_changed(self, monitor, file1, file2, event_type):
        if event_type == Gio.FileMonitorEvent.CREATED or event_type == Gio.FileMonitorEvent.DELETED:
            GLib.timeout_add_seconds(1, self.set_modules_model)

    def set_modules_model(self):
        self.ts_modules_model.clear()
        self.extra_mod_paths.clear()
        num_modules = 0
        num_activated_modules = self.port_modules.set_backing_files()
        self.extra_mod_paths = self.port_modules.set_extra_mod_paths()

        if self.port_modules.is_copy_2_ram:
            iter = self.ts_modules_model.append(None, [None, "Copied to RAM", None, None])
            module_list = self.port_modules.populate_modules("/mnt/live/memory/copy2ram/")
            num_modules += len(module_list)
            self.insert_modules_in_model(module_list, iter)

        elif os.path.exists(self.portdir_env_dir):
            iter = self.ts_modules_model.append(None, [None, "Base", None, None])
            module_list = self.port_modules.populate_modules(self.portdir_env_dir + "/base/")
            num_modules += len(module_list)
            self.insert_modules_in_model(module_list, iter)

            iter = self.ts_modules_model.append(None, [None, "Modules", None, None])
            module_list = self.port_modules.populate_modules(self.portdir_env_dir + "/modules/")
            num_modules += len(module_list)
            self.insert_modules_in_model(module_list, iter)

            if os.path.exists(self.portdir_env_dir + "/optional/"):
                iter = self.ts_modules_model.append(None, [None, "Optional", None, None])
                module_list = self.port_modules.populate_modules(self.portdir_env_dir + "/optional/")
                num_modules += len(module_list)
                sub_dirs = next(os.walk(self.portdir_env_dir + "/optional/"))[1]
                self.insert_modules_in_model(module_list, iter)
                if sub_dirs:
                    for sub_dir in sub_dirs:
                        module_list = self.port_modules.populate_modules(self.portdir_env_dir + "/optional/" + sub_dir + "/")
                        num_modules += len(module_list)
                        self.insert_modules_in_model(module_list, iter) 
        
        ''' Extramod Dirs '''
        for extra_mod_path in self.extra_mod_paths:
            # print(extra_mod_path, self.extra_mod_paths[extra_mod_path])
            iter = self.ts_modules_model.append(None, [None, "Extramod Dir (" + extra_mod_path + ")", None, None])
            module_list = self.port_modules.populate_modules(self.extra_mod_paths[extra_mod_path] + "/")
            num_modules += len(module_list)
            self.insert_modules_in_model(module_list, iter)
    
        ''' User defined Dirs '''
        if os.path.exists(os.environ["HOME"] + "/.config/lsmodules"):
            with open(os.environ["HOME"] + "/.config/lsmodules", mode = "r", encoding = "utf-8") as fd: 
                for fline in fd:
                    if fline.startswith("#"):
                        continue
                    fline = fline.lstrip()
                    fline = fline[:-1]
                    if fline.startswith("$PORTDIR"):
                        fline = fline.replace("$PORTDIR", self.portdir_env_dir) 
                    elif fline.startswith("$BOOTDEV"):
                        fline = fline.replace("$BOOTDEV", self.bootdev_env_dir)
                    if os.path.exists(fline):                    
                        iter = self.ts_modules_model.append(None, [None, "Custom Dir (" + fline + ")", None, None])
                        module_list = self.port_modules.populate_modules(fline + "/")
                        num_modules += len(module_list)
                        self.insert_modules_in_model(module_list, iter)

        ''' Remaining Modules '''                    
        module_list = self.port_modules.populate_remaining_modules()
        if module_list:
            iter = self.ts_modules_model.append(None, [None, "Other", None, None])
            self.insert_modules_in_model(module_list, iter)
            num_modules += len(module_list)

        self.l_num_mods.set_text(str(num_activated_modules) + "/" + str(num_modules))
        self.tree_view.expand_all()

        selection = self.tree_view.get_selection()        
        path = Gtk.TreePath([0, 0])
        selection.select_path(path)
        self.spinner.stop()

win = MainWindow()
win.connect("delete-event", Gtk.main_quit)
win.connect("destroy", Gtk.main_quit) 
win.show_all()
Gtk.main()

#widget = Gtk.TreeViewColumn()
#print(dir(widget.props))

pterid
Contributor
Contributor
Posts: 110
Joined: 01 Feb 2025, 20:13
Distribution: Porteus 5.01 Xfce on ext4 USB

lsmodules tweaks

Post#4 by pterid » 12 May 2026, 21:12

I'm done now. I have bloated lsmodules beyond all reason.

Image
  • The right-click menu now has Activate/Deactivate... and Delete... items.
  • If you have Module Maker in your path, you can launch it with the leftmost of the icon buttons, or the New... right-click menu item.
  • The icon buttons have tooltips.
  • The module list auto-refreshes when you move or delete anything.
It is getting to the point where I have repeated myself a couple of times, and it would benefit from a refactor (e.g. some things would work out nicer if we store a list of module folder paths in the MainWindow class), but everything seems to work in my manual testing so far.

Known issues:
  • The only thing preventing you from moving or deleting base modules is the fact that they are activated. I should probably add a more explicit defense preventing that.

Code: Select all

#!/usr/bin/python3

## Porteus Module Activation Status
## Author: jssouza

import os
import shutil
import glob
import gi
gi.require_version('Gtk', '3.0')
gi.require_version('Vte', '2.91')
from gi.repository import Gtk, Vte, Gdk, GdkPixbuf, Gio, GLib
from subprocess import run

import signal
signal.signal(signal.SIGINT, signal.SIG_DFL)


class TermWindow(Gtk.Window):

    def __init__(self, command_args):
        Gtk.Window.__init__(self, title = "Update Module ", border_width = 5, height_request = 500, width_request = 550, icon_name = "system-settings")

        self.vb = Gtk.Box(spacing = 5, orientation = Gtk.Orientation.VERTICAL)
        self.vb.set_homogeneous(False)

        terminal = Vte.Terminal()
        pty = Vte.Pty.new_sync(Vte.PtyFlags.DEFAULT)
        terminal.set_pty(pty)
        pty.spawn_async(None, command_args, None, GLib.SpawnFlags.DO_NOT_REAP_CHILD, None, None, -1, None, self.ready)

        self.connect("delete-event", self.on_window_close)

        scrolledwindow = Gtk.ScrolledWindow()
        scrolledwindow.add(terminal)

        self.vb.pack_start(scrolledwindow, True, True, 0)

        self.hb_bottom = Gtk.Box(spacing = 5)
        self.hb_bottom.set_homogeneous(False)
        self.ok_button = Gtk.Button.new_with_label("Quit")
        self.ok_button.connect("clicked", self.on_quit_button_clicked)
        self.hb_bottom.pack_end(self.ok_button, False, False, 6)

        self.vb.pack_start(self.hb_bottom, False, False, 6)
        self.add(self.vb)

    def ready(self, pty, task):
        # print('ready')
        None

    def on_window_close(self):
        self.destroy()

    def on_quit_button_clicked(self, button):
        self.destroy()


class PortModules:
    '''Port Modules Class'''

    backing_files = []
    extramod_dirs = []
    extramod_paths = {}
    is_copy_2_ram = False	

    def __init__(self):
        self.set_extra_mods()
        self.check_copy2ram()

    def check_copy2ram(self):
        with open('/etc/bootcmd.cfg', encoding = 'utf-8') as fd:
            self.is_copy_2_ram = "copy2ram" in fd.read()

    def set_extra_mods(self):
        with open('/etc/bootcmd.cfg', encoding = 'utf-8') as fd:
            for fline in fd:
                if fline.startswith('extramod='):
                    fline = fline[9:-1]
                    self.extramod_dirs = fline.split(';')
                    i = len(self.extramod_dirs)
                    while i > 0:
                        if self.extramod_dirs[i - 1].startswith("UUID") or self.extramod_dirs[i - 1].startswith("LABEL"):
                            index = self.extramod_dirs[i - 1].find("/")
                            self.extramod_dirs[i - 1] = self.extramod_dirs[i - 1][index + 1:]
                        i -= 1
                    # print(self.extramod_dirs)

    def set_extra_mod_paths(self):
        self.extramod_paths.clear()
        for extramod_dir in self.extramod_dirs:
            for backing_file in self.backing_files:
                if extramod_dir + "/" in backing_file:
                    index = backing_file.rfind("/")
                    self.extramod_paths[extramod_dir] = backing_file[:index]
                    break
        return self.extramod_paths

    def set_backing_files(self):
        del self.backing_files[:]
        os.chdir('/sys/block/')
        loop_devices = glob.glob('loop*')
        for loop_device in loop_devices:
            if os.path.exists('/sys/block/' + loop_device + '/loop/'):
                with open('/sys/block/' + loop_device + '/loop/backing_file', encoding = 'utf-8') as fd:
                    fline = fd.read()
                    if fline[:-1].endswith(".xzm"):
                        self.backing_files.append(fline[:-1])
        return len(self.backing_files)
        # print(self.backing_files)

    def populate_modules(self, path):
        module_list = {}
        if os.path.exists(path):     
            os.chdir(path)
            modules = glob.glob('*.xzm')
            modules.sort()
            for module in modules:
                if os.path.realpath(module) in self.backing_files:
                    is_activated = True
                    self.backing_files.remove(os.path.realpath(module))
                else:
                    is_activated = False            
                module_list[os.path.realpath(module)] = is_activated
        return module_list

    def populate_remaining_modules(self):
        module_list = {}
        for backing_file in self.backing_files:
            module_list[backing_file] = True
        return module_list


class MoveWindow(Gtk.Window):
    '''dialog for moving modules between different folders'''

    def __init__(self, parent: Gtk.Window, folder_list: list[str], mod_path: str):

        Gtk.Window.__init__(self, title = "Move Module", border_width = 10, height_request = 300, width_request = 350, icon_name = "folder-move")

        self.parent = parent
        self.mod_path = mod_path
        self.mod_name = os.path.basename(mod_path)

        self.connect("delete-event", self.on_window_close)

        self.vb = Gtk.Box(spacing = 10, orientation = Gtk.Orientation.VERTICAL)
        self.vb.set_homogeneous(False)

        self.hb1 = Gtk.Box(spacing = 5)
        self.l_intro = Gtk.Label(label = "Select a new folder for the module")
        self.l_intro.set_justify(Gtk.Justification.LEFT)
        self.hb1.pack_start(self.l_intro, False, False, 0)
        self.vb.pack_start(self.hb1, False, False, 0)

        self.hb2 = Gtk.Box(spacing = 5)
        self.l_mod_name = Gtk.Label()
        self.l_mod_name.set_markup(f"<b>{self.mod_name}</b>")
        self.l_mod_name.set_justify(Gtk.Justification.CENTER)
        self.hb2.pack_start(self.l_mod_name, True, True, 0)
        self.vb.pack_start(self.hb2, False, False, 0)

        self.folder_store = Gtk.ListStore(str)
        for fp in folder_list:
            self.folder_store.append([fp])
        self.folder_view = Gtk.TreeView(model = self.folder_store)
        column = Gtk.TreeViewColumn("Folders")
        folder_renderer_text = Gtk.CellRendererText()
        column.pack_start(folder_renderer_text, False)
        column.add_attribute(folder_renderer_text, "text", 0)
        self.folder_view.append_column(column)

        self.vb.pack_start(self.folder_view, False, False, 0)

        self.hb4 = Gtk.Box(spacing = 5)
        self.cancel_button = Gtk.Button.new_with_label("Cancel")
        self.cancel_button.connect("clicked", self.on_window_close, None)
        self.hb4.pack_start(self.cancel_button, True, False, 0)
        self.move_button = Gtk.Button.new_with_label("Move")
        self.move_button.connect("clicked", self.move_module_file)
        self.hb4.pack_end(self.move_button, True, False, 0)

        self.vb.pack_end(self.hb4, False, False, 0)

        self.add(self.vb)

    def on_window_close(self, _arg1, _arg2):
        self.destroy()
        self.parent.set_modules_model()

    def move_module_file(self, _arg):
        model, treeiter = self.folder_view.get_selection().get_selected()
        new_folder = model[treeiter][0]

        new_path = os.path.join(new_folder, self.mod_name)
        if os.path.exists(self.mod_path) and not os.path.exists(new_path):
            try:
                shutil.move(self.mod_path, new_path)
                print(f'Moved {self.mod_path} to {new_path}')
            except OSError:
                self.parent.msg_dialog("Operating system error: could not move the module.")
        else:
            self.parent.msg_dialog("Destination file already exists!")
            print(f'Not OK to move {self.mod_path} to {new_path}')

        self.on_window_close(None, None)

class MainWindow(Gtk.Window):
    portdir_env_dir = os.environ["PORTDIR"]
    bootdev_env_dir = os.environ["BOOTDEV"]
    extra_mod_paths = {}
    port_modules = PortModules()
    theme = Gtk.IconTheme.get_default()
    activated_icon = Gtk.IconTheme.load_icon(theme, "gtk-yes", 24, Gtk.IconLookupFlags.USE_BUILTIN)
    not_activated_icon = Gtk.IconTheme.load_icon(theme, "gtk-no", 24, Gtk.IconLookupFlags.USE_BUILTIN)
    #add_icon = Gtk.IconTheme.load_icon(theme, "gtk-add", 24, Gtk.IconLookupFlags.USE_BUILTIN)
    new_enabled = shutil.which('gtk-slapt-mod')
    update_enabled = shutil.which('upgrade-mods')

    def __init__(self):

        Gtk.Window.__init__(self, title = "Porteus Modules", border_width = 5, height_request = 550, width_request = 500, icon_name = "cdr")

        self.vb = Gtk.Box(spacing = 5, orientation = Gtk.Orientation.VERTICAL)
        self.vb.set_homogeneous(False)

        self.hb_top = Gtk.Box(spacing = 5)
        self.hb_top.set_homogeneous(False)
        self.l_num_mods_txt = Gtk.Label(label = "Modules Activated: ")
        self.l_num_mods_txt.set_justify(Gtk.Justification.RIGHT)
        self.l_num_mods = Gtk.Label(label = "")
        self.l_num_mods.set_justify(Gtk.Justification.LEFT)
        self.spinner = Gtk.Spinner()
        self.hb_top.pack_start(self.l_num_mods_txt, False, False, 0)
        self.hb_top.pack_start(self.l_num_mods, False, False, 0)
        self.hb_top.pack_start(self.spinner, False, False, 0)
        self.add_button = Gtk.Button.new_from_icon_name("folder-new", Gtk.IconSize.BUTTON)
        self.add_button.set_tooltip_text("New custom module folder")
        #self.add_button.set_relief(Gtk.ReliefStyle.NONE)
        self.add_button.connect("clicked", self.on_add_button_clicked)
        self.hb_top.pack_end(self.add_button, False, False, 0)
        self.mod_add_button = Gtk.Button.new_from_icon_name("cdr", Gtk.IconSize.BUTTON)
        self.mod_add_button.set_tooltip_text("Activate module (browse)")
        #self.mod_add_button.set_relief(Gtk.ReliefStyle.NONE)
        self.mod_add_button.connect("clicked", self.on_mod_add_button_clicked)
        self.hb_top.pack_end(self.mod_add_button, False, False, 0)
        self.refresh_button = Gtk.Button.new_from_icon_name("view-refresh", Gtk.IconSize.BUTTON)
        self.refresh_button.set_tooltip_text("Refresh module list")
        self.refresh_button.connect("clicked", self.on_refresh_button_clicked)
        self.hb_top.pack_end(self.refresh_button, False, False, 0)
        if self.new_enabled:
            self.new_button = Gtk.Button.new_from_icon_name("go-down", Gtk.IconSize.BUTTON)
            self.new_button.set_tooltip_text("Get new modules")
            self.new_button.connect("clicked", self.on_new_button_clicked)
            self.hb_top.pack_end(self.new_button, False, False, 0)

        self.vb.pack_start(self.hb_top, False, False, 5)

        self.ts_modules_model = Gtk.TreeStore(GdkPixbuf.Pixbuf, str, bool, str)

        self.tree_view = Gtk.TreeView(model = self.ts_modules_model)
        self.setup_tree_view()
        self.tree_view.connect("row-activated", self.on_row_activated)

        self.right_click_menu = Gtk.Menu()
        self.tree_view.connect("button-press-event", self.on_tree_button_press)


        self.scrolled_win = Gtk.ScrolledWindow()
        self.scrolled_win.set_policy(Gtk.PolicyType.AUTOMATIC, Gtk.PolicyType.AUTOMATIC)
        self.scrolled_win.add(self.tree_view)
        self.vb.pack_start(self.scrolled_win, True, True, 0)


        self.hb_bottom = Gtk.Box(spacing = 5)
        self.hb_bottom.set_homogeneous(False)
        self.l_mod_path = Gtk.Label(label = "")
        self.hb_bottom.pack_start(self.l_mod_path, False, False, 6)

        self.vb.pack_start(self.hb_bottom, False, False, 6)
        self.add(self.vb)

        self.select = self.tree_view.get_selection()
        self.select.connect("changed", self.on_tree_selection_changed)

        self.gio_file = Gio.File.new_for_path("/mnt/live/memory/images")
        self.monitor = self.gio_file.monitor_directory(Gio.FileMonitorFlags.NONE, None)
        self.monitor.connect("changed", self.on_mods_dir_changed)

        if self.bootdev_env_dir.startswith("/mnt/isoloop"):
            self.bootdev_env_dir = "/mnt/live" + self.portdir_env_dir    

        self.set_modules_model()

    def msg_dialog(self, msg):
        dialog = Gtk.MessageDialog(transient_for=self, 
                                   flags=0,
                                   message_type=Gtk.MessageType.INFO,
                                   buttons=Gtk.ButtonsType.OK,
                                   text=msg)
        dialog.run()
        dialog.destroy()
    
    def on_refresh_button_clicked(self, button):
        self.set_modules_model()

    def on_new_button_clicked(self, button):
        self.do_new(None, None)

    def on_add_button_clicked(self, button):
        dialog = Gtk.FileChooserDialog(title = "Choose a modules directory", parent = self,
            action = Gtk.FileChooserAction.SELECT_FOLDER)
        dialog.add_buttons(Gtk.STOCK_CANCEL, Gtk.ResponseType.CANCEL,
             "Select", Gtk.ResponseType.OK)
        dialog.set_default_size(800, 400)

        response = dialog.run()
        if Gtk.ResponseType.OK == response:
            self.add_custom_modules_path(dialog.get_filename())

        dialog.destroy()

    def on_mod_add_button_clicked(self, button):
        dialog = Gtk.FileChooserDialog(title = "Choose a module to activate/deactivate", parent = self,
            action = Gtk.FileChooserAction.OPEN)
        dialog.add_buttons(Gtk.STOCK_CANCEL, Gtk.ResponseType.CANCEL,
             "Select", Gtk.ResponseType.OK)
        dialog.set_default_size(800, 400)

        response = dialog.run()
        if Gtk.ResponseType.OK == response:
            run(["/usr/bin/activate", dialog.get_filename()])

        dialog.destroy()

    def add_custom_modules_path(self, dir_path):
        if os.path.exists(dir_path):
            if dir_path.startswith(self.portdir_env_dir + "/base") or \
               dir_path.startswith(self.portdir_env_dir + "/modules") or \
               dir_path.startswith(self.portdir_env_dir + "/optional"):
                self.msg_dialog("Porteus directories already added")
                return 

            for extra_mod_path in self.extra_mod_paths:
                if dir_path == self.extra_mod_paths[extra_mod_path]: 
                    self.msg_dialog("Directory already added as part of extramod= cheatcode")
                    return


        if os.path.exists(os.environ["HOME"] + "/.config/lsmodules"):
            with open(os.environ["HOME"] + "/.config/lsmodules", mode = "r", encoding = "utf-8") as fd: 
                for fline in fd:
                    if fline.startswith("#"):
                        fline = fline[1:]
                    fline = fline.lstrip()
                    if fline.startswith("$PORTDIR"):
                        fline = fline.replace("$PORTDIR", self.portdir_env_dir) 
                    if fline.startswith("$BOOTDEV"):
                        fline = fline.replace("$BOOTDEV", self.bootdev_env_dir)
                    if dir_path == fline[:-1]:
                        self.msg_dialog("Directory already added")
                        return
            with open(os.environ["HOME"] + "/.config/lsmodules", mode = "a", encoding = "utf-8") as fd:
                fd.write(dir_path + "\n")                                
        else:
            with open(os.environ["HOME"] + "/.config/lsmodules", mode = "a+", encoding = "utf-8") as fd:
                fd.write(dir_path + "\n")
        self.set_modules_model()                        
        
            
    def on_tree_selection_changed(self, selection):
            model, iter = selection.get_selected()
            if iter is not None:
                if model[iter][0] is not None:
                    self.l_mod_path.set_markup("<i>" + model[iter][3] + "/" + model[iter][1] + "</i>")
                else:
                    self.l_mod_path.set_text("")                    

    def on_row_activated(self, tree_view, path, column):
        self.spinner.start()
        model = tree_view.get_model()
        iter = model.get_iter(path)
        row = model.get(iter, 0, 1, 2, 3)
        if row[0] is not None:
            if row[3] not in self.portdir_env_dir + "/base/" and row[3] not in "/mnt/live/memory/copy2ram":
                if row[2]:
                    cmd_string = "deactivate"
                else:
                    cmd_string = "activate"
                mod_path = row[3] + "/" + row[1]
                GLib.timeout_add(100, self.run_module_activator, cmd_string, mod_path)
            else:
                self.msg_dialog("Base Modules should not be Activated/Deactivated")
                self.spinner.stop()

    def run_module_activator(self, cmd_string, mod_path):
        run([cmd_string, mod_path])

    def on_tree_button_press(self, treeview, event):
        clicked_path, clicked_column, cell_x, cell_y = treeview.get_path_at_pos(event.x, event.y)
        if event.type == Gdk.EventType.BUTTON_PRESS and event.button == 3 and clicked_path:
            clicked_data = treeview.get_model()[clicked_path]
            clicked_mod_path = f"{clicked_data[3]}/{clicked_data[1]}"
            is_activated = clicked_data[2]
            self.build_and_show_right_click_menu(clicked_mod_path, is_activated)

    def build_and_show_right_click_menu(self, mod_path, is_activated):
        menu = self.right_click_menu
        if menu.is_visible:
            menu.hide()
        for mi in menu.get_children():
            menu.remove(mi)
        activation_label = "Deactivate..." if is_activated else "Activate..."
        activation_item = Gtk.MenuItem(label=activation_label)
        activation_item.connect("button-press-event", self.do_activation, mod_path, is_activated)
        menu.append(activation_item)
        if self.new_enabled:
            new_item = Gtk.MenuItem(label="New...")
            new_item.connect("button-press-event", self.do_new)
            menu.append(new_item)
        move_item = Gtk.MenuItem(label="Move...")
        move_item.connect("button-press-event", self.do_move, mod_path, is_activated)
        menu.append(move_item)
        if self.update_enabled:
            update_item = Gtk.MenuItem(label="Update...")
            update_item.connect("button-press-event", self.do_update, mod_path)
            menu.append(update_item)
        delete_item = Gtk.MenuItem(label="Delete...")
        delete_item.connect("button-press-event", self.do_delete, mod_path, is_activated)
        menu.append(delete_item)
        menu.show_all()
        menu.popup(None, None, None, None, 0, Gtk.get_current_event_time())

    def do_activation(self, menu_item, event, mod_path, is_activated):
        self.spinner.start()
        cmd_string = "deactivate" if is_activated else "activate"
        if self.portdir_env_dir + "/base/" in mod_path or "/mnt/live/memory/copy2ram" in mod_path:
            self.msg_dialog("Base Modules should not be Activated/Deactivated")
            self.spinner.stop()
        else:
            GLib.timeout_add(100, self.run_module_activator, cmd_string, mod_path)


    def do_new(self, menu_item, event):
        if self.new_enabled:
            pid, fdin, fdout, fderr = GLib.spawn_async([self.new_enabled])
        else:
            self.msg_dialog("Module Maker program is not installed.")

    def do_move(self, menu_item, event, mod_path, is_activated):
        if is_activated:
            self.msg_dialog("You cannot move an activated module. Deactivate it first!")
        else:
            # assemble folder list
            # this would be easier if we had done it at init time
            default_folder_names = ['base', 'modules', 'optional']
            folder_set = {self.portdir_env_dir + '/' + fname for fname in default_folder_names if os.path.exists(self.portdir_env_dir + '/' + fname)}
            for emp in self.port_modules.extramod_dirs:
                if os.path.exists(emp):
                    folder_set.add(emp)
            for row in self.ts_modules_model:
                folder_title = row[1]
                if 'Custom Dir (' in folder_title: 
                    folder_title_stripped = folder_title.replace('Custom Dir (', '')
                    folder_title_stripped = folder_title_stripped[:-1]
                    if os.path.exists(folder_title_stripped):
                        folder_set.add(folder_title_stripped)
                if 'Extramod Dir (' in folder_title:
                    folder_title_stripped = folder_title.replace('Extramod Dir (', '')
                    folder_title_stripped = folder_title_stripped[:-1]
                    if os.path.exists(folder_title_stripped):
                        folder_set.add(folder_title_stripped)
            def folder_add_fn(store, treepath, treeiter):
                if store[treeiter][3]:
                    folder_set.add(store[treeiter][3])
            self.ts_modules_model.foreach(folder_add_fn)
            print(folder_set)
            print(mod_path)

            move_window = MoveWindow(self, list(folder_set), mod_path)
            move_window.set_modal(True)
            move_window.show_all()

    def do_update(self, menu_item, event, mod_path):
        if self.update_enabled:
            term_window = TermWindow([self.update_enabled, "-Uv", mod_path])
            # term_window = TermWindow(["echo", "hi"])
            term_window.show_all()

    def do_delete(self, menu_item, event, mod_path, is_activated):
        if is_activated:
            self.msg_dialog("You cannot delete an activated module. Deactivate it first!")
        else:
            mod_name = os.path.basename(mod_path)
            confirm_text = f"Do you really want to delete {mod_name}?"
            confirm_delete_dialog = Gtk.MessageDialog(transient_for=self, 
                                       flags=0,
                                       message_type=Gtk.MessageType.WARNING,
                                       text=confirm_text)
            confirm_delete_dialog.format_secondary_text("This cannot be undone.")
            confirm_delete_dialog.add_button(button_text="Cancel",
                                             response_id=Gtk.ResponseType.CANCEL)
            confirm_delete_dialog.add_button(button_text="Delete",
                                             response_id=Gtk.ResponseType.ACCEPT)
            user_response = confirm_delete_dialog.run()
            if user_response == Gtk.ResponseType.ACCEPT and os.path.exists(mod_path):
                try:
                    os.remove(mod_path)
                    self.set_modules_model()
                except OSError:
                    self.parent.msg_dialog("Operating system error: could not delete the module.")
            confirm_delete_dialog.destroy()


    def cell_data_func(self, column, renderer, model, iter, data):
        row = model.get(iter, 0, 1)
        if row[0] is None:
            markup_txt = "<b>" + row[1] + "</b>"
            renderer.set_property("markup", markup_txt)

    def setup_tree_view(self):
        column = Gtk.TreeViewColumn("Module Name")
        renderer_pixbuf = Gtk.CellRendererPixbuf()
        renderer_text = Gtk.CellRendererText()
        column.pack_start(renderer_pixbuf, False)
        column.pack_start(renderer_text, False)

        column.add_attribute(renderer_pixbuf, "pixbuf", 0)
        column.add_attribute(renderer_text, "text", 1)
        column.set_cell_data_func(renderer_text, self.cell_data_func, None)
        # column.set_sort_column_id(1)

        self.tree_view.append_column(column)

    def insert_modules_in_model(self, module_list, iter):
        for module in module_list:
            (dirname, filename) = os.path.split(module)     
            if True == module_list[module]:
                icon = self.activated_icon
            else:
                icon = self.not_activated_icon 
            self.ts_modules_model.append(iter, [icon, filename, module_list[module], dirname])

    def on_mods_dir_changed(self, monitor, file1, file2, event_type):
        if event_type == Gio.FileMonitorEvent.CREATED or event_type == Gio.FileMonitorEvent.DELETED:
            GLib.timeout_add_seconds(1, self.set_modules_model)

    def set_modules_model(self):
        self.ts_modules_model.clear()
        self.extra_mod_paths.clear()
        num_modules = 0
        num_activated_modules = self.port_modules.set_backing_files()
        self.extra_mod_paths = self.port_modules.set_extra_mod_paths()

        if self.port_modules.is_copy_2_ram:
            iter = self.ts_modules_model.append(None, [None, "Copied to RAM", None, None])
            module_list = self.port_modules.populate_modules("/mnt/live/memory/copy2ram/")
            num_modules += len(module_list)
            self.insert_modules_in_model(module_list, iter)

        elif os.path.exists(self.portdir_env_dir):
            iter = self.ts_modules_model.append(None, [None, "Base", None, None])
            module_list = self.port_modules.populate_modules(self.portdir_env_dir + "/base/")
            num_modules += len(module_list)
            self.insert_modules_in_model(module_list, iter)

            iter = self.ts_modules_model.append(None, [None, "Modules", None, None])
            module_list = self.port_modules.populate_modules(self.portdir_env_dir + "/modules/")
            num_modules += len(module_list)
            self.insert_modules_in_model(module_list, iter)

            if os.path.exists(self.portdir_env_dir + "/optional/"):
                iter = self.ts_modules_model.append(None, [None, "Optional", None, None])
                module_list = self.port_modules.populate_modules(self.portdir_env_dir + "/optional/")
                num_modules += len(module_list)
                sub_dirs = next(os.walk(self.portdir_env_dir + "/optional/"))[1]
                self.insert_modules_in_model(module_list, iter)
                if sub_dirs:
                    for sub_dir in sub_dirs:
                        module_list = self.port_modules.populate_modules(self.portdir_env_dir + "/optional/" + sub_dir + "/")
                        num_modules += len(module_list)
                        self.insert_modules_in_model(module_list, iter) 
        
        ''' Extramod Dirs '''
        for extra_mod_path in self.extra_mod_paths:
            # print(extra_mod_path, self.extra_mod_paths[extra_mod_path])
            iter = self.ts_modules_model.append(None, [None, "Extramod Dir (" + extra_mod_path + ")", None, None])
            module_list = self.port_modules.populate_modules(self.extra_mod_paths[extra_mod_path] + "/")
            num_modules += len(module_list)
            self.insert_modules_in_model(module_list, iter)
    
        ''' User defined Dirs '''
        if os.path.exists(os.environ["HOME"] + "/.config/lsmodules"):
            with open(os.environ["HOME"] + "/.config/lsmodules", mode = "r", encoding = "utf-8") as fd: 
                for fline in fd:
                    if fline.startswith("#"):
                        continue
                    fline = fline.lstrip()
                    fline = fline[:-1]
                    if fline.startswith("$PORTDIR"):
                        fline = fline.replace("$PORTDIR", self.portdir_env_dir) 
                    elif fline.startswith("$BOOTDEV"):
                        fline = fline.replace("$BOOTDEV", self.bootdev_env_dir)
                    if os.path.exists(fline):                    
                        iter = self.ts_modules_model.append(None, [None, "Custom Dir (" + fline + ")", None, None])
                        module_list = self.port_modules.populate_modules(fline + "/")
                        num_modules += len(module_list)
                        self.insert_modules_in_model(module_list, iter)

        ''' Remaining Modules '''                    
        module_list = self.port_modules.populate_remaining_modules()
        if module_list:
            iter = self.ts_modules_model.append(None, [None, "Other", None, None])
            self.insert_modules_in_model(module_list, iter)
            num_modules += len(module_list)

        self.l_num_mods.set_text(str(num_activated_modules) + "/" + str(num_modules))
        self.tree_view.expand_all()

        selection = self.tree_view.get_selection()        
        path = Gtk.TreePath([0, 0])
        selection.select_path(path)
        self.spinner.stop()

win = MainWindow()
win.connect("delete-event", Gtk.main_quit)
win.connect("destroy", Gtk.main_quit) 
win.show_all()
Gtk.main()

#widget = Gtk.TreeViewColumn()
#print(dir(widget.props))

rych
Warlord
Warlord
Posts: 874
Joined: 04 Jan 2014, 04:27
Distribution: Porteus 5.0 x64 OpenBox
Location: NZ
Contact:

lsmodules tweaks

Post#5 by rych » 13 May 2026, 09:25

pterid wrote:
12 May 2026, 21:12
The only thing preventing you from moving or deleting base modules is the fact that they are activated.
It's actually OKAY to simply delete/rename the old/previous xzm while it's activated: the current mount is unaffected until reboot.

According to the Linux `unlink()` man page: if the name was the last link to a file but any processes still have the file open, the file will remain in existence until the last file descriptor referring to it is closed.

More precisely, deleting a file via `unlink()` just lowers the link count and removes the directory entry. Closing open file descriptors is what lowers the reference count. The kernel keeps both a link count and a reference count for each active inode; an inode is only marked as unused and the filesystem told to free its space when *both* counts go to zero.

So when you `rm` the .xzm file:

1. **The filename disappears immediately** from the directory. It will no longer show up in `ls`.
2. **The inode's link count drops to 0** — but the loop device driver's open file descriptor holds the reference count above zero.
3. **The inode and all its data blocks remain fully intact** on disk. The kernel simply marks them as "orphaned" — no directory entry points to them, but they are alive.
4. The data persists until the last open handle closes. This is why disk space usage doesn't decrease immediately after deleting large files — the blocks are still occupied.
5. **The loop device `/dev/loopN` continues functioning normally**, squashfs keeps serving files on demand from the loop device, and all your activated module's files remain visible through AUFS as if nothing happened.

**In short: yes, the currently running session is completely unaffected.** The module keeps working. Applications launched from it keep working. Nothing breaks.

pterid
Contributor
Contributor
Posts: 110
Joined: 01 Feb 2025, 20:13
Distribution: Porteus 5.01 Xfce on ext4 USB

lsmodules tweaks

Post#6 by pterid » 13 May 2026, 09:55

Thanks for the feedback!
rych wrote:
13 May 2026, 09:25
It's actually OKAY to simply delete/rename the old/previous xzm while it's activated: the current mount is unaffected until reboot.
We discussed this in the thread about the updater... My opinion is that we shouldn't allow distro tools to do this. It may be technically OK, but it leaves the system in a confusing state that other tools then have to deal with, and I just don't understand why a user would ever need or want to do it. You can easily disable the checks I put in, if you want.

rych
Warlord
Warlord
Posts: 874
Joined: 04 Jan 2014, 04:27
Distribution: Porteus 5.0 x64 OpenBox
Location: NZ
Contact:

lsmodules tweaks

Post#7 by rych » 14 May 2026, 09:57

pterid wrote:
13 May 2026, 09:55
why a user would ever need or want to do it
I've been such a user (porteus user not a user of your tools). This is great for updating modules. When we update a module, the old one can be renamed / "deleted", even if it sometimes cannot be deactivated. So that at restart it'll be physically removed or at least not activated. That wasn't a feedback on your tools, but rather on the alleged system limitation which in reality is not.

pterid
Contributor
Contributor
Posts: 110
Joined: 01 Feb 2025, 20:13
Distribution: Porteus 5.01 Xfce on ext4 USB

lsmodules tweaks

Post#8 by pterid » 14 May 2026, 12:19

rych wrote:
14 May 2026, 09:57
pterid wrote:
13 May 2026, 09:55
why a user would ever need or want to do it
I've been such a user (porteus user not a user of your tools). This is great for updating modules. When we update a module, the old one can be renamed / "deleted", even if it sometimes cannot be deactivated. So that at restart it'll be physically removed or at least not activated. That wasn't a feedback on your tools, but rather on the alleged system limitation which in reality is not.
Oh I see! I was not alleging any system limitation. I know you can delete an active module.

I said "The only thing preventing you from moving or deleting base modules is the fact that they are activated".

I meant "I chose to add a check that prevents you from deleting an activated module. That usually prevents you from deleting base modules, because they're usually activated. It might be better to make an explicit check for base modules to prevent deleting them because they are base modules"

Edit: also thanks for the inspiration - I guess to update an active module, upgrade-mods should:
  • deactivate it
  • build the upgraded version
  • move it into the modules folder, displacing the old one
  • activate the new one

User avatar
dreadbird
Shogun
Shogun
Posts: 264
Joined: 08 Dec 2024, 04:30
Distribution: porteus5

lsmodules tweaks

Post#9 by dreadbird » 23 Jun 2026, 18:49

I like it what about another section that shows activated modules that arent in the modules folder? so if you activated one in /tmp for example it would show that. also show the /base modules in that section. like a misc section. I see that base is in its own section but grouping all that arent in the modules folder? as there usually wouldnt be many modules outside of the modules folder you could only show activated modules. if you deactivate a module it drops from the list. So it could be like a reference point what modules are currently activated? ok these are candidates for updates.

The spinner is good. You could have a label on the right side on a timer that resets it to say idle. then when you perform actions simply change the label updating. no updates found. So key actions and it can also show any errors or returns
the timer to reset to idle every 40 seconds and every time the label is updated to reset the timer

Post Reply