User asked: '窗口应该可以前后左右拉'
Added custom edge drag handling in MainWindow (QMainWindow's default
resize support exists but ignored + we want full control for
maximized state too):
New methods in ui/main_window.py:
- _hit_test_edge(pos) -> 'L'/'R'/'T'/'B'/'TL'/'TR'/'BL'/'BR'/''
6px margin around window. Returns which edge (or corner) the
cursor is on. Returns '' when maximized/fullscreen.
- _edge_to_cursor(edge): sets Qt.SizeHor/Ver/FDiag/BDiag cursor
- _do_resize(global_pos): computes new geometry based on drag
delta. Respects self.minimumWidth/Height (QMainWindow's auto
min size from menu/toolbar/statusbar) and screen bounds.
- _restore_from_max(global_pos): when maximized, click+drag on
TOP edge → showNormal() + set geometry to mouse-relative size
+ continue drag from 'TL' corner (like Windows native behavior)
- mousePressEvent/Move/Release: hook into resize state
- leaveEvent: clear cursor when leaving window
- changeEvent: clear resize state on window state change
State fields added in __init__:
- self._resize_edge, self._resize_start_geo, self._resize_start_pos
- self._resize_margin = 6
- self._dragging_from_max = False
Fix: 'QTabWidget.RightSide' was wrong API; used 'QTabBar.RightSide'
in the multi-tab commit (unrelated, but I noticed while testing).
New test_resize.py (5 tests, all pass):
- 8-region hit-test (4 edges + 4 corners)
- Right-edge drag → width grows
- Left-edge drag → x moves right (width limited by minW)
- Top-left corner drag
- Maximized + click top edge → restored to normal
Existing tests (core 6/6, UI 3/3) still pass.
Build: 57MB exe.
- 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
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).