Add help overlay with keyboard shortcuts (h/F1/?)
Full-screen popup showing all keybindings grouped by category: navigation, sorting, process management, and general controls. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
79
systats.py
79
systats.py
@@ -437,6 +437,37 @@ 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."""
|
||||||
|
|
||||||
@@ -586,6 +617,7 @@ class Renderer:
|
|||||||
self.data = data
|
self.data = data
|
||||||
self.scroll_offset = 0
|
self.scroll_offset = 0
|
||||||
self.proc_mgr = ProcessManager()
|
self.proc_mgr = ProcessManager()
|
||||||
|
self.show_help = False
|
||||||
|
|
||||||
def draw(self):
|
def draw(self):
|
||||||
self.scr.erase()
|
self.scr.erase()
|
||||||
@@ -623,6 +655,10 @@ class Renderer:
|
|||||||
# Processes (rest of screen)
|
# Processes (rest of screen)
|
||||||
self._draw_processes(y, 0, w, h - y)
|
self._draw_processes(y, 0, w, h - y)
|
||||||
|
|
||||||
|
# Help overlay
|
||||||
|
if self.show_help:
|
||||||
|
self._draw_help(h, w)
|
||||||
|
|
||||||
self.scr.refresh()
|
self.scr.refresh()
|
||||||
|
|
||||||
def _draw_header(self, y, w):
|
def _draw_header(self, y, w):
|
||||||
@@ -1029,6 +1065,41 @@ class Renderer:
|
|||||||
" Enter:apply Esc:cancel ".center(popup_w),
|
" Enter:apply Esc: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):
|
||||||
|
popup_w = min(w - 4, 52)
|
||||||
|
popup_h = min(h - 2, len(HELP_LINES) + 4)
|
||||||
|
mx = (w - popup_w) // 2
|
||||||
|
my = (h - popup_h) // 2
|
||||||
|
|
||||||
|
# Background
|
||||||
|
for row in range(popup_h):
|
||||||
|
safe_addstr(self.scr, my + row, mx, " " * popup_w, curses.color_pair(C_POPUP_BG))
|
||||||
|
|
||||||
|
# Title
|
||||||
|
title = " SYSTATS - Keyboard Shortcuts "
|
||||||
|
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):
|
||||||
|
row_y = my + 2 + i
|
||||||
|
if row_y >= my + popup_h - 1:
|
||||||
|
break
|
||||||
|
if not key and not desc:
|
||||||
|
continue
|
||||||
|
if not desc:
|
||||||
|
# Section header
|
||||||
|
safe_addstr(self.scr, row_y, mx + 2, key,
|
||||||
|
curses.color_pair(C_POPUP_HL) | curses.A_BOLD)
|
||||||
|
else:
|
||||||
|
line = f" {key:<20s} {desc}"
|
||||||
|
safe_addstr(self.scr, row_y, mx, line[:popup_w].ljust(popup_w),
|
||||||
|
curses.color_pair(C_POPUP_BG))
|
||||||
|
|
||||||
|
safe_addstr(self.scr, my + popup_h - 1, mx,
|
||||||
|
" Press h, F1 or ? to close ".center(popup_w),
|
||||||
|
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" Filter: {pm.filter_text}_ "
|
||||||
@@ -1060,6 +1131,12 @@ def main(stdscr):
|
|||||||
processes = pm.filtered_processes(data.processes)
|
processes = pm.filtered_processes(data.processes)
|
||||||
max_idx = max(0, len(processes) - 1)
|
max_idx = max(0, len(processes) - 1)
|
||||||
|
|
||||||
|
# ── Help overlay ──
|
||||||
|
if renderer.show_help:
|
||||||
|
if key in (ord("h"), ord("?"), curses.KEY_F1, 27):
|
||||||
|
renderer.show_help = False
|
||||||
|
continue
|
||||||
|
|
||||||
# ── Filter mode input ──
|
# ── Filter mode input ──
|
||||||
if pm.filter_mode:
|
if pm.filter_mode:
|
||||||
if key == 27: # ESC
|
if key == 27: # ESC
|
||||||
@@ -1122,6 +1199,8 @@ def main(stdscr):
|
|||||||
break
|
break
|
||||||
elif key == 27: # ESC
|
elif key == 27: # ESC
|
||||||
break
|
break
|
||||||
|
elif key in (ord("h"), ord("?"), curses.KEY_F1):
|
||||||
|
renderer.show_help = True
|
||||||
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
|
||||||
|
|||||||
Reference in New Issue
Block a user