From a4dbdc7bf11731147bfb9e716439cb47b24f8b9b Mon Sep 17 00:00:00 2001 From: l0ng-ai <24760907+l0ng-ai@users.noreply.github.com> Date: Wed, 23 Sep 2026 00:24:38 +0800 Subject: [PATCH 1/4] Redesign settings navigation, search, and editing workflows --- README.md | 2 +- README.zh-CN.md | 2 +- crates/tty7-core/src/core/config.rs | 33 +- docs/agents/orchestration.mdx | 3 +- docs/agents/overview.mdx | 2 +- docs/agents/status.mdx | 4 +- docs/cli/agent-skill.mdx | 2 +- docs/customization/fonts.mdx | 2 +- docs/customization/settings.mdx | 50 +- docs/getting-started/first-launch.mdx | 6 +- docs/getting-started/installation.mdx | 3 +- docs/reference/troubleshooting.mdx | 8 +- docs/terminal/history.mdx | 4 +- docs/terminal/mouse-and-scrolling.mdx | 4 +- docs/terminal/prompt.mdx | 12 +- docs/terminal/selection-and-clipboard.mdx | 4 +- src/core/config.rs | 15 +- src/ui/app.rs | 547 ++++- src/ui/i18n/en.rs | 68 +- src/ui/i18n/ja.rs | 73 +- src/ui/i18n/mod.rs | 23 +- src/ui/i18n/zh.rs | 71 +- src/ui/settings.rs | 2333 ++++++++++++++++----- src/ui/ssh_connect.rs | 33 +- 24 files changed, 2599 insertions(+), 705 deletions(-) diff --git a/README.md b/README.md index fa649748..e2df9af9 100644 --- a/README.md +++ b/README.md @@ -63,7 +63,7 @@ Native builds for macOS, Windows, and Linux on [**Releases**](https://github.com ## Supported agents **Detection** is free: brand avatar, branch + diff, tab title. -**Status** takes one click under Settings → Agents to install that agent's hook, +**Status** takes one click under Settings → Integrations to install that agent's hook, and brings the status dot, notifications, the tray icon, `tty7 wait`, and resume after a reboot. **Fork** needs both — the agent's own fork command, and the hook that tells tty7 which session to fork. diff --git a/README.zh-CN.md b/README.zh-CN.md index b782628c..c063e975 100644 --- a/README.zh-CN.md +++ b/README.zh-CN.md @@ -62,7 +62,7 @@ macOS、Windows、Linux 的原生构建都在 [**Releases**](https://github.com/ ## 支持的 agent **识别**无需配置:品牌头像、分支与 diff、标签页标题。 -**状态**需要在设置 → Agents 中为该 agent 安装 hook,一次点击,之后才有状态点、通知、托盘提醒、`tty7 wait` 和重启后恢复会话。 +**状态**需要在设置 → 集成 中为该 agent 安装 hook,一次点击,之后才有状态点、通知、托盘提醒、`tty7 wait` 和重启后恢复会话。 **Fork** 两个条件都要:agent 自己提供 fork 命令,且 hook 已装——tty7 得知道 fork 的是哪个会话。
diff --git a/crates/tty7-core/src/core/config.rs b/crates/tty7-core/src/core/config.rs index e30445fc..eb725298 100644 --- a/crates/tty7-core/src/core/config.rs +++ b/crates/tty7-core/src/core/config.rs @@ -841,28 +841,25 @@ impl Config { } pub fn save(&self) { + if let Err(error) = self.try_save() { + log::warn!("failed to save config: {error}"); + } + } + + /// Persist without hiding a failure from an interactive settings editor. + pub fn try_save(&self) -> std::io::Result<()> { if self.quarantined { - // The file this instance stands in for could not be read, so what - // the user wrote is still on disk — writing these defaults over it - // is the wholesale loss #537 is about. The fix is to repair the - // file; the next load that parses produces a writable config. - log::warn!("not saving over a config file that failed to load; fix or remove it first"); - return; + return Err(std::io::Error::other( + "the existing configuration could not be read; repair it before saving", + )); } - let Some(path) = Self::path() else { - return; - }; + let path = Self::path() + .ok_or_else(|| std::io::Error::other("configuration directory is unavailable"))?; if let Some(parent) = path.parent() { - let _ = std::fs::create_dir_all(parent); - } - match serde_json::to_string_pretty(self) { - Ok(text) => { - if let Err(e) = write_atomic(&path, text.as_bytes()) { - log::warn!("failed to write config at {}: {e}", path.display()); - } - } - Err(e) => log::warn!("failed to serialize config: {e}"), + std::fs::create_dir_all(parent)?; } + let text = serde_json::to_string_pretty(self).map_err(std::io::Error::other)?; + write_atomic(&path, text.as_bytes()) } fn path() -> Option { diff --git a/docs/agents/orchestration.mdx b/docs/agents/orchestration.mdx index 7c830378..b8020eb1 100644 --- a/docs/agents/orchestration.mdx +++ b/docs/agents/orchestration.mdx @@ -154,8 +154,7 @@ If you are an agent yourself, you are in that list too. ## Teaching an agent to do this -tty7 installs nothing into `~/.claude` for it — no switch in **Settings → -Agents** writes a skill, and none ever will. What the agent needs to know ships +tty7 installs nothing into `~/.claude` for it — no switch in **Settings → Integrations** writes a skill, and none ever will. What the agent needs to know ships in the repository instead, as a skill you install yourself: ```bash diff --git a/docs/agents/overview.mdx b/docs/agents/overview.mdx index e0e21f5b..dd543888 100644 --- a/docs/agents/overview.mdx +++ b/docs/agents/overview.mdx @@ -91,7 +91,7 @@ Live status — **working**, **needs your input**, **done** — comes from the a itself, over a channel tty7 installs into that agent's configuration. It powers the status dots, the notifications, the tray icon, and `tty7 wait`. -Installing takes one click per agent under **Settings → Agents**. +Installing takes one click per agent under **Settings → Integrations**. [Status and notifications →](/agents/status) ## Where to go next diff --git a/docs/agents/status.mdx b/docs/agents/status.mdx index 256e7a87..095a9899 100644 --- a/docs/agents/status.mdx +++ b/docs/agents/status.mdx @@ -9,7 +9,7 @@ the agent say which one it is. ## Installing the hooks -**Settings → Agents** lists every agent that can report status, with an +**Settings → Integrations** lists every agent that can report status, with an **Install** button beside each: | Agent | | @@ -53,7 +53,7 @@ The same three states are what `tty7 agents` reports as `working` / `waiting` / ## Notifications -Two, both following your **Settings → Window & Tabs → Notifications** policy: +Two, both following your **Settings → General → Notifications** policy: - **"needs your permission…"** the moment an agent blocks on you - **"finished after 42s"** at the end of a turn diff --git a/docs/cli/agent-skill.mdx b/docs/cli/agent-skill.mdx index 09fcaaa2..fcebaa01 100644 --- a/docs/cli/agent-skill.mdx +++ b/docs/cli/agent-skill.mdx @@ -19,7 +19,7 @@ repository: a `SKILL.md` and two references — the full command table, and a delegation playbook the agent reads before handing work to another agent. - This is the only skill tty7 has — nothing in **Settings → Agents** installs + This is the only skill tty7 has — nothing in **Settings → Integrations** installs one for you. Using the CLI to run *other* agents — a worker pane, `tty7 wait`, collecting the result — is covered separately. [Orchestration →](/agents/orchestration) diff --git a/docs/customization/fonts.mdx b/docs/customization/fonts.mdx index 3e1b59f5..7d47f4fc 100644 --- a/docs/customization/fonts.mdx +++ b/docs/customization/fonts.mdx @@ -3,7 +3,7 @@ title: "Fonts" description: "The bundled default, fallback chains, ligatures, and why CJK needs a word." --- -**Settings → Appearance → Typography** covers the everyday choices; the rest is +**Settings → Appearance → Terminal text** covers the everyday choices; the rest is `config.json`. | Setting | Default | | diff --git a/docs/customization/settings.mdx b/docs/customization/settings.mdx index b93b5526..81aaa7d0 100644 --- a/docs/customization/settings.mdx +++ b/docs/customization/settings.mdx @@ -14,33 +14,51 @@ section something is in. ## The eight sections + + Interface language, startup and layout restore, tray icon, default terminal, + and command completion notifications. + - Theme, sync with system, typography, cursor, transparency, language. + Theme and colors, interface and terminal fonts, cursor, background image, + transparency, and inactive panes. - Shell and start directory, scrollback and scrolling, mouse, bell, per-pane - history. + Shell and starting directory, prompt editor, completion and command history, + scrollback, scrolling, bell, and opening links and files. - - Prompt features, selection & clipboard, keyboard (Option as Meta), links. - - - Hosts, defaults, security, and every per-profile field. - - - Hook installation per agent and per machine, and the CLI on PATH. + + Keyboard shortcuts, Option as Meta, mouse behavior, selection, and clipboard. - Startup window, tab bar position and grouping, notifications, tray icon. + Tab position and placement, sidebar grouping, and diff previews. - - Every shortcut, the tmux preset, the prefix. + + Hosts, connection defaults, security, authentication, proxies, and forwarding. + + + AI agent hooks per machine and the tty7 command on PATH. - Version, update channel, and the updater. + Version, updates and their proxy, and the background session service. +## Search and edit + +Search matches setting names, descriptions, English aliases, and configuration +keys such as `mouse_zoom_modifier`. Ordinary results are editable directly; +use the path above a result to open its category. SSH, theme editors, and +keyboard shortcuts have dedicated views. + +Use **Modified only** to find settings that differ from their defaults. A +modified setting offers **Reset setting**; installation and maintenance actions +do not have a generic reset. + +Ordinary controls save automatically. Text fields commit valid values on Enter +or when focus leaves the field. Custom theme edits are previewed until you save; +cancel restores the original theme. SSH profiles also use explicit saving. +Leaving an edited form offers save, discard, or continue editing. + ## The settings file Everything the Settings window writes goes to one file: @@ -75,7 +93,7 @@ The file is written atomically, and read forgivingly: ## Language -**Settings → Appearance → Language** switches the interface between English, +**Settings → General → Language** switches the interface between English, 简体中文, and 日本語. The choice is explicit — the system language is never inferred — and CLI output stays English regardless, so agent and script integrations keep a stable surface. diff --git a/docs/getting-started/first-launch.mdx b/docs/getting-started/first-launch.mdx index 80eb645a..f5246011 100644 --- a/docs/getting-started/first-launch.mdx +++ b/docs/getting-started/first-launch.mdx @@ -45,7 +45,7 @@ inheriting the active pane's directory either way. ## 3. macOS only: decide what Option does -**Settings → Input → Keyboard → Option (⌥) acts as Meta.** +**Settings → Keyboard & Mouse → Keyboard → Option (⌥) acts as Meta.** Off (the default), ⌥ B types `∫`, which is what macOS has always done. On, it sends the escape chord shells expect, so ⌥ B moves back @@ -54,7 +54,7 @@ leave it off if you type accented characters. ## 4. If you use coding agents, install the hooks -**Settings → Agents.** tty7 detects 20 coding CLIs by process name on its own — +**Settings → Integrations.** tty7 detects 20 coding CLIs by process name on its own — you get brand avatars and tab labels for free. The *status dots*, the "needs your permission" notifications, and `tty7 wait` all need one more thing: a small hook the agent calls to report what it is doing. @@ -84,7 +84,7 @@ shells. ## 6. Tune the notifications -**Settings → Window & Tabs → Notifications.** By default tty7 posts a desktop +**Settings → General → Notifications.** By default tty7 posts a desktop notification when a foreground command that ran longer than 10 seconds finishes — but only while the window is unfocused. Set **Notify on command finish** to *Never* or *Always*, and move the threshold if 10 seconds is the diff --git a/docs/getting-started/installation.mdx b/docs/getting-started/installation.mdx index 4f5f3e59..1211a214 100644 --- a/docs/getting-started/installation.mdx +++ b/docs/getting-started/installation.mdx @@ -79,8 +79,7 @@ in some other terminal — open panes and read them back. uninstaller removes it again. A `tty7` you installed yourself — a `cargo install` build, a package manager's -copy — is never replaced. To turn the whole thing off, uncheck **Settings → -Agents → Install the tty7 command on PATH**. +copy — is never replaced. To turn the whole thing off, uncheck **Settings → Integrations → Install the tty7 command on PATH**. Inside a tty7 pane the CLI works regardless of PATH, because panes inherit the diff --git a/docs/reference/troubleshooting.mdx b/docs/reference/troubleshooting.mdx index ee5b40bc..f8de1b27 100644 --- a/docs/reference/troubleshooting.mdx +++ b/docs/reference/troubleshooting.mdx @@ -17,7 +17,7 @@ machine links exist. Most of what follows is a specific answer this gives you. The CLI is put on PATH the first time the app launches. If it is missing: -- Check **Settings → Agents → Install the tty7 command on PATH** is on. +- Check **Settings → Integrations → Install the tty7 command on PATH** is on. - On Unix it symlinks into whichever of `/opt/homebrew/bin`, `/usr/local/bin`, `~/.local/bin`, `~/bin`, `~/.cargo/bin` your PATH already covers — if none of those are on your PATH, add one. @@ -77,8 +77,8 @@ the local server already holds. Connect from the GUI first. Both are switches, and turning one off hands the key straight back to your shell: -- **Settings → Input → Prompt → Tab completion** -- **Settings → Input → Prompt → History search** +- **Settings → Terminal → Prompt & command history → Tab completion** +- **Settings → Terminal → Prompt & command history → Command history search** If they do nothing at all in a particular pane, the shell there probably has no [shell integration](/reference/shell-integration) — elvish, xonsh and friends @@ -157,7 +157,7 @@ on the far end) or for a local pane on **Windows**. ## ⌥ B types `∫` instead of moving a word -That is macOS's default. Turn on **Settings → Input → Keyboard → Option (⌥) acts +That is macOS's default. Turn on **Settings → Keyboard & Mouse → Keyboard → Option (⌥) acts as Meta**. ## CJK characters have a gap on the right diff --git a/docs/terminal/history.mdx b/docs/terminal/history.mdx index 1a5c9225..a400915e 100644 --- a/docs/terminal/history.mdx +++ b/docs/terminal/history.mdx @@ -28,7 +28,7 @@ runs it. Esc closes without touching it. ### Handing ⌃ R back If you already have an fzf, percol, atuin or McFly binding you like, turn off -**Settings → Input → Prompt → History search** (`history_search: false`). +**Settings → Terminal → Prompt & command history → Command history search** (`history_search: false`). ⌃ R then goes to the shell, and whatever you bound there keeps working. @@ -52,7 +52,7 @@ never seen still appears; it just arrives without a timestamp or an exit code. By default every pane shares your shell's history file, which is what a terminal has always done: a command typed in one pane is available in the next. -**Settings → Input → Prompt → Give each pane its own shell history** +**Settings → Terminal → Prompt & command history → Separate command history per pane** (`per_pane_history: true`) changes that. Each pane gets a private history file: - **seeded** from your real history when the pane opens, so it is not blank diff --git a/docs/terminal/mouse-and-scrolling.mdx b/docs/terminal/mouse-and-scrolling.mdx index cd77a3a7..e5871290 100644 --- a/docs/terminal/mouse-and-scrolling.mdx +++ b/docs/terminal/mouse-and-scrolling.mdx @@ -45,7 +45,7 @@ Under **Settings → Terminal → Mouse**: | ⌘ 0 | Back to the configured size | | ⌘ + wheel | Zoom by scrolling over a terminal | -The base size is **Settings → Appearance → Typography → Font size**, 15 px by +The base size is **Settings → Appearance → Terminal text → Terminal font size**, 15 px by default. The rest of the interface has its own size — **Interface font size**, 16 px, adjustable from 12 to 24 — so you can scale the chrome without touching the terminal grid, or the other way round. @@ -63,7 +63,7 @@ the terminal grid, or the other way round. ## Command-finished notifications -**Settings → Window & Tabs → Notifications** posts a desktop notification when a +**Settings → General → Notifications** posts a desktop notification when a foreground command finishes: - **Notify on command finish** — *Never*, *When unfocused* (default), or diff --git a/docs/terminal/prompt.mdx b/docs/terminal/prompt.mdx index a7228d06..075e8b4b 100644 --- a/docs/terminal/prompt.mdx +++ b/docs/terminal/prompt.mdx @@ -42,8 +42,8 @@ build up first, and it carries across sessions and reboots. When tty7 has nothing useful to offer, the ⇥ falls through to your shell's own completion, so a carefully configured zsh setup is not lost. -To hand ⇥ back to the shell entirely, turn off **Settings → Input → -Prompt → Tab completion** (`tab_completion` in `config.json`). +To hand ⇥ back to the shell entirely, turn off **Settings → Terminal → +Prompt & command history → Tab completion** (`tab_completion` in `config.json`). ## Syntax highlighting @@ -63,7 +63,7 @@ The prompt behaves like a text field, because it is one: Everything readline does still works — this sits on top, it does not replace it. - On macOS, turn on **Settings → Input → Keyboard → Option (⌥) acts as Meta** if + On macOS, turn on **Settings → Keyboard & Mouse → Keyboard → Option (⌥) acts as Meta** if you want ⌥ B / ⌥ F to move by word instead of typing `∫` and `ƒ`. @@ -126,12 +126,12 @@ to the shell: | Setting | Key it releases | |---|---| -| **Settings → Input → Prompt → Tab completion** | ⇥ → your shell's completion | -| **Settings → Input → Prompt → History search** | ⌃ R → your shell's reverse-i-search, or your fzf binding | +| **Settings → Terminal → Prompt & command history → Tab completion** | ⇥ → your shell's completion | +| **Settings → Terminal → Prompt & command history → Command history search** | ⌃ R → your shell's reverse-i-search, or your fzf binding | ### Giving the whole prompt back to the shell -Turning off **Settings → Input → Prompt → Prompt editor** +Turning off **Settings → Terminal → Prompt & command history → tty7 prompt editor** (`prompt_editor: false`) hands over not one key but the line itself. Every keystroke at the prompt — printable characters, arrows, IME commits, paste — goes straight to the PTY, and your shell's own line editor does the editing: zsh's diff --git a/docs/terminal/selection-and-clipboard.mdx b/docs/terminal/selection-and-clipboard.mdx index 30856770..ef3547c0 100644 --- a/docs/terminal/selection-and-clipboard.mdx +++ b/docs/terminal/selection-and-clipboard.mdx @@ -26,7 +26,7 @@ you are pointing at: | A bracket or quote | The matching pair, and everything between them | | CJK text | The word, segmented by dictionary rather than by character | -Turn it off with **Settings → Input → Selection & clipboard → Smart selection**. +Turn it off with **Settings → Keyboard & Mouse → Selection & clipboard → Smart selection**. With it off, double-click falls back to plain word selection using the `word_separators` list from `config.json` — by default: @@ -41,7 +41,7 @@ With it off, double-click falls back to plain word selection using the | Copy | ⌘ C | Ctrl ⇧ C | | Paste | ⌘ V | Ctrl ⇧ V · ⇧ Insert | -Two settings shape what lands where, both under **Settings → Input → Selection +Two settings shape what lands where, both under **Settings → Keyboard & Mouse → Selection & clipboard**: diff --git a/src/core/config.rs b/src/core/config.rs index 116689c1..dfffb045 100644 --- a/src/core/config.rs +++ b/src/core/config.rs @@ -21,8 +21,19 @@ impl Config { #[cfg(test)] pub fn save(&self) { - assert_scratch_config_dir("Config::save"); - self.0.save(); + if let Err(error) = self.try_save() { + log::warn!("failed to save config: {error}"); + } + } + + #[cfg(test)] + pub fn try_save(&self) -> std::io::Result<()> { + assert_scratch_config_dir("Config::try_save"); + // GUI tests have separate App globals but share the process's scratch + // directory. Serialize their writes just as a real UI event loop does. + static WRITE_LOCK: std::sync::Mutex<()> = std::sync::Mutex::new(()); + let _guard = WRITE_LOCK.lock().unwrap_or_else(|error| error.into_inner()); + self.0.try_save() } } diff --git a/src/ui/app.rs b/src/ui/app.rs index 9fbe7bc3..90e34dfb 100644 --- a/src/ui/app.rs +++ b/src/ui/app.rs @@ -865,6 +865,7 @@ pub struct Tty7App { /// the live search matched, so exactly one row per page carries the anchor /// the page scrolls to. pub(crate) settings_hit_anchored: Cell, + last_settings_location: (SettingsSection, gpui::Point), pub(crate) right_panel_width: Rc>, pub(crate) right_panel_dragging: Rc>, /// The docked document column's share of the terminal column, live. Held @@ -1472,6 +1473,7 @@ impl Tty7App { settings_row_width: Cell::new(f32::MAX), settings_viewport_w: Cell::new(f32::MAX), settings_hit_anchored: Cell::new(false), + last_settings_location: (SettingsSection::General, gpui::point(px(0.), px(0.))), right_panel_width: Rc::new(Cell::new(right_panel_width)), right_panel_dragging: Rc::new(Cell::new(false)), document_ratio: Rc::new(Cell::new(document_ratio)), @@ -2145,7 +2147,7 @@ impl Tty7App { } let cfg = cx.global_mut::(); cfg.font_size = size; - cfg.save(); + self.persist_settings_config(cx); cx.notify(); } @@ -2165,7 +2167,7 @@ impl Tty7App { return; } cfg.ui_font_size = size; - cfg.save(); + self.persist_settings_config(cx); // Unlike the settings that only redraw the window they were changed // in, this one re-lays-out every open window, and each reads the new // rem from the global on its own next frame. @@ -2198,7 +2200,7 @@ impl Tty7App { } let cfg = cx.global_mut::(); cfg.line_height = mul; - cfg.save(); + self.persist_settings_config(cx); cx.notify(); } @@ -2211,6 +2213,13 @@ impl Tty7App { } pub(crate) fn set_preset(&mut self, id: &str, window: &mut Window, cx: &mut Context) { + if self.theme_draft_dirty() { + let id = id.to_string(); + self.with_settings_edits_resolved(window, cx, move |this, window, cx| { + this.set_preset(&id, window, cx) + }); + return; + } // A confirmed pick ends any preview: there is nothing left to roll back. self.theme_preview_restore = None; self.write_preset(id, cx); @@ -2258,6 +2267,13 @@ impl Tty7App { window: &mut Window, cx: &mut Context, ) { + if self.theme_draft_dirty() { + let id = id.to_string(); + self.with_settings_edits_resolved(window, cx, move |this, window, cx| { + this.set_slot_preset(dark_slot, &id, window, cx) + }); + return; + } let cfg = cx.global_mut::(); if dark_slot { cfg.theme_preset_dark = id.to_string(); @@ -2273,6 +2289,12 @@ impl Tty7App { window: &mut Window, cx: &mut Context, ) { + if self.theme_draft_dirty() { + self.with_settings_edits_resolved(window, cx, move |this, window, cx| { + this.set_theme_follow_system(on, window, cx) + }); + return; + } if on { let manual = cx.global::().theme_preset.clone(); let manual_dark = crate::ui::presets::by_id(cx, &manual).dark; @@ -2325,7 +2347,7 @@ impl Tty7App { apply_theme(Some(window), cx); set_menus(cx); if persist { - cx.global::().save(); + self.persist_settings_config(cx); } self.rebuild_theme_editor(window, cx); self.sync_window_opacity_slider(window, cx); @@ -2427,6 +2449,54 @@ impl Tty7App { } } + pub(crate) fn theme_draft_dirty(&self) -> bool { + self.active_settings() + .is_some_and(|s| s.theme_draft.is_some()) + } + + pub(crate) fn save_theme_draft(&mut self, window: &mut Window, cx: &mut Context) -> bool { + let Some((_, draft)) = self.active_settings().and_then(|s| s.theme_draft.clone()) else { + return true; + }; + if let Err(error) = crate::ui::presets::write_theme_file(&draft) { + if let Some(s) = self.active_settings_mut() { + s.theme_draft_error = Some(error.to_string()); + } + crate::ui::host_ops::HostOps::notify_err( + window, + cx, + t(L10nKey::ThemeSaveFailed), + &error, + ); + return false; + } + if let Some(s) = self.active_settings_mut() { + s.theme_draft = None; + s.theme_draft_error = None; + } + cx.notify(); + true + } + + pub(crate) fn cancel_theme_draft(&mut self, window: &mut Window, cx: &mut Context) { + if let Some(s) = self.active_settings_mut() { + s.theme_draft_error = None; + } + if let Some((original, _)) = self + .active_settings_mut() + .and_then(|s| s.theme_draft.take()) + { + let mut themes = crate::ui::presets::all(cx); + if let Some(slot) = themes.iter_mut().find(|t| t.id == original.id) { + *slot = original; + } + cx.set_global(crate::ui::presets::Themes(themes)); + apply_theme(Some(window), cx); + self.rebuild_theme_editor(window, cx); + cx.notify(); + } + } + fn mutate_active_theme( &mut self, mutate: impl FnOnce(&mut crate::ui::presets::Theme), @@ -2439,14 +2509,35 @@ impl Tty7App { return; } mutate(&mut theme); - if let Err(e) = crate::ui::presets::write_theme_file(&theme) { - // Every colour edit runs through here. Without this the picker - // moves, the theme does not, and nothing says why. - log::warn!("failed to write theme file: {e}"); - crate::ui::host_ops::HostOps::notify_err(window, cx, t(L10nKey::ThemeSaveFailed), &e); - return; + if let Some(state) = self.active_settings_mut() { + let original = state + .theme_draft + .as_ref() + .filter(|(original, _)| original.id == id) + .map(|(original, _)| original.clone()) + .unwrap_or_else(|| crate::ui::presets::by_id(cx, &id)); + if crate::ui::presets::to_yaml(&original) == crate::ui::presets::to_yaml(&theme) { + state.theme_draft = None; + } else { + state.theme_draft = Some((original, theme.clone())); + } + let mut themes = crate::ui::presets::all(cx); + if let Some(slot) = themes.iter_mut().find(|t| t.id == id) { + *slot = theme; + } + cx.set_global(crate::ui::presets::Themes(themes)); + } else { + if let Err(error) = crate::ui::presets::write_theme_file(&theme) { + crate::ui::host_ops::HostOps::notify_err( + window, + cx, + t(L10nKey::ThemeSaveFailed), + &error, + ); + return; + } + crate::ui::presets::load_registry(cx); } - crate::ui::presets::load_registry(cx); apply_theme(Some(window), cx); cx.notify(); } @@ -2490,7 +2581,7 @@ impl Tty7App { ) { cx.global_mut::().window_opacity = Some(v.clamp(0.2, 1.0)); apply_theme(Some(window), cx); - cx.global::().save(); + self.persist_settings_config(cx); cx.notify(); } @@ -2502,7 +2593,7 @@ impl Tty7App { ) { cx.global_mut::().window_blur = Some(on); apply_theme(Some(window), cx); - cx.global::().save(); + self.persist_settings_config(cx); cx.notify(); } @@ -2515,7 +2606,7 @@ impl Tty7App { ) { cx.global_mut::().window_backdrop = backdrop; apply_theme(Some(window), cx); - cx.global::().save(); + self.persist_settings_config(cx); // A material changes the default opacity (SYSTEM_MATERIAL_OPACITY // vs 1.0), so the slider must track the new effective value. self.sync_window_opacity_slider(window, cx); @@ -2533,7 +2624,7 @@ impl Tty7App { clear_window_override_values(config, cfg!(target_os = "windows")); } apply_theme(Some(window), cx); - cx.global::().save(); + self.persist_settings_config(cx); self.sync_window_opacity_slider(window, cx); #[cfg(target_os = "windows")] self.sync_window_backdrop_select(window, cx); @@ -2731,7 +2822,7 @@ impl Tty7App { } let cfg = cx.global_mut::(); cfg.font_features = features; - cfg.save(); + self.persist_settings_config(cx); cx.notify(); } @@ -2754,15 +2845,42 @@ impl Tty7App { self.apply_terminal_config_to_panes(&cfg, cx); } + pub(crate) fn persist_settings_config(&mut self, cx: &mut Context) { + let config = cx.global::().clone(); + let error = config.try_save().err().map(|error| error.to_string()); + if let Some(message) = &error { + log::warn!("failed to save settings: {message}"); + } + if let Some(s) = self.active_settings_mut() { + if error.is_none() { + s.saved_config = config; + } + s.save_error = error; + } + cx.notify(); + } + + pub(crate) fn discard_unsaved_settings(&mut self, window: &mut Window, cx: &mut Context) { + let snapshot = self + .active_settings() + .filter(|s| s.save_error.is_some()) + .map(|s| s.saved_config.clone()); + if let Some(snapshot) = snapshot { + cx.set_global(snapshot); + if let Some(s) = self.active_settings_mut() { + s.save_error = None; + } + self.reload_from_config(window, cx); + } + } + pub(crate) fn update_config( &mut self, cx: &mut Context, mutate: impl FnOnce(&mut Config), ) { - let cfg = cx.global_mut::(); - mutate(cfg); - cfg.save(); - cx.notify(); + mutate(cx.global_mut::()); + self.persist_settings_config(cx); } pub(crate) fn set_link_url(&mut self, on: bool, cx: &mut Context) { @@ -5517,6 +5635,246 @@ impl Tty7App { ); } + pub(crate) fn reset_settings_value( + &mut self, + title: L10nKey, + window: &mut Window, + cx: &mut Context, + ) { + let defaults = Config::default(); + match title { + L10nKey::SettingsDimInactivePanes => { + self.set_dim_inactive_panes(defaults.dim_inactive_panes, cx) + } + L10nKey::SettingsCursorBlink => self.set_cursor_blink(defaults.cursor_blink, cx), + L10nKey::SettingsCursorShape => self.set_cursor_style(defaults.cursor_style, cx), + L10nKey::SettingsScrollback => self.set_scrollback_limit(defaults.scrollback_limit, cx), + L10nKey::SettingsNewTabPosition => { + self.set_new_tab_position(defaults.new_tab_position, cx) + } + L10nKey::SettingsTabBarPosition => { + self.set_tab_bar_position(defaults.tab_bar_position, cx) + } + L10nKey::SettingsSidebarGrouping => { + self.set_sidebar_grouping(defaults.sidebar_grouping, cx) + } + L10nKey::SettingsDiffPreviewFromCounts => { + self.set_sidebar_diff_preview(defaults.sidebar_diff_preview, cx) + } + L10nKey::SettingsNotifyOnCommandFinish => { + self.set_notify_mode(defaults.notify_on_command_finish, cx) + } + L10nKey::SettingsNotifyThreshold => { + self.set_notify_threshold(defaults.notify_threshold_secs, cx) + } + L10nKey::SettingsTerminalBell => self.set_bell_mode(defaults.bell, cx), + L10nKey::SettingsRestoreLastLayout => { + self.set_restore_session(defaults.restore_session, cx) + } + L10nKey::SettingsPerPaneHistory => { + self.set_per_pane_history(defaults.per_pane_history, cx) + } + L10nKey::SettingsShowTrayIcon => self.set_show_tray_icon(defaults.show_tray_icon, cx), + L10nKey::SettingsOptionAsMeta => { + self.set_macos_option_as_alt(defaults.macos_option_as_alt, cx) + } + L10nKey::SettingsHideMouseWhileTyping => { + self.set_mouse_hide_while_typing(defaults.mouse_hide_while_typing, cx) + } + L10nKey::SettingsFocusFollowsMouse => { + self.set_focus_follows_mouse(defaults.focus_follows_mouse, cx) + } + L10nKey::SettingsReportMouseToApps => { + self.set_mouse_reporting(defaults.mouse_reporting, cx) + } + L10nKey::SettingsScrollSpeed => { + self.set_mouse_scroll_multiplier(defaults.mouse_scroll_multiplier, cx) + } + L10nKey::SettingsSmoothScroll => self.set_smooth_scroll(defaults.smooth_scroll, cx), + L10nKey::SettingsMouseZoom => { + self.set_mouse_zoom_modifier(defaults.mouse_zoom_modifier, cx) + } + L10nKey::SettingsTrimTrailingSpaces => { + self.set_clipboard_trim(defaults.clipboard_trim_trailing_spaces, cx) + } + L10nKey::SettingsCopyOnSelect => self.set_copy_on_select(defaults.copy_on_select, cx), + L10nKey::SettingsSmartSelection => self.set_smart_select(defaults.smart_select, cx), + L10nKey::SettingsPromptEditor => self.set_prompt_editor(defaults.prompt_editor, cx), + L10nKey::SettingsTabCompletion => self.set_tab_completion(defaults.tab_completion, cx), + L10nKey::SettingsHistorySearch => self.set_history_search(defaults.history_search, cx), + L10nKey::SettingsStartupWindow => self.set_startup_mode(defaults.startup_mode, cx), + L10nKey::SettingsRememberWindowSize => { + self.set_remember_window_size(defaults.remember_window_size, cx) + } + L10nKey::SettingsCheckUpdatesOnLaunch => { + self.set_check_for_updates(defaults.check_for_updates, cx) + } + L10nKey::SettingsAutoDownload => { + self.set_auto_download_updates(defaults.auto_download_updates, cx) + } + L10nKey::SettingsUpdateChannel => self.set_update_channel(defaults.update_channel, cx), + L10nKey::DetectUrls => self.set_link_url(defaults.link_url, cx), + L10nKey::ForwardSshLoopbackLinks => { + self.set_ssh_loopback_forward(defaults.ssh_loopback_forward, cx) + } + L10nKey::SettingsVerifyHostKeys => { + self.set_verify_host_keys(defaults.verify_host_keys, cx) + } + L10nKey::WarnBeforeClosing => { + self.set_ssh_warn_on_close(defaults.ssh_warn_on_close, cx) + } + L10nKey::SettingsLanguage => self.set_gui_language( + Self::normalize_gui_language(&defaults.gui_language), + window, + cx, + ), + L10nKey::SettingsFontSize => self.reset_font_size(cx), + L10nKey::SettingsUiFontSize => self.reset_ui_font_size(cx), + L10nKey::SettingsLineHeight => self.reset_line_height(cx), + L10nKey::SettingsFontFamily => { + self.commit_font_family(defaults.font_family.clone(), cx) + } + L10nKey::SettingsBoldFont => self.commit_font_family_emphasis( + true, + crate::ui::settings::font_default_label().to_string(), + cx, + ), + L10nKey::SettingsItalicFont => self.commit_font_family_emphasis( + false, + crate::ui::settings::font_default_label().to_string(), + cx, + ), + L10nKey::SettingsUiFontFamily => self.commit_ui_font_family( + crate::ui::settings::ui_font_default_label().to_string(), + window, + cx, + ), + L10nKey::SettingsSyncWithSystem => { + self.set_theme_follow_system(defaults.theme_follow_system, window, cx) + } + L10nKey::SettingsLegiblePalette => { + self.set_theme_legible_palette(defaults.theme_legible_palette, window, cx) + } + L10nKey::SettingsOpacity => { + self.update_config(cx, |cfg| cfg.window_opacity = None); + apply_theme(Some(window), cx); + } + L10nKey::SettingsBlur => { + self.update_config(cx, |cfg| cfg.window_blur = None); + apply_theme(Some(window), cx); + } + #[cfg(target_os = "windows")] + L10nKey::SettingsBackdrop => { + self.set_window_backdrop(defaults.window_backdrop, window, cx) + } + L10nKey::SettingsFontLigatures => self.set_font_ligatures( + defaults + .font_features + .as_ref() + .is_some_and(|f| f.is_calt_enabled() == Some(true)), + cx, + ), + L10nKey::OpenFilesWith => { + self.update_config(cx, |cfg| cfg.link_file_open = defaults.link_file_open); + } + L10nKey::SettingsProgram => { + self.update_config(cx, |cfg| cfg.shell = defaults.shell.clone()) + } + L10nKey::SettingsArguments => self.update_config(cx, |cfg| { + if let Some(shell) = &mut cfg.shell { + shell.args.clear(); + } + }), + L10nKey::SettingsStartIn => self.update_config(cx, |cfg| { + cfg.working_directory.strategy = defaults.working_directory.strategy + }), + L10nKey::SettingsCustomPath => self.update_config(cx, |cfg| { + cfg.working_directory.path = defaults.working_directory.path.clone() + }), + L10nKey::SettingsAppHttpProxy => { + self.update_config(cx, |cfg| cfg.http_proxy = defaults.http_proxy.clone()) + } + L10nKey::SettingsOpenFilesCommand => self.update_config(cx, |cfg| { + cfg.link_file_command = defaults.link_file_command.clone() + }), + _ => return, + } + self.refresh_settings_controls(title, window, cx); + } + + fn refresh_settings_controls( + &mut self, + title: L10nKey, + window: &mut Window, + cx: &mut Context, + ) { + let mut subs = Vec::new(); + match title { + L10nKey::SettingsFontFamily + | L10nKey::SettingsBoldFont + | L10nKey::SettingsItalicFont + | L10nKey::SettingsUiFontFamily => { + let (font, bold, italic, ui) = self.build_font_selects(&mut subs, window, cx); + if let Some(s) = self.active_settings_mut() { + s.font_select = font; + s.font_bold_select = bold; + s.font_italic_select = italic; + s.ui_font_select = ui; + } + } + L10nKey::SettingsLanguage => { + let value = self.build_language_select(&mut subs, window, cx); + if let Some(s) = self.active_settings_mut() { + s.language_select = value; + } + } + L10nKey::SettingsProgram | L10nKey::SettingsArguments => { + let (program, args, _) = self.build_shell_inputs(&mut subs, window, cx); + if let Some(s) = self.active_settings_mut() { + s.shell_program_input = program; + s.shell_args_input = args; + } + } + L10nKey::SettingsCustomPath => { + let value = cx.global::().working_directory.path.clone(); + if let Some(s) = self.active_settings() { + s.wd_path_input + .clone() + .update(cx, |s, cx| s.set_value(value, window, cx)); + } + } + L10nKey::SettingsOpenFilesCommand => { + let value = self.build_link_file_command_input(&mut subs, window, cx); + if let Some(s) = self.active_settings_mut() { + s.link_file_command_input = value; + } + } + L10nKey::SettingsAppHttpProxy => { + let value = self.build_http_proxy_input(&mut subs, window, cx); + if let Some(s) = self.active_settings_mut() { + s.http_proxy_input = value; + } + } + L10nKey::SettingsScrollSpeed => { + let value = self.build_scroll_slider(&mut subs, window, cx); + if let Some(s) = self.active_settings_mut() { + s.scroll_slider = value; + } + } + L10nKey::SettingsOpacity | L10nKey::SettingsBlur | L10nKey::SettingsBackdrop => { + let value = self.build_window_opacity_slider(&mut subs, window, cx); + if let Some(s) = self.active_settings_mut() { + s.window_opacity_slider = value; + } + } + _ => {} + } + if let Some(s) = self.active_settings_mut() { + s._subs.extend(subs); + } + cx.notify(); + } + fn toggle_settings(&mut self, window: &mut Window, cx: &mut Context) { if self.settings.is_some() { self.close_settings_checked(window, cx); @@ -5561,6 +5919,16 @@ impl Tty7App { }), ); + let shortcut_search = cx + .new(|cx| InputState::new(window, cx).placeholder(t(L10nKey::SettingsNavKeybindings))); + subs.push( + cx.subscribe_in(&shortcut_search, window, |_, _, ev, _, cx| { + if matches!(ev, InputEvent::Change) { + cx.notify(); + } + }), + ); + let ssh_filter = cx.new(|cx| { InputState::new(window, cx).placeholder(t(crate::ui::i18n::L10nKey::FilterHosts)) }); @@ -5584,12 +5952,20 @@ impl Tty7App { ); let content_scroll = gpui::ScrollHandle::new(); + content_scroll.set_offset(self.last_settings_location.1); let search_anchor = gpui::ScrollAnchor::for_handle(content_scroll.clone()); self.settings = Some(SettingsState { focus_handle: focus_handle.clone(), - section: SettingsSection::Appearance, + section: self.last_settings_location.0, search: settings_search, + shortcut_search, + modified_only: false, + search_active: false, + search_return_offset: self.last_settings_location.1, + search_selection: 0, + search_rows: std::cell::RefCell::new(None), + focused_setting: None, content_scroll, ssh_master_scroll: gpui::ScrollHandle::new(), ssh_detail_scroll: gpui::ScrollHandle::new(), @@ -5611,6 +5987,10 @@ impl Tty7App { scroll_slider, window_opacity_slider, theme_editor: None, + theme_draft: None, + theme_draft_error: None, + save_error: None, + saved_config: cx.global::().clone(), theme_panel_open: false, theme_panel_slot: crate::ui::settings::ThemeSlot::Manual, theme_search, @@ -5860,7 +6240,7 @@ impl Tty7App { cfg.gui_language = code.to_string(); } set_locale(code); - cx.global::().save(); + self.persist_settings_config(cx); set_menus(cx); // Explorer reads its menu wording from the registry, so it is the one // surface a language change does not reach on its own. No-op unless @@ -6072,7 +6452,7 @@ impl Tty7App { return; } cfg.http_proxy = value; - cfg.save(); + self.persist_settings_config(cx); cx.notify(); } @@ -6096,7 +6476,7 @@ impl Tty7App { return; } cfg.link_file_command = command; - cfg.save(); + self.persist_settings_config(cx); cx.notify(); } @@ -6114,13 +6494,18 @@ impl Tty7App { .step(0.01) .default_value(eff) }); - subs.push( - cx.subscribe_in(&slider, window, |this, _s, ev: &SliderEvent, window, cx| { - if let SliderEvent::Change(v) = ev { - this.set_window_opacity(v.start(), window, cx); + subs.push(cx.subscribe_in( + &slider, + window, + |this, _s, ev: &SliderEvent, window, cx| match ev { + SliderEvent::Change(v) => { + cx.global_mut::().window_opacity = Some(v.start().clamp(0.2, 1.0)); + apply_theme(Some(window), cx); + cx.notify(); } - }), - ); + SliderEvent::Release(_) => this.persist_settings_config(cx), + }, + )); slider } @@ -6141,17 +6526,27 @@ impl Tty7App { subs.push(cx.subscribe_in( &scroll_slider, window, - |this, _s, ev: &SliderEvent, _w, cx| { - if let SliderEvent::Change(v) = ev { - this.set_mouse_scroll_multiplier(v.start(), cx); + |this, _s, ev: &SliderEvent, _w, cx| match ev { + SliderEvent::Change(v) => { + cx.global_mut::().mouse_scroll_multiplier = v.start().clamp(0.1, 10.0); + cx.notify(); } + SliderEvent::Release(_) => this.persist_settings_config(cx), }, )); scroll_slider } pub(crate) fn close_settings(&mut self, window: &mut Window, cx: &mut Context) { - if self.settings.take().is_some() { + if let Some(s) = self.settings.take() { + self.last_settings_location = ( + s.section, + if s.search_active { + s.search_return_offset + } else { + s.content_scroll.offset() + }, + ); self.focus_active(window, cx); cx.notify(); } @@ -6166,7 +6561,21 @@ impl Tty7App { if self.settings.is_none() { self.toggle_settings(window, cx); } - self.select_settings_section(section, cx); + self.navigate_settings(section, None, window, cx); + } + + /// Resolve a pending form before performing both parts of an external + /// navigation. Opening the page and loading its form must be one action. + pub(crate) fn open_ssh_profile_form( + &mut self, + profile: crate::core::ssh_profile::SshProfile, + window: &mut Window, + cx: &mut Context, + ) { + self.with_settings_edits_resolved(window, cx, move |this, window, cx| { + this.open_settings_section(SettingsSection::Ssh, window, cx); + this.ssh_form_load(&profile, window, cx); + }); } pub(crate) fn open_ssh_profile_in_settings( @@ -6175,16 +6584,21 @@ impl Tty7App { window: &mut Window, cx: &mut Context, ) { - self.open_settings_section(SettingsSection::Ssh, window, cx); - if let Some(profile) = cx - .global::() - .ssh_profiles - .iter() - .find(|p| p.id == id) - .cloned() - { - self.ssh_form_load(&profile, window, cx); - } + self.with_settings_edits_resolved(window, cx, move |this, window, cx| { + // Read after resolving edits, so saving and reopening the same + // profile cannot load a snapshot from before that save. + if let Some(profile) = cx + .global::() + .ssh_profiles + .iter() + .find(|p| p.id == id) + .cloned() + { + this.open_ssh_profile_form(profile, window, cx); + } else { + this.open_settings_section(SettingsSection::Ssh, window, cx); + } + }); } pub(crate) fn open_ssh_profile_new_from_target( @@ -6193,7 +6607,6 @@ impl Tty7App { window: &mut Window, cx: &mut Context, ) { - self.open_settings_section(SettingsSection::Ssh, window, cx); let mut profile = crate::core::ssh_profile::SshProfile::new(String::new()); if let Some(qc) = crate::core::ssh_profile::parse_quick_connect(&target) { profile.port = qc.port_or_default(); @@ -6205,7 +6618,7 @@ impl Tty7App { profile.name = profile.host.clone(); } } - self.ssh_form_load(&profile, window, cx); + self.open_ssh_profile_form(profile, window, cx); } fn commit_font_family(&mut self, family: String, cx: &mut Context) { @@ -6218,7 +6631,7 @@ impl Tty7App { } let cfg = cx.global_mut::(); cfg.font_family = family; - cfg.save(); + self.persist_settings_config(cx); cx.notify(); } @@ -6247,7 +6660,7 @@ impl Tty7App { } else { cfg.font_family_italic = family; } - cfg.save(); + self.persist_settings_config(cx); cx.notify(); } @@ -6258,7 +6671,7 @@ impl Tty7App { return; } cfg.ui_font_family = family; - cfg.save(); + self.persist_settings_config(cx); apply_theme(Some(window), cx); cx.refresh_windows(); cx.notify(); @@ -6434,7 +6847,7 @@ impl Tty7App { return; } cfg.shell = shell; - cfg.save(); + self.persist_settings_config(cx); } // Shell discovery runs off the UI thread and now includes the saved // configured shell, so refresh the menu without blocking Settings. @@ -6451,7 +6864,7 @@ impl Tty7App { return; } cfg.working_directory.strategy = strategy; - cfg.save(); + self.persist_settings_config(cx); cx.notify(); } @@ -6477,7 +6890,7 @@ impl Tty7App { return; } cfg.working_directory.path = path; - cfg.save(); + self.persist_settings_config(cx); cx.notify(); } @@ -6923,19 +7336,23 @@ impl Tty7App { } pub(crate) fn autoselect_settings_search(&mut self, cx: &mut Context) { - let Some(settings) = self.settings.as_ref() else { - return; - }; - let query = settings.search.read(cx).value().trim().to_lowercase(); - if query.is_empty() { - return; - } - if crate::ui::settings::section_match_count(settings.section, &query) > 0 { - return; - } - if let Some(best) = crate::ui::settings::best_matching_section(&query) { - self.select_settings_section(best, cx); + if let Some(s) = self.settings.as_mut() { + let active = !s.search.read(cx).value().trim().is_empty() || s.modified_only; + if active && !s.search_active { + s.search_return_offset = s.content_scroll.offset(); + } + if active { + s.content_scroll.set_offset(gpui::point(px(0.), px(0.))); + } else if s.search_active { + s.content_scroll.set_offset(s.search_return_offset); + } + s.search_active = active; + s.search_selection = 0; + if active { + s.focused_setting = None; + } } + cx.notify(); } pub(crate) fn start_recording_key( diff --git a/src/ui/i18n/en.rs b/src/ui/i18n/en.rs index 4884935d..88adb297 100644 --- a/src/ui/i18n/en.rs +++ b/src/ui/i18n/en.rs @@ -2,6 +2,24 @@ use super::L10nKey; pub fn translate_en(key: L10nKey) -> &'static str { match key { + L10nKey::SettingsSaveError => "Changes could not be saved: {error}", + L10nKey::SettingsRetrySave => "Retry saving", + + L10nKey::SettingsNavGeneral => "General", + L10nKey::SettingsEditShortcuts => "Edit shortcuts…", + L10nKey::SettingsModifiedOnly => "Modified only", + L10nKey::SettingsModified => "Modified", + L10nKey::SettingsResetValue => "Reset setting", + L10nKey::SettingsSearchResults => "Search results", + L10nKey::SettingsOpenSetting => "Open setting", + L10nKey::SettingsNoModified => "No modified settings match this filter.", + L10nKey::SettingsTerminalFontGroup => "Terminal text", + L10nKey::SettingsInterfaceFontGroup => "Interface text", + L10nKey::SettingsUnsavedTitle => "Save changes before leaving?", + L10nKey::SettingsUnsavedBody => "Save your changes, discard them, or continue editing.", + L10nKey::SettingsSaveChanges => "Save changes", + L10nKey::SettingsThemeDraft => "Theme changes are previewed until you save.", + L10nKey::SearchTabs => "Search tabs…", L10nKey::SearchFiles => "Search files…", L10nKey::SearchThemes => "Search themes…", @@ -71,11 +89,11 @@ pub fn translate_en(key: L10nKey) -> &'static str { L10nKey::Keep => "Keep", L10nKey::SettingsNavAppearance => "Appearance", L10nKey::SettingsNavTerminal => "Terminal", - L10nKey::SettingsNavInput => "Input", + L10nKey::SettingsNavInput => "Keyboard & Mouse", L10nKey::SettingsNavSsh => "SSH", - L10nKey::SettingsNavAgents => "Agents", + L10nKey::SettingsNavAgents => "Integrations", L10nKey::SettingsNavWindowTabs => "Window & Tabs", - L10nKey::SettingsNavKeybindings => "Keybindings", + L10nKey::SettingsNavKeybindings => "Keyboard shortcuts", L10nKey::SettingsNavAbout => "About", L10nKey::SettingsHeader => "SETTINGS", L10nKey::Reset => "Reset", @@ -88,20 +106,20 @@ pub fn translate_en(key: L10nKey) -> &'static str { "Pick a color theme. Each one sets its own light or dark look." } L10nKey::SettingsTypography => "Typography", - L10nKey::SettingsFontSize => "Font size", + L10nKey::SettingsFontSize => "Terminal font size", L10nKey::SettingsFontSizeDesc => "Terminal text size in pixels.", L10nKey::SettingsUiFontSize => "Interface font size", L10nKey::SettingsUiFontSizeDesc => { "Text size everywhere outside the terminal — tabs, panels and settings. \ Raise it on a display that is not Retina." } - L10nKey::SettingsUiFontFamily => "Interface font family", + L10nKey::SettingsUiFontFamily => "Interface font", L10nKey::SettingsUiFontFamilyDesc => { "Face used for tabs, sidebars, dialogs and settings; Default uses the system UI font." } L10nKey::SettingsLineHeight => "Line height", L10nKey::SettingsLineHeightDesc => "Row spacing as a multiple of the font size.", - L10nKey::SettingsFontFamily => "Font family", + L10nKey::SettingsFontFamily => "Terminal font", L10nKey::SettingsFontFamilyDesc => "Pick from fonts installed on your system.", L10nKey::SettingsBoldFont => "Bold font", L10nKey::SettingsBoldFontDesc => { @@ -387,18 +405,18 @@ pub fn translate_en(key: L10nKey) -> &'static str { L10nKey::SettingsShellIntro => { "The program each new terminal launches. Leave Program empty to use the platform default ({default})." } - L10nKey::SettingsProgram => "Program", + L10nKey::SettingsProgram => "Shell program", L10nKey::SettingsProgramDesc => { "Executable name on PATH or an absolute path (e.g. zsh, fish, pwsh)." } - L10nKey::SettingsArguments => "Arguments", + L10nKey::SettingsArguments => "Shell arguments", L10nKey::SettingsArgumentsDesc => { "Launch flags, split like a command line — quote anything containing spaces (e.g. -l, or -c \"echo hi\")." } L10nKey::SettingsArgumentsInvalid => { "The quotes do not balance — this value was not saved." } - L10nKey::SettingsStartIn => "Start in", + L10nKey::SettingsStartIn => "Starting directory", L10nKey::SettingsStartInDesc => { "What a fresh shell starts in: tty7's launch directory, your home folder, or a fixed path." } @@ -414,7 +432,7 @@ pub fn translate_en(key: L10nKey) -> &'static str { "Applies to shells with nothing to inherit, like a window's first tab. New tabs and splits still inherit the active pane's directory; open shells keep running." } L10nKey::SettingsScrolling => "Scrolling", - L10nKey::SettingsScrollback => "Scrollback", + L10nKey::SettingsScrollback => "Scrollback buffer", L10nKey::SettingsScrollbackDesc => "Lines of history kept per pane. Applies to new panes.", L10nKey::SettingsScrollSpeed => "Scroll speed", L10nKey::SettingsScrollSpeedDesc => "Multiplier applied to mouse-wheel scrolling.", @@ -476,11 +494,11 @@ pub fn translate_en(key: L10nKey) -> &'static str { L10nKey::SettingsBellModeVisual => "Visual", L10nKey::SettingsBellModeAudible => "Audible", L10nKey::SettingsBellModeBoth => "Both", - L10nKey::SettingsPrompt => "Prompt", + L10nKey::SettingsPrompt => "Prompt & command history", L10nKey::SettingsPromptIntro => { "tty7's own editor and menus at the shell prompt. Turn one off to hand that much back to the shell." } - L10nKey::SettingsPromptEditor => "Prompt editor", + L10nKey::SettingsPromptEditor => "tty7 prompt editor", L10nKey::SettingsPromptEditorDesc => { "tty7 edits the line you type at the shell prompt: selection, undo, and the menus below. Off hands the prompt back to the shell's own editor — ZLE, readline, fish." } @@ -491,7 +509,7 @@ pub fn translate_en(key: L10nKey) -> &'static str { L10nKey::SettingsTabCompletionDesc => { "Tab at the prompt opens tty7's completion menu. When off, Tab goes to the shell's own completion instead." } - L10nKey::SettingsHistorySearch => "History search", + L10nKey::SettingsHistorySearch => "Command history search", L10nKey::SettingsHistorySearchDesc => { "⌃R at the prompt opens tty7's fuzzy history menu. Off sends ⌃R to the shell — its own reverse-i-search, or whatever you bound there (fzf, percol)." } @@ -590,11 +608,11 @@ pub fn translate_en(key: L10nKey) -> &'static str { L10nKey::SettingsNotifyOnCommandFinishDesc => { "Desktop alert after a long foreground command completes." } - L10nKey::SettingsNotifyThreshold => "Notify threshold", + L10nKey::SettingsNotifyThreshold => "Minimum command duration", L10nKey::SettingsNotifyThresholdDesc => { - "How long a command must run to qualify as \"long\"." + "Notify only when a command runs for at least this long." } - L10nKey::SettingsWindow => "Window", + L10nKey::SettingsWindow => "Startup & restore", L10nKey::NotifyModeNever => "Never", L10nKey::NotifyModeUnfocused => "When unfocused", L10nKey::NotifyModeAlways => "Always", @@ -678,9 +696,9 @@ pub fn translate_en(key: L10nKey) -> &'static str { } L10nKey::SettingsUpdateChannelStable => "Stable", L10nKey::SettingsUpdateChannelNightly => "Nightly", - L10nKey::SettingsDaemonStale => "The background server is still running {build}.", + L10nKey::SettingsDaemonStale => "The background session service is still running {build}.", L10nKey::SettingsDaemonStaleDesc => { - "tty7 was updated in place: the app is new, your panes are still served by the old build. Restarting the server picks up the new one and ends everything running in your panes. No hurry — do it when they're idle." + "tty7 was updated in place: the app is new, your panes are still served by the old build. Restarting the session service picks up the new one and ends everything running in your panes. No hurry — do it when they're idle." } L10nKey::UpdateDialogTitle => "Update available", L10nKey::UpdateDialogDetail => { @@ -732,14 +750,14 @@ pub fn translate_en(key: L10nKey) -> &'static str { L10nKey::SettingsCheckUpdatesOnLaunch => "Check for updates on launch", L10nKey::SettingsCommandLine => "Command line", L10nKey::SettingsCommandLineDesc => { - "Put the bundled tty7 command on your PATH, so scripts and agents can drive tty7 from any terminal — inside a pane it works either way. Turn off to keep your own build unshadowed. Applies at next launch." + "Make the bundled tty7 command available to scripts and AI agents. Takes effect on the next launch; turning this off does not remove an existing installation." } L10nKey::SettingsInstallCliOnPath => "Install the tty7 command on PATH", - L10nKey::SettingsServer => "Server", + L10nKey::SettingsServer => "Background session service", L10nKey::SettingsServerDesc => { - "Restarts the background server that keeps your shells running. Every shell on this computer ends; your tabs and layout reopen with fresh ones." + "Keeps terminal sessions running in the background. Restarting ends all shell processes on this computer and reopens the layout with new shells." } - L10nKey::SettingsRestartServer => "Restart server…", + L10nKey::SettingsRestartServer => "Restart session service…", L10nKey::SettingsAppHttpProxy => "Proxy for updates", L10nKey::SettingsAppHttpProxyDesc => { "Used only for tty7's update checks and downloads, not for programs in your panes. Empty follows the system proxy." @@ -823,7 +841,7 @@ pub fn translate_en(key: L10nKey) -> &'static str { L10nKey::SettingsSearchKeybindingsKeywords => { "shortcut hotkey keyboard binding chord tmux preset rebind prefix" } - L10nKey::SettingsSearchKeybindingsTitle => "Keybindings", + L10nKey::SettingsSearchKeybindingsTitle => "Keyboard shortcuts", L10nKey::SettingsSearchLineHeightKeywords => "typography leading spacing", L10nKey::SettingsSearchUiFontFamilyKeywords => { "interface font family ui typeface typography chrome sidebar tab" @@ -1687,7 +1705,7 @@ pub fn translate_en(key: L10nKey) -> &'static str { L10nKey::Replace => "Replace", L10nKey::SftpErrorInvalidOctalMode => "invalid octal mode", L10nKey::SettingsDaemonStaleDescInPlace => { - "tty7 was updated in place: the app is new, your panes still run on the old build. The server can swap itself for the new one without stopping, so your shells carry straight over. Panes on tty7's built-in SSH client are the exception — those close and need reopening." + "tty7 was updated in place: the app is new, your panes still run on the old build. The session service can swap itself for the new one without stopping, so your shells carry straight over. Panes on tty7's built-in SSH client are the exception — those close and need reopening." } L10nKey::AppRestartServerBodyInPlace => { "The server swaps itself for this build in place: your shells keep running, and the window reconnects a moment later. Panes on tty7's built-in SSH client are the exception — those close and need reopening." @@ -1695,7 +1713,7 @@ pub fn translate_en(key: L10nKey) -> &'static str { L10nKey::PaneRestoredScreenBanner => { "restored screen — this shell is new, nothing above it is still running" } - L10nKey::SettingsPerPaneHistory => "Give each pane its own shell history", + L10nKey::SettingsPerPaneHistory => "Separate command history per pane", L10nKey::SettingsPerPaneHistoryDescription => { "Up walks through what you ran in this pane, not every pane interleaved. A new pane starts from your existing history and writes back what it adds when it closes. Applies to bash and zsh panes tty7 can set up; a shell started with your own arguments is left alone." } diff --git a/src/ui/i18n/ja.rs b/src/ui/i18n/ja.rs index d4e30d71..bf3347a6 100644 --- a/src/ui/i18n/ja.rs +++ b/src/ui/i18n/ja.rs @@ -2,6 +2,23 @@ use super::L10nKey; pub fn translate_ja(key: L10nKey) -> Option<&'static str> { Some(match key { + L10nKey::SettingsNavGeneral => "一般", + L10nKey::SettingsEditShortcuts => "ショートカットを編集…", + L10nKey::SettingsModifiedOnly => "変更済みのみ", + L10nKey::SettingsModified => "変更済み", + L10nKey::SettingsResetValue => "既定値に戻す", + L10nKey::SettingsSearchResults => "検索結果", + L10nKey::SettingsOpenSetting => "設定を開く", + L10nKey::SettingsNoModified => "この条件に一致する変更済みの設定はありません。", + L10nKey::SettingsTerminalFontGroup => "ターミナルの文字", + L10nKey::SettingsInterfaceFontGroup => "インターフェイスの文字", + L10nKey::SettingsUnsavedTitle => "移動する前に変更を保存しますか?", + L10nKey::SettingsUnsavedBody => "変更を保存、破棄、または編集を続けられます。", + L10nKey::SettingsSaveChanges => "変更を保存", + L10nKey::SettingsThemeDraft => "テーマの変更は保存するまでプレビューされます。", + L10nKey::SettingsSaveError => "変更を保存できませんでした:{error}", + L10nKey::SettingsRetrySave => "保存を再試行", + L10nKey::SearchTabs => "タブを検索…", L10nKey::SearchFiles => "ファイルを検索…", L10nKey::SearchThemes => "テーマを検索…", @@ -75,11 +92,11 @@ pub fn translate_ja(key: L10nKey) -> Option<&'static str> { L10nKey::Keep => "保持", L10nKey::SettingsNavAppearance => "外観", L10nKey::SettingsNavTerminal => "ターミナル", - L10nKey::SettingsNavInput => "入力", + L10nKey::SettingsNavInput => "キーボードとマウス", L10nKey::SettingsNavSsh => "SSH", - L10nKey::SettingsNavAgents => "エージェント", + L10nKey::SettingsNavAgents => "連携", L10nKey::SettingsNavWindowTabs => "ウィンドウとタブ", - L10nKey::SettingsNavKeybindings => "キーバインド", + L10nKey::SettingsNavKeybindings => "キーボードショートカット", L10nKey::SettingsNavAbout => "情報", L10nKey::SettingsHeader => "設定", L10nKey::Reset => "リセット", @@ -92,19 +109,19 @@ pub fn translate_ja(key: L10nKey) -> Option<&'static str> { "配色テーマを選びます。明るいテーマと暗いテーマがあります" } L10nKey::SettingsTypography => "タイポグラフィ", - L10nKey::SettingsFontSize => "フォントサイズ", + L10nKey::SettingsFontSize => "ターミナルの文字サイズ", L10nKey::SettingsFontSizeDesc => "ターミナルテキストのサイズ(ピクセル)", - L10nKey::SettingsUiFontSize => "インターフェースのフォントサイズ", + L10nKey::SettingsUiFontSize => "画面の文字サイズ", L10nKey::SettingsUiFontSizeDesc => { "ターミナル以外すべての文字サイズ(タブ・パネル・設定)。Retina でないディスプレイでは大きめに" } - L10nKey::SettingsUiFontFamily => "インターフェースのフォントファミリー", + L10nKey::SettingsUiFontFamily => "画面のフォント", L10nKey::SettingsUiFontFamilyDesc => { "タブ、サイドバー、ダイアログ、設定で使用するフォント。デフォルトではシステム UI フォントを使用します。" } L10nKey::SettingsLineHeight => "行の高さ", L10nKey::SettingsLineHeightDesc => "フォントサイズに対する行間の倍率", - L10nKey::SettingsFontFamily => "フォントファミリー", + L10nKey::SettingsFontFamily => "ターミナルのフォント", L10nKey::SettingsFontFamilyDesc => "システムにインストールされているフォントから選択", L10nKey::SettingsBoldFont => "太字フォント", L10nKey::SettingsBoldFontDesc => { @@ -394,18 +411,18 @@ pub fn translate_ja(key: L10nKey) -> Option<&'static str> { L10nKey::SettingsShellIntro => { "新しいターミナルで起動するプログラム。空欄ならプラットフォーム既定の {default} を使います" } - L10nKey::SettingsProgram => "プログラム", + L10nKey::SettingsProgram => "シェルプログラム", L10nKey::SettingsProgramDesc => { "PATH 上の実行可能ファイル名または絶対パス。例: zsh、fish、pwsh" } - L10nKey::SettingsArguments => "引数", + L10nKey::SettingsArguments => "シェル引数", L10nKey::SettingsArgumentsDesc => { "コマンドラインと同じ規則で分割される起動フラグ。空白を含むものはクォートしてください(例: -l、-c \"echo hi\")" } L10nKey::SettingsArgumentsInvalid => { "引用符が対応していないため、この値は保存されませんでした" } - L10nKey::SettingsStartIn => "初期作業ディレクトリ", + L10nKey::SettingsStartIn => "開始ディレクトリ", L10nKey::SettingsStartInDesc => { "新しいシェルの開始場所: tty7 の起動ディレクトリ、ホームフォルダ、または固定パス" } @@ -421,7 +438,7 @@ pub fn translate_ja(key: L10nKey) -> Option<&'static str> { "継承元のないシェル(ウィンドウの最初のタブなど)に適用されます。新しいタブと分割はアクティブなペインのディレクトリを引き継ぎ、開いているシェルは動き続けます" } L10nKey::SettingsScrolling => "スクロール", - L10nKey::SettingsScrollback => "スクロールバック", + L10nKey::SettingsScrollback => "スクロールバックバッファー", L10nKey::SettingsScrollbackDesc => { "各ペインに保存する履歴の行数。新しいペインに適用されます" } @@ -487,11 +504,11 @@ pub fn translate_ja(key: L10nKey) -> Option<&'static str> { L10nKey::SettingsBellModeVisual => "視覚的(画面点滅)", L10nKey::SettingsBellModeAudible => "音声(効果音)", L10nKey::SettingsBellModeBoth => "点滅 + 音声", - L10nKey::SettingsPrompt => "プロンプト", + L10nKey::SettingsPrompt => "プロンプトとコマンド履歴", L10nKey::SettingsPromptIntro => { "シェルプロンプトでの tty7 独自のエディターとメニュー。オフにするとその分がシェルに渡されます" } - L10nKey::SettingsPromptEditor => "プロンプトエディター", + L10nKey::SettingsPromptEditor => "tty7 のプロンプトエディター", L10nKey::SettingsPromptEditorDesc => { "シェルプロンプトで入力する行を tty7 が編集します — 選択、取り消し、下のメニュー。オフにするとシェル自身の行エディター(ZLE、readline、fish)に戻ります" } @@ -502,7 +519,7 @@ pub fn translate_ja(key: L10nKey) -> Option<&'static str> { L10nKey::SettingsTabCompletionDesc => { "プロンプトで Tab を押すと tty7 の補完メニューが開きます。オフの場合、Tab はシェル自身の補完に渡されます" } - L10nKey::SettingsHistorySearch => "履歴検索", + L10nKey::SettingsHistorySearch => "コマンド履歴検索", L10nKey::SettingsHistorySearchDesc => { "プロンプトで ⌃R を押すと tty7 のファジー履歴メニューが開きます。オフなら ⌃R はシェルへ — 逆方向検索や、そこでバインドしたもの(fzf、percol)" } @@ -599,9 +616,11 @@ pub fn translate_ja(key: L10nKey) -> Option<&'static str> { L10nKey::SettingsNotifyOnCommandFinishDesc => { "長時間のフォアグラウンドコマンドが完了したらデスクトップ通知を表示" } - L10nKey::SettingsNotifyThreshold => "通知閾値(秒)", - L10nKey::SettingsNotifyThresholdDesc => "「長時間」とみなすのに必要なコマンドの実行時間", - L10nKey::SettingsWindow => "ウィンドウ", + L10nKey::SettingsNotifyThreshold => "コマンド実行時間の下限", + L10nKey::SettingsNotifyThresholdDesc => { + "この時間以上実行されたコマンドの完了を通知します。" + } + L10nKey::SettingsWindow => "起動と復元", L10nKey::NotifyModeNever => "通知しない", L10nKey::NotifyModeUnfocused => "非フォーカス時のみ", L10nKey::NotifyModeAlways => "常に通知", @@ -687,9 +706,9 @@ pub fn translate_ja(key: L10nKey) -> Option<&'static str> { } L10nKey::SettingsUpdateChannelStable => "安定版", L10nKey::SettingsUpdateChannelNightly => "ナイトリー", - L10nKey::SettingsDaemonStale => "バックグラウンドサーバーは {build} のままです。", + L10nKey::SettingsDaemonStale => "バックグラウンドセッションサービスは {build} のままです。", L10nKey::SettingsDaemonStaleDesc => { - "tty7 はその場で更新されました。アプリは新しく、ペインはまだ以前のビルドのサーバーが処理しています。再起動すると新しいビルドに切り替わり、ペインで動いているプロセスはすべて終了します。急ぐ必要はなく、ペインが空いているときにどうぞ" + "tty7 はその場で更新されました。アプリは新しく、ペインはまだ以前のビルドのセッションサービスが処理しています。再起動すると新しいビルドに切り替わり、ペインで動いているプロセスはすべて終了します。急ぐ必要はなく、ペインが空いているときにどうぞ" } L10nKey::UpdateDialogTitle => "アップデートがあります", L10nKey::UpdateDialogDetail => { @@ -741,14 +760,14 @@ pub fn translate_ja(key: L10nKey) -> Option<&'static str> { L10nKey::SettingsCheckUpdatesOnLaunch => "起動時にアップデートを確認", L10nKey::SettingsCommandLine => "コマンドライン", L10nKey::SettingsCommandLineDesc => { - "同梱の tty7 コマンドを PATH に入れ、スクリプトやエージェントが任意のターミナルから tty7 を操作できるようにします(ペイン内ではどちらでも動きます)。自分でビルドした tty7 を優先したい場合はオフに。次回起動時に有効" + "付属の tty7 コマンドをスクリプトや AI エージェントから利用できます。次回起動時に反映されます。無効にしてもインストール済みのコマンドは削除されません。" } L10nKey::SettingsInstallCliOnPath => "`tty7` コマンドを PATH にインストール", - L10nKey::SettingsServer => "デーモンサーバー", + L10nKey::SettingsServer => "バックグラウンドセッションサービス", L10nKey::SettingsServerDesc => { - "シェルを動かし続けているバックグラウンドサーバーを再起動します。このコンピュータ上のすべてのシェルが終了し、タブとレイアウトは新しいシェルで開き直します" + "ターミナルセッションをバックグラウンドで維持します。再起動すると、このコンピューター上のすべてのシェルプロセスを終了し、新しいシェルでレイアウトを開き直します。" } - L10nKey::SettingsRestartServer => "サーバーを再起動…", + L10nKey::SettingsRestartServer => "セッションサービスを再起動…", L10nKey::SettingsAppHttpProxy => "アップデート用プロキシ", L10nKey::SettingsAppHttpProxyDesc => { "tty7 自身の更新チェックとダウンロードにのみ使用し、ペインで実行中のプログラムには影響しません。空欄ならシステムのプロキシに従います" @@ -860,7 +879,7 @@ pub fn translate_ja(key: L10nKey) -> Option<&'static str> { L10nKey::SettingsSearchKeybindingsKeywords => { "ショートカット ホットキー キーボード バインディング コード tmux プリセット 再バインド プレフィックス keybindings shortcut hotkey binding chord prefix" } - L10nKey::SettingsSearchKeybindingsTitle => "キーバインド", + L10nKey::SettingsSearchKeybindingsTitle => "キーボードショートカット", L10nKey::SettingsSearchLineHeightKeywords => { "タイポグラフィ リーディング 行間 line height typography leading spacing" } @@ -1760,7 +1779,7 @@ pub fn translate_ja(key: L10nKey) -> Option<&'static str> { L10nKey::Replace => "置き換える", L10nKey::SftpErrorInvalidOctalMode => "無効な 8 進数モードです", L10nKey::SettingsDaemonStaleDescInPlace => { - "tty7 はその場で更新されました。アプリは新しく、ペインはまだ前のビルドで動いています。サーバーは停止せずに新しいビルドへ置き換えられるので、シェルはそのまま引き継がれます。tty7 内蔵の SSH クライアントを使うペインだけは例外で、その接続は閉じられ、開き直しが必要です" + "tty7 はその場で更新されました。アプリは新しく、ペインはまだ前のビルドで動いています。セッションサービスは停止せずに新しいビルドへ置き換えられるので、シェルはそのまま引き継がれます。tty7 内蔵の SSH クライアントを使うペインだけは例外で、その接続は閉じられ、開き直しが必要です" } L10nKey::AppRestartServerBodyInPlace => { "サーバーは停止せずに自分自身をこのビルドへ置き換えます。シェルは動いたままで、ウィンドウはすぐに再接続します。tty7 内蔵の SSH クライアントを使うペインだけは例外で、その接続は閉じられ、開き直しが必要です" @@ -1768,7 +1787,7 @@ pub fn translate_ja(key: L10nKey) -> Option<&'static str> { L10nKey::PaneRestoredScreenBanner => { "復元された画面 — 以下は新しいシェルで、これより上のものは動いていません" } - L10nKey::SettingsPerPaneHistory => "ペインごとに独自のシェル履歴を持たせる", + L10nKey::SettingsPerPaneHistory => "ペインごとにコマンド履歴を分離", L10nKey::SettingsPerPaneHistoryDescription => { "上キーでたどるのは、全ペインが混ざったものではなくこのペインで実行したコマンドです。新しいペインは既存の履歴から始まり、追加分は閉じるときに書き戻されます。対象は tty7 が設定できる bash と zsh のペインで、独自の引数で起動したシェルはそのままです" } @@ -1854,7 +1873,7 @@ pub fn translate_ja(key: L10nKey) -> Option<&'static str> { L10nKey::AppMenuKeyboardShortcuts => "キーボードショートカット", L10nKey::AppMenuJoinDiscord => "Discord に参加", L10nKey::AppMenuReportIssue => "問題を報告…", - L10nKey::AppMenuRestartServer => "サーバーを再起動…", + L10nKey::AppMenuRestartServer => "セッションサービスを再起動…", L10nKey::WindowUntitled => "無題", L10nKey::TrayShowTty7 => "tty7 を表示", L10nKey::TrayNotifications => "通知", diff --git a/src/ui/i18n/mod.rs b/src/ui/i18n/mod.rs index 627f7c2c..09ad0ebc 100644 --- a/src/ui/i18n/mod.rs +++ b/src/ui/i18n/mod.rs @@ -62,13 +62,30 @@ macro_rules! l10n_keys { pub enum L10nKey { $($key),* } impl L10nKey { - #[cfg(test)] pub(crate) const ALL: &'static [L10nKey] = &[$(L10nKey::$key),*]; } }; } l10n_keys! { + SettingsSaveError, + SettingsRetrySave, + + SettingsNavGeneral, + SettingsEditShortcuts, + SettingsModifiedOnly, + SettingsModified, + SettingsResetValue, + SettingsSearchResults, + SettingsOpenSetting, + SettingsNoModified, + SettingsTerminalFontGroup, + SettingsInterfaceFontGroup, + SettingsUnsavedTitle, + SettingsUnsavedBody, + SettingsSaveChanges, + SettingsThemeDraft, + SearchTabs, SearchFiles, SearchThemes, @@ -1579,15 +1596,11 @@ mod tests { // and no locale renames either. L10nKey::PanelShell, L10nKey::PanelSsh, - // The zh copy calls the background process "server" throughout — - // this heading is that word on its own. - L10nKey::SettingsServer, // "Shell" and "Agent" are the words the Chinese- and // Japanese-speaking developer audience uses for these; a // translation here would be less clear, not more. L10nKey::SettingsShell, L10nKey::CmdGroupAgents, - L10nKey::SettingsNavAgents, L10nKey::SettingsAgentsIntro, ]; diff --git a/src/ui/i18n/zh.rs b/src/ui/i18n/zh.rs index 8b53c7a7..196eb290 100644 --- a/src/ui/i18n/zh.rs +++ b/src/ui/i18n/zh.rs @@ -2,6 +2,23 @@ use super::L10nKey; pub fn translate_zh(key: L10nKey) -> Option<&'static str> { Some(match key { + L10nKey::SettingsNavGeneral => "常规", + L10nKey::SettingsEditShortcuts => "编辑快捷键…", + L10nKey::SettingsModifiedOnly => "仅显示已修改", + L10nKey::SettingsModified => "已修改", + L10nKey::SettingsResetValue => "恢复默认值", + L10nKey::SettingsSearchResults => "搜索结果", + L10nKey::SettingsOpenSetting => "打开设置", + L10nKey::SettingsNoModified => "没有符合筛选条件的已修改设置。", + L10nKey::SettingsTerminalFontGroup => "终端文字", + L10nKey::SettingsInterfaceFontGroup => "界面文字", + L10nKey::SettingsUnsavedTitle => "离开前保存更改?", + L10nKey::SettingsUnsavedBody => "可以保存更改、放弃更改,或继续编辑。", + L10nKey::SettingsSaveChanges => "保存更改", + L10nKey::SettingsThemeDraft => "主题更改正在预览,保存后才会写入文件。", + L10nKey::SettingsSaveError => "无法保存更改:{error}", + L10nKey::SettingsRetrySave => "重新保存", + L10nKey::SearchTabs => "搜索标签页…", L10nKey::SearchFiles => "搜索文件…", L10nKey::SearchThemes => "搜索主题…", @@ -67,11 +84,11 @@ pub fn translate_zh(key: L10nKey) -> Option<&'static str> { L10nKey::Keep => "保留", L10nKey::SettingsNavAppearance => "外观", L10nKey::SettingsNavTerminal => "终端", - L10nKey::SettingsNavInput => "输入", + L10nKey::SettingsNavInput => "键盘与鼠标", L10nKey::SettingsNavSsh => "SSH", - L10nKey::SettingsNavAgents => "Agents", + L10nKey::SettingsNavAgents => "集成", L10nKey::SettingsNavWindowTabs => "窗口与标签页", - L10nKey::SettingsNavKeybindings => "按键绑定", + L10nKey::SettingsNavKeybindings => "快捷键", L10nKey::SettingsNavAbout => "关于", L10nKey::SettingsHeader => "设置", L10nKey::Reset => "重置", @@ -82,19 +99,19 @@ pub fn translate_zh(key: L10nKey) -> Option<&'static str> { L10nKey::SettingsThemeIntroTitle => "主题", L10nKey::SettingsThemeIntroDesc => "选择配色主题。每个主题都有各自的浅色或深色外观。", L10nKey::SettingsTypography => "字体排版", - L10nKey::SettingsFontSize => "字号", + L10nKey::SettingsFontSize => "终端字号", L10nKey::SettingsFontSizeDesc => "终端文字大小(像素)。", L10nKey::SettingsUiFontSize => "界面字号", L10nKey::SettingsUiFontSizeDesc => { "终端以外所有地方的文字大小——标签页、面板、设置。非 Retina 显示器上可以调大。" } - L10nKey::SettingsUiFontFamily => "界面字体族", + L10nKey::SettingsUiFontFamily => "界面字体", L10nKey::SettingsUiFontFamilyDesc => { "用于标签页、侧栏、弹窗和设置的字体;默认使用系统 UI 字体。" } L10nKey::SettingsLineHeight => "行高", L10nKey::SettingsLineHeightDesc => "行间距为字号的倍数。", - L10nKey::SettingsFontFamily => "字体族", + L10nKey::SettingsFontFamily => "终端字体", L10nKey::SettingsFontFamilyDesc => "从系统已安装的字体中选择。", L10nKey::SettingsBoldFont => "粗体字体", L10nKey::SettingsBoldFontDesc => "粗体文字使用的字体;默认由主字体合成。", @@ -348,14 +365,14 @@ pub fn translate_zh(key: L10nKey) -> Option<&'static str> { L10nKey::SettingsShellIntro => { "每个新终端启动的程序。将“程序”留空可使用平台默认值({default})。" } - L10nKey::SettingsProgram => "程序", + L10nKey::SettingsProgram => "Shell 程序", L10nKey::SettingsProgramDesc => "PATH 中的可执行文件名或绝对路径,例如 zsh、fish、pwsh。", - L10nKey::SettingsArguments => "参数", + L10nKey::SettingsArguments => "Shell 参数", L10nKey::SettingsArgumentsDesc => { "启动参数,按命令行规则切分——含空格的参数请用引号包住(例如 -l,或 -c \"echo hi\")。" } L10nKey::SettingsArgumentsInvalid => "引号不配对,该值未保存。", - L10nKey::SettingsStartIn => "起始目录", + L10nKey::SettingsStartIn => "启动目录", L10nKey::SettingsStartInDesc => "新 shell 的启动目录:tty7 的启动目录、主目录或固定路径。", L10nKey::SettingsCustomPath => "自定义路径", L10nKey::SettingsCustomPathDesc => "新 shell 启动的目录。", @@ -367,7 +384,7 @@ pub fn translate_zh(key: L10nKey) -> Option<&'static str> { "仅适用于没有目录可继承的 shell,例如窗口的第一个标签页。新标签页和分屏仍继承活动窗格的目录,已打开的 shell 继续运行。" } L10nKey::SettingsScrolling => "滚动", - L10nKey::SettingsScrollback => "回滚行数", + L10nKey::SettingsScrollback => "终端输出历史", L10nKey::SettingsScrollbackDesc => "每个窗格保留的历史行数。仅适用于新窗格。", L10nKey::SettingsScrollSpeed => "滚动速度", L10nKey::SettingsScrollSpeedDesc => "应用于鼠标滚轮滚动的倍率。", @@ -420,11 +437,11 @@ pub fn translate_zh(key: L10nKey) -> Option<&'static str> { L10nKey::SettingsBellModeVisual => "闪烁", L10nKey::SettingsBellModeAudible => "声音", L10nKey::SettingsBellModeBoth => "闪烁 + 声音", - L10nKey::SettingsPrompt => "提示符", + L10nKey::SettingsPrompt => "提示符与命令历史", L10nKey::SettingsPromptIntro => { "shell 提示符处的 tty7 自带编辑器与菜单。关闭某项即可把这部分交还给 shell。" } - L10nKey::SettingsPromptEditor => "提示符编辑器", + L10nKey::SettingsPromptEditor => "tty7 提示符编辑器", L10nKey::SettingsPromptEditorDesc => { "由 tty7 编辑你在 shell 提示符上敲的这一行:选择、撤销,以及下面这些菜单。关闭后交还给 shell 自己的行编辑器——ZLE、readline、fish。" } @@ -435,7 +452,7 @@ pub fn translate_zh(key: L10nKey) -> Option<&'static str> { L10nKey::SettingsTabCompletionDesc => { "在提示符按 Tab 打开 tty7 的补全菜单。关闭后 Tab 交由 shell 自身的补全处理。" } - L10nKey::SettingsHistorySearch => "历史搜索", + L10nKey::SettingsHistorySearch => "命令历史搜索", L10nKey::SettingsHistorySearchDesc => { "在提示符按 ⌃R 打开 tty7 的模糊历史菜单。关闭后 ⌃R 交给 shell——它自带的反向搜索,或你绑定的其它功能(fzf、percol)。" } @@ -524,9 +541,9 @@ pub fn translate_zh(key: L10nKey) -> Option<&'static str> { L10nKey::SettingsNotifications => "通知", L10nKey::SettingsNotifyOnCommandFinish => "命令完成时通知", L10nKey::SettingsNotifyOnCommandFinishDesc => "较长的前台命令完成后发出桌面提醒。", - L10nKey::SettingsNotifyThreshold => "通知阈值", - L10nKey::SettingsNotifyThresholdDesc => "命令需运行多久才能算作“较长”。", - L10nKey::SettingsWindow => "窗口", + L10nKey::SettingsNotifyThreshold => "最短命令运行时间", + L10nKey::SettingsNotifyThresholdDesc => "仅在命令运行达到此时长后发送完成通知。", + L10nKey::SettingsWindow => "启动与恢复", L10nKey::NotifyModeNever => "从不", L10nKey::NotifyModeUnfocused => "窗口未聚焦时", L10nKey::NotifyModeAlways => "总是", @@ -602,9 +619,9 @@ pub fn translate_zh(key: L10nKey) -> Option<&'static str> { } L10nKey::SettingsUpdateChannelStable => "稳定版", L10nKey::SettingsUpdateChannelNightly => "每夜构建", - L10nKey::SettingsDaemonStale => "后台 server 仍运行在 {build}。", + L10nKey::SettingsDaemonStale => "后台 后台会话服务 仍运行在 {build}。", L10nKey::SettingsDaemonStaleDesc => { - "tty7 是原地升级的:界面已是新版,pane 还由旧版 server 托管。重启 server 换成新版,代价是 pane 里正在跑的进程全部结束。不急,挑 pane 空闲时再重启。" + "tty7 是原地升级的:界面已是新版,pane 还由旧版 后台会话服务 托管。重启 后台会话服务 换成新版,代价是 pane 里正在跑的进程全部结束。不急,挑 pane 空闲时再重启。" } L10nKey::UpdateDialogTitle => "有可用更新", L10nKey::UpdateDialogDetail => { @@ -650,14 +667,14 @@ pub fn translate_zh(key: L10nKey) -> Option<&'static str> { L10nKey::SettingsCheckUpdatesOnLaunch => "启动时检查更新", L10nKey::SettingsCommandLine => "命令行", L10nKey::SettingsCommandLineDesc => { - "把自带的 tty7 命令加入 PATH,让脚本和 agent 能从任意终端驱动 tty7——在 tty7 窗格内两种情况都可用。自己构建的 tty7 不想被遮蔽就关掉。下次启动生效。" + "让脚本和 AI Agent 使用随应用提供的 tty7 命令。下次启动生效;关闭后不会移除已安装的命令。" } L10nKey::SettingsInstallCliOnPath => "将 `tty7` 命令安装到 PATH", - L10nKey::SettingsServer => "Server", + L10nKey::SettingsServer => "后台会话服务", L10nKey::SettingsServerDesc => { - "重启在后台维持 shell 运行的 server。这台计算机上所有 shell 都会结束;标签页和布局会以全新的 shell 重新打开。" + "在后台维持终端会话。重启会结束这台计算机上的所有 Shell 进程,并按原布局打开新的 Shell。" } - L10nKey::SettingsRestartServer => "重启 server…", + L10nKey::SettingsRestartServer => "重启后台会话服务…", L10nKey::SettingsAppHttpProxy => "更新代理", L10nKey::SettingsAppHttpProxyDesc => { "仅用于 tty7 自身的更新检查和下载,不影响面板中运行的程序。留空则跟随系统代理。" @@ -765,7 +782,7 @@ pub fn translate_zh(key: L10nKey) -> Option<&'static str> { L10nKey::SettingsSearchKeybindingsKeywords => { "按键绑定 快捷键 热键 键盘 绑定 前缀 tmux keybindings shortcut hotkey binding prefix" } - L10nKey::SettingsSearchKeybindingsTitle => "按键绑定", + L10nKey::SettingsSearchKeybindingsTitle => "快捷键", L10nKey::SettingsSearchLineHeightKeywords => { "行高 行间距 行距 typography line height spacing leading" } @@ -1599,7 +1616,7 @@ pub fn translate_zh(key: L10nKey) -> Option<&'static str> { L10nKey::Replace => "覆盖", L10nKey::SftpErrorInvalidOctalMode => "无效的八进制模式", L10nKey::SettingsDaemonStaleDescInPlace => { - "tty7 是原地更新的:应用是新的,面板还跑在旧版上。server 可以不停机就换成新版,shell 直接延续下来。用 tty7 内置 SSH 客户端的面板除外——那些连接会断开,需要重新打开。" + "tty7 是原地更新的:应用是新的,面板还跑在旧版上。后台会话服务 可以不停机就换成新版,shell 直接延续下来。用 tty7 内置 SSH 客户端的面板除外——那些连接会断开,需要重新打开。" } L10nKey::AppRestartServerBodyInPlace => { "后台 server 会原地把自己换成当前这个版本:shell 继续运行,窗口稍后自动连回去。用 tty7 内置 SSH 客户端的面板除外——那些连接会断开,需要重新打开。" @@ -1607,7 +1624,7 @@ pub fn translate_zh(key: L10nKey) -> Option<&'static str> { L10nKey::PaneRestoredScreenBanner => { "已恢复的画面 —— 下面是新的 shell,上面的内容都已不在运行" } - L10nKey::SettingsPerPaneHistory => "每个面板用自己的 shell 历史", + L10nKey::SettingsPerPaneHistory => "各窗格使用独立命令历史", L10nKey::SettingsPerPaneHistoryDescription => { "上方向键翻的是这个面板里跑过的命令,而不是所有面板混在一起。新面板从已有历史开始,关闭时把新增的写回去。只对 tty7 能接管的 bash 和 zsh 面板生效;用你自己参数启动的 shell 不受影响。" } @@ -1691,7 +1708,7 @@ pub fn translate_zh(key: L10nKey) -> Option<&'static str> { L10nKey::AppMenuKeyboardShortcuts => "键盘快捷键", L10nKey::AppMenuJoinDiscord => "加入 Discord", L10nKey::AppMenuReportIssue => "报告问题…", - L10nKey::AppMenuRestartServer => "重启 server…", + L10nKey::AppMenuRestartServer => "重启 后台会话服务…", L10nKey::WindowUntitled => "未命名", L10nKey::TrayShowTty7 => "显示 tty7", L10nKey::TrayNotifications => "通知", @@ -1728,7 +1745,7 @@ pub fn translate_zh(key: L10nKey) -> Option<&'static str> { L10nKey::AppMenuEnterFullscreen => "进入全屏", L10nKey::HomeTimeOverWeekAgo => "一周多前", L10nKey::Search => "搜索", - L10nKey::SettingsDaemonStaleRestart => "重启 server", + L10nKey::SettingsDaemonStaleRestart => "重启 后台会话服务", L10nKey::SettingsNoneLower => "无", L10nKey::SettingsSearchCommandLineToolTitle => "命令行工具", L10nKey::TabContextMarkUnread => "标记为未读", diff --git a/src/ui/settings.rs b/src/ui/settings.rs index a8c2c443..0d60d7ad 100644 --- a/src/ui/settings.rs +++ b/src/ui/settings.rs @@ -1,7 +1,7 @@ use gpui::{ - Animation, AnimationExt as _, AnyElement, App, Background, Context, Div, Entity, FontWeight, - Image, ImageFormat, KeyDownEvent, MouseButton, SharedString, Stateful, Subscription, Window, - div, img, prelude::*, px, relative, rgb, + Animation, AnimationExt as _, AnyElement, App, Background, Context, Div, Entity, + Focusable as _, FontWeight, Image, ImageFormat, KeyDownEvent, MouseButton, SharedString, + Stateful, Subscription, Window, div, img, prelude::*, px, relative, rgb, }; use gpui_component::InteractiveElementExt as _; use gpui_component::button::{Button, ButtonCustomVariant, ButtonVariants as _}; @@ -14,10 +14,10 @@ use gpui_component::select::{SearchableVec, Select, SelectEvent, SelectState}; use gpui_component::sidebar::{Sidebar, SidebarCollapsible, SidebarMenu, SidebarMenuItem}; use gpui_component::slider::{Slider, SliderState}; use gpui_component::{ - ActiveTheme as _, Disableable as _, Icon, IconName, IndexPath, Sizable as _, WindowExt as _, - h_flex, v_flex, + ActiveTheme as _, Disableable as _, Icon, IconName, IndexPath, Selectable as _, Sizable as _, + WindowExt as _, h_flex, v_flex, }; -use std::cell::Cell; +use std::cell::{Cell, RefCell}; use std::sync::Arc; use uuid::Uuid; @@ -299,11 +299,27 @@ fn group_thousands(n: usize) -> String { } fn settings_row_id(label: &str, _desc: &str) -> SharedString { - SharedString::from(format!("settings-row-{label}")) + let id = settings_search_entries() + .iter() + .find(|entry| t(entry.title) == label) + .map(|entry| format!("{:?}", entry.title)) + .or_else(|| { + L10nKey::ALL + .iter() + .find(|&&key| t(key) == label) + .map(|key| format!("{key:?}")) + }) + .unwrap_or_else(|| label.to_string()); + SharedString::from(format!("settings-row-{id}")) } fn settings_header_id(title: &str) -> SharedString { - SharedString::from(format!("settings-header-{title}")) + let id = L10nKey::ALL + .iter() + .find(|&&key| t(key) == title) + .map(|key| format!("{key:?}")) + .unwrap_or_else(|| title.to_string()); + SharedString::from(format!("settings-header-{id}")) } /// Whether the reset control has any effective override to clear on this @@ -317,9 +333,10 @@ fn window_overrides_active(config: &Config, backdrop_is_local: bool) -> bool { #[derive(Clone, Copy, PartialEq, Eq)] pub(crate) enum SettingsSection { + General, Appearance, Terminal, - Input, + KeyboardMouse, Ssh, Agents, WindowTabs, @@ -329,21 +346,56 @@ pub(crate) enum SettingsSection { impl SettingsSection { pub(crate) const ALL: [SettingsSection; 8] = [ + SettingsSection::General, SettingsSection::Appearance, SettingsSection::Terminal, - SettingsSection::Input, + SettingsSection::KeyboardMouse, + SettingsSection::WindowTabs, SettingsSection::Ssh, SettingsSection::Agents, - SettingsSection::WindowTabs, - SettingsSection::Keybindings, SettingsSection::About, ]; + pub(crate) fn navigation_section(self) -> Self { + match self { + Self::Keybindings => Self::KeyboardMouse, + other => other, + } + } + + fn title(self) -> L10nKey { + match self { + Self::General => L10nKey::SettingsNavGeneral, + Self::Appearance => L10nKey::SettingsNavAppearance, + Self::Terminal => L10nKey::SettingsNavTerminal, + Self::KeyboardMouse => L10nKey::SettingsNavInput, + Self::Ssh => L10nKey::SettingsNavSsh, + Self::Agents => L10nKey::SettingsNavAgents, + Self::WindowTabs => L10nKey::SettingsNavWindowTabs, + Self::Keybindings => L10nKey::SettingsNavKeybindings, + Self::About => L10nKey::SettingsNavAbout, + } + } + + fn icon(self) -> Icon { + Icon::new(match self { + Self::General => IconName::Settings2, + Self::Appearance => IconName::Palette, + Self::Terminal => IconName::SquareTerminal, + Self::KeyboardMouse | Self::Keybindings => IconName::CaseSensitive, + Self::Ssh => IconName::Globe, + Self::Agents => IconName::Bot, + Self::WindowTabs => IconName::WindowRestore, + Self::About => return Icon::empty().path("icons/circle-info.svg"), + }) + } + fn profile_label(self) -> &'static str { match self { + SettingsSection::General => "settings:general", SettingsSection::Appearance => "settings:appearance", SettingsSection::Terminal => "settings:terminal", - SettingsSection::Input => "settings:input", + SettingsSection::KeyboardMouse => "settings:keyboard-mouse", SettingsSection::Ssh => "settings:ssh", SettingsSection::Agents => "settings:agents", SettingsSection::WindowTabs => "settings:window-tabs", @@ -367,6 +419,42 @@ fn settings_search_entries() -> &'static [SearchEntry] { &[ SearchEntry { section: Appearance, + title: SettingsUiFontSize, + keywords: SettingsSearchFontSizeKeywords, + }, + SearchEntry { + section: Terminal, + title: SettingsPerPaneHistory, + keywords: SettingsSearchHistorySearchKeywords, + }, + SearchEntry { + section: KeyboardMouse, + title: SettingsMouseZoom, + keywords: SettingsSearchScrollSpeedKeywords, + }, + SearchEntry { + section: Terminal, + title: SettingsCustomPath, + keywords: SettingsSearchStartInKeywords, + }, + SearchEntry { + section: Terminal, + title: SettingsOpenFilesCommand, + keywords: SettingsSearchOpenFilesWithKeywords, + }, + #[cfg(target_os = "macos")] + SearchEntry { + section: General, + title: SettingsDefaultTerminal, + keywords: SettingsSearchAboutKeywords, + }, + SearchEntry { + section: About, + title: SettingsServer, + keywords: SettingsSearchAboutKeywords, + }, + SearchEntry { + section: General, title: SettingsLanguage, keywords: SettingsSearchLanguageKeywords, }, @@ -502,17 +590,17 @@ fn settings_search_entries() -> &'static [SearchEntry] { keywords: SettingsSearchSmoothScrollKeywords, }, SearchEntry { - section: Terminal, + section: KeyboardMouse, title: SettingsFocusFollowsMouse, keywords: SettingsSearchFocusFollowsMouseKeywords, }, SearchEntry { - section: Terminal, + section: KeyboardMouse, title: SettingsHideMouseWhileTyping, keywords: SettingsSearchHideMouseWhileTypingKeywords, }, SearchEntry { - section: Terminal, + section: KeyboardMouse, title: SettingsReportMouseToApps, keywords: SettingsSearchReportMouseToAppsKeywords, }, @@ -537,37 +625,38 @@ fn settings_search_entries() -> &'static [SearchEntry] { keywords: SettingsSearchOpenFilesWithKeywords, }, SearchEntry { - section: Input, + section: Terminal, title: SettingsPromptEditor, keywords: SettingsSearchPromptEditorKeywords, }, SearchEntry { - section: Input, + section: Terminal, title: SettingsTabCompletion, keywords: SettingsSearchTabCompletionKeywords, }, SearchEntry { - section: Input, + section: Terminal, title: SettingsHistorySearch, keywords: SettingsSearchHistorySearchKeywords, }, + #[cfg(target_os = "macos")] SearchEntry { - section: Input, + section: KeyboardMouse, title: SettingsOptionAsMeta, keywords: SettingsSearchOptionAsMetaKeywords, }, SearchEntry { - section: Input, + section: KeyboardMouse, title: SettingsSmartSelection, keywords: SettingsSearchSmartSelectionKeywords, }, SearchEntry { - section: Input, + section: KeyboardMouse, title: SettingsCopyOnSelect, keywords: SettingsSearchCopyOnSelectKeywords, }, SearchEntry { - section: Input, + section: KeyboardMouse, title: SettingsTrimTrailingSpaces, keywords: SettingsSearchTrimTrailingSpacesKeywords, }, @@ -657,22 +746,22 @@ fn settings_search_entries() -> &'static [SearchEntry] { keywords: SettingsSearchKimiCodeKeywords, }, SearchEntry { - section: WindowTabs, + section: General, title: SettingsStartupWindow, keywords: SettingsSearchStartupWindowKeywords, }, SearchEntry { - section: WindowTabs, + section: General, title: SettingsRememberWindowSize, keywords: SettingsSearchRememberWindowSizeKeywords, }, SearchEntry { - section: WindowTabs, + section: General, title: SettingsRestoreLastLayout, keywords: SettingsSearchRestoreLastLayoutKeywords, }, SearchEntry { - section: WindowTabs, + section: General, title: SettingsShowTrayIcon, keywords: SettingsSearchShowTrayIconKeywords, }, @@ -697,17 +786,17 @@ fn settings_search_entries() -> &'static [SearchEntry] { keywords: SettingsSearchDiffPreviewFromCountsKeywords, }, SearchEntry { - section: WindowTabs, + section: General, title: SettingsNotifyOnCommandFinish, keywords: SettingsSearchNotifyOnCommandFinishKeywords, }, SearchEntry { - section: WindowTabs, + section: General, title: SettingsNotifyThreshold, keywords: SettingsSearchNotifyThresholdKeywords, }, SearchEntry { - section: Keybindings, + section: KeyboardMouse, title: SettingsSearchKeybindingsTitle, keywords: SettingsSearchKeybindingsKeywords, }, @@ -744,9 +833,292 @@ fn settings_search_entries() -> &'static [SearchEntry] { ] } +impl SearchEntry { + fn config_key(&self) -> &'static str { + match self.title { + L10nKey::SettingsDimInactivePanes => "dim_inactive_panes", + L10nKey::SettingsCursorBlink => "cursor_blink", + L10nKey::SettingsCursorShape => "cursor_style", + L10nKey::SettingsScrollback => "scrollback_limit", + L10nKey::SettingsNewTabPosition => "new_tab_position", + L10nKey::SettingsTabBarPosition => "tab_bar_position", + L10nKey::SettingsSidebarGrouping => "sidebar_grouping", + L10nKey::SettingsDiffPreviewFromCounts => "sidebar_diff_preview", + L10nKey::SettingsNotifyOnCommandFinish => "notify_on_command_finish", + L10nKey::SettingsNotifyThreshold => "notify_threshold_secs", + L10nKey::SettingsTerminalBell => "bell", + L10nKey::SettingsRestoreLastLayout => "restore_session", + L10nKey::SettingsPerPaneHistory => "per_pane_history", + L10nKey::SettingsShowTrayIcon => "show_tray_icon", + L10nKey::SettingsOptionAsMeta => "macos_option_as_alt", + L10nKey::SettingsHideMouseWhileTyping => "mouse_hide_while_typing", + L10nKey::SettingsFocusFollowsMouse => "focus_follows_mouse", + L10nKey::SettingsReportMouseToApps => "mouse_reporting", + L10nKey::SettingsScrollSpeed => "mouse_scroll_multiplier", + L10nKey::SettingsSmoothScroll => "smooth_scroll", + L10nKey::SettingsMouseZoom => "mouse_zoom_modifier", + L10nKey::SettingsTrimTrailingSpaces => "clipboard_trim_trailing_spaces", + L10nKey::SettingsCopyOnSelect => "copy_on_select", + L10nKey::SettingsSmartSelection => "smart_select", + L10nKey::SettingsPromptEditor => "prompt_editor", + L10nKey::SettingsTabCompletion => "tab_completion", + L10nKey::SettingsHistorySearch => "history_search", + L10nKey::SettingsStartupWindow => "startup_mode", + L10nKey::SettingsRememberWindowSize => "remember_window_size", + L10nKey::SettingsCheckUpdatesOnLaunch => "check_for_updates", + L10nKey::SettingsAutoDownload => "auto_download_updates", + L10nKey::SettingsUpdateChannel => "update_channel", + L10nKey::DetectUrls => "link_url", + L10nKey::ForwardSshLoopbackLinks => "ssh_loopback_forward", + L10nKey::SettingsVerifyHostKeys => "verify_host_keys", + L10nKey::WarnBeforeClosing => "ssh_warn_on_close", + L10nKey::SettingsLanguage => "gui_language", + L10nKey::SettingsProgram => "shell.program", + L10nKey::SettingsArguments => "shell.args", + L10nKey::SettingsStartIn => "working_directory.strategy", + L10nKey::SettingsCustomPath => "working_directory.path", + L10nKey::SettingsFontSize => "font_size", + L10nKey::SettingsUiFontSize => "ui_font_size", + L10nKey::SettingsLineHeight => "line_height", + L10nKey::SettingsFontFamily => "font_family", + L10nKey::SettingsBoldFont => "font_family_bold", + L10nKey::SettingsItalicFont => "font_family_italic", + L10nKey::SettingsUiFontFamily => "ui_font_family", + L10nKey::SettingsFontLigatures => "font_features", + L10nKey::SettingsOpacity => "window_opacity", + L10nKey::SettingsBlur => "window_blur", + L10nKey::SettingsBackdrop => "window_backdrop", + L10nKey::SettingsSyncWithSystem => "theme_follow_system", + L10nKey::SettingsLegiblePalette => "theme_legible_palette", + L10nKey::SettingsThemeIntroTitle => "theme_preset", + L10nKey::SettingsAppHttpProxy => "http_proxy", + L10nKey::SettingsOpenFilesCommand => "link_file_command", + L10nKey::OpenFilesWith => "link_file_open", + L10nKey::SettingsInstallCliOnPath => "install_cli_on_path", + L10nKey::SettingsSearchKeybindingsTitle => "keybindings", + L10nKey::SettingsHosts => "ssh_profiles", + _ => "", + } + } + fn description(&self) -> &'static str { + match self.title { + L10nKey::SettingsUiFontSize => t(L10nKey::SettingsUiFontSizeDesc), + L10nKey::SettingsMouseZoom => t(L10nKey::SettingsMouseZoomDesc), + L10nKey::SettingsCustomPath => t(L10nKey::SettingsCustomPathDesc), + L10nKey::SettingsDefaultTerminal => t(L10nKey::SettingsDefaultTerminalDesc), + L10nKey::SettingsServer => t(L10nKey::SettingsServerDesc), + L10nKey::SettingsLanguage => t(L10nKey::SettingsLanguageDesc), + L10nKey::SettingsSyncWithSystem => t(L10nKey::SettingsSyncWithSystemDesc), + L10nKey::SettingsLegiblePalette => t(L10nKey::SettingsLegiblePaletteDesc), + L10nKey::SettingsOpacity => t(L10nKey::SettingsOpacityDesc), + L10nKey::SettingsBlur => t(L10nKey::SettingsBlurDesc), + L10nKey::SettingsBackdrop => t(L10nKey::SettingsBackdropDesc), + L10nKey::SettingsDimInactivePanes => t(L10nKey::SettingsDimInactivePanesDesc), + L10nKey::SettingsFontSize => t(L10nKey::SettingsFontSizeDesc), + L10nKey::SettingsUiFontFamily => t(L10nKey::SettingsUiFontFamilyDesc), + L10nKey::SettingsLineHeight => t(L10nKey::SettingsLineHeightDesc), + L10nKey::SettingsFontFamily => t(L10nKey::SettingsFontFamilyDesc), + L10nKey::SettingsBoldFont => t(L10nKey::SettingsBoldFontDesc), + L10nKey::SettingsItalicFont => t(L10nKey::SettingsItalicFontDesc), + L10nKey::SettingsFontLigatures => t(L10nKey::SettingsFontLigaturesDesc), + L10nKey::SettingsCursorShape => t(L10nKey::SettingsCursorShapeDesc), + L10nKey::SettingsCursorBlink => t(L10nKey::SettingsCursorBlinkDesc), + L10nKey::SettingsBackgroundImage => t(L10nKey::SettingsBackgroundImageDesc), + L10nKey::SettingsImageOpacity => t(L10nKey::SettingsImageOpacityDesc), + L10nKey::SettingsProgram => t(L10nKey::SettingsProgramDesc), + L10nKey::SettingsArguments => t(L10nKey::SettingsArgumentsDesc), + L10nKey::SettingsStartIn => t(L10nKey::SettingsStartInDesc), + L10nKey::SettingsScrollback => t(L10nKey::SettingsScrollbackDesc), + L10nKey::SettingsScrollSpeed => t(L10nKey::SettingsScrollSpeedDesc), + L10nKey::SettingsSmoothScroll => t(L10nKey::SettingsSmoothScrollDesc), + L10nKey::SettingsFocusFollowsMouse => t(L10nKey::SettingsFocusFollowsMouseDesc), + L10nKey::SettingsHideMouseWhileTyping => t(L10nKey::SettingsHideMouseWhileTypingDesc), + L10nKey::SettingsReportMouseToApps => t(L10nKey::SettingsReportMouseToAppsDesc), + L10nKey::SettingsTerminalBell => t(L10nKey::SettingsTerminalBellDesc), + L10nKey::SettingsPromptEditor => t(L10nKey::SettingsPromptEditorDesc), + L10nKey::SettingsTabCompletion => t(L10nKey::SettingsTabCompletionDesc), + L10nKey::SettingsHistorySearch => t(L10nKey::SettingsHistorySearchDesc), + L10nKey::SettingsOptionAsMeta => t(L10nKey::SettingsOptionAsMetaDesc), + L10nKey::SettingsSmartSelection => t(L10nKey::SettingsSmartSelectionDesc), + L10nKey::SettingsCopyOnSelect => t(L10nKey::SettingsCopyOnSelectDesc), + L10nKey::SettingsTrimTrailingSpaces => t(L10nKey::SettingsTrimTrailingSpacesDesc), + L10nKey::SettingsVerifyHostKeys => t(L10nKey::SettingsVerifyHostKeysDesc), + L10nKey::SettingsStartupWindow => t(L10nKey::SettingsStartupWindowDesc), + L10nKey::SettingsRememberWindowSize => t(L10nKey::SettingsRememberWindowSizeDesc), + L10nKey::SettingsRestoreLastLayout => t(L10nKey::SettingsRestoreLastLayoutDesc), + L10nKey::SettingsShowTrayIcon => t(L10nKey::SettingsShowTrayIconDesc), + L10nKey::SettingsNewTabPosition => t(L10nKey::SettingsNewTabPositionDesc), + L10nKey::SettingsTabBarPosition => t(L10nKey::SettingsTabBarPositionDesc), + L10nKey::SettingsSidebarGrouping => t(L10nKey::SettingsSidebarGroupingDesc), + L10nKey::SettingsDiffPreviewFromCounts => t(L10nKey::SettingsDiffPreviewFromCountsDesc), + L10nKey::SettingsNotifyOnCommandFinish => t(L10nKey::SettingsNotifyOnCommandFinishDesc), + L10nKey::SettingsNotifyThreshold => t(L10nKey::SettingsNotifyThresholdDesc), + L10nKey::SettingsAppHttpProxy => t(L10nKey::SettingsAppHttpProxyDesc), + L10nKey::SettingsUpdateChannel => t(L10nKey::SettingsUpdateChannelDesc), + L10nKey::SettingsAutoDownload => t(L10nKey::SettingsAutoDownloadDesc), + L10nKey::SettingsPerPaneHistory => t(L10nKey::SettingsPerPaneHistoryDescription), + L10nKey::DetectUrls => t(L10nKey::SettingsDetectUrlsDesc), + L10nKey::ForwardSshLoopbackLinks => t(L10nKey::SettingsForwardSshLoopbackLinksDesc), + L10nKey::OpenFilesWith => t(L10nKey::SettingsOpenFilesModeDesc), + _ => "", + } + } + fn rank(&self, query: &str) -> u8 { + let label = t(self.title).to_lowercase(); + let key = self.config_key(); + if label == query || key == query { + 0 + } else if label.starts_with(query) || (!key.is_empty() && key.starts_with(query)) { + 1 + } else if t(self.keywords) + .split_whitespace() + .any(|word| word.eq_ignore_ascii_case(query)) + { + 2 + } else { + 3 + } + } + fn modified(&self, cfg: &Config) -> bool { + let defaults = Config::default(); + match self.title { + L10nKey::SettingsDimInactivePanes => { + cfg.dim_inactive_panes != defaults.dim_inactive_panes + } + L10nKey::SettingsCursorBlink => cfg.cursor_blink != defaults.cursor_blink, + L10nKey::SettingsCursorShape => cfg.cursor_style != defaults.cursor_style, + L10nKey::SettingsScrollback => cfg.scrollback_limit != defaults.scrollback_limit, + L10nKey::SettingsNewTabPosition => cfg.new_tab_position != defaults.new_tab_position, + L10nKey::SettingsTabBarPosition => cfg.tab_bar_position != defaults.tab_bar_position, + L10nKey::SettingsSidebarGrouping => cfg.sidebar_grouping != defaults.sidebar_grouping, + L10nKey::SettingsDiffPreviewFromCounts => { + cfg.sidebar_diff_preview != defaults.sidebar_diff_preview + } + L10nKey::SettingsNotifyOnCommandFinish => { + cfg.notify_on_command_finish != defaults.notify_on_command_finish + } + L10nKey::SettingsNotifyThreshold => { + cfg.notify_threshold_secs != defaults.notify_threshold_secs + } + L10nKey::SettingsTerminalBell => cfg.bell != defaults.bell, + L10nKey::SettingsRestoreLastLayout => cfg.restore_session != defaults.restore_session, + L10nKey::SettingsPerPaneHistory => cfg.per_pane_history != defaults.per_pane_history, + L10nKey::SettingsShowTrayIcon => cfg.show_tray_icon != defaults.show_tray_icon, + L10nKey::SettingsOptionAsMeta => { + cfg.macos_option_as_alt != defaults.macos_option_as_alt + } + L10nKey::SettingsHideMouseWhileTyping => { + cfg.mouse_hide_while_typing != defaults.mouse_hide_while_typing + } + L10nKey::SettingsFocusFollowsMouse => { + cfg.focus_follows_mouse != defaults.focus_follows_mouse + } + L10nKey::SettingsReportMouseToApps => cfg.mouse_reporting != defaults.mouse_reporting, + L10nKey::SettingsScrollSpeed => { + cfg.mouse_scroll_multiplier != defaults.mouse_scroll_multiplier + } + L10nKey::SettingsSmoothScroll => cfg.smooth_scroll != defaults.smooth_scroll, + L10nKey::SettingsMouseZoom => cfg.mouse_zoom_modifier != defaults.mouse_zoom_modifier, + L10nKey::SettingsTrimTrailingSpaces => { + cfg.clipboard_trim_trailing_spaces != defaults.clipboard_trim_trailing_spaces + } + L10nKey::SettingsCopyOnSelect => cfg.copy_on_select != defaults.copy_on_select, + L10nKey::SettingsSmartSelection => cfg.smart_select != defaults.smart_select, + L10nKey::SettingsPromptEditor => cfg.prompt_editor != defaults.prompt_editor, + L10nKey::SettingsTabCompletion => cfg.tab_completion != defaults.tab_completion, + L10nKey::SettingsHistorySearch => cfg.history_search != defaults.history_search, + L10nKey::SettingsStartupWindow => cfg.startup_mode != defaults.startup_mode, + L10nKey::SettingsRememberWindowSize => { + cfg.remember_window_size != defaults.remember_window_size + } + L10nKey::SettingsCheckUpdatesOnLaunch => { + cfg.check_for_updates != defaults.check_for_updates + } + L10nKey::SettingsAutoDownload => { + cfg.auto_download_updates != defaults.auto_download_updates + } + L10nKey::SettingsUpdateChannel => cfg.update_channel != defaults.update_channel, + L10nKey::DetectUrls => cfg.link_url != defaults.link_url, + L10nKey::ForwardSshLoopbackLinks => { + cfg.ssh_loopback_forward != defaults.ssh_loopback_forward + } + L10nKey::SettingsVerifyHostKeys => cfg.verify_host_keys != defaults.verify_host_keys, + L10nKey::WarnBeforeClosing => cfg.ssh_warn_on_close != defaults.ssh_warn_on_close, + L10nKey::SettingsLanguage => cfg.gui_language != defaults.gui_language, + L10nKey::SettingsFontSize => cfg.font_size != defaults.font_size, + L10nKey::SettingsUiFontSize => cfg.ui_font_size != defaults.ui_font_size, + L10nKey::SettingsLineHeight => cfg.line_height != defaults.line_height, + L10nKey::SettingsFontFamily => cfg.font_family != defaults.font_family, + L10nKey::SettingsBoldFont => cfg.font_family_bold != defaults.font_family_bold, + L10nKey::SettingsItalicFont => cfg.font_family_italic != defaults.font_family_italic, + L10nKey::SettingsUiFontFamily => cfg.ui_font_family != defaults.ui_font_family, + L10nKey::SettingsOpacity => cfg.window_opacity != defaults.window_opacity, + L10nKey::SettingsBlur => cfg.window_blur != defaults.window_blur, + L10nKey::SettingsBackdrop => cfg.window_backdrop != defaults.window_backdrop, + L10nKey::SettingsSyncWithSystem => { + cfg.theme_follow_system != defaults.theme_follow_system + } + L10nKey::SettingsLegiblePalette => { + cfg.theme_legible_palette != defaults.theme_legible_palette + } + L10nKey::SettingsThemeIntroTitle => { + cfg.theme_preset != defaults.theme_preset + || cfg.theme_preset_light != defaults.theme_preset_light + || cfg.theme_preset_dark != defaults.theme_preset_dark + } + L10nKey::SettingsAppHttpProxy => cfg.http_proxy != defaults.http_proxy, + L10nKey::SettingsOpenFilesCommand => { + cfg.link_file_command != defaults.link_file_command + } + L10nKey::OpenFilesWith => cfg.link_file_open != defaults.link_file_open, + L10nKey::SettingsSearchKeybindingsTitle => { + cfg.keybindings != defaults.keybindings + || cfg.keybinding_preset != defaults.keybinding_preset + || cfg.prefix != defaults.prefix + } + L10nKey::SettingsProgram => { + cfg.shell.as_ref().map(|s| &s.program) + != defaults.shell.as_ref().map(|s| &s.program) + } + L10nKey::SettingsArguments => cfg.shell.as_ref().is_some_and(|s| !s.args.is_empty()), + L10nKey::SettingsStartIn => { + cfg.working_directory.strategy != defaults.working_directory.strategy + } + L10nKey::SettingsCustomPath => { + cfg.working_directory.path != defaults.working_directory.path + } + L10nKey::SettingsFontLigatures => cfg.font_features != defaults.font_features, + _ => false, + } + } +} + fn entry_matches(entry: &SearchEntry, query: &str) -> bool { - t(entry.title).to_lowercase().contains(query) - || t(entry.keywords).to_lowercase().contains(query) + let query = query.trim().to_lowercase(); + query.split_whitespace().all(|word| { + t(entry.title).to_lowercase().contains(word) + || match entry.title { + L10nKey::SettingsMouseZoom => { + "zoom modifier scroll wheel 缩放 滚轮 修饰键 ズーム".contains(word) + } + L10nKey::SettingsPerPaneHistory => { + "per pane shell history independent 独立 命令历史".contains(word) + } + L10nKey::SettingsServer => { + "daemon server background service 后台 服务".contains(word) + } + _ => false, + } + || t(entry.keywords).to_lowercase().contains(word) + || entry.description().to_lowercase().contains(word) + || entry.config_key().contains(word) + || crate::ui::i18n::alias_translations(entry.title) + .iter() + .any(|s| s.to_lowercase().contains(word)) + || crate::ui::i18n::alias_translations(entry.keywords) + .iter() + .any(|s| s.to_lowercase().contains(word)) + }) } /// Whether one keybinding row answers the query. @@ -784,7 +1156,9 @@ pub(crate) fn section_match_count(section: SettingsSection, query: &str) -> usiz .filter(|e| e.section == section && entry_matches(e, query)) .count(); match section { - SettingsSection::Keybindings => indexed + keybinding_match_count(query), + SettingsSection::KeyboardMouse | SettingsSection::Keybindings => { + indexed + keybinding_match_count(query) + } _ => indexed, } } @@ -812,13 +1186,14 @@ pub(crate) fn total_match_count(query: &str) -> usize { .sum() } +#[cfg(test)] pub(crate) fn best_matching_section(query: &str) -> Option { - SettingsSection::ALL - .into_iter() - .map(|s| (s, section_match_count(s, query))) - .filter(|(_, n)| *n > 0) - .reduce(|best, cur| if cur.1 > best.1 { cur } else { best }) - .map(|(s, _)| s) + settings_search_entries() + .iter() + .filter(|entry| entry_matches(entry, query)) + .min_by_key(|entry| entry.rank(query)) + .map(|entry| entry.section) + .or_else(|| (keybinding_match_count(query) > 0).then_some(SettingsSection::KeyboardMouse)) } pub(crate) struct ThemeEditor { @@ -834,6 +1209,15 @@ pub(crate) struct SettingsState { pub(crate) focus_handle: gpui::FocusHandle, pub(crate) section: SettingsSection, pub(crate) search: Entity, + pub(crate) shortcut_search: Entity, + pub(crate) modified_only: bool, + pub(crate) save_error: Option, + pub(crate) saved_config: Config, + pub(crate) search_active: bool, + pub(crate) search_return_offset: gpui::Point, + pub(crate) search_selection: usize, + pub(crate) search_rows: RefCell>>, + pub(crate) focused_setting: Option, /// The page's own scroll, and an anchor on it that the first matching row /// claims. Searching tells you "Appearance (2)"; these are what carry you /// to the two, which on a long page start well below the fold. @@ -860,6 +1244,8 @@ pub(crate) struct SettingsState { pub(crate) scroll_slider: Entity, pub(crate) window_opacity_slider: Entity, pub(crate) theme_editor: Option, + pub(crate) theme_draft: Option<(presets::Theme, presets::Theme)>, + pub(crate) theme_draft_error: Option, pub(crate) theme_panel_open: bool, pub(crate) theme_panel_slot: ThemeSlot, pub(crate) theme_search: Entity, @@ -1531,6 +1917,312 @@ fn seed_input( } impl Tty7App { + pub(crate) fn with_settings_edits_resolved( + &mut self, + window: &mut Window, + cx: &mut Context, + action: impl FnOnce(&mut Self, &mut Window, &mut Context) + 'static, + ) { + let save_failed = self + .active_settings() + .is_some_and(|s| s.save_error.is_some()); + if !save_failed && !self.ssh_form_dirty(cx) && !self.theme_draft_dirty() { + action(self, window, cx); + return; + } + let answer = window.prompt::( + gpui::PromptLevel::Warning, + t(L10nKey::SettingsUnsavedTitle), + Some(t(L10nKey::SettingsUnsavedBody)), + &[ + gpui::PromptButton::ok(t(L10nKey::SettingsSaveChanges)), + gpui::PromptButton::new(t(L10nKey::EditorDiscard)), + gpui::PromptButton::cancel(t(L10nKey::SettingsKeepEditing)), + ], + cx, + ); + cx.spawn_in(window, async move |this, cx| { + let Ok(choice) = answer.await else { + return; + }; + let _ = this.update_in(cx, |this, window, cx| { + match choice { + 0 => { + if this + .active_settings() + .is_some_and(|s| s.save_error.is_some()) + { + this.persist_settings_config(cx); + if this + .active_settings() + .is_some_and(|s| s.save_error.is_some()) + { + return; + } + } + if this.ssh_form_dirty(cx) && this.save_editing_profile(cx).is_none() { + return; + } + if !this.save_theme_draft(window, cx) { + return; + } + } + 1 => { + this.discard_unsaved_settings(window, cx); + this.cancel_theme_draft(window, cx); + if let Some(s) = this.active_settings_mut() { + s.ssh_form = None; + } + } + _ => return, + } + action(this, window, cx); + }); + }) + .detach(); + } + + pub(crate) fn navigate_settings( + &mut self, + target: SettingsSection, + setting: Option, + window: &mut Window, + cx: &mut Context, + ) { + if self.ssh_form_dirty(cx) + || self.theme_draft_dirty() + || self + .active_settings() + .is_some_and(|s| s.save_error.is_some()) + { + self.with_settings_edits_resolved(window, cx, move |this, window, cx| { + this.navigate_settings(target, setting, window, cx) + }); + return; + } + if let Some(state) = self.active_settings() { + state + .search + .clone() + .update(cx, |search, cx| search.set_value("", window, cx)); + } + let setting = match setting { + Some(L10nKey::SettingsCustomPath) + if cx.global::().working_directory.strategy + != crate::core::config::WdStrategy::Custom => + { + Some(L10nKey::SettingsStartIn) + } + Some(L10nKey::SettingsOpenFilesCommand) + if cx.global::().file_open_mode() != LinkFileOpen::Command => + { + Some(L10nKey::OpenFilesWith) + } + other => other, + }; + if let Some(state) = self.active_settings_mut() { + state.modified_only = false; + state.search_active = false; + state.focused_setting = setting; + state.reveal_first_hit.set(setting.is_some()); + state.content_scroll.set_offset(gpui::point(px(0.), px(0.))); + state.theme_panel_open = false; + if target == SettingsSection::Ssh + && matches!( + setting, + Some(L10nKey::SettingsVerifyHostKeys | L10nKey::WarnBeforeClosing) + ) + { + state.ssh_detail = SshDetail::Defaults; + } + } + self.select_settings_section(target, cx); + } + + fn render_settings_search(&self, cx: &mut Context) -> AnyElement { + let Some(state) = self.active_settings() else { + return div().into_any_element(); + }; + let query = state.search.read(cx).value().trim().to_lowercase(); + let modified_only = state.modified_only; + *state.search_rows.borrow_mut() = Some(Vec::new()); + // Build once, then retain the matching controls. Discarded page chrome + // never enters the element tree, so controls keep their usual IDs. + for section in SettingsSection::ALL { + if !settings_search_entries() + .iter() + .any(|entry| entry.section == section && entry_matches(entry, &query)) + { + continue; + } + match section { + SettingsSection::General => { + self.render_settings_general(cx); + } + SettingsSection::Appearance => { + self.render_settings_appearance(cx); + } + SettingsSection::Terminal => { + self.render_settings_terminal(cx); + } + SettingsSection::KeyboardMouse => { + self.render_settings_input(cx); + } + SettingsSection::WindowTabs => { + self.render_window_preferences(false, cx); + } + SettingsSection::About => { + self.render_settings_about(cx); + } + // These pages have their own host/action editors. + _ => {} + } + } + let mut controls = self + .active_settings() + .unwrap() + .search_rows + .borrow_mut() + .take() + .unwrap_or_default(); + let cfg = cx.global::(); + let mut matches = settings_search_entries() + .iter() + .filter(|entry| entry_matches(entry, &query) && (!modified_only || entry.modified(cfg))) + .collect::>(); + matches.sort_by_key(|entry| { + ( + entry.rank(&query), + SettingsSection::ALL + .iter() + .position(|&s| s == entry.section) + .unwrap_or(0), + ) + }); + let mut list = v_flex().gap_3(); + for (index, entry) in matches.iter().enumerate() { + let title = entry.title; + let section = if title == L10nKey::SettingsSearchKeybindingsTitle { + SettingsSection::Keybindings + } else { + entry.section + }; + let control = controls + .iter() + .position(|(key, _)| *key == title) + .map(|i| controls.remove(i).1); + let path = format!("{} › {}", t(entry.section.title()), t(title)); + let row = v_flex() + .id(SharedString::from(format!("search-result-{title:?}"))) + .px_3() + .py_2() + .rounded_lg() + .border_1() + .border_color(cx.theme().border) + .anchor_scroll( + self.active_settings() + .filter(|s| s.search_selection == index) + .map(|s| s.search_anchor.clone()), + ) + .when( + self.active_settings() + .is_some_and(|s| s.search_selection == index), + |v| v.border_color(cx.theme().primary), + ) + .child( + Button::new(SharedString::from(format!("search-path-{title:?}"))) + .label(path) + .ghost() + .small() + .on_click(cx.listener(move |this, _, window, cx| { + this.navigate_settings(section, Some(title), window, cx) + })), + ) + .child(control.unwrap_or_else(|| { + self.settings_row( + t(title), + entry.description(), + Button::new(SharedString::from(format!("search-open-{title:?}"))) + .label(t(L10nKey::SettingsOpenSetting)) + .small() + .on_click(cx.listener(move |this, _, window, cx| { + this.navigate_settings(section, Some(title), window, cx) + })) + .into_any_element(), + cx, + ) + .into_any_element() + })); + list = list.child(row); + } + if matches.is_empty() && (modified_only || keybinding_match_count(&query) == 0) { + list = list.child( + div() + .text_sm() + .text_color(cx.theme().muted_foreground) + .child(if modified_only { + t(L10nKey::SettingsNoModified).to_string() + } else { + t_fmt(L10nKey::SettingsNothingMatches, &[("query", &query)]) + }), + ); + } + // Action names remain searchable without exposing an entire shortcut + // editor in the results list. + if !query.is_empty() && !modified_only { + let mut index = matches.len(); + for (action, _) in crate::ui::keymap::default_bindings() { + if keybinding_matches_query(&action, &query) { + let (_, label) = crate::ui::keymap::action_entry(&action); + let anchor = self + .active_settings() + .filter(|s| s.search_selection == index) + .map(|s| s.search_anchor.clone()); + let button = Button::new(SharedString::from(format!("search-action-{action}"))) + .label(format!( + "{} › {}", + t(L10nKey::SettingsNavKeybindings), + label + )) + .ghost() + .small() + .selected( + self.active_settings() + .is_some_and(|s| s.search_selection == index), + ) + .on_click(cx.listener(move |this, _, window, cx| { + let action = action.clone(); + this.with_settings_edits_resolved( + window, + cx, + move |this, window, cx| { + this.navigate_settings( + SettingsSection::Keybindings, + None, + window, + cx, + ); + if let Some(s) = this.active_settings() { + s.shortcut_search + .clone() + .update(cx, |s, cx| s.set_value(action, window, cx)); + } + }, + ); + })); + list = list.child( + div() + .id(SharedString::from(format!("search-shortcut-{index}"))) + .anchor_scroll(anchor) + .child(button), + ); + index += 1; + } + } + } + list.into_any_element() + } + pub(crate) fn render_settings( &self, window: &mut Window, @@ -1561,14 +2253,21 @@ impl Tty7App { None => return div(), }; let query = search.read(cx).value().trim().to_lowercase(); - let show_theme_panel = theme_panel_open && section == SettingsSection::Appearance; + let searching = self.active_settings().is_some_and(|s| s.search_active); + let layout_section = if searching { + SettingsSection::General + } else { + section + }; + let show_theme_panel = + !searching && theme_panel_open && section == SettingsSection::Appearance; let viewport_w = window.viewport_size().width.as_f32(); let ui_scale = ui_scale(cx); self.settings_viewport_w.set(viewport_w); - let cols = settings_columns(section, show_theme_panel, viewport_w); + let cols = settings_columns(layout_section, show_theme_panel, viewport_w); self.settings_row_width.set(settings_row_width( - section, + layout_section, show_theme_panel, viewport_w, ui_scale, @@ -1584,13 +2283,23 @@ impl Tty7App { // saying so sits at the top of the page, and a reader who searched // from halfway down would otherwise be left with the untouched page // the note exists to explain. + if searching + && let Some(s) = self.active_settings() + && s.reveal_first_hit.replace(false) + { + s.search_anchor.scroll_to(window, cx); + } if let Some(s) = self.active_settings() + && !searching && s.reveal_first_hit.get() { s.reveal_first_hit.set(false); - let matched_here = section_match_count(section, &query) > 0; + let matched_here = + section_match_count(section, &query) > 0 || s.focused_setting.is_some(); let matched_nowhere = total_match_count(&query) == 0; - if !query.is_empty() && (matched_here || matched_nowhere) { + if s.focused_setting.is_some() + || (!query.is_empty() && (matched_here || matched_nowhere)) + { s.search_anchor.scroll_to(window, cx); } } @@ -1607,9 +2316,11 @@ impl Tty7App { }; let item = SidebarMenuItem::new(label) .icon(icon) - .active(section == target) - .on_click(move |_, _window, cx| { - view.update(cx, |this, cx| this.select_settings_section(target, cx)); + .active(section.navigation_section() == target) + .on_click(move |_, window, cx| { + view.update(cx, |this, cx| { + this.navigate_settings(target, None, window, cx) + }); }); if count > 0 { item.suffix(move |_w, _cx| { @@ -1623,47 +2334,11 @@ impl Tty7App { } }; - let nav_body = SidebarMenu::new() - .child(nav_item( - t(L10nKey::SettingsNavAppearance), - SettingsSection::Appearance, - Icon::new(IconName::Palette), - )) - .child(nav_item( - t(L10nKey::SettingsNavTerminal), - SettingsSection::Terminal, - Icon::new(IconName::SquareTerminal), - )) - .child(nav_item( - t(L10nKey::SettingsNavInput), - SettingsSection::Input, - Icon::new(IconName::Settings2), - )) - .child(nav_item( - t(L10nKey::SettingsNavSsh), - SettingsSection::Ssh, - Icon::new(IconName::Globe), - )) - .child(nav_item( - t(L10nKey::SettingsNavAgents), - SettingsSection::Agents, - Icon::new(IconName::Bot), - )) - .child(nav_item( - t(L10nKey::SettingsNavWindowTabs), - SettingsSection::WindowTabs, - Icon::new(IconName::WindowRestore), - )) - .child(nav_item( - t(L10nKey::SettingsNavKeybindings), - SettingsSection::Keybindings, - Icon::new(IconName::CaseSensitive), - )) - .child(nav_item( - t(L10nKey::SettingsNavAbout), - SettingsSection::About, - Icon::empty().path("icons/circle-info.svg"), - )); + let nav_body = SettingsSection::ALL + .into_iter() + .fold(SidebarMenu::new(), |menu, target| { + menu.child(nav_item(t(target.title()), target, target.icon())) + }); let sidebar = Sidebar::new("settings-sidebar") .collapsible(SidebarCollapsible::None) @@ -1700,38 +2375,57 @@ impl Tty7App { ), ), ) - .child(nav_body); + .child(nav_body) + .footer( + Button::new("settings-modified-filter") + .label(t(L10nKey::SettingsModifiedOnly)) + .ghost() + .small() + .selected(self.active_settings().is_some_and(|s| s.modified_only)) + .on_click(cx.listener(|this, _, _window, cx| { + if let Some(s) = this.active_settings_mut() { + s.modified_only = !s.modified_only; + } + this.autoselect_settings_search(cx); + })), + ); - let content = match section { - SettingsSection::Appearance => self.render_settings_appearance(cx), - SettingsSection::Terminal => self.render_settings_terminal(cx), - SettingsSection::Input => self.render_settings_input(cx), - SettingsSection::Ssh => self.render_settings_ssh(cx), - SettingsSection::Agents => self.render_settings_agents(cx), - SettingsSection::WindowTabs => self.render_settings_window_tabs(cx), - SettingsSection::Keybindings => self.render_settings_keybindings(cx), - SettingsSection::About => self.render_settings_about(cx), + let content = if searching { + self.render_settings_search(cx) + } else { + match section { + SettingsSection::General => self.render_settings_general(cx), + SettingsSection::Appearance => self.render_settings_appearance(cx), + SettingsSection::Terminal => self.render_settings_terminal(cx), + SettingsSection::KeyboardMouse => self.render_settings_input(cx), + SettingsSection::Ssh => self.render_settings_ssh(cx), + SettingsSection::Agents => self.render_settings_agents(cx), + SettingsSection::WindowTabs => self.render_window_preferences(false, cx), + SettingsSection::Keybindings => self.render_settings_keybindings(cx), + SettingsSection::About => self.render_settings_about(cx), + } }; // A query that matches nothing anywhere leaves the nav badge-less and // `autoselect_settings_search` with nowhere to go, so without this the // page just sits there looking like the search did nothing. - let no_match_note = (!query.is_empty() && total_match_count(&query) == 0).then(|| { - div() - .id("settings-no-match") - .anchor_scroll(self.active_settings().map(|s| s.search_anchor.clone())) - .mb_6() - .px_3() - .py_2() - .rounded_lg() - .bg(note_bg) - .text_sm() - .text_color(header_muted) - .child(t_fmt( - L10nKey::SettingsNothingMatches, - &[("query", query.as_str())], - )) - }); + let no_match_note = (!searching && !query.is_empty() && total_match_count(&query) == 0) + .then(|| { + div() + .id("settings-no-match") + .anchor_scroll(self.active_settings().map(|s| s.search_anchor.clone())) + .mb_6() + .px_3() + .py_2() + .rounded_lg() + .bg(note_bg) + .text_sm() + .text_color(header_muted) + .child(t_fmt( + L10nKey::SettingsNothingMatches, + &[("query", query.as_str())], + )) + }); // No fill of its own: the root already paints the opaque surface and // the background image behind it, and repainting here would hide the @@ -1742,12 +2436,36 @@ impl Tty7App { // honour does not push the nav back — it overflows, and overflow here // means content painted off the edge of the window, which is the other // half of the bug this file is fixing. - let content_pane = if section == SettingsSection::Ssh { + let content_pane = if !searching && section == SettingsSection::Ssh { v_flex() .id("settings-content") .flex_1() .min_w_0() .h_full() + .when_some( + self.active_settings().and_then(|s| s.save_error.clone()), + |v, error| { + v.child( + v_flex() + .p_3() + .gap_2() + .child( + div().text_sm().child(t_fmt( + L10nKey::SettingsSaveError, + &[("error", &error)], + )), + ) + .child( + Button::new("retry-ssh-settings-save") + .label(t(L10nKey::SettingsRetrySave)) + .small() + .on_click(cx.listener(|this, _, _window, cx| { + this.persist_settings_config(cx) + })), + ), + ) + }, + ) .child(content) .into_any_element() } else { @@ -1791,6 +2509,86 @@ impl Tty7App { .max_w(px(READING_COLUMN * ui_scale)) .mx_auto() .children(no_match_note) + .child( + div() + .mb_5() + .text_xl() + .font_weight(FontWeight::SEMIBOLD) + .child(t(if searching { + L10nKey::SettingsSearchResults + } else { + section.title() + })), + ) + .when(self.theme_draft_dirty(), |v| { + v.child( + v_flex() + .mb_4() + .gap_2() + .child( + div().text_sm().child(t(L10nKey::SettingsThemeDraft)), + ) + .when_some( + self.active_settings() + .and_then(|s| s.theme_draft_error.clone()), + |v, error| { + v.child(div().text_sm().child(t_fmt( + L10nKey::SettingsSaveError, + &[("error", &error)], + ))) + }, + ) + .child( + h_flex() + .gap_2() + .child( + Button::new("save-theme-draft") + .label(t(L10nKey::SettingsSaveChanges)) + .small() + .on_click(cx.listener( + |this, _, window, cx| { + this.save_theme_draft(window, cx); + }, + )), + ) + .child( + Button::new("cancel-theme-draft") + .label(t(L10nKey::Cancel)) + .ghost() + .small() + .on_click(cx.listener( + |this, _, window, cx| { + this.cancel_theme_draft(window, cx) + }, + )), + ), + ), + ) + }) + .when_some( + self.active_settings().and_then(|s| s.save_error.clone()), + |v, error| { + v.child( + v_flex() + .mb_4() + .gap_2() + .child(div().text_sm().child(t_fmt( + L10nKey::SettingsSaveError, + &[("error", &error)], + ))) + .child( + Button::new("retry-settings-save") + .label(t(L10nKey::SettingsRetrySave)) + .small() + .on_click(cx.listener( + |this, _, _window, cx| { + this.persist_settings_config(cx) + }, + )), + ), + ) + }, + ) .child(content), ), ); @@ -1826,6 +2624,87 @@ impl Tty7App { // layer is the picker: closing the whole page instead threw away a // panel the user had opened a moment ago, and left them to walk // back to Appearance to try again. + .capture_key_down(cx.listener(move |this, ev: &KeyDownEvent, window, cx| { + if searching + && this + .active_settings() + .is_some_and(|s| s.search.read(cx).focus_handle(cx).is_focused(window)) + { + let key = ev.keystroke.key.as_str(); + if matches!(key, "up" | "down" | "enter") { + let s = this.active_settings().unwrap(); + let query = s.search.read(cx).value().trim().to_lowercase(); + let mut entries = settings_search_entries() + .iter() + .filter(|e| { + entry_matches(e, &query) + && (!s.modified_only || e.modified(cx.global::())) + }) + .collect::>(); + entries.sort_by_key(|e| { + ( + e.rank(&query), + SettingsSection::ALL + .iter() + .position(|&s| s == e.section) + .unwrap_or(0), + ) + }); + let actions = if s.modified_only { + Vec::new() + } else { + crate::ui::keymap::default_bindings() + .into_iter() + .filter(|(action, _)| keybinding_matches_query(action, &query)) + .map(|(action, _)| action) + .collect::>() + }; + let count = entries.len() + actions.len(); + if count > 0 { + let index = s.search_selection.min(count - 1); + if key == "enter" && index >= entries.len() { + let action = actions[index - entries.len()].clone(); + this.with_settings_edits_resolved( + window, + cx, + move |this, window, cx| { + this.navigate_settings( + SettingsSection::Keybindings, + None, + window, + cx, + ); + if let Some(s) = this.active_settings() { + s.shortcut_search.clone().update(cx, |s, cx| { + s.set_value(action, window, cx) + }); + } + }, + ); + } else if key == "enter" { + let entry = entries[index]; + let target = + if entry.title == L10nKey::SettingsSearchKeybindingsTitle { + SettingsSection::Keybindings + } else { + entry.section + }; + this.navigate_settings(target, Some(entry.title), window, cx); + } else if let Some(s) = this.active_settings_mut() { + s.search_selection = if key == "down" { + (index + 1).min(count - 1) + } else { + index.saturating_sub(1) + }; + s.reveal_first_hit.set(true); + cx.notify(); + } + } + cx.stop_propagation(); + return; + } + } + })) .on_key_down(cx.listener(move |this, ev: &KeyDownEvent, window, cx| { if ev.keystroke.key.as_str() != "escape" { return; @@ -1834,6 +2713,19 @@ impl Tty7App { this.close_theme_panel(window, cx); return; } + if searching { + if let Some(s) = this.active_settings_mut() { + s.modified_only = false; + } + if let Some(s) = this.active_settings() { + s.search + .clone() + .update(cx, |s, cx| s.set_value("", window, cx)); + } + this.autoselect_settings_search(cx); + cx.stop_propagation(); + return; + } this.close_settings_checked(window, cx); })) .children(background_layers) @@ -1953,6 +2845,15 @@ impl Tty7App { /// with the one answer left above the fold. fn first_hit_anchor(&self, label: &str, cx: &Context) -> Option { let s = self.active_settings()?; + if s.search_active || s.search_rows.borrow().is_some() { + return None; + } + if let Some(target) = s.focused_setting { + if t(target) == label && !self.settings_hit_anchored.replace(true) { + return Some(s.search_anchor.clone()); + } + return None; + } let query = s.search.read(cx).value().trim().to_lowercase(); if query.is_empty() || section_match_count(s.section, &query) == 0 { return None; @@ -2024,6 +2925,13 @@ impl Tty7App { let theme = cx.theme(); let label = label.into(); let desc = desc.into(); + let entry = settings_search_entries() + .iter() + .find(|entry| t(entry.title) == label); + let modified = entry.is_some_and(|entry| entry.modified(cx.global::())); + let capture = self + .active_settings() + .is_some_and(|s| s.search_rows.borrow().is_some()); // Descriptions can contain live status (for example an agent hook target), so they // must not participate in the identity that preserves GPUI's hover state. let element_id = settings_row_id(&label, &desc); @@ -2031,7 +2939,10 @@ impl Tty7App { // findable once you are on the page. Only mark rows when the section // actually holds a match — otherwise a query that landed elsewhere // would grey out a page the user is simply reading. - let (hit, miss) = match self.active_settings() { + let (hit, miss) = match self + .active_settings() + .filter(|s| !s.search_active && !capture) + { Some(s) => { let query = s.search.read(cx).value().trim().to_lowercase(); match query.is_empty() || section_match_count(s.section, &query) == 0 { @@ -2071,7 +2982,38 @@ impl Tty7App { .child(desc), ) }); - div() + let labels = labels.when(modified, |v| { + v.child( + h_flex() + .gap_2() + .items_center() + .child( + div() + .text_xs() + .text_color(theme.muted_foreground) + .child(t(L10nKey::SettingsModified)), + ) + .when_some( + entry.filter(|e| { + e.title != L10nKey::SettingsSearchKeybindingsTitle + && e.title != L10nKey::SettingsThemeIntroTitle + }), + |v, entry| { + let key = entry.title; + v.child( + Button::new(SharedString::from(format!("reset-setting-{key:?}"))) + .label(t(L10nKey::SettingsResetValue)) + .ghost() + .small() + .on_click(cx.listener(move |this, _, window, cx| { + this.reset_settings_value(key, window, cx) + })), + ) + }, + ), + ) + }); + let row = div() .id(element_id) .flex() .when(stacked, |row| row.flex_col().items_start().gap_2()) @@ -2098,7 +3040,20 @@ impl Tty7App { .when(stacked, |c| c.w_full()) .when(!stacked, |c| c.flex_shrink_0()) .child(control), - ) + ); + if capture { + if let Some(entry) = entry { + self.active_settings() + .unwrap() + .search_rows + .borrow_mut() + .as_mut() + .unwrap() + .push((entry.title, row.into_any_element())); + return div().id("captured-setting"); + } + } + row } pub(crate) fn segmented( @@ -2222,6 +3177,63 @@ impl Tty7App { .into_any_element() } + fn render_settings_general(&self, cx: &mut Context) -> AnyElement { + let Some(state) = self.active_settings() else { + return div().into_any_element(); + }; + let language_select = state.language_select.clone(); + let foreground = cx.theme().foreground; + let muted_fg = cx.theme().muted_foreground; + let control_h = px(24.); + let language_control = Select::new(&language_select) + .small() + .w(px(FIELD_W)) + .h(control_h) + .menu_max_h(px(224.)) + .into_any_element(); + + v_flex() + .child(self.settings_row(t(L10nKey::SettingsLanguage), t(L10nKey::SettingsLanguageDesc), language_control, cx)) + .child(self.section_rule(cx)) + .child(self.render_window_preferences(true, cx)) + .when(cfg!(target_os = "macos"), |this| { + this.child(self.section_rule(cx)).child( + v_flex() + .gap_2() + .child( + div() + .text_sm() + .font_weight(FontWeight::SEMIBOLD) + .text_color(foreground) + .child(t(L10nKey::SettingsDefaultTerminal)), + ) + .child( + div() + .text_xs() + .text_color(muted_fg) + .child(t(L10nKey::SettingsDefaultTerminalDesc)), + ) + .child( + Button::new("set-default-terminal") + .label(t(L10nKey::SettingsDefaultTerminalSet)) + .small() + .on_click(cx.listener(|_, _, window, cx| { + let message = match crate::core::default_terminal::set_as_default_terminal() { + Ok(()) => t(L10nKey::SettingsDefaultTerminalSetSuccess).to_string(), + Err(error) => t_fmt( + L10nKey::SettingsDefaultTerminalSetFailed, + &[("error", &error)], + ), + }; + window.push_notification(message, cx); + })), + ), + ) + }) + + .into_any_element() + } + fn render_settings_appearance(&self, cx: &mut Context) -> AnyElement { let theme = cx.theme(); let foreground = theme.foreground; @@ -2229,14 +3241,13 @@ impl Tty7App { let hover_bg = gpui::rgb(cx.global::().window.hover); let stepper_bg = theme.secondary.opacity(0.35); let font_size = self.font_size; - let (font_select, font_bold_select, font_italic_select, ui_font_select, language_select) = + let (font_select, font_bold_select, font_italic_select, ui_font_select) = match self.active_settings() { Some(s) => ( s.font_select.clone(), s.font_bold_select.clone(), s.font_italic_select.clone(), s.ui_font_select.clone(), - s.language_select.clone(), ), None => return div().into_any_element(), }; @@ -2269,37 +3280,35 @@ impl Tty7App { .child(glyph) }; let control_h = px(24.); - let stepper_row = - move |dec: Stateful
, value: String, inc: Stateful
, reset: Button| { - h_flex() - .items_center() - .gap_3() - .child(reset) - .child( - h_flex() - .items_center() - .h(control_h) - .rounded(rounding::TRACK_RADIUS) - .bg(stepper_bg) - .border_1() - .border_color(border) - .overflow_hidden() - .child(dec) - .child( - div() - .min_w(px(40.)) - .border_l_1() - .border_color(border) - .py_1() - .text_center() - .text_sm() - .text_color(foreground) - .child(value), - ) - .child(inc), - ) - .into_any_element() - }; + let stepper_row = move |dec: Stateful
, value: String, inc: Stateful
| { + h_flex() + .items_center() + .gap_3() + .child( + h_flex() + .items_center() + .h(control_h) + .rounded(rounding::TRACK_RADIUS) + .bg(stepper_bg) + .border_1() + .border_color(border) + .overflow_hidden() + .child(dec) + .child( + div() + .min_w(px(40.)) + .border_l_1() + .border_color(border) + .py_1() + .text_center() + .text_sm() + .text_color(foreground) + .child(value), + ) + .child(inc), + ) + .into_any_element() + }; let font_size_control = stepper_row( step("font-dec", "−", 0).on_click( cx.listener(|this, _, _w, cx| this.change_font_size(-FONT_SIZE_STEP, cx)), @@ -2307,11 +3316,6 @@ impl Tty7App { format!("{:.0}", font_size), step("font-inc", "+", 2) .on_click(cx.listener(|this, _, _w, cx| this.change_font_size(FONT_SIZE_STEP, cx))), - Button::new("font-reset") - .label(t(L10nKey::Reset)) - .ghost() - .small() - .on_click(cx.listener(|this, _, _w, cx| this.reset_font_size(cx))), ); let ui_font_size = self.ui_font_size(cx); @@ -2323,11 +3327,6 @@ impl Tty7App { step("ui-font-inc", "+", 2).on_click( cx.listener(|this, _, _w, cx| this.change_ui_font_size(UI_FONT_SIZE_STEP, cx)), ), - Button::new("ui-font-reset") - .label(t(L10nKey::Reset)) - .ghost() - .small() - .on_click(cx.listener(|this, _, _w, cx| this.reset_ui_font_size(cx))), ); let line_height = self.line_height; @@ -2339,11 +3338,6 @@ impl Tty7App { step("lh-inc", "+", 2).on_click( cx.listener(|this, _, _w, cx| this.change_line_height(LINE_HEIGHT_STEP, cx)), ), - Button::new("lh-reset") - .label(t(L10nKey::Reset)) - .ghost() - .small() - .on_click(cx.listener(|this, _, _w, cx| this.reset_line_height(cx))), ); let font_dropdown = |state: &Entity>>| { @@ -2363,12 +3357,6 @@ impl Tty7App { .checked(font_ligatures) .on_click(cx.listener(|this, on: &bool, _w, cx| this.set_font_ligatures(*on, cx))) .into_any_element(); - let language_control = Select::new(&language_select) - .small() - .w(px(FIELD_W)) - .h(control_h) - .menu_max_h(px(224.)) - .into_any_element(); let cursor_idx = match cursor_style { CursorStyle::Block => 0, @@ -2409,19 +3397,11 @@ impl Tty7App { .child(self.section_rule(cx)) .child(self.render_window_section(cx)) .child(self.section_rule(cx)) - .child(self.section_header(t(L10nKey::SettingsLanguage), cx)) + .child(self.section_header(t(L10nKey::SettingsInterfaceFontGroup), cx)) .child(self.settings_row( - t(L10nKey::SettingsLanguage), - t(L10nKey::SettingsLanguageDesc), - language_control, - cx, - )) - .child(self.section_rule(cx)) - .child(self.section_header(t(L10nKey::SettingsTypography), cx)) - .child(self.settings_row( - t(L10nKey::SettingsFontSize), - t(L10nKey::SettingsFontSizeDesc), - font_size_control, + t(L10nKey::SettingsUiFontFamily), + t(L10nKey::SettingsUiFontFamilyDesc), + ui_font_family_control, cx, )) .child(self.settings_row( @@ -2430,10 +3410,18 @@ impl Tty7App { ui_font_size_control, cx, )) + .child(self.section_rule(cx)) + .child(self.section_header(t(L10nKey::SettingsTerminalFontGroup), cx)) .child(self.settings_row( - t(L10nKey::SettingsUiFontFamily), - t(L10nKey::SettingsUiFontFamilyDesc), - ui_font_family_control, + t(L10nKey::SettingsFontFamily), + t(L10nKey::SettingsFontFamilyDesc), + font_family_control, + cx, + )) + .child(self.settings_row( + t(L10nKey::SettingsFontSize), + t(L10nKey::SettingsFontSizeDesc), + font_size_control, cx, )) .child(self.settings_row( @@ -2442,12 +3430,6 @@ impl Tty7App { line_height_control, cx, )) - .child(self.settings_row( - t(L10nKey::SettingsFontFamily), - t(L10nKey::SettingsFontFamilyDesc), - font_family_control, - cx, - )) .child(self.settings_row( t(L10nKey::SettingsBoldFont), t(L10nKey::SettingsBoldFontDesc), @@ -2915,7 +3897,7 @@ impl Tty7App { detail == SshDetail::Defaults, None, sf, - cx.listener(|this, _, _w, cx| this.select_ssh_defaults(cx)), + cx.listener(|this, _, window, cx| this.select_ssh_defaults(window, cx)), None, cx, )); @@ -3081,6 +4063,9 @@ impl Tty7App { Some(live), sf, cx.listener(move |this, _, window, cx| { + if selected { + return; + } if let Some(profile) = cx .global::() .ssh_profiles @@ -3224,7 +4209,13 @@ impl Tty7App { live } - pub(crate) fn select_ssh_defaults(&mut self, cx: &mut Context) { + pub(crate) fn select_ssh_defaults(&mut self, window: &mut Window, cx: &mut Context) { + if self.ssh_form_dirty(cx) { + self.with_settings_edits_resolved(window, cx, |this, window, cx| { + this.select_ssh_defaults(window, cx) + }); + return; + } if let Some(s) = self.active_settings_mut() { s.ssh_form = None; s.ssh_detail = SshDetail::Defaults; @@ -3233,26 +4224,12 @@ impl Tty7App { } fn toggle_ssh_group(&mut self, key: String, cx: &mut Context) { - let selected_here = match self.active_settings().map(|s| s.ssh_detail) { - Some(SshDetail::Profile(id)) => cx - .global::() - .ssh_profiles - .iter() - .find(|p| p.id == id) - .is_some_and(|p| ssh_group_key(p) == key), - _ => false, - }; - let Some(s) = self.active_settings_mut() else { - return; - }; - let collapsing = !s.ssh_collapsed_groups.remove(&key); - if collapsing { - s.ssh_collapsed_groups.insert(key); - if selected_here { - s.ssh_form = None; - s.ssh_detail = SshDetail::Defaults; + if let Some(s) = self.active_settings_mut() { + if !s.ssh_collapsed_groups.remove(&key) { + s.ssh_collapsed_groups.insert(key); } } + // Collapsing the list never discards the profile being edited. cx.notify(); } @@ -3535,6 +4512,13 @@ impl Tty7App { window: &mut Window, cx: &mut Context, ) { + if self.ssh_form_dirty(cx) { + let profile = profile.clone(); + self.with_settings_edits_resolved(window, cx, move |this, window, cx| { + this.ssh_form_load(&profile, window, cx) + }); + return; + } let jump_name = profile .jump_host .and_then(|id| { @@ -3784,9 +4768,36 @@ impl Tty7App { cfg.ssh_profiles.push(profile); } }); + if self + .active_settings() + .is_some_and(|s| s.save_error.is_some()) + { + return None; + } Some(id) } + fn cancel_ssh_form(&mut self, window: &mut Window, cx: &mut Context) { + let id = self + .active_settings() + .and_then(|s| s.ssh_form.as_ref().map(|form| form.editing)); + let saved = id.and_then(|id| { + cx.global::() + .ssh_profiles + .iter() + .find(|p| p.id == id) + .cloned() + }); + if let Some(s) = self.active_settings_mut() { + s.ssh_form = None; + s.ssh_detail = SshDetail::Defaults; + } + if let Some(profile) = saved { + self.ssh_form_load(&profile, window, cx); + } + cx.notify(); + } + pub(crate) fn save_ssh_form(&mut self, cx: &mut Context) { self.save_editing_profile(cx); cx.notify(); @@ -3814,22 +4825,9 @@ impl Tty7App { /// closes as the tail of something they explicitly chose, and has already /// saved or does not care. pub(crate) fn close_settings_checked(&mut self, window: &mut Window, cx: &mut Context) { - if !self.ssh_form_dirty(cx) { - self.close_settings(window, cx); - return; - } - let answer = window.prompt( - gpui::PromptLevel::Warning, - t(L10nKey::SettingsDiscardChangesTitle), - Some(t(L10nKey::SettingsDiscardChangesBody)), - &crate::ui::confirm_answers(t(L10nKey::EditorDiscard), t(L10nKey::SettingsKeepEditing)), - cx, - ); - cx.spawn_in(window, async move |this, cx| { - let Ok(0) = answer.await else { return }; - let _ = this.update_in(cx, |this, window, cx| this.close_settings(window, cx)); - }) - .detach(); + self.with_settings_edits_resolved(window, cx, |this, window, cx| { + this.close_settings(window, cx) + }); } /// Dial the host the form is holding — without saving it, and without @@ -4257,6 +5255,10 @@ impl Tty7App { }); let header = h_flex() + .w_full() + .when(self.settings_row_under(STACK_ROW_BELOW, cx), |v| { + v.flex_col() + }) .items_start() .justify_between() .gap_4() @@ -4296,7 +5298,19 @@ impl Tty7App { .child( h_flex() .flex_shrink_0() + .max_w_full() + .flex_wrap() .gap_2() + .child( + Button::new("ssh-form-cancel") + .label(t(L10nKey::Cancel)) + .ghost() + .small() + .disabled(!dirty) + .on_click( + cx.listener(|this, _, window, cx| this.cancel_ssh_form(window, cx)), + ), + ) .child( // Dials the host exactly as Connect would — proxy, jump // and all — but keeps the answer here instead of @@ -5307,12 +6321,8 @@ impl Tty7App { let cfg = cx.global::(); let link_url = cfg.link_url; let ssh_loopback_forward = cfg.ssh_loopback_forward; - let mouse_hide = cfg.mouse_hide_while_typing; - let focus_follows = cfg.focus_follows_mouse; let scroll_mult = cfg.mouse_scroll_multiplier; let smooth_scroll = cfg.smooth_scroll; - let mouse_reporting = cfg.mouse_reporting; - let mouse_zoom = cfg.mouse_zoom_modifier; let bell = cfg.bell; // A bucket highlights only on an exact match; any other value gets a // "Custom (N)" cell so the highlight never claims a number the config @@ -5386,56 +6396,6 @@ impl Tty7App { }, ); - let focus_switch = crate::ui::theme::switch("term-focus-follows", cx) - .checked(focus_follows) - .on_click(cx.listener(|this, on: &bool, _w, cx| this.set_focus_follows_mouse(*on, cx))) - .into_any_element(); - let mouse_hide_switch = crate::ui::theme::switch("term-mouse-hide", cx) - .checked(mouse_hide) - .on_click( - cx.listener(|this, on: &bool, _w, cx| this.set_mouse_hide_while_typing(*on, cx)), - ) - .into_any_element(); - let mouse_report_switch = crate::ui::theme::switch("term-mouse-report", cx) - .checked(mouse_reporting) - .on_click(cx.listener(|this, on: &bool, _w, cx| this.set_mouse_reporting(*on, cx))) - .into_any_element(); - // Ctrl only earns a cell where it is a different key from the - // platform modifier: off macOS the two are the same key, and a - // segmented control with the same key twice is a bug the user has to - // decode. A config that names `ctrl` there still highlights it, in the - // one cell that means it. - let mac = cfg!(target_os = "macos"); - let zoom_labels: Vec<&str> = if mac { - vec!["⌘", "⌃", "⌥", t(L10nKey::SettingsMouseZoomOff)] - } else { - vec!["Ctrl", "Alt", t(L10nKey::SettingsMouseZoomOff)] - }; - let zoom_idx = match (mouse_zoom, mac) { - (MouseZoomModifier::Platform, _) => 0, - (MouseZoomModifier::Ctrl, true) => 1, - (MouseZoomModifier::Ctrl, false) => 0, - (MouseZoomModifier::Alt, true) => 2, - (MouseZoomModifier::Alt, false) => 1, - (MouseZoomModifier::None, true) => 3, - (MouseZoomModifier::None, false) => 2, - }; - let zoom_control = self.segmented( - "term-mouse-zoom", - &zoom_labels, - zoom_idx, - cx, - move |this, ix, _w, cx| { - let modifier = match (ix, mac) { - (0, _) => MouseZoomModifier::Platform, - (1, true) => MouseZoomModifier::Ctrl, - (1, false) => MouseZoomModifier::Alt, - (2, true) => MouseZoomModifier::Alt, - _ => MouseZoomModifier::None, - }; - this.set_mouse_zoom_modifier(modifier, cx); - }, - ); let bell_idx = match bell { BellMode::None => 0, BellMode::Visual => 1, @@ -5488,6 +6448,8 @@ impl Tty7App { v_flex() .child(self.render_shell_group(cx)) .child(self.section_rule(cx)) + .child(self.render_input_groups(true, cx)) + .child(self.section_rule(cx)) .child(self.section_header(t(L10nKey::SettingsScrolling), cx)) .child(self.settings_row( t(L10nKey::SettingsScrollback), @@ -5508,32 +6470,6 @@ impl Tty7App { cx, )) .child(self.section_rule(cx)) - .child(self.section_header(t(L10nKey::SettingsMouse), cx)) - .child(self.settings_row( - t(L10nKey::SettingsFocusFollowsMouse), - t(L10nKey::SettingsFocusFollowsMouseDesc), - focus_switch, - cx, - )) - .child(self.settings_row( - t(L10nKey::SettingsHideMouseWhileTyping), - t(L10nKey::SettingsHideMouseWhileTypingDesc), - mouse_hide_switch, - cx, - )) - .child(self.settings_row( - t(L10nKey::SettingsReportMouseToApps), - t(L10nKey::SettingsReportMouseToAppsDesc), - mouse_report_switch, - cx, - )) - .child(self.settings_row( - t(L10nKey::SettingsMouseZoom), - t(L10nKey::SettingsMouseZoomDesc), - zoom_control, - cx, - )) - .child(self.section_rule(cx)) .child(self.section_header(t(L10nKey::SettingsBell), cx)) .child(self.settings_row( t(L10nKey::SettingsTerminalBell), @@ -5587,6 +6523,107 @@ impl Tty7App { } fn render_settings_input(&self, cx: &mut Context) -> AnyElement { + let cfg = cx.global::(); + let mouse_hide = cfg.mouse_hide_while_typing; + let focus_follows = cfg.focus_follows_mouse; + let mouse_reporting = cfg.mouse_reporting; + let mouse_zoom = cfg.mouse_zoom_modifier; + let focus_switch = crate::ui::theme::switch("term-focus-follows", cx) + .checked(focus_follows) + .on_click(cx.listener(|this, on: &bool, _w, cx| this.set_focus_follows_mouse(*on, cx))) + .into_any_element(); + let mouse_hide_switch = crate::ui::theme::switch("term-mouse-hide", cx) + .checked(mouse_hide) + .on_click( + cx.listener(|this, on: &bool, _w, cx| this.set_mouse_hide_while_typing(*on, cx)), + ) + .into_any_element(); + let mouse_report_switch = crate::ui::theme::switch("term-mouse-report", cx) + .checked(mouse_reporting) + .on_click(cx.listener(|this, on: &bool, _w, cx| this.set_mouse_reporting(*on, cx))) + .into_any_element(); + // Ctrl only earns a cell where it is a different key from the + // platform modifier: off macOS the two are the same key, and a + // segmented control with the same key twice is a bug the user has to + // decode. A config that names `ctrl` there still highlights it, in the + // one cell that means it. + let mac = cfg!(target_os = "macos"); + let zoom_labels: Vec<&str> = if mac { + vec!["⌘", "⌃", "⌥", t(L10nKey::SettingsMouseZoomOff)] + } else { + vec!["Ctrl", "Alt", t(L10nKey::SettingsMouseZoomOff)] + }; + let zoom_idx = match (mouse_zoom, mac) { + (MouseZoomModifier::Platform, _) => 0, + (MouseZoomModifier::Ctrl, true) => 1, + (MouseZoomModifier::Ctrl, false) => 0, + (MouseZoomModifier::Alt, true) => 2, + (MouseZoomModifier::Alt, false) => 1, + (MouseZoomModifier::None, true) => 3, + (MouseZoomModifier::None, false) => 2, + }; + let zoom_control = self.segmented( + "term-mouse-zoom", + &zoom_labels, + zoom_idx, + cx, + move |this, ix, _w, cx| { + let modifier = match (ix, mac) { + (0, _) => MouseZoomModifier::Platform, + (1, true) => MouseZoomModifier::Ctrl, + (1, false) => MouseZoomModifier::Alt, + (2, true) => MouseZoomModifier::Alt, + _ => MouseZoomModifier::None, + }; + this.set_mouse_zoom_modifier(modifier, cx); + }, + ); + v_flex() + .child( + self.settings_row( + t(L10nKey::SettingsSearchKeybindingsTitle), + t(L10nKey::SettingsKeybindingsIntroDesc), + Button::new("open-keybindings") + .label(t(L10nKey::SettingsEditShortcuts)) + .small() + .on_click(cx.listener(|this, _, window, cx| { + this.navigate_settings(SettingsSection::Keybindings, None, window, cx) + })) + .into_any_element(), + cx, + ), + ) + .child(self.render_input_groups(false, cx)) + .child(self.section_rule(cx)) + .child(self.section_header(t(L10nKey::SettingsMouse), cx)) + .child(self.settings_row( + t(L10nKey::SettingsFocusFollowsMouse), + t(L10nKey::SettingsFocusFollowsMouseDesc), + focus_switch, + cx, + )) + .child(self.settings_row( + t(L10nKey::SettingsHideMouseWhileTyping), + t(L10nKey::SettingsHideMouseWhileTypingDesc), + mouse_hide_switch, + cx, + )) + .child(self.settings_row( + t(L10nKey::SettingsReportMouseToApps), + t(L10nKey::SettingsReportMouseToAppsDesc), + mouse_report_switch, + cx, + )) + .child(self.settings_row( + t(L10nKey::SettingsMouseZoom), + t(L10nKey::SettingsMouseZoomDesc), + zoom_control, + cx, + )) + .into_any_element() + } + + fn render_input_groups(&self, prompt: bool, cx: &mut Context) -> AnyElement { let cfg = cx.global::(); let option_as_alt = cfg.macos_option_as_alt; let prompt_editor = cfg.prompt_editor; @@ -5653,61 +6690,65 @@ impl Tty7App { }); v_flex() - .child(self.section_intro( - t(L10nKey::SettingsPrompt), - t(L10nKey::SettingsPromptIntro), - cx, - )) - .child(self.settings_row( - t(L10nKey::SettingsPromptEditor), - t(L10nKey::SettingsPromptEditorDesc), - prompt_editor_switch, - cx, - )) - .child(self.settings_row_gated_when( - t(L10nKey::SettingsTabCompletion), - gated(L10nKey::SettingsTabCompletionDesc), - tab_completion_switch, - !prompt_editor, - cx, - )) - .child(self.settings_row_gated_when( - t(L10nKey::SettingsHistorySearch), - gated(L10nKey::SettingsHistorySearchDesc), - history_search_switch, - !prompt_editor, - cx, - )) - .child(self.settings_row( - t(L10nKey::SettingsPerPaneHistory), - t(L10nKey::SettingsPerPaneHistoryDescription), - per_pane_history_switch, - cx, - )) - .child(self.section_rule(cx)) - .child(self.section_header(t(L10nKey::SettingsSelectionClipboard), cx)) - .child(self.settings_row( - t(L10nKey::SettingsSmartSelection), - t(L10nKey::SettingsSmartSelectionDesc), - smart_select_switch, - cx, - )) - .child(self.settings_row( - t(L10nKey::SettingsCopyOnSelect), - t(L10nKey::SettingsCopyOnSelectDesc), - copy_on_select_switch, - cx, - )) - .child(self.settings_row( - t(L10nKey::SettingsTrimTrailingSpaces), - t(L10nKey::SettingsTrimTrailingSpacesDesc), - trim_switch, - cx, - )) - .when_some(option_alt_row, |v, row| { + .when(prompt, |v| { + v.child(self.section_intro( + t(L10nKey::SettingsPrompt), + t(L10nKey::SettingsPromptIntro), + cx, + )) + .child(self.settings_row( + t(L10nKey::SettingsPromptEditor), + t(L10nKey::SettingsPromptEditorDesc), + prompt_editor_switch, + cx, + )) + .child(self.settings_row_gated_when( + t(L10nKey::SettingsTabCompletion), + gated(L10nKey::SettingsTabCompletionDesc), + tab_completion_switch, + !prompt_editor, + cx, + )) + .child(self.settings_row_gated_when( + t(L10nKey::SettingsHistorySearch), + gated(L10nKey::SettingsHistorySearchDesc), + history_search_switch, + !prompt_editor, + cx, + )) + .child(self.settings_row( + t(L10nKey::SettingsPerPaneHistory), + t(L10nKey::SettingsPerPaneHistoryDescription), + per_pane_history_switch, + cx, + )) + }) + .when(!prompt, |v| { v.child(self.section_rule(cx)) - .child(self.section_header(t(L10nKey::SettingsKeyboard), cx)) - .child(row) + .child(self.section_header(t(L10nKey::SettingsSelectionClipboard), cx)) + .child(self.settings_row( + t(L10nKey::SettingsSmartSelection), + t(L10nKey::SettingsSmartSelectionDesc), + smart_select_switch, + cx, + )) + .child(self.settings_row( + t(L10nKey::SettingsCopyOnSelect), + t(L10nKey::SettingsCopyOnSelectDesc), + copy_on_select_switch, + cx, + )) + .child(self.settings_row( + t(L10nKey::SettingsTrimTrailingSpaces), + t(L10nKey::SettingsTrimTrailingSpacesDesc), + trim_switch, + cx, + )) + .when_some(option_alt_row, |v, row| { + v.child(self.section_rule(cx)) + .child(self.section_header(t(L10nKey::SettingsKeyboard), cx)) + .child(row) + }) }) .into_any_element() } @@ -5909,7 +6950,7 @@ impl Tty7App { ) } - fn render_settings_window_tabs(&self, cx: &mut Context) -> AnyElement { + fn render_window_preferences(&self, general: bool, cx: &mut Context) -> AnyElement { let cfg = cx.global::(); let startup_idx = match cfg.startup_mode { crate::core::config::StartupMode::Normal => 0, @@ -6060,71 +7101,77 @@ impl Tty7App { ); v_flex() - .child(self.section_header(t(L10nKey::SettingsWindow), cx)) - .child(self.settings_row( - t(L10nKey::SettingsStartupWindow), - t(L10nKey::SettingsStartupWindowDesc), - startup_radio, - cx, - )) - .child(self.settings_row( - t(L10nKey::SettingsRememberWindowSize), - t(L10nKey::SettingsRememberWindowSizeDesc), - remember_window_switch, - cx, - )) - .child(self.settings_row( - t(L10nKey::SettingsRestoreLastLayout), - t(L10nKey::SettingsRestoreLastLayoutDesc), - restore_switch, - cx, - )) - .child(self.settings_row( - t(L10nKey::SettingsShowTrayIcon), - t(L10nKey::SettingsShowTrayIconDesc), - tray_switch, - cx, - )) - .child(self.section_rule(cx)) - .child(self.section_header(t(L10nKey::SettingsTabs), cx)) - .child(self.settings_row( - t(L10nKey::SettingsNewTabPosition), - t(L10nKey::SettingsNewTabPositionDesc), - new_tab_radio, - cx, - )) - .child(self.settings_row( - t(L10nKey::SettingsTabBarPosition), - t(L10nKey::SettingsTabBarPositionDesc), - tab_bar_radio, - cx, - )) - .child(self.settings_row( - t(L10nKey::SettingsSidebarGrouping), - t(L10nKey::SettingsSidebarGroupingDesc), - sidebar_grouping_radio, - cx, - )) - .child(self.settings_row( - t(L10nKey::SettingsDiffPreviewFromCounts), - t(L10nKey::SettingsDiffPreviewFromCountsDesc), - sidebar_diff_switch, - cx, - )) - .child(self.section_rule(cx)) - .child(self.section_header(t(L10nKey::SettingsNotifications), cx)) - .child(self.settings_row( - t(L10nKey::SettingsNotifyOnCommandFinish), - t(L10nKey::SettingsNotifyOnCommandFinishDesc), - notify_radio, - cx, - )) - .child(self.settings_row( - t(L10nKey::SettingsNotifyThreshold), - t(L10nKey::SettingsNotifyThresholdDesc), - threshold_radio, - cx, - )) + .when(general, |v| { + v.child(self.section_header(t(L10nKey::SettingsWindow), cx)) + .child(self.settings_row( + t(L10nKey::SettingsStartupWindow), + t(L10nKey::SettingsStartupWindowDesc), + startup_radio, + cx, + )) + .child(self.settings_row( + t(L10nKey::SettingsRememberWindowSize), + t(L10nKey::SettingsRememberWindowSizeDesc), + remember_window_switch, + cx, + )) + .child(self.settings_row( + t(L10nKey::SettingsRestoreLastLayout), + t(L10nKey::SettingsRestoreLastLayoutDesc), + restore_switch, + cx, + )) + .child(self.settings_row( + t(L10nKey::SettingsShowTrayIcon), + t(L10nKey::SettingsShowTrayIconDesc), + tray_switch, + cx, + )) + }) + .when(!general, |v| { + v.child(self.section_rule(cx)) + .child(self.section_header(t(L10nKey::SettingsTabs), cx)) + .child(self.settings_row( + t(L10nKey::SettingsNewTabPosition), + t(L10nKey::SettingsNewTabPositionDesc), + new_tab_radio, + cx, + )) + .child(self.settings_row( + t(L10nKey::SettingsTabBarPosition), + t(L10nKey::SettingsTabBarPositionDesc), + tab_bar_radio, + cx, + )) + .child(self.settings_row( + t(L10nKey::SettingsSidebarGrouping), + t(L10nKey::SettingsSidebarGroupingDesc), + sidebar_grouping_radio, + cx, + )) + .child(self.settings_row( + t(L10nKey::SettingsDiffPreviewFromCounts), + t(L10nKey::SettingsDiffPreviewFromCountsDesc), + sidebar_diff_switch, + cx, + )) + }) + .when(general, |v| { + v.child(self.section_rule(cx)) + .child(self.section_header(t(L10nKey::SettingsNotifications), cx)) + .child(self.settings_row( + t(L10nKey::SettingsNotifyOnCommandFinish), + t(L10nKey::SettingsNotifyOnCommandFinishDesc), + notify_radio, + cx, + )) + .child(self.settings_row( + t(L10nKey::SettingsNotifyThreshold), + t(L10nKey::SettingsNotifyThresholdDesc), + threshold_radio, + cx, + )) + }) .into_any_element() } @@ -6556,7 +7603,7 @@ impl Tty7App { let section = SettingsSection::Keybindings; let query = self .active_settings() - .map(|s| s.search.read(cx).value().trim().to_lowercase()) + .map(|s| s.shortcut_search.read(cx).value().trim().to_lowercase()) .unwrap_or_default(); let (foreground, muted, border, kbd_bg, accent) = { let t = cx.theme(); @@ -6857,6 +7904,10 @@ impl Tty7App { } v_flex() + .when_some(self.active_settings(), |v, s| { + v.child(Input::new(&s.shortcut_search).small()) + .child(self.section_rule(cx)) + }) .child(self.section_intro( t(L10nKey::SettingsNavKeybindings), t(L10nKey::SettingsKeybindingsIntroDesc), @@ -7033,40 +8084,6 @@ impl Tty7App { .text_color(muted_fg) .child(t(L10nKey::SettingsAboutDesc1)), ) - .when(cfg!(target_os = "macos"), |this| { - this.child(self.section_rule(cx)).child( - v_flex() - .gap_2() - .child( - div() - .text_sm() - .font_weight(FontWeight::SEMIBOLD) - .text_color(foreground) - .child(t(L10nKey::SettingsDefaultTerminal)), - ) - .child( - div() - .text_xs() - .text_color(muted_fg) - .child(t(L10nKey::SettingsDefaultTerminalDesc)), - ) - .child( - Button::new("set-default-terminal") - .label(t(L10nKey::SettingsDefaultTerminalSet)) - .small() - .on_click(cx.listener(|_, _, window, cx| { - let message = match crate::core::default_terminal::set_as_default_terminal() { - Ok(()) => t(L10nKey::SettingsDefaultTerminalSetSuccess).to_string(), - Err(error) => t_fmt( - L10nKey::SettingsDefaultTerminalSetFailed, - &[("error", &error)], - ), - }; - window.push_notification(message, cx); - })), - ), - ) - }) .child(self.section_rule(cx)) .child(self.section_header(t(L10nKey::SettingsUpdates), cx)) .child( @@ -7345,6 +8362,102 @@ impl Tty7App { mod tests { use super::*; + #[test] + fn the_catalog_has_unique_titles_and_default_values_are_unmodified() { + let defaults = Config::default(); + for (i, entry) in settings_search_entries().iter().enumerate() { + assert!( + !settings_search_entries()[..i] + .iter() + .any(|e| e.title == entry.title), + "duplicate {:?}", + entry.title + ); + assert!( + !entry.modified(&defaults), + "default {:?} is modified", + entry.title + ); + assert!(SettingsSection::ALL.contains(&entry.section)); + } + assert_eq!(SettingsSection::ALL.len(), 8); + assert!(!SettingsSection::ALL.contains(&SettingsSection::Keybindings)); + } + + #[test] + fn config_keys_and_cross_language_names_reach_the_same_setting() { + for locale in ["en", "zh-CN", "ja-JP"] { + crate::ui::i18n::set_locale(locale); + for (query, title, section) in [ + ( + "gui_language", + L10nKey::SettingsLanguage, + SettingsSection::General, + ), + ( + "mouse_zoom_modifier", + L10nKey::SettingsMouseZoom, + SettingsSection::KeyboardMouse, + ), + ( + "per_pane_history", + L10nKey::SettingsPerPaneHistory, + SettingsSection::Terminal, + ), + ( + "ui_font_size", + L10nKey::SettingsUiFontSize, + SettingsSection::Appearance, + ), + ( + "Shell program", + L10nKey::SettingsProgram, + SettingsSection::Terminal, + ), + ] { + let entry = settings_search_entries() + .iter() + .find(|e| e.title == title) + .unwrap(); + assert!(entry_matches(entry, query), "{locale}: {query}"); + assert_eq!( + best_matching_section(query).unwrap().profile_label(), + section.profile_label() + ); + } + } + crate::ui::i18n::set_locale("en"); + } + + #[test] + fn settings_row_ids_survive_language_changes() { + for key in [ + L10nKey::SettingsFontSize, + L10nKey::SettingsLanguage, + L10nKey::SettingsMouseZoom, + ] { + crate::ui::i18n::set_locale("en"); + let expected = settings_row_id(t(key), ""); + for locale in ["zh-CN", "ja-JP"] { + crate::ui::i18n::set_locale(locale); + assert_eq!(settings_row_id(t(key), ""), expected); + } + } + crate::ui::i18n::set_locale("en"); + } + + #[test] + fn modified_settings_compare_their_own_values_only() { + let mut cfg = Config::default(); + cfg.notify_threshold_secs += 1; + let changed = settings_search_entries() + .iter() + .filter(|e| e.modified(&cfg)) + .map(|e| e.title) + .collect::>(); + assert_eq!(changed, vec![L10nKey::SettingsNotifyThreshold]); + } + /// A shortcut is the first thing someone searching a settings window for a /// feature by name is after, and the Keybindings page was the one page the /// search could not see into: searching "split" found the settings that @@ -7810,11 +8923,11 @@ mod tests { let mut cases: Vec<(&str, SettingsSection)> = vec![ ("opacity", Appearance), ("blur", Appearance), - ("completion", Input), - ("ctrl-r", Input), + ("completion", Terminal), + ("ctrl-r", Terminal), ("grouping", WindowTabs), - ("threshold", WindowTabs), - ("report mouse", Terminal), + ("threshold", General), + ("report mouse", KeyboardMouse), ("nushell", Terminal), ("open files with", Terminal), ("bell", Terminal), @@ -7869,18 +8982,21 @@ mod tests { #[test] fn index_titles_match_rendered_row_labels() { for title in [ - "Start in", + "Starting directory", "Restore last layout", "Terminal bell", "Report mouse to apps", "Open files with", "Sidebar grouping", "Tab completion", - "History search", + "Command history search", "Dim inactive panes", "Option (⌥) acts as Meta", "Install the tty7 command on PATH", ] { + if title == "Option (⌥) acts as Meta" && !cfg!(target_os = "macos") { + continue; + } assert!( settings_search_entries() .iter() @@ -8190,6 +9306,7 @@ mod gpui_tests { use gpui::{AppContext as _, Entity, TestAppContext, VisualTestContext, px, size}; fn harness(cx: &mut TestAppContext) -> (Entity, VisualTestContext) { + crate::core::config::pin_test_config_dir(); cx.executor().allow_parking(); cx.update(|cx| { gpui_component::init(cx); @@ -8214,6 +9331,280 @@ mod gpui_tests { (app, vcx) } + #[gpui::test] + fn enter_on_a_shortcut_search_result_opens_its_local_filter(cx: &mut TestAppContext) { + let (app, mut vcx) = harness(cx); + app.update_in(&mut vcx, |app, window, cx| { + app.open_settings_section(SettingsSection::General, window, cx); + let input = app.active_settings().unwrap().search.clone(); + input.update(cx, |input, cx| input.set_value("SplitRight", window, cx)); + app.autoselect_settings_search(cx); + }); + vcx.run_until_parked(); + vcx.simulate_keystrokes("enter"); + vcx.run_until_parked(); + app.update_in(&mut vcx, |app, _, cx| { + let state = app.active_settings().unwrap(); + assert!(state.section == SettingsSection::Keybindings); + assert!(!state.search_active); + assert_eq!( + state.shortcut_search.read(cx).value().as_str(), + "SplitRight" + ); + }); + } + + #[gpui::test] + fn external_ssh_navigation_resolves_the_current_form_once(cx: &mut TestAppContext) { + crate::core::config::pin_test_config_dir(); + let (app, mut vcx) = harness(cx); + let mut original = crate::core::ssh_profile::SshProfile::new("original"); + original.host = "original.example.com".into(); + let id = original.id; + app.update_in(&mut vcx, |app, window, cx| { + cx.global_mut::() + .ssh_profiles + .push(original.clone()); + app.open_ssh_profile_in_settings(id, window, cx); + let input = app + .active_settings() + .unwrap() + .ssh_form + .as_ref() + .unwrap() + .host + .clone(); + input.update(cx, |input, cx| { + input.set_value("edited.example.com", window, cx) + }); + app.open_ssh_profile_new_from_target("new.example.com".into(), window, cx); + }); + vcx.run_until_parked(); + assert!(vcx.has_pending_prompt()); + vcx.simulate_prompt_answer(crate::ui::i18n::t( + crate::ui::i18n::L10nKey::SettingsKeepEditing, + )); + vcx.run_until_parked(); + app.update_in(&mut vcx, |app, window, cx| { + assert_eq!( + app.active_settings() + .unwrap() + .ssh_form + .as_ref() + .unwrap() + .editing, + id + ); + assert!(app.ssh_form_dirty(cx)); + app.open_ssh_profile_new_from_target("new.example.com".into(), window, cx); + }); + vcx.run_until_parked(); + assert!(vcx.has_pending_prompt()); + vcx.simulate_prompt_answer(crate::ui::i18n::t(crate::ui::i18n::L10nKey::EditorDiscard)); + vcx.run_until_parked(); + assert!(!vcx.has_pending_prompt()); + app.update_in(&mut vcx, |app, _, cx| { + let form = app.active_settings().unwrap().ssh_form.as_ref().unwrap(); + assert_ne!(form.editing, id); + assert_eq!(form.host.read(cx).value().as_str(), "new.example.com"); + }); + app.update_in(&mut vcx, |app, window, cx| { + app.cancel_ssh_form(window, cx); + app.open_ssh_profile_in_settings(id, window, cx); + let input = app + .active_settings() + .unwrap() + .ssh_form + .as_ref() + .unwrap() + .host + .clone(); + input.update(cx, |s, cx| s.set_value("saved.example.com", window, cx)); + app.open_ssh_profile_in_settings(id, window, cx); + }); + vcx.run_until_parked(); + vcx.simulate_prompt_answer(crate::ui::i18n::t( + crate::ui::i18n::L10nKey::SettingsSaveChanges, + )); + vcx.run_until_parked(); + assert!(!vcx.has_pending_prompt()); + app.update_in(&mut vcx, |app, _, cx| { + assert!(!app.ssh_form_dirty(cx)); + assert_eq!( + app.active_settings() + .unwrap() + .ssh_form + .as_ref() + .unwrap() + .host + .read(cx) + .value() + .as_str(), + "saved.example.com" + ); + }); + } + + #[gpui::test] + fn failed_settings_writes_remain_visible_until_retry_succeeds(cx: &mut TestAppContext) { + crate::core::config::pin_test_config_dir(); + let (app, mut vcx) = harness(cx); + app.update_in(&mut vcx, |app, window, cx| { + app.open_settings_section(SettingsSection::General, window, cx); + cx.global_mut::().quarantined = true; + app.set_notify_threshold(73, cx); + assert!(app.active_settings().unwrap().save_error.is_some()); + cx.global_mut::().quarantined = false; + app.persist_settings_config(cx); + assert!(app.active_settings().unwrap().save_error.is_none()); + assert_eq!(cx.global::().notify_threshold_secs, 73); + }); + vcx.run_until_parked(); + } + + #[gpui::test] + fn theme_edits_preview_without_writing_and_cancel_restores_original(cx: &mut TestAppContext) { + crate::core::config::pin_test_config_dir(); + let dir = tempfile::tempdir().unwrap(); + let path = dir.path().join("draft.yaml"); + let (app, mut vcx) = harness(cx); + app.update_in(&mut vcx, |app, window, cx| { + let mut theme = crate::ui::presets::all(cx).remove(0); + theme.id = "settings-test-theme".into(); + theme.path = Some(path.clone()); + crate::ui::presets::write_theme_file(&theme).unwrap(); + let original_file = std::fs::read(&path).unwrap(); + let original_color = theme.accent; + let mut themes = crate::ui::presets::all(cx); + themes.push(theme); + cx.set_global(crate::ui::presets::Themes(themes)); + cx.global_mut::().theme_follow_system = false; + cx.global_mut::().theme_preset = "settings-test-theme".into(); + app.open_settings_section(SettingsSection::Appearance, window, cx); + app.edit_active_theme( + crate::ui::app::ThemeEdit::Accent, + gpui::rgb(0x123456).into(), + window, + cx, + ); + assert!(app.theme_draft_dirty()); + assert_eq!(std::fs::read(&path).unwrap(), original_file); + app.cancel_theme_draft(window, cx); + assert!(!app.theme_draft_dirty()); + assert_eq!( + crate::ui::presets::by_id(cx, "settings-test-theme").accent, + original_color + ); + app.edit_active_theme( + crate::ui::app::ThemeEdit::Accent, + gpui::rgb(0x654321).into(), + window, + cx, + ); + assert!(app.save_theme_draft(window, cx)); + assert_ne!(std::fs::read(&path).unwrap(), original_file); + assert!(!app.theme_draft_dirty()); + app.edit_active_theme( + crate::ui::app::ThemeEdit::Accent, + gpui::rgb(0x102030).into(), + window, + cx, + ); + app.active_settings_mut() + .unwrap() + .theme_draft + .as_mut() + .unwrap() + .1 + .path = Some(dir.path().to_path_buf()); + assert!(!app.save_theme_draft(window, cx)); + assert!(app.theme_draft_dirty()); + assert!(app.active_settings().unwrap().theme_draft_error.is_some()); + }); + } + + #[gpui::test] + fn search_reuses_rows_and_reset_changes_only_the_selected_setting(cx: &mut TestAppContext) { + crate::core::config::pin_test_config_dir(); + let (app, mut vcx) = harness(cx); + app.update_in(&mut vcx, |app, window, cx| { + app.open_settings_section(SettingsSection::General, window, cx); + app.set_notify_threshold(71, cx); + app.set_copy_on_select(!Config::default().copy_on_select, cx); + let search = app.active_settings().unwrap().search.clone(); + search.update(cx, |s, cx| s.set_value("notify_threshold_secs", window, cx)); + app.autoselect_settings_search(cx); + }); + vcx.simulate_resize(size(px(720.), px(560.))); + vcx.run_until_parked(); + app.update_in(&mut vcx, |app, window, cx| { + assert!(app.active_settings().unwrap().search_active); + assert!( + app.active_settings() + .unwrap() + .search_rows + .borrow() + .is_none() + ); + app.reset_settings_value( + crate::ui::i18n::L10nKey::SettingsNotifyThreshold, + window, + cx, + ); + assert_eq!( + cx.global::().notify_threshold_secs, + Config::default().notify_threshold_secs + ); + assert_eq!( + cx.global::().copy_on_select, + !Config::default().copy_on_select + ); + }); + vcx.run_until_parked(); + } + + #[gpui::test] + fn every_category_and_full_search_can_layout_at_minimum_width(cx: &mut TestAppContext) { + let (app, mut vcx) = harness(cx); + for section in SettingsSection::ALL { + app.update_in(&mut vcx, |app, window, cx| { + app.open_settings_section(section, window, cx) + }); + vcx.simulate_resize(size(px(720.), px(560.))); + vcx.run_until_parked(); + } + app.update_in(&mut vcx, |app, _, cx| { + app.active_settings_mut().unwrap().modified_only = true; + app.autoselect_settings_search(cx); + }); + vcx.run_until_parked(); + } + + #[gpui::test] + fn clearing_search_preserves_the_category_and_target_navigation_clears_search( + cx: &mut TestAppContext, + ) { + let (app, mut vcx) = harness(cx); + app.update_in(&mut vcx, |app, window, cx| { + app.open_settings_section(SettingsSection::WindowTabs, window, cx); + let search = app.active_settings().unwrap().search.clone(); + search.update(cx, |s, cx| s.set_value("mouse", window, cx)); + app.autoselect_settings_search(cx); + assert!(app.active_settings().unwrap().section == SettingsSection::WindowTabs); + search.update(cx, |s, cx| s.set_value("", window, cx)); + app.autoselect_settings_search(cx); + assert!(!app.active_settings().unwrap().search_active); + app.navigate_settings( + SettingsSection::KeyboardMouse, + Some(crate::ui::i18n::L10nKey::SettingsMouseZoom), + window, + cx, + ); + assert!(app.active_settings().unwrap().section == SettingsSection::KeyboardMouse); + }); + vcx.run_until_parked(); + } + #[gpui::test] fn appearance_section_lays_out_with_its_rounded_controls(cx: &mut TestAppContext) { let (app, mut vcx) = harness(cx); @@ -8278,7 +9669,7 @@ mod gpui_tests { crate::core::config::pin_test_config_dir(); let (app, mut vcx) = harness(cx); app.update_in(&mut vcx, |app, window, cx| { - app.open_settings_section(SettingsSection::Input, window, cx); + app.open_settings_section(SettingsSection::Terminal, window, cx); app.set_history_search(false, cx); app.set_prompt_editor(false, cx); }); diff --git a/src/ui/ssh_connect.rs b/src/ui/ssh_connect.rs index 167543fe..373968ba 100644 --- a/src/ui/ssh_connect.rs +++ b/src/ui/ssh_connect.rs @@ -150,8 +150,7 @@ impl Tty7App { window: &mut gpui::Window, cx: &mut gpui::Context, ) { - self.open_settings_section(crate::ui::settings::SettingsSection::Ssh, window, cx); - self.add_new_profile(window, cx); + self.open_ssh_profile_form(SshProfile::new(String::new()), window, cx); } /// The connection in the focused pane, when it was dialled by hand rather @@ -199,17 +198,18 @@ impl Tty7App { ) { let profile = profile_from_live_spec(spec); let jumped = spec.jump.is_some(); - self.open_settings_section(crate::ui::settings::SettingsSection::Ssh, window, cx); - self.ssh_form_load(&profile, window, cx); - if jumped { - use gpui_component::WindowExt as _; - // Silently dropping the hop would leave a host that saves fine and - // then cannot be reached. - window.push_notification( - crate::ui::i18n::t(crate::ui::i18n::L10nKey::SshSaveDroppedJumpHost), - cx, - ); - } + self.with_settings_edits_resolved(window, cx, move |this, window, cx| { + this.open_ssh_profile_form(profile, window, cx); + if jumped { + use gpui_component::WindowExt as _; + // Silently dropping the hop would leave a host that saves fine and + // then cannot be reached. + window.push_notification( + crate::ui::i18n::t(crate::ui::i18n::L10nKey::SshSaveDroppedJumpHost), + cx, + ); + } + }); } /// The host form for a machine you are looking at somewhere else: its own @@ -234,12 +234,7 @@ impl Tty7App { profile.id = Uuid::new_v4(); profile.name = alias.clone(); profile.group = None; - self.open_settings_section( - crate::ui::settings::SettingsSection::Ssh, - window, - cx, - ); - self.ssh_form_load(&profile, window, cx); + self.open_ssh_profile_form(profile, window, cx); } None => self.open_ssh_profile_new_from_target(alias.clone(), window, cx), }, From ec0894962514f4b17ed23b790a6089fe66bcf678 Mon Sep 17 00:00:00 2001 From: l0ng-ai <24760907+l0ng-ai@users.noreply.github.com> Date: Wed, 23 Sep 2026 08:30:53 +0800 Subject: [PATCH 2/4] Keep About focused on app information and update status --- docs/customization/settings.mdx | 5 +- docs/reference/updates.mdx | 7 +- src/ui/settings.rs | 274 ++++++++++++++++---------------- 3 files changed, 148 insertions(+), 138 deletions(-) diff --git a/docs/customization/settings.mdx b/docs/customization/settings.mdx index 81aaa7d0..a20ce287 100644 --- a/docs/customization/settings.mdx +++ b/docs/customization/settings.mdx @@ -16,7 +16,8 @@ section something is in. Interface language, startup and layout restore, tray icon, default terminal, - and command completion notifications. + command completion notifications, update preferences and proxy, and the + background session service. Theme and colors, interface and terminal fonts, cursor, background image, @@ -39,7 +40,7 @@ section something is in. AI agent hooks per machine and the tty7 command on PATH. - Version, updates and their proxy, and the background session service. + App information, version, manual update checks, and available updates. diff --git a/docs/reference/updates.mdx b/docs/reference/updates.mdx index fc877fda..09867a5b 100644 --- a/docs/reference/updates.mdx +++ b/docs/reference/updates.mdx @@ -3,8 +3,9 @@ title: "Updates" description: "How tty7 updates itself, and what the two channels mean." --- -**Settings → About** is where everything lives: the version you are on, the -channel you follow, and the button that installs what is waiting. +**Settings → About** shows your current version and lets you check for and +install updates. **Settings → General → Updates** contains the update channel, +automatic check and download preferences, and update proxy. ## How it works @@ -33,7 +34,7 @@ Declining an update defers it rather than retiring it; it comes back later. ## Channels -**Settings → About → Update channel.** +**Settings → General → Updates → Update channel.** | | | |---|---| diff --git a/src/ui/settings.rs b/src/ui/settings.rs index 0d60d7ad..e79cb9fa 100644 --- a/src/ui/settings.rs +++ b/src/ui/settings.rs @@ -449,7 +449,7 @@ fn settings_search_entries() -> &'static [SearchEntry] { keywords: SettingsSearchAboutKeywords, }, SearchEntry { - section: About, + section: General, title: SettingsServer, keywords: SettingsSearchAboutKeywords, }, @@ -806,22 +806,22 @@ fn settings_search_entries() -> &'static [SearchEntry] { keywords: SettingsSearchAboutKeywords, }, SearchEntry { - section: About, + section: General, title: SettingsAppHttpProxy, keywords: SettingsSearchAppHttpProxyKeywords, }, SearchEntry { - section: About, + section: General, title: SettingsUpdateChannel, keywords: SettingsSearchUpdateChannelKeywords, }, SearchEntry { - section: About, + section: General, title: SettingsCheckUpdatesOnLaunch, keywords: SettingsSearchCheckUpdatesOnLaunchKeywords, }, SearchEntry { - section: About, + section: General, title: SettingsAutoDownload, keywords: SettingsSearchAutoDownloadKeywords, }, @@ -3231,6 +3231,8 @@ impl Tty7App { ) }) + .child(self.section_rule(cx)) + .child(self.render_settings_maintenance(cx)) .into_any_element() } @@ -7948,49 +7950,9 @@ impl Tty7App { .into_any_element() } - fn render_settings_about(&self, cx: &mut Context) -> AnyElement { - // Copied out rather than held: `self.segmented` below needs `cx` - // mutably, and a live `cx.theme()` borrow would keep it locked. - let (foreground, muted_fg, danger) = { - let theme = cx.theme(); - (theme.foreground, theme.muted_foreground, theme.danger) - }; - - let update_status = cx - .try_global::() - .cloned() - .unwrap_or_default(); - let update = update_status.available.clone(); - let update_busy = matches!( - update_status.phase, - crate::core::update::UpdatePhase::Checking - | crate::core::update::UpdatePhase::Downloading { .. } - | crate::core::update::UpdatePhase::Verifying - | crate::core::update::UpdatePhase::Installing - ); - let transferring = matches!( - update_status.phase, - crate::core::update::UpdatePhase::Downloading { .. } - | crate::core::update::UpdatePhase::Verifying - ); - // A staged package whose directory has since been swept away is not an - // offer worth making. - let ready = update_status - .ready - .clone() - .filter(crate::core::update::PendingUpdate::is_usable); - // "You're running the latest version" directly above "27.0.0 is ready - // to install" is a contradiction, and a reachable one: a release that - // gets pulled after someone downloaded it leaves exactly this pair. - // The staged package is the more useful of the two claims. - let phase_text = localized_update_phase(&update_status.phase).filter(|_| { - ready.is_none() - || !matches!( - update_status.phase, - crate::core::update::UpdatePhase::UpToDate - ) - }); - let failure = update_status.failure.clone(); + fn render_settings_maintenance(&self, cx: &mut Context) -> AnyElement { + let foreground = cx.theme().foreground; + let muted_fg = cx.theme().muted_foreground; let stale_daemon = crate::daemon::spawn::local_daemon_stale_build(); // Whether picking up the new build costs the user their running panes // decides what this offer is, so it decides what it says. @@ -8042,13 +8004,138 @@ impl Tty7App { .when_some(http_proxy_error, |this, line| this.child(line)) .into_any_element(); + v_flex() + .child(self.section_header(t(L10nKey::SettingsUpdates), cx)) + .child(self.settings_row( + t(L10nKey::SettingsUpdateChannel), + t(L10nKey::SettingsUpdateChannelDesc), + channel_picker, + cx, + )) + .child( + self.settings_row( + t(L10nKey::SettingsCheckUpdatesOnLaunch), + t(L10nKey::SettingsCheckUpdatesDesc), + crate::ui::theme::switch("check-updates", cx) + .checked(check_for_updates) + .on_click(cx.listener(|this, on: &bool, _w, cx| { + this.set_check_for_updates(*on, cx) + })) + .into_any_element(), + cx, + ), + ) + .child( + self.settings_row( + t(L10nKey::SettingsAutoDownload), + t(L10nKey::SettingsAutoDownloadDesc), + crate::ui::theme::switch("auto-download-updates", cx) + .checked(auto_download) + .on_click(cx.listener(|this, on: &bool, _w, cx| { + this.set_auto_download_updates(*on, cx) + })) + .into_any_element(), + cx, + ), + ) + .child(self.settings_row( + t(L10nKey::SettingsAppHttpProxy), + t(L10nKey::SettingsAppHttpProxyDesc), + http_proxy_control, + cx, + )) + .child(self.section_rule(cx)) + .child(self.section_header(t(L10nKey::SettingsServer), cx)) + .child( + v_flex() + .gap_2() + // The other half of an in-place update: the app is new, the + // process serving every pane is not. Said here rather than + // beside the update controls, so the one button that offers + // to pick the new build up stays the only one on the page. + .when_some(stale_daemon.as_deref(), |this, build| { + this.child( + div() + .text_sm() + .text_color(foreground) + .child(t_fmt(L10nKey::SettingsDaemonStale, &[("build", build)])), + ) + }) + .child( + div() + .text_sm() + .text_color(muted_fg) + // A stale server has a more specific thing to say + // than the section's standing description, and it + // ends with the same button. + .child(t(if stale_daemon.is_some() { + stale_daemon_note + } else { + L10nKey::SettingsServerDesc + })), + ) + .child( + h_flex().child( + Button::new("restart-daemon") + .label(t(L10nKey::SettingsRestartServer)) + .small() + .on_click(cx.listener(|this, _, window, cx| { + this.restart_daemon(window, cx) + })), + ), + ), + ) + .into_any_element() + } + + fn render_settings_about(&self, cx: &mut Context) -> AnyElement { + // Copy colors before constructing controls that borrow `cx` mutably. + let (foreground, muted_fg, danger) = { + let theme = cx.theme(); + (theme.foreground, theme.muted_foreground, theme.danger) + }; + + let update_status = cx + .try_global::() + .cloned() + .unwrap_or_default(); + let update = update_status.available.clone(); + let update_busy = matches!( + update_status.phase, + crate::core::update::UpdatePhase::Checking + | crate::core::update::UpdatePhase::Downloading { .. } + | crate::core::update::UpdatePhase::Verifying + | crate::core::update::UpdatePhase::Installing + ); + let transferring = matches!( + update_status.phase, + crate::core::update::UpdatePhase::Downloading { .. } + | crate::core::update::UpdatePhase::Verifying + ); + // A staged package whose directory has since been swept away is not an + // offer worth making. + let ready = update_status + .ready + .clone() + .filter(crate::core::update::PendingUpdate::is_usable); + // "You're running the latest version" directly above "27.0.0 is ready + // to install" is a contradiction, and a reachable one: a release that + // gets pulled after someone downloaded it leaves exactly this pair. + // The staged package is the more useful of the two claims. + let phase_text = localized_update_phase(&update_status.phase).filter(|_| { + ready.is_none() + || !matches!( + update_status.phase, + crate::core::update::UpdatePhase::UpToDate + ) + }); + let failure = update_status.failure.clone(); let logo = Arc::new(Image::from_bytes( ImageFormat::Png, include_bytes!("../../assets/logo@256.png").to_vec(), )); v_flex() - .child(self.section_header(t(L10nKey::SettingsNavAbout), cx)) .child( h_flex() .gap_4() @@ -8273,85 +8360,6 @@ impl Tty7App { })), ) }), - ) - .child(self.settings_row( - t(L10nKey::SettingsUpdateChannel), - t(L10nKey::SettingsUpdateChannelDesc), - channel_picker, - cx, - )) - .child( - self.settings_row( - t(L10nKey::SettingsCheckUpdatesOnLaunch), - t(L10nKey::SettingsCheckUpdatesDesc), - crate::ui::theme::switch("check-updates", cx) - .checked(check_for_updates) - .on_click(cx.listener(|this, on: &bool, _w, cx| { - this.set_check_for_updates(*on, cx) - })) - .into_any_element(), - cx, - ), - ) - .child( - self.settings_row( - t(L10nKey::SettingsAutoDownload), - t(L10nKey::SettingsAutoDownloadDesc), - crate::ui::theme::switch("auto-download-updates", cx) - .checked(auto_download) - .on_click(cx.listener(|this, on: &bool, _w, cx| { - this.set_auto_download_updates(*on, cx) - })) - .into_any_element(), - cx, - ), - ), - ) - .child(self.settings_row( - t(L10nKey::SettingsAppHttpProxy), - t(L10nKey::SettingsAppHttpProxyDesc), - http_proxy_control, - cx, - )) - .child(self.section_rule(cx)) - .child(self.section_header(t(L10nKey::SettingsServer), cx)) - .child( - v_flex() - .gap_2() - // The other half of an in-place update: the app is new, the - // process serving every pane is not. Said here rather than - // beside the update controls, so the one button that offers - // to pick the new build up stays the only one on the page. - .when_some(stale_daemon.as_deref(), |this, build| { - this.child( - div() - .text_sm() - .text_color(foreground) - .child(t_fmt(L10nKey::SettingsDaemonStale, &[("build", build)])), - ) - }) - .child( - div() - .text_sm() - .text_color(muted_fg) - // A stale server has a more specific thing to say - // than the section's standing description, and it - // ends with the same button. - .child(t(if stale_daemon.is_some() { - stale_daemon_note - } else { - L10nKey::SettingsServerDesc - })), - ) - .child( - h_flex().child( - Button::new("restart-daemon") - .label(t(L10nKey::SettingsRestartServer)) - .small() - .on_click(cx.listener(|this, _, window, cx| { - this.restart_daemon(window, cx) - })), - ), ), ) .into_any_element() @@ -8936,13 +8944,13 @@ mod tests { ("symlink", Agents), // Rows the index had no entry for at all, so the query counted // nothing, no badge appeared and no row lit up: the whole Updates - // group on About, and Smooth scrolling between two rows that were + // group on General, and Smooth scrolling between two rows that were // both findable. ("smooth", Terminal), - ("nightly", About), - ("channel", About), - ("metered", About), - ("automatic", About), + ("nightly", General), + ("channel", General), + ("metered", General), + ("automatic", General), // A headline feature the index had never heard of: "background // image" matched nothing, and typing it walked the page to About // because "background" alone hits Download updates in the From acb8075c6494cd25b661f1f568b482b81e34d3c3 Mon Sep 17 00:00:00 2001 From: l0ng-ai <24760907+l0ng-ai@users.noreply.github.com> Date: Wed, 23 Sep 2026 08:35:45 +0800 Subject: [PATCH 3/4] Unify tty7 server terminology across settings and restart flows --- docs/customization/settings.mdx | 2 +- src/ui/i18n/en.rs | 42 ++++++++++++++++----------------- src/ui/i18n/ja.rs | 40 +++++++++++++++---------------- src/ui/i18n/mod.rs | 1 + src/ui/i18n/zh.rs | 40 +++++++++++++++---------------- 5 files changed, 61 insertions(+), 64 deletions(-) diff --git a/docs/customization/settings.mdx b/docs/customization/settings.mdx index a20ce287..ace7beaa 100644 --- a/docs/customization/settings.mdx +++ b/docs/customization/settings.mdx @@ -17,7 +17,7 @@ section something is in. Interface language, startup and layout restore, tray icon, default terminal, command completion notifications, update preferences and proxy, and the - background session service. + tty7 server. Theme and colors, interface and terminal fonts, cursor, background image, diff --git a/src/ui/i18n/en.rs b/src/ui/i18n/en.rs index 88adb297..fa451851 100644 --- a/src/ui/i18n/en.rs +++ b/src/ui/i18n/en.rs @@ -77,7 +77,7 @@ pub fn translate_en(key: L10nKey) -> &'static str { L10nKey::Close => "Close", L10nKey::QuitStopServerTitle => "Quit and Stop Server?", L10nKey::QuitStopServerBody => { - "This quits tty7 and stops the background server; anything running in your shells is terminated. Your tabs and layout reopen with fresh shells next launch. (Closing the window only retires tty7 to the tray — the shells keep running.)" + "This quits tty7 and stops tty7 server; anything running in your shells is terminated. Your tabs and layout reopen with fresh shells next launch. (Closing the window only retires tty7 to the tray — the shells keep running.)" } L10nKey::QuitAndStop => "Quit and Stop", L10nKey::CloseSshConnectionTitle => "Close this SSH connection?", @@ -696,16 +696,16 @@ pub fn translate_en(key: L10nKey) -> &'static str { } L10nKey::SettingsUpdateChannelStable => "Stable", L10nKey::SettingsUpdateChannelNightly => "Nightly", - L10nKey::SettingsDaemonStale => "The background session service is still running {build}.", + L10nKey::SettingsDaemonStale => "tty7 server is still running {build}.", L10nKey::SettingsDaemonStaleDesc => { - "tty7 was updated in place: the app is new, your panes are still served by the old build. Restarting the session service picks up the new one and ends everything running in your panes. No hurry — do it when they're idle." + "tty7 was updated in place: the app is new, your panes are still served by the old build. Restarting tty7 server picks up the new one and ends everything running in your panes. No hurry — do it when they're idle." } L10nKey::UpdateDialogTitle => "Update available", L10nKey::UpdateDialogDetail => { - "tty7 {version} is available — you're on {current}. Installing restarts the app; the background server keeps running, so your panes survive." + "tty7 {version} is available — you're on {current}. Installing restarts the app; tty7 server keeps running, so your panes survive." } L10nKey::UpdateDialogDetailWindows => { - "tty7 {version} is available — you're on {current}. Installing restarts the app and the background service: processes in your panes are ended, and your tabs and layout come back with fresh shells." + "tty7 {version} is available — you're on {current}. Installing restarts the app and tty7 server: processes in your panes are ended, and your tabs and layout come back with fresh shells." } L10nKey::UpdateDialogDetailManual => { "tty7 {version} is available — you're on {current}. {hint}" @@ -753,11 +753,11 @@ pub fn translate_en(key: L10nKey) -> &'static str { "Make the bundled tty7 command available to scripts and AI agents. Takes effect on the next launch; turning this off does not remove an existing installation." } L10nKey::SettingsInstallCliOnPath => "Install the tty7 command on PATH", - L10nKey::SettingsServer => "Background session service", + L10nKey::SettingsServer => "tty7 server", L10nKey::SettingsServerDesc => { - "Keeps terminal sessions running in the background. Restarting ends all shell processes on this computer and reopens the layout with new shells." + "Manages terminal sessions on this computer and keeps them running in the background." } - L10nKey::SettingsRestartServer => "Restart session service…", + L10nKey::SettingsRestartServer => "Restart tty7 server…", L10nKey::SettingsAppHttpProxy => "Proxy for updates", L10nKey::SettingsAppHttpProxyDesc => { "Used only for tty7's update checks and downloads, not for programs in your panes. Empty follows the system proxy." @@ -947,7 +947,7 @@ pub fn translate_en(key: L10nKey) -> &'static str { L10nKey::SwitcherNoMatch => "No workspace or machine matches.", L10nKey::AddSshHost => "Add SSH Host…", L10nKey::ClickForNewWindow => "click for a new window", - L10nKey::RestartServer => "Restart Server", + L10nKey::RestartServer => "Restart tty7 server", L10nKey::OtherMachines => "Other Machines", L10nKey::Ok => "OK", L10nKey::SftpNoTransfers => "No transfers yet.", @@ -1430,7 +1430,7 @@ pub fn translate_en(key: L10nKey) -> &'static str { L10nKey::IoBusy => "Something else has it open.", L10nKey::IoTimedOut => "The machine did not answer in time.", L10nKey::TreeWindowOpenedEmpty => { - "The server never handed over this window's tabs, so it opened empty. Nothing was lost — they come back when it answers. If it doesn't, run \"Restart Server\" from the command palette." + "The server never handed over this window's tabs, so it opened empty. Nothing was lost — they come back when it answers. If it doesn't, run \"Restart tty7 server\" from the command palette." } L10nKey::CmdGroupTabsPanes => "Tabs & Panes", L10nKey::CmdGroupWorkspaces => "Workspaces", @@ -1544,26 +1544,26 @@ pub fn translate_en(key: L10nKey) -> &'static str { L10nKey::CmdDocumentation => "Documentation", L10nKey::CmdJoinDiscord => "Join the Discord", L10nKey::CmdReportIssue => "Report an Issue…", - L10nKey::CmdRestartServer => "Restart Server…", + L10nKey::CmdRestartServer => "Restart tty7 server…", L10nKey::CmdRestartServerSubtitle => "ends every running shell; layout is kept", L10nKey::CmdQuitTty7 => "Quit tty7", L10nKey::CmdQuitTty7Subtitle => "stops the server; every running shell ends", L10nKey::CmdQuickConnect => "Connect to \"{target}\"", L10nKey::CmdQuickConnectSaveProfile => "Save \"{target}\" as profile…", L10nKey::CmdRecent => "Recent", - L10nKey::AppRestartServerTitle => "Restart Server?", - L10nKey::AppRestartServerFailed => "Could not restart the background server: {error}", + L10nKey::AppRestartServerTitle => "Restart tty7 server?", + L10nKey::AppRestartServerFailed => "Could not restart tty7 server: {error}", L10nKey::AppRestartServerMismatchDetail => { - "The server holding your shells speaks protocol {protocol} (build v{build}); this app speaks {ours}, so your tabs are out of reach.\n\nQuit: nothing changes — the server and your shells keep running.\nRestart: tabs come back with fresh shells; anything running now is killed." + "tty7 server holding your shells speaks protocol {protocol} (build v{build}); this app speaks {ours}, so your tabs are out of reach.\n\nQuit: nothing changes — tty7 server and your shells keep running.\nRestart: tabs come back with fresh shells; anything running now is killed." } L10nKey::AppRestartServerDialectDetail => { - "The server holding your shells speaks control dialect v{dialect} (build v{build}); this app speaks v{ours}, so every window opens empty.\n\nQuit: nothing changes — the server and your shells keep running.\nRestart: tabs come back with fresh shells; anything running now is killed." + "tty7 server holding your shells speaks control dialect v{dialect} (build v{build}); this app speaks v{ours}, so every window opens empty.\n\nQuit: nothing changes — tty7 server and your shells keep running.\nRestart: tabs come back with fresh shells; anything running now is killed." } L10nKey::AppRestartServerDialectNewerDetail => { - "The server holding your shells speaks control dialect v{dialect} (build v{build}); this app speaks v{ours}, so every window opens empty.\n\nQuit and install the newer build: the real fix — your shells survive it.\nRestart: tabs come back with fresh shells; anything running now is killed." + "tty7 server holding your shells speaks control dialect v{dialect} (build v{build}); this app speaks v{ours}, so every window opens empty.\n\nQuit and install the newer build: the real fix — your shells survive it.\nRestart: tabs come back with fresh shells; anything running now is killed." } L10nKey::AppRestartServerOldDetail => { - "The server holding your shells predates the version handshake, so this app can't tell what it speaks.\n\nQuit: nothing changes — the server and your shells keep running.\nRestart: tabs come back with fresh shells; anything running now is killed." + "tty7 server holding your shells predates the version handshake, so this app can't tell what it speaks.\n\nQuit: nothing changes — tty7 server and your shells keep running.\nRestart: tabs come back with fresh shells; anything running now is killed." } L10nKey::AppRestart => "Restart", L10nKey::AppRestartServerNoServer => { @@ -1705,10 +1705,10 @@ pub fn translate_en(key: L10nKey) -> &'static str { L10nKey::Replace => "Replace", L10nKey::SftpErrorInvalidOctalMode => "invalid octal mode", L10nKey::SettingsDaemonStaleDescInPlace => { - "tty7 was updated in place: the app is new, your panes still run on the old build. The session service can swap itself for the new one without stopping, so your shells carry straight over. Panes on tty7's built-in SSH client are the exception — those close and need reopening." + "tty7 was updated in place: the app is new, your panes still run on the old build. tty7 server can swap itself for the new one without stopping, so your shells carry straight over. Panes on tty7's built-in SSH client are the exception — those close and need reopening." } L10nKey::AppRestartServerBodyInPlace => { - "The server swaps itself for this build in place: your shells keep running, and the window reconnects a moment later. Panes on tty7's built-in SSH client are the exception — those close and need reopening." + "tty7 server swaps itself for this build in place: your shells keep running, and the window reconnects a moment later. Panes on tty7's built-in SSH client are the exception — those close and need reopening." } L10nKey::PaneRestoredScreenBanner => { "restored screen — this shell is new, nothing above it is still running" @@ -1799,7 +1799,7 @@ pub fn translate_en(key: L10nKey) -> &'static str { L10nKey::AppMenuKeyboardShortcuts => "Keyboard Shortcuts", L10nKey::AppMenuJoinDiscord => "Join the Discord", L10nKey::AppMenuReportIssue => "Report an Issue…", - L10nKey::AppMenuRestartServer => "Restart Server…", + L10nKey::AppMenuRestartServer => "Restart tty7 server…", L10nKey::WindowUntitled => "Untitled", L10nKey::TrayShowTty7 => "Show tty7", L10nKey::TrayNotifications => "Notifications", @@ -1836,7 +1836,7 @@ pub fn translate_en(key: L10nKey) -> &'static str { L10nKey::AppMenuEnterFullscreen => "Enter Full Screen", L10nKey::HomeTimeOverWeekAgo => "over a week ago", L10nKey::Search => "Search", - L10nKey::SettingsDaemonStaleRestart => "Restart Service", + L10nKey::SettingsDaemonStaleRestart => "Restart tty7 server", L10nKey::SettingsNoneLower => "none", L10nKey::SettingsSearchCommandLineToolTitle => "Command line tool", L10nKey::TabContextMarkUnread => "Mark as Unread", diff --git a/src/ui/i18n/ja.rs b/src/ui/i18n/ja.rs index bf3347a6..4dc09e82 100644 --- a/src/ui/i18n/ja.rs +++ b/src/ui/i18n/ja.rs @@ -78,7 +78,7 @@ pub fn translate_ja(key: L10nKey) -> Option<&'static str> { L10nKey::Close => "閉じる", L10nKey::QuitStopServerTitle => "tty7 を終了してサーバーを停止しますか?", L10nKey::QuitStopServerBody => { - "tty7 を終了してバックグラウンドサーバーを停止します。シェルで実行中のものはすべて終了します。タブとレイアウトは次回起動時に新しいシェルで開きます。(ウィンドウを閉じるだけならトレイに退避し、シェルは動き続けます)" + "tty7 を終了してtty7 serverを停止します。シェルで実行中のものはすべて終了します。タブとレイアウトは次回起動時に新しいシェルで開きます。(ウィンドウを閉じるだけならトレイに退避し、シェルは動き続けます)" } L10nKey::QuitAndStop => "終了して停止", L10nKey::CloseSshConnectionTitle => "この SSH 接続を閉じますか?", @@ -706,16 +706,16 @@ pub fn translate_ja(key: L10nKey) -> Option<&'static str> { } L10nKey::SettingsUpdateChannelStable => "安定版", L10nKey::SettingsUpdateChannelNightly => "ナイトリー", - L10nKey::SettingsDaemonStale => "バックグラウンドセッションサービスは {build} のままです。", + L10nKey::SettingsDaemonStale => "tty7 serverは {build} のままです。", L10nKey::SettingsDaemonStaleDesc => { - "tty7 はその場で更新されました。アプリは新しく、ペインはまだ以前のビルドのセッションサービスが処理しています。再起動すると新しいビルドに切り替わり、ペインで動いているプロセスはすべて終了します。急ぐ必要はなく、ペインが空いているときにどうぞ" + "tty7 はその場で更新されました。アプリは新しく、ペインはまだ以前のビルドのtty7 serverが処理しています。再起動すると新しいビルドに切り替わり、ペインで動いているプロセスはすべて終了します。急ぐ必要はなく、ペインが空いているときにどうぞ" } L10nKey::UpdateDialogTitle => "アップデートがあります", L10nKey::UpdateDialogDetail => { - "tty7 {version} が利用できます(現在 {current})。インストールするとアプリが再起動します。バックグラウンドサーバーは動いたままなので、ペインの中身は残ります" + "tty7 {version} が利用できます(現在 {current})。インストールするとアプリが再起動します。tty7 serverは動いたままなので、ペインの中身は残ります" } L10nKey::UpdateDialogDetailWindows => { - "tty7 {version} が利用できます(現在 {current})。インストールするとアプリとバックグラウンドサービスが再起動します。ペインのプロセスは終了し、タブとレイアウトは新しいシェルで復元されます" + "tty7 {version} が利用できます(現在 {current})。インストールするとアプリとtty7 serverが再起動します。ペインのプロセスは終了し、タブとレイアウトは新しいシェルで復元されます" } L10nKey::UpdateDialogDetailManual => { "tty7 {version} が利用できます(現在 {current})。{hint}" @@ -763,11 +763,11 @@ pub fn translate_ja(key: L10nKey) -> Option<&'static str> { "付属の tty7 コマンドをスクリプトや AI エージェントから利用できます。次回起動時に反映されます。無効にしてもインストール済みのコマンドは削除されません。" } L10nKey::SettingsInstallCliOnPath => "`tty7` コマンドを PATH にインストール", - L10nKey::SettingsServer => "バックグラウンドセッションサービス", + L10nKey::SettingsServer => "tty7 server", L10nKey::SettingsServerDesc => { - "ターミナルセッションをバックグラウンドで維持します。再起動すると、このコンピューター上のすべてのシェルプロセスを終了し、新しいシェルでレイアウトを開き直します。" + "このコンピューターのターミナルセッションを管理し、バックグラウンドで実行し続けます。" } - L10nKey::SettingsRestartServer => "セッションサービスを再起動…", + L10nKey::SettingsRestartServer => "tty7 serverを再起動…", L10nKey::SettingsAppHttpProxy => "アップデート用プロキシ", L10nKey::SettingsAppHttpProxyDesc => { "tty7 自身の更新チェックとダウンロードにのみ使用し、ペインで実行中のプログラムには影響しません。空欄ならシステムのプロキシに従います" @@ -1609,25 +1609,23 @@ pub fn translate_ja(key: L10nKey) -> Option<&'static str> { L10nKey::CmdQuickConnect => "「{target}」に接続", L10nKey::CmdQuickConnectSaveProfile => "「{target}」をプロファイルとして保存…", L10nKey::CmdRecent => "最近", - L10nKey::AppRestartServerTitle => "サーバーを再起動しますか?", - L10nKey::AppRestartServerFailed => { - "バックグラウンドサーバーを再起動できませんでした: {error}" - } + L10nKey::AppRestartServerTitle => "tty7 server を再起動しますか?", + L10nKey::AppRestartServerFailed => "tty7 serverを再起動できませんでした: {error}", L10nKey::AppRestartServerMismatchDetail => { - "サーバーはプロトコル {protocol}(ビルド v{build})、このアプリは {ours} のため、タブを取り出せません。\n\n終了:何も変わりません。サーバーもシェルも動き続けます。\n再起動:タブは新しいシェルで戻り、いま実行中のものは終了します" + "tty7 serverはプロトコル {protocol}(ビルド v{build})、このアプリは {ours} のため、タブを取り出せません。\n\n終了:何も変わりません。tty7 serverもシェルも動き続けます。\n再起動:タブは新しいシェルで戻り、いま実行中のものは終了します" } L10nKey::AppRestartServerDialectDetail => { - "サーバーは制御方言 v{dialect}(ビルド v{build})、このアプリは v{ours} のため、ウィンドウはどれも空で開きます。\n\n終了:何も変わりません。サーバーもシェルも動き続けます。\n再起動:タブは新しいシェルで戻り、いま実行中のものは終了します" + "tty7 serverは制御方言 v{dialect}(ビルド v{build})、このアプリは v{ours} のため、ウィンドウはどれも空で開きます。\n\n終了:何も変わりません。tty7 serverもシェルも動き続けます。\n再起動:タブは新しいシェルで戻り、いま実行中のものは終了します" } L10nKey::AppRestartServerDialectNewerDetail => { - "サーバーは制御方言 v{dialect}(ビルド v{build})、このアプリは v{ours} のため、ウィンドウはどれも空で開きます。\n\n終了して新しいビルドを入れる:根本的な解決で、シェルはそのまま残ります。\n再起動:タブは新しいシェルで戻り、いま実行中のものは終了します" + "tty7 serverは制御方言 v{dialect}(ビルド v{build})、このアプリは v{ours} のため、ウィンドウはどれも空で開きます。\n\n終了して新しいビルドを入れる:根本的な解決で、シェルはそのまま残ります。\n再起動:タブは新しいシェルで戻り、いま実行中のものは終了します" } L10nKey::AppRestartServerOldDetail => { - "サーバーはバージョン照合より前のもので、何を話すか分かりません。\n\n終了:何も変わりません。サーバーもシェルも動き続けます。\n再起動:タブは新しいシェルで戻り、いま実行中のものは終了します" + "tty7 serverはバージョン照合より前のもので、何を話すか分かりません。\n\n終了:何も変わりません。tty7 serverもシェルも動き続けます。\n再起動:タブは新しいシェルで戻り、いま実行中のものは終了します" } L10nKey::AppRestart => "再起動", L10nKey::AppRestartServerNoServer => { - "{label} には再起動できるサーバーがありません。このコンピュータが --stdio で実行しているプログラムです。代わりにワークスペースを止めてください" + "{label} には再起動できるtty7 serverがありません。このコンピュータが --stdio で実行しているプログラムです。代わりにワークスペースを止めてください" } L10nKey::AppRestartServerBody => { "このコンピュータのシェルはすべて終了します。タブとレイアウトは保持され、新しいシェルで開きます" @@ -1779,10 +1777,10 @@ pub fn translate_ja(key: L10nKey) -> Option<&'static str> { L10nKey::Replace => "置き換える", L10nKey::SftpErrorInvalidOctalMode => "無効な 8 進数モードです", L10nKey::SettingsDaemonStaleDescInPlace => { - "tty7 はその場で更新されました。アプリは新しく、ペインはまだ前のビルドで動いています。セッションサービスは停止せずに新しいビルドへ置き換えられるので、シェルはそのまま引き継がれます。tty7 内蔵の SSH クライアントを使うペインだけは例外で、その接続は閉じられ、開き直しが必要です" + "tty7 はその場で更新されました。アプリは新しく、ペインはまだ前のビルドで動いています。tty7 serverは停止せずに新しいビルドへ置き換えられるので、シェルはそのまま引き継がれます。tty7 内蔵の SSH クライアントを使うペインだけは例外で、その接続は閉じられ、開き直しが必要です" } L10nKey::AppRestartServerBodyInPlace => { - "サーバーは停止せずに自分自身をこのビルドへ置き換えます。シェルは動いたままで、ウィンドウはすぐに再接続します。tty7 内蔵の SSH クライアントを使うペインだけは例外で、その接続は閉じられ、開き直しが必要です" + "tty7 serverは停止せずに自分自身をこのビルドへ置き換えます。シェルは動いたままで、ウィンドウはすぐに再接続します。tty7 内蔵の SSH クライアントを使うペインだけは例外で、その接続は閉じられ、開き直しが必要です" } L10nKey::PaneRestoredScreenBanner => { "復元された画面 — 以下は新しいシェルで、これより上のものは動いていません" @@ -1873,7 +1871,7 @@ pub fn translate_ja(key: L10nKey) -> Option<&'static str> { L10nKey::AppMenuKeyboardShortcuts => "キーボードショートカット", L10nKey::AppMenuJoinDiscord => "Discord に参加", L10nKey::AppMenuReportIssue => "問題を報告…", - L10nKey::AppMenuRestartServer => "セッションサービスを再起動…", + L10nKey::AppMenuRestartServer => "tty7 serverを再起動…", L10nKey::WindowUntitled => "無題", L10nKey::TrayShowTty7 => "tty7 を表示", L10nKey::TrayNotifications => "通知", @@ -1910,7 +1908,7 @@ pub fn translate_ja(key: L10nKey) -> Option<&'static str> { L10nKey::AppMenuEnterFullscreen => "全画面表示", L10nKey::HomeTimeOverWeekAgo => "1 週間以上前", L10nKey::Search => "検索", - L10nKey::SettingsDaemonStaleRestart => "サービスを再起動", + L10nKey::SettingsDaemonStaleRestart => "tty7 server を再起動", L10nKey::SettingsNoneLower => "なし", L10nKey::SettingsSearchCommandLineToolTitle => "コマンドラインツール", L10nKey::TabContextMarkUnread => "未読としてマーク", diff --git a/src/ui/i18n/mod.rs b/src/ui/i18n/mod.rs index 09ad0ebc..0a261b16 100644 --- a/src/ui/i18n/mod.rs +++ b/src/ui/i18n/mod.rs @@ -1568,6 +1568,7 @@ mod tests { L10nKey::HostOpsError, L10nKey::SftpTransferProgress, // Product names. + L10nKey::SettingsServer, L10nKey::SettingsAgentClaudeCode, L10nKey::SettingsAgentCodex, L10nKey::SettingsAgentTraeCode, diff --git a/src/ui/i18n/zh.rs b/src/ui/i18n/zh.rs index 196eb290..355a17d1 100644 --- a/src/ui/i18n/zh.rs +++ b/src/ui/i18n/zh.rs @@ -72,7 +72,7 @@ pub fn translate_zh(key: L10nKey) -> Option<&'static str> { L10nKey::Close => "关闭", L10nKey::QuitStopServerTitle => "退出并停止 server?", L10nKey::QuitStopServerBody => { - "这会退出 tty7 并停止后台 server,shell 里正在跑的东西都会被终止。标签页和布局会保留,下次启动时以全新的 shell 打开。(只关窗口的话应用收进托盘,shell 继续跑。)" + "这会退出 tty7 并停止tty7 server,shell 里正在跑的东西都会被终止。标签页和布局会保留,下次启动时以全新的 shell 打开。(只关窗口的话应用收进托盘,shell 继续跑。)" } L10nKey::QuitAndStop => "退出并停止", L10nKey::CloseSshConnectionTitle => "关闭这个 SSH 连接?", @@ -619,16 +619,16 @@ pub fn translate_zh(key: L10nKey) -> Option<&'static str> { } L10nKey::SettingsUpdateChannelStable => "稳定版", L10nKey::SettingsUpdateChannelNightly => "每夜构建", - L10nKey::SettingsDaemonStale => "后台 后台会话服务 仍运行在 {build}。", + L10nKey::SettingsDaemonStale => "tty7 server 仍运行在 {build}。", L10nKey::SettingsDaemonStaleDesc => { - "tty7 是原地升级的:界面已是新版,pane 还由旧版 后台会话服务 托管。重启 后台会话服务 换成新版,代价是 pane 里正在跑的进程全部结束。不急,挑 pane 空闲时再重启。" + "tty7 是原地升级的:界面已是新版,pane 还由旧版 tty7 server 托管。重启 tty7 server 换成新版,代价是 pane 里正在跑的进程全部结束。不急,挑 pane 空闲时再重启。" } L10nKey::UpdateDialogTitle => "有可用更新", L10nKey::UpdateDialogDetail => { - "tty7 {version} 已发布,你现在是 {current}。安装会重启应用;后台 server 不动,pane 里的东西都还在。" + "tty7 {version} 已发布,你现在是 {current}。安装会重启应用;tty7 server 不动,pane 里的东西都还在。" } L10nKey::UpdateDialogDetailWindows => { - "tty7 {version} 已发布,你现在是 {current}。安装会重启应用和后台 server:pane 里的进程会被结束,标签页和布局以全新的 shell 恢复。" + "tty7 {version} 已发布,你现在是 {current}。安装会重启应用和tty7 server:pane 里的进程会被结束,标签页和布局以全新的 shell 恢复。" } L10nKey::UpdateDialogDetailManual => "tty7 {version} 已发布,你现在是 {current}。{hint}", L10nKey::UpdateDialogCannotSelfUpdate => "这份安装无法自行更新。", @@ -670,11 +670,9 @@ pub fn translate_zh(key: L10nKey) -> Option<&'static str> { "让脚本和 AI Agent 使用随应用提供的 tty7 命令。下次启动生效;关闭后不会移除已安装的命令。" } L10nKey::SettingsInstallCliOnPath => "将 `tty7` 命令安装到 PATH", - L10nKey::SettingsServer => "后台会话服务", - L10nKey::SettingsServerDesc => { - "在后台维持终端会话。重启会结束这台计算机上的所有 Shell 进程,并按原布局打开新的 Shell。" - } - L10nKey::SettingsRestartServer => "重启后台会话服务…", + L10nKey::SettingsServer => "tty7 server", + L10nKey::SettingsServerDesc => "管理这台计算机上的终端会话,让会话在后台持续运行。", + L10nKey::SettingsRestartServer => "重启 tty7 server…", L10nKey::SettingsAppHttpProxy => "更新代理", L10nKey::SettingsAppHttpProxyDesc => { "仅用于 tty7 自身的更新检查和下载,不影响面板中运行的程序。留空则跟随系统代理。" @@ -1466,23 +1464,23 @@ pub fn translate_zh(key: L10nKey) -> Option<&'static str> { L10nKey::CmdQuickConnect => "连接到“{target}”", L10nKey::CmdQuickConnectSaveProfile => "将“{target}”保存为主机配置…", L10nKey::CmdRecent => "最近使用", - L10nKey::AppRestartServerTitle => "重启 server?", - L10nKey::AppRestartServerFailed => "无法重启后台 server:{error}", + L10nKey::AppRestartServerTitle => "重启 tty7 server?", + L10nKey::AppRestartServerFailed => "无法重启tty7 server:{error}", L10nKey::AppRestartServerMismatchDetail => { - "server 用协议 {protocol}(构建 v{build}),此应用用 {ours},标签页取不出来。\n\n退出:什么都不变,server 和 shell 继续运行。\n重启:标签页带全新 shell 回来,现在跑着的东西会被杀掉。" + "tty7 server 用协议 {protocol}(构建 v{build}),此应用用 {ours},标签页取不出来。\n\n退出:什么都不变,tty7 server 和 shell 继续运行。\n重启:标签页带全新 shell 回来,现在跑着的东西会被杀掉。" } L10nKey::AppRestartServerDialectDetail => { - "server 用 control 方言 v{dialect}(构建 v{build}),此应用用 v{ours},每个窗口都开成空的。\n\n退出:什么都不变,server 和 shell 继续运行。\n重启:标签页带全新 shell 回来,现在跑着的东西会被杀掉。" + "tty7 server 用 control 方言 v{dialect}(构建 v{build}),此应用用 v{ours},每个窗口都开成空的。\n\n退出:什么都不变,tty7 server 和 shell 继续运行。\n重启:标签页带全新 shell 回来,现在跑着的东西会被杀掉。" } L10nKey::AppRestartServerDialectNewerDetail => { - "server 用 control 方言 v{dialect}(构建 v{build}),此应用用 v{ours},每个窗口都开成空的。\n\n退出并装上更新的构建:真正的解法,shell 全都还在。\n重启:标签页带全新 shell 回来,现在跑着的东西会被杀掉。" + "tty7 server 用 control 方言 v{dialect}(构建 v{build}),此应用用 v{ours},每个窗口都开成空的。\n\n退出并装上更新的构建:真正的解法,shell 全都还在。\n重启:标签页带全新 shell 回来,现在跑着的东西会被杀掉。" } L10nKey::AppRestartServerOldDetail => { - "server 早于版本握手,此应用无从得知它说的是什么。\n\n退出:什么都不变,server 和 shell 继续运行。\n重启:标签页带全新 shell 回来,现在跑着的东西会被杀掉。" + "tty7 server 早于版本握手,此应用无从得知它说的是什么。\n\n退出:什么都不变,tty7 server 和 shell 继续运行。\n重启:标签页带全新 shell 回来,现在跑着的东西会被杀掉。" } L10nKey::AppRestart => "重启", L10nKey::AppRestartServerNoServer => { - "{label} 没有自己的 server 可重启——它是本机通过 --stdio 运行的程序。请改为停止其工作区。" + "{label} 没有自己的 tty7 server 可重启——它是本机通过 --stdio 运行的程序。请改为停止其工作区。" } L10nKey::AppRestartServerBody => { "这会结束本机上所有 shell。标签页和布局会保留,并以全新的 shell 重新打开。" @@ -1616,10 +1614,10 @@ pub fn translate_zh(key: L10nKey) -> Option<&'static str> { L10nKey::Replace => "覆盖", L10nKey::SftpErrorInvalidOctalMode => "无效的八进制模式", L10nKey::SettingsDaemonStaleDescInPlace => { - "tty7 是原地更新的:应用是新的,面板还跑在旧版上。后台会话服务 可以不停机就换成新版,shell 直接延续下来。用 tty7 内置 SSH 客户端的面板除外——那些连接会断开,需要重新打开。" + "tty7 是原地更新的:应用是新的,面板还跑在旧版上。tty7 server 可以不停机就换成新版,shell 直接延续下来。用 tty7 内置 SSH 客户端的面板除外——那些连接会断开,需要重新打开。" } L10nKey::AppRestartServerBodyInPlace => { - "后台 server 会原地把自己换成当前这个版本:shell 继续运行,窗口稍后自动连回去。用 tty7 内置 SSH 客户端的面板除外——那些连接会断开,需要重新打开。" + "tty7 server 会原地把自己换成当前这个版本:shell 继续运行,窗口稍后自动连回去。用 tty7 内置 SSH 客户端的面板除外——那些连接会断开,需要重新打开。" } L10nKey::PaneRestoredScreenBanner => { "已恢复的画面 —— 下面是新的 shell,上面的内容都已不在运行" @@ -1708,7 +1706,7 @@ pub fn translate_zh(key: L10nKey) -> Option<&'static str> { L10nKey::AppMenuKeyboardShortcuts => "键盘快捷键", L10nKey::AppMenuJoinDiscord => "加入 Discord", L10nKey::AppMenuReportIssue => "报告问题…", - L10nKey::AppMenuRestartServer => "重启 后台会话服务…", + L10nKey::AppMenuRestartServer => "重启 tty7 server…", L10nKey::WindowUntitled => "未命名", L10nKey::TrayShowTty7 => "显示 tty7", L10nKey::TrayNotifications => "通知", @@ -1745,7 +1743,7 @@ pub fn translate_zh(key: L10nKey) -> Option<&'static str> { L10nKey::AppMenuEnterFullscreen => "进入全屏", L10nKey::HomeTimeOverWeekAgo => "一周多前", L10nKey::Search => "搜索", - L10nKey::SettingsDaemonStaleRestart => "重启 后台会话服务", + L10nKey::SettingsDaemonStaleRestart => "重启 tty7 server", L10nKey::SettingsNoneLower => "无", L10nKey::SettingsSearchCommandLineToolTitle => "命令行工具", L10nKey::TabContextMarkUnread => "标记为未读", From a26dab532a6c1ef1ee7c29a9f2df2fc7ad0a300a Mon Sep 17 00:00:00 2001 From: l0ng-ai <24760907+l0ng-ai@users.noreply.github.com> Date: Wed, 23 Sep 2026 08:47:15 +0800 Subject: [PATCH 4/4] Align settings terminology and documentation navigation --- docs/customization/keybindings.mdx | 4 +- docs/getting-started/first-launch.mdx | 2 +- docs/getting-started/installation.mdx | 2 +- docs/reference/keyboard-shortcuts.mdx | 2 +- docs/reference/troubleshooting.mdx | 2 +- docs/terminal/mouse-and-scrolling.mdx | 2 +- src/ui/i18n/en.rs | 8 ++-- src/ui/i18n/ja.rs | 58 +++++++++++++-------------- src/ui/i18n/mod.rs | 1 - src/ui/i18n/zh.rs | 56 +++++++++++++------------- 10 files changed, 68 insertions(+), 69 deletions(-) diff --git a/docs/customization/keybindings.mdx b/docs/customization/keybindings.mdx index ab36b884..b30d42ee 100644 --- a/docs/customization/keybindings.mdx +++ b/docs/customization/keybindings.mdx @@ -3,7 +3,7 @@ title: "Keybindings" description: "Rebinding anything, chord sequences, and the tmux preset." --- -**Settings → Keybindings** (⌘ ,) lists every shortcut in the app, +**Settings → Keyboard & Mouse → Keyboard shortcuts** (⌘ ,) lists every shortcut in the app, grouped the same way the command palette is. The search box at the top of Settings reaches this page too: type what a feature is called — `split`, `commit`, `sftp` — and the page narrows to the shortcuts for it. Action names @@ -63,7 +63,7 @@ page. ## The tmux preset -**Settings → Keybindings → Preset → tmux** remaps pane and tab actions onto a +**Settings → Keyboard & Mouse → Keyboard shortcuts → Preset → tmux** remaps pane and tab actions onto a prefix — ⌃ B by default, changeable in the **Prefix** field beside it. diff --git a/docs/getting-started/first-launch.mdx b/docs/getting-started/first-launch.mdx index f5246011..59ee13f5 100644 --- a/docs/getting-started/first-launch.mdx +++ b/docs/getting-started/first-launch.mdx @@ -95,7 +95,7 @@ same policy. ## 7. Coming from tmux? -**Settings → Keybindings → Preset → tmux** remaps pane and tab actions onto a +**Settings → Keyboard & Mouse → Keyboard shortcuts → Preset → tmux** remaps pane and tab actions onto a prefix, ⌃ B by default. ⌃ B C opens a tab, ⌃ B % splits, ⌃ B then an arrow moves focus. diff --git a/docs/getting-started/installation.mdx b/docs/getting-started/installation.mdx index 1211a214..48b35c37 100644 --- a/docs/getting-started/installation.mdx +++ b/docs/getting-started/installation.mdx @@ -92,7 +92,7 @@ tty7 checks for updates every six hours and can update itself: **Settings → About → Check now**, then **Update and relaunch**. Releases are downloaded and verified in the background so applying one is just a restart. -Pick **Stable** or **Nightly** under **Settings → About → Update channel**. See +Pick **Stable** or **Nightly** under **Settings → General → Updates → Update channel**. See [Updates and channels](/reference/updates) for what each feed publishes and how switching behaves. diff --git a/docs/reference/keyboard-shortcuts.mdx b/docs/reference/keyboard-shortcuts.mdx index a526c78c..a89becae 100644 --- a/docs/reference/keyboard-shortcuts.mdx +++ b/docs/reference/keyboard-shortcuts.mdx @@ -3,7 +3,7 @@ title: "Keyboard shortcuts" description: "Every default binding, plus the action names for rebinding." --- -**Settings → Keybindings** (⌘ ,) is the live version of this page — +**Settings → Keyboard & Mouse → Keyboard shortcuts** (⌘ ,) is the live version of this page — it shows what *your* copy is bound to. This is the shipped default. ## Tabs and workspaces diff --git a/docs/reference/troubleshooting.mdx b/docs/reference/troubleshooting.mdx index f8de1b27..e46cb524 100644 --- a/docs/reference/troubleshooting.mdx +++ b/docs/reference/troubleshooting.mdx @@ -187,7 +187,7 @@ If the file cannot be parsed, tty7 starts on defaults and keeps your original at ## Selecting text inside vim / less selects the app's own thing Hold ⇧ while dragging to keep the gesture local, or turn off -**Settings → Terminal → Mouse → Report mouse to apps**. +**Settings → Keyboard & Mouse → Mouse → Report mouse to apps**. ## `tty7 capture … | head -1` printed a Rust panic diff --git a/docs/terminal/mouse-and-scrolling.mdx b/docs/terminal/mouse-and-scrolling.mdx index e5871290..7ddffee4 100644 --- a/docs/terminal/mouse-and-scrolling.mdx +++ b/docs/terminal/mouse-and-scrolling.mdx @@ -24,7 +24,7 @@ running through it. ## The pointer -Under **Settings → Terminal → Mouse**: +Under **Settings → Keyboard & Mouse → Mouse**: | Setting | Default | What it does | |---|---|---| diff --git a/src/ui/i18n/en.rs b/src/ui/i18n/en.rs index fa451851..6d446525 100644 --- a/src/ui/i18n/en.rs +++ b/src/ui/i18n/en.rs @@ -9,7 +9,7 @@ pub fn translate_en(key: L10nKey) -> &'static str { L10nKey::SettingsEditShortcuts => "Edit shortcuts…", L10nKey::SettingsModifiedOnly => "Modified only", L10nKey::SettingsModified => "Modified", - L10nKey::SettingsResetValue => "Reset setting", + L10nKey::SettingsResetValue => "Reset to default", L10nKey::SettingsSearchResults => "Search results", L10nKey::SettingsOpenSetting => "Open setting", L10nKey::SettingsNoModified => "No modified settings match this filter.", @@ -75,7 +75,7 @@ pub fn translate_en(key: L10nKey) -> &'static str { L10nKey::RememberKeychain => "Remember (keychain)", L10nKey::Cancel => "Cancel", L10nKey::Close => "Close", - L10nKey::QuitStopServerTitle => "Quit and Stop Server?", + L10nKey::QuitStopServerTitle => "Quit and stop tty7 server?", L10nKey::QuitStopServerBody => { "This quits tty7 and stops tty7 server; anything running in your shells is terminated. Your tabs and layout reopen with fresh shells next launch. (Closing the window only retires tty7 to the tray — the shells keep running.)" } @@ -403,7 +403,7 @@ pub fn translate_en(key: L10nKey) -> &'static str { L10nKey::SettingsOff => "Off", L10nKey::SettingsShell => "Shell", L10nKey::SettingsShellIntro => { - "The program each new terminal launches. Leave Program empty to use the platform default ({default})." + "The program each new terminal launches. Leave Shell program empty to use the platform default ({default})." } L10nKey::SettingsProgram => "Shell program", L10nKey::SettingsProgramDesc => { @@ -535,7 +535,7 @@ pub fn translate_en(key: L10nKey) -> &'static str { L10nKey::SettingsOptionAsMetaDesc => { "⌥+key sends the escape chord shells expect (⌥B = back one word) instead of typing a special character (∫)." } - L10nKey::SettingsAgentsIntro => "Agents", + L10nKey::SettingsAgentsIntro => "AI agents", L10nKey::SettingsAgentsIntroDesc => { "Hooks give panes running these agents live status (working / waiting / done) in the tab bar. Only inside tty7." } diff --git a/src/ui/i18n/ja.rs b/src/ui/i18n/ja.rs index 4dc09e82..52a56077 100644 --- a/src/ui/i18n/ja.rs +++ b/src/ui/i18n/ja.rs @@ -76,9 +76,9 @@ pub fn translate_ja(key: L10nKey) -> Option<&'static str> { L10nKey::RememberKeychain => "キーチェーンに保存", L10nKey::Cancel => "キャンセル", L10nKey::Close => "閉じる", - L10nKey::QuitStopServerTitle => "tty7 を終了してサーバーを停止しますか?", + L10nKey::QuitStopServerTitle => "tty7 を終了して tty7 server を停止しますか?", L10nKey::QuitStopServerBody => { - "tty7 を終了してtty7 serverを停止します。シェルで実行中のものはすべて終了します。タブとレイアウトは次回起動時に新しいシェルで開きます。(ウィンドウを閉じるだけならトレイに退避し、シェルは動き続けます)" + "tty7 を終了して tty7 server を停止します。シェルで実行中のものはすべて終了します。タブとレイアウトは次回起動時に新しいシェルで開きます。(ウィンドウを閉じるだけならトレイに退避し、シェルは動き続けます)" } L10nKey::QuitAndStop => "終了して停止", L10nKey::CloseSshConnectionTitle => "この SSH 接続を閉じますか?", @@ -409,7 +409,7 @@ pub fn translate_ja(key: L10nKey) -> Option<&'static str> { L10nKey::SettingsOff => "オフ", L10nKey::SettingsShell => "シェル", L10nKey::SettingsShellIntro => { - "新しいターミナルで起動するプログラム。空欄ならプラットフォーム既定の {default} を使います" + "新しいターミナルで起動するプログラム。「シェルプログラム」を空欄にすると、プラットフォーム既定の {default} を使います。" } L10nKey::SettingsProgram => "シェルプログラム", L10nKey::SettingsProgramDesc => { @@ -543,7 +543,7 @@ pub fn translate_ja(key: L10nKey) -> Option<&'static str> { L10nKey::SettingsOptionAsMetaDesc => { "⌥+キーでシェルが期待するエスケープシーケンス(⌥B = 単語 1 つ戻る)を送信し、特殊文字(∫)を入力しない" } - L10nKey::SettingsAgentsIntro => "エージェント", + L10nKey::SettingsAgentsIntro => "AI エージェント", L10nKey::SettingsAgentsIntroDesc => { "フックにより、これらのエージェントを実行するペインの状態(作業中 / 待機中 / 完了)がタブバーに表示されます。tty7 内でのみ有効" } @@ -706,16 +706,16 @@ pub fn translate_ja(key: L10nKey) -> Option<&'static str> { } L10nKey::SettingsUpdateChannelStable => "安定版", L10nKey::SettingsUpdateChannelNightly => "ナイトリー", - L10nKey::SettingsDaemonStale => "tty7 serverは {build} のままです。", + L10nKey::SettingsDaemonStale => "tty7 server は {build} のままです。", L10nKey::SettingsDaemonStaleDesc => { - "tty7 はその場で更新されました。アプリは新しく、ペインはまだ以前のビルドのtty7 serverが処理しています。再起動すると新しいビルドに切り替わり、ペインで動いているプロセスはすべて終了します。急ぐ必要はなく、ペインが空いているときにどうぞ" + "tty7 はその場で更新されました。アプリは新しく、ペインはまだ以前のビルドの tty7 server が処理しています。再起動すると新しいビルドに切り替わり、ペインで動いているプロセスはすべて終了します。急ぐ必要はなく、ペインが空いているときにどうぞ" } L10nKey::UpdateDialogTitle => "アップデートがあります", L10nKey::UpdateDialogDetail => { - "tty7 {version} が利用できます(現在 {current})。インストールするとアプリが再起動します。tty7 serverは動いたままなので、ペインの中身は残ります" + "tty7 {version} が利用できます(現在 {current})。インストールするとアプリが再起動します。tty7 server は動いたままなので、ペインの中身は残ります" } L10nKey::UpdateDialogDetailWindows => { - "tty7 {version} が利用できます(現在 {current})。インストールするとアプリとtty7 serverが再起動します。ペインのプロセスは終了し、タブとレイアウトは新しいシェルで復元されます" + "tty7 {version} が利用できます(現在 {current})。インストールするとアプリと tty7 server が再起動します。ペインのプロセスは終了し、タブとレイアウトは新しいシェルで復元されます" } L10nKey::UpdateDialogDetailManual => { "tty7 {version} が利用できます(現在 {current})。{hint}" @@ -767,7 +767,7 @@ pub fn translate_ja(key: L10nKey) -> Option<&'static str> { L10nKey::SettingsServerDesc => { "このコンピューターのターミナルセッションを管理し、バックグラウンドで実行し続けます。" } - L10nKey::SettingsRestartServer => "tty7 serverを再起動…", + L10nKey::SettingsRestartServer => "tty7 server を再起動…", L10nKey::SettingsAppHttpProxy => "アップデート用プロキシ", L10nKey::SettingsAppHttpProxyDesc => { "tty7 自身の更新チェックとダウンロードにのみ使用し、ペインで実行中のプログラムには影響しません。空欄ならシステムのプロキシに従います" @@ -1009,7 +1009,7 @@ pub fn translate_ja(key: L10nKey) -> Option<&'static str> { L10nKey::SwitcherNoMatch => "一致するワークスペースまたはマシンがありません", L10nKey::AddSshHost => "SSH ホストを追加…", L10nKey::ClickForNewWindow => "クリックで新しいウィンドウを開く", - L10nKey::RestartServer => "サーバーを再起動", + L10nKey::RestartServer => "tty7 server を再起動", L10nKey::OtherMachines => "その他のマシン", L10nKey::Ok => "OK", L10nKey::SftpNoTransfers => "転送はまだありません", @@ -1357,7 +1357,7 @@ pub fn translate_ja(key: L10nKey) -> Option<&'static str> { } L10nKey::RemoteThisComputer => "このコンピュータ", L10nKey::RemoteProfileGone => "削除されたプロファイル", - L10nKey::RemoteRestartTitle => "「{machine}」上の tty7 サーバーを再起動しますか?", + L10nKey::RemoteRestartTitle => "「{machine}」上の tty7 serverを再起動しますか?", L10nKey::RemoteRestartBody => { "{machine} 上のシェルは、表示されていないものも含めてすべて終了します。ワークスペースとレイアウトは保持され、新しいシェルで開きます" } @@ -1365,13 +1365,13 @@ pub fn translate_ja(key: L10nKey) -> Option<&'static str> { "tty7 は {machine} に対応するサーバーをインストールして起動します。\n\n{machine} で実行中のすべてのセッションが終了します。このウィンドウが接続していないセッションも含みます" } L10nKey::RemoteRestartFailedTitle => { - "「{machine}」上の tty7 サーバーは再起動されませんでした" + "「{machine}」上の tty7 serverは再起動されませんでした" } L10nKey::RemoteRestartFailedBody => { "{error}\n\nそこで実行中のセッションは古いビルドのままです。セッションがなくなっている場合は、再接続時にこのビルドのサーバーが起動します" } L10nKey::RemoteHostUnreachable => "{machine} に到達できませんでした: {error}", - L10nKey::RemoteInstallTitle => "「{machine}」に tty7 サーバーをインストールしますか?", + L10nKey::RemoteInstallTitle => "「{machine}」に tty7 serverをインストールしますか?", L10nKey::RemoteInstallDetail => { "tty7 はサーバーバイナリを {machine} に書き込み、{machine} でワークスペースをホストできるようにします。{machine} 上の他のものには触れず、sudo も使いません。\n\n{path_label}\u{2003}{path}\n{version_label}\u{2003}{version}\n{size_label}\u{2003}{size}\n{from_label}\u{2003}{from}\n{sha_label}\u{2003}{sha256}\n\n{silent_upgrades}" } @@ -1384,7 +1384,7 @@ pub fn translate_ja(key: L10nKey) -> Option<&'static str> { "このマシンでの今後のアップグレードはサイレントにインストールされます" } L10nKey::RemoteInstallBytes => "バイト", - L10nKey::RemoteMismatchTitle => "「{machine}」上の tty7 サーバーを更新しますか?", + L10nKey::RemoteMismatchTitle => "「{machine}」上の tty7 serverを更新しますか?", L10nKey::RemoteMismatchDetail => { "{machine} はサーバー {running} で動いていますが、このクライアント({wanted})はそのプロトコルを話せません。対応するサーバーはインストール済みですが、セッションは実行中のサーバー上にあります。\n\n{replace_server}\u{2003}{wanted} に置き換え、そのサーバー上のセッションをすべて終了します。\n{cancel}\u{2003}{machine} はそのままです。このウィンドウは接続しません" } @@ -1393,10 +1393,10 @@ pub fn translate_ja(key: L10nKey) -> Option<&'static str> { L10nKey::RemoteMismatchUnknownBuild => "不明なビルド", L10nKey::RemoteMismatchUnknownBuildFromExe => "不明なビルド({exe} から)", L10nKey::RemoteServerOutdated => { - "{machine} の tty7 サーバーが古く({build})、この tty7 からは通信できません。更新すると接続できます" + "{machine} の tty7 serverが古く({build})、この tty7 からは通信できません。更新すると接続できます" } L10nKey::RemoteServerTooNew => { - "{machine} の tty7 サーバー({build})は、この tty7 より新しいバージョンです。このコンピューターの tty7 を更新するか、向こうのサーバーを対応するものに置き換えてください" + "{machine} の tty7 server({build})は、この tty7 より新しいバージョンです。このコンピューターの tty7 を更新するか、向こうのサーバーを対応するものに置き換えてください" } L10nKey::RemoteDaemonStartFailed => { "tty7 のローカルサーバーを起動できませんでした: {error}" @@ -1414,13 +1414,13 @@ pub fn translate_ja(key: L10nKey) -> Option<&'static str> { "ローカルの --stdio ワークスペースには SSH 接続がありません" } L10nKey::RemoteHostNotTty7 => { - "{machine} は応答しましたが、tty7 サーバーとしては応答しませんでした: {error}" + "{machine} は応答しましたが、tty7 serverとしては応答しませんでした: {error}" } L10nKey::RemoteWorkspaceListFailed => { "{machine} に接続しましたが、ワークスペースの一覧を取得できませんでした: {error}" } L10nKey::RemoteServerRestartFailed => { - "{machine} 上の tty7 サーバーを再起動できませんでした: {error}" + "{machine} 上の tty7 serverを再起動できませんでした: {error}" } L10nKey::RemoteNoRouteToHost => "tty7 は {machine} に到達する手段を失いました", L10nKey::RemoteMachineTreeUnexpectedReply => { @@ -1490,7 +1490,7 @@ pub fn translate_ja(key: L10nKey) -> Option<&'static str> { L10nKey::IoBusy => "他のプログラムが使用中です。", L10nKey::IoTimedOut => "時間内に応答がありませんでした。", L10nKey::TreeWindowOpenedEmpty => { - "サーバーがこのウィンドウのタブを渡さなかったため、空のまま開きました。失われたものはなく、応答すれば戻ります。戻らない場合はコマンドパレットの「サーバーを再起動」を実行してください" + "サーバーがこのウィンドウのタブを渡さなかったため、空のまま開きました。失われたものはなく、応答すれば戻ります。戻らない場合はコマンドパレットの「tty7 server を再起動」を実行してください" } L10nKey::CmdGroupTabsPanes => "タブとペイン", L10nKey::CmdGroupWorkspaces => "ワークスペース", @@ -1602,7 +1602,7 @@ pub fn translate_ja(key: L10nKey) -> Option<&'static str> { L10nKey::CmdDocumentation => "ドキュメント", L10nKey::CmdJoinDiscord => "Discord に参加", L10nKey::CmdReportIssue => "問題を報告…", - L10nKey::CmdRestartServer => "サーバーを再起動…", + L10nKey::CmdRestartServer => "tty7 server を再起動…", L10nKey::CmdRestartServerSubtitle => "実行中のすべてのシェルを終了し、レイアウトは保持", L10nKey::CmdQuitTty7 => "tty7 を終了", L10nKey::CmdQuitTty7Subtitle => "サーバーを停止し、実行中のすべてのシェルを終了", @@ -1610,22 +1610,22 @@ pub fn translate_ja(key: L10nKey) -> Option<&'static str> { L10nKey::CmdQuickConnectSaveProfile => "「{target}」をプロファイルとして保存…", L10nKey::CmdRecent => "最近", L10nKey::AppRestartServerTitle => "tty7 server を再起動しますか?", - L10nKey::AppRestartServerFailed => "tty7 serverを再起動できませんでした: {error}", + L10nKey::AppRestartServerFailed => "tty7 server を再起動できませんでした: {error}", L10nKey::AppRestartServerMismatchDetail => { - "tty7 serverはプロトコル {protocol}(ビルド v{build})、このアプリは {ours} のため、タブを取り出せません。\n\n終了:何も変わりません。tty7 serverもシェルも動き続けます。\n再起動:タブは新しいシェルで戻り、いま実行中のものは終了します" + "tty7 server はプロトコル {protocol}(ビルド v{build})、このアプリは {ours} のため、タブを取り出せません。\n\n終了:何も変わりません。tty7 server もシェルも動き続けます。\n再起動:タブは新しいシェルで戻り、いま実行中のものは終了します" } L10nKey::AppRestartServerDialectDetail => { - "tty7 serverは制御方言 v{dialect}(ビルド v{build})、このアプリは v{ours} のため、ウィンドウはどれも空で開きます。\n\n終了:何も変わりません。tty7 serverもシェルも動き続けます。\n再起動:タブは新しいシェルで戻り、いま実行中のものは終了します" + "tty7 server は制御方言 v{dialect}(ビルド v{build})、このアプリは v{ours} のため、ウィンドウはどれも空で開きます。\n\n終了:何も変わりません。tty7 server もシェルも動き続けます。\n再起動:タブは新しいシェルで戻り、いま実行中のものは終了します" } L10nKey::AppRestartServerDialectNewerDetail => { - "tty7 serverは制御方言 v{dialect}(ビルド v{build})、このアプリは v{ours} のため、ウィンドウはどれも空で開きます。\n\n終了して新しいビルドを入れる:根本的な解決で、シェルはそのまま残ります。\n再起動:タブは新しいシェルで戻り、いま実行中のものは終了します" + "tty7 server は制御方言 v{dialect}(ビルド v{build})、このアプリは v{ours} のため、ウィンドウはどれも空で開きます。\n\n終了して新しいビルドを入れる:根本的な解決で、シェルはそのまま残ります。\n再起動:タブは新しいシェルで戻り、いま実行中のものは終了します" } L10nKey::AppRestartServerOldDetail => { - "tty7 serverはバージョン照合より前のもので、何を話すか分かりません。\n\n終了:何も変わりません。tty7 serverもシェルも動き続けます。\n再起動:タブは新しいシェルで戻り、いま実行中のものは終了します" + "tty7 server はバージョン照合より前のもので、何を話すか分かりません。\n\n終了:何も変わりません。tty7 server もシェルも動き続けます。\n再起動:タブは新しいシェルで戻り、いま実行中のものは終了します" } L10nKey::AppRestart => "再起動", L10nKey::AppRestartServerNoServer => { - "{label} には再起動できるtty7 serverがありません。このコンピュータが --stdio で実行しているプログラムです。代わりにワークスペースを止めてください" + "{label} には再起動できる tty7 server がありません。このコンピュータが --stdio で実行しているプログラムです。代わりにワークスペースを止めてください" } L10nKey::AppRestartServerBody => { "このコンピュータのシェルはすべて終了します。タブとレイアウトは保持され、新しいシェルで開きます" @@ -1777,10 +1777,10 @@ pub fn translate_ja(key: L10nKey) -> Option<&'static str> { L10nKey::Replace => "置き換える", L10nKey::SftpErrorInvalidOctalMode => "無効な 8 進数モードです", L10nKey::SettingsDaemonStaleDescInPlace => { - "tty7 はその場で更新されました。アプリは新しく、ペインはまだ前のビルドで動いています。tty7 serverは停止せずに新しいビルドへ置き換えられるので、シェルはそのまま引き継がれます。tty7 内蔵の SSH クライアントを使うペインだけは例外で、その接続は閉じられ、開き直しが必要です" + "tty7 はその場で更新されました。アプリは新しく、ペインはまだ前のビルドで動いています。tty7 server は停止せずに新しいビルドへ置き換えられるので、シェルはそのまま引き継がれます。tty7 内蔵の SSH クライアントを使うペインだけは例外で、その接続は閉じられ、開き直しが必要です" } L10nKey::AppRestartServerBodyInPlace => { - "tty7 serverは停止せずに自分自身をこのビルドへ置き換えます。シェルは動いたままで、ウィンドウはすぐに再接続します。tty7 内蔵の SSH クライアントを使うペインだけは例外で、その接続は閉じられ、開き直しが必要です" + "tty7 server は停止せずに自分自身をこのビルドへ置き換えます。シェルは動いたままで、ウィンドウはすぐに再接続します。tty7 内蔵の SSH クライアントを使うペインだけは例外で、その接続は閉じられ、開き直しが必要です" } L10nKey::PaneRestoredScreenBanner => { "復元された画面 — 以下は新しいシェルで、これより上のものは動いていません" @@ -1871,7 +1871,7 @@ pub fn translate_ja(key: L10nKey) -> Option<&'static str> { L10nKey::AppMenuKeyboardShortcuts => "キーボードショートカット", L10nKey::AppMenuJoinDiscord => "Discord に参加", L10nKey::AppMenuReportIssue => "問題を報告…", - L10nKey::AppMenuRestartServer => "tty7 serverを再起動…", + L10nKey::AppMenuRestartServer => "tty7 server を再起動…", L10nKey::WindowUntitled => "無題", L10nKey::TrayShowTty7 => "tty7 を表示", L10nKey::TrayNotifications => "通知", diff --git a/src/ui/i18n/mod.rs b/src/ui/i18n/mod.rs index 0a261b16..3b08c2bd 100644 --- a/src/ui/i18n/mod.rs +++ b/src/ui/i18n/mod.rs @@ -1602,7 +1602,6 @@ mod tests { // translation here would be less clear, not more. L10nKey::SettingsShell, L10nKey::CmdGroupAgents, - L10nKey::SettingsAgentsIntro, ]; for &key in KEPT_IN_ENGLISH { diff --git a/src/ui/i18n/zh.rs b/src/ui/i18n/zh.rs index 355a17d1..37e40fa3 100644 --- a/src/ui/i18n/zh.rs +++ b/src/ui/i18n/zh.rs @@ -15,7 +15,7 @@ pub fn translate_zh(key: L10nKey) -> Option<&'static str> { L10nKey::SettingsUnsavedTitle => "离开前保存更改?", L10nKey::SettingsUnsavedBody => "可以保存更改、放弃更改,或继续编辑。", L10nKey::SettingsSaveChanges => "保存更改", - L10nKey::SettingsThemeDraft => "主题更改正在预览,保存后才会写入文件。", + L10nKey::SettingsThemeDraft => "正在预览主题更改。保存以保留,取消以还原。", L10nKey::SettingsSaveError => "无法保存更改:{error}", L10nKey::SettingsRetrySave => "重新保存", @@ -70,9 +70,9 @@ pub fn translate_zh(key: L10nKey) -> Option<&'static str> { L10nKey::RememberKeychain => "记住(钥匙串)", L10nKey::Cancel => "取消", L10nKey::Close => "关闭", - L10nKey::QuitStopServerTitle => "退出并停止 server?", + L10nKey::QuitStopServerTitle => "退出并停止 tty7 server?", L10nKey::QuitStopServerBody => { - "这会退出 tty7 并停止tty7 server,shell 里正在跑的东西都会被终止。标签页和布局会保留,下次启动时以全新的 shell 打开。(只关窗口的话应用收进托盘,shell 继续跑。)" + "这会退出 tty7 并停止 tty7 server,shell 里正在跑的东西都会被终止。标签页和布局会保留,下次启动时以全新的 shell 打开。(只关窗口的话应用收进托盘,shell 继续跑。)" } L10nKey::QuitAndStop => "退出并停止", L10nKey::CloseSshConnectionTitle => "关闭这个 SSH 连接?", @@ -247,8 +247,8 @@ pub fn translate_zh(key: L10nKey) -> Option<&'static str> { "在关闭带有活动 SSH 会话的标签页或窗格前请求确认。" } L10nKey::SettingsNewHost => "新主机", - L10nKey::SettingsDiscardChangesTitle => "丢弃未保存的改动?", - L10nKey::SettingsDiscardChangesBody => "你正在编辑的连接有还没保存的改动。", + L10nKey::SettingsDiscardChangesTitle => "放弃未保存的更改?", + L10nKey::SettingsDiscardChangesBody => "当前连接有未保存的更改。", L10nKey::SettingsKeepEditing => "继续编辑", L10nKey::SettingsName => "名称", L10nKey::SettingsNameDesc => "此连接的标签。", @@ -363,7 +363,7 @@ pub fn translate_zh(key: L10nKey) -> Option<&'static str> { L10nKey::SettingsOff => "关", L10nKey::SettingsShell => "Shell", L10nKey::SettingsShellIntro => { - "每个新终端启动的程序。将“程序”留空可使用平台默认值({default})。" + "每个新终端启动的程序。将“Shell 程序”留空可使用平台默认值({default})。" } L10nKey::SettingsProgram => "Shell 程序", L10nKey::SettingsProgramDesc => "PATH 中的可执行文件名或绝对路径,例如 zsh、fish、pwsh。", @@ -384,8 +384,8 @@ pub fn translate_zh(key: L10nKey) -> Option<&'static str> { "仅适用于没有目录可继承的 shell,例如窗口的第一个标签页。新标签页和分屏仍继承活动窗格的目录,已打开的 shell 继续运行。" } L10nKey::SettingsScrolling => "滚动", - L10nKey::SettingsScrollback => "终端输出历史", - L10nKey::SettingsScrollbackDesc => "每个窗格保留的历史行数。仅适用于新窗格。", + L10nKey::SettingsScrollback => "终端输出历史行数", + L10nKey::SettingsScrollbackDesc => "每个窗格保留的终端输出行数。仅适用于新窗格。", L10nKey::SettingsScrollSpeed => "滚动速度", L10nKey::SettingsScrollSpeedDesc => "应用于鼠标滚轮滚动的倍率。", L10nKey::SettingsSmoothScroll => "平滑滚动", @@ -476,11 +476,11 @@ pub fn translate_zh(key: L10nKey) -> Option<&'static str> { L10nKey::SettingsOptionAsMetaDesc => { "⌥+按键 发送 shell 期望的转义组合键(⌥B = 后退一个词),而不是输入特殊字符(∫)。" } - L10nKey::SettingsAgentsIntro => "Agents", + L10nKey::SettingsAgentsIntro => "AI agent", L10nKey::SettingsAgentsIntroDesc => { - "hook 让跑这些 agent 的窗格在标签栏实时显示状态(进行中 / 等待中 / 已完成)。仅在 tty7 内生效。" + "安装 hook,让运行 AI agent 的窗格在标签栏显示实时状态(进行中 / 等待中 / 已完成)。仅在 tty7 内生效。" } - L10nKey::SettingsReadingAgentConfig => "正在读取这台机器的 agent 配置…", + L10nKey::SettingsReadingAgentConfig => "正在读取这台机器的 AI agent 配置…", L10nKey::SettingsStatusNotInstalled => "未安装", L10nKey::SettingsStatusInstalled => "已安装", L10nKey::SettingsStatusOutdated => "已过时", @@ -579,7 +579,7 @@ pub fn translate_zh(key: L10nKey) -> Option<&'static str> { L10nKey::KeybindForkSessionLeft => "向左 Fork 会话", L10nKey::KeybindForkSessionDown => "向下 Fork 会话", L10nKey::KeybindForkSessionUp => "向上 Fork 会话", - L10nKey::SettingsAboutDesc1 => "终端工作台:常驻会话、远程工作、agent。", + L10nKey::SettingsAboutDesc1 => "终端工作台:持久会话、远程开发、AI agent。", L10nKey::SettingsDefaultTerminal => "默认终端", L10nKey::SettingsDefaultTerminalDesc => { "将 tty7 设为 Unix 可执行文件、SSH 链接和 man 页面链接的 macOS 默认终端。tty7 仍可打开文件夹和脚本,但不会替换 Finder 的文件夹处理程序。自行指定终端的应用可能不会遵循此设置。" @@ -615,20 +615,20 @@ pub fn translate_zh(key: L10nKey) -> Option<&'static str> { } L10nKey::SettingsUpdateChannel => "更新通道", L10nKey::SettingsUpdateChannelDesc => { - "Stable 跟随正式发布的版本,Nightly 跟随每晚从最新代码构建的版本——更新更快,但没有经过发布测试。" + "稳定版(Stable)跟随正式发布的版本;每夜构建(Nightly)跟随每晚从最新代码构建的版本,更新更快,但未经发布测试。" } L10nKey::SettingsUpdateChannelStable => "稳定版", L10nKey::SettingsUpdateChannelNightly => "每夜构建", L10nKey::SettingsDaemonStale => "tty7 server 仍运行在 {build}。", L10nKey::SettingsDaemonStaleDesc => { - "tty7 是原地升级的:界面已是新版,pane 还由旧版 tty7 server 托管。重启 tty7 server 换成新版,代价是 pane 里正在跑的进程全部结束。不急,挑 pane 空闲时再重启。" + "tty7 是原地升级的:界面已是新版,窗格还由旧版 tty7 server 托管。重启 tty7 server 换成新版,代价是窗格里正在跑的进程全部结束。不急,挑窗格空闲时再重启。" } L10nKey::UpdateDialogTitle => "有可用更新", L10nKey::UpdateDialogDetail => { - "tty7 {version} 已发布,你现在是 {current}。安装会重启应用;tty7 server 不动,pane 里的东西都还在。" + "tty7 {version} 已发布,你现在是 {current}。安装会重启应用;tty7 server 不动,窗格里的东西都还在。" } L10nKey::UpdateDialogDetailWindows => { - "tty7 {version} 已发布,你现在是 {current}。安装会重启应用和tty7 server:pane 里的进程会被结束,标签页和布局以全新的 shell 恢复。" + "tty7 {version} 已发布,你现在是 {current}。安装会重启应用和 tty7 server:窗格里的进程会被结束,标签页和布局以全新的 shell 恢复。" } L10nKey::UpdateDialogDetailManual => "tty7 {version} 已发布,你现在是 {current}。{hint}", L10nKey::UpdateDialogCannotSelfUpdate => "这份安装无法自行更新。", @@ -667,15 +667,15 @@ pub fn translate_zh(key: L10nKey) -> Option<&'static str> { L10nKey::SettingsCheckUpdatesOnLaunch => "启动时检查更新", L10nKey::SettingsCommandLine => "命令行", L10nKey::SettingsCommandLineDesc => { - "让脚本和 AI Agent 使用随应用提供的 tty7 命令。下次启动生效;关闭后不会移除已安装的命令。" + "让脚本和 AI agent 使用随应用提供的 tty7 命令。下次启动生效;关闭后不会移除已安装的命令。" } - L10nKey::SettingsInstallCliOnPath => "将 `tty7` 命令安装到 PATH", + L10nKey::SettingsInstallCliOnPath => "将 tty7 命令安装到 PATH", L10nKey::SettingsServer => "tty7 server", L10nKey::SettingsServerDesc => "管理这台计算机上的终端会话,让会话在后台持续运行。", L10nKey::SettingsRestartServer => "重启 tty7 server…", L10nKey::SettingsAppHttpProxy => "更新代理", L10nKey::SettingsAppHttpProxyDesc => { - "仅用于 tty7 自身的更新检查和下载,不影响面板中运行的程序。留空则跟随系统代理。" + "仅用于 tty7 自身的更新检查和下载,不影响窗格中运行的程序。留空则跟随系统代理。" } L10nKey::SettingsAppHttpProxyInvalid => "不是有效的代理地址,该值未保存。", L10nKey::SettingsAgentClaudeCode => "Claude Code", @@ -910,7 +910,7 @@ pub fn translate_zh(key: L10nKey) -> Option<&'static str> { L10nKey::SwitcherNoMatch => "没有匹配的工作区或机器。", L10nKey::AddSshHost => "添加 SSH 主机…", L10nKey::ClickForNewWindow => "点击打开新窗口", - L10nKey::RestartServer => "重启 server", + L10nKey::RestartServer => "重启 tty7 server", L10nKey::OtherMachines => "其他机器", L10nKey::Ok => "确定", L10nKey::SftpNoTransfers => "还没有传输任务。", @@ -1345,7 +1345,7 @@ pub fn translate_zh(key: L10nKey) -> Option<&'static str> { L10nKey::IoBusy => "有别的程序正占着它。", L10nKey::IoTimedOut => "对方没有在规定时间内响应。", L10nKey::TreeWindowOpenedEmpty => { - "server 没有交出这个窗口的标签页,所以窗口是空的。什么都没丢,它一响应就会回来。如果一直不回来,在命令面板里执行「重启 server」。" + "tty7 server 没有交出这个窗口的标签页,所以窗口是空的。什么都没丢,它一响应就会回来。如果一直不回来,在命令面板里执行「重启 tty7 server」。" } L10nKey::CmdGroupTabsPanes => "标签页与窗格", L10nKey::CmdGroupWorkspaces => "工作区", @@ -1451,13 +1451,13 @@ pub fn translate_zh(key: L10nKey) -> Option<&'static str> { L10nKey::CmdAgentSendGitDiffForReview => "Agent:发送 git diff 以供审查", L10nKey::CmdAgentSendGitDiffSubtitle => "git diff → 运行中的编码 agent", L10nKey::CmdSettings => "设置…", - L10nKey::CmdKeyboardShortcuts => "键盘快捷键", + L10nKey::CmdKeyboardShortcuts => "快捷键", L10nKey::CmdAboutTty7 => "关于 tty7", L10nKey::CmdCheckForUpdates => "检查更新…", L10nKey::CmdDocumentation => "文档", L10nKey::CmdJoinDiscord => "加入 Discord", L10nKey::CmdReportIssue => "报告问题…", - L10nKey::CmdRestartServer => "重启 server…", + L10nKey::CmdRestartServer => "重启 tty7 server…", L10nKey::CmdRestartServerSubtitle => "结束所有运行中的 shell;保留布局", L10nKey::CmdQuitTty7 => "退出 tty7", L10nKey::CmdQuitTty7Subtitle => "停止服务;结束所有运行中的 shell", @@ -1465,7 +1465,7 @@ pub fn translate_zh(key: L10nKey) -> Option<&'static str> { L10nKey::CmdQuickConnectSaveProfile => "将“{target}”保存为主机配置…", L10nKey::CmdRecent => "最近使用", L10nKey::AppRestartServerTitle => "重启 tty7 server?", - L10nKey::AppRestartServerFailed => "无法重启tty7 server:{error}", + L10nKey::AppRestartServerFailed => "无法重启 tty7 server:{error}", L10nKey::AppRestartServerMismatchDetail => { "tty7 server 用协议 {protocol}(构建 v{build}),此应用用 {ours},标签页取不出来。\n\n退出:什么都不变,tty7 server 和 shell 继续运行。\n重启:标签页带全新 shell 回来,现在跑着的东西会被杀掉。" } @@ -1614,17 +1614,17 @@ pub fn translate_zh(key: L10nKey) -> Option<&'static str> { L10nKey::Replace => "覆盖", L10nKey::SftpErrorInvalidOctalMode => "无效的八进制模式", L10nKey::SettingsDaemonStaleDescInPlace => { - "tty7 是原地更新的:应用是新的,面板还跑在旧版上。tty7 server 可以不停机就换成新版,shell 直接延续下来。用 tty7 内置 SSH 客户端的面板除外——那些连接会断开,需要重新打开。" + "tty7 是原地更新的:应用是新的,窗格还跑在旧版上。tty7 server 可以不停机就换成新版,shell 直接延续下来。用 tty7 内置 SSH 客户端的窗格除外——那些连接会断开,需要重新打开。" } L10nKey::AppRestartServerBodyInPlace => { - "tty7 server 会原地把自己换成当前这个版本:shell 继续运行,窗口稍后自动连回去。用 tty7 内置 SSH 客户端的面板除外——那些连接会断开,需要重新打开。" + "tty7 server 会原地把自己换成当前这个版本:shell 继续运行,窗口稍后自动连回去。用 tty7 内置 SSH 客户端的窗格除外——那些连接会断开,需要重新打开。" } L10nKey::PaneRestoredScreenBanner => { "已恢复的画面 —— 下面是新的 shell,上面的内容都已不在运行" } L10nKey::SettingsPerPaneHistory => "各窗格使用独立命令历史", L10nKey::SettingsPerPaneHistoryDescription => { - "上方向键翻的是这个面板里跑过的命令,而不是所有面板混在一起。新面板从已有历史开始,关闭时把新增的写回去。只对 tty7 能接管的 bash 和 zsh 面板生效;用你自己参数启动的 shell 不受影响。" + "上方向键翻的是这个窗格里跑过的命令,而不是所有窗格混在一起。新窗格从已有历史开始,关闭时把新增的写回去。只对 tty7 能接管的 bash 和 zsh 窗格生效;用你自己参数启动的 shell 不受影响。" } L10nKey::IntegrationNoticeBlocked => { "“{wrapper}”截获了此窗格的 shell 上报,内联补全和 Ctrl+R 菜单不可用。shell 自带的历史搜索仍可使用。" @@ -1703,7 +1703,7 @@ pub fn translate_zh(key: L10nKey) -> Option<&'static str> { L10nKey::AppMenuRevealInFolder => "打开所在文件夹", L10nKey::AppMenuCopyLinkPath => "复制路径", L10nKey::AppMenuDocumentation => "tty7 文档", - L10nKey::AppMenuKeyboardShortcuts => "键盘快捷键", + L10nKey::AppMenuKeyboardShortcuts => "快捷键", L10nKey::AppMenuJoinDiscord => "加入 Discord", L10nKey::AppMenuReportIssue => "报告问题…", L10nKey::AppMenuRestartServer => "重启 tty7 server…",