diff --git a/nwg_panel/processes.py b/nwg_panel/processes.py index 972491df..09ffa2f2 100644 --- a/nwg_panel/processes.py +++ b/nwg_panel/processes.py @@ -13,6 +13,7 @@ import os import socket import sys +from enum import Enum import psutil from i3ipc import Connection @@ -27,6 +28,22 @@ his = os.getenv("HYPRLAND_INSTANCE_SIGNATURE") +class SortOrder(Enum): + NONE = 0 + PID = 1 + PPID = 2 + NAME = 3 + USERNAME = 4 + CPU_PERCENT = 5 + MEMORY_PERCENT = 6 + + +sort_order = SortOrder.PID + +# We need to get_allocated_width of each one inside a function later +btn_pid, btn_ppid, btn_owner, btn_cpu, btn_mem, btn_name = None, None, None, None, None, None, + + def hyprctl(cmd): s = socket.socket(socket.AF_UNIX, socket.SOCK_STREAM) s.connect("/tmp/hypr/{}/.socket.sock".format(os.getenv("HYPRLAND_INSTANCE_SIGNATURE"))) @@ -42,13 +59,8 @@ def hyprctl(cmd): eprint("Neither sway nor hyprland socket detected, terminating.") sys.exit(1) -W_PID = 10 -W_PPID = 10 W_OWNER = 10 -W_CPU = 7 -W_MEM = 7 W_NAME = 24 -W_WINDOW = 24 # Fallback icon names dict: win_name -> icon_name aliases = { @@ -80,6 +92,7 @@ def terminate(btn, pid): def list_processes(once=False): + tree = None if swaysock: tree = Connection().get_tree() elif his: @@ -93,6 +106,33 @@ def list_processes(once=False): if proc.info['username'] == os.getenv('USER') or not settings["processes-own-only"]: processes[proc.info['pid']] = proc.info + processes_list = [] + for pid in processes: + item = { + "pid": pid, + "ppid": processes[pid]["ppid"], + "name": processes[pid]["name"], + "username": processes[pid]["username"], + "cpu_percent": processes[pid]["cpu_percent"], + "memory_percent": processes[pid]["memory_percent"] + } + processes_list.append(item) + + if sort_order == SortOrder.PID: + sorted_list = processes_list # they are already sorted by PID, no need to sort + elif sort_order == SortOrder.PPID: + sorted_list = sorted(processes_list, key=lambda d: d['ppid']) + elif sort_order == SortOrder.NAME: + sorted_list = sorted(processes_list, key=lambda d: d['name'].upper()) + elif sort_order == SortOrder.USERNAME: + sorted_list = sorted(processes_list, key=lambda d: d['username'].upper()) + elif sort_order == SortOrder.CPU_PERCENT: + sorted_list = sorted(processes_list, key=lambda d: d['cpu_percent'], reverse=True) + elif sort_order == SortOrder.MEMORY_PERCENT: + sorted_list = sorted(processes_list, key=lambda d: d['memory_percent'], reverse=True) + else: + sorted_list = processes_list + # At first, we need to add grid to the scrolled window (as in former add_with_viewport). # In next iterations, we add the grid directly to already existing viewport, to avoid the scrolled window floating. if scrolled_window and scrolled_window.get_children(): @@ -105,18 +145,18 @@ def list_processes(once=False): grid.destroy() grid = Gtk.Grid.new() - grid.set_row_spacing(3) - grid.set_row_homogeneous(True) + grid.set_column_spacing(3) if viewport: viewport.add(grid) - else: + elif scrolled_window: scrolled_window.add(grid) idx = 1 - for pid in processes: + for item in sorted_list: cons = None mapped = {} + pid = item['pid'] if swaysock: cons = tree.find_by_pid(pid) elif his: @@ -127,12 +167,10 @@ def list_processes(once=False): if not cons or not settings["processes-background-only"]: lbl = Gtk.Label.new(str(pid)) - lbl.set_width_chars(W_PID) lbl.set_xalign(0) grid.attach(lbl, 1, idx, 1, 1) lbl = Gtk.Label.new(str(processes[pid]["ppid"])) - lbl.set_width_chars(W_PPID) lbl.set_xalign(0) grid.attach(lbl, 2, idx, 1, 1) @@ -140,7 +178,6 @@ def list_processes(once=False): if len(owner) > W_OWNER - 1: owner = "{}…".format(owner[:W_OWNER - 2]) lbl = Gtk.Label.new(owner) - lbl.set_width_chars(W_OWNER) lbl.set_xalign(0) grid.attach(lbl, 3, idx, 1, 1) @@ -149,13 +186,16 @@ def list_processes(once=False): lbl = Gtk.Label.new("{}%".format(str(percent))) else: lbl = Gtk.Label() - lbl.set_markup("{}".format(str(percent))) - lbl.set_width_chars(W_CPU) + lbl.set_markup("{}%".format(str(percent))) lbl.set_xalign(0) grid.attach(lbl, 4, idx, 1, 1) - lbl = Gtk.Label.new("{}%".format(str(round(processes[pid]["memory_percent"], 2)))) - lbl.set_width_chars(W_MEM) + percent = processes[pid]["memory_percent"] + if percent < 1: + lbl = Gtk.Label.new("{}%".format(str(round(percent, 2)))) + else: + lbl = Gtk.Label() + lbl.set_markup("{}%".format(str(round(percent, 2)))) lbl.set_xalign(0) grid.attach(lbl, 5, idx, 1, 1) @@ -173,32 +213,28 @@ def list_processes(once=False): win_name = mapped["pid"] if win_name: - lbl = Gtk.Label.new(win_name) - lbl.set_width_chars(W_WINDOW) + lbl = Gtk.Label.new(" {}".format(win_name)) lbl.set_xalign(0) grid.attach(lbl, 8, idx, 1, 1) name = processes[pid]["name"] if theme.lookup_icon(name, 16, Gtk.IconLookupFlags.FORCE_SYMBOLIC): img = Gtk.Image.new_from_icon_name(name, Gtk.IconSize.MENU) - img.set_property("name", "icon") img.set_property("halign", Gtk.Align.END) grid.attach(img, 6, idx, 1, 1) # fallback icon name elif win_name and theme.lookup_icon(win_name, 16, Gtk.IconLookupFlags.FORCE_SYMBOLIC): img = Gtk.Image.new_from_icon_name(win_name, Gtk.IconSize.MENU) - img.set_property("name", "icon") img.set_property("halign", Gtk.Align.END) grid.attach(img, 6, idx, 1, 1) elif win_name and win_name in aliases and theme.lookup_icon(aliases[win_name], 16, Gtk.IconLookupFlags.FORCE_SYMBOLIC): img = Gtk.Image.new_from_icon_name(aliases[win_name], Gtk.IconSize.MENU) - img.set_property("name", "icon") img.set_property("halign", Gtk.Align.END) grid.attach(img, 6, idx, 1, 1) - if len(name) > W_NAME - 1: - name = "{}…".format(name[:W_NAME - 2]) + if len(name) > W_NAME: + name = "{}…".format(name[:W_NAME - 1]) lbl = Gtk.Label.new(name) lbl.set_width_chars(W_NAME) lbl.set_xalign(0) @@ -207,19 +243,70 @@ def list_processes(once=False): if processes[pid]["username"] == user: btn = Gtk.Button.new_from_icon_name("gtk-close", Gtk.IconSize.MENU) btn.set_property("name", "btn-kill") - btn.set_property("hexpand", False) - btn.set_property("halign", Gtk.Align.START) + btn.set_property("halign", Gtk.Align.CENTER) btn.connect("clicked", terminate, pid) grid.attach(btn, 0, idx, 1, 1) idx += 1 + # placeholders to align column width with the button box on top + img = Gtk.Image() + grid.attach(img, 0, idx + 1, 2, 1) + img.set_size_request(btn_pid.get_allocated_width(), 0) + + img = Gtk.Image() + grid.attach(img, 2, idx + 1, 1, 1) + img.set_size_request(btn_ppid.get_allocated_width(), 0) + + img = Gtk.Image() + grid.attach(img, 3, idx + 1, 1, 1) + img.set_size_request(btn_owner.get_allocated_width(), 0) + + img = Gtk.Image() + grid.attach(img, 4, idx + 1, 1, 1) + img.set_size_request(btn_cpu.get_allocated_width(), 0) + + img = Gtk.Image() + grid.attach(img, 5, idx + 1, 1, 1) + img.set_size_request(btn_mem.get_allocated_width(), 0) + + img = Gtk.Image() + grid.attach(img, 6, idx + 1, 2, 1) + img.set_size_request(btn_name.get_allocated_width(), 0) + grid.show_all() if not once: return True +def set_sort_order(btn, order): + global sort_order + sort_order = order + + btn_pid.set_label(" PID ") + btn_ppid.set_label(" PPID ") + btn_owner.set_label(" Owner ") + btn_cpu.set_label(" CPU% ") + btn_mem.set_label(" Mem% ") + btn_name.set_label(" Name ") + + if order == SortOrder.PID: + btn_pid.set_label(" PID ⨞") + if order == SortOrder.PPID: + btn_ppid.set_label(" PPID ⨞") + if order == SortOrder.USERNAME: + btn_owner.set_label(" Owner ⨞") + if order == SortOrder.CPU_PERCENT: + btn_cpu.set_label(" CPU% ⨞") + if order == SortOrder.MEMORY_PERCENT: + btn_mem.set_label(" Mem% ⨞") + if order == SortOrder.NAME: + btn_name.set_label(" Name ⨞") + + list_processes() + + def on_background_cb(check_button): settings["processes-background-only"] = check_button.get_active() save_json(settings, os.path.join(get_config_dir(), "common-settings.json")) @@ -257,58 +344,45 @@ def main(): box = Gtk.Box.new(Gtk.Orientation.VERTICAL, 6) box.set_property("margin", 6) box.set_property("vexpand", True) - win.add(box) - - wrapper = Gtk.Box.new(Gtk.Orientation.HORIZONTAL, 0) - wrapper.set_property("name", "header") + wrapper = Gtk.Box.new(Gtk.Orientation.VERTICAL, 0) box.pack_start(wrapper, False, False, 0) - desc_box = Gtk.Box.new(Gtk.Orientation.HORIZONTAL, 0) - wrapper.pack_start(desc_box, False, True, 0) + hbox = Gtk.Box.new(Gtk.Orientation.HORIZONTAL, 3) + wrapper.pack_start(hbox, True, True, 0) - img = Gtk.Image() - img.set_property("name", "img-empty") - desc_box.pack_start(img, False, False, 0) - - lbl = Gtk.Label.new("PID") - lbl.set_width_chars(W_PID) - lbl.set_xalign(0) - desc_box.pack_start(lbl, False, False, 0) - - lbl = Gtk.Label.new("PPID") - lbl.set_width_chars(W_PPID) - lbl.set_xalign(0) - desc_box.pack_start(lbl, False, False, 0) - - lbl = Gtk.Label.new("Owner") - lbl.set_width_chars(W_OWNER) - lbl.set_xalign(0) - desc_box.pack_start(lbl, False, False, 0) - - lbl = Gtk.Label.new("CPU%") - lbl.set_width_chars(W_CPU) - lbl.set_xalign(0) - desc_box.pack_start(lbl, True, True, 0) - - lbl = Gtk.Label.new("Mem%") - lbl.set_width_chars(W_MEM) - lbl.set_xalign(0) - desc_box.pack_start(lbl, True, True, 0) + global btn_pid, btn_ppid, btn_owner, btn_cpu, btn_mem, btn_name - img = Gtk.Image() - img.set_property("name", "icon") - desc_box.pack_start(img, True, True, 0) + btn_pid = Gtk.Button.new_with_label(" PID ⨞") + btn_pid.connect("clicked", set_sort_order, SortOrder.PID) + hbox.pack_start(btn_pid, False, False, 0) + btn_pid.show() - lbl = Gtk.Label.new("Name") - lbl.set_width_chars(W_NAME) - lbl.set_xalign(0) - desc_box.pack_start(lbl, True, True, 0) + btn_ppid = Gtk.Button.new_with_label(" PPID ") + btn_ppid.connect("clicked", set_sort_order, SortOrder.PPID) + hbox.pack_start(btn_ppid, False, False, 0) + btn_ppid.show() - if swaysock: - global window_lbl - window_lbl = Gtk.Label.new("Window") - window_lbl.set_width_chars(W_WINDOW) - window_lbl.set_xalign(0) - desc_box.pack_start(window_lbl, True, True, 0) + btn_owner = Gtk.Button.new_with_label(" Owner ") + btn_owner.connect("clicked", set_sort_order, SortOrder.USERNAME) + hbox.pack_start(btn_owner, False, False, 0) + + btn_cpu = Gtk.Button.new_with_label(" CPU% ") + btn_cpu.connect("clicked", set_sort_order, SortOrder.CPU_PERCENT) + hbox.pack_start(btn_cpu, False, False, 0) + + btn_mem = Gtk.Button.new_with_label(" Mem% ") + btn_mem.connect("clicked", set_sort_order, SortOrder.MEMORY_PERCENT) + hbox.pack_start(btn_mem, False, False, 0) + + btn_name = Gtk.Button.new_with_label(" Name ") + btn_name.connect("clicked", set_sort_order, SortOrder.NAME) + hbox.pack_start(btn_name, False, False, 0) + + global window_lbl + window_lbl = Gtk.Label.new(" Window") + window_lbl.set_xalign(0) + hbox.pack_start(window_lbl, False, False, 0) + + win.add(box) global scrolled_window scrolled_window = Gtk.ScrolledWindow.new(None, None) @@ -320,12 +394,12 @@ def main(): dist.set_property("vexpand", True) box.pack_start(dist, True, True, 0) - hbox = Gtk.Box.new(Gtk.Orientation.HORIZONTAL, 6) + hbox = Gtk.Box.new(Gtk.Orientation.HORIZONTAL, 0) hbox.set_property("margin", 6) box.pack_start(hbox, False, False, 0) img = Gtk.Image.new_from_icon_name("nwg-processes", Gtk.IconSize.LARGE_TOOLBAR) - hbox.pack_start(img, False, False, 0) + hbox.pack_start(img, False, False, 6) lbl = Gtk.Label() lbl.set_markup("nwg-processes") @@ -352,16 +426,13 @@ def main(): provider = Gtk.CssProvider() style_context = Gtk.StyleContext() style_context.add_provider_for_screen(screen, provider, Gtk.STYLE_PROVIDER_PRIORITY_APPLICATION) - css = b""" #header { background-color: rgba(0, 0, 0, 0.3) } - #icon { margin-right: 6px } - #img-empty { margin-right: 15px; border: 1px } - #btn-kill { padding: 0; border: 0; margin-right: 6px } - label { font-family: DejaVu Sans Mono, monospace } """ + css = b""" #btn-kill { padding: 0; border: 0; margin: 0 } + label { font-family: DejaVu Sans Mono } """ provider.load_from_data(css) win.show_all() - win.set_size_request(0, win.get_allocated_width() * 0.5) + win.set_size_request(0, 500) list_processes() diff --git a/setup.py b/setup.py index 0c7c0605..044ddbdd 100644 --- a/setup.py +++ b/setup.py @@ -8,7 +8,7 @@ def read(f_name): setup( name='nwg-panel', - version='0.9.8', + version='0.9.9', description='GTK3-based panel for sway and Hyprland Wayland compositors', packages=find_packages(), include_package_data=True,