程序改名为 ShellRS,命名空间改为 shellrs
- 界面显示名(窗口标题、标题栏、文案)改为 ShellRS - crate、二进制、动作命名空间、线程名改为 shellrs - 数据目录、数据库文件、SHELLRS_DATA_DIR、钥匙串服务名改为 shellrs;程序尚未发布,不保留旧名兼容代码
This commit is contained in:
1 parent
5b6eee0b71
commit
e3b5e20efe
27 files changed
+117
-117
No files matched your search
@@ -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:<user>@<host>:<port>" # 查钥匙串条目
|
||||
sqlite3 ~/Library/Application\ Support/shellrs/shellrs.db '.schema' # 查落盘结果
|
||||
security find-generic-password -s shellrs -a "password:<user>@<host>:<port>" # 查钥匙串条目
|
||||
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<GroupId>`,`Session.os` 是 `Option<HostOs>`)、`database.rs`(`SessionDatabase`:rusqlite 连接、`PRAGMA user_version` 迁移、行与模型的映射)、`store.rs`(`SessionStore` 实体,**内存是唯一事实来源**,写穿到数据库)、`outline.rs`(纯函数:store → `TreeItem`,递归铺开嵌套分组;节点 id 形如 `g:<id>` / `s:<id>`;`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 校验写 `<data_dir>/known_hosts`(**不碰** `~/.ssh/known_hosts`),认证链是 agent → publickey → password → keyboard-interactive。密码和私钥口令先查 `secrets`,查不到或被服务器拒绝才走 prompt 弹框;被拒绝时**不删**钥匙串条目,只在弹框说明里讲清原因。
|
||||
|
||||
@@ -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:<user>@<host>:<port>" # 查钥匙串条目
|
||||
sqlite3 ~/Library/Application\ Support/shellrs/shellrs.db '.schema' # 查落盘结果
|
||||
security find-generic-password -s shellrs -a "password:<user>@<host>:<port>" # 查钥匙串条目
|
||||
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<GroupId>`,`Session.os` 是 `Option<HostOs>`)、`database.rs`(`SessionDatabase`:rusqlite 连接、`PRAGMA user_version` 迁移、行与模型的映射)、`store.rs`(`SessionStore` 实体,**内存是唯一事实来源**,写穿到数据库)、`outline.rs`(纯函数:store → `TreeItem`,递归铺开嵌套分组;节点 id 形如 `g:<id>` / `s:<id>`;`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 校验写 `<data_dir>/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` 占同样宽度的槽位,否则标签对不齐。
|
||||
|
||||
Generated
+1
-1
@@ -7403,7 +7403,7 @@ dependencies = [
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "shellr"
|
||||
name = "shellrs"
|
||||
version = "0.1.0"
|
||||
dependencies = [
|
||||
"alacritty_terminal",
|
||||
|
||||
+2
-2
@@ -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 }
|
||||
|
||||
@@ -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-<UUID>.backup`,再发布新文件,此兼容路径不保证原子性。发布响应丢失时根据受管理临时路径、阶段、类型和大小恢复,无法安全判断则保留恢复文件并报告。
|
||||
文件先写入 `<目标文件名>.filepart`。写入请求采用有界流水线,收到每个写入确认并关闭成功后才发布正式文件;服务器支持时先调用 `fsync@openssh.com`。支持 `posix-rename@openssh.com` 的服务器使用原子替换;其他服务器先将旧文件移到 `.shellrs-<UUID>.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` 的随机端口,使用测试凭据。限制本地套接字的沙箱会跳过该部分,需要在允许回环套接字的环境运行。
|
||||
|
||||
|
||||
+10
-10
@@ -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
|
||||
|
||||
+7
-7
@@ -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<PathBuf> {
|
||||
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<PathBuf> {
|
||||
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<PathBuf> {
|
||||
let dir = data_dir();
|
||||
|
||||
@@ -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()));
|
||||
|
||||
+1
-1
@@ -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 文件浏览),
|
||||
|
||||
+12
-12
@@ -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<SharedString>) {
|
||||
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<SharedString>) {
|
||||
/// 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<SharedString>) {
|
||||
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<SharedString>) {
|
||||
}
|
||||
|
||||
fn load_store() -> anyhow::Result<SessionStore> {
|
||||
let path = shellr::app::database_path()?;
|
||||
let path = shellrs::app::database_path()?;
|
||||
Ok(SessionStore::load(SessionDatabase::open(&path)?)?)
|
||||
}
|
||||
@@ -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());
|
||||
|
||||
+2
-2
@@ -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";
|
||||
|
||||
/// 一条秘密的身份。
|
||||
///
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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 {
|
||||
"系统钥匙串不可用,这台机器上无法保存密码,每次连接都会询问。"
|
||||
};
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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;
|
||||
|
||||
+2
-2
@@ -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()
|
||||
|
||||
+1
-1
@@ -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)
|
||||
|
||||
+2
-2
@@ -27,7 +27,7 @@ pub fn parse_probe(output: &str) -> Option<HostOs> {
|
||||
"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<HostOs> {
|
||||
///
|
||||
/// `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<HostOs> {
|
||||
let mut like = None;
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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())
|
||||
{
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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] {
|
||||
[
|
||||
|
||||
@@ -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(
|
||||
|
||||
@@ -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(
|
||||
|
||||
@@ -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.;
|
||||
|
||||
+44
-44
@@ -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<FakeConnectionTester>,
|
||||
) -> (WindowHandle<Root>, Entity<Workspace>) {
|
||||
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<FakeTerminalFactory>,
|
||||
) -> (WindowHandle<Root>, Entity<Workspace>) {
|
||||
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<dyn TerminalTransportFactory>,
|
||||
) -> (WindowHandle<Root>, Entity<Workspace>) {
|
||||
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<Mutex<Vec<UploadRequest>>>,
|
||||
downloads: Arc<Mutex<Vec<shellr::sftp::DownloadRequest>>>,
|
||||
operations: Arc<Mutex<Vec<shellr::sftp::RemoteOperation>>>,
|
||||
downloads: Arc<Mutex<Vec<shellrs::sftp::DownloadRequest>>>,
|
||||
operations: Arc<Mutex<Vec<shellrs::sftp::RemoteOperation>>>,
|
||||
events: Arc<Mutex<Vec<async_channel::Sender<SftpEvent>>>>,
|
||||
/// When set, the next connection waits for a message before it is up.
|
||||
hold_connection: Arc<Mutex<Option<mpsc::Receiver<()>>>>,
|
||||
}
|
||||
impl SftpTransportProvider for FakeSftpProvider {
|
||||
fn create(&self, _: &shellr::session::Session) -> Box<dyn SftpTransport> {
|
||||
fn create(&self, _: &shellrs::session::Session) -> Box<dyn SftpTransport> {
|
||||
Box::new(FakeSftpTransport {
|
||||
requests: self.requests.clone(),
|
||||
downloads: self.downloads.clone(),
|
||||
@@ -3679,8 +3679,8 @@ impl SftpTransportProvider for FakeSftpProvider {
|
||||
}
|
||||
struct FakeSftpTransport {
|
||||
requests: Arc<Mutex<Vec<UploadRequest>>>,
|
||||
downloads: Arc<Mutex<Vec<shellr::sftp::DownloadRequest>>>,
|
||||
operations: Arc<Mutex<Vec<shellr::sftp::RemoteOperation>>>,
|
||||
downloads: Arc<Mutex<Vec<shellrs::sftp::DownloadRequest>>>,
|
||||
operations: Arc<Mutex<Vec<shellrs::sftp::RemoteOperation>>>,
|
||||
events: Arc<Mutex<Vec<async_channel::Sender<SftpEvent>>>>,
|
||||
hold: Option<mpsc::Receiver<()>>,
|
||||
}
|
||||
@@ -3690,7 +3690,7 @@ impl SftpTransport for FakeSftpTransport {
|
||||
commands: async_channel::Receiver<SftpCommand>,
|
||||
events: async_channel::Sender<SftpEvent>,
|
||||
) -> 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<FakeSftpProvider>,
|
||||
local: FakeLocalDirectory,
|
||||
) -> (WindowHandle<Root>, Entity<Workspace>) {
|
||||
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<Workspace>, 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<T>(
|
||||
workspace: &Entity<Workspace>,
|
||||
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<T>(
|
||||
|
||||
#[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;
|
||||
|
||||
Reference in new issue
Block a user