在线升级:标题栏按钮更醒目,更新对话框不再列更新说明
标题栏的更新按钮改成 success 色的浅色按钮(出错时 warning 色),不再是 普通图标加一个小绿点。更新对话框不再显示更新说明,页脚两个按钮:「查看 更新内容」打开 https://shellrs.com/changelog,「重启并安装」照旧;去掉 「稍后」,用关闭按钮或 Esc 关。 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
1 parent
de0fafe6ee
commit
ba63b3d73f
11 files changed
+93
-77
No files matched your search
+1
-1
@@ -1,6 +1,6 @@
|
||||
# 更新日志
|
||||
|
||||
每个版本一节,标题写成 `## [版本号]`,可以在后面加日期。发布流程(`.github/workflows/release.yml`)把与 tag 同版本的那一节原样作为更新说明:显示在 GitHub Release 和 ShellRS 的更新对话框里,用 Markdown 书写,面向使用者。
|
||||
每个版本一节,标题写成 `## [版本号]`,可以在后面加日期。发布流程(`.github/workflows/release.yml`)把与 tag 同版本的那一节原样作为更新说明:显示在 GitHub Release 和官网的更新日志里(ShellRS 的更新对话框用「查看更新内容」打开官网的更新日志),用 Markdown 书写,面向使用者。
|
||||
|
||||
## [0.1.0]
|
||||
|
||||
|
||||
@@ -10,7 +10,7 @@ ShellRS(crate 与二进制都叫 `shellrs`)是一个类似 Xshell / WinSCP
|
||||
|
||||
主机和分组是**真实持久化**的,存在一个 SQLite 文件里(`~/Library/Application Support/shellrs/shellrs.db`,`SHELLRS_DATA_DIR` 可覆盖目录)。本地终端(`portable-pty` + `alacritty_terminal`)和 SSH 远程连接(`russh`)也都是真的。密码与私钥口令存在系统钥匙串里(`keyring`),**数据库里永远不出现秘密**。SFTP 双栏浏览、上传、下载和文件操作(删除、重命名、属性、新建)也已接入真实文件系统,上传和下载都支持断点续传,并按 WinSCP 的方式排队(每个 SFTP 标签一个传输队列);目录同步尚未实现。SSH 端口转发(本地 `-L`、远程 `-R`、动态 `-D`)也是真的,规则和主机存在同一个库里。凭据(密码 / 密钥 / SSH Agent)也存在这个库里(密码和口令照旧只进钥匙串,粘贴或生成的私钥是数据目录 `keys/` 下的文件),主机可以「使用凭据」而不是自己填登录信息。主机还有「连接方式」:直接连接、经过别的主机做 SSH 跳板(可多级)、或经由 HTTP / SOCKS5 代理,终端、SFTP、端口转发、外部 CLI 和测试连接都照它走。功能的详细说明见 `docs/manual.md`(`README.md` / `README.en.md` 是开源首页,截图在 `docs/screenshots/`)。
|
||||
|
||||
已确认的产品决定:WinSCP 式双栏文件浏览器是「SFTP」Dock 标签页,和终端一样每次「打开 SFTP」都新开一个(同一主机可以开多个,各自独立连接),操作逻辑对标 WinSCP Commander(两行工具栏、路径标签、WinSCP 的列、列表右键菜单、高亮多选;面板顶上不再有「用户@主机:端口」一行;本地侧与远程侧对称,本地删除进废纸篓;书签按主机、按本地 / 远程各一份持久化);传输进行中还能再发起传输,新批次在 SFTP 标签底部的「传输队列」里排队依次执行(WinSCP 的队列面板,不做前台进度对话框);暂不做 Dock 布局持久化、过滤、查找文件和目录树;首次启动是空库,不预置任何分组或主机;分组支持任意层级嵌套,主机也可以不属于任何分组(渲染在树的根层级);删除分组会连同其子分组和里面的主机一起删,删除前确认并写明数量。端口转发不是 Dock 标签页,而是侧栏的另一个列表,由标题栏「ShellRS」后面的三个图标(主机 / 端口转发 / 凭据)切换;一条规则经由一个已保存的主机,启动时自己建一条 SSH 连接,不计入主机的连接状态,主机「断开连接」不停它;删除主机(或含它的分组)连同经由它的规则一起删,确认框写明数量;规则可设「启动 ShellRS 时自动开启」,「创建」只保存不启动;新建规则时「经由主机」留空(不预选当前标签的主机,也不预选第一台),由用户自己选;断线后按 1、3、10 秒自动重连三次,重连不弹任何问答;配置对话框用排成一行的三张类型卡片、实时示意图、一句话说明和一直循环的流向动画说明三种转发,示意图和那句话里的服务器按角色叫「SSH 服务器」,不写所选主机的名称;暂不做转发的外部 CLI 接口、规则排序和监听端口 0。凭据是侧栏的第三个列表(密码 / 密钥 / SSH Agent 三种),**凭据带用户名**:主机「使用凭据」时不再填用户名,改凭据的用户名所有用它的主机一起变;主机对话框的「认证方式」是三选一:「密码」(只有用户名和密码,密码留空就每次连接时询问,不悄悄试密钥)、「使用凭据」和「无密码」(只有用户名:依次试服务器免认证、SSH Agent 和 `~/.ssh` 的默认私钥,服务器要求密码时直接报错、不询问;密钥之后再要验证码照样问);主机自己不再有私钥文件,要用私钥就建密钥凭据;密钥凭据的私钥有三种来源——「本机文件」(只引用路径,不导入内容)、「粘贴」和「生成新密钥」(Ed25519 默认,另有 RSA 4096)——在同一个对话框里(类型选「密钥」后多一排来源单选),不按类型拆对话框,因为编辑时要能改类型、换钥匙,用它的主机一起跟着变;粘贴和生成的私钥由 ShellRS 写成数据目录 `keys/` 下的文件(不进钥匙串:Windows 凭据管理器一条最多 2560 字节,RSA 私钥放不下),凭据引用它的路径,编辑时就显示为「本机文件」;空列表和空白处的右键菜单多一个「生成密钥…」,打开的是同一个对话框、已选好「密钥 · 生成新密钥」,密钥凭据的右键菜单有「复制公钥」;删除仍在用的凭据要确认并写明台数,删除后那些主机保留用户名,密码凭据的改成「密码」(连接时询问),密钥和 Agent 凭据的改成「无密码」(`CredentialKind::without_credential`),密码和私钥不复制过去,已打开的连接不断开;主机对话框里不做「新建凭据…」(设计规范不叠弹层);Windows 上 Agent 走 OpenSSH Agent 的命名管道,「无密码」也试它;暂不做自定义 Agent 套接字、Pageant、凭据的外部 CLI 接口和凭据排序。主机对话框「分组」下面是「连接方式」三选一:「直接连接」(默认)、「SSH 跳板」和「代理连接」。跳板选的是已保存的主机,可多级,**只走这里列出的链**:跳板主机自己的连接方式不参与(所以不会成环),每台用它自己的地址、端口、认证和 known_hosts 条目;跳板列表不做拖动排序(删了重加)。代理是 HTTP(`CONNECT`)或 SOCKS5(域名交给代理解析),用户名和密码可选,密码按「代理地址 + 用户名」进钥匙串(`SecretRef::Proxy`),代理要求认证或拒绝时直接报错、不弹问答。跳板和代理不叠加。删除被别的主机用作跳板的主机:确认框写明台数,那些主机的跳板列表里留下「已删除的主机」占位(库里 `ON DELETE SET NULL`),连接时直接报错,不悄悄改成直连或少跳一级,表单里留着占位就保存会报错;删除不重连已打开的连接。在线升级:主域名 sLine truncated
|
||||
已确认的产品决定:WinSCP 式双栏文件浏览器是「SFTP」Dock 标签页,和终端一样每次「打开 SFTP」都新开一个(同一主机可以开多个,各自独立连接),操作逻辑对标 WinSCP Commander(两行工具栏、路径标签、WinSCP 的列、列表右键菜单、高亮多选;面板顶上不再有「用户@主机:端口」一行;本地侧与远程侧对称,本地删除进废纸篓;书签按主机、按本地 / 远程各一份持久化);传输进行中还能再发起传输,新批次在 SFTP 标签底部的「传输队列」里排队依次执行(WinSCP 的队列面板,不做前台进度对话框);暂不做 Dock 布局持久化、过滤、查找文件和目录树;首次启动是空库,不预置任何分组或主机;分组支持任意层级嵌套,主机也可以不属于任何分组(渲染在树的根层级);删除分组会连同其子分组和里面的主机一起删,删除前确认并写明数量。端口转发不是 Dock 标签页,而是侧栏的另一个列表,由标题栏「ShellRS」后面的三个图标(主机 / 端口转发 / 凭据)切换;一条规则经由一个已保存的主机,启动时自己建一条 SSH 连接,不计入主机的连接状态,主机「断开连接」不停它;删除主机(或含它的分组)连同经由它的规则一起删,确认框写明数量;规则可设「启动 ShellRS 时自动开启」,「创建」只保存不启动;新建规则时「经由主机」留空(不预选当前标签的主机,也不预选第一台),由用户自己选;断线后按 1、3、10 秒自动重连三次,重连不弹任何问答;配置对话框用排成一行的三张类型卡片、实时示意图、一句话说明和一直循环的流向动画说明三种转发,示意图和那句话里的服务器按角色叫「SSH 服务器」,不写所选主机的名称;暂不做转发的外部 CLI 接口、规则排序和监听端口 0。凭据是侧栏的第三个列表(密码 / 密钥 / SSH Agent 三种),**凭据带用户名**:主机「使用凭据」时不再填用户名,改凭据的用户名所有用它的主机一起变;主机对话框的「认证方式」是三选一:「密码」(只有用户名和密码,密码留空就每次连接时询问,不悄悄试密钥)、「使用凭据」和「无密码」(只有用户名:依次试服务器免认证、SSH Agent 和 `~/.ssh` 的默认私钥,服务器要求密码时直接报错、不询问;密钥之后再要验证码照样问);主机自己不再有私钥文件,要用私钥就建密钥凭据;密钥凭据的私钥有三种来源——「本机文件」(只引用路径,不导入内容)、「粘贴」和「生成新密钥」(Ed25519 默认,另有 RSA 4096)——在同一个对话框里(类型选「密钥」后多一排来源单选),不按类型拆对话框,因为编辑时要能改类型、换钥匙,用它的主机一起跟着变;粘贴和生成的私钥由 ShellRS 写成数据目录 `keys/` 下的文件(不进钥匙串:Windows 凭据管理器一条最多 2560 字节,RSA 私钥放不下),凭据引用它的路径,编辑时就显示为「本机文件」;空列表和空白处的右键菜单多一个「生成密钥…」,打开的是同一个对话框、已选好「密钥 · 生成新密钥」,密钥凭据的右键菜单有「复制公钥」;删除仍在用的凭据要确认并写明台数,删除后那些主机保留用户名,密码凭据的改成「密码」(连接时询问),密钥和 Agent 凭据的改成「无密码」(`CredentialKind::without_credential`),密码和私钥不复制过去,已打开的连接不断开;主机对话框里不做「新建凭据…」(设计规范不叠弹层);Windows 上 Agent 走 OpenSSH Agent 的命名管道,「无密码」也试它;暂不做自定义 Agent 套接字、Pageant、凭据的外部 CLI 接口和凭据排序。主机对话框「分组」下面是「连接方式」三选一:「直接连接」(默认)、「SSH 跳板」和「代理连接」。跳板选的是已保存的主机,可多级,**只走这里列出的链**:跳板主机自己的连接方式不参与(所以不会成环),每台用它自己的地址、端口、认证和 known_hosts 条目;跳板列表不做拖动排序(删了重加)。代理是 HTTP(`CONNECT`)或 SOCKS5(域名交给代理解析),用户名和密码可选,密码按「代理地址 + 用户名」进钥匙串(`SecretRef::Proxy`),代理要求认证或拒绝时直接报错、不弹问答。跳板和代理不叠加。删除被别的主机用作跳板的主机:确认框写明台数,那些主机的跳板列表里留下「已删除的主机」占位(库里 `ON DELETE SET NULL`),连接时直接报错,不悄悄改成直连或少跳一级,表单里留着占位就保存会报错;删除不重连已打开的连接。在线升级:主域名 sLine truncated
|
||||
|
||||
做 UI 之前先读 `gpui-kit` 与 `gpui-kit-design-guides` 两个技能(其中的 Coding Guides 和 Design Guides 在本仓库是规范,不是参考)。不要凭记忆臆造 gpui-kit API:以 `~/.cargo/registry/src/*/gpui-component-0.6.1/`、`gpui-base-0.6.1/` 的源码或 `https://gpui-kit.com/component/<name>.md` 为准。
|
||||
|
||||
@@ -194,4 +194,4 @@ cargo test -- --ignored # 会读写真实钥匙串的测试
|
||||
|
||||
## 测试约定
|
||||
|
||||
UI 测试(`tests/ui/`)在无头窗口里渲染真实的 `Workspace`(夹具传入 `SettingsStore::in_memory()`,不读写真实的设置文件),通过 `gpui_kit::test::TestWindowExt`(`click`、`double_click`、`input`、`press`、`find`、`within`)驱动。它们是**一个**测试程序(`tests/ui/main.rs`,`cargo test --test ui`),按功能分模块:`host_dialog`(主机对话框、连接方式、备注、钥匙串里的密码、测试连接)、`host_tree`(搜索、分组、拖动、行的 tooltip 和右键命令)、`connection`(连接主机、认证问答、连接状态、系统和延迟)、`terminal`(本地终端、输入、选择、查找、清屏、字体、尺寸)、`workspace`(标签页和关闭、开始页、标题栏、右侧栏、对话框的共性)、`monitor`(系统监控)、`snippets`(命令片段)、`history`(历史命令)、`netstat`(网络连接)、`processes`(进程管理)、`services`(系统服务)、`docker`(Docker)、`settings`、`sftp`(SFTP 标签的面板、导航、书签、文件操作、选择)、`transfer`(上传、下载、拖动传输、队列)、`forward`、`credential`、`update`;共用的夹具和假实现在 `support/`:`support/mod.rs` 是公共的 `use`(`pub use`,所以各模块开头只写 `use crate::support::*;`)、种子 id 和 `open_workspace` / `in_frame` 这类通用助手,`support/{connection,terminal,sftp,forward,credential}.rs` 是各功能的假实现和夹具。新测试放进对应功能的模块;只有一个模块用的助手和假实现就放在那个模块里,两个以上模块用的才放进 `support`(项、字段和方法都要 `pub`)。单元测试起子进程一律经 `crate::testing`(`sh`、`sh_accepts`,别的程序先拿 `no_forks()` 再启动):macOS 建套接字和标记「不传给子进程」是两步,别的测试线程恰好在这中间起子进程,子进程就攥着这个套接字的副本,测试关掉的监听套接字照样能连上——`cli::tests` 里「残留的套接字不算在运行」两个测试因此在加了几个工具的脚本测试之后每轮必挂;建这种套接字的测试同样拿着 `no_forks()`。**不要在 `tests/` 下另开测试文件**:每个文件都是单独的测试程序,要再链接一遍整个 GPUI。`Workspace::new` 要求外部传入 `Entity<HostStore>`:生产环境由 `main.rs` 从数据库加载,测试用 `open_workspace_with_store` 注入 `HostStore::seed()`(或一个临时文件上的真库)。**不要用右键菜单驱动测试**:菜单项的 ElementId 只是序号,而且 `PopupMenu` 实体会让测试以「leaked handles」失败;直接 `window.dispatch_action` 派发菜单会派发的那个动作。测试模块里要显式导入类型,`use gpui_kit::*` 会遮蔽 `#[test]`。被测试查询的元素带稳定的领域 id,自定义 `div` 还要加 `.test_support()`:`("host-row", id)`、`("host-os", id)` 与开始页的 `("recent-host-os", id)`(`aria_label` 是系统名,未探测时是「未探测到系统」)、`("group-row", id)`、`("terminal", id)`、`("explorer", explorer_id)`、`("local-pane"|"remote-pane", explorer_id)`(SFTP 的元素都按标签 id,测试里第一个 SFTP 标签是 `SFTP_TAB` = 1)、`("close-terminal", id)`、设置的 `open-settings`(主机面板底部)/ `settings`(页面)/ `settings-tab` / `close-settings`、设置页只渲染当前分类(左栏条目的 id 由 `Settings` 按位置生成,是 `within("settings")` 里的 `0-<序号>`,外观是 `0-0`、终端是 `0-1`、外部 CLI 是 `0-2`、关于是 `0-3`;开关是 `group-<组>` › `item-<项>` › `check`;外部 CLI 页的 `cli-binary`(安装 / 移除按钮)与 `cli-binary-status`、`skill-<agent>` 与 `skill-status-<agent>`(agent 为 `agents` / `codex` / `claude` / `opencode` / `workbuddy`,状态行的 `aria_label` 是显示的文字)、`check-cli-integration`、`copy-agent-skill`;设置项同理,下拉按钮是 `group-<组>` › `item-<项>` › `btn`,`label` 是当前选项的文字)、终端字体预览 `terminal-font-preview`(`aria_label` 形如「Menlo 13 px,行高 1.54」)、`("close-explorer", id)`、标签标题 `("terminal-tab", id)` / `("explorer-tab", explorer_id)` / `("local-terminal-tab", id)`(`aria_label` 是标签文字)、`("terminal-tab-os", id)` 与 `("explorer-tab-os", explorer_id)`、`test-connection`、`("terminal-latency", id)` 与 SFTP 标签的 `("sftp-latency", explorer_id)`(`aria_label` 是「32 ms」或「超时」;SFTP 测试往 `FakeSftpProvider::events` 里送 `SftpEvent::Latency`)、`terminal-find`(查找输入框)、`terminal-find-count`(`aria_label` 是「3/12」「无结果」或「N 项」,查询为空或搜索还在去抖时不存在)、`terminal-find-previous` / `-next` / `-close`、`tab-name`(重命名标签对话框)、侧栏行的 tooltip `host-tooltip` / `forward-tooltip` / `creLine truncated
|
||||
UI 测试(`tests/ui/`)在无头窗口里渲染真实的 `Workspace`(夹具传入 `SettingsStore::in_memory()`,不读写真实的设置文件),通过 `gpui_kit::test::TestWindowExt`(`click`、`double_click`、`input`、`press`、`find`、`within`)驱动。它们是**一个**测试程序(`tests/ui/main.rs`,`cargo test --test ui`),按功能分模块:`host_dialog`(主机对话框、连接方式、备注、钥匙串里的密码、测试连接)、`host_tree`(搜索、分组、拖动、行的 tooltip 和右键命令)、`connection`(连接主机、认证问答、连接状态、系统和延迟)、`terminal`(本地终端、输入、选择、查找、清屏、字体、尺寸)、`workspace`(标签页和关闭、开始页、标题栏、右侧栏、对话框的共性)、`monitor`(系统监控)、`snippets`(命令片段)、`history`(历史命令)、`netstat`(网络连接)、`processes`(进程管理)、`services`(系统服务)、`docker`(Docker)、`settings`、`sftp`(SFTP 标签的面板、导航、书签、文件操作、选择)、`transfer`(上传、下载、拖动传输、队列)、`forward`、`credential`、`update`;共用的夹具和假实现在 `support/`:`support/mod.rs` 是公共的 `use`(`pub use`,所以各模块开头只写 `use crate::support::*;`)、种子 id 和 `open_workspace` / `in_frame` 这类通用助手,`support/{connection,terminal,sftp,forward,credential}.rs` 是各功能的假实现和夹具。新测试放进对应功能的模块;只有一个模块用的助手和假实现就放在那个模块里,两个以上模块用的才放进 `support`(项、字段和方法都要 `pub`)。单元测试起子进程一律经 `crate::testing`(`sh`、`sh_accepts`,别的程序先拿 `no_forks()` 再启动):macOS 建套接字和标记「不传给子进程」是两步,别的测试线程恰好在这中间起子进程,子进程就攥着这个套接字的副本,测试关掉的监听套接字照样能连上——`cli::tests` 里「残留的套接字不算在运行」两个测试因此在加了几个工具的脚本测试之后每轮必挂;建这种套接字的测试同样拿着 `no_forks()`。**不要在 `tests/` 下另开测试文件**:每个文件都是单独的测试程序,要再链接一遍整个 GPUI。`Workspace::new` 要求外部传入 `Entity<HostStore>`:生产环境由 `main.rs` 从数据库加载,测试用 `open_workspace_with_store` 注入 `HostStore::seed()`(或一个临时文件上的真库)。**不要用右键菜单驱动测试**:菜单项的 ElementId 只是序号,而且 `PopupMenu` 实体会让测试以「leaked handles」失败;直接 `window.dispatch_action` 派发菜单会派发的那个动作。测试模块里要显式导入类型,`use gpui_kit::*` 会遮蔽 `#[test]`。被测试查询的元素带稳定的领域 id,自定义 `div` 还要加 `.test_support()`:`("host-row", id)`、`("host-os", id)` 与开始页的 `("recent-host-os", id)`(`aria_label` 是系统名,未探测时是「未探测到系统」)、`("group-row", id)`、`("terminal", id)`、`("explorer", explorer_id)`、`("local-pane"|"remote-pane", explorer_id)`(SFTP 的元素都按标签 id,测试里第一个 SFTP 标签是 `SFTP_TAB` = 1)、`("close-terminal", id)`、设置的 `open-settings`(主机面板底部)/ `settings`(页面)/ `settings-tab` / `close-settings`、设置页只渲染当前分类(左栏条目的 id 由 `Settings` 按位置生成,是 `within("settings")` 里的 `0-<序号>`,外观是 `0-0`、终端是 `0-1`、外部 CLI 是 `0-2`、关于是 `0-3`;开关是 `group-<组>` › `item-<项>` › `check`;外部 CLI 页的 `cli-binary`(安装 / 移除按钮)与 `cli-binary-status`、`skill-<agent>` 与 `skill-status-<agent>`(agent 为 `agents` / `codex` / `claude` / `opencode` / `workbuddy`,状态行的 `aria_label` 是显示的文字)、`check-cli-integration`、`copy-agent-skill`;设置项同理,下拉按钮是 `group-<组>` › `item-<项>` › `btn`,`label` 是当前选项的文字)、终端字体预览 `terminal-font-preview`(`aria_label` 形如「Menlo 13 px,行高 1.54」)、`("close-explorer", id)`、标签标题 `("terminal-tab", id)` / `("explorer-tab", explorer_id)` / `("local-terminal-tab", id)`(`aria_label` 是标签文字)、`("terminal-tab-os", id)` 与 `("explorer-tab-os", explorer_id)`、`test-connection`、`("terminal-latency", id)` 与 SFTP 标签的 `("sftp-latency", explorer_id)`(`aria_label` 是「32 ms」或「超时」;SFTP 测试往 `FakeSftpProvider::events` 里送 `SftpEvent::Latency`)、`terminal-find`(查找输入框)、`terminal-find-count`(`aria_label` 是「3/12」「无结果」或「N 项」,查询为空或搜索还在去抖时不存在)、`terminal-find-previous` / `-next` / `-close`、`tab-name`(重命名标签对话框)、侧栏行的 tooltip `host-tooltip` / `forward-tooltip` / `creLine truncated
|
||||
+1
-1
@@ -316,7 +316,7 @@ bash 在 shell 退出时才把这次执行的命令写进历史文件,所以
|
||||
| Windows x64 | 安装程序,按当前用户安装到 `%LOCALAPPDATA%\Programs\ShellRS`,不需要管理员 | 支持 |
|
||||
| Linux x64 | AppImage | 支持 |
|
||||
|
||||
ShellRS 启动 30 秒后检查一次有没有新版本,之后每 6 小时检查一次。有新版本就在后台下载并校验,完成后标题栏右上角出现更新图标。点开可以看更新内容,点「重启并安装」就换成新版本并重新打开。重启会断开已打开的连接、停止传输(续传进度保留)和端口转发,对话框里会写明具体有哪些。
|
||||
ShellRS 启动 30 秒后检查一次有没有新版本,之后每 6 小时检查一次。有新版本就在后台下载并校验,完成后标题栏右上角出现绿色的更新按钮。点开后,「查看更新内容」打开官网的更新日志,「重启并安装」换成新版本并重新打开。重启会断开已打开的连接、停止传输(续传进度保留)和端口转发,对话框里会写明具体有哪些。
|
||||
|
||||
「设置 › 关于」里的「应用更新」只有三行:
|
||||
|
||||
|
||||
+4
-1
@@ -121,12 +121,15 @@ gpui_kit::actions!(
|
||||
CheckForUpdates,
|
||||
/// Download the newer ShellRS that was found.
|
||||
DownloadUpdate,
|
||||
/// Show the newer ShellRS: what changed, and restarting into it.
|
||||
/// Show the newer ShellRS: restarting into it, and where to read
|
||||
/// what changed.
|
||||
ShowUpdate,
|
||||
/// Restart into the downloaded ShellRS.
|
||||
RestartToUpdate,
|
||||
/// Open the page to download ShellRS by hand.
|
||||
OpenDownloadPage,
|
||||
/// Open the website's changelog.
|
||||
OpenChangelog,
|
||||
]
|
||||
);
|
||||
|
||||
|
||||
+2
-1
@@ -65,7 +65,8 @@ icon_assets!(
|
||||
Ban,
|
||||
CircleDot,
|
||||
FileText,
|
||||
Zap
|
||||
Zap,
|
||||
ExternalLink
|
||||
]
|
||||
);
|
||||
|
||||
|
||||
@@ -27,6 +27,9 @@ pub const MANIFEST_URL: &str = "https://dl.shellrs.com/update/v1/{channel}.json"
|
||||
/// Where a person downloads ShellRS by hand.
|
||||
pub const DOWNLOAD_PAGE: &str = "https://shellrs.com/download";
|
||||
|
||||
/// What each version changed.
|
||||
pub const CHANGELOG_PAGE: &str = "https://shellrs.com/changelog";
|
||||
|
||||
/// The minisign public keys a manifest may be signed with: the key in use,
|
||||
/// and a spare kept offline so the first can be replaced. Rotating means
|
||||
/// shipping a release that knows the new key before signing with it.
|
||||
|
||||
+35
-44
@@ -1,5 +1,6 @@
|
||||
//! The update dialog: what the newer ShellRS brings, how far its download
|
||||
//! is, and restarting into it.
|
||||
//! The update dialog: how far the newer ShellRS's download is, and
|
||||
//! restarting into it. What it changes is not listed here: a button opens
|
||||
//! the website's changelog.
|
||||
//!
|
||||
//! Opened from the title bar button and from 设置 › 关于. What a restart
|
||||
//! interrupts is written here, above the button, so restarting needs no
|
||||
@@ -10,23 +11,20 @@ use std::rc::Rc;
|
||||
use gpui_kit::component::{
|
||||
ActiveTheme as _, Sizable as _, WindowExt as _,
|
||||
button::{Button, ButtonVariants as _},
|
||||
dialog::{DialogClose, DialogFooter},
|
||||
dialog::DialogFooter,
|
||||
progress::Progress,
|
||||
text::TextView,
|
||||
v_flex,
|
||||
};
|
||||
use gpui_kit::prelude::FluentBuilder as _;
|
||||
use gpui_kit::*;
|
||||
|
||||
use crate::app::{DownloadUpdate, OpenDownloadPage, RestartToUpdate};
|
||||
use crate::app::{CatalogIcon, DownloadUpdate, OpenChangelog, OpenDownloadPage, RestartToUpdate};
|
||||
|
||||
use super::status::{RestartImpact, UpdateStep, percent, restart_note};
|
||||
use super::updater::{Phase, Stage, UpdateSnapshot, Updater};
|
||||
|
||||
/// The dialog's width, in rems.
|
||||
const DIALOG_WIDTH: f32 = 32.;
|
||||
/// The notes scroll beyond this height, in rems.
|
||||
const NOTES_HEIGHT: f32 = 18.;
|
||||
|
||||
/// Counts what a restart would interrupt, at the time the dialog draws.
|
||||
pub type ImpactCounter = Rc<dyn Fn(&App) -> RestartImpact>;
|
||||
@@ -54,46 +52,31 @@ impl Render for UpdateDialog {
|
||||
let snapshot = self.updater.read(cx).snapshot();
|
||||
let impact = (self.impact)(cx);
|
||||
let muted = cx.theme().muted_foreground;
|
||||
let release = snapshot.release.clone();
|
||||
let versions = match snapshot
|
||||
.release
|
||||
.as_ref()
|
||||
.and_then(|release| release.published_on())
|
||||
{
|
||||
Some(day) => format!("当前版本 {} · 新版本发布于 {day}", snapshot.current),
|
||||
None => format!("当前版本 {}", snapshot.current),
|
||||
};
|
||||
v_flex()
|
||||
.id("update-dialog")
|
||||
.test_support()
|
||||
.gap_3()
|
||||
.text_sm()
|
||||
.child(
|
||||
div()
|
||||
.text_color(muted)
|
||||
.child(match (&release, &snapshot.manual) {
|
||||
(Some(release), _) => match release.published_on() {
|
||||
Some(day) => format!("当前版本 {} · 发布于 {day}", snapshot.current),
|
||||
None => format!("当前版本 {}", snapshot.current),
|
||||
},
|
||||
_ => format!("当前版本 {}", snapshot.current),
|
||||
}),
|
||||
)
|
||||
.children(
|
||||
release
|
||||
.as_ref()
|
||||
.filter(|release| !release.notes.trim().is_empty())
|
||||
.map(|release| {
|
||||
div()
|
||||
.id("update-notes")
|
||||
.test_support()
|
||||
.aria_label(release.notes.clone())
|
||||
.max_h(rems(NOTES_HEIGHT))
|
||||
.overflow_y_scroll()
|
||||
.rounded(cx.theme().radius)
|
||||
.border_1()
|
||||
.border_color(cx.theme().border)
|
||||
.p_3()
|
||||
.child(
|
||||
TextView::markdown(
|
||||
SharedString::from(format!("update-notes-{}", release.version)),
|
||||
release.notes.clone(),
|
||||
)
|
||||
.selectable(true),
|
||||
)
|
||||
}),
|
||||
v_flex()
|
||||
.gap_1()
|
||||
.when(snapshot.phase == Phase::Ready, |lines| {
|
||||
lines.child(
|
||||
div()
|
||||
.id("update-ready")
|
||||
.test_support()
|
||||
.child("新版本已下载,重启 ShellRS 即可完成更新。"),
|
||||
)
|
||||
})
|
||||
.child(div().text_color(muted).child(versions)),
|
||||
)
|
||||
.when_some(progress(&snapshot), |body, (label, value)| {
|
||||
body.child(
|
||||
@@ -193,7 +176,8 @@ pub fn open_update_dialog(
|
||||
});
|
||||
}
|
||||
|
||||
/// 稍后, and the one step that applies now.
|
||||
/// Where to read what changed, and the one step that applies now. The
|
||||
/// dialog closes by its close button or Escape.
|
||||
fn footer(snapshot: &UpdateSnapshot, dispatch: FocusHandle) -> DialogFooter {
|
||||
let action: Option<(&'static str, &'static str, Box<dyn Action>)> =
|
||||
snapshot.step().map(|step| -> (_, _, Box<dyn Action>) {
|
||||
@@ -213,9 +197,16 @@ fn footer(snapshot: &UpdateSnapshot, dispatch: FocusHandle) -> DialogFooter {
|
||||
),
|
||||
}
|
||||
});
|
||||
let close = if action.is_some() { "稍后" } else { "关闭" };
|
||||
let changelog = dispatch.clone();
|
||||
DialogFooter::new()
|
||||
.child(DialogClose::new().trigger(move |button| button.label(close)))
|
||||
.child(
|
||||
Button::new("open-changelog")
|
||||
.icon(CatalogIcon::ExternalLink)
|
||||
.label("查看更新内容")
|
||||
.on_click(move |_, window, cx| {
|
||||
changelog.dispatch_action(&OpenChangelog, window, cx);
|
||||
}),
|
||||
)
|
||||
.children(action.map(|(id, label, action)| {
|
||||
let dispatch = dispatch.clone();
|
||||
Button::new(id)
|
||||
|
||||
+18
-19
@@ -1,6 +1,5 @@
|
||||
use gpui_kit::component::{
|
||||
ActiveTheme as _, IconName, Selectable as _, Sizable as _, TitleBar,
|
||||
badge::Badge,
|
||||
button::{Button, ButtonGroup, ButtonVariants as _},
|
||||
h_flex,
|
||||
};
|
||||
@@ -11,6 +10,7 @@ use crate::app::{
|
||||
CatalogIcon, NewHost, NewLocalTerminal, ShowCredentials, ShowForwards, ShowHosts, ShowUpdate,
|
||||
ToggleHostPanel, ToggleTheme,
|
||||
};
|
||||
use crate::shared::tinted;
|
||||
use crate::update::UpdateBadge;
|
||||
|
||||
use super::sidebar::SidebarMode;
|
||||
@@ -146,25 +146,24 @@ pub fn render_title_bar(
|
||||
}),
|
||||
)
|
||||
.children(update.map(|update| {
|
||||
// A dot on the icon, in the colour of what it says.
|
||||
Badge::new()
|
||||
.dot()
|
||||
.color(if update.trouble {
|
||||
cx.theme().warning
|
||||
} else {
|
||||
cx.theme().success
|
||||
// The one coloured button up here, in the colour of
|
||||
// what it says: the only sign that an update waits.
|
||||
Button::new("update-available")
|
||||
.custom(tinted(
|
||||
if update.trouble {
|
||||
cx.theme().warning
|
||||
} else {
|
||||
cx.theme().success
|
||||
},
|
||||
cx,
|
||||
))
|
||||
.small()
|
||||
.icon(CatalogIcon::CircleArrowUp)
|
||||
.tooltip(update.label.clone())
|
||||
.accessibility_label(update.label)
|
||||
.on_click(move |_, window, cx| {
|
||||
show_update.dispatch_action(&ShowUpdate, window, cx)
|
||||
})
|
||||
.child(
|
||||
Button::new("update-available")
|
||||
.ghost()
|
||||
.small()
|
||||
.icon(CatalogIcon::CircleArrowUp)
|
||||
.tooltip(update.label.clone())
|
||||
.accessibility_label(update.label)
|
||||
.on_click(move |_, window, cx| {
|
||||
show_update.dispatch_action(&ShowUpdate, window, cx)
|
||||
}),
|
||||
)
|
||||
}))
|
||||
.child(
|
||||
Button::new("theme-toggle")
|
||||
|
||||
@@ -6,9 +6,12 @@ use std::rc::Rc;
|
||||
use gpui_kit::component::{WindowExt as _, notification::Notification};
|
||||
use gpui_kit::*;
|
||||
|
||||
use crate::app::{CheckForUpdates, DownloadUpdate, OpenDownloadPage, RestartToUpdate, ShowUpdate};
|
||||
use crate::app::{
|
||||
CheckForUpdates, DownloadUpdate, OpenChangelog, OpenDownloadPage, RestartToUpdate, ShowUpdate,
|
||||
};
|
||||
use crate::host::ConnectionState;
|
||||
use crate::terminal::TerminalLifecycle;
|
||||
use crate::update::build_info::CHANGELOG_PAGE;
|
||||
use crate::update::{RestartImpact, UpdaterEvent, open_update_dialog};
|
||||
|
||||
use super::workspace_view::Workspace;
|
||||
@@ -76,6 +79,16 @@ impl Workspace {
|
||||
cx.open_url(&page);
|
||||
}
|
||||
|
||||
/// The whole changelog, not one version's section.
|
||||
pub(super) fn on_open_changelog(
|
||||
&mut self,
|
||||
_: &OpenChangelog,
|
||||
_: &mut Window,
|
||||
cx: &mut Context<Self>,
|
||||
) {
|
||||
cx.open_url(CHANGELOG_PAGE);
|
||||
}
|
||||
|
||||
/// The title bar shows the update button only in some phases; the
|
||||
/// version after an update is said once the new one runs.
|
||||
pub(super) fn on_updater_event(
|
||||
|
||||
@@ -2215,6 +2215,7 @@ impl Render for Workspace {
|
||||
.on_action(cx.listener(Self::on_show_update))
|
||||
.on_action(cx.listener(Self::on_restart_to_update))
|
||||
.on_action(cx.listener(Self::on_open_download_page))
|
||||
.on_action(cx.listener(Self::on_open_changelog))
|
||||
.on_action(cx.listener(Self::on_copy_agent_skill))
|
||||
.on_action(cx.listener(Self::on_close_settings))
|
||||
.on_action(cx.listener(Self::on_close_active_tab))
|
||||
|
||||
+12
-7
@@ -376,7 +376,7 @@ async fn with_automatic_updates_off_a_found_update_waits_for_download(cx: &mut T
|
||||
}
|
||||
|
||||
#[gpui_kit::test]
|
||||
async fn the_update_dialog_shows_the_notes_and_restarts_into_the_new_version(
|
||||
async fn the_update_dialog_links_the_changelog_and_restarts_into_the_new_version(
|
||||
cx: &mut TestAppContext,
|
||||
) {
|
||||
let (handle, workspace) = open_workspace(cx);
|
||||
@@ -393,15 +393,20 @@ async fn the_update_dialog_shows_the_notes_and_restarts_into_the_new_version(
|
||||
window.click("update-available", cx)
|
||||
});
|
||||
in_frame(cx, handle, |window, _| {
|
||||
let notes = window
|
||||
.find("update-notes")
|
||||
.label()
|
||||
.unwrap_or_default()
|
||||
.to_string();
|
||||
assert!(notes.contains("在线升级"), "{notes}");
|
||||
assert!(window.try_find("update-ready").is_some());
|
||||
assert!(window.try_find("restart-to-update").is_some());
|
||||
});
|
||||
|
||||
// What changed is on the website's changelog; the dialog stays.
|
||||
in_frame(cx, handle, |window, cx| window.click("open-changelog", cx));
|
||||
assert_eq!(
|
||||
cx.opened_url().as_deref(),
|
||||
Some("https://shellrs.com/changelog")
|
||||
);
|
||||
in_frame(cx, handle, |window, _| {
|
||||
assert!(window.try_find("update-dialog").is_some());
|
||||
});
|
||||
|
||||
let restart = cx.expect_restart();
|
||||
in_frame(cx, handle, |window, cx| {
|
||||
window.click("restart-to-update", cx)
|
||||
|
||||
Reference in new issue
Block a user