Skip to content

Add custom font setting for command input#177

Open
zenaurora wants to merge 201 commits into
jeff141:mainfrom
zenaurora:feature/add-custom-input-font
Open

Add custom font setting for command input#177
zenaurora wants to merge 201 commits into
jeff141:mainfrom
zenaurora:feature/add-custom-input-font

Conversation

@zenaurora

Copy link
Copy Markdown

Overview

Add a dedicated font configuration for the command input box, allowing it to use a custom font independently of the terminal output font.

Changes

  • Added input-font-family and input-font-size properties to Theme
  • Updated command input component to use the new font settings
  • Exposed the font customization option in settings

Why

Previously the command input was hardcoded. Now users can customize the input box font separately for better UI experience.
image

jeff141 and others added 30 commits June 5, 2026 14:09
fix: 修复ssh密钥认证连接失败
…nux 桌面集成

Security fixes / 安全修复:
- jeff141#12 Windows 命令注入: open_with_os 改用 ShellExecuteW + sanitize_filename
- jeff141#10 编辑会话密码回显: 不再回填真实密码, 留空保留原密码
- jeff141#8  密码内存清零: 新增 Secret 类型(zeroize), Drop 清零 + Debug 脱敏

Features / 新增:
- jeff141#9 完整 i18n: 中/英双语, 运行时实时切换(Slint @tr + bundled .po + Rust t())
- 私钥文件选择器 + .pub 容错 + 路径统一 /
- Linux: meatshell.desktop + install-linux.sh + xdg_app_id(Wayland 图标)
- README 截图(docs/screenshots, 敏感信息已打码)

详见 CHANGELOG.md / see CHANGELOG.md for details.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
The fullwidth plus (U+FF0B +) had no glyph in the Latin font Slint picks for
English text, so it rendered as a tofu box. Use an ASCII "+" instead (renders
in any font; Chinese button now shows "+ 新建会话").

全角加号在英文(拉丁字体)语境下没有字形 → 豆腐块。改用 ASCII "+",任何字体都能
渲染;中文按钮也相应改为 "+ 新建会话"。

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Add an "Import ~/.ssh/config" button on the welcome page. Parses the standard
SSH config (Host / HostName / User / Port / IdentityFile; wildcard `Host *`
blocks skipped) and adds each host as a session, skipping duplicates by alias or
host+user. Hosts with an IdentityFile default to key auth. Includes a unit test.

欢迎页新增「导入 ~/.ssh/config」按钮:解析标准 SSH 配置(Host/HostName/User/
Port/IdentityFile,跳过 `Host *` 通配块),将主机加为会话并按别名或 host+user 去
重;带 IdentityFile 的默认密钥认证。含单元测试。

Also: bump to v0.2.3, update CHANGELOG.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Move the SSH-config import action out of the welcome header (cleaner) into the
gear/settings menu as a third item next to language toggle and About. The import
result hint still shows in the Quick-connect header.

把导入操作从欢迎页头部(挤在新建会话旁不够美观)移到齿轮设置菜单,作为第三个菜单
项,与语言切换、关于并列;导入结果提示仍显示在快速连接栏。

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Add .github/workflows/release.yml: builds native binaries for Windows
(x86_64), Linux (x86_64), and macOS (arm64 + x86_64) via a matrix. Installs the
Slint/rfd/GTK system deps on the Linux runner. On a `v*` tag it attaches the
archives to a GitHub Release; on manual dispatch it uploads workflow artifacts.

新增三平台(Windows/Linux/macOS arm64+x86)构建发布工作流;Linux runner 自动安装
Slint/rfd/GTK 依赖;打 v* tag 自动建 Release 并上传产物。

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
install-linux.sh now picks up the `meatshell` binary sitting next to it (the
release-package layout) when no argument is given, falling back to the
source-tree ./target/release path only when there's no sibling binary. Also
auto-chmod +x the binary. README (zh/en) gains a Download & install section
covering the GitHub Actions builds for Windows / Linux / macOS.

install-linux.sh 默认用同目录的 meatshell(发布包布局),无参数即可装;源码树下
才回退到 ./target/release。并自动给二进制加可执行权限。README 中英补充三平台下载
与 Linux 傻瓜式安装说明。

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
GitHub Actions only runs workflows under .github/workflows/. The release
workflow had ended up at .github/release.yml (which GitHub treats as a release-
notes config, not a workflow), so it never triggered. Move it back.

