Add PL/EN i18n with runtime toggle (L key) and --lang flag

All UI strings translated: section titles, popups, status messages,
help screen, hints. Switch language live with L or start with --lang pl.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
lukasz@orzechowski.eu
2026-03-24 00:39:28 +01:00
parent e0d3a48240
commit a40332b569

View File

@@ -26,6 +26,132 @@ AMD_GPU_PATH = "/sys/class/drm"
REFRESH_INTERVAL = 1.0 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 # History length for sparklines
HIST_LEN = 60 HIST_LEN = 60
@@ -437,37 +563,6 @@ C_POPUP_BG = 12
C_POPUP_HL = 13 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: class ProcessManager:
"""Interactive process management: kill, nice, filter, details.""" """Interactive process management: kill, nice, filter, details."""
@@ -515,36 +610,36 @@ class ProcessManager:
def kill_selected(self, processes, sig): def kill_selected(self, processes, sig):
proc = self.get_selected_proc(processes) proc = self.get_selected_proc(processes)
if not proc: if not proc:
self._set_status("No process selected") self._set_status(t("no_proc_selected"))
return return
pid = proc.get("pid") pid = proc.get("pid")
name = proc.get("name", "?") name = proc.get("name", "?")
try: try:
os.kill(pid, sig) os.kill(pid, sig)
sig_name = signal.Signals(sig).name 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: except ProcessLookupError:
self._set_status(f"Process {pid} not found") self._set_status(t("proc_not_found", pid=pid))
except PermissionError: 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: except Exception as e:
self._set_status(f"Error: {e}") self._set_status(f"Error: {e}")
def renice_selected(self, processes, nice_val): def renice_selected(self, processes, nice_val):
proc = self.get_selected_proc(processes) proc = self.get_selected_proc(processes)
if not proc: if not proc:
self._set_status("No process selected") self._set_status(t("no_proc_selected"))
return return
pid = proc.get("pid") pid = proc.get("pid")
name = proc.get("name", "?") name = proc.get("name", "?")
try: try:
p = psutil.Process(pid) p = psutil.Process(pid)
p.nice(nice_val) 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: except psutil.NoSuchProcess:
self._set_status(f"Process {pid} not found") self._set_status(t("proc_not_found", pid=pid))
except psutil.AccessDenied: 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: except Exception as e:
self._set_status(f"Error: {e}") self._set_status(f"Error: {e}")
@@ -665,9 +760,9 @@ class Renderer:
d = self.data d = self.data
hostname = os.uname().nodename hostname = os.uname().nodename
kernel = os.uname().release kernel = os.uname().release
header = f" SYSTATS | {hostname} | Linux {kernel} | Up: {d.uptime} " header = f" SYSTATS | {hostname} | Linux {kernel} | {t('up')}: {d.uptime} "
header += f"| Load: {d.load_avg[0]:.2f} {d.load_avg[1]:.2f} {d.load_avg[2]:.2f} " header += f"| {t('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"| {t('procs')}: {d.proc_count} ({d.proc_running} {t('run')}) "
brand = " Electric Entropy Lab " brand = " Electric Entropy Lab "
header = header.ljust(w - len(brand))[:w - len(brand)] header = header.ljust(w - len(brand))[:w - len(brand)]
safe_addstr(self.scr, y, 0, header, curses.color_pair(C_HEADER) | curses.A_BOLD) 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): def _draw_memory(self, y, x, w):
d = self.data d = self.data
box_h = 5 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 bar_w = w - 4
iy = y + 1 iy = y + 1
@@ -742,7 +837,7 @@ class Renderer:
def _draw_network(self, y, x, w): def _draw_network(self, y, x, w):
d = self.data d = self.data
box_h = 6 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 iy = y + 1
bar_w = w - 4 bar_w = w - 4
@@ -764,7 +859,7 @@ class Renderer:
# Net total # Net total
total = d.prev_net total = d.prev_net
safe_addstr(self.scr, iy, x + 2, 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)) curses.color_pair(C_DIM))
iy += 1 iy += 1
@@ -790,12 +885,12 @@ class Renderer:
d = self.data d = self.data
num_parts = min(len(d.partitions), 4) num_parts = min(len(d.partitions), 4)
box_h = 3 + num_parts * 2 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 iy = y + 1
bar_w = w - 4 bar_w = w - 4
# IO rates # 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)) safe_addstr(self.scr, iy, x + 2, io_str, curses.color_pair(C_ACCENT))
iy += 1 iy += 1
@@ -814,7 +909,7 @@ class Renderer:
def _draw_gpu(self, y, x, w): def _draw_gpu(self, y, x, w):
d = self.data d = self.data
box_h = 2 + len(d.gpus) * 4 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 iy = y + 1
bar_w = w - 4 bar_w = w - 4
@@ -858,7 +953,7 @@ class Renderer:
return y return y
box_h = min(2 + len(entries), 8) 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 iy = y + 1
cols = max(1, w // 30) cols = max(1, w // 30)
@@ -893,13 +988,13 @@ class Renderer:
processes = pm.filtered_processes(d.processes) processes = pm.filtered_processes(d.processes)
# Title with keybindings # Title with keybindings
filter_info = f" filter: '{pm.filter_text}'" if pm.filter_text else "" filter_info = f" {t('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}" 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) draw_box(self.scr, y, x, box_h, w, title)
iy = y + 1 iy = y + 1
# Header # 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)) safe_addstr(self.scr, iy, x + 1, hdr[:w - 2], curses.color_pair(C_HEADER))
iy += 1 iy += 1
@@ -946,7 +1041,7 @@ class Renderer:
safe_addstr(self.scr, status_y, x + 2, status[:w - 4], safe_addstr(self.scr, status_y, x + 2, status[:w - 4],
curses.color_pair(C_CRIT) | curses.A_BOLD) curses.color_pair(C_CRIT) | curses.A_BOLD)
else: 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)) safe_addstr(self.scr, status_y, x + 2, hint[:w - 4], curses.color_pair(C_DIM))
# Scrollbar indicator # Scrollbar indicator
@@ -982,7 +1077,7 @@ class Renderer:
safe_addstr(self.scr, my + row, mx, " " * menu_w, curses.color_pair(C_POPUP_BG)) safe_addstr(self.scr, my + row, mx, " " * menu_w, curses.color_pair(C_POPUP_BG))
# Title # 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), safe_addstr(self.scr, my, mx, title[:menu_w].center(menu_w),
curses.color_pair(C_POPUP_HL) | curses.A_BOLD) 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)) 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), safe_addstr(self.scr, row_y, mx + 1, f" {name} ".ljust(menu_w - 2),
curses.color_pair(C_POPUP_BG)) 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) curses.color_pair(C_POPUP_BG) | curses.A_DIM)
def _draw_details_popup(self, y, x, w, avail_h): def _draw_details_popup(self, y, x, w, avail_h):
@@ -1015,7 +1111,7 @@ class Renderer:
for row in range(popup_h): for row in range(popup_h):
safe_addstr(self.scr, my + row, mx, " " * popup_w, curses.color_pair(C_POPUP_BG)) 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), safe_addstr(self.scr, my, mx, title[:popup_w].center(popup_w),
curses.color_pair(C_POPUP_HL) | curses.A_BOLD) 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 + 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" CPU: {details['cpu_percent']:.1f}% MEM: {details['memory_percent']:.1f}%",
f" RSS: {details['rss']} VMS: {details['vms']}", f" RSS: {details['rss']} VMS: {details['vms']}",
f" IO R: {details['io_read']} IO W: {details['io_write']}", f" IO R: {details['io_read']} IO W: {details['io_write']}",
f" FDs: {details['num_fds']} Connections: {details['connections']}", f" FDs: {details['num_fds']} {t('connections')}: {details['connections']}",
f" Created: {details['create_time']}", f" {t('created')}: {details['create_time']}",
f" Exe: {details.get('exe', '?')}", f" Exe: {details.get('exe', '?')}",
f" CWD: {details.get('cwd', '?')}", f" CWD: {details.get('cwd', '?')}",
f" Cmd: {details.get('cmdline', '?')}", 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), safe_addstr(self.scr, my + 2 + i, mx, line[:popup_w].ljust(popup_w),
curses.color_pair(C_POPUP_BG)) 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) curses.color_pair(C_POPUP_BG) | curses.A_DIM)
def _draw_nice_dialog(self, y, x, w, avail_h): 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) 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 + 1, mx, "\u2500" * popup_w, curses.color_pair(C_POPUP_BG))
safe_addstr(self.scr, my + 2, mx + 2, 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)) curses.color_pair(C_POPUP_BG))
safe_addstr(self.scr, my + 4, mx, 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) curses.color_pair(C_POPUP_BG) | curses.A_DIM)
def _draw_help(self, h, w): def _draw_help(self, h, w):
help_lines = get_help_lines()
popup_w = min(w - 4, 52) 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 mx = (w - popup_w) // 2
my = (h - popup_h) // 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)) safe_addstr(self.scr, my + row, mx, " " * popup_w, curses.color_pair(C_POPUP_BG))
# Title # Title
title = " SYSTATS - Keyboard Shortcuts " title = f" {t('help_title')} "
safe_addstr(self.scr, my, mx, title.center(popup_w), safe_addstr(self.scr, my, mx, title.center(popup_w),
curses.color_pair(C_POPUP_HL) | curses.A_BOLD) 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 + 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 row_y = my + 2 + i
if row_y >= my + popup_h - 1: if row_y >= my + popup_h - 1:
break break
@@ -1097,12 +1195,12 @@ class Renderer:
curses.color_pair(C_POPUP_BG)) curses.color_pair(C_POPUP_BG))
safe_addstr(self.scr, my + popup_h - 1, mx, 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) curses.color_pair(C_POPUP_BG) | curses.A_DIM)
def _draw_filter_bar(self, y, x, w): def _draw_filter_bar(self, y, x, w):
pm = self.proc_mgr 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], safe_addstr(self.scr, y, x + 1, bar.ljust(w - 2)[:w - 2],
curses.color_pair(C_POPUP_HL) | curses.A_BOLD) curses.color_pair(C_POPUP_HL) | curses.A_BOLD)
@@ -1165,7 +1263,7 @@ def main(stdscr):
val = max(-20, min(19, val)) val = max(-20, min(19, val))
pm.renice_selected(processes, val) pm.renice_selected(processes, val)
except ValueError: except ValueError:
pm._set_status("Invalid nice value") pm._set_status(t("invalid_nice"))
pm.show_nice_dialog = False pm.show_nice_dialog = False
pm.nice_value = "" pm.nice_value = ""
elif key in (curses.KEY_BACKSPACE, 127, 8): elif key in (curses.KEY_BACKSPACE, 127, 8):
@@ -1201,6 +1299,10 @@ def main(stdscr):
break break
elif key in (ord("h"), ord("?"), curses.KEY_F1): elif key in (ord("h"), ord("?"), curses.KEY_F1):
renderer.show_help = True 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"): elif key == ord("c"):
data.process_sort = "cpu" data.process_sort = "cpu"
data.process_sort_reverse = True data.process_sort_reverse = True
@@ -1260,6 +1362,12 @@ def main(stdscr):
if __name__ == "__main__": 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: try:
curses.wrapper(main) curses.wrapper(main)
except KeyboardInterrupt: except KeyboardInterrupt: