gruntfutuk

Ok-Truck-28

Nov 9th, 2025
101
0
Never
1
Not a member of Pastebin yet? Sign Up, it unlocks many cool features!
Python 38.63 KB | None | 0 0
  1. import os, sys, time, subprocess, threading, collections
  2. import psutil
  3. import tkinter as tk
  4. from tkinter import ttk, messagebox
  5. from tkinter.scrolledtext import ScrolledText
  6. from matplotlib.figure import Figure
  7. from matplotlib.backends.backend_tkagg import FigureCanvasTkAgg
  8.  
  9. # ---- Configuration ----
  10. REFRESH_MS = 1000  # background sampling interval (ms)
  11. UI_UPDATE_MS = 800  # UI update interval (ms)
  12. HISTORY_POINTS = 60  # history length for charts
  13.  
  14. # ---- Helpers ----
  15. def safe_run(cmd, timeout=1.0):
  16.     try:
  17.         return subprocess.check_output(cmd, shell=True, stderr=subprocess.DEVNULL, universal_newlines=True, timeout=timeout).strip()
  18.     except Exception:
  19.         return ""
  20.  
  21. def size_fmt(n):
  22.     try:
  23.         n = float(n)
  24.     except Exception:
  25.         return str(n)
  26.     for unit in ['B', 'KB', 'MB', 'GB', 'TB', 'PB']:
  27.         if abs(n) < 1024.0:
  28.             return f"{n:3.1f} {unit}"
  29.         n /= 1024.0
  30.     return f"{n:.1f} EB"
  31.  
  32. def cpu_name():
  33.     try:
  34.         with open("/proc/cpuinfo", "r") as f:
  35.             for line in f:
  36.                 if line.lower().startswith("model name"):
  37.                     return line.split(":", 1)[1].strip()
  38.     except Exception:
  39.         pass
  40.     out = safe_run("lscpu | grep 'Model name' || true")
  41.     if out and ":" in out:
  42.         return out.split(":", 1)[1].strip()
  43.     return "CPU"
  44.  
  45. def detect_nvidia():
  46.     return bool(safe_run("which nvidia-smi"))
  47.  
  48. def query_nvidia():
  49.     out = safe_run("nvidia-smi --query-gpu=index,name,utilization.gpu,memory.total,memory.used --format=csv,noheader,nounits")
  50.     gpus = []
  51.     if not out:
  52.         return gpus
  53.     for line in out.splitlines():
  54.         parts = [p.strip() for p in line.split(",")]
  55.         if len(parts) >= 5:
  56.             try:
  57.                 gpus.append({
  58.                     "index": int(parts[0]),
  59.                     "name": parts[1],
  60.                     "util": float(parts[2]),
  61.                     "mem_total": float(parts[3]),
  62.                     "mem_used": float(parts[4])
  63.                 })
  64.             except Exception:
  65.                 pass
  66.     return gpus
  67.  
  68. # ---- Background sampler thread ----
  69. class Sampler(threading.Thread):
  70.     def __init__(self, interval_ms=REFRESH_MS):
  71.         super().__init__(daemon=True)
  72.         self.interval = max(50, interval_ms) / 1000.0
  73.         self.lock = threading.Lock()
  74.         self.running = True
  75.         # histories
  76.         self.cpu_hist = collections.deque([0] * HISTORY_POINTS, maxlen=HISTORY_POINTS)
  77.         self.mem_hist = collections.deque([0] * HISTORY_POINTS, maxlen=HISTORY_POINTS)
  78.         self.net_rx_hist = collections.deque([0] * HISTORY_POINTS, maxlen=HISTORY_POINTS)
  79.         self.net_tx_hist = collections.deque([0] * HISTORY_POINTS, maxlen=HISTORY_POINTS)
  80.         self.disk_read_rate = {}  # per-device B/s
  81.         self.disk_write_rate = {}
  82.         # last counters
  83.         self.last_net = psutil.net_io_counters()
  84.         self.last_disk = psutil.disk_io_counters(perdisk=True)
  85.         self.nvidia = detect_nvidia()
  86.         self.nvidia_info = []
  87.         self.sampled = {}
  88.         self.start()
  89.  
  90.     def run(self):
  91.         while self.running:
  92.             try:
  93.                 cpu = psutil.cpu_percent(interval=None)
  94.                 mem = psutil.virtual_memory().percent
  95.                 now_net = psutil.net_io_counters()
  96.                 rx = now_net.bytes_recv - self.last_net.bytes_recv
  97.                 tx = now_net.bytes_sent - self.last_net.bytes_sent
  98.                 sec = max(self.interval, 0.001)
  99.                 rx_rate = rx / sec; tx_rate = tx / sec
  100.                 self.last_net = now_net
  101.                 # disk io rates
  102.                 cur_disk = psutil.disk_io_counters(perdisk=True)
  103.                 dr = {}; dw = {}
  104.                 for k, v in cur_disk.items():
  105.                     pv = self.last_disk.get(k)
  106.                     if pv:
  107.                         dr[k] = (v.read_bytes - pv.read_bytes) / sec
  108.                         dw[k] = (v.write_bytes - pv.write_bytes) / sec
  109.                     else:
  110.                         dr[k] = 0.0; dw[k] = 0.0
  111.                 self.last_disk = cur_disk
  112.                 # nvidia
  113.                 ninfo = []
  114.                 if self.nvidia:
  115.                     ninfo = query_nvidia()
  116.                 # write into sampled with lock
  117.                 with self.lock:
  118.                     self.cpu_hist.append(cpu); self.mem_hist.append(mem)
  119.                     self.net_rx_hist.append(rx_rate); self.net_tx_hist.append(tx_rate)
  120.                     self.disk_read_rate = dr; self.disk_write_rate = dw
  121.                     self.nvidia_info = ninfo
  122.                     self.sampled['cpu'] = cpu; self.sampled['mem'] = mem
  123.                     self.sampled['rx_rate'] = rx_rate; self.sampled['tx_rate'] = tx_rate
  124.                     self.sampled['timestamp'] = time.time()
  125.             except Exception:
  126.                 pass
  127.             time.sleep(self.interval)
  128.  
  129.     def stop(self):
  130.         self.running = False
  131.  
  132. # ---- Main App ----
  133. class TaskManagerApp(tk.Tk):
  134.     def __init__(self):
  135.         super().__init__()
  136.         self.title("Task Manager - Win11 Dark (Optimized)")
  137.         self.geometry("1200x750")
  138.         self.configure(bg="#141414")
  139.         self.style = ttk.Style(self)
  140.         try:
  141.             self.style.theme_use("clam")
  142.         except Exception:
  143.             pass
  144.         self.style.configure("TNotebook", background="#141414")
  145.         self.style.configure("TNotebook.Tab", background="#1f1f1f", foreground="white", padding=[10, 6])
  146.         self.style.map("TNotebook.Tab", background=[("selected", "#2b2b2b")])
  147.         self.style.configure("Treeview", background="#1b1b1b", foreground="white", fieldbackground="#1b1b1b", rowheight=20)
  148.         self.style.configure("Treeview.Heading", background="#262626", foreground="white")
  149.         self.refresh_ms = REFRESH_MS
  150.         self.sampler = Sampler(self.refresh_ms)
  151.         self.create_widgets()
  152.         self.after(UI_UPDATE_MS, self.ui_update_loop)
  153.         self.protocol("WM_DELETE_WINDOW", self.on_close)
  154.  
  155.     def create_widgets(self):
  156.         nb = ttk.Notebook(self)
  157.         nb.pack(fill=tk.BOTH, expand=True, padx=8, pady=8)
  158.         # tabs
  159.         self.tab_processes = ttk.Frame(nb); nb.add(self.tab_processes, text="Processes")
  160.         self.tab_performance = ttk.Frame(nb); nb.add(self.tab_performance, text="Performance")
  161.         self.tab_startup = ttk.Frame(nb); nb.add(self.tab_startup, text="Startup")
  162.         self.tab_users = ttk.Frame(nb); nb.add(self.tab_users, text="Users")
  163.         self.tab_details = ttk.Frame(nb); nb.add(self.tab_details, text="Details")
  164.         # build each tab
  165.         self.build_processes_tab(self.tab_processes)
  166.         self.build_performance_tab(self.tab_performance)
  167.         self.build_startup_tab(self.tab_startup)
  168.         self.build_users_tab(self.tab_users)
  169.         self.build_details_tab(self.tab_details)
  170.         # bottom controls
  171.         ctrl = tk.Frame(self, bg="#141414"); ctrl.pack(fill=tk.X, padx=8, pady=(0, 8))
  172.         ttk.Button(ctrl, text="Refresh Now", command=self.force_refresh).pack(side=tk.LEFT, padx=4)
  173.         ttk.Button(ctrl, text="End Task", command=self.end_task).pack(side=tk.LEFT, padx=4)
  174.         ttk.Button(ctrl, text="Kill (SIGKILL)", command=self.kill_task).pack(side=tk.LEFT, padx=4)
  175.         ttk.Button(ctrl, text="Force-Kill (xkill mode)", command=self.xkill_mode).pack(side=tk.LEFT, padx=4)
  176.         ttk.Button(ctrl, text="Show Details", command=self.show_selected_details).pack(side=tk.LEFT, padx=4)
  177.         ttk.Label(ctrl, text="Auto-refresh:", background="#141414", foreground="white").pack(side=tk.LEFT, padx=(16, 4))
  178.         self.auto_var = tk.BooleanVar(value=True)
  179.         ttk.Checkbutton(ctrl, text="On/Off", variable=self.auto_var).pack(side=tk.LEFT)
  180.         ttk.Label(ctrl, text="UI(ms):", background="#141414", foreground="white").pack(side=tk.LEFT, padx=(16, 4))
  181.         self.ui_interval_var = tk.IntVar(value=UI_UPDATE_MS)
  182.         ttk.Entry(ctrl, textvariable=self.ui_interval_var, width=6).pack(side=tk.LEFT, padx=4)
  183.         ttk.Button(ctrl, text="Set UI Interval", command=self.set_ui_interval).pack(side=tk.LEFT, padx=4)
  184.  
  185.     # ---- Processes tab ----
  186.     def build_processes_tab(self, parent):
  187.         f = tk.Frame(parent, bg="#141414"); f.pack(fill=tk.BOTH, expand=True, padx=8, pady=8)
  188.         cols = ("pid", "name", "user", "cpu", "mem", "status")
  189.         self.proc_tree = ttk.Treeview(f, columns=cols, show="headings", selectmode="browse")
  190.         for c, h in (("pid", "PID"), ("name", "Name"), ("user", "User"), ("cpu", "CPU %"), ("mem", "Mem %"), ("status", "Status")):
  191.             self.proc_tree.heading(c, text=h); self.proc_tree.column(c, width=120 if c != "name" else 420, anchor="w")
  192.         vsb = ttk.Scrollbar(f, orient="vertical", command=self.proc_tree.yview); self.proc_tree.configure(yscroll=vsb.set)
  193.         self.proc_tree.pack(side=tk.LEFT, fill=tk.BOTH, expand=True); vsb.pack(side=tk.LEFT, fill=tk.Y)
  194.         self.proc_tree.bind("<Double-1>", lambda e: self.show_selected_details())
  195.         # search box and refresh button
  196.         right = tk.Frame(f, bg="#141414"); right.pack(side=tk.LEFT, fill=tk.Y, padx=(8, 0))
  197.         ttk.Label(right, text="Filter:", background="#141414", foreground="white").pack(anchor="nw")
  198.         self.filter_var = tk.StringVar(value="")
  199.         ttk.Entry(right, textvariable=self.filter_var, width=30).pack(anchor="nw", pady=(0, 8))
  200.         ttk.Button(right, text="Refresh", command=self.refresh_processes_now).pack(anchor="nw")
  201.         ttk.Button(right, text="Kill selected", command=self.kill_task).pack(anchor="nw", pady=(8, 0))
  202.  
  203.     def refresh_processes_now(self):
  204.         # lightweight iteration
  205.         sel_pid = None
  206.         sel = self.proc_tree.selection()
  207.         if sel: sel_pid = self.proc_tree.item(sel[0])["values"][0]
  208.         for r in self.proc_tree.get_children(): self.proc_tree.delete(r)
  209.         keyword = self.filter_var.get().lower().strip()
  210.         # prime cpu
  211.         for p in psutil.process_iter():
  212.             try:
  213.                 p.cpu_percent(interval=None)
  214.             except Exception:
  215.                 pass
  216.         for p in psutil.process_iter(['pid', 'name', 'username', 'cpu_percent', 'memory_percent', 'status']):
  217.             try:
  218.                 info = p.info
  219.                 name = (info.get('name') or "")
  220.                 if keyword and keyword not in name.lower():
  221.                     continue
  222.                 self.proc_tree.insert("", "end", values=(info.get('pid'), name, info.get('username') or "", f"{(info.get('cpu_percent') or 0):.1f}", f"{(info.get('memory_percent') or 0):.1f}", info.get('status') or ""))
  223.             except (psutil.NoSuchProcess, psutil.AccessDenied):
  224.                 continue
  225.         # restore selection
  226.         if sel_pid:
  227.             for iid in self.proc_tree.get_children():
  228.                 if str(self.proc_tree.item(iid)["values"][0]) == str(sel_pid):
  229.                     self.proc_tree.selection_set(iid); break
  230.  
  231.     # ---- Performance tab ----
  232.     def build_performance_tab(self, parent):
  233.         frame = tk.Frame(parent, bg="#141414"); frame.pack(fill=tk.BOTH, expand=True, padx=8, pady=8)
  234.         left = tk.Frame(frame, bg="#141414"); left.pack(side=tk.LEFT, fill=tk.BOTH, expand=True)
  235.         right = tk.Frame(frame, bg="#141414", width=380); right.pack(side=tk.RIGHT, fill=tk.Y, padx=(8, 0))
  236.         # CPU block (big)
  237.         cpu_block = ttk.LabelFrame(left, text="CPU", padding=6); cpu_block.pack(fill=tk.X, padx=4, pady=4)
  238.         self.cpu_name_lbl = ttk.Label(cpu_block, text=cpu_name()); self.cpu_name_lbl.pack(anchor="w")
  239.         self.cpu_freq_lbl = ttk.Label(cpu_block, text="Freq: N/A"); self.cpu_freq_lbl.pack(anchor="w")
  240.         self.cpu_bar = ttk.Progressbar(cpu_block, orient="horizontal", length=800, mode="determinate", maximum=100); self.cpu_bar.pack(fill=tk.X, pady=(4, 4))
  241.         self.cpu_chart_fig = Figure(figsize=(6, 1.6), dpi=100, facecolor="#141414")
  242.         self.cpu_ax = self.cpu_chart_fig.add_subplot(111); self.cpu_ax.set_facecolor("#141414"); self.cpu_canvas = FigureCanvasTkAgg(self.cpu_chart_fig, master=cpu_block); self.cpu_canvas.get_tk_widget().pack(fill=tk.X)
  243.         # GPU block
  244.         gpu_block = ttk.LabelFrame(left, text="GPU", padding=6); gpu_block.pack(fill=tk.X, padx=4, pady=4)
  245.         self.gpu_text = ttk.Label(gpu_block, text="GPU: N/A"); self.gpu_text.pack(anchor="w")
  246.         self.gpu_bar = ttk.Progressbar(gpu_block, orient="horizontal", length=800, mode="determinate", maximum=100); self.gpu_bar.pack(fill=tk.X, pady=(4, 4))
  247.         self.gpu_chart_fig = Figure(figsize=(6, 1), dpi=90, facecolor="#141414"); self.gpu_ax = self.gpu_chart_fig.add_subplot(111); self.gpu_ax.set_facecolor("#141414"); self.gpu_canvas = FigureCanvasTkAgg(self.gpu_chart_fig, master=gpu_block); self.gpu_canvas.get_tk_widget().pack(fill=tk.X)
  248.         # Memory block
  249.         mem_block = ttk.LabelFrame(left, text="Memory", padding=6); mem_block.pack(fill=tk.X, padx=4, pady=4)
  250.         self.mem_lbl = ttk.Label(mem_block, text="Memory: N/A"); self.mem_lbl.pack(anchor="w")
  251.         self.mem_bar = ttk.Progressbar(mem_block, orient="horizontal", length=800, mode="determinate", maximum=100); self.mem_bar.pack(fill=tk.X, pady=(4, 4))
  252.         self.mem_chart_fig = Figure(figsize=(6, 1), dpi=90, facecolor="#141414"); self.mem_ax = self.mem_chart_fig.add_subplot(111); self.mem_ax.set_facecolor("#141414"); self.mem_canvas = FigureCanvasTkAgg(self.mem_chart_fig, master=mem_block); self.mem_canvas.get_tk_widget().pack(fill=tk.X)
  253.         # Storage block (list + small chart)
  254.         disk_block = ttk.LabelFrame(left, text="Storage", padding=6); disk_block.pack(fill=tk.BOTH, padx=4, pady=4, expand=True)
  255.         cols = ("device", "mount", "model", "total", "used", "free", "%", "r/s", "w/s")
  256.         self.disk_tree = ttk.Treeview(disk_block, columns=cols, show="headings", height=6)
  257.         for c, h in (("device", "Device"), ("mount", "Mount"), ("model", "Model"), ("total", "Total"), ("used", "Used"), ("free", "Free"), ("%", "% Used"), ("r/s", "Read/s"), ("w/s", "Write/s")):
  258.             self.disk_tree.heading(c, text=h); self.disk_tree.column(c, width=120 if c in ("device", "mount", "model") else 90, anchor="center")
  259.         vsb = ttk.Scrollbar(disk_block, orient="vertical", command=self.disk_tree.yview); self.disk_tree.configure(yscroll=vsb.set)
  260.         self.disk_tree.pack(side=tk.LEFT, fill=tk.BOTH, expand=True); vsb.pack(side=tk.LEFT, fill=tk.Y)
  261.         self.disk_chart_fig = Figure(figsize=(6, 1.2), dpi=90, facecolor="#141414"); self.disk_ax = self.disk_chart_fig.add_subplot(111); self.disk_ax.set_facecolor("#141414"); self.disk_canvas = FigureCanvasTkAgg(self.disk_chart_fig, master=disk_block); self.disk_canvas.get_tk_widget().pack(fill=tk.X, padx=6, pady=4)
  262.         # Right column: Network + small summary
  263.         net_block = ttk.LabelFrame(right, text="Network", padding=6); net_block.pack(fill=tk.X, padx=4, pady=4)
  264.         self.net_lbl = ttk.Label(net_block, text="RX: 0/s | TX: 0/s"); self.net_lbl.pack(anchor="w")
  265.         self.net_chart_fig = Figure(figsize=(3.2, 3), dpi=100, facecolor="#141414")
  266.         self.net_ax = self.net_chart_fig.add_subplot(111); self.net_ax.set_facecolor("#141414"); self.net_canvas = FigureCanvasTkAgg(self.net_chart_fig, master=net_block); self.net_canvas.get_tk_widget().pack(fill=tk.BOTH, expand=True)
  267.         # GPU details on right
  268.         gpu_info_block = ttk.LabelFrame(right, text="GPU Details", padding=6); gpu_info_block.pack(fill=tk.BOTH, expand=False, padx=4, pady=4)
  269.         self.gpu_info_text = ScrolledText(gpu_info_block, height=6, bg="#111111", fg="white"); self.gpu_info_text.pack(fill=tk.BOTH, expand=True)
  270.         # prepare disk models mapping
  271.         self.disk_models = self._disk_model_map()
  272.  
  273.     def _disk_model_map(self):
  274.         out = safe_run("lsblk -ndo NAME,MODEL 2>/dev/null")
  275.         m = {}
  276.         for line in out.splitlines():
  277.             parts = line.split(None, 1)
  278.             if not parts:
  279.                 continue
  280.             name = parts[0]
  281.             model = parts[1] if len(parts) > 1 else ""
  282.             m["/dev/" + name] = model
  283.         return m
  284.  
  285.     # ---- Startup tab ----
  286.     def build_startup_tab(self, parent):
  287.         f = tk.Frame(parent, bg="#141414"); f.pack(fill=tk.BOTH, expand=True, padx=8, pady=8)
  288.         cols = ("name", "exec", "path", "enabled")
  289.         self.start_tree = ttk.Treeview(f, columns=cols, show="headings")
  290.         for c, h in (("name", "Name"), ("exec", "Exec"), ("path", "File"), ("enabled", "Enabled")):
  291.             self.start_tree.heading(c, text=h); self.start_tree.column(c, width=300 if c == "path" else 140)
  292.         vsb = ttk.Scrollbar(f, orient="vertical", command=self.start_tree.yview); self.start_tree.configure(yscroll=vsb.set)
  293.         self.start_tree.pack(side=tk.LEFT, fill=tk.BOTH, expand=True); vsb.pack(side=tk.LEFT, fill=tk.Y)
  294.         btns = tk.Frame(parent, bg="#141414"); btns.pack(fill=tk.X, padx=8, pady=(0, 8))
  295.         ttk.Button(btns, text="Refresh Startup", command=self.refresh_startup).pack(side=tk.LEFT, padx=4)
  296.         ttk.Button(btns, text="Open Autostart Folder", command=self.open_autostart).pack(side=tk.LEFT, padx=4)
  297.         ttk.Button(btns, text="Disable (move .disabled)", command=self.disable_startup).pack(side=tk.LEFT, padx=4)
  298.         self.refresh_startup()
  299.  
  300.     def refresh_startup(self):
  301.         # list desktop autostart + systemd enabled services
  302.         def parse_desktop(path):
  303.             name = ""; execv = ""; enabled = "Yes"
  304.             try:
  305.                 with open(path, "r", errors="ignore") as f:
  306.                     for L in f:
  307.                         if "=" in L:
  308.                             k, v = L.split("=", 1); k = k.strip(); v = v.strip()
  309.                             if k.lower() == "name":
  310.                                 name = v
  311.                             if k.lower() == "exec":
  312.                                 execv = v
  313.                     if path.endswith(".disabled"):
  314.                         enabled = "No"
  315.             except Exception:
  316.                 pass
  317.             return (name or os.path.basename(path), execv, path, enabled)
  318.  
  319.         self.start_tree.delete(*self.start_tree.get_children())
  320.         home = os.path.expanduser("~")
  321.         paths = [os.path.join(home, ".config", "autostart"), "/etc/xdg/autostart"]
  322.         for p in paths:
  323.             if os.path.isdir(p):
  324.                 for fn in sorted(os.listdir(p)):
  325.                     if fn.endswith(".desktop") or fn.endswith(".desktop.disabled"):
  326.                         self.start_tree.insert("", "end", values=parse_desktop(os.path.join(p, fn)))
  327.         # systemd user
  328.         out = safe_run("systemctl --user list-unit-files --type=service --state=enabled 2>/dev/null")
  329.         if out:
  330.             for line in out.splitlines():
  331.                 if line.strip() and not line.startswith("UNIT"):
  332.                     svc = line.split()[0]
  333.                     self.start_tree.insert("", "end", values=(svc, "systemd --user", "(systemd user)", "Yes"))
  334.         # system services (may require permission)
  335.         out2 = safe_run("systemctl list-unit-files --type=service --state=enabled 2>/dev/null")
  336.         if out2:
  337.             for line in out2.splitlines():
  338.                 if line.strip() and not line.startswith("UNIT"):
  339.                     svc = line.split()[0]
  340.                     self.start_tree.insert("", "end", values=(svc, "systemd", "(system)", "Yes"))
  341.  
  342.     def open_autostart(self):
  343.         path = os.path.expanduser("~/.config/autostart"); os.makedirs(path, exist_ok=True)
  344.         os.system(f'xdg-open "{path}" &')
  345.  
  346.     def disable_startup(self):
  347.         sel = self.start_tree.selection()
  348.         if not sel:
  349.             messagebox.showwarning("No selection", "Select a startup entry."); return
  350.         path = self.start_tree.item(sel[0])["values"][2]
  351.         try:
  352.             new = path + ".disabled"; os.rename(path, new); messagebox.showinfo("Disabled", f"Moved to {new}"); self.refresh_startup()
  353.         except Exception as e:
  354.             messagebox.showerror("Error", str(e))
  355.  
  356.     # ---- Users tab ----
  357.     def build_users_tab(self, parent):
  358.         f = tk.Frame(parent, bg="#141414"); f.pack(fill=tk.BOTH, expand=True, padx=8, pady=8)
  359.         cols = ("user", "terminal", "host", "started")
  360.         self.user_tree = ttk.Treeview(f, columns=cols, show="headings")
  361.         for c, h in (("user", "User"), ("terminal", "Terminal"), ("host", "Host"), ("started", "Started")):
  362.             self.user_tree.heading(c, text=h); self.user_tree.column(c, width=220)
  363.         vsb = ttk.Scrollbar(f, orient="vertical", command=self.user_tree.yview); self.user_tree.configure(yscroll=vsb.set)
  364.         self.user_tree.pack(side=tk.LEFT, fill=tk.BOTH, expand=True); vsb.pack(side=tk.LEFT, fill=tk.Y)
  365.         self.refresh_users()
  366.  
  367.     def refresh_users(self):
  368.         self.user_tree.delete(*self.user_tree.get_children())
  369.         for u in psutil.users():
  370.             started = time.strftime("%Y-%m-%d %H:%M:%S", time.localtime(u.started)) if getattr(u, "started", None) else ""
  371.             self.user_tree.insert("", "end", values=(u.name, getattr(u, "terminal", ""), getattr(u, "host", ""), started))
  372.  
  373.     # ---- Details tab ----
  374.     def build_details_tab(self, parent):
  375.         f = tk.Frame(parent, bg="#141414"); f.pack(fill=tk.BOTH, expand=True, padx=8, pady=8)
  376.         cols = ("pid", "name", "cpu", "mem", "cmd")
  377.         self.details_tree = ttk.Treeview(f, columns=cols, show="headings")
  378.         for c, h in (("pid", "PID"), ("name", "Name"), ("cpu", "CPU%"), ("mem", "Mem%"), ("cmd", "Cmdline")):
  379.             self.details_tree.heading(c, text=h); self.details_tree.column(c, width=140 if c != "cmd" else 520)
  380.         vsb = ttk.Scrollbar(f, orient="vertical", command=self.details_tree.yview); self.details_tree.configure(yscroll=vsb.set)
  381.         self.details_tree.pack(side=tk.LEFT, fill=tk.BOTH, expand=True); vsb.pack(side=tk.LEFT, fill=tk.Y)
  382.         self.details_tree.bind("<Double-1>", lambda e: self.open_detail_window())
  383.  
  384.     def refresh_details(self):
  385.         self.details_tree.delete(*self.details_tree.get_children())
  386.         for p in psutil.process_iter(['pid', 'name', 'cpu_percent', 'memory_percent', 'cmdline']):
  387.             try:
  388.                 cmd = " ".join(p.info.get('cmdline') or [])
  389.                 self.details_tree.insert("", "end", values=(p.info.get('pid'), p.info.get('name') or "", f"{(p.info.get('cpu_percent') or 0):.1f}", f"{(p.info.get('memory_percent') or 0):.1f}", cmd))
  390.             except (psutil.NoSuchProcess, psutil.AccessDenied):
  391.                 continue
  392.  
  393.     def open_detail_window(self):
  394.         sel = self.details_tree.selection()
  395.         if not sel:
  396.             return
  397.         pid = int(self.details_tree.item(sel[0])['values'][0])
  398.         try:
  399.             p = psutil.Process(pid)
  400.             info = p.as_dict(attrs=['pid', 'name', 'exe', 'cmdline', 'cwd', 'username', 'create_time', 'status', 'cpu_percent', 'memory_percent', 'num_threads', 'io_counters'], ad_value="N/A")
  401.             txt = []
  402.             txt.append(f"PID: {info.get('pid')}"); txt.append(f"Name: {info.get('name')}"); txt.append(f"Exe: {info.get('exe')}")
  403.             txt.append(f"Cmdline: {' '.join(info.get('cmdline') or [])}"); txt.append(f"CWD: {info.get('cwd')}"); txt.append(f"User: {info.get('username')}")
  404.             txt.append(f"Started: {time.ctime(info.get('create_time')) if info.get('create_time') not in (None, 'N/A') else 'N/A'}"); txt.append(f"Status: {info.get('status')}")
  405.             txt.append(f"CPU%: {info.get('cpu_percent')}"); txt.append(f"Memory%: {info.get('memory_percent')}")
  406.             io = info.get('io_counters');
  407.             if io and io != "N/A":
  408.                 txt.append(f"I/O: read={getattr(io, 'read_bytes', 'N/A')}, write={getattr(io, 'write_bytes', 'N/A')}")
  409.             txt.append(f"Threads: {info.get('num_threads')}")
  410.             win = tk.Toplevel(self); win.title(f"Details - PID {pid}"); win.configure(bg="#141414")
  411.             st = ScrolledText(win, width=100, height=20, bg="#111111", fg="white"); st.pack(fill=tk.BOTH, expand=True, padx=8, pady=8); st.insert("1.0", "\n".join(txt))
  412.         except Exception as e:
  413.             messagebox.showerror("Error", str(e))
  414.  
  415.     # ---- Actions: End / Kill / xkill ----
  416.     def end_task(self):
  417.         pid = self._get_selected_pid_from_proc()
  418.         if not pid:
  419.             return
  420.         try:
  421.             psutil.Process(pid).terminate()
  422.             messagebox.showinfo("Terminated", f"Sent TERM to PID {pid}")
  423.             self.refresh_processes_now()
  424.         except Exception as e:
  425.             messagebox.showerror("Error", str(e))
  426.  
  427.     def kill_task(self):
  428.         pid = self._get_selected_pid_from_proc()
  429.         if not pid:
  430.             return
  431.         try:
  432.             psutil.Process(pid).kill()
  433.             messagebox.showinfo("Killed", f"Sent KILL to PID {pid}")
  434.             self.refresh_processes_now()
  435.         except Exception as e:
  436.             messagebox.showerror("Error", str(e))
  437.  
  438.     def xkill_mode(self):
  439.         # try system xkill first
  440.         if safe_run("which xkill"):
  441.             try:
  442.                 # launch xkill in background; user will click window to kill it
  443.                 subprocess.Popen(["xkill"])
  444.                 messagebox.showinfo("xkill", "xkill started. Click a window to kill it.")
  445.                 return
  446.             except Exception as e:
  447.                 messagebox.showerror("Error launching xkill", str(e))
  448.             return
  449.         # fallback: ask user to select a process to kill (already available) or use xdotool to get window under cursor
  450.         if safe_run("which xdotool"):
  451.             try:
  452.                 # instruct user to move cursor and press Enter
  453.                 messagebox.showinfo("xkill fallback", "Move mouse over window to kill, then press OK.")
  454.                 out = safe_run("xdotool getwindowfocus getwindowpid 2>/dev/null || xdotool getmouselocation --shell && xprop -root _NET_ACTIVE_WINDOW")
  455.                 # we will attempt to get window pid by window id under cursor - best-effort
  456.                 # simpler approach: call xdotool getwindowfocus getwindowpid
  457.                 pid_str = safe_run("xdotool getwindowfocus getwindowpid 2>/dev/null")
  458.                 if pid_str:
  459.                     pid = int(pid_str.strip())
  460.                     psutil.Process(pid).kill()
  461.                     messagebox.showinfo("Killed", f"Killed PID {pid} (from window under cursor)")
  462.                     return
  463.             except Exception:
  464.                 pass
  465.         messagebox.showinfo("xkill unavailable", "xkill and xdotool not available. Use End Task / Kill on selected process.")
  466.  
  467.     def _get_selected_pid_from_proc(self):
  468.         sel = self.proc_tree.selection()
  469.         if not sel:
  470.             messagebox.showwarning("No selection", "Select a process first in Processes tab.")
  471.             return None
  472.         try:
  473.             return int(self.proc_tree.item(sel[0])["values"][0])
  474.         except Exception:
  475.             return None
  476.  
  477.     # ---- UI update loop ----
  478.     def ui_update_loop(self):
  479.         if self.auto_var.get():
  480.             # update processes, details, users, performance displays
  481.             try:
  482.                 self.refresh_processes_now()
  483.                 self.refresh_details()
  484.                 self.refresh_users()
  485.                 self.update_performance_ui()
  486.                 # self.refresh_startup() # Removed as refresh_startup is called on its own
  487.             except Exception:
  488.                 pass
  489.         # schedule next
  490.         try:
  491.             ms = int(self.ui_interval_var.get())
  492.             if ms < 200:
  493.                 ms = UI_UPDATE_MS
  494.             self.after(ms, self.ui_update_loop)
  495.         except Exception:
  496.             self.after(UI_UPDATE_MS, self.ui_update_loop)
  497.  
  498.     def force_refresh(self):
  499.         self.refresh_processes_now(); self.refresh_details(); self.refresh_users(); self.update_performance_ui(); self.refresh_startup()
  500.  
  501.     # ---- Performance UI updater (reads sampler) ----
  502.     def update_performance_ui(self):
  503.         s = self.sampler
  504.         with s.lock:
  505.             cpu = s.sampled.get('cpu', 0)
  506.             mem = s.sampled.get('mem', 0)
  507.             rx_rate = s.sampled.get('rx_rate', 0)
  508.             tx_rate = s.sampled.get('tx_rate', 0)
  509.             cpu_hist = list(s.cpu_hist)
  510.             mem_hist = list(s.mem_hist)
  511.             rx_hist = list(s.net_rx_hist)
  512.             tx_hist = list(s.net_tx_hist)
  513.             disk_r = dict(s.disk_read_rate)
  514.             disk_w = dict(s.disk_write_rate)
  515.             ninfo = list(s.nvidia_info)
  516.         # CPU stats
  517.         try:
  518.             freq = psutil.cpu_freq()
  519.             freq_text = f"Freq: {freq.current:.0f} MHz" if freq else "Freq: N/A"
  520.         except Exception:
  521.             freq_text = "Freq: N/A"
  522.         self.cpu_name_lbl.config(text=cpu_name())
  523.         self.cpu_freq_lbl.config(text=freq_text)
  524.         self.cpu_bar['value'] = cpu
  525.         # draw cpu chart
  526.         self.cpu_ax.cla()
  527.         self.cpu_ax.plot(cpu_hist, color='cyan')
  528.         self.cpu_ax.set_ylim(0, 100)
  529.         self.cpu_ax.set_facecolor('#141414'); self.cpu_ax.tick_params(colors='white')
  530.         self.cpu_canvas.draw_idle()
  531.         # GPU
  532.         if ninfo:
  533.             g = ninfo[0]
  534.             self.gpu_text.config(text=f"{g['name']} | Util {g['util']:.0f}% | VRAM {g['mem_used']}/{g['mem_total']} MiB")
  535.             self.gpu_bar['value'] = g['util']
  536.             self.gpu_ax.cla(); self.gpu_ax.plot([g['util']] * len(cpu_hist), color='magenta'); self.gpu_ax.set_ylim(0, 100); self.gpu_canvas.draw_idle()
  537.             # detailed text
  538.             self.gpu_info_text.delete('1.0', tk.END)
  539.             for g in ninfo:
  540.                 self.gpu_info_text.insert(tk.END, f"{g['index']}: {g['name']} - Util {g['util']:.0f}% | Mem {g['mem_used']}/{g['mem_total']} MiB\n")
  541.         else:
  542.             self.gpu_text.config(text="GPU: not available or unsupported"); self.gpu_bar['value'] = 0
  543.         # Memory
  544.         self.mem_lbl.config(text=f"Memory: {mem:.1f}%")
  545.         self.mem_bar['value'] = mem
  546.         self.mem_ax.cla(); self.mem_ax.plot(mem_hist, color='lime'); self.mem_ax.set_ylim(0, 100); self.mem_canvas.draw_idle()
  547.         # Disks - show partitions and per-device rates
  548.         self.disk_tree.delete(*self.disk_tree.get_children())
  549.         parts = psutil.disk_partitions(all=False)
  550.         seen_mounts = set()
  551.         for part in parts:
  552.             try:
  553.                 if part.mountpoint in seen_mounts:
  554.                     continue
  555.                 seen_mounts.add(part.mountpoint)
  556.                 usage = psutil.disk_usage(part.mountpoint)
  557.                 dev = part.device
  558.                 model = self.disk_models.get(dev, "")
  559.                 key = os.path.basename(dev)
  560.                 r = disk_r.get(key, 0.0); w = disk_w.get(key, 0.0)
  561.                 self.disk_tree.insert("", "end", values=(dev, part.mountpoint, model, size_fmt(usage.total), size_fmt(usage.used), size_fmt(usage.free), f"{usage.percent:.1f}%", size_fmt(r) + "/s", size_fmt(w) + "/s"))
  562.             except Exception:
  563.                 continue
  564.         # disk chart: top read+write combined
  565.         top_r = sorted(disk_r.items(), key=lambda kv: kv[1], reverse=True)[:5]
  566.         top_w = sorted(disk_w.items(), key=lambda kv: kv[1], reverse=True)[:5]
  567.         labels = [k for k, _ in top_r] or ['-']
  568.         values = [v for _, v in top_r] or [0]
  569.         self.disk_ax.cla()
  570.         self.disk_ax.bar(range(len(values)), [v / 1024.0 for v in values])
  571.         self.disk_ax.set_ylabel("KB/s"); self.disk_ax.set_xticks(range(len(values))); self.disk_ax.set_xticklabels(labels, rotation=30, color='white')
  572.         self.disk_ax.set_facecolor('#141414'); self.disk_canvas.draw_idle()
  573.         # Network
  574.         self.net_lbl.config(text=f"RX: {size_fmt(rx_rate)}/s | TX: {size_fmt(tx_rate)}/s")
  575.         net_series = [(rx + tx) / 1024.0 for rx, tx in zip(rx_hist, tx_hist)]
  576.         self.net_ax.cla(); self.net_ax.plot([r / 1024.0 for r in rx_hist], label='RX KB/s'); self.net_ax.plot([t / 1024.0 for t in tx_hist], label='TX KB/s')
  577.         self.net_ax.legend(loc='upper right', facecolor='#141414', labelcolor='white'); self.net_ax.set_facecolor('#141414'); self.net_canvas.draw_idle()
  578.  
  579.     # ---- Misc refresh helpers ----
  580.     def refresh_startup(self):
  581.         try:
  582.             self.start_tree.delete(*self.start_tree.get_children())
  583.             home = os.path.expanduser("~")
  584.             pths = [os.path.join(home, ".config", "autostart"), "/etc/xdg/autostart"]
  585.             for p in pths:
  586.                 if os.path.isdir(p):
  587.                     for fn in sorted(os.listdir(p)):
  588.                         if fn.endswith(".desktop") or fn.endswith(".desktop.disabled"):
  589.                             path = os.path.join(p, fn)
  590.                             name = ""; execv = ""; enabled = "Yes"
  591.                             try:
  592.                                 with open(path, "r", errors="ignore") as f:
  593.                                     for L in f:
  594.                                         if "=" in L:
  595.                                             k, v = L.split("=", 1); k = k.strip().lower(); v = v.strip()
  596.                                             if k == "name":
  597.                                                 name = v
  598.                                             if k == "exec":
  599.                                                 execv = v
  600.                                             if path.endswith(".disabled"):
  601.                                                 enabled = "No"
  602.                             except Exception:
  603.                                 pass
  604.                             self.start_tree.insert("", "end", values=(name or fn, execv, path, enabled))
  605.             # systemd user/system enabled
  606.             out = safe_run("systemctl --user list-unit-files --type=service --state=enabled 2>/dev/null")
  607.             if out:
  608.                 for line in out.splitlines():
  609.                     if line.strip() and not line.startswith("UNIT"):
  610.                         svc = line.split()[0]; self.start_tree.insert("", "end", values=(svc, "systemd --user", "(systemd user)", "Yes"))
  611.             out2 = safe_run("systemctl list-unit-files --type=service --state=enabled 2>/dev/null")
  612.             if out2:
  613.                 for line in out2.splitlines():
  614.                     if line.strip() and not line.startswith("UNIT"):
  615.                         svc = line.split()[0]; self.start_tree.insert("", "end", values=(svc, "systemd", "(system)", "Yes"))
  616.         except Exception:
  617.             pass
  618.  
  619.     def refresh_users(self):
  620.         try:
  621.             self.user_tree.delete(*self.user_tree.get_children())
  622.             for u in psutil.users():
  623.                 started = time.strftime("%Y-%m-%d %H:%M:%S", time.localtime(u.started)) if getattr(u, "started", None) else ""
  624.                 self.user_tree.insert("", "end", values=(u.name, getattr(u, "terminal", ""), getattr(u, "host", ""), started))
  625.         except Exception:
  626.             pass
  627.  
  628.     # Fix recursion: implement actual refresh wrapper
  629.     def refresh_processes_now(self):
  630.         try:
  631.             sel_pid = None; sel = self.proc_tree.selection()
  632.             if sel:
  633.                 sel_pid = self.proc_tree.item(sel[0])["values"][0]
  634.             self.proc_tree.delete(*self.proc_tree.get_children())
  635.             keyword = self.filter_var.get().lower().strip() if hasattr(self, 'filter_var') else ""
  636.             # prime cpu
  637.             for p in psutil.process_iter():
  638.                 try:
  639.                     p.cpu_percent(interval=None)
  640.                 except Exception:
  641.                     pass
  642.             for p in psutil.process_iter(['pid', 'name', 'username', 'cpu_percent', 'memory_percent', 'status']):
  643.                 try:
  644.                     info = p.info
  645.                     name = (info.get('name') or "")
  646.                     if keyword and keyword not in name.lower():
  647.                         continue
  648.                     self.proc_tree.insert("", "end", values=(info.get('pid'), name, info.get('username') or "", f"{(info.get('cpu_percent') or 0):.1f}", f"{(info.get('memory_percent') or 0):.1f}", info.get('status') or ""))
  649.                 except (psutil.NoSuchProcess, psutil.AccessDenied):
  650.                     continue
  651.             if sel_pid:
  652.                 for iid in self.proc_tree.get_children():
  653.                     if str(self.proc_tree.item(iid)["values"][0]) == str(sel_pid):
  654.                         self.proc_tree.selection_set(iid); break
  655.         except Exception:
  656.             pass
  657.  
  658.     def set_ui_interval(self):
  659.         try:
  660.             v = int(self.ui_interval_var.get())
  661.             if v < 200:
  662.                 raise ValueError
  663.             # schedule uses variable in ui_update_loop
  664.             messagebox.showinfo("Set", f"UI interval set to {v} ms")
  665.         except Exception:
  666.             messagebox.showerror("Invalid", "Enter integer >=200")
  667.  
  668.     def show_selected_details(self):
  669.         sel = self.proc_tree.selection()
  670.         if not sel:
  671.             messagebox.showwarning("No selection", "Select a process first.")
  672.             return
  673.         pid = int(self.proc_tree.item(sel[0])["values"][0])
  674.         try:
  675.             p = psutil.Process(pid)
  676.             info = p.as_dict(attrs=['pid', 'name', 'exe', 'cmdline', 'cwd', 'username', 'create_time', 'status', 'cpu_percent', 'memory_percent', 'num_threads', 'io_counters'], ad_value="N/A")
  677.             lines = []
  678.             lines.append(f"PID: {info.get('pid')}")
  679.             lines.append(f"Name: {info.get('name')}")
  680.             lines.append(f"Exe: {info.get('exe')}")
  681.             lines.append(f"Cmdline: {' '.join(info.get('cmdline') or [])}")
  682.             lines.append(f"CWD: {info.get('cwd')}")
  683.             lines.append(f"User: {info.get('username')}")
  684.             lines.append(f"Started: {time.ctime(info.get('create_time')) if info.get('create_time') not in (None, 'N/A') else 'N/A'}")
  685.             lines.append(f"Status: {info.get('status')}")
  686.             lines.append(f"CPU%: {info.get('cpu_percent')}")
  687.             lines.append(f"Memory%: {info.get('memory_percent')}")
  688.             io = info.get('io_counters')
  689.             if io and io != "N/A":
  690.                 lines.append(f"I/O: read={getattr(io, 'read_bytes', 'N/A')}, write={getattr(io, 'write_bytes', 'N/A')}")
  691.             lines.append(f"Threads: {info.get('num_threads')}")
  692.             win = tk.Toplevel(self); win.title(f"Details - PID {pid}"); win.configure(bg="#141414")
  693.             st = ScrolledText(win, width=100, height=20, bg="#111111", fg="white"); st.pack(fill=tk.BOTH, expand=True, padx=8, pady=8); st.insert("1.0", "\n".join(lines))
  694.         except Exception as e:
  695.             messagebox.showerror("Error", str(e))
  696.  
  697.     def on_close(self):
  698.         try:
  699.             self.sampler.stop()
  700.         except Exception:
  701.             pass
  702.         self.destroy()
  703.  
  704. # ---- Run ----
  705. def main():
  706.     try:
  707.         import psutil
  708.     except Exception:
  709.         print("psutil missing. Install: sudo apt install python3-psutil")
  710.         return
  711.     app = TaskManagerApp()
  712.     app.mainloop()
  713.  
  714. if __name__ == "__main__":
  715.     main()
Advertisement
Comments
Add Comment
Please, Sign In to add comment