GitHub Actions 只运行 .github/workflows/ 下的工作流;文件被放到了 .github/
release.yml(被当成 release notes 配置,不是 workflow),不会触发。移回正确位置。

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
text-muted (#6b6f7a) was only ~3.4:1 on the dark background — below WCAG AA —
so placeholders, secondary labels and disk details were hard to read. Brighten
text-muted to #9196a3 and text-secondary slightly to #b4b9c4.

text-muted 在深色背景上对比度仅 ~3.4:1(低于 WCAG AA),占位符/次要标签/磁盘明细
偏暗看不清。提亮 text-muted 与 text-secondary。

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Connections can now tunnel through a SOCKS5 (socks5://) or HTTP CONNECT
(http://) proxy with optional user:pass@ credentials, for network-restricted
environments. Resolved from the per-session "Proxy" field, falling back to the
$ALL_PROXY environment variable; empty = direct. src/proxy.rs establishes the
tunneled TcpStream (tokio-socks for SOCKS5, a hand-rolled HTTP CONNECT) and the
existing connection code hands it to russh's connect_stream. Verified
end-to-end: meatshell -> local SOCKS5 proxy -> server, both shell and SFTP.

新增 SSH/SFTP 经 SOCKS5 / HTTP CONNECT 代理(支持 user:pass@ 认证),用于网络受限
环境。会话「代理」字段优先,留空则用 $ALL_PROXY,再空直连。已端到端验证:
meatshell -> 本地 SOCKS5 代理 -> 服务器,shell 与 SFTP 均走代理。

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
jeff141#16 (perf): pipelined SFTP upload — keep ~32 WRITE requests in flight on a
  dedicated raw SFTP channel instead of write-and-wait, hiding RTT (was ~15x
  slower than scp). Verified: 3 MB upload, remote md5 matches local exactly.
jeff141#18 (bug): dragging the SFTP panel up cleared recent terminal output, because
  vt100's set_size truncates the grid from the bottom. Before shrinking, save the
  top rows to scrollback and scroll so the bottom (recent) rows stay visible.
jeff141#15 (security): debug logs recorded raw SSH keystroke bytes (could include
  passwords); log only the byte length now (ssh.rs + app.rs).

jeff141#16 上传流水线(并发 32 个 WRITE,藏住 RTT,原比 scp 慢约 15 倍);已验证 3MB 上传
远端 md5 与本地完全一致。jeff141#18 上拉面板清空输出(vt100 缩小从底部截断),改为缩小前
存顶部行入回滚并滚动保留底部。jeff141#15 debug 日志不再记录原始按键(可能含密码)。

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
- 新增 format_mem_mib():总内存 >=1GiB 时自动用 GiB 单位显示
- StatRow detail 文本固定宽度改为 min-width + stretch,不再被进度条挤压
- 大数值(>=100G)或整数G值省略小数点,进一步缩短字符串长度
- detail min-width 从 56px 提升至 64px
The earlier jeff141#15 fix only covered the byte-length log site. Two more leaks
remained in wire_key_input: send_key logged the raw key string at debug
level, and the [KEY_DIAG] IME diagnostic logged Shift-typed key code points
at info level (no RUST_LOG required). Both could expose password characters.

Add redact_key(): reveals only C0/C1 control code points (the IME markers
the diagnostics rely on) and masks every printable character.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Add two non-SSH transports that reuse the full terminal pipeline:

- Serial (src/serial.rs): opens COM3 / /dev/ttyUSB0 via the serialport
  crate (blocking reader thread + spawn_blocking writes), configurable
  baud / data bits / stop bits / parity / flow control. Closes jeff141#14.
- Telnet (src/telnet.rs): TCP with RFC 854 IAC option negotiation
  (suppress-go-ahead, echo, NAWS window size), strips IAC from the data
  stream, escapes 0xFF in input, re-sends NAWS on resize, and tunnels
  through the existing SOCKS5/HTTP proxy. Closes jeff141#17.

Session gains a kind field (ssh/serial/telnet) plus serial settings;
old configs default to ssh. The session dialog gets a connection-type
selector and per-kind fields. Serial/Telnet have no SFTP or resource
monitor, so those panels are hidden/labelled accordingly.

CI installs libudev-dev for the serialport crate on Linux.

Verified: telnet end-to-end against a local echo server (IAC stripped,
bidirectional input/output), serial worker error path (open COM99 ->
localized error in terminal), and all three dialog modes render correctly.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…n shrink

The previous shrink fix scrolled the screen up by the full row delta and
saved that many top rows to scrollback. When the grid was not full (a
fresh shell with a few prompt lines and blank space below), this pushed
the real content off the top into history, leaving a blank screen with
the cursor stranded at the top; rapid up/down dragging repeated it until
the prompt was gone.

Now scroll up only by max(0, cursor_row+1 - new_rows): rows below the
cursor are unused blanks and truncate for free, so content and the prompt
stay put and rapid resizing is stable.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Selection endpoints were stored as visible-window rows, so when the view
auto-scrolled during a drag (dragging past the bottom edge of a short
terminal to select more than one screen of scrollback) the anchor could
not stay pinned to content that scrolled off-window. Releasing then copied
only the final visible window, losing everything above it — so a
top-to-bottom selection of scrolled-back output never captured all of it.

Store the anchor and focus as absolute (combined_row, col) indices over the
virtual history+live buffer. vis_to_abs maps a visible row to its absolute
index for the current view; selection_rects_visible clips highlights to the
window; extract_selection_text reads the whole selection from the combined
buffer regardless of the current scroll position. Auto-scroll now just moves
the view and re-points the focus at the edge row, leaving the absolute anchor
fixed. Removes the old visible-coordinate helpers.

Unit tests cover the visible<->absolute mapping, history+live extraction,
view-independence of a copy, and highlight clipping.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
SFTP delete is irreversible (no trash), so a misclick on the context-menu
Delete item could silently destroy a file. Gate it behind an in-app
ConfirmDialog (ui/confirm_dialog.slint) styled to match the rest of
meatshell — a dark card over a dimmed backdrop, like SessionDialog, with a
red destructive button and the full path shown. The confirmation lives in
the UI layer: the context-menu delete now opens the modal and stashes the
target; the existing Rust delete only fires on confirm. Backdrop click or
Cancel dismisses without deleting.

Verified end-to-end: Delete opens the themed dialog; the red button removes
the file (status "已删除…", row disappears); Cancel/backdrop leaves it intact.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
The download path was built directly from the server-supplied name via
base_name(), so a malicious SFTP server could craft a name with path
separators, shell-special characters, or a Windows reserved device name
(CON, NUL, COM1, ...) to write outside the chosen directory or target a
device. The open/edit flow already ran names through sanitize_filename();
the download path did not.

Run downloads through sanitize_filename, and harden that helper to also
neutralise Windows reserved device names (reserved case-insensitively and
even with an extension) and strip leading whitespace, while preserving Unix
dotfiles. Unit tests cover separators/traversal, special chars, reserved
names, whitespace trimming, and the empty fallback.

Closes jeff141#26. (jeff141#24, the resize-churn cursor bug, was already fixed by the
cursor-based shrink change.)

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
The sidebar monitor runs a small loop over an SSH exec channel parsing
/proc and df output. Three hardenings against a malicious/compromised
server (which controls that output and the shell environment):

1. PATH hijacking: prepend "PATH=/usr/bin:/bin:/usr/sbin:/sbin; export PATH"
   to the monitor command so a server with a tampered PATH or BASH_ENV cannot
   shadow awk/cat/df/sleep. A fixed PATH is more portable than hardcoding one
   absolute path per tool; monitoring is best-effort either way.

2. Unbounded buffer: the reassembly buffer grew until the __MSTICK__ marker
   arrived. A server that streams data and never emits the marker could grow
   it without limit. Cap the leftover tail at 1 MiB and drop it past that.

3. Parser robustness: a server can put arbitrary values in /proc/stat,
   /proc/net/dev and df output. Sum CPU jiffies and the KiB->bytes multiply
   with saturating arithmetic (no debug overflow panic), and cap interfaces
   and filesystems at 64 per sample so a flood of fabricated rows cannot
   swamp the parser or sidebar.

Unit tests cover df saturation, CPU overflow values, and the interface cap.
Verified live that the PATH-reset monitor still reports CPU/mem/swap/net/disk.

Closes jeff141#27.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Uses linuxdeploy + linuxdeploy-plugin-appimage (APPIMAGE_EXTRACT_AND_RUN=1
so no FUSE needed in CI). Produces a single-file portable executable alongside
the existing .tar.gz; both are uploaded as workflow artifacts and attached to
GitHub Releases.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
…on-error)

`--plugin appimage` conflicts with `--output appimage`; the output plugin is
invoked automatically by --output. Also added `continue-on-error: true` so a
future AppImage failure can't take the regular .tar.gz down with it.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Adds an image crate dependency (Linux-only, png feature only) and a
set_window_icon helper that decodes the bundled 512x512 PNG at startup
and hands it to slint::Window::set_window_icon. This makes the Wayland/
X11 dock show the correct icon when running the AppImage without
AppImageLauncher, or the bare binary without install-linux.sh.

Windows keeps its icon from the linked .ico; macOS from the app bundle --
neither is affected.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
slint::Window has no set_window_icon method; replace with the winit
equivalent accessed via with_winit_window. Works on X11 sessions
(sets _NET_WM_ICON); is a no-op on Wayland by design.

修复 Linux 窗口图标:改用 winit API,slint::Window 并无
set_window_icon 方法,改为通过 with_winit_window 调用 winit 的同名
方法。X11 会话下生效(写入 _NET_WM_ICON);Wayland 下为空操作,符合预期。
Passwords in sessions.json are no longer stored in plaintext. On first
launch a random 256-bit key is written to secret.key in the same config
directory (mode 0600 on Unix). Each non-empty password is encrypted with
ChaCha20-Poly1305 (random 96-bit nonce per value) and stored as
"enc:v1:<base64url(nonce||ciphertext)>".

Legacy plaintext passwords from older installs are kept as-is in memory
and silently re-encrypted on the next save — no user action required.
Copying sessions.json alone is no longer sufficient to read credentials.

新增:密码静态加密,改用 ChaCha20-Poly1305 存储

sessions.json 中的密码不再明文保存。首次启动时在配置目录生成随机
256 位密钥文件 secret.key(Unix 下权限 0600)。每个非空密码使用
ChaCha20-Poly1305(每次随机 96 位 nonce)加密后以
"enc:v1:<base64url(nonce||密文)>" 格式写入 JSON。

旧版本的明文密码加载后保持原样,下次保存配置时自动加密,无需用户操作。
单独拿走 sessions.json 已无法还原密码。
- Add reactive light/dark theme system with sun/moon toggle button;
  defaults to system preference (dark-light crate), persisted in config
- Redesign ANSI 16-color palette: separate fg (ANSI16_LIGHT) and bg
  (ANSI16_LIGHT_BG) tables for light mode so slot-7 white stays readable
  as text while TUI background fills render as light pastels
- Add HSL-based transform for true-color (24-bit) backgrounds in light
  mode: dark RGB backgrounds (L < 0.45) are remapped to light pastels,
  making btop/htop/vim feel native to the light theme
- Add symmetric darken_light_fg() transform: light RGB foregrounds
  (L >= 0.55) are darkened so btop text stays readable on white background
- Store raw vt100::Color in HistSpan; convert at render time so theme
  switching retroactively recolors the entire scrollback history
- Bump version to 0.2.5
feat: 新增浅色/深色主题,TUI 程序自适应配色

- 添加响应式浅色/深色主题切换,顶栏太阳/月亮按钮一键切换;
  启动时跟随系统设置(dark-light crate),偏好持久化到配置文件
- 重设计 ANSI 16 色调色板:浅色模式下前景(ANSI16_LIGHT)与背景
  (ANSI16_LIGHT_BG)使用独立映射表,确保 slot-7 白色作为文字仍
  可读,TUI 填充块渲染为浅色粉彩风格
- 对真彩色(24-bit)背景添加 HSL 亮度重映射:浅色模式下深色 RGB
  背景(亮度 L < 0.45)自动映射为浅色,btop/htop/vim 在浅色主题下
  视觉风格统一
- 添加对称的 darken_light_fg():浅色 RGB 前景(L ≥ 0.55)压暗,
  确保 btop 文字在白色背景上可读
- HistSpan 改为存储原始 vt100::Color,渲染时再转换颜色,切换主题
  可即时重绘整个历史滚动缓冲区
- 版本升至 0.2.5
jeff141 and others added 29 commits June 20, 2026 14:42
…groups (jeff141#55)

快捷命令新增可选分组,体验对齐欢迎页的会话分组:

- 数据:QuickCommand 增加 group 字段(serde default,旧配置兼容);留空归入隐式
  「default」分组。
- 命令栏弹窗:按分组渲染,每组带标题,点击收起/展开(运行时状态,不持久化)。
- 管理对话框:新增「分组(可选)」输入框;列表按分组显示分组头。
- 分组会重排显示顺序,删除改用 orig-index 回指原始存储位置,避免删错条目。
- 新增 toggle-quick-group 回调 + on_toggle_quick_group 处理(用运行时 HashSet
  记录已收起的分组)。
- 补 zh 翻译:分组(可选)、留空 → 默认分组。

Quick commands gain an optional group, mirroring the welcome page's session
groups:

- Data: QuickCommand gets a `group` field (serde default → backward compatible);
  empty means the implicit "default" group.
- Command-bar popup: rendered grouped, each group has a clickable header to
  collapse/expand (runtime-only state, not persisted).
- Manage dialog: new "Group (optional)" field; list shows group headers.
- Grouping reorders the display, so delete now uses orig-index pointing back to
  the stored entry to avoid deleting the wrong one.
- New toggle-quick-group callback + on_toggle_quick_group handler (collapsed
  groups tracked in a runtime HashSet).
- zh translations added.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…uick commands with full group ops (jeff141#55)

把欢迎页快速连接会话的整套右键管理搬到快捷命令的管理对话框:

- 右键命令:编辑(进编辑模式,Add 变 Save)、复制、删除、移动到分组。
- 右键分组标题:重命名分组、删除空分组、新建分组;另加「+ 新建分组」按钮。
- 数据:config 增加 quick_groups(显式分组,支持空分组持久化),并新增
  add/remove/rename_quick_group、update_quick_command 方法。
- 模型:空分组渲染为 orig-index = -1 的占位头行,可被重命名/删除(对齐空会话文件夹)。
- 移动到「default」= 清空分组名;分组重排后删除用 orig-index 回指原始条目。
- 新增回调与处理:edit/save/duplicate/move-quick-command、submit/delete-quick-group。

Ports the welcome page's quick-connect right-click management onto the quick-
command manage dialog:

- Right-click a command: Edit (edit mode — Add becomes Save), Duplicate, Delete,
  Move to group.
- Right-click a group header: Rename group, Delete (empty) group, New group; plus
  a "+ New group" button.
- Data: config gains `quick_groups` (explicit groups so empty ones persist) and
  add/remove/rename_quick_group + update_quick_command helpers.
- Model: an empty group is a placeholder header row (orig-index -1) that can be
  renamed/deleted, mirroring empty session folders.
- Move to "default" clears the group; deletes use orig-index since grouping
  reorders the display.
- New callbacks + handlers: edit/save/duplicate/move-quick-command,
  submit/delete-quick-group.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…groups collapsed by default (jeff141#55)

上一版把右键菜单只加在管理对话框,但用户是在命令栏弹窗上操作,那里没有右键
菜单(只有左键运行/收起),所以右键没反应。

- 把整套右键菜单搬进命令栏弹窗:右键命令 → 编辑/复制/删除/移动到分组;右键分组
  标题 → 重命名/删除空分组/新建分组。TouchArea 同时带 clicked + pointer-event,
  与会话列表一致(只有 pointer-event 时在 Flickable 内不抓取右键)。
- 管理对话框的右键 TouchArea 也补上 clicked,保证在 Flickable 内右键生效。
- 分组默认全收起:启动时把所有分组名放进收起集合(init 渲染与运行时集合一致)。
- 新增 TerminalView 回调并在 app.slint 转发到 root:edit/duplicate/delete/move-
  quick-command、new/rename/delete-quick-group。
- 补 zh 翻译「重命名分组」。

The previous commit only added the right-click menus to the manage dialog, but
the user operates in the command-bar popup, which had no right-click menu (only
left-click run / collapse) — so right-click did nothing.

- Port the full right-click menus into the popup: right-click a command for
  Edit/Duplicate/Delete/Move-to; right-click a group header for
  Rename/Delete-empty/New group. Each TouchArea now has both clicked and
  pointer-event, matching the session list (a pointer-event-only TouchArea
  doesn't grab right-clicks inside a Flickable).
- The manage-dialog right-click TouchAreas got a clicked grab too.
- Groups collapsed by default: seed the collapse set with all group names at
  startup (init render and runtime set agree).
- New TerminalView callbacks forwarded to root in app.slint.
- zh translation for "Rename group".

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…k log noise

- 过滤器新增 fontdb=error:屏蔽 fontdb 对无法解析、但会自动跳过的系统字体发出的
  「malformed font」WARN(如 Windows 的 mstmc.ttf);保留 error 级别以便真正的错误仍可见。
- 把 resolve_ui_font_family 里"命中系统字体 / MEATSHELL_UI_FONT 覆盖"两条日志从
  warn 降为 debug;只有"找不到任何系统 CJK 字体、回退内置"的失败路径仍保留 warn
  (继续进 error.log,便于诊断)。
- silence_icu 改名 quiet_noise(现在同时管 ICU4X 与 fontdb)。

- Add fontdb=error to the log filter so fontdb's harmless "malformed font" WARN for
  fonts it skips (e.g. Windows mstmc.ttf) is suppressed while real errors stay.
- Demote the "using system CJK font" / MEATSHELL_UI_FONT override lines in
  resolve_ui_font_family from warn to debug; only the fallback failure path stays
  at warn (still lands in error.log for diagnostics).
- Rename silence_icu -> quiet_noise (now covers both ICU4X and fontdb).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…r SFTP key, show import result (jeff141#133)

问题1:私钥带口令时,终端能连上但 SFTP 目录树加载失败,报「failed to load
key... the key is encrypted」。原因是 SFTP 单独建连时调用 load_secret_key 传了
None,没把口令传进去;而 shell 会话用的是 Some(password)。改为和 shell 一致:
复用会话 password 字段作口令(空=未加密)。

问题2:「导入 ~/.ssh/config」点了像没反应。其实导入有执行,但结果提示
(ssh-import-hint)只显示在欢迎页;用户开着会话时欢迎页被终端盖住,看不到反馈。
改为:点击后不再关闭设置菜单,并把结果(已导入 N / 没有新主机 / 未找到)直接显示
在菜单项下方;打开设置时清空旧提示。

Problem 1: with a passphrase-protected private key the shell connects but the
SFTP tree fails with "failed to load key... the key is encrypted". SFTP's
dedicated connection called load_secret_key with None instead of the passphrase
(the shell passes Some(password)). Reuse the session password field as the
passphrase (empty = unencrypted), matching the shell.

Problem 2: "Import ~/.ssh/config" looked like a no-op. The import ran, but its
result hint only rendered on the welcome page, which is hidden behind an active
session. Keep the settings menu open on click and show the result (imported N /
no new hosts / not found) inline under the item; clear the stale hint when the
menu opens.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…ble process window, redesigned settings

- 侧栏:在资源面板与主区之间加可拖动分隔条,宽度可在 160–520px 间调节并持久化到配置(重启保留);折叠时分隔条隐藏,拖动期间禁用折叠动画以跟手。
- 窗口:初始尺寸 1200×760 → 1440×900,对齐同类客户端的舒适默认值。
- 进程监视:从内嵌浮层提升为真正的独立 OS 窗口(可拖出父窗口/拖到第二屏),无边框自绘标题栏 + 缩放手柄;两窗口共享同一 VecModel 实时刷新;修复独立窗口主题(Theme 为每窗口 global)与 CJK 字体未同步的问题。
- 设置-界面:右侧改为「分区 + 标签左·控件右」的简约布局(iOS 风开关、[− 值 +] 步进器、固定紧凑字号,不随界面缩放放大);保留内嵌模态浮层,打开时遮罩吞鼠标 + 抢焦点吞键盘,禁止任何输入,卡片仅能在窗口内拖动。
- Sidebar: draggable splitter between the resource panel and main area; width adjustable within 160–520px and persisted to config (survives restart); the splitter hides when collapsed and the collapse animation is disabled while dragging for 1:1 tracking.
- Window: initial size 1200×760 → 1440×900, matching the comfortable default of comparable clients.
- Process monitor: promoted from an in-app overlay to a real top-level OS window (draggable outside the parent / onto a second monitor) with a frameless custom titlebar and resize grip; both windows share one VecModel for live updates; fixes detached-window theme (Theme is a per-window global) and CJK font not being synced.
- Settings → Interface: right pane redesigned into a clean "section + label-left · control-right" layout (iOS-style switches, [− value +] steppers, fixed compact typography that ignores UI scale); kept as an embedded modal overlay that blocks all input while open (veil swallows mouse, focus scope swallows keys) and whose card can only be dragged within the window.
… back to keyboard-interactive on password-auth failure (jeff141#86)

JumpServer 等堡垒机默认只放行 keyboard-interactive、不放行 password 方法,导致
直接"认证失败"——Xshell/MobaXterm/WindTerm 能登正是因为会自动回退。本次:
密码认证失败后,断开并重连一条全新连接,再用 keyboard-interactive 以密码应答
服务器提示。注意:russh 在一次失败的认证后无法在同一句柄上切换认证方法(会
卡死),因此必须重连;为此抽出 connect_ssh() 复用连接逻辑。已在真实的
keyboard-interactive-only sshd 上验证登录成功。
JumpServer and many bastions disable the `password` SSH method and only accept
keyboard-interactive, so plain password auth fails outright — other clients get
in because they fall back automatically. Now: on password-auth failure,
disconnect and reconnect on a fresh handle, then authenticate via
keyboard-interactive answering each prompt with the password. russh hangs if a
second auth method is attempted on a handle whose first attempt already failed,
so a reconnect is required; connect_ssh() is factored out to reuse the connect
path. Verified against a real keyboard-interactive-only sshd.
… back to keyboard-interactive on password-auth failure (jeff141#86)

JumpServer 等堡垒机默认只放行 keyboard-interactive、不放行 password 方法,导致
直接"认证失败"——Xshell/MobaXterm/WindTerm 能登正是因为会自动回退。本次:
密码认证失败后,断开并重连一条全新连接,再用 keyboard-interactive 以密码应答
服务器提示。注意:russh 在一次失败的认证后无法在同一句柄上切换认证方法(会
卡死),因此必须重连;为此抽出 connect_ssh() 复用连接逻辑。已在真实的
keyboard-interactive-only sshd 上验证登录成功。
JumpServer and many bastions disable the `password` SSH method and only accept
keyboard-interactive, so plain password auth fails outright — other clients get
in because they fall back automatically. Now: on password-auth failure,
disconnect and reconnect on a fresh handle, then authenticate via
keyboard-interactive answering each prompt with the password. russh hangs if a
second auth method is attempted on a handle whose first attempt already failed,
so a reconnect is required; connect_ssh() is factored out to reuse the connect
path. Verified against a real keyboard-interactive-only sshd.
…e + SFTP) + layout persistence

- 停靠:资源面板与 SFTP 面板均可拖到四条边(上/下/左/右);拖动手柄时四边浮现高亮放置
  区,松手即吸附。两个面板都可拖动调大小,折叠后缩成停靠边缘的一个小展开按钮(彻底隐藏)。
- 自适应:资源面板横向停靠时小组件改为横排;SFTP 竖向(窄)停靠隐藏目录树,并随宽度
  渐进隐藏「大小→时间」列(名称将被挤断时才让位),横向(宽)停靠则恒显示全部列;SFTP
  工具栏新增专用拖动手柄,密集控件下也好抓。
- 持久化:两面板的停靠边/尺寸 + 父窗口大小,退出保存、启动恢复,保留用户偏好布局。
- 修复:macOS 欢迎页标题/副标题/卡片被拉开错位——Welcome 显式填满内容区、头部钉顶。
- 发版:0.4.13 → 0.4.14,更新 CHANGELOG。

实现要点:用绝对几何把面板按 dock 状态铺到边缘(资源面板在窗口级 dock-area、SFTP 在
终端区级 dock-region),中央内容包裹进 clip 容器避免溢出;拖动吸附复用「绝对光标 −
容器原点」的稳定坐标;resize 直接以光标在容器内的位置定位面板边缘(避免增量累积);
持久化集中在退出路径的 save_layout()。

- Docking: both the resource panel and the SFTP panel can be dragged to any edge
  (top/bottom/left/right); dragging the handle highlights drop zones on all four edges
  and releasing snaps it there. Both are drag-resizable and collapse to a small expand
  button on the docked edge (fully hiding the panel).
- Responsive: the resource panel lays its widgets out in a row when docked horizontally;
  the SFTP panel hides its directory tree when docked vertically (narrow) and
  progressively drops the Size → Modified columns as it narrows (only once Name would
  elide), while a horizontal (wide) dock always shows every column. A dedicated drag
  grip was added to the SFTP toolbar so it's grabbable despite the busy toolbar.
- Persistence: each panel's docked edge/size and the window size are saved on exit and
  restored next launch, keeping the user's preferred layout.
- Fix: macOS welcome page had its title/tagline/card spread apart — Welcome now fills
  the content area explicitly and the header is pinned to the top.
- Release: bump 0.4.13 → 0.4.14, update CHANGELOG.

Implementation: panels are placed by absolute geometry per their dock state (the
resource panel in a window-level dock-area, SFTP in a terminal-level dock-region), with
the central content wrapped in a clipping container to prevent overflow; drag-to-dock
reuses the stable "absolute cursor − container origin" coordinate; resize positions the
panel edge directly at the cursor within the container (no incremental accumulation);
persistence is centralised in save_layout() on every exit path.
…on lines with CJK (wide) glyphs (jeff141#132)

终端每行的纯文本(供复制和查找使用)按"一字一字符"存储,但一个中文(CJK)
字在网格上占 2 列。复制时却把"选区列号"直接当成"字符下标"去切字符串,导致
丢失的字符数恰好等于选区前面的中文字数:
- 选 "1pctl update password",前面有 "提示"+"修改密码可执行命令" 共 11 个汉字,
  于是丢掉前 11 个字符,实际复制到 "e password";
- 选 "修改密码可执行命令: 1pctl update password",前面 "提示" 2 字,丢掉 "修改"。

修复:引入 unicode-width(锁定到 vt100 所用的同一版本 0.1.14,保证"是否宽字符"
的判断与 vt100 在网格上摆放 CJK 字形的方式一致),建立"字符下标 ↔ 网格列"的
换算:
- extract_selection_text:把选区列号换算回正确的字符下标再切片,中文不再错位;
  在宽字形的第 2 个格上起选也会整字纳入(无法半选一个汉字)。
- compute_find_matches(同源潜在 bug):查找高亮框原先也按字符下标绘制,中文行
  之后的匹配框会偏移,现改为输出网格列,框能精确罩住文字。

新增回归测试 extract_handles_wide_cjk_columns、find_matches_report_grid_columns_past_cjk,
复现 jeff141#132 的精确场景。

The per-row plain text backing copy and find stores one char per glyph, but a
wide (CJK) glyph occupies two grid cells. Copy treated a selection's column as a
char index, so the number of dropped characters equalled the count of wide glyphs
before the selection — selecting "1pctl update password" yielded "e password",
and "修改密码可执行命令: 1pctl update password" lost its leading "修改".

Fix: add unicode-width (pinned to 0.1.14, the same version vt100 uses, so the
"is this glyph two cells wide?" decision matches how vt100 lays CJK glyphs on the
grid) and convert between char index and grid column:
- extract_selection_text maps selection columns back to char indices before
  slicing, so CJK no longer drifts; anchoring on the second cell of a wide glyph
  still grabs the whole glyph.
- compute_find_matches (same latent bug) emitted highlight rects at char indices,
  mis-placing them after CJK; it now reports grid columns so highlights line up.

Adds regression tests extract_handles_wide_cjk_columns and
find_matches_report_grid_columns_past_cjk covering the exact jeff141#132 case.
- **终端内查找:Ctrl+F 唤出查找栏。** 在会话里按 Ctrl+F 即可弹出顶部查找栏(与右键菜单
  → 查找一致),输入即时高亮所有匹配,Esc 关闭;已在「设置 → 快捷键」中登记。
  **Find in terminal: Ctrl+F opens the find bar.** Press Ctrl+F in a session to bring up
  the find bar (same as right-click → Find); matches highlight as you type and Esc closes
  it. Now listed under Settings → Shortcuts.

- **面板可拖动吸附停靠(资源面板 + SFTP)。** 资源面板和 SFTP 面板现在都能拖到四条边
  (上/下/左/右):拖动面板手柄时,四条边浮现高亮放置区,松手即吸附到那条边。两个面板都
  可拖动调节大小;折叠后会缩成停靠边缘的一个小展开按钮(彻底隐藏面板)。**自适应:** 资源
  面板横向(上/下)停靠时,内部小组件自动改为横排;SFTP 竖向(左/右、窄)停靠时隐藏目录树,
  并随宽度**渐进隐藏「大小→时间」列**(名称快被挤成「…」时才让位),横向(上/下、宽)停靠
  则恒显示全部列。SFTP 工具栏左侧新增专用拖动手柄,密集控件下也能稳稳拖动。
  **Drag-to-dock panels (resource panel + SFTP).** Both the resource panel and the SFTP
  panel can now be dragged to any edge (top / bottom / left / right): dragging the
  panel's handle shows highlighted drop zones on all four edges, and releasing snaps it
  there. Both panels are drag-resizable and collapse to a small expand button on their
  docked edge (fully hiding the panel). **Responsive:** the resource panel lays its
  widgets out in a row when docked horizontally; the SFTP panel hides its directory tree
  when docked vertically (narrow) and progressively drops the **Size → Modified** columns
  as it narrows (only once the Name would elide to “…”), while a horizontal (wide) dock
  always shows every column. A dedicated drag grip was added to the SFTP toolbar so the
  panel is grabbable even though its toolbar is full of controls.

- **布局持久化。** 两个面板的停靠边与宽/高,以及父窗口大小,都会在退出时保存、下次启动
  恢复——可以保留你喜欢的窗口尺寸和面板布局。
  **Layout persistence.** Each panel's docked edge and size, plus the window size, are
  saved on exit and restored on the next launch — so your preferred window size and
  panel arrangement stick.

### Changed / 优化

- **历史命令的搜索框移到下拉框底部 (jeff141#131)。** 命令历史下拉向上展开,搜索框原先在顶部、
  位置随历史条数上下浮动、不好找;现在固定在下拉框**底部**(紧挨命令输入框),列表在其上方
  填充并可滚动——位置稳定、一眼可见,和 FinalShell 一致。
  **History search box moved to the bottom of the dropdown (jeff141#131).** The command-history
  dropdown opens upward; the search box used to sit at the top, drifting up and down with
  the number of entries and hard to find. It's now pinned to the **bottom** of the
  dropdown (right above the command input), with the scrollable list filling the space
  above it — a fixed, immediately visible spot, matching FinalShell.

- **SFTP 折叠按钮与资源面板统一,并保持在右侧。** 两个面板现在共用同一个展开按钮组件;
  SFTP 的控件本就在右侧,折叠后的展开按钮也随之停在右下/右上,不再突兀地跳到左边。
  **SFTP collapse button unified with the resource panel, kept on the right.** Both panels
  now share one expand-button component; since SFTP's controls live on the right, its
  collapsed expand button stays at the bottom-/top-right instead of jumping to the left.

### Fixed / 修复

- **含中文的行复制/查找列错位 (jeff141#132)。** 终端纯文本按「一字一字符」存储,而中文(CJK)字
  在网格上占两列;复制时把选区列号当作字符下标,导致丢失的字符数恰好等于选区前面的中文
  字数(如选「1pctl update password」实际只复制到「e password」)。现引入 unicode-width
  做「字符↔网格列」换算:复制所见即所得,在宽字形第二格起选也会整字纳入;查找高亮框(同源
  问题)改按网格列绘制,中文行之后也能精确罩住文字。
  **Copy & find column drift on lines with CJK glyphs (jeff141#132).** The terminal's plain text
  stores one char per glyph, but a wide (CJK) glyph spans two grid cells; copy treated a
  selection's column as a char index, dropping as many characters as there were wide glyphs
  before the selection (selecting “1pctl update password” yielded only “e password”). A
  char-to-column conversion (via unicode-width) makes copy WYSIWYG — anchoring on the
  second cell of a wide glyph still grabs the whole glyph — and find highlights (same root
  cause) now sit on grid columns so they line up after CJK.

- **macOS 欢迎页布局错位。** 欢迎页的标题、副标题、快速连接卡片在 macOS 上被拉开(标题与
  副标题间出现大空隙)。现在 Welcome 显式填满内容区、头部固定在顶部按自然高度排列,卡片填满
  其余空间。
  **macOS welcome-page layout spread apart.** The title, tagline and quick-connect card
  were spaced out on macOS (a large gap between the title and tagline). The Welcome view
  now explicitly fills the content area and the header is pinned to the top at its
  natural height, with the card filling the rest.
…dir next to the executable (jeff141#141)

所有用户数据(sessions.json、加密密钥 secret.key、known_hosts、error.log)的路径
现在由 config.rs 新增的 data_dir() 统一解析(OnceLock 缓存),errlog.rs 与
known_hosts.rs 不再各自调用 ProjectDirs。

解析策略(便携优先):
- 优先使用「可执行文件同目录下的 config/」——并用一次真实写探针确认目录可写,
  这样便携版可随 U 盘携带,也不再往用户目录(%APPDATA%)塞东西;
- 当 exe 位于只读位置(Program Files / /usr)、写探针失败时,回退到旧的按用户
  系统配置目录(ProjectDirs.config_dir(),即 0.4.15 之前的存放位置),因此老安装
  原样可用、行为不变。

向下兼容:首次落到便携目录时,把旧用户目录里的 sessions.json / secret.key /
known_hosts 复制过去(只复制不删除、不覆盖已存在文件,作为兜底;Unix 下补回
secret.key 的 0600 权限),升级用户不会丢失已保存的会话。

说明:这解决了 jeff141#141「便携版随身携带」的核心诉求;原帖另提的「文件对话框自选
任意目录」尚未实现,留作后续。

All user data (sessions.json, the secret.key encryption key, known_hosts, and
error.log) now resolves through a single data_dir() added in config.rs (cached
via OnceLock); errlog.rs and known_hosts.rs route through it instead of each
calling ProjectDirs.

Resolution is portable-first:
- prefer a config/ folder beside the executable, gated on a real write probe, so
  a portable build can travel on a USB stick and stops cluttering the user
  profile (%APPDATA%);
- when the exe sits somewhere read-only (Program Files / /usr) and the probe
  fails, fall back to the per-user OS config dir (ProjectDirs.config_dir(), the
  pre-0.4.15 location), so existing installs keep working unchanged.

Backward compatible: on the first launch that lands on the portable dir, the
legacy per-user sessions.json / secret.key / known_hosts are copied over
(copy-not-move, never overwriting, re-applying 0600 on secret.key on Unix) so
upgrading users don't lose saved sessions.

This covers the core "carry it around" ask of jeff141#141; the issue's other request —
a file dialog to pick an arbitrary directory — is left as follow-up.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…ming with global tinting and 3 built-ins

新增「设置 → 界面 → 壁纸」:macOS 风格缩略图选择器,3 张内置壁纸(简约·浅、简约·暗、
幻想3048)+「选择文件…」自定义图片。

实现:
- 新增 src/wallpaper.rs:三张内置壁纸全部程序化逐像素绘制(渐变 + 柔光;幻想3048 是
  赛博朋克合成波——星空、带扫描线的发光星球、霓虹透视网格),无图片资源;自定义图片用
  image crate 解码(改为跨平台依赖)。两条路径都产出 RGBA 缓冲 + 从平均色推导的调色板
  (强调色走 HSL 提纯、深浅按亮度、tint 取平均色)。
- theme.slint:新增壁纸图/active/wp-accent/wp-tint 等属性与 frost() 函数——壁纸激活时
  面板与终端背景磨砂半透并向壁纸主色微调,强调色改用提取色,窗口底色用恒不透明的
  window-base。app.slint 在窗口最底层铺一张 image-fit:cover 的壁纸;proc_window.slint
  同样获得壁纸图层 + 磨砂内容衬底,独立进程窗也是同款毛玻璃。
- app.rs:apply_wallpaper 加载图+调色板并下发;抽出 apply_dark_mode 统一翻转主题 + 每个
  终端缓冲区的 is_dark + 重渲染(修复内置壁纸切换深浅时终端文字不跟着变、发灰的问题),
  主题开关与壁纸切换共用它;sync_proc_theme 同步壁纸给进程窗。内置款按亮度自动设深浅,
  自定义照片保留用户的主题开关选择以保证可读性。
- config.rs:持久化 wallpaper id;默认值 "builtin:tech"(幻想3048,暗色)——全新安装、
  老用户升级(缺字段)均默认开启,显式选「无」(空串)或自定义则保留;含单元测试。

Adds Settings → Interface → Wallpaper: a macOS-style thumbnail picker with 3 built-ins
(Meat Light, Meat Dark, Fantasy 3048) plus a "Choose file…" option for a custom image.

- New src/wallpaper.rs: all three built-ins are drawn procedurally per-pixel (gradients +
  soft glow; Fantasy 3048 is a cyberpunk synthwave scene — starfield, scan-lined glowing
  planet, neon perspective grid), no image assets; custom images are decoded via the image
  crate (now a cross-platform dependency). Both paths yield an RGBA buffer plus a palette
  derived from the average colour (HSL-saturated accent, brightness-based light/dark,
  average-colour tint).
- theme.slint: adds wallpaper image / active / wp-accent / wp-tint plus a frost() helper —
  when active, panel and terminal backgrounds frost translucently and pull toward the
  wallpaper's colour, the accent uses the extracted colour, and the window paints an
  always-opaque window-base. app.slint paints a cover-fit wallpaper behind everything;
  proc_window.slint gets the same wallpaper layer + a frosted content backdrop so the
  detached process window matches.
- app.rs: apply_wallpaper loads the image + palette and pushes them; a new apply_dark_mode
  flips the theme AND every terminal buffer's is_dark AND re-renders (fixing greyed-out
  terminal text when switching built-in wallpapers across light/dark), shared by the theme
  toggle and wallpaper switching; sync_proc_theme mirrors the wallpaper to the process
  window. Built-ins set light/dark from brightness; custom photos keep the user's theme
  toggle for readability.
- config.rs: persists the wallpaper id; defaults to "builtin:tech" (Fantasy 3048, dark) for
  new installs and field-missing upgrades, while an explicit "none" (empty) or custom path
  is preserved; includes a unit test.
把本轮累积改动整体作为 0.4.16 发布(0.4.15 未正式发布,跳过):
- 沉浸式壁纸主题:3 张内置(简约·浅/暗、幻想3048)+ 自定义图片 + 全局沉浸配色
  (磨砂面板 + 提取主色重着色)+ 进程窗同款毛玻璃 + 默认「幻想3048 + 暗色」;
- 便携模式:配置/日志改存程序同目录 config/,向下兼容回退用户目录 (jeff141#141);
- Ctrl+F 终端内查找(右键→查找的快捷键),并登记到设置-快捷键;
- 面板拖动吸附停靠(资源面板 + SFTP,四边)+ 布局持久化;
- 修复含中文行的选区复制/查找列错位(宽字符) (jeff141#132);
- 历史命令搜索框移到下拉框底部 (jeff141#131);
- macOS 欢迎页布局错位修复。

仅版本号与 CHANGELOG 标题改动;功能代码已在前序提交落地。

Ship this cycle's accumulated work as 0.4.16 (0.4.15 was never formally released, skipped):
- Immersive wallpaper theming: 3 built-ins (Meat Light/Dark, Fantasy 3048) + custom images
  + global tinting (frosted panels + accent recoloured from the image) + a matching frosted
  process window + a "Fantasy 3048 + dark" default;
- Portable mode: config/logs move to a config/ folder beside the app, falling back to the
  per-user dir for compatibility (jeff141#141);
- Ctrl+F terminal find (the right-click → Find shortcut), listed under Settings → Shortcuts;
- Drag-to-dock panels (resource panel + SFTP, all four edges) + layout persistence;
- Fix copy/find column drift on lines with CJK (wide) glyphs (jeff141#132);
- Move the command-history search box to the bottom of the dropdown (jeff141#131);
- Fix the macOS welcome-page layout.

Version number and CHANGELOG heading only; the feature code landed in prior commits.
承接 jeff141#86 的 keyboard-interactive 回退:之前的实现对每个提示都用密码应答,所以
JumpServer 开了 MFA 后,第二个"验证码"提示被塞了密码,必然失败——这正是公众号评论
区说"不支持 JumpServer"的真实原因。
本次让 keyboard-interactive 变成真正交互式:
- keyboard_interactive_auth():第一个"密码"挑战用已保存的密码自动应答(沿用 jeff141#86),
  其余挑战(MFA / 验证码)弹窗向用户索取;looks_like_mfa() 按中英文关键字
  (code/otp/mfa/2fa/factor/duo/验证码/动态/令牌…)区分两类提示,已加单元测试。
- 新增 SessionEvent::MfaPrompt + MfaResponder(oneshot 回送),复用 jeff141#109/jeff141#110 既有的
  "握手阻塞 → 弹窗 → 答案回送"管线。
- UI:新增「双重验证 (MFA)」模态框,显示服务器原始提示文字,输入框按 echo 决定是否
  隐藏,回车即提交(为此给 LabeledInput 加了 accepted 回调)。
- 终端与 SFTP 并发连同一会话时,第二个验证码提示并入同一弹窗,只需输入一次;不跨重连
  缓存答案,所以输错码后重连会重新弹框,而不是静默重放旧码。
已用一台仅启用 keyboard-interactive 的 MFA mock 服务端做端到端验证:错码被拒、正确码
进入 shell;终端+SFTP 只问一次;重连正常复弹。
feat(ssh): support MFA / verification-code login on bastions that force MFA
(JumpServer etc.) (jeff141#86)
Builds on the jeff141#86 keyboard-interactive fallback: the old code answered every
prompt with the password, so once JumpServer enables MFA the second
"verification code" prompt got the password and auth always failed — the real
reason behind the "doesn't support JumpServer" complaints.
This makes keyboard-interactive truly interactive:
- keyboard_interactive_auth() answers the first password challenge automatically
  with the stored password (as in jeff141#86) and pops a dialog asking the user for any
  other challenge (MFA / code). looks_like_mfa() classifies prompts by EN/CN
  keywords (code/otp/mfa/2fa/factor/duo/验证码/动态/令牌…); unit-tested.
- Adds SessionEvent::MfaPrompt + MfaResponder (oneshot reply), reusing the
  existing jeff141#109/jeff141#110 "block the handshake → prompt → reply" plumbing.
- UI: a new "Two-factor (MFA)" modal showing the server's own prompt text, with
  the field hidden per the echo flag and Enter-to-submit (LabeledInput gained an
  accepted callback).
- When the shell and SFTP connect concurrently the second code prompt merges
  into the same dialog (typed once); answers are NOT cached across attempts, so a
  wrong code re-prompts on reconnect instead of being silently replayed.
Verified end-to-end against a keyboard-interactive-only MFA mock server: a wrong
code is rejected, the correct code opens a shell, shell+SFTP ask once, and
reconnect re-prompts correctly.
反馈:开 3 个 SSH 连接静置数小时,程序仍占用约 10% CPU(Tabby 无此问题)。根因是
即便窗口在后台、终端空闲,几个周期性定时器仍在不停触发**整窗重绘**(Slint+femtovg
每帧重画整个窗口,现在还叠了壁纸),其中最稳定的是 ~530ms 的光标闪烁和 1Hz 的本地
系统采样。

本次按"竞品(Tabby / Windows Terminal)在窗口隐藏/失焦时暂停渲染与轮询"的思路处理:
- 光标闪烁定时器仅在窗口获焦时运行(Theme.window-focused);失焦时光标改为常亮——
  既不消失也不再触发重绘。
- 用 winit 的 Focused / Occluded / Resized(0×0,即 Windows 最小化)事件维护窗口三态:
  最小化/被遮挡 → 1Hz 采样器完全暂停(连 sysinfo 刷新也省);可见但失焦 → 降到 ~5s;
  前台 → 原样 1Hz。焦点回来立即恢复满速。

实测:无会话、窗口置于后台时 CPU 从空转降到约 0.5%(16 核归一化)。带活动会话时,远程
监控(每 2s 回传)仍在跑,后续可按需降频(本 issue 的跟进项)。

perf(ui): cut idle CPU — stop the cursor blink when unfocused and pause/throttle
the system sampler in the background (jeff141#127)

Report: three idle SSH connections left for hours still burned ~10% CPU (Tabby
doesn't). The cause: even with the window backgrounded and terminals idle, a few
periodic timers kept forcing full-window repaints (Slint+femtovg redraws the
whole window per frame, now with a wallpaper on top) — chiefly the ~530ms cursor
blink and the 1 Hz local system sampler.

Following how Tabby / Windows Terminal pause rendering and polling when the window
is hidden/unfocused:
- The cursor-blink timer only runs while the window is focused (Theme.window-
  focused); when unfocused the cursor shows solid, so it neither vanishes nor
  keeps repainting.
- winit Focused / Occluded / Resized(0×0 = Windows minimize) events drive a
  three-state activity: minimized/occluded → the 1 Hz sampler fully pauses (no
  sysinfo refresh either); visible-but-unfocused → backs off to ~5 s; foreground
  → full 1 Hz. Refocusing resumes immediately.

Measured: with no session and the window backgrounded, CPU drops from spinning to
~0.5% (normalised over 16 cores). With live sessions the 2 s remote monitor still
runs; throttling that is the follow-up (jeff141#127).
…141#135)

README 的 macOS 段过时:写成 `tar -xzf …macos-*.tar.gz` 解压、对裸 `meatshell` 二进制
去隔离再 `./meatshell` 运行。但发布流程(release.yml 用 `ditto -c -k --keepParent`)产出的
是 .zip,里面是 meatshell.app 应用包,三条命令全对不上,导致用户照着做失败 (jeff141#135)。

改为与真实产物一致:解压 .zip → (可选)移入 /Applications → 对 .app 去 com.apple.quarantine
隔离属性(否则提示「已损坏,无法打开」)→ open。中英两版 README 同步更新。

docs: fix the macOS install steps — the artifact is a .zip + meatshell.app, not a
.tar.gz + bare binary (jeff141#135)

The README's macOS section was stale: it said to `tar -xzf …macos-*.tar.gz`, clear
quarantine on a bare `meatshell` binary, and run `./meatshell`. But the release
workflow (release.yml, `ditto -c -k --keepParent`) ships a .zip containing the
meatshell.app bundle, so none of those three commands match what users download
(jeff141#135). Updated both README.md and README.en.md to: unzip the .zip → optionally
move to /Applications → clear com.apple.quarantine on the .app (otherwise macOS
says it "is damaged") → open.
…1#140)

① Windows/pwsh 服务端 shell 失效(回归,自 0.4.7):cwd 跟随注入的是 POSIX 专用 hook,
   pwsh/cmd 跑不了也不回吐 OSC 7,客户端一直屏蔽输出 → 终端空白卡死(SFTP 独立通道不受影响)。
   - ssh.rs:给"屏蔽输出等 OSC 7"加 1.2s 超时兜底,到点放行并尽量抹掉回显的注入行;
   - 新增会话选项 disable_shell_integration(会话编辑「高级」复选框),勾上后完全不注入、
     也不开资源监控,Windows 上干净处理。
② 纯键盘命令历史检索:命令框 Ctrl+R 唤出历史并聚焦搜索框,↑↓ 选、回车执行、Esc 关并回终端;
   终端 Ctrl+Shift+R 跳过去(用 Shift 保留 shell 自身反向搜索);选中行高亮;已登记到 设置→快捷键。
③ 关窗 + Esc:关闭确认弹窗抢键盘焦点(回车/空格关、Esc 取消),终端背后不再误输入;
   设置/关闭确认/凭据/MFA 均支持 Esc 关闭(为此给 LabeledInput 加 canceled/auto-focus);
   快捷键弹窗超长可滚动。

未采用 FinalShell 的 Alt / Ctrl 双击(冲突 + 双击判定别扭),改用 Ctrl+R / Ctrl+Shift+R。

feat(ssh,ui): fix Windows/pwsh server shell + close/Esc dialog UX + keyboard-only
history search (jeff141#140)

① Windows/pwsh server shell broke (regression since 0.4.7): the cwd-follow setup injects
   a POSIX-only hook that pwsh/cmd can't run and never echoes the OSC 7 the client waits
   for, so output stayed hidden and the terminal went blank (SFTP, a separate channel, was
   fine).
   - ssh.rs: the output-suppression window now has a 1.2 s timeout — past it we stop hiding
     output and best-effort strip the echoed setup line;
   - new per-session disable_shell_integration option (advanced section) that skips the hook
     and the resource monitor entirely for a clean Windows experience.
② Keyboard-only command-history search: Ctrl+R in the command box opens history with its
   search box focused (↑↓ select, Enter run, Esc close/return to terminal); Ctrl+Shift+R
   jumps there from the terminal (Shift keeps the shell's reverse-search); selected row
   highlighted; listed under Settings → Shortcuts.
③ Close/Esc: the close-confirm dialog grabs keyboard focus (Enter/Space close, Esc cancel)
   so the terminal no longer receives input behind it; Settings / close-confirm / credential
   / MFA close with Esc (LabeledInput gained canceled/auto-focus); the shortcuts dialog
   scrolls when too tall.

Uses Ctrl+R / Ctrl+Shift+R instead of FinalShell's Alt / Ctrl-double (conflict-prone).
0.4.17 汇总 0.4.16 之后的改动:MFA / 验证码登录 (jeff141#86)、空闲降耗 (jeff141#127)、macOS 安装说明
修正 (jeff141#135)、Windows/pwsh shell 修复 + 关窗/Esc + 键盘历史检索 (jeff141#140)。详见 CHANGELOG。

chore(release): release 0.4.17

Bundles the work since 0.4.16: MFA / verification-code login (jeff141#86), idle-CPU cuts (jeff141#127),
corrected macOS install docs (jeff141#135), and the Windows/pwsh shell fix + close/Esc UX +
keyboard history search (jeff141#140). See CHANGELOG.
现象:SFTP 用一阵子后点目录无反应、底部提示"文件夹读取失败",删除/上传/下载全废。
根因:SFTP(和终端)连接都没有 keepalive,只靠 inactivity_timeout。空闲时没有任何流量,
连接被 NAT / 防火墙的空闲 TCP 超时、服务器 sshd 空闲断开、或 russh 自己的 inactivity_timeout
掐掉,之后不会自动重连 → 操作全失败。终端不受影响是因为资源监控每 2s 在跑、连接不空闲;
SFTP 没有这种持续流量,所以先掉。

修复:两条连接都改用 russh 的 keepalive_interval=30s(替掉裸 inactivity_timeout)——空闲时
每 30s 发保活包,连接不再被中间设备/服务器掐断;真死了由 keepalive_max(默认 3,约 90s)关闭。
顺带也保护了 jeff141#140 "禁用 shell 集成"后会空闲的终端连接。

注:这与 jeff141#127 省电模式无关——那只暂停本地 UI 采样,不碰任何 SSH/SFTP 连接。

fix(ssh,sftp): add a connection keepalive to fix SFTP dying after idle (jeff141#160)

Symptom: after a while the SFTP panel stops responding — clicking a directory does
nothing and a "folder read failed" line appears; delete/upload/download all fail.
Cause: neither the SFTP nor the terminal connection set a keepalive; they relied only on
inactivity_timeout. When idle, no traffic flows, so the connection is dropped by NAT /
firewall idle-TCP timeouts, the server's sshd idle disconnect, or russh's own
inactivity_timeout — and nothing reconnects. The terminal survives only because the
resource monitor streams every 2 s; SFTP has no such traffic, so it dies first.

Fix: both connections now use russh's keepalive_interval = 30 s (replacing the bare
inactivity_timeout), so idle connections are kept alive; keepalive_max (default 3, ~90 s)
still closes a genuinely dead connection. Also protects the terminal connection that can
now go idle when shell integration is disabled (jeff141#140).

Note: unrelated to the jeff141#127 power-saving mode, which only pauses the local UI sampler and
never touches SSH/SFTP connections.
)

现象:git clone / curl 输出过程中,终端突然按极窄宽度(约 10 字符)折行、内容错乱。
根因:term-cols = max(10, floor((width-20)/cell-w)),且 term-cols 一变就立刻 resize PTY。
某帧布局回流时 root.width 瞬间读成 ≈0,term-cols 塌到下限 10,于是把远程 PTY 缩到 10 列并
reflow vt100,正在输出的 git/curl 就按 10 列折行;宽度随后恢复,但内容已乱。
修复:把 PTY resize 抽成 apply_terminal_resize,resize 回调加 150ms 单次防抖——只应用"静置后"
的最终尺寸,一闪而过的坏值不会发到服务器;顺带让拖窗口也只在停手后 resize 一次。
fix(terminal): debounce PTY resize to fix git clone/curl output wrapping at ~10 cols (jeff141#163)
Symptom: during a git clone / curl, output suddenly wraps at a tiny width (~10 chars)
and garbles. Cause: term-cols = max(10, floor((width-20)/cell-w)) and every term-cols
change immediately resizes the PTY. A layout reflow can momentarily report a near-zero
root.width, collapsing term-cols to its 10-col floor, which resizes the remote PTY to 10
columns and reflows vt100 — so the in-flight git/curl output wraps at 10. The width then
recovers, but the output is already mangled.
Fix: factor the resize into apply_terminal_resize and debounce the callback by 150 ms, so
only the settled size is applied and a transient bad value never reaches the server (also
stops spamming the server during a window drag).
自绘标题栏的无边框窗口在 Windows 上丢了系统圆角和投影,用 DWM 补回:
- DwmSetWindowAttribute(DWMWA_WINDOW_CORNER_PREFERENCE = DWMWCP_ROUND) → Win11 圆角
  (Win10 自动忽略,无害);
- DwmExtendFrameIntoClientArea(1px 边距) → 找回 WS_POPUP 窗口本来没有的系统投影
  (那 1px 是玻璃,被不透明背景盖住,只露阴影)。
HWND 经 winit 获取(新增 raw-window-handle 0.6,与 winit 同版本);DWM 用直接 FFI 链
dwmapi,不引 windows-sys。在 winit 首个窗口事件里应用一次——此时 HWND 必已创建;早前
用 run 前的定时器会因 HWND 尚未就绪而 no-op(所以一开始没生效)。仅 cfg(windows),
其他平台空实现。

feat(ui): native rounded corners + drop shadow for the frameless window on Win11 (jeff141#162/jeff141#166)

The borderless window (we draw our own title bar) loses the OS rounded corners and shadow
on Windows. Restored via DWM:
- DwmSetWindowAttribute(DWMWA_WINDOW_CORNER_PREFERENCE = DWMWCP_ROUND) → Win11 rounded
  corners (ignored on Win10, harmless);
- DwmExtendFrameIntoClientArea (1px margin) → brings back the system shadow a WS_POPUP
  window otherwise has none of (the margin is glass, painted over by our opaque background,
  so only the shadow shows).
HWND comes from winit (new raw-window-handle 0.6 dep, matching winit's); DWM is called via
direct FFI linking dwmapi (no windows-sys). Applied once on the first winit window event,
where the HWND is guaranteed to exist — a pre-run timer no-op'd because the HWND wasn't
created yet. cfg(windows) only; a no-op elsewhere.
Mac 有 Ctrl 和 Cmd 两个键;之前 app 快捷键全用 Ctrl,占掉了物理 Ctrl,导致 nano 里
^X 等控制键发不到 shell(得按 Cmd 才行)。现按 macOS 习惯:app 快捷键(查找/复制/粘贴/
历史/保存)在 Mac 上用 ⌘(meta),物理 Ctrl 原样直达 shell;其它 ⌘ 组合被吞掉不当字符
输入。命令框的 Ctrl+A/E/K/U 行编辑仍用 Ctrl(macOS 标准 readline 绑定)。设置→快捷键
在 Mac 上显示 ⌘/⌃。全部以 Theme.is-mac 门控,Windows/Linux 行为不变。

feat(ui): use Cmd (⌘) for app shortcuts on macOS, freeing Ctrl for the terminal (jeff141#158)

macOS has both Ctrl and Cmd; the app shortcuts all used Ctrl, hijacking the physical
Ctrl key so terminal control keys (^X in nano, etc.) couldn't reach the shell. Now, the
macOS way: app shortcuts (find/copy/paste/history/save) use ⌘ (meta) on macOS while the
physical Ctrl passes straight through to the shell; any other ⌘ combo is swallowed rather
than typed. The command box's Ctrl+A/E/K/U line editing stays on Ctrl (standard macOS
readline). Settings → Shortcuts shows ⌘/⌃ on macOS. All gated on Theme.is-mac, so
Windows/Linux behaviour is unchanged.
设置与下载按钮会随 toolbar-x/y-off 在资源面板停靠右/上时位移,但两个下拉菜单用的是
固定坐标,于是面板停右边时菜单飘到资源面板上。现给两个下拉的 x 减去 toolbar-x-off、
y 加上 toolbar-y-off,与按钮位移一致,菜单始终贴着按钮。

fix(ui): settings/download dropdowns follow their buttons so they don't land on
the docked resource panel

The gear and download buttons shift by toolbar-x/y-off when the resource panel docks
right/top, but the two dropdown menus used fixed coordinates, so the menu floated over
the resource panel when it was docked right. The dropdowns now subtract toolbar-x-off
and add toolbar-y-off, matching the buttons, so each menu stays anchored to its button.
沉浸壁纸 + 浅色模式下,text-secondary(#6e6e73)、尤其 text-muted(#aeaeb2)在偏白的
磨砂背景上太浅、观感差(副标题、磁盘/路径标签、内存明细、设置说明等)。把浅色那一侧
分别加深到 #55555a / #7c7c82,层级不变、对比度提上来;深色模式不变。
fix(ui): darken secondary/muted text in light mode for readability on the bright background
With the immersive wallpaper + light mode, text-secondary (#6e6e73) and especially
text-muted (#aeaeb2) were too faint on the bright frosted background (subtitle, disk/path
labels, memory detail, settings hints…). The light-mode values are darkened to #55555a /
#7c7c82 — same hierarchy, better contrast; dark mode unchanged.
0.4.17 之后的改动汇总(详见 CHANGELOG):
- Win11 无边框窗口圆角 + 投影 (jeff141#162/jeff141#166)
- macOS 上 app 快捷键改用 Cmd,释放 Ctrl 给终端 (jeff141#158)
- SFTP 闲置失效修复(加 keepalive)(jeff141#160)
- git clone/curl 输出乱折行修复(PTY resize 防抖)(jeff141#163)
- 设置/下载下拉跟随按钮、浅色文字加深、展开按钮旁黑块等界面修复
- 记录 RUSTSEC-2026-0154 不可达、暂不升级 russh (jeff141#151)

chore(release): release 0.4.18

Changes since 0.4.17 (see CHANGELOG):
- Win11 frameless rounded corners + drop shadow (jeff141#162/jeff141#166)
- macOS app shortcuts use Cmd, freeing Ctrl for the terminal (jeff141#158)
- fix SFTP dying after idle via a connection keepalive (jeff141#160)
- fix git clone/curl output wrapping at ~10 cols via PTY-resize debounce (jeff141#163)
- UI fixes: dropdowns follow their buttons, darker light-mode text, no wallpaper
  black block behind the expand button
- document RUSTSEC-2026-0154 as unreachable; hold russh (jeff141#151)
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants