
This commit ensures compatibility with older Python versions by correcting a syntax error in f-strings. The fix involves using single quotes inside the f-string to avoid mismatched parentheses.
Commit 84e01967e2
should also include this change.
Signed-off-by: Ludovico Piero <lewdovico@gnuweeb.org>
454 lines
14 KiB
Python
454 lines
14 KiB
Python
#!/usr/bin/env python3
|
|
|
|
"""
|
|
nwg-shell helper script to preview system processes
|
|
Copyright (c) 2023-2024 Piotr Miller
|
|
e-mail: nwg.piotr@gmail.com
|
|
GitHub: https://github.com/nwg-piotr/nwg-panel
|
|
Project: https://nwg-piotr.github.io/nwg-shell
|
|
License: MIT
|
|
"""
|
|
|
|
import json
|
|
import os
|
|
import socket
|
|
import sys
|
|
from enum import Enum
|
|
|
|
import psutil
|
|
from i3ipc import Connection
|
|
import gi
|
|
|
|
gi.require_version('Gtk', '3.0')
|
|
from gi.repository import Gtk, Gdk, GLib
|
|
|
|
from nwg_panel.tools import get_config_dir, load_json, save_json, check_key, eprint
|
|
|
|
swaysock = os.getenv('SWAYSOCK')
|
|
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):
|
|
# /tmp/hypr moved to $XDG_RUNTIME_DIR/hypr in #5788
|
|
xdg_runtime_dir = os.getenv("XDG_RUNTIME_DIR")
|
|
hypr_dir = f"{xdg_runtime_dir}/hypr" if xdg_runtime_dir and os.path.isdir(
|
|
f"{xdg_runtime_dir}/hypr") else "/tmp/hypr"
|
|
|
|
s = socket.socket(socket.AF_UNIX, socket.SOCK_STREAM)
|
|
s.connect(f"{hypr_dir}/{os.getenv('HYPRLAND_INSTANCE_SIGNATURE')}/.socket.sock")
|
|
|
|
s.send(cmd.encode("utf-8"))
|
|
output = s.recv(20480).decode('utf-8')
|
|
s.close()
|
|
|
|
return output
|
|
|
|
|
|
if not swaysock and not his:
|
|
eprint("Neither sway nor hyprland socket detected, terminating.")
|
|
sys.exit(1)
|
|
|
|
W_OWNER = 10
|
|
W_NAME = 24
|
|
|
|
# Fallback icon names dict: win_name -> icon_name
|
|
aliases = {
|
|
"Gimp-2.10": "gimp",
|
|
"nwg-panel-config": "nwg-panel"
|
|
}
|
|
|
|
settings = {} # nwg-panel common settings
|
|
scrolled_window = None
|
|
grid = Gtk.Grid()
|
|
window_lbl = None
|
|
|
|
theme = Gtk.IconTheme.get_default()
|
|
|
|
|
|
def handle_keyboard(win, event):
|
|
if event.type == Gdk.EventType.KEY_RELEASE and event.keyval == Gdk.KEY_Escape:
|
|
win.destroy()
|
|
|
|
|
|
def terminate(btn, pid):
|
|
print("Terminating {}".format(pid))
|
|
try:
|
|
os.kill(pid, 15)
|
|
except Exception as e:
|
|
eprint(e)
|
|
|
|
list_processes()
|
|
|
|
|
|
def list_processes(once=False):
|
|
tree = None
|
|
if swaysock:
|
|
tree = Connection().get_tree()
|
|
elif his:
|
|
output = hyprctl("j/clients")
|
|
clients = json.loads(output)
|
|
|
|
processes = {}
|
|
|
|
user = os.getenv('USER')
|
|
for proc in psutil.process_iter(['pid', 'ppid', 'name', 'username', 'cpu_percent', 'memory_percent']):
|
|
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():
|
|
viewport = scrolled_window.get_children()[0]
|
|
else:
|
|
viewport = None
|
|
|
|
global grid
|
|
if grid:
|
|
grid.destroy()
|
|
|
|
grid = Gtk.Grid.new()
|
|
grid.set_column_spacing(3)
|
|
|
|
if viewport:
|
|
viewport.add(grid)
|
|
elif scrolled_window:
|
|
scrolled_window.add(grid)
|
|
|
|
idx = 1
|
|
for item in sorted_list:
|
|
cons = None
|
|
mapped = {}
|
|
pid = item['pid']
|
|
if swaysock:
|
|
cons = tree.find_by_pid(pid)
|
|
elif his:
|
|
for client in clients:
|
|
if client["pid"] == pid and client["mapped"]:
|
|
mapped["pid"] = client["class"]
|
|
break
|
|
|
|
if not cons or not settings["processes-background-only"]:
|
|
lbl = Gtk.Label.new(str(pid))
|
|
lbl.set_xalign(0)
|
|
grid.attach(lbl, 1, idx, 1, 1)
|
|
|
|
lbl = Gtk.Label.new(str(processes[pid]["ppid"]))
|
|
lbl.set_xalign(0)
|
|
grid.attach(lbl, 2, idx, 1, 1)
|
|
|
|
owner = processes[pid]["username"]
|
|
if len(owner) > W_OWNER - 1:
|
|
owner = "{}…".format(owner[:W_OWNER - 2])
|
|
lbl = Gtk.Label.new(owner)
|
|
lbl.set_xalign(0)
|
|
grid.attach(lbl, 3, idx, 1, 1)
|
|
|
|
percent = processes[pid]["cpu_percent"]
|
|
if percent == 0:
|
|
lbl = Gtk.Label.new("{}%".format(str(percent)))
|
|
else:
|
|
lbl = Gtk.Label()
|
|
lbl.set_markup("<b>{}%</b>".format(str(percent)))
|
|
lbl.set_xalign(0)
|
|
grid.attach(lbl, 4, idx, 1, 1)
|
|
|
|
percent = processes[pid]["memory_percent"]
|
|
if percent < 1:
|
|
lbl = Gtk.Label.new("{}%".format(str(round(percent, 2))))
|
|
else:
|
|
lbl = Gtk.Label()
|
|
lbl.set_markup("<b>{}%</b>".format(str(round(percent, 2))))
|
|
lbl.set_xalign(0)
|
|
grid.attach(lbl, 5, idx, 1, 1)
|
|
|
|
win_name = ""
|
|
if cons:
|
|
if cons[0].app_id:
|
|
win_name = cons[0].app_id
|
|
elif cons[0].window_class:
|
|
win_name = cons[0].window_class
|
|
elif cons[0].name:
|
|
win_name = cons[0].name
|
|
elif cons[0].window_title:
|
|
win_name = cons[0].window_title
|
|
elif mapped:
|
|
win_name = mapped["pid"]
|
|
|
|
if win_name:
|
|
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("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("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("halign", Gtk.Align.END)
|
|
grid.attach(img, 6, idx, 1, 1)
|
|
|
|
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)
|
|
grid.attach(lbl, 7, idx, 1, 1)
|
|
|
|
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("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"))
|
|
if window_lbl:
|
|
window_lbl.set_visible(not settings["processes-background-only"])
|
|
|
|
list_processes()
|
|
|
|
|
|
def on_own_cb(check_button):
|
|
settings["processes-own-only"] = check_button.get_active()
|
|
save_json(settings, os.path.join(get_config_dir(), "common-settings.json"))
|
|
|
|
list_processes()
|
|
|
|
|
|
def main():
|
|
GLib.set_prgname('nwg-processes')
|
|
global settings
|
|
settings = load_json(os.path.join(get_config_dir(), "common-settings.json"))
|
|
defaults = {
|
|
"processes-background-only": False,
|
|
"processes-own-only": True,
|
|
"processes-interval-ms": 2000
|
|
}
|
|
for key in defaults:
|
|
check_key(settings, key, defaults[key])
|
|
if not swaysock:
|
|
settings["processes-background-only"] = False
|
|
|
|
win = Gtk.Window.new(Gtk.WindowType.TOPLEVEL)
|
|
win.connect('destroy', Gtk.main_quit)
|
|
win.connect("key-release-event", handle_keyboard)
|
|
|
|
box = Gtk.Box.new(Gtk.Orientation.VERTICAL, 6)
|
|
box.set_property("margin", 6)
|
|
box.set_property("vexpand", True)
|
|
wrapper = Gtk.Box.new(Gtk.Orientation.VERTICAL, 0)
|
|
box.pack_start(wrapper, False, False, 0)
|
|
hbox = Gtk.Box.new(Gtk.Orientation.HORIZONTAL, 3)
|
|
wrapper.pack_start(hbox, True, True, 0)
|
|
|
|
global btn_pid, btn_ppid, btn_owner, btn_cpu, btn_mem, btn_name
|
|
|
|
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()
|
|
|
|
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()
|
|
|
|
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)
|
|
scrolled_window.set_policy(Gtk.PolicyType.NEVER, Gtk.PolicyType.AUTOMATIC)
|
|
scrolled_window.set_propagate_natural_height(True)
|
|
box.pack_start(scrolled_window, True, True, 0)
|
|
|
|
dist = Gtk.Box.new(Gtk.Orientation.VERTICAL, 0)
|
|
dist.set_property("vexpand", True)
|
|
box.pack_start(dist, True, True, 0)
|
|
|
|
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, 6)
|
|
|
|
lbl = Gtk.Label()
|
|
lbl.set_markup("<b>nwg-processes</b>")
|
|
hbox.pack_start(lbl, False, False, 0)
|
|
|
|
if swaysock:
|
|
cb = Gtk.CheckButton.new_with_label("Background only")
|
|
cb.set_tooltip_text("Processes that don't belong to the sway tree")
|
|
cb.set_active(settings["processes-background-only"])
|
|
cb.connect("toggled", on_background_cb)
|
|
hbox.pack_start(cb, False, False, 6)
|
|
|
|
cb = Gtk.CheckButton.new_with_label("{}'s only".format(os.getenv('USER')))
|
|
cb.set_tooltip_text("Processes that belong to the current $USER")
|
|
cb.set_active(settings["processes-own-only"])
|
|
cb.connect("toggled", on_own_cb)
|
|
hbox.pack_start(cb, False, False, 6)
|
|
|
|
btn = Gtk.Button.new_with_label("Close")
|
|
hbox.pack_end(btn, False, False, 0)
|
|
btn.connect("clicked", Gtk.main_quit)
|
|
|
|
screen = Gdk.Screen.get_default()
|
|
provider = Gtk.CssProvider()
|
|
style_context = Gtk.StyleContext()
|
|
style_context.add_provider_for_screen(screen, provider, Gtk.STYLE_PROVIDER_PRIORITY_APPLICATION)
|
|
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, 500)
|
|
|
|
list_processes()
|
|
|
|
if settings["processes-interval-ms"] > 0:
|
|
Gdk.threads_add_timeout(GLib.PRIORITY_DEFAULT_IDLE, settings["processes-interval-ms"], list_processes)
|
|
else:
|
|
GLib.timeout_add(1000, list_processes, True)
|
|
|
|
Gtk.main()
|
|
|
|
|
|
if __name__ == '__main__':
|
|
main()
|