diff --git a/systats.py b/systats.py index ddee9e3..73677fb 100755 --- a/systats.py +++ b/systats.py @@ -26,6 +26,132 @@ AMD_GPU_PATH = "/sys/class/drm" REFRESH_INTERVAL = 1.0 +# ─── i18n ─────────────────────────────────────────────────────────── + +LANGS = ["en", "pl"] +current_lang = "en" + +TRANSLATIONS = { + # Header + "up": {"en": "Up", "pl": "Czas"}, + "load": {"en": "Load", "pl": "Obc."}, + "procs": {"en": "Procs", "pl": "Proc"}, + "run": {"en": "run", "pl": "dzia."}, + # Sections + "memory": {"en": "MEMORY", "pl": "PAMIEC"}, + "network": {"en": "NETWORK", "pl": "SIEC"}, + "disk": {"en": "DISK", "pl": "DYSK"}, + "gpu": {"en": "GPU", "pl": "GPU"}, + "temps": {"en": "TEMPS", "pl": "TEMP"}, + "processes": {"en": "PROCESSES", "pl": "PROCESY"}, + # Disk + "read": {"en": "Read", "pl": "Odczyt"}, + "write": {"en": "Write", "pl": "Zapis"}, + # Network + "total": {"en": "Total", "pl": "Suma"}, + # Process table + "filter": {"en": "filter", "pl": "filtr"}, + "name": {"en": "NAME", "pl": "NAZWA"}, + # Process hints + "hint_sort": {"en": "sort", "pl": "sort."}, + "hint_reverse": {"en": "reverse", "pl": "odwroc"}, + "hint_filter": {"en": "filter", "pl": "filtr"}, + "hint_signals": {"en": "signals", "pl": "sygnaly"}, + "hint_details": {"en": "details", "pl": "szczeg."}, + "hint_nice": {"en": "nice", "pl": "priorytet"}, + "hint_quit": {"en": "quit", "pl": "wyjscie"}, + # Kill menu + "send_signal_to": {"en": "Send signal to", "pl": "Wyslij sygnal do"}, + "send": {"en": "send", "pl": "wyslij"}, + "cancel": {"en": "cancel", "pl": "anuluj"}, + "apply": {"en": "apply", "pl": "zastosuj"}, + # Details + "proc_details": {"en": "Process Details", "pl": "Szczegoly procesu"}, + "created": {"en": "Created", "pl": "Utworzony"}, + "connections": {"en": "Connections", "pl": "Polaczenia"}, + "close": {"en": "close", "pl": "zamknij"}, + # Renice + "nice_value": {"en": "Nice value (-20..19)", "pl": "Priorytet (-20..19)"}, + # Filter bar + "filter_label": {"en": "Filter", "pl": "Filtr"}, + # Status messages + "no_proc_selected": {"en": "No process selected", "pl": "Nie wybrano procesu"}, + "sent_to": {"en": "Sent {sig} to {name} (PID {pid})", "pl": "Wyslano {sig} do {name} (PID {pid})"}, + "proc_not_found": {"en": "Process {pid} not found", "pl": "Proces {pid} nie znaleziony"}, + "perm_denied": {"en": "Permission denied: {name} (PID {pid})", "pl": "Brak uprawnien: {name} (PID {pid})"}, + "set_nice": {"en": "Set nice {val} for {name} (PID {pid})", "pl": "Ustawiono priorytet {val} dla {name} (PID {pid})"}, + "invalid_nice": {"en": "Invalid nice value", "pl": "Nieprawidlowy priorytet"}, + # Help + "help_title": {"en": "SYSTATS - Keyboard Shortcuts", "pl": "SYSTATS - Skroty klawiszowe"}, + "help_close": {"en": "Press h, F1 or ? to close", "pl": "Nacisnij h, F1 lub ? aby zamknac"}, + "help_nav": {"en": "NAVIGATION", "pl": "NAWIGACJA"}, + "help_down": {"en": "Move cursor down", "pl": "Kursor w dol"}, + "help_up": {"en": "Move cursor up", "pl": "Kursor w gore"}, + "help_pgdn": {"en": "Scroll down 20", "pl": "Przewin 20 w dol"}, + "help_pgup": {"en": "Scroll up 20", "pl": "Przewin 20 w gore"}, + "help_home": {"en": "Go to top", "pl": "Na poczatek"}, + "help_end": {"en": "Go to bottom", "pl": "Na koniec"}, + "help_sorting": {"en": "SORTING", "pl": "SORTOWANIE"}, + "help_sort_cpu": {"en": "Sort by CPU%", "pl": "Sortuj wg CPU%"}, + "help_sort_mem": {"en": "Sort by MEM%", "pl": "Sortuj wg MEM%"}, + "help_sort_pid": {"en": "Sort by PID", "pl": "Sortuj wg PID"}, + "help_sort_name": {"en": "Sort by name", "pl": "Sortuj wg nazwy"}, + "help_reverse": {"en": "Reverse sort order", "pl": "Odwroc sortowanie"}, + "help_proc": {"en": "PROCESS MGMT", "pl": "ZARZ. PROCESAMI"}, + "help_sigterm": {"en": "Send SIGTERM to process", "pl": "Wyslij SIGTERM"}, + "help_sigkill": {"en": "Send SIGKILL to process", "pl": "Wyslij SIGKILL"}, + "help_sigmenu": {"en": "Signal menu (choose signal)", "pl": "Menu sygnalow"}, + "help_nice_pos": {"en": "Renice process", "pl": "Zmien priorytet"}, + "help_nice_neg": {"en": "Renice process (negative)", "pl": "Zmien priorytet (ujemny)"}, + "help_details": {"en": "Process details", "pl": "Szczegoly procesu"}, + "help_filter": {"en": "Filter processes", "pl": "Filtruj procesy"}, + "help_general": {"en": "GENERAL", "pl": "OGOLNE"}, + "help_help": {"en": "Toggle this help", "pl": "Pokaz/ukryj pomoc"}, + "help_lang": {"en": "Switch language (EN/PL)", "pl": "Zmien jezyk (EN/PL)"}, + "help_quit": {"en": "Quit", "pl": "Wyjscie"}, +} + + +def t(key, **kwargs): + """Get translated string.""" + s = TRANSLATIONS.get(key, {}).get(current_lang, key) + if kwargs: + s = s.format(**kwargs) + return s + + +def get_help_lines(): + return [ + (t("help_nav"), ""), + ("j / Down", t("help_down")), + ("k / Up", t("help_up")), + ("PgDn", t("help_pgdn")), + ("PgUp", t("help_pgup")), + ("Home", t("help_home")), + ("End", t("help_end")), + ("", ""), + (t("help_sorting"), ""), + ("c", t("help_sort_cpu")), + ("m", t("help_sort_mem")), + ("p", t("help_sort_pid")), + ("n", t("help_sort_name")), + ("r", t("help_reverse")), + ("", ""), + (t("help_proc"), ""), + ("T", t("help_sigterm")), + ("K", t("help_sigkill")), + ("F9", t("help_sigmenu")), + ("+ / F8", t("help_nice_pos")), + ("- / F7", t("help_nice_neg")), + ("d / F5 / Enter", t("help_details")), + ("/ / f", t("help_filter")), + ("", ""), + (t("help_general"), ""), + ("h / F1 / ?", t("help_help")), + ("L", t("help_lang")), + ("q / Q / Esc", t("help_quit")), + ] + # History length for sparklines HIST_LEN = 60 @@ -437,37 +563,6 @@ C_POPUP_BG = 12 C_POPUP_HL = 13 -HELP_LINES = [ - ("NAVIGATION", ""), - ("j / Down", "Move cursor down"), - ("k / Up", "Move cursor up"), - ("PgDn", "Scroll down 20"), - ("PgUp", "Scroll up 20"), - ("Home", "Go to top"), - ("End", "Go to bottom"), - ("", ""), - ("SORTING", ""), - ("c", "Sort by CPU%"), - ("m", "Sort by MEM%"), - ("p", "Sort by PID"), - ("n", "Sort by name"), - ("r", "Reverse sort order"), - ("", ""), - ("PROCESS MGMT", ""), - ("T", "Send SIGTERM to process"), - ("K", "Send SIGKILL to process"), - ("F9", "Signal menu (choose signal)"), - ("+ / F8", "Renice process"), - ("- / F7", "Renice process (negative)"), - ("d / F5 / Enter", "Process details"), - ("/ / f", "Filter processes"), - ("", ""), - ("GENERAL", ""), - ("h / F1 / ?", "Toggle this help"), - ("q / Q / Esc", "Quit"), -] - - class ProcessManager: """Interactive process management: kill, nice, filter, details.""" @@ -515,36 +610,36 @@ class ProcessManager: def kill_selected(self, processes, sig): proc = self.get_selected_proc(processes) if not proc: - self._set_status("No process selected") + self._set_status(t("no_proc_selected")) return pid = proc.get("pid") name = proc.get("name", "?") try: os.kill(pid, sig) sig_name = signal.Signals(sig).name - self._set_status(f"Sent {sig_name} to {name} (PID {pid})") + self._set_status(t("sent_to", sig=sig_name, name=name, pid=pid)) except ProcessLookupError: - self._set_status(f"Process {pid} not found") + self._set_status(t("proc_not_found", pid=pid)) except PermissionError: - self._set_status(f"Permission denied: {name} (PID {pid})") + self._set_status(t("perm_denied", name=name, pid=pid)) except Exception as e: self._set_status(f"Error: {e}") def renice_selected(self, processes, nice_val): proc = self.get_selected_proc(processes) if not proc: - self._set_status("No process selected") + self._set_status(t("no_proc_selected")) return pid = proc.get("pid") name = proc.get("name", "?") try: p = psutil.Process(pid) p.nice(nice_val) - self._set_status(f"Set nice {nice_val} for {name} (PID {pid})") + self._set_status(t("set_nice", val=nice_val, name=name, pid=pid)) except psutil.NoSuchProcess: - self._set_status(f"Process {pid} not found") + self._set_status(t("proc_not_found", pid=pid)) except psutil.AccessDenied: - self._set_status(f"Permission denied: {name} (PID {pid})") + self._set_status(t("perm_denied", name=name, pid=pid)) except Exception as e: self._set_status(f"Error: {e}") @@ -665,9 +760,9 @@ class Renderer: d = self.data hostname = os.uname().nodename kernel = os.uname().release - header = f" SYSTATS | {hostname} | Linux {kernel} | Up: {d.uptime} " - header += f"| Load: {d.load_avg[0]:.2f} {d.load_avg[1]:.2f} {d.load_avg[2]:.2f} " - header += f"| Procs: {d.proc_count} ({d.proc_running} run) " + header = f" SYSTATS | {hostname} | Linux {kernel} | {t('up')}: {d.uptime} " + header += f"| {t('load')}: {d.load_avg[0]:.2f} {d.load_avg[1]:.2f} {d.load_avg[2]:.2f} " + header += f"| {t('procs')}: {d.proc_count} ({d.proc_running} {t('run')}) " brand = " Electric Entropy Lab " header = header.ljust(w - len(brand))[:w - len(brand)] safe_addstr(self.scr, y, 0, header, curses.color_pair(C_HEADER) | curses.A_BOLD) @@ -715,7 +810,7 @@ class Renderer: def _draw_memory(self, y, x, w): d = self.data box_h = 5 - draw_box(self.scr, y, x, box_h, w, "MEMORY") + draw_box(self.scr, y, x, box_h, w, t("memory")) bar_w = w - 4 iy = y + 1 @@ -742,7 +837,7 @@ class Renderer: def _draw_network(self, y, x, w): d = self.data box_h = 6 - draw_box(self.scr, y, x, box_h, w, "NETWORK") + draw_box(self.scr, y, x, box_h, w, t("network")) iy = y + 1 bar_w = w - 4 @@ -764,7 +859,7 @@ class Renderer: # Net total total = d.prev_net safe_addstr(self.scr, iy, x + 2, - f"Total RX: {fmt_bytes_short(total.bytes_recv)} TX: {fmt_bytes_short(total.bytes_sent)}", + f"{t('total')} RX: {fmt_bytes_short(total.bytes_recv)} TX: {fmt_bytes_short(total.bytes_sent)}", curses.color_pair(C_DIM)) iy += 1 @@ -790,12 +885,12 @@ class Renderer: d = self.data num_parts = min(len(d.partitions), 4) box_h = 3 + num_parts * 2 - draw_box(self.scr, y, x, box_h, w, "DISK") + draw_box(self.scr, y, x, box_h, w, t("disk")) iy = y + 1 bar_w = w - 4 # IO rates - io_str = f"Read: {fmt_bytes_short(d.disk_read_rate)}/s Write: {fmt_bytes_short(d.disk_write_rate)}/s" + io_str = f"{t('read')}: {fmt_bytes_short(d.disk_read_rate)}/s {t('write')}: {fmt_bytes_short(d.disk_write_rate)}/s" safe_addstr(self.scr, iy, x + 2, io_str, curses.color_pair(C_ACCENT)) iy += 1 @@ -814,7 +909,7 @@ class Renderer: def _draw_gpu(self, y, x, w): d = self.data box_h = 2 + len(d.gpus) * 4 - draw_box(self.scr, y, x, box_h, w, "GPU") + draw_box(self.scr, y, x, box_h, w, t("gpu")) iy = y + 1 bar_w = w - 4 @@ -858,7 +953,7 @@ class Renderer: return y box_h = min(2 + len(entries), 8) - draw_box(self.scr, y, x, box_h, w, "TEMPS") + draw_box(self.scr, y, x, box_h, w, t("temps")) iy = y + 1 cols = max(1, w // 30) @@ -893,13 +988,13 @@ class Renderer: processes = pm.filtered_processes(d.processes) # Title with keybindings - filter_info = f" filter: '{pm.filter_text}'" if pm.filter_text else "" - title = f"PROCESSES [{d.process_sort}] F9:kill F7/F8:nice F5:details /:filter{filter_info}" + filter_info = f" {t('filter')}: '{pm.filter_text}'" if pm.filter_text else "" + title = f"{t('processes')} [{d.process_sort}] F9:kill F7/F8:{t('hint_nice')} F5:{t('hint_details')} /:{t('hint_filter')}{filter_info}" draw_box(self.scr, y, x, box_h, w, title) iy = y + 1 # Header - hdr = f"{'PID':>7s} {'USER':<10s} {'CPU%':>6s} {'MEM%':>6s} {'THR':>4s} {'RES':>8s} {'S':1s} {'NAME'}" + hdr = f"{'PID':>7s} {'USER':<10s} {'CPU%':>6s} {'MEM%':>6s} {'THR':>4s} {'RES':>8s} {'S':1s} {t('name')}" safe_addstr(self.scr, iy, x + 1, hdr[:w - 2], curses.color_pair(C_HEADER)) iy += 1 @@ -946,7 +1041,7 @@ class Renderer: safe_addstr(self.scr, status_y, x + 2, status[:w - 4], curses.color_pair(C_CRIT) | curses.A_BOLD) else: - hint = "q:quit c/m/p/n:sort r:reverse /:filter K:KILL T:TERM F9:signals d:details +/-:nice" + hint = f"q:{t('hint_quit')} c/m/p/n:{t('hint_sort')} r:{t('hint_reverse')} /:{t('hint_filter')} K:KILL T:TERM F9:{t('hint_signals')} d:{t('hint_details')} +/-:{t('hint_nice')}" safe_addstr(self.scr, status_y, x + 2, hint[:w - 4], curses.color_pair(C_DIM)) # Scrollbar indicator @@ -982,7 +1077,7 @@ class Renderer: safe_addstr(self.scr, my + row, mx, " " * menu_w, curses.color_pair(C_POPUP_BG)) # Title - title = f" Send signal to {proc_name}({proc_pid}) " + title = f" {t('send_signal_to')} {proc_name}({proc_pid}) " safe_addstr(self.scr, my, mx, title[:menu_w].center(menu_w), curses.color_pair(C_POPUP_HL) | curses.A_BOLD) safe_addstr(self.scr, my + 1, mx, "\u2500" * menu_w, curses.color_pair(C_POPUP_BG)) @@ -996,7 +1091,8 @@ class Renderer: safe_addstr(self.scr, row_y, mx + 1, f" {name} ".ljust(menu_w - 2), curses.color_pair(C_POPUP_BG)) - safe_addstr(self.scr, my + menu_h - 1, mx, " Enter:send Esc:cancel ".center(menu_w), + safe_addstr(self.scr, my + menu_h - 1, mx, + f" Enter:{t('send')} Esc:{t('cancel')} ".center(menu_w), curses.color_pair(C_POPUP_BG) | curses.A_DIM) def _draw_details_popup(self, y, x, w, avail_h): @@ -1015,7 +1111,7 @@ class Renderer: for row in range(popup_h): safe_addstr(self.scr, my + row, mx, " " * popup_w, curses.color_pair(C_POPUP_BG)) - title = f" Process Details: {details['name']} (PID {details['pid']}) " + title = f" {t('proc_details')}: {details['name']} (PID {details['pid']}) " safe_addstr(self.scr, my, mx, title[:popup_w].center(popup_w), curses.color_pair(C_POPUP_HL) | curses.A_BOLD) safe_addstr(self.scr, my + 1, mx, "\u2500" * popup_w, curses.color_pair(C_POPUP_BG)) @@ -1027,8 +1123,8 @@ class Renderer: f" CPU: {details['cpu_percent']:.1f}% MEM: {details['memory_percent']:.1f}%", f" RSS: {details['rss']} VMS: {details['vms']}", f" IO R: {details['io_read']} IO W: {details['io_write']}", - f" FDs: {details['num_fds']} Connections: {details['connections']}", - f" Created: {details['create_time']}", + f" FDs: {details['num_fds']} {t('connections')}: {details['connections']}", + f" {t('created')}: {details['create_time']}", f" Exe: {details.get('exe', '?')}", f" CWD: {details.get('cwd', '?')}", f" Cmd: {details.get('cmdline', '?')}", @@ -1038,7 +1134,8 @@ class Renderer: safe_addstr(self.scr, my + 2 + i, mx, line[:popup_w].ljust(popup_w), curses.color_pair(C_POPUP_BG)) - safe_addstr(self.scr, my + popup_h - 1, mx, " Press Esc or d to close ".center(popup_w), + safe_addstr(self.scr, my + popup_h - 1, mx, + f" Esc/d: {t('close')} ".center(popup_w), curses.color_pair(C_POPUP_BG) | curses.A_DIM) def _draw_nice_dialog(self, y, x, w, avail_h): @@ -1059,15 +1156,16 @@ class Renderer: curses.color_pair(C_POPUP_HL) | curses.A_BOLD) safe_addstr(self.scr, my + 1, mx, "\u2500" * popup_w, curses.color_pair(C_POPUP_BG)) safe_addstr(self.scr, my + 2, mx + 2, - f"Nice value (-20..19): {pm.nice_value}_".ljust(popup_w - 4), + f"{t('nice_value')}: {pm.nice_value}_".ljust(popup_w - 4), curses.color_pair(C_POPUP_BG)) safe_addstr(self.scr, my + 4, mx, - " Enter:apply Esc:cancel ".center(popup_w), + f" Enter:{t('apply')} Esc:{t('cancel')} ".center(popup_w), curses.color_pair(C_POPUP_BG) | curses.A_DIM) def _draw_help(self, h, w): + help_lines = get_help_lines() popup_w = min(w - 4, 52) - popup_h = min(h - 2, len(HELP_LINES) + 4) + popup_h = min(h - 2, len(help_lines) + 4) mx = (w - popup_w) // 2 my = (h - popup_h) // 2 @@ -1076,12 +1174,12 @@ class Renderer: safe_addstr(self.scr, my + row, mx, " " * popup_w, curses.color_pair(C_POPUP_BG)) # Title - title = " SYSTATS - Keyboard Shortcuts " + title = f" {t('help_title')} " safe_addstr(self.scr, my, mx, title.center(popup_w), curses.color_pair(C_POPUP_HL) | curses.A_BOLD) safe_addstr(self.scr, my + 1, mx, "\u2500" * popup_w, curses.color_pair(C_POPUP_BG)) - for i, (key, desc) in enumerate(HELP_LINES): + for i, (key, desc) in enumerate(help_lines): row_y = my + 2 + i if row_y >= my + popup_h - 1: break @@ -1097,12 +1195,12 @@ class Renderer: curses.color_pair(C_POPUP_BG)) safe_addstr(self.scr, my + popup_h - 1, mx, - " Press h, F1 or ? to close ".center(popup_w), + f" {t('help_close')} ".center(popup_w), curses.color_pair(C_POPUP_BG) | curses.A_DIM) def _draw_filter_bar(self, y, x, w): pm = self.proc_mgr - bar = f" Filter: {pm.filter_text}_ " + bar = f" {t('filter_label')}: {pm.filter_text}_ " safe_addstr(self.scr, y, x + 1, bar.ljust(w - 2)[:w - 2], curses.color_pair(C_POPUP_HL) | curses.A_BOLD) @@ -1165,7 +1263,7 @@ def main(stdscr): val = max(-20, min(19, val)) pm.renice_selected(processes, val) except ValueError: - pm._set_status("Invalid nice value") + pm._set_status(t("invalid_nice")) pm.show_nice_dialog = False pm.nice_value = "" elif key in (curses.KEY_BACKSPACE, 127, 8): @@ -1201,6 +1299,10 @@ def main(stdscr): break elif key in (ord("h"), ord("?"), curses.KEY_F1): renderer.show_help = True + elif key == ord("L"): + global current_lang + idx = LANGS.index(current_lang) + current_lang = LANGS[(idx + 1) % len(LANGS)] elif key == ord("c"): data.process_sort = "cpu" data.process_sort_reverse = True @@ -1260,6 +1362,12 @@ def main(stdscr): if __name__ == "__main__": + import argparse + parser = argparse.ArgumentParser(description="systats - Terminal system monitor | Electric Entropy Lab") + parser.add_argument("--lang", choices=LANGS, default="en", + help="UI language: en (English) or pl (Polski)") + args = parser.parse_args() + current_lang = args.lang try: curses.wrapper(main) except KeyboardInterrupt: