- ui/theme.py: light/dark QSS stylesheets, persisted to ~/.sshclient/settings.json
- main_window.py: View menu with dark theme checkbox, applies on startup
- ui/widgets.py: SparklineChart widget for mini trend visualization
- MonitorPanel: CPU and memory sparkline charts showing last 60 samples
- Charts cleared on stop/switch host
- core/monitor.py: add fast metrics script (0.3s vs 1.4s sleep),
collect() now defaults to fast=True for responsive UI
- ui/workers.py: MonitorWorker changed from loop mode to single-shot
(QTimer re-arms next cycle on finished signal, avoids paramiko
blocking in infinite loop)
- ui/widgets.py: remove debug print from _kick_one_sample
- test_monitor_nonblock.py: fix sample counting by wrapping _on_sample
instead of disconnecting signals (old approach missed new workers)
- test_process_monitor.py: assertions account for MAX_RENDER_ROWS=200,
fix MEM% column index (4 not 3) in sort test
User feedback: '进程比较关键,以下显示太少了,需要继续优化下监控UI'
Process monitor panel was rebuilt:
1. core/monitor.py: 200 -> 500 processes, added PPID / PRI / NICE
New ps fields: pid ppid user pcpu pmem vsz rss stat pri nice etimes times args
Python parser: new fields ppid, pri, nice
2. ui/widgets.py MonitorPanel:
a) Toolbar: search box + sort dropdown (CPU/MEM/PID/start/user/comm)
+ view toggle (Flat / Tree) + CPU% min filter (SpinBox 0-100%)
+ batch kill button
b) Table: 12 columns (Flat) / 13 cols (Tree + indent col)
PID PPID USER CPU% MEM% RSS STAT PRI NI '启动' 'CPU时间' 命令
- Compact row height 20px (was default 30)
- Extended selection (Ctrl/Shift multi)
- Color highlights:
CPU% >=80 red bg+fg bold, >=50 red fg bold, >=20 orange
MEM% >=15 orange bg+fg bold, >=5 orange
STAT: Z orange bg + orange-red fg bold, R green, D purple
NI: negative green, positive orange
- Red bg on EXTREME rows for instant visibility
c) Tree view: builds PPID tree with ├─/└─/│ connectors;
root processes (PID 1) at depth 0, sorted by CPU within siblings.
Useful for 'which process is spawning these things?'
d) Right side panel:
- 进程详情 (QTextEdit dark theme): full PID/PPID/USER/STAT/PRI/NI/
CPU/MEM/RSS/VSZ/etime/cputime, command, parent process name,
child processes list (top 10)
- 🔥 CPU TOP 5 (QListWidget dark): click to jump+select in main table
- 💾 内存 TOP 5 (QListWidget dark): same
e) Right-click menu enhanced:
kill (SIGTERM) | kill -9 (SIGKILL) | batch kill (if multi-select)
| copy PID | copy command | filter by command | view details
f) Batch kill: collects all selected PIDs, runs 'kill p1 p2 p3 ...'
in a single SSH call, then loops 'kill -0' to report survivors.
One confirmation dialog lists top 10 + '还有 N 个'.
g) Status row at bottom: 'Ctrl+A 全选, Shift+点击多选, 右键批量操作'
3. test_process_monitor.py: 5/5 tests pass with 500 rows, tree view,
TOP 5, CPU min filter, etc.
All tests pass: core 6/6 + UI 3/3 + E2E 6/6 + terminal 5/5 + proc 5/5
Build: 57 MB single-file exe.
User asked for process monitoring. Added:
1. core/monitor.py:
- Extended _LINUX_METRICS_SCRIPT with ===PROC=== section
- ps -e -o stat= | awk counts total/running/sleep/disk-sleep/zombie
- ps -eo pid,user,pcpu,pmem,vsz,rss,stat,etimes,times,args
Uses etimes/times (numeric) instead of start/time (string with
spaces) to avoid column-shift bugs. awk joins 10th col onwards
with spaces so comm retains its full command line.
- New empty-dict fields: proc_total, proc_running, proc_sleep,
proc_disk, proc_zombie, processes
- New processes[] array with full per-process fields
2. ui/widgets.py MonitorPanel:
- New _build_proc_summary() - 4-stat overview card (total/running/
sleep/zombie, color-coded)
- New _build_proc_table() - 9-column process list with:
* Search box (multi-keyword AND match across pid/user/stat/comm)
* Sort dropdown (CPU / MEM / PID / start time / user)
* Color-coded CPU% (red >=50%, orange >=20%)
* Color-coded MEM% (red >=10%, orange >=5%)
* Color-coded STAT (red for zombie)
* RSS formatted with format_bytes
* ETIME / TIME formatted as 5d3h / 2h15m / 45s
- _apply_proc_filter() - filter + sort + render in one pass
- _proc_context_menu() - right-click menu:
* kill (SIGTERM) | kill -9 (SIGKILL) | copy PID | copy cmd |
filter by this command
- _proc_kill_selected() / _proc_kill() - sends kill over SSH
with confirmation dialog, shows remote exit code in result
- Splitter: 4 metric cards + proc summary + disk + net on top,
process list on bottom. User-draggable.
3. New test_process_monitor.py (5 tests, all pass):
- Real SSH connection, collects process data
- Renders MonitorPanel with 200 rows
- Tests search filter / sort change / clear
- Tests killing a real sleep process: starts sleep 60, finds it
in the table, calls _proc_kill (with QMessageBox monkey-patched
to auto-yes), verifies remote kill -0 returns 'No such process'
- Also fixed passwords in test_e2e.py + test_terminal.py to match
the working local sshd credentials
All tests pass: core 6/6 + UI 3/3 + E2E 6/6 + terminal 5/5 + proc 5/5
Build: 57 MB single-file exe.
User reported: each prompt line shows a leading '[?]' box.
Root cause: Ubuntu's default PS1 uses \\e]0;user@host:dir\\a
(OSC 0, set window title) which our ANSI parser did not recognize.
It only handled SGR (\\e[...m), so the raw bytes were drawn as tofu boxes.
Fix in ui/terminal_panel.py:
- Added _OSC_RE for ESC ] ... BEL|ESC\\ sequences (set title, set cwd)
- Added _DCS_RE for ESC P/X/^/_ ... ESC\\ (DCS/SOS/PM/APC)
- _on_data() branches ESC prefix by next byte: '['->CSI, '] '->OSC,
'P/X/^/_'->DCS, others->skip 2 bytes
- Captured OSC 0/1/2 content into self._window_title (so future
versions can display it)
- _strip_ansi() now also strips OSC/DCS (for clean history recall)
Added [4b/5] test: injects a literal Ubuntu PS1 byte stream
(OSC 0 + SGR colors) and verifies the rendering is just the plain
'root@ubuntu2204:~# ' with no tofu / no leaked escape chars.
All 21 tests pass (core 6 + UI 3 + E2E 6 + terminal 6).
User feedback: 'your terminal can only send commands, not type
freely. Useless.'
Replaced the old 'single command + output' terminal with a true
interactive shell panel:
NEW: ui/terminal_panel.py
- Uses paramiko invoke_shell() to open a long-lived shell session
- Every keystroke is forwarded to the remote shell (not a textbox)
- Remote output (incl. ANSI color codes) is parsed and rendered
inline in a QPlainTextEdit (16-color SGR, bold, underline, invert)
- Cursor kept on the current input line; user can't edit past output
- Local command history with up/down navigation (stores last 200)
- Standard terminal shortcuts: Ctrl+C (SIGINT), Ctrl+D (EOF),
Ctrl+L (clear), Ctrl+A/E (line start/end), Ctrl+U/K/W
- Terminal resize forwarded to remote PTY (vim/top work correctly)
- Tab key forwarded to remote for completion
CHANGED: core/ssh_client.py
- Added open_shell(term_type, cols, rows) -> Channel
- Added resize_shell(chan, cols, rows)
CHANGED: ui/main_window.py
- Replaced old _build_terminal_tab (now removed) with TerminalPanel
- On host connect -> auto-open shell, switch to Terminal tab
- On host disconnect / switch -> close shell cleanly
NEW: test_terminal.py (5 tests, all pass)
- Real SSH connection, opens shell
- Sends 'ls /tmp' via simulated keypresses, verifies 162-line output
- Sends ANSI color command, verifies colored text appears
- Tests up-arrow history recall
- Verifies prompt returns after command completes
All existing tests still pass (core 6/6 + UI 3/3 + E2E 6/6 + terminal 5/5)
User reported: '已连接, 但终端啥也不显示'
Likely cause: command ran but stdout was empty / scrolled to wrong place
/ output widget didn't refresh before worker finished.
Changes:
- Eagerly show the typed command in the output area (\$ <cmd>) so user
sees the input was received even if the worker hangs/fails
- Show '(stdout 为空)' placeholder when worker returns no output, so it's
clear the worker actually ran
- Force output.repaint() and ensureCursorVisible() after append
- Add 2 diagnostic buttons in terminal bottom bar:
* [🔍 诊断] prints current SSH connection / worker state
* [▶ 测试命令] pre-fills a known-good test command
- Make CommandWorker explicitly check conn.connected before exec
- Surface full traceback in stderr on exception instead of bare str(e)
All 15 existing tests still pass (core 6/6 + UI 3/3 + E2E 6/6).
- Remove all non-ASCII text (cmd default GBK mangles UTF-8)
- Replace 'if errorlevel 1 (' multi-line blocks with 'goto :label'
- Replace 'echo(' with 'echo.'
- Use 'for %%I' to get file size (avoid UTF-8 console findstr issues)
- All error paths now end with explicit exit /b 1
Fixes#1: '涓嬭浇鍦板潃' (mojibake) and '('") parse errors.