diff --git a/AGENTS.md b/AGENTS.md index 05248ad..2793ccb 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -2,9 +2,9 @@ ## 项目是什么 -shellr 是一个类似 Xshell / WinSCP 的 SSH 会话管理工具,基于 `gpui-kit` 0.6.1(GPUI + gpui-base + gpui-component)。界面文案用中文,标识符用英文。 +ShellRS(crate 与二进制都叫 `shellrs`)是一个类似 Xshell / WinSCP 的 SSH 会话管理工具,基于 `gpui-kit` 0.6.1(GPUI + gpui-base + gpui-component)。界面文案用中文,标识符用英文。 -会话和分组是**真实持久化**的,存在一个 SQLite 文件里(`~/Library/Application Support/shellr/shellr.db`,`SHELLR_DATA_DIR` 可覆盖目录)。本地终端(`portable-pty` + `alacritty_terminal`)和 SSH 远程连接(`russh`)也都是真的。密码与私钥口令存在系统钥匙串里(`keyring`),**数据库里永远不出现秘密**。SFTP 双栏浏览与上传也已接入真实文件系统,支持断点续传;下载、目录同步和传输队列尚未实现。详见 `README.md`。 +会话和分组是**真实持久化**的,存在一个 SQLite 文件里(`~/Library/Application Support/shellrs/shellrs.db`,`SHELLRS_DATA_DIR` 可覆盖目录)。本地终端(`portable-pty` + `alacritty_terminal`)和 SSH 远程连接(`russh`)也都是真的。密码与私钥口令存在系统钥匙串里(`keyring`),**数据库里永远不出现秘密**。SFTP 双栏浏览与上传也已接入真实文件系统,支持断点续传;下载、目录同步和传输队列尚未实现。详见 `README.md`。 已确认的产品决定:WinSCP 式双栏文件浏览器是每个会话独立的「SFTP」Dock 标签页;暂不做传输队列和 Dock 布局持久化;首次启动是空库,不预置任何分组或会话;分组支持任意层级嵌套,会话也可以不属于任何分组(渲染在树的根层级);删除分组会连同其子分组和里面的会话一起删,删除前确认并写明数量。 @@ -22,8 +22,8 @@ cargo test --test ui new_session -- --nocapture # 按名称片段跑单个 UI cargo clippy --all-targets -- -D warnings # 必须无警告 cargo fmt --check -sqlite3 ~/Library/Application\ Support/shellr/shellr.db '.schema' # 查落盘结果 -security find-generic-password -s shellr -a "password:@:" # 查钥匙串条目 +sqlite3 ~/Library/Application\ Support/shellrs/shellrs.db '.schema' # 查落盘结果 +security find-generic-password -s shellrs -a "password:@:" # 查钥匙串条目 cargo test -- --ignored # 会读写真实钥匙串的测试,默认跳过 ``` @@ -35,7 +35,7 @@ cargo test -- --ignored # 会读写真实钥匙串的测试 单个二进制 crate,带 `src/lib.rs`,这样 `tests/ui.rs` 能驱动生产环境的 `Workspace`。模块按能力划分,将来可拆成独立 crate;功能模块不得依赖 `workspace`(窗口壳),也不得触碰彼此的内部实现。 -- `app/` — `actions.rs` 定义全部用户命令(`gpui_kit::actions!` 单元动作 + 携带 `SessionId` / `GroupId` 的 `session_action!` / `group_action!` 动作);`paths.rs` 解析数据目录(`SHELLR_DATA_DIR` → `dirs::data_dir()/shellr`)与数据库路径;`mod.rs` 绑定快捷键、初始化 gpui-kit 并 `set_locale("zh-CN")`;`assets.rs` 用 `icon_assets!` 把额外的 Lucide 图标并入默认图标包——额外图标用 `CatalogIcon::*`,默认包用 `IconName::*`;`OS_ICONS` 另外嵌入 `assets/icons/os/*.svg`(各操作系统的真实 logo,取自 Simple Icons,CC0;Windows 那个是自己画的四格,Simple Icons 已下架),用 `Icon::default().path(os.icon_path())` 渲染。 +- `app/` — `actions.rs` 定义全部用户命令(`gpui_kit::actions!` 单元动作 + 携带 `SessionId` / `GroupId` 的 `session_action!` / `group_action!` 动作);`paths.rs` 解析数据目录(`SHELLRS_DATA_DIR` → `dirs::data_dir()/shellrs`)与数据库路径;`mod.rs` 绑定快捷键、初始化 gpui-kit 并 `set_locale("zh-CN")`;`assets.rs` 用 `icon_assets!` 把额外的 Lucide 图标并入默认图标包——额外图标用 `CatalogIcon::*`,默认包用 `IconName::*`;`OS_ICONS` 另外嵌入 `assets/icons/os/*.svg`(各操作系统的真实 logo,取自 Simple Icons,CC0;Windows 那个是自己画的四格,Simple Icons 已下架),用 `Icon::default().path(os.icon_path())` 渲染。 - `session/` — `model.rs`(Session / SessionGroup / SessionDraft / GroupDraft / `HostOs`;`Session.group` 与 `SessionGroup.parent` 都是 `Option`,`Session.os` 是 `Option`)、`database.rs`(`SessionDatabase`:rusqlite 连接、`PRAGMA user_version` 迁移、行与模型的映射)、`store.rs`(`SessionStore` 实体,**内存是唯一事实来源**,写穿到数据库)、`outline.rs`(纯函数:store → `TreeItem`,递归铺开嵌套分组;节点 id 形如 `g:` / `s:`;`group_options` 给表单提供按全路径标注的分组列表)、`session_panel.rs`(左侧 Dock 面板:搜索 + 树 + 右键菜单)、`session_dialog.rs`、`group_dialog.rs`(`GroupForm`、`open_group_dialog`、`confirm_delete_group`)。 - `terminal/` — 本地终端是真的:`transport.rs`(字节流传输的 trait)、`local_pty.rs`(`portable-pty` 实现,跑在专用线程上)、`engine.rs`(驱动 `alacritty_terminal`,解析线程 + 16ms UI 轮询批量刷新)、`terminal_view.rs`(网格渲染、选区、输入编码)、`local_terminal_panel.rs`(⌘T 开的本地标签页)、`model.rs`。远程会话的传输在 `ssh/`,中间区的标签页是 `terminal_panel.rs`。共享 prompt 协议位于 `connection.rs`,`transport.rs` 通过 `TerminalPrompt` / `TerminalPromptKind` / `TerminalSecret` 重导出保持兼容。 - `ssh/` — `probe.rs`(探测远端系统的命令与纯解析函数,外加驱动两步探测的 `HostOsProbe` 状态机)、`transport.rs`:真实的 russh 客户端。known_hosts 校验写 `/known_hosts`(**不碰** `~/.ssh/known_hosts`),认证链是 agent → publickey → password → keyboard-interactive。密码和私钥口令先查 `secrets`,查不到或被服务器拒绝才走 prompt 弹框;被拒绝时**不删**钥匙串条目,只在弹框说明里讲清原因。 diff --git a/CLAUDE.md b/CLAUDE.md index e651ddc..c5c2710 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -4,9 +4,9 @@ This file provides guidance to Claude Code (claude.ai/code) when working with co ## 项目是什么 -shellr 是一个类似 Xshell / WinSCP 的 SSH 会话管理工具,基于 `gpui-kit` 0.6.1(GPUI + gpui-base + gpui-component)。界面文案用中文,标识符用英文。 +ShellRS(crate 与二进制都叫 `shellrs`)是一个类似 Xshell / WinSCP 的 SSH 会话管理工具,基于 `gpui-kit` 0.6.1(GPUI + gpui-base + gpui-component)。界面文案用中文,标识符用英文。 -会话和分组是**真实持久化**的,存在一个 SQLite 文件里(`~/Library/Application Support/shellr/shellr.db`,`SHELLR_DATA_DIR` 可覆盖目录)。本地终端(`portable-pty` + `alacritty_terminal`)和 SSH 远程连接(`russh`)也都是真的。密码与私钥口令存在系统钥匙串里(`keyring`),**数据库里永远不出现秘密**。SFTP 双栏浏览、上传、下载和文件操作(删除、重命名、属性、新建)也已接入真实文件系统,上传和下载都支持断点续传;目录同步和传输队列尚未实现。详见 `README.md`。 +会话和分组是**真实持久化**的,存在一个 SQLite 文件里(`~/Library/Application Support/shellrs/shellrs.db`,`SHELLRS_DATA_DIR` 可覆盖目录)。本地终端(`portable-pty` + `alacritty_terminal`)和 SSH 远程连接(`russh`)也都是真的。密码与私钥口令存在系统钥匙串里(`keyring`),**数据库里永远不出现秘密**。SFTP 双栏浏览、上传、下载和文件操作(删除、重命名、属性、新建)也已接入真实文件系统,上传和下载都支持断点续传;目录同步和传输队列尚未实现。详见 `README.md`。 已确认的产品决定:WinSCP 式双栏文件浏览器是「SFTP」Dock 标签页,和终端一样每次「打开 SFTP」都新开一个(同一会话可以开多个,各自独立连接),操作逻辑对标 WinSCP Commander(两行工具栏、路径标签、WinSCP 的列、列表右键菜单、高亮多选;面板顶上不再有「用户@主机:端口」一行;本地侧与远程侧对称,本地删除进废纸篓;书签按会话、按本地 / 远程各一份持久化);暂不做传输队列、Dock 布局持久化、过滤、查找文件和目录树;首次启动是空库,不预置任何分组或会话;分组支持任意层级嵌套,会话也可以不属于任何分组(渲染在树的根层级);删除分组会连同其子分组和里面的会话一起删,删除前确认并写明数量。 @@ -24,8 +24,8 @@ cargo test --test ui new_session -- --nocapture # 按名称片段跑单个 UI cargo clippy --all-targets -- -D warnings # 必须无警告 cargo fmt --check -sqlite3 ~/Library/Application\ Support/shellr/shellr.db '.schema' # 查落盘结果 -security find-generic-password -s shellr -a "password:@:" # 查钥匙串条目 +sqlite3 ~/Library/Application\ Support/shellrs/shellrs.db '.schema' # 查落盘结果 +security find-generic-password -s shellrs -a "password:@:" # 查钥匙串条目 cargo test -- --ignored # 会读写真实钥匙串的测试,默认跳过 ``` @@ -37,12 +37,12 @@ cargo test -- --ignored # 会读写真实钥匙串的测试 单个二进制 crate,带 `src/lib.rs`,这样 `tests/ui.rs` 能驱动生产环境的 `Workspace`。模块按能力划分,将来可拆成独立 crate;功能模块不得依赖 `workspace`(窗口壳),也不得触碰彼此的内部实现。 -- `app/` — `actions.rs` 定义全部用户命令(`gpui_kit::actions!` 单元动作 + 携带 `SessionId` / `GroupId` 的 `session_action!` / `group_action!` 动作);`paths.rs` 解析数据目录(`SHELLR_DATA_DIR` → `dirs::data_dir()/shellr`)与数据库路径;`mod.rs` 绑定快捷键、初始化 gpui-kit 并 `set_locale("zh-CN")`;`assets.rs` 用 `icon_assets!` 把额外的 Lucide 图标并入默认图标包——额外图标用 `CatalogIcon::*`,默认包用 `IconName::*`;`OS_ICONS` 另外嵌入 `assets/icons/os/*.svg`(各操作系统的真实 logo,取自 Simple Icons,CC0;Windows 那个是自己画的四格,Simple Icons 已下架),用 `Icon::default().path(os.icon_path())` 渲染。 +- `app/` — `actions.rs` 定义全部用户命令(`gpui_kit::actions!` 单元动作 + 携带 `SessionId` / `GroupId` 的 `session_action!` / `group_action!` 动作);`paths.rs` 解析数据目录(`SHELLRS_DATA_DIR` → `dirs::data_dir()/shellrs`)与数据库路径;`mod.rs` 绑定快捷键、初始化 gpui-kit 并 `set_locale("zh-CN")`;`assets.rs` 用 `icon_assets!` 把额外的 Lucide 图标并入默认图标包——额外图标用 `CatalogIcon::*`,默认包用 `IconName::*`;`OS_ICONS` 另外嵌入 `assets/icons/os/*.svg`(各操作系统的真实 logo,取自 Simple Icons,CC0;Windows 那个是自己画的四格,Simple Icons 已下架),用 `Icon::default().path(os.icon_path())` 渲染。 - `session/` — `model.rs`(Session / SessionGroup / SessionDraft / GroupDraft / `HostOs`;`Session.group` 与 `SessionGroup.parent` 都是 `Option`,`Session.os` 是 `Option`)、`database.rs`(`SessionDatabase`:rusqlite 连接、`PRAGMA user_version` 迁移、行与模型的映射)、`store.rs`(`SessionStore` 实体,**内存是唯一事实来源**,写穿到数据库)、`outline.rs`(纯函数:store → `TreeItem`,递归铺开嵌套分组;节点 id 形如 `g:` / `s:`;`group_options` 给表单提供按全路径标注的分组列表)、`session_panel.rs`(左侧 Dock 面板:搜索 + 树 + 右键菜单)、`session_dialog.rs`、`group_dialog.rs`(`GroupForm`、`open_group_dialog`、`confirm_delete_group`)。 - `terminal/` — 本地终端是真的:`transport.rs`(字节流传输的 trait)、`local_pty.rs`(`portable-pty` 实现,跑在专用线程上)、`engine.rs`(驱动 `alacritty_terminal`,解析线程 + 16ms UI 轮询批量刷新)、`terminal_view.rs`(网格渲染、选区、输入编码、内容区右键菜单、查找条)、`search.rs`(查找)、`local_terminal_panel.rs`(⌘T 开的本地标签页)、`model.rs`、`font.rs`(终端字体:全局量 `TerminalFont` 与默认值 / 取值范围、`is_font_installed`、后台用的 `monospace_font_families`、设置页的 `TerminalFontPreview`)。远程会话的传输在 `ssh/`,中间区的标签页是 `terminal_panel.rs`。共享 prompt 协议位于 `connection.rs`,`transport.rs` 通过 `TerminalPrompt` / `TerminalPromptKind` / `TerminalSecret` 重导出保持兼容。 - `ssh/` — `probe.rs`(探测远端系统的命令与纯解析函数,外加驱动两步探测的 `HostOsProbe` 状态机)、`transport.rs`:真实的 russh 客户端。known_hosts 校验写 `/known_hosts`(**不碰** `~/.ssh/known_hosts`),认证链是 agent → publickey → password → keyboard-interactive。密码和私钥口令先查 `secrets`,查不到或被服务器拒绝才走 prompt 弹框;被拒绝时**不删**钥匙串条目,只在弹框说明里讲清原因。shell 起来后主循环每 5 秒用 `Handle::send_ping()`(`keepalive@openssh.com`,服务器回成功或失败都算回复)量一次往返,5 秒没回就记为超时,结果以 `TerminalTransportEvent::Latency` 送出;引擎把最新一次存成状态(和 `lifecycle` 一样,重启、退出、失败时清空),`TerminalPanel::title_suffix` 在标签栏右侧显示:< 100 ms 用 `success`,100–200 ms 用 `warning`,> 200 ms 或超时用 `danger`,分档在 `Latency::level`。 - **会话对话框的「测试连接」是真登录。** 会话模块不依赖 `ssh`:`connection.rs` 定义 `LoginTest`(表单当前值,秘密用 `Zeroizing`,`Debug` 打码)和 `ConnectionTester` 接口,工作区用和终端 / SFTP 共享的 `SshConnector` 造 `ssh::SshConnectionTester` 注入进去(`Workspace::new_with_services` 的最后一个服务参数,UI 测试注入 `FakeConnectionTester`)。测试用的秘密来源 `FormSecrets` 只读:本端点的登录密码、表单所选私钥的口令只认表单里的值(钥匙串里的旧密码不参与),其余条目回退钥匙串,从不写入。`SshPrompts::non_interactive()` 让缺密码 / 口令 / 键盘交互直接以带类型的 `MissingCredential` 失败而不弹框,失败原因由 `tester.rs` 的 `describe_test_failure` 按类型映射成中文。陌生主机密钥仍然要问:工作线程通过 channel 把问题交给表单,表单每 50 ms 轮询,在会话对话框上方弹「首次连接此主机」,信任就写入 known_hosts;密钥已变化一律判失败。结果只以通知呈现:「连接成功」,或标题「连接失败」加原因。 -- `secrets/` — 系统钥匙串(`keyring` 4:macOS Keychain / Windows 凭据管理器 / Secret Service)。`SecretRef` 决定归属:密码按 `user@host:port`(改名、复制会话都不丢,同一台机器的多个会话共用一条),私钥口令按文件路径(同一把钥匙只问一次)。`SecretStore` 的三个方法全是阻塞的,**只能**在后台执行器或传输层工作线程上调用。`InMemorySecretStore` 给测试,`NoSecretStore` 给没有钥匙串的机器(界面据此禁用密码字段)。 +- `secrets/` — 系统钥匙串(`keyring` 4:macOS Keychain / Windows 凭据管理器 / Secret Service)。`SecretRef` 决定归属:密码按 `user@host:port`(改名、复制会话都不丢,同一台机器的多个会话共用一条),私钥口令按文件路径(同一把钥匙只问一次)。`SecretStore` 的三个方法全是阻塞的,**只能**在后台执行器或传输层工作线程上调用。`InMemorySecretStore` 给测试,`NoSecretStore` 给没有钥匙串的机器(界面据此禁用密码字段)。服务名是 `SERVICE` = `shellrs`。 - `explorer/` — SFTP 标签页的界面。`explorer_panel.rs`(连接、事件轮询、`execute` 执行全部 `ExplorerCommand`、进度条)、`file_pane.rs`(一侧面板:两行工具栏、目录列表 `Select`、列表容器上的右键菜单和拖放;远程侧在这里显示「正在连接」和断开原因加「重新连接」)、`path_label.rs`(WinSCP 的路径标签:`/home/tester/` 逐级可点,悬停时从根到该级高亮,放不下时中段折成 `…`;点当前目录或双击空白处打开「打开目录/书签」)、`open_directory_dialog.rs`(WinSCP 的「打开目录」:目录输入框加本侧书签列表,添加 / 删除 / 上移 / 下移)、`file_listing.rs`(`DataTable` 委托:WinSCP 的列、选中行 / 光标的绘制、行点击与拖动)、`selection.rs`(纯函数的多选模型)、`history.rs`(后退 / 前进)、`pane_menu.rs`(右键、书签、新建菜单)、`pane_operations.rs`(把确认过的文件操作派给 SFTP 工作线程或本地提供者,完成后刷新)、`file_dialogs.rs`(删除确认、重命名 / 新建的名称表单)、`properties_dialog.rs`(权限 3×3 与八进制,`PermissionDraft` 是纯函数)、`transfer_dialog.rs`(上传 / 下载确认、冲突 / 续传 / 出错问答、关闭确认)、`model.rs`(`FileEntry` 保存原始值,`format_kilobytes` / `format_changed` / `format_rights` 负责显示)。`ExplorerAction` 统一路由到工作区;文件扫描与网络操作在后台,目录响应按请求编号丢弃过期结果。 - `sftp/` — 可注入传输与本地目录接口(`LocalDirectoryProvider` 也负责本地的废纸篓、重命名、新建和改权限)、`RemotePath`、russh-sftp 3.0.0 协议适配(`RemoteFs`:lstat / stat / read_dir / 读写 / rmdir / setstat;`list` 解析 `longname` 得到所有者,并发 `stat` 解析链接目标)、`operations.rs`(删除、重命名、新建、递归改权限,显式栈遍历,从不跟随链接)、`upload.rs` / `download.rs`(有界并发分块传输、`.filepart`、冲突与续传问答)、`journal.rs`(原子 JSON 续传记录:上传在 `upload-resume/`,下载在 `download-resume/`)、`worker.rs`(`TransferBatch` 二选一、三次自动重连)。`ssh/connection.rs` 的 `SshConnector` 与终端共享认证、钥匙串服务及 known_hosts 写锁。 - `workspace/` — `workspace_view.rs` 持有 `SessionStore`、`DockArea`、按会话登记的面板注册表,以及**全部动作处理器**;`title_bar.rs`、`status_bar.rs`、`recent_sessions.rs`(中间区没有标签页时显示的「最近连接」开始页,**不是** Dock 面板;行的右键菜单就是会话树里会话的那个菜单,两处都调用 `session::session_menu`,不许分叉)、`dock_skin.rs`(`WorkspaceDockSkin`:包一层 `DockSkin`,中间区为空时用 `deferred` 把开始页画在空的中间区之上;工作区在 `DockEvent::LayoutChanged` 时同步「中间区是否为空」并在刚变空时把焦点移到开始页)。 @@ -66,7 +66,7 @@ cargo test -- --ignored # 会读写真实钥匙串的测试 - **SFTP UI 测试完全注入。** 使用 `Workspace::new_with_services` 注入目录与传输,不访问用户服务器和钥匙串;`FakeLocalDirectory` 只记录本地操作,不真的改文件。工作线程事件由 UI 定时轮询,禁止后台线程直接唤醒 GPUI 前台任务;实体回调中的 SFTP 动作通过 `ExplorerDispatch` 延后派发,避免重入实体。 - **一个命令一个处理器。** 按钮、菜单、右键菜单、快捷键都只派发 `app/actions.rs` 里的动作,由 `Workspace` 处理。新增命令加在那里,不要写临时闭包直接改状态。 -- **设置写穿到 JSON,由工作区落到窗口上。** 设置是应用级的,不进会话数据库,存在 `settings.json`(`app::settings_path`),整文件先写临时文件再改名替换。`SettingsStore` 和 `SessionStore` 一样内存说了算:`update` 改内存 → 写文件 → `notify`,写失败发 `SettingsStoreEvent::PersistFailed`,工作区弹错误通知;`update_unnotified` 是给单元测试的纯内存一半。文件缺字段用默认值,读不出来就用默认设置并在启动时提示,文件留到下一次改动时覆盖。设置页的下拉只写 store(下拉的 setter 拿不到 `Window`);工作区 `observe_in` store 调 `settings::apply`,另外 `observe_window_appearance` 让「跟随系统」跟着系统切换,构造时在第一帧之前先应用一次。标题栏的主题开关(`ToggleTheme`)也只是把「应用外观」写成当前的反面,不再直接改主题,所以两处永远一致并且会保存。界面语言目前只影响 gpui-kit 自己的文案(`set_locale`),shellr 的文案还是中文,所以默认是「简体中文」而不是「跟随系统」;翻译完成后再改默认值。locale 是进程级全局量,UI 测试并行运行,所以 UI 测试不切换语言,只由单元测试覆盖 `locale` 的解析。 +- **设置写穿到 JSON,由工作区落到窗口上。** 设置是应用级的,不进会话数据库,存在 `settings.json`(`app::settings_path`),整文件先写临时文件再改名替换。`SettingsStore` 和 `SessionStore` 一样内存说了算:`update` 改内存 → 写文件 → `notify`,写失败发 `SettingsStoreEvent::PersistFailed`,工作区弹错误通知;`update_unnotified` 是给单元测试的纯内存一半。文件缺字段用默认值,读不出来就用默认设置并在启动时提示,文件留到下一次改动时覆盖。设置页的下拉只写 store(下拉的 setter 拿不到 `Window`);工作区 `observe_in` store 调 `settings::apply`,另外 `observe_window_appearance` 让「跟随系统」跟着系统切换,构造时在第一帧之前先应用一次。标题栏的主题开关(`ToggleTheme`)也只是把「应用外观」写成当前的反面,不再直接改主题,所以两处永远一致并且会保存。界面语言目前只影响 gpui-kit 自己的文案(`set_locale`),ShellRS 的文案还是中文,所以默认是「简体中文」而不是「跟随系统」;翻译完成后再改默认值。locale 是进程级全局量,UI 测试并行运行,所以 UI 测试不切换语言,只由单元测试覆盖 `locale` 的解析。 - **终端字体走全局量。** 终端模块不能依赖 settings,所以 `settings::apply` 把字体设成 `terminal::TerminalFont` 全局量并 `window.refresh()`,`TerminalView` 在 render(字体族、字号)和 prepaint(行高 `row_height`,按设备像素取整)里读它;格子变了,下一帧 prepaint 就按新尺寸 resize,已打开的终端立即生效。默认值就是加设置之前的样子:主题的等宽字体(macOS 上是 Menlo)、13 px、行高 1.54(13 × 1.54 取整后正好是原来的 20 px 行)。字体族存 `None` 表示跟随主题默认,下拉里选默认那一项也存 `None`。文件里的字体族只有装着才用(`is_font_installed`,进程内读一次字体列表;GPUI 找不到字体族时会换成别的字体),否则退回默认,文件里的值保留。数字在 `AppSettings::normalized` 里夹到范围并取整(字号整像素、行高两位小数),读文件和每次修改都过一遍;gpui-kit 的数字框用 `f64::to_string` 显示,步进 0.1 会显示 `1.7000000000000002`,store 取整后数字框下一次渲染会同步成取整后的值。字号用下拉不用数字框:数字框每敲一个字符就夹到最小值,输入 14 时敲下「1」就变成 8。字体下拉只列等宽字体:`monospace_font_families` 要加载每个字体族(macOS 上约 150 ms),在后台线程跑一次、进程内缓存,结果回来前下拉里只有默认字体;判定是「i」和「M」一样宽且窄于 0.8 em,后一条排除 Webdings 这种方块符号字体。预览 `TerminalFontPreview` 用和终端一样的排法(每格一个字形、宽字符后面补一个空格格子、终端的行高和配色),不是普通文字排版,否则中文的宽度和终端里对不上。 - **持久化写穿,内存说了算。** `SessionStore` 带 `Context` 的 mutator 负责「改内存 → 写库 → `notify`」,`*_unnotified` 那一半是纯内存的(给单元测试和加载用)。写库同步发生在 UI 线程:要落盘的都是用户在对话框里确认的单行写入,不是流。写失败时内存里的改动照样生效,store 发 `SessionStoreEvent::PersistFailed`,工作区订阅后弹一个错误通知——**绝不静默吞掉**。`ConnectionState` 是运行时状态,不入库;`last_connected_at` 入库,开始页的「最近连接」因此能跨启动。删除分组靠数据库两个外键的 `ON DELETE CASCADE`,内存里的 `remove_group_unnotified` 必须给出一样的结果,并把被删会话的 id 返回给工作区去关标签页。 - **主机系统每次连上都重探一遍。** 认证过、shell 起来、`Started` 发出去之后,传输层另开一条 exec 通道跑 `uname -s; cat /etc/os-release`,在主 IO 循环里和 shell 的输出一起轮询,所以终端不会为了探测多等一个往返。POSIX 那条什么都没输出(Windows 的 shell 就是这样)时再问一次 `cmd /c ver`,还是不认就放弃,会话保留原来的标记。结果沿 `TerminalTransportEvent::HostOsDetected` → 引擎 → `TerminalView` → `TerminalPanel` → 工作区 → `SessionStore::set_host_os` 这条链走,和 prompt 是同一套管线。`os` 列由 `set_host_os` 单独写,`update_session` 不碰它(同 `last_connected_at`)。标记是 `shared::HostMark`,会话树、开始页、远程终端标签和 SFTP 标签共用:探到了就用该系统的品牌色打底、白色(浅色品牌用黑色)画 logo,没探到就退回 `muted` 底色加会话名第一个字。徽章沿用 `Avatar` 的圆形画法,尺寸走 `Sizable`——树里和远程终端 / SFTP 标签上用 `small`(20px),开始页用默认的 `medium`(32px)。`AlmaLinux` 和 `macOS` 没有单一品牌色,改用 `foreground` / `background`,这也是 Apple 官方的画法。品牌色写在 `host_os!` 表里而不是渲染处,那是设计规范允许的「颜色本身即数据」那一档。会话树里分组行和其他行用 `plain_mark` 占同样宽度的槽位,否则标签对不齐。 diff --git a/Cargo.lock b/Cargo.lock index 22e489f..6b07797 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -7403,7 +7403,7 @@ dependencies = [ ] [[package]] -name = "shellr" +name = "shellrs" version = "0.1.0" dependencies = [ "alacritty_terminal", diff --git a/Cargo.toml b/Cargo.toml index ae0e85c..578185c 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,5 +1,5 @@ [package] -name = "shellr" +name = "shellrs" version = "0.1.0" edition = "2024" publish = false @@ -34,7 +34,7 @@ gpui-kit = { version = "0.6.1", features = ["test-support"] } tempfile = "3" # Optimize the rendering stack in debug builds (per gpui-kit's installation -# guide); shellr itself stays unoptimized and debuggable. +# guide); shellrs itself stays unoptimized and debuggable. [profile.dev.package] gpui-pre = { opt-level = 3 } gpui-pre-platform = { opt-level = 3 } diff --git a/README.md b/README.md index beeaae1..e9c4d0b 100644 --- a/README.md +++ b/README.md @@ -1,4 +1,4 @@ -# shellr +# ShellRS 基于 GPUI Kit 的中文 SSH 会话管理工具,提供持久化会话与分组、本地终端、SSH 多终端和对标 WinSCP 的 SFTP 双栏浏览器。 @@ -131,7 +131,7 @@ ## 断点续传和替换 -文件先写入 `<目标文件名>.filepart`。写入请求采用有界流水线,收到每个写入确认并关闭成功后才发布正式文件;服务器支持时先调用 `fsync@openssh.com`。支持 `posix-rename@openssh.com` 的服务器使用原子替换;其他服务器先将旧文件移到 `.shellr-.backup`,再发布新文件,此兼容路径不保证原子性。发布响应丢失时根据受管理临时路径、阶段、类型和大小恢复,无法安全判断则保留恢复文件并报告。 +文件先写入 `<目标文件名>.filepart`。写入请求采用有界流水线,收到每个写入确认并关闭成功后才发布正式文件;服务器支持时先调用 `fsync@openssh.com`。支持 `posix-rename@openssh.com` 的服务器使用原子替换;其他服务器先将旧文件移到 `.shellrs-.backup`,再发布新文件,此兼容路径不保证原子性。发布响应丢失时根据受管理临时路径、阶段、类型和大小恢复,无法安全判断则保留恢复文件并报告。 续传记录保存在应用数据目录的 `upload-resume/`,采用版本化 JSON 和原子写入,不写入密码、口令或私钥,不改变会话数据库结构。记录用于绑定端点、主机指纹、来源、目标和安全替换阶段。续传方式与 WinSCP 相同:读取远端 `.filepart` 的当前大小,在本地文件中跳过相同长度并继续传输;使用者需确保来源仍是同一版本。上传过程不会为了校验而回读远端文件内容。 @@ -156,7 +156,7 @@ cargo clippy --all-targets -- -D warnings cargo fmt --check ``` -数据目录默认是系统应用数据目录中的 `shellr`,可通过 `SHELLR_DATA_DIR` 覆盖。会话保存在 `shellr.db`,主机信任记录保存在该目录的 `known_hosts`,凭据使用系统钥匙串。 +数据目录默认是系统应用数据目录中的 `shellrs`,可通过 `SHELLRS_DATA_DIR` 覆盖。会话保存在 `shellrs.db`,主机信任记录保存在该目录的 `known_hosts`,凭据使用系统钥匙串(服务名 `shellrs`)。 `src/sftp/tests.rs` 使用临时目录与故障注入覆盖上传和下载的文件内容、空目录、链接、覆盖、权限、取消、源文件变化、乱序与短读、续传及替换各阶段恢复,以及删除、重命名、新建和递归改权限不跟随链接。Unix 协议测试使用本机 OpenSSH `sftp-server`(macOS `/usr/libexec/sftp-server`,Linux `/usr/lib/openssh/sftp-server`);工作线程测试仅监听 `127.0.0.1` 的随机端口,使用测试凭据。限制本地套接字的沙箱会跳过该部分,需要在允许回环套接字的环境运行。 diff --git a/src/app/actions.rs b/src/app/actions.rs index 467eb89..0d9f1f3 100644 --- a/src/app/actions.rs +++ b/src/app/actions.rs @@ -11,7 +11,7 @@ use crate::{ }; gpui_kit::actions!( - shellr, + shellrs, [ /// Open the new-session dialog for a session at the root of the tree. NewSession, @@ -67,7 +67,7 @@ macro_rules! session_action { ($(#[$doc:meta])* $name:ident) => { $(#[$doc])* #[derive(Action, Clone, PartialEq, Eq, Deserialize)] - #[action(namespace = shellr, no_json)] + #[action(namespace = shellrs, no_json)] pub struct $name(pub SessionId); }; } @@ -76,7 +76,7 @@ macro_rules! group_action { ($(#[$doc:meta])* $name:ident) => { $(#[$doc])* #[derive(Action, Clone, PartialEq, Eq, Deserialize)] - #[action(namespace = shellr, no_json)] + #[action(namespace = shellrs, no_json)] pub struct $name(pub GroupId); }; } @@ -85,7 +85,7 @@ macro_rules! local_terminal_action { ($(#[$doc:meta])* $name:ident) => { $(#[$doc])* #[derive(Action, Clone, PartialEq, Eq, Deserialize)] - #[action(namespace = shellr, no_json)] + #[action(namespace = shellrs, no_json)] pub struct $name(pub LocalTerminalId); }; } @@ -94,7 +94,7 @@ macro_rules! explorer_action { ($(#[$doc:meta])* $name:ident) => { $(#[$doc])* #[derive(Action, Clone, PartialEq, Eq, Deserialize)] - #[action(namespace = shellr, no_json)] + #[action(namespace = shellrs, no_json)] pub struct $name(pub ExplorerId); }; } @@ -103,7 +103,7 @@ macro_rules! remote_terminal_action { ($(#[$doc:meta])* $name:ident) => { $(#[$doc])* #[derive(Action, Clone, PartialEq, Eq, Deserialize)] - #[action(namespace = shellr, no_json)] + #[action(namespace = shellrs, no_json)] pub struct $name(pub RemoteTerminalId); }; } @@ -157,7 +157,7 @@ group_action!( /// Move a session-tree row by dropping it beside a peer or into a group. #[derive(Action, Clone, PartialEq, Eq, Deserialize)] -#[action(namespace = shellr, no_json)] +#[action(namespace = shellrs, no_json)] pub struct MoveSessionNode { pub source: SessionNode, pub destination: NodeDrop, @@ -237,7 +237,7 @@ impl CloseScope { /// Close several tabs of the tab bar that holds `tab`, each through its own /// close path. #[derive(Action, Clone, PartialEq, Eq, Deserialize)] -#[action(namespace = shellr, no_json)] +#[action(namespace = shellrs, no_json)] pub struct CloseTabs { pub tab: CenterTab, pub scope: CloseScope, @@ -377,7 +377,7 @@ pub enum ExplorerCommand { CloseConfirmed, } #[derive(Action, Clone, PartialEq, Eq, Deserialize)] -#[action(namespace = shellr, no_json)] +#[action(namespace = shellrs, no_json)] pub struct ExplorerAction { explorer: ExplorerId, command: ExplorerCommand, @@ -408,7 +408,7 @@ impl ExplorerAction { /// A file-list key binding. The binding carries the pane side, and the /// workspace sends it to the explorer that holds focus. #[derive(Action, Clone, PartialEq, Eq, Deserialize)] -#[action(namespace = shellr, no_json)] +#[action(namespace = shellrs, no_json)] pub struct ExplorerShortcut(pub ExplorerCommand); /// UI entities dispatch after their update has finished so Workspace can read diff --git a/src/app/paths.rs b/src/app/paths.rs index bfbf6c4..0193a3e 100644 --- a/src/app/paths.rs +++ b/src/app/paths.rs @@ -1,19 +1,19 @@ -//! Where shellr keeps its data on disk. +//! Where ShellRS keeps its data on disk. use std::path::PathBuf; /// Overrides the data directory. Set by tests and handy during development. -const DATA_DIR_ENV: &str = "SHELLR_DATA_DIR"; +const DATA_DIR_ENV: &str = "SHELLRS_DATA_DIR"; -/// The directory holding shellr's own files, e.g. -/// `~/Library/Application Support/shellr` on macOS. Falls back to the +/// The directory holding ShellRS's own files, e.g. +/// `~/Library/Application Support/shellrs` on macOS. Falls back to the /// current directory on the platforms where `dirs` has nothing to offer. pub fn data_dir() -> PathBuf { if let Some(dir) = std::env::var_os(DATA_DIR_ENV) { return PathBuf::from(dir); } dirs::data_dir() - .map(|dir| dir.join("shellr")) + .map(|dir| dir.join("shellrs")) .unwrap_or_else(|| PathBuf::from(".")) } @@ -22,7 +22,7 @@ pub fn data_dir() -> PathBuf { pub fn database_path() -> std::io::Result { let dir = data_dir(); std::fs::create_dir_all(&dir)?; - Ok(dir.join("shellr.db")) + Ok(dir.join("shellrs.db")) } /// The interface settings, a small JSON file beside the database. @@ -32,7 +32,7 @@ pub fn settings_path() -> std::io::Result { Ok(dir.join("settings.json")) } -/// shellr's private host-key trust store. It deliberately does not read or +/// ShellRS's private host-key trust store. It deliberately does not read or /// modify OpenSSH's `~/.ssh/known_hosts`. pub fn known_hosts_path() -> std::io::Result { let dir = data_dir(); diff --git a/src/explorer/explorer_panel.rs b/src/explorer/explorer_panel.rs index 1205108..bf4674e 100644 --- a/src/explorer/explorer_panel.rs +++ b/src/explorer/explorer_panel.rs @@ -128,7 +128,7 @@ impl ExplorerPanel { let transport = provider.create(&session); let failed = sender.clone(); if let Err(error) = std::thread::Builder::new() - .name("shellr-sftp".into()) + .name("shellrs-sftp".into()) .spawn(move || { if let Err(error) = transport.run(receiver, sender.clone()) { let _ = sender.send_blocking(SftpEvent::Disconnected(error.to_string())); diff --git a/src/lib.rs b/src/lib.rs index 1b79150..f86957b 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -1,4 +1,4 @@ -//! shellr: an SSH session manager with real SSH and local terminals. +//! ShellRS: an SSH session manager with real SSH and local terminals. //! //! Modules are organized by capability so they can become crates later: //! `session` (会话管理), `terminal` (终端), `explorer` (SFTP 文件浏览), diff --git a/src/main.rs b/src/main.rs index 57f88e2..959875c 100644 --- a/src/main.rs +++ b/src/main.rs @@ -1,27 +1,27 @@ use gpui_kit::component::{Root, WindowExt as _, notification::Notification}; use gpui_kit::*; -use shellr::session::{SessionDatabase, SessionStore}; -use shellr::settings::SettingsStore; +use shellrs::session::{SessionDatabase, SessionStore}; +use shellrs::settings::SettingsStore; fn main() { gpui_kit::application() - .with_assets(shellr::app::AppAssets) + .with_assets(shellrs::app::AppAssets) .run(|cx| { - shellr::init(cx); + shellrs::init(cx); cx.activate(true); - let options = shellr::workspace::window_options(cx); + let options = shellrs::workspace::window_options(cx); cx.spawn(async move |cx| { cx.open_window(options, |window, cx| { window.activate_window(); - window.set_window_title("shellr"); + window.set_window_title("ShellRS"); let (store, problem) = open_store(); let store = cx.new(|_| store); let (settings, settings_problem) = open_settings(); let settings = cx.new(|_| settings); - let workspace = - cx.new(|cx| shellr::workspace::Workspace::new(store, settings, window, cx)); + let workspace = cx + .new(|cx| shellrs::workspace::Workspace::new(store, settings, window, cx)); for problem in [problem, settings_problem].into_iter().flatten() { window.push_notification(Notification::error(problem), cx); } @@ -41,11 +41,11 @@ fn main() { /// live. A machine without one still runs; the session form says so and /// disables its password field. fn open_store() -> (SessionStore, Option) { - let secrets = shellr::secrets::system_secret_store(); + let secrets = shellrs::secrets::system_secret_store(); match load_store() { Ok(store) => (store.with_secrets(secrets), None), Err(error) => { - eprintln!("shellr: 无法打开本地数据库:{error}"); + eprintln!("shellrs: 无法打开本地数据库:{error}"); ( SessionStore::empty().with_secrets(secrets), Some(format!("无法打开本地数据库,本次运行的改动不会被保存:{error}").into()), @@ -57,7 +57,7 @@ fn open_store() -> (SessionStore, Option) { /// Load the settings file. Without a data directory the settings still /// work, only for this run; the returned message says so. fn open_settings() -> (SettingsStore, Option) { - match shellr::app::settings_path() { + match shellrs::app::settings_path() { Ok(path) => SettingsStore::load(path), Err(error) => ( SettingsStore::in_memory(), @@ -67,6 +67,6 @@ fn open_settings() -> (SettingsStore, Option) { } fn load_store() -> anyhow::Result { - let path = shellr::app::database_path()?; + let path = shellrs::app::database_path()?; Ok(SessionStore::load(SessionDatabase::open(&path)?)?) } diff --git a/src/secrets/keychain.rs b/src/secrets/keychain.rs index 74eea82..41a2901 100644 --- a/src/secrets/keychain.rs +++ b/src/secrets/keychain.rs @@ -85,7 +85,7 @@ mod tests { fn the_system_keychain_round_trips() { let store = KeychainSecretStore::new(); assert!(store.is_available(), "这台机器上没有可用的系统钥匙串"); - let secret = SecretRef::password("shellr-test", "invalid.example", 22); + let secret = SecretRef::password("shellrs-test", "invalid.example", 22); store.delete(&secret).unwrap(); assert!(store.get(&secret).unwrap().is_none()); diff --git a/src/secrets/mod.rs b/src/secrets/mod.rs index c21bea6..a41468b 100644 --- a/src/secrets/mod.rs +++ b/src/secrets/mod.rs @@ -1,6 +1,6 @@ //! 系统钥匙串(macOS Keychain Services / Windows 凭据管理器 / Secret Service)。 //! -//! shellr 的秘密只存在这里。数据库里永远不出现密码或口令,`session/database.rs` +//! ShellRS 的秘密只存在这里。数据库里永远不出现密码或口令,`session/database.rs` //! 的 `schema_never_contains_secret_columns` 守着这条不变量。 mod keychain; @@ -16,7 +16,7 @@ pub use keychain::KeychainSecretStore; pub use memory::{InMemorySecretStore, NoSecretStore}; /// 钥匙串里的服务名,所有条目共用。改了会让已保存的秘密全部失联。 -pub const SERVICE: &str = "shellr"; +pub const SERVICE: &str = "shellrs"; /// 一条秘密的身份。 /// diff --git a/src/session/database.rs b/src/session/database.rs index d93f74d..b21a64e 100644 --- a/src/session/database.rs +++ b/src/session/database.rs @@ -575,7 +575,7 @@ mod tests { #[test] fn reopening_the_same_file_keeps_the_data() { let dir = tempfile::tempdir().unwrap(); - let path = dir.path().join("shellr.db"); + let path = dir.path().join("shellrs.db"); { let db = SessionDatabase::open(&path).unwrap(); db.insert_group(&group(1, "生产", None)).unwrap(); @@ -592,7 +592,7 @@ mod tests { #[test] fn tree_order_and_parent_survive_reopening() { let dir = tempfile::tempdir().unwrap(); - let path = dir.path().join("shellr.db"); + let path = dir.path().join("shellrs.db"); { let db = SessionDatabase::open(&path).unwrap(); db.insert_group(&group(1, "生产", None)).unwrap(); diff --git a/src/session/session_dialog.rs b/src/session/session_dialog.rs index fbfa409..8933c5d 100644 --- a/src/session/session_dialog.rs +++ b/src/session/session_dialog.rs @@ -345,7 +345,7 @@ impl SessionForm { let (result_tx, result_rx) = std::sync::mpsc::channel(); let tester = self.tester.clone(); let spawned = std::thread::Builder::new() - .name("shellr-connection-test".into()) + .name("shellrs-connection-test".into()) .spawn(move || { // Nobody left to answer (the form closed) reads as "no". let trust: TrustCallback = Box::new(move |prompt| { @@ -519,7 +519,7 @@ impl Render for SessionForm { .unwrap_or_default(); let keychain = self.secrets.is_available(); let secret_note = if keychain { - "密码保存在系统钥匙串,不会写入 shellr 的数据库。" + "密码保存在系统钥匙串,不会写入 ShellRS 的数据库。" } else { "系统钥匙串不可用,这台机器上无法保存密码,每次连接都会询问。" }; diff --git a/src/settings/model.rs b/src/settings/model.rs index c39afc7..40ffca8 100644 --- a/src/settings/model.rs +++ b/src/settings/model.rs @@ -80,7 +80,7 @@ pub trait Choice: Copy + PartialEq + 'static { } } -/// 界面语言. Only gpui-kit's own strings follow it for now; shellr's copy is +/// 界面语言. Only gpui-kit's own strings follow it for now; ShellRS's copy is /// still Chinese until it is translated. #[derive(Clone, Copy, Debug, Default, PartialEq, Eq, Serialize, Deserialize)] pub enum InterfaceLanguage { diff --git a/src/settings/store.rs b/src/settings/store.rs index b44270e..435e2bf 100644 --- a/src/settings/store.rs +++ b/src/settings/store.rs @@ -139,7 +139,7 @@ mod tests { fn a_change_is_written_and_read_back() { let dir = tempfile::tempdir().unwrap(); // The data directory may not exist yet. - let path = dir.path().join("shellr").join("settings.json"); + let path = dir.path().join("shellrs").join("settings.json"); let (mut store, _) = SettingsStore::load(path.clone()); let written = store.update_unnotified(|settings| { settings.appearance = Appearance::Dark; diff --git a/src/sftp/journal.rs b/src/sftp/journal.rs index 41863a7..cbc60b1 100644 --- a/src/sftp/journal.rs +++ b/src/sftp/journal.rs @@ -68,7 +68,7 @@ impl ResumeRecord { source_metadata: None, link_target: None, temporary, - backup: parent.join(&format!(".shellr-{id}.backup"))?, + backup: parent.join(&format!(".shellrs-{id}.backup"))?, target, original, phase: PublishPhase::Writing, @@ -110,7 +110,7 @@ impl ResumeRecord { fn managed_uuid_path(path: &RemotePath, target: &RemotePath, suffix: &str) -> bool { let name = path.as_str().rsplit('/').next().unwrap_or_default(); - name.strip_prefix(".shellr-") + name.strip_prefix(".shellrs-") .and_then(|value| value.strip_suffix(suffix)) .and_then(|value| uuid::Uuid::parse_str(value).ok()) .is_some() diff --git a/src/sftp/tests.rs b/src/sftp/tests.rs index 3346515..a798d1f 100644 --- a/src/sftp/tests.rs +++ b/src/sftp/tests.rs @@ -766,7 +766,7 @@ fn paths_and_journals_reject_unsafe_identity() { .validate("endpoint", "changed key", &source, &record.target) .is_err() ); - record.temporary = RemotePath::new("/other/.shellr-evil.filepart").unwrap(); + record.temporary = RemotePath::new("/other/.shellrs-evil.filepart").unwrap(); assert!( record .validate("endpoint", "key", &source, &record.target) diff --git a/src/ssh/probe.rs b/src/ssh/probe.rs index 4173e73..50e952d 100644 --- a/src/ssh/probe.rs +++ b/src/ssh/probe.rs @@ -27,7 +27,7 @@ pub fn parse_probe(output: &str) -> Option { "OpenBSD" => Some(HostOs::OpenBsd), "NetBSD" => Some(HostOs::NetBsd), // A Linux box with no `/etc/os-release`, or one shipping an id no - // build of shellr knows, still gets the generic mark. + // build of ShellRS knows, still gets the generic mark. "Linux" => Some(distribution(output).unwrap_or(HostOs::Linux)), _ => None, } @@ -42,7 +42,7 @@ pub fn parse_windows_probe(output: &str) -> Option { /// /// `ID_LIKE` is the fallback for derivatives, so Linux Mint shows Ubuntu and /// Pop!_OS shows Debian rather than a bare penguin. It only applies when `ID` -/// itself is unknown, so a distribution shellr does know is never mistaken for +/// itself is unknown, so a distribution ShellRS does know is never mistaken for /// its parent. fn distribution(output: &str) -> Option { let mut like = None; diff --git a/src/ssh/transport.rs b/src/ssh/transport.rs index fd8fa49..e707cfc 100644 --- a/src/ssh/transport.rs +++ b/src/ssh/transport.rs @@ -86,7 +86,7 @@ impl TerminalTransport for SshTerminalTransport { let stop = Arc::new(AtomicBool::new(false)); let bridge_stop = stop.clone(); let bridge = thread::Builder::new() - .name("shellr-ssh-command-bridge".into()) + .name("shellrs-ssh-command-bridge".into()) .spawn(move || { while !bridge_stop.load(Ordering::Acquire) { match commands.recv_timeout(Duration::from_millis(20)) { @@ -220,7 +220,7 @@ impl SshTerminalTransport { let _ = channel.close().await; let _ = handle.disconnect( russh::Disconnect::ByApplication, - "shellr closed the terminal", + "ShellRS closed the terminal", "zh-CN", ).await; }).await; diff --git a/src/terminal/engine.rs b/src/terminal/engine.rs index c4d7c08..dd4ee21 100644 --- a/src/terminal/engine.rs +++ b/src/terminal/engine.rs @@ -549,7 +549,7 @@ impl TerminalRuntime { let parser_term = term.clone(); let parser_ui_events = ui_events.clone(); thread::Builder::new() - .name("shellr-terminal-parser".into()) + .name("shellrs-terminal-parser".into()) .spawn(move || { let mut processor = Processor::new(); while let Ok(event) = transport_receiver.recv_blocking() { @@ -619,7 +619,7 @@ impl TerminalRuntime { .expect("terminal parser thread"); thread::Builder::new() - .name("shellr-terminal-transport".into()) + .name("shellrs-terminal-transport".into()) .spawn(move || { if let Err(error) = transport.run(size, command_receiver, transport_events.clone()) { diff --git a/src/terminal/local_pty.rs b/src/terminal/local_pty.rs index 277b8b6..b6783f1 100644 --- a/src/terminal/local_pty.rs +++ b/src/terminal/local_pty.rs @@ -44,7 +44,7 @@ impl TerminalTransport for LocalPtyTransport { .unwrap_or_else(CommandBuilder::new_default_prog); command.env("TERM", "xterm-256color"); command.env("COLORTERM", "truecolor"); - command.env("TERM_PROGRAM", "shellr"); + command.env("TERM_PROGRAM", "ShellRS"); let mut child = pair .slave @@ -57,7 +57,7 @@ impl TerminalTransport for LocalPtyTransport { let output_events = events.clone(); let reader_thread = thread::Builder::new() - .name("shellr-pty-reader".into()) + .name("shellrs-pty-reader".into()) .spawn(move || { let mut buffer = [0_u8; 8192]; loop { diff --git a/src/terminal/terminal_view.rs b/src/terminal/terminal_view.rs index c7b48d9..850ee5f 100644 --- a/src/terminal/terminal_view.rs +++ b/src/terminal/terminal_view.rs @@ -53,7 +53,7 @@ struct FindBar { _subscription: Subscription, } -gpui_kit::actions!(shellr_terminal, [SendTab, SendBackTab]); +gpui_kit::actions!(shellrs_terminal, [SendTab, SendBackTab]); pub(crate) fn terminal_key_bindings() -> [KeyBinding; 2] { [ diff --git a/src/terminal/transport.rs b/src/terminal/transport.rs index 12d7280..3a85137 100644 --- a/src/terminal/transport.rs +++ b/src/terminal/transport.rs @@ -58,7 +58,7 @@ pub enum TerminalTransportEvent { Failed(String), } -/// One running transport. Implementations may block because shellr always +/// One running transport. Implementations may block because ShellRS always /// invokes this method on a dedicated worker thread. pub trait TerminalTransport: Send + 'static { fn run( diff --git a/src/workspace/title_bar.rs b/src/workspace/title_bar.rs index b10e6d3..cc50ee0 100644 --- a/src/workspace/title_bar.rs +++ b/src/workspace/title_bar.rs @@ -24,7 +24,7 @@ pub fn render_title_bar(sessions_visible: bool, target: &FocusHandle, cx: &App) div() .text_sm() .font_weight(FontWeight::MEDIUM) - .child("shellr"), + .child("ShellRS"), ), ) .child( diff --git a/src/workspace/workspace_view.rs b/src/workspace/workspace_view.rs index 41bb77b..cf6cbc4 100644 --- a/src/workspace/workspace_view.rs +++ b/src/workspace/workspace_view.rs @@ -56,7 +56,7 @@ use super::{ title_bar::render_title_bar, }; -const DOCK_ID: &str = "shellr-dock"; +const DOCK_ID: &str = "shellrs-dock"; const DOCK_VERSION: usize = 1; /// Interface zoom bounds for the base font, in pixels (the theme's unit). const FONT_SIZE_MIN: f32 = 12.; diff --git a/tests/ui.rs b/tests/ui.rs index 4a5b306..72cb7cd 100644 --- a/tests/ui.rs +++ b/tests/ui.rs @@ -12,31 +12,31 @@ use gpui_kit::{ MouseMoveEvent, TestAppContext, WindowHandle, point, px, size, }; -use shellr::app::{ +use shellrs::app::{ CenterTab, ClearTerminal, CloseScope, CloseTabs, CollapseAllGroups, ConnectGroup, ConnectSession, CopySessionHost, DeleteGroup, DeleteSession, DisconnectTerminal, EditSession, ExpandAllGroups, FindInTerminal, FindNextInTerminal, FindPreviousInTerminal, NewLocalTerminal, NewSessionInGroup, OpenExplorer, ReconnectTerminal, RenameGroup, RenameTerminal, }; -use shellr::connection::{ConnectionPromptKind, ConnectionTester, LoginTest, TrustCallback}; -use shellr::explorer::ExplorerId; -use shellr::secrets::{InMemorySecretStore, SecretRef, SecretStore as _}; -use shellr::session::{ +use shellrs::connection::{ConnectionPromptKind, ConnectionTester, LoginTest, TrustCallback}; +use shellrs::explorer::ExplorerId; +use shellrs::secrets::{InMemorySecretStore, SecretRef, SecretStore as _}; +use shellrs::session::{ AuthKind, ConnectionState, GroupDraft, GroupId, HostOs, SessionDatabase, SessionDraft, SessionId, SessionStore, }; -use shellr::settings::{Appearance, InterfaceLanguage, SettingsStore}; -use shellr::sftp::{ +use shellrs::settings::{Appearance, InterfaceLanguage, SettingsStore}; +use shellrs::sftp::{ DirectoryEntry, DirectoryListing, EntryKind, FileMetadata, LocalDirectoryProvider, RemotePath, SftpCommand, SftpEvent, SftpTransport, SftpTransportProvider, UploadRequest, }; -use shellr::terminal::{ +use shellrs::terminal::{ FixedRemoteTerminalTransportProvider, Latency, LocalTerminalId, RemoteTerminalId, TerminalFont, TerminalLifecycle, TerminalPrompt, TerminalPromptField, TerminalPromptKind, TerminalPromptReply, TerminalSize, TerminalTransport, TerminalTransportCommand, TerminalTransportEvent, TerminalTransportFactory, }; -use shellr::workspace::Workspace; +use shellrs::workspace::Workspace; /// Seeded session ids, in insertion order (see `SessionStore::seed`). const WEB_01: u64 = 1; @@ -72,7 +72,7 @@ fn open_workspace_with_tester( store: SessionStore, tester: Arc, ) -> (WindowHandle, Entity) { - cx.update(shellr::init); + cx.update(shellrs::init); let mut workspace = None; let handle = cx.open_window(size(px(1280.), px(800.)), |window, cx| { let store = cx.new(|_| store); @@ -325,7 +325,7 @@ fn open_workspace_with_factory( cx: &mut TestAppContext, factory: Arc, ) -> (WindowHandle, Entity) { - cx.update(shellr::init); + cx.update(shellrs::init); let mut workspace = None; let handle = cx.open_window(size(px(1280.), px(800.)), |window, cx| { let store = cx.new(|_| SessionStore::seed()); @@ -356,7 +356,7 @@ fn open_workspace_with_remote_factory( store: SessionStore, factory: Arc, ) -> (WindowHandle, Entity) { - cx.update(shellr::init); + cx.update(shellrs::init); let mut workspace = None; let handle = cx.open_window(size(px(1280.), px(800.)), |window, cx| { let store = cx.new(|_| store); @@ -782,7 +782,7 @@ async fn opening_sftp_updates_connection_state_without_terminal(cx: &mut TestApp assert_eq!(workspace.explorers_of(id, cx).len(), 1); assert_eq!( workspace.store().read(cx).session(id).unwrap().state, - shellr::session::ConnectionState::Connected + shellrs::session::ConnectionState::Connected ); }); } @@ -1194,7 +1194,7 @@ fn search_filters_the_tree(cx: &mut TestAppContext) { #[gpui_kit::test] fn group_expansion_survives_reopening_the_database(cx: &mut TestAppContext) { let dir = tempfile::tempdir().unwrap(); - let path = dir.path().join("shellr.db"); + let path = dir.path().join("shellrs.db"); let database = SessionDatabase::open(&path).unwrap(); let seed = SessionStore::seed(); for group in seed.groups() { @@ -1260,7 +1260,7 @@ fn group_expansion_survives_reopening_the_database(cx: &mut TestAppContext) { #[gpui_kit::test] fn expand_and_collapse_all_groups_include_nested_groups_and_persist(cx: &mut TestAppContext) { let dir = tempfile::tempdir().unwrap(); - let path = dir.path().join("shellr.db"); + let path = dir.path().join("shellrs.db"); let database = SessionDatabase::open(&path).unwrap(); let mut seed = SessionStore::seed(); let nested = @@ -1480,7 +1480,7 @@ fn dragging_a_host_to_blank_tree_space_moves_it_to_the_root(cx: &mut TestAppCont #[gpui_kit::test] fn dragged_order_survives_reopening_the_database(cx: &mut TestAppContext) { let dir = tempfile::tempdir().unwrap(); - let path = dir.path().join("shellr.db"); + let path = dir.path().join("shellrs.db"); let database = SessionDatabase::open(&path).unwrap(); let seed = SessionStore::seed(); for group in seed.groups() { @@ -2987,7 +2987,7 @@ async fn deleting_a_group_removes_its_sessions_and_closes_their_tabs(cx: &mut Te #[gpui_kit::test] async fn groups_and_sessions_are_read_back_from_the_database(cx: &mut TestAppContext) { let directory = tempfile::tempdir().expect("temp dir"); - let path = directory.path().join("shellr.db"); + let path = directory.path().join("shellrs.db"); let store = SessionStore::load(SessionDatabase::open(&path).expect("database opened")) .expect("store loaded"); // A first launch starts with nothing at all. @@ -3660,14 +3660,14 @@ async fn the_start_page_marks_recent_hosts_with_their_operating_system(cx: &mut #[derive(Default)] struct FakeSftpProvider { requests: Arc>>, - downloads: Arc>>, - operations: Arc>>, + downloads: Arc>>, + operations: Arc>>, events: Arc>>>, /// When set, the next connection waits for a message before it is up. hold_connection: Arc>>>, } impl SftpTransportProvider for FakeSftpProvider { - fn create(&self, _: &shellr::session::Session) -> Box { + fn create(&self, _: &shellrs::session::Session) -> Box { Box::new(FakeSftpTransport { requests: self.requests.clone(), downloads: self.downloads.clone(), @@ -3679,8 +3679,8 @@ impl SftpTransportProvider for FakeSftpProvider { } struct FakeSftpTransport { requests: Arc>>, - downloads: Arc>>, - operations: Arc>>, + downloads: Arc>>, + operations: Arc>>, events: Arc>>>, hold: Option>, } @@ -3690,7 +3690,7 @@ impl SftpTransport for FakeSftpTransport { commands: async_channel::Receiver, events: async_channel::Sender, ) -> anyhow::Result<()> { - use shellr::sftp::{TransferChoice, TransferPhase, TransferProgress}; + use shellrs::sftp::{TransferChoice, TransferPhase, TransferProgress}; self.events.lock().unwrap().push(events.clone()); if let Some(hold) = &self.hold { let _ = hold.recv(); @@ -3730,7 +3730,7 @@ impl SftpTransport for FakeSftpTransport { self.downloads.lock().unwrap().push(request); events.send_blocking(SftpEvent::Progress( TransferProgress::new(TransferPhase::Transferring) - .with_direction(shellr::sftp::TransferDirection::Download), + .with_direction(shellrs::sftp::TransferDirection::Download), ))?; } SftpCommand::Upload(request) => { @@ -3797,7 +3797,7 @@ impl LocalDirectoryProvider for FakeLocalDirectory { fn set_permissions( &self, paths: &[std::path::PathBuf], - edit: shellr::sftp::PermissionEdit, + edit: shellrs::sftp::PermissionEdit, recursive: bool, add_x_to_dirs: bool, ) -> anyhow::Result<()> { @@ -3847,7 +3847,7 @@ fn open_workspace_with_services( provider: Arc, local: FakeLocalDirectory, ) -> (WindowHandle, Entity) { - cx.update(shellr::init); + cx.update(shellrs::init); // Dialogs slide in over real time; small targets such as checkboxes // would move between the frame that locates them and the click. cx.update(|cx| cx.set_reduce_motion(true)); @@ -3955,7 +3955,7 @@ fn pane_selection(workspace: &Entity, remote: bool, cx: &App) -> Vec< async fn sftp_multi_selection_keyboard_upload_freezes_paths_and_cancel_resumes( cx: &mut TestAppContext, ) { - use shellr::app::{ExplorerAction, ExplorerCommand}; + use shellrs::app::{ExplorerAction, ExplorerCommand}; let provider = Arc::new(FakeSftpProvider::default()); let (handle, workspace) = open_workspace_with_sftp(cx, provider.clone()); open_test_explorer(cx, handle).await; @@ -4073,9 +4073,9 @@ async fn sftp_native_picker_and_external_drop_share_confirmation(cx: &mut TestAp cx.update_window(handle.into(), |_, window, cx| { window.render_frame(cx); window.dispatch_action( - Box::new(shellr::app::ExplorerAction::new( + Box::new(shellrs::app::ExplorerAction::new( ExplorerId(SFTP_TAB), - shellr::app::ExplorerCommand::ChooseFiles, + shellrs::app::ExplorerCommand::ChooseFiles, )), cx, ); @@ -4133,7 +4133,7 @@ async fn sftp_native_picker_and_external_drop_share_confirmation(cx: &mut TestAp #[gpui_kit::test] async fn sftp_internal_drag_conflict_and_close_confirmation(cx: &mut TestAppContext) { - use shellr::sftp::{TransferQuestion, TransferQuestionKind}; + use shellrs::sftp::{TransferQuestion, TransferQuestionKind}; let provider = Arc::new(FakeSftpProvider::default()); let (handle, workspace) = open_workspace_with_sftp(cx, provider.clone()); open_test_explorer(cx, handle).await; @@ -4348,7 +4348,7 @@ async fn sftp_sort_range_selection_and_dialog_focus_preserve_path_identity( #[gpui_kit::test] async fn sftp_panes_list_winscp_columns_and_open_links_to_directories(cx: &mut TestAppContext) { - use shellr::explorer::FileKind; + use shellrs::explorer::FileKind; let provider = Arc::new(FakeSftpProvider::default()); let (handle, workspace) = open_workspace_with_sftp(cx, provider); open_test_explorer(cx, handle).await; @@ -4416,8 +4416,8 @@ async fn click_remote_tool( #[gpui_kit::test] async fn sftp_toolbar_goes_up_root_home_back_and_forward(cx: &mut TestAppContext) { - use shellr::app::{ExplorerAction, ExplorerCommand}; - use shellr::session::BookmarkSide; + use shellrs::app::{ExplorerAction, ExplorerCommand}; + use shellrs::session::BookmarkSide; let provider = Arc::new(FakeSftpProvider::default()); let (handle, workspace) = open_workspace_with_sftp(cx, provider); open_test_explorer(cx, handle).await; @@ -4528,7 +4528,7 @@ async fn sftp_toolbar_goes_up_root_home_back_and_forward(cx: &mut TestAppContext fn remote_pane_state( workspace: &Entity, cx: &mut TestAppContext, - read: impl FnOnce(&shellr::explorer::FilePane) -> T, + read: impl FnOnce(&shellrs::explorer::FilePane) -> T, ) -> T { cx.update(|cx| { read( @@ -4545,7 +4545,7 @@ fn remote_pane_state( #[gpui_kit::test] async fn sftp_path_label_opens_ancestors_and_the_open_directory_dialog(cx: &mut TestAppContext) { - use shellr::app::{ExplorerAction, ExplorerCommand}; + use shellrs::app::{ExplorerAction, ExplorerCommand}; let provider = Arc::new(FakeSftpProvider::default()); let (handle, workspace) = open_workspace_with_sftp(cx, provider); open_test_explorer(cx, handle).await; @@ -4686,7 +4686,7 @@ async fn sftp_path_label_opens_ancestors_and_the_open_directory_dialog(cx: &mut #[gpui_kit::test] async fn sftp_bookmark_dialog_adds_orders_removes_and_opens(cx: &mut TestAppContext) { - use shellr::session::BookmarkSide; + use shellrs::session::BookmarkSide; let provider = Arc::new(FakeSftpProvider::default()); let (handle, workspace) = open_workspace_with_sftp(cx, provider); open_test_explorer(cx, handle).await; @@ -4825,7 +4825,7 @@ async fn sftp_bookmark_dialog_adds_orders_removes_and_opens(cx: &mut TestAppCont #[gpui_kit::test] async fn sftp_path_label_folds_the_middle_of_a_long_path(cx: &mut TestAppContext) { - use shellr::app::{ExplorerAction, ExplorerCommand}; + use shellrs::app::{ExplorerAction, ExplorerCommand}; let provider = Arc::new(FakeSftpProvider::default()); let (handle, _) = open_workspace_with_sftp(cx, provider); open_test_explorer(cx, handle).await; @@ -4938,9 +4938,9 @@ fn press_on_row( #[gpui_kit::test] async fn sftp_file_commands_confirm_validate_and_send_one_operation(cx: &mut TestAppContext) { - use shellr::app::{ExplorerAction, ExplorerCommand}; - use shellr::explorer::NewEntryKind; - use shellr::sftp::{PermissionEdit, RemoteOperation}; + use shellrs::app::{ExplorerAction, ExplorerCommand}; + use shellrs::explorer::NewEntryKind; + use shellrs::sftp::{PermissionEdit, RemoteOperation}; let provider = Arc::new(FakeSftpProvider::default()); let local = FakeLocalDirectory::default(); let (handle, workspace) = open_workspace_with_services(cx, provider.clone(), local.clone()); @@ -5223,7 +5223,7 @@ async fn the_sftp_tab_shows_the_host_mark_like_its_terminal_tabs(cx: &mut TestAp #[gpui_kit::test] async fn the_sftp_tab_can_be_renamed_and_follow_the_session_again(cx: &mut TestAppContext) { - use shellr::app::RenameExplorer; + use shellrs::app::RenameExplorer; let provider = Arc::new(FakeSftpProvider::default()); let (handle, workspace) = open_workspace_with_sftp(cx, provider); open_test_explorer(cx, handle).await; @@ -5342,7 +5342,7 @@ async fn sftp_clicking_empty_list_space_makes_that_pane_current_at_once(cx: &mut #[gpui_kit::test] async fn opening_sftp_again_opens_another_tab_of_its_own(cx: &mut TestAppContext) { - use shellr::app::{DisconnectSession, ExplorerAction, ExplorerCommand}; + use shellrs::app::{DisconnectSession, ExplorerAction, ExplorerCommand}; let provider = Arc::new(FakeSftpProvider::default()); let (handle, workspace) = open_workspace_with_sftp(cx, provider); open_test_explorer(cx, handle).await; @@ -5470,7 +5470,7 @@ async fn sftp_connecting_shows_under_the_list_without_moving_it(cx: &mut TestApp #[gpui_kit::test] async fn sftp_reading_a_directory_never_moves_the_list(cx: &mut TestAppContext) { - use shellr::app::{ExplorerAction, ExplorerCommand}; + use shellrs::app::{ExplorerAction, ExplorerCommand}; let provider = Arc::new(FakeSftpProvider::default()); let (handle, workspace) = open_workspace_with_sftp(cx, provider); open_test_explorer(cx, handle).await; @@ -5511,7 +5511,7 @@ async fn sftp_reading_a_directory_never_moves_the_list(cx: &mut TestAppContext) #[gpui_kit::test] async fn sftp_discards_stale_directory_replies(cx: &mut TestAppContext) { - use shellr::app::{ExplorerAction, ExplorerCommand}; + use shellrs::app::{ExplorerAction, ExplorerCommand}; let provider = Arc::new(FakeSftpProvider::default()); let (handle, workspace) = open_workspace_with_sftp(cx, provider.clone()); open_test_explorer(cx, handle).await;