diff --git a/CHANGELOG.md b/CHANGELOG.md index 14ed7ee6..4fef9522 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -9,6 +9,21 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ### Added +- **Give the prompt back to the shell** — a new **Settings → Input → Prompt → + Prompt editor** switch (`prompt_editor` in `config.json`, on by default). + Turned off, tty7 stops editing the shell prompt: every keystroke there — + printable keys, arrows, IME commits, paste, and ⌃ R — + goes straight to the PTY, so zsh's ZLE, bash's readline and fish's reader own + the line and the keys bound in a dotfile behave exactly as they do outside + tty7, history traversal included. Previously the only way to get there was to + hide the shell's own name from tty7 so integration never armed. Shell + integration is untouched by the switch: prompt boundaries, working directory, + exit codes, notifications and `tty7 procs` keep working. Tab completion and + history search are menus tty7 opens inside that editor, so both grey out + while it is off rather than sitting there doing nothing. It reaches open + panes immediately, and a half-typed line is handed to the shell rather than + dropped on the way over. + - **A host can be proved without spending a tab on it.** **Test** in the SSH host form dials the host exactly as Connect would — proxy, jump host, host key and authentication, all on the daemon — and reports back in place: diff --git a/crates/tty7-core/src/core/config.rs b/crates/tty7-core/src/core/config.rs index 95092e8b..ccbb916e 100644 --- a/crates/tty7-core/src/core/config.rs +++ b/crates/tty7-core/src/core/config.rs @@ -228,6 +228,18 @@ pub struct Config { pub show_tray_icon: bool, #[serde(default, deserialize_with = "de_lenient")] pub bell: BellMode, + /// Whether tty7 edits the shell prompt itself. On by default: the inline + /// editor is what gives a prompt selection, undo, a completion menu and the + /// fuzzy history — none of which a shell's own line editor offers. + /// + /// Off hands every keystroke at the prompt straight to the PTY, so zsh's + /// ZLE / readline / fish own editing again and the keybindings written in a + /// dotfile work exactly as they do outside tty7. Shell integration itself + /// stays on: prompt boundaries, cwd, exit status and notifications are + /// unaffected. `tab_completion` and `history_search` are tty7's own menus, + /// so both are moot while this is off. + #[serde(default = "default_true")] + pub prompt_editor: bool, #[serde(default = "default_true")] pub tab_completion: bool, #[serde(default = "default_true")] @@ -557,6 +569,7 @@ impl Default for Config { restore_session: true, show_tray_icon: true, bell: BellMode::Visual, + prompt_editor: true, tab_completion: true, history_search: true, cursor_style: CursorStyle::Block, @@ -1559,6 +1572,7 @@ mod tests { let cfg = Config::default(); assert!(cfg.restore_session); assert!(cfg.mouse_reporting); + assert!(cfg.prompt_editor); assert!(cfg.tab_completion); assert!(cfg.history_search); assert_eq!(cfg.notify_threshold_secs, 10); @@ -1567,6 +1581,7 @@ mod tests { let cfg: Config = serde_json::from_str(r#"{"font_size": 15.0}"#).unwrap(); assert!(cfg.restore_session); assert!(cfg.mouse_reporting); + assert!(cfg.prompt_editor); assert!(cfg.tab_completion); assert!(cfg.history_search); assert_eq!(cfg.notify_threshold_secs, 10); @@ -1576,6 +1591,8 @@ mod tests { assert!(!cfg.tab_completion); let cfg: Config = serde_json::from_str(r#"{"history_search": false}"#).unwrap(); assert!(!cfg.history_search); + let cfg: Config = serde_json::from_str(r#"{"prompt_editor": false}"#).unwrap(); + assert!(!cfg.prompt_editor); let cfg: Config = serde_json::from_str( r#"{"restore_session": false, "mouse_reporting": false, "bell": "audible"}"#, diff --git a/docs/reference/configuration.mdx b/docs/reference/configuration.mdx index 3c26ce9d..98c13588 100644 --- a/docs/reference/configuration.mdx +++ b/docs/reference/configuration.mdx @@ -108,8 +108,9 @@ their id from the file name. [More about themes →](/customization/themes) | Key | Type | Default | | |---|---|---|---| -| `tab_completion` | bool | `true` | tty7's completion menu on . Off hands the key to the shell. | -| `history_search` | bool | `true` | tty7's fuzzy history on ⌃ R. Off hands the key to the shell. | +| `prompt_editor` | bool | `true` | tty7 edits the shell prompt. Off sends every key at the prompt to the shell, so its own line editor (ZLE, readline) owns editing. Shell integration stays on. | +| `tab_completion` | bool | `true` | tty7's completion menu on . Off hands the key to the shell. Needs `prompt_editor`. | +| `history_search` | bool | `true` | tty7's fuzzy history on ⌃ R. Off hands the key to the shell. Needs `prompt_editor`. | | `smart_select` | bool | `true` | Double-click grabs URLs, paths, bracket pairs, CJK words. | | `word_separators` | string | see below | Characters that end a word. Used when smart selection is off. | | `copy_on_select` | bool | `false` | | diff --git a/docs/terminal/history.mdx b/docs/terminal/history.mdx index c3e875ec..1a5c9225 100644 --- a/docs/terminal/history.mdx +++ b/docs/terminal/history.mdx @@ -32,6 +32,12 @@ If you already have an fzf, percol, atuin or McFly binding you like, turn off ⌃ R then goes to the shell, and whatever you bound there keeps working. +That releases ⌃ R and nothing else — still walks tty7's +own history list. To give the shell the whole prompt, including and +whatever widget you bound to it, turn off +[the prompt editor](/terminal/prompt#giving-the-whole-prompt-back-to-the-shell) +(`prompt_editor: false`). + ## Where the history comes from Your existing shell history file, as-is. Nothing is imported or converted, and a diff --git a/docs/terminal/prompt.mdx b/docs/terminal/prompt.mdx index 5e71f2d4..a7228d06 100644 --- a/docs/terminal/prompt.mdx +++ b/docs/terminal/prompt.mdx @@ -121,10 +121,45 @@ readout, the "command finished" notification, and `tty7 procs` are built on. ## Turning it off -Both prompt features are switches, and turning one off hands its key straight -back to the shell: +Each prompt feature is a switch, and turning one off hands its key straight back +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 | + +### Giving the whole prompt back to the shell + +Turning off **Settings → Input → Prompt → 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 +ZLE, bash's readline, fish's reader. A widget you bound yourself runs exactly as +it does outside tty7: + +```zsh +# ~/.zshrc — works at a tty7 prompt with the editor off +bindkey '^[[A' history-beginning-search-backward-end +``` + +What you give up is the layer this page describes: ghost suggestions, the +completion menu, the fuzzy ⌃ R, the multi-line editor, mouse caret +placement and undo on the prompt. Tab completion and history search grey out in +Settings while it is off — both are menus tty7 opens inside that editor, so +there is nothing left for them to switch. + +What you keep is everything shell integration reports: prompt boundaries, the +working directory, exit codes, "command finished" notifications, the sidebar's +branch readout, `tty7 procs`. The setting moves the line editor, not the +integration. + +It applies to open panes immediately, so there is nothing to restart, and a line +you had half-typed is handed to the shell rather than dropped. + + + Reach for this if the shell's own history traversal matters to you — shared + history between panes, `HIST_FIND_NO_DUPS`, a prefix search bound to + — or if a plugin you rely on (zsh-autosuggestions, + zsh-syntax-highlighting, atuin) should own the line instead. + diff --git a/src/terminal/view.rs b/src/terminal/view.rs index 70d1f6d4..e15ed78d 100644 --- a/src/terminal/view.rs +++ b/src/terminal/view.rs @@ -239,6 +239,12 @@ pub struct TerminalView { git_status_cwd: Option, last_agent_activity: u64, cmd: CmdEditor, + /// Mirrors `Config::prompt_editor`. Cached rather than read from the global + /// because the ownership question ("does this keystroke belong to the local + /// editor?") is asked from `&self` helpers that have no `App` to read from; + /// `report_mouse` is cached for the same reason, and both are refreshed + /// from `reload_from_config` when the config changes under a live pane. + pub(crate) prompt_editor: bool, typeahead: Typeahead, hold: GapHold, history: Vec, @@ -1038,6 +1044,7 @@ impl TerminalView { .as_ref() .map(crate::core::config::gpui_font_features); let report_mouse = config.mouse_reporting; + let prompt_editor = config.prompt_editor; let mut font = gpui::font(font_family); font.fallbacks = Some(gpui::FontFallbacks::from_fonts(fallbacks.clone())); if let Some(features) = &font_features { @@ -1232,6 +1239,7 @@ impl TerminalView { git_status_cwd: None, last_agent_activity: 0, cmd: CmdEditor::new(), + prompt_editor, typeahead: Typeahead::new(), hold: GapHold::new(), history: history.entries, @@ -1738,10 +1746,14 @@ impl TerminalView { return; } + // Ctrl-R landing on the PTY is only worth a notice when the user still + // expects tty7's menu. With the prompt editor off, the shell owning + // Ctrl-R is exactly what was asked for. if m.control && !m.platform && !m.alt && ks.key == "r" + && self.prompt_editor && cx.global::().history_search { self.note_integration_gap(cx); @@ -3411,7 +3423,36 @@ impl TerminalView { self.input_inactive_reason().is_none() } + /// Moves this pane between tty7's inline editor and the shell's own line + /// editor while it is live. + /// + /// A line the user already typed is not dropped on the floor: it is handed + /// to the shell the same way an unknown chord hands one over, so the text + /// is still on the prompt to finish under ZLE/readline. A multi-line draft + /// has nowhere to go on a shell prompt, so it stays in the editor and comes + /// back if the setting is turned on again. + pub(crate) fn set_prompt_editor(&mut self, on: bool, cx: &mut Context) { + if self.prompt_editor == on { + return; + } + if !on && self.input_active() && !self.cmd.text().is_empty() { + self.handoff_line_to_shell(&[], cx); + } + self.prompt_editor = on; + self.close_completion(); + self.reverse_search = None; + cx.notify(); + } + fn input_inactive_reason(&self) -> Option<&'static str> { + // The one gate for `prompt_editor: false`. Every path that could take a + // prompt away from the shell — keys, IME commits, paste, Tab, the + // completion and reverse-search menus, the input bar itself — asks this + // first, so answering here is what makes the mode whole instead of a + // special case per key. + if !self.prompt_editor { + return Some("the inline prompt editor is turned off"); + } if self.terminal.exited { return Some("the shell has exited"); } @@ -3451,7 +3492,11 @@ impl TerminalView { } fn shell_owns_prompt(&self) -> bool { - self.shell_vi_prompt() || self.handoff_active() + // With the prompt editor off the shell owns every prompt, always. That + // is what keeps the gap hold and the typeahead record — both of which + // exist to feed tty7's editor, and one of which erases the line with + // ^U before doing it — away from a line only ZLE is editing. + !self.prompt_editor || self.shell_vi_prompt() || self.handoff_active() } fn on_alt_screen(&self) -> bool { @@ -9839,6 +9884,143 @@ mod gpui_tests { ); } + /// The whole point of `prompt_editor: false`: a prefix and an arrow key + /// reach the PTY at a live OSC 133 prompt, so the shell's own line editor + /// (zsh's ZLE, readline) runs the widget bound there — including a history + /// widget reading the shell's own, shared history. + #[gpui::test] + fn prompt_editor_off_hands_typing_and_arrows_to_the_shell(cx: &mut TestAppContext) { + let (window, mut daemon) = harness(cx); + DaemonMsg::Prompt { + active: true, + at_prompt: true, + last_exit: None, + } + .encode(&mut daemon) + .unwrap(); + // The editor engages first: this is a pane with working shell + // integration, which the mode has to keep working. + wait_for_input_active(&window, cx); + + window + .update(cx, |view, window, cx| { + view.history = ["quit"].into_iter().map(String::from).collect(); + view.history_frecency = vec![0.0; view.history.len()]; + view.set_prompt_editor(false, cx); + assert!( + !view.input_active(), + "the shell owns the prompt in native input mode" + ); + assert!( + view.terminal.at_prompt(), + "OSC 133 prompt tracking stays live" + ); + + type_char(view, "h", window, cx); + let up = KeyDownEvent { + keystroke: key("up"), + is_held: false, + prefer_character_input: false, + }; + view.on_key_down(&up, window, cx); + + assert_eq!(view.cmd.text(), "", "nothing was typed into tty7's editor"); + assert!( + view.history_nav.is_none(), + "Up never touched tty7's own history" + ); + }) + .unwrap(); + + assert_eq!(next_input_until_timeout(&mut daemon), Some(b"h".to_vec())); + assert_eq!( + next_input_until_timeout(&mut daemon), + Some(b"\x1b[A".to_vec()), + "the physical Up key reaches the PTY for ZLE to bind" + ); + + window + .update(cx, |view, _, cx| { + view.set_prompt_editor(true, cx); + assert!( + view.input_active(), + "turning it back on re-arms the editor at the same prompt" + ); + }) + .unwrap(); + } + + /// Tab and Ctrl-R are the two keys with their own opt-outs; turning the + /// editor off has to hand them over too, without the missing-integration + /// notice that Ctrl-R raises when tty7 *wanted* the key and could not have + /// it. + #[gpui::test] + fn prompt_editor_off_hands_over_tab_and_ctrl_r(cx: &mut TestAppContext) { + let (window, mut daemon) = harness(cx); + DaemonMsg::Prompt { + active: true, + at_prompt: true, + last_exit: None, + } + .encode(&mut daemon) + .unwrap(); + wait_for_input_active(&window, cx); + + window + .update(cx, |view, window, cx| { + view.set_prompt_editor(false, cx); + view.created_at = std::time::Instant::now() - INTEGRATION_GRACE * 2; + view.tab_pressed(true, cx); + + let ctrl_r = KeyDownEvent { + keystroke: key("ctrl-r"), + is_held: false, + prefer_character_input: false, + }; + view.on_key_down(&ctrl_r, window, cx); + assert!(view.completion.is_none(), "no tty7 completion menu"); + assert!(view.reverse_search.is_none(), "no tty7 history menu"); + assert!( + view.integration_notice.is_none(), + "the shell owning ^R is what was asked for, not a gap to report" + ); + }) + .unwrap(); + + assert_eq!(next_input_until_timeout(&mut daemon), Some(b"\t".to_vec())); + assert_eq!(next_input_until_timeout(&mut daemon), Some(vec![0x12])); + } + + /// Turning the setting off mid-line must not eat what is already typed — + /// the editor hands the line over the way an unknown chord does, so it is + /// still on the prompt for the shell to finish. + #[gpui::test] + fn turning_the_prompt_editor_off_hands_the_typed_line_over(cx: &mut TestAppContext) { + let (window, mut daemon) = harness(cx); + DaemonMsg::Prompt { + active: true, + at_prompt: true, + last_exit: None, + } + .encode(&mut daemon) + .unwrap(); + wait_for_input_active(&window, cx); + + window + .update(cx, |view, _, cx| { + view.cmd.set("git status"); + view.set_prompt_editor(false, cx); + assert_eq!(view.cmd.text(), "", "the editor let the line go"); + }) + .unwrap(); + + assert_eq!( + next_input_until_timeout(&mut daemon), + Some(b"git status".to_vec()), + "the half-typed line is on the shell's prompt now" + ); + } + #[gpui::test] fn reverse_search_menu_survives_a_real_render_pass(cx: &mut TestAppContext) { let (window, mut daemon) = harness(cx); diff --git a/src/ui/app.rs b/src/ui/app.rs index 1bb1ed04..8e38723a 100644 --- a/src/ui/app.rs +++ b/src/ui/app.rs @@ -2869,6 +2869,21 @@ impl Tty7App { self.update_config(cx, |cfg| cfg.smart_select = on); } + /// Hands the prompt to the shell's own line editor, or takes it back. + /// + /// Live panes carry a cached copy of the flag (see + /// [`crate::terminal::view::TerminalView::prompt_editor`]), so the switch + /// has to reach them: without this, only panes opened afterwards would + /// change hands. + pub(crate) fn set_prompt_editor(&mut self, on: bool, cx: &mut Context) { + self.update_config(cx, |cfg| cfg.prompt_editor = on); + for tab in &self.tabs { + for leaf in tab.pane.terminals() { + leaf.update(cx, |v, cx| v.set_prompt_editor(on, cx)); + } + } + } + pub(crate) fn set_tab_completion(&mut self, on: bool, cx: &mut Context) { self.update_config(cx, |cfg| cfg.tab_completion = on); } @@ -5294,6 +5309,7 @@ impl Tty7App { } } let report_mouse = cx.global::().mouse_reporting; + let prompt_editor = cx.global::().prompt_editor; for tab in &self.tabs { for leaf in tab.pane.terminals() { leaf.update(cx, |v, cx| { @@ -5301,6 +5317,11 @@ impl Tty7App { v.report_mouse = report_mouse; cx.notify(); } + // A hand edit of `config.json` — or another window's + // settings page — has to move a live pane between the + // local editor and ZLE too, not just the window that + // flipped the switch. + v.set_prompt_editor(prompt_editor, cx); }); } } diff --git a/src/ui/i18n/en.rs b/src/ui/i18n/en.rs index 7613adad..8e6243be 100644 --- a/src/ui/i18n/en.rs +++ b/src/ui/i18n/en.rs @@ -452,7 +452,14 @@ pub fn translate_en(key: L10nKey) -> &'static str { L10nKey::SettingsBellModeBoth => "Both", L10nKey::SettingsPrompt => "Prompt", L10nKey::SettingsPromptIntro => { - "tty7's own menus at the shell prompt. Turn one off to hand the key back to the shell." + "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::SettingsPromptEditorDesc => { + "tty7 owns the line you type at the shell prompt: selection, undo, and the menus below. When off, every key, IME commit and paste at the prompt goes straight to the shell, so its own line editor — zsh's ZLE, readline, fish — does the editing and the keys you bound there behave as written. Shell integration stays on either way." + } + L10nKey::SettingsNeedsPromptEditor => { + "Needs the prompt editor: with it off, this key already belongs to the shell." } L10nKey::SettingsTabCompletion => "Tab completion", L10nKey::SettingsTabCompletionDesc => { @@ -841,6 +848,9 @@ pub fn translate_en(key: L10nKey) -> &'static str { L10nKey::SettingsSearchLegiblePaletteKeywords => { "legible contrast bright palette psreadline parameter readable" } + L10nKey::SettingsSearchPromptEditorKeywords => { + "prompt editor native shell input line editor zle readline fish keybindings ime paste" + } L10nKey::SettingsSearchTabBarPositionKeywords => { "tabs vertical sidebar left top layout rail" } diff --git a/src/ui/i18n/ja.rs b/src/ui/i18n/ja.rs index dea441f0..db677de2 100644 --- a/src/ui/i18n/ja.rs +++ b/src/ui/i18n/ja.rs @@ -461,7 +461,14 @@ pub fn translate_ja(key: L10nKey) -> Option<&'static str> { L10nKey::SettingsBellModeBoth => "点滅 + 音声", L10nKey::SettingsPrompt => "プロンプト", L10nKey::SettingsPromptIntro => { - "シェルプロンプトに表示する tty7 独自のメニュー。オフにするとキーはシェルに渡されます" + "シェルプロンプトでの tty7 独自のエディターとメニュー。オフにするとその分がシェルに渡されます" + } + L10nKey::SettingsPromptEditor => "プロンプトエディター", + L10nKey::SettingsPromptEditorDesc => { + "シェルプロンプトで入力する行を tty7 が持ちます — 選択、取り消し、そして下のメニュー。オフにすると、プロンプトでのキー、IME の確定、貼り付けはすべてシェルへ直接渡り、シェル自身の行エディター(zsh の ZLE、readline、fish)が編集を担当するため、そこでバインドしたキーがそのまま動きます。どちらの場合もシェル統合は有効なままです" + } + L10nKey::SettingsNeedsPromptEditor => { + "プロンプトエディターが必要です。オフの間、このキーはすでにシェルのものです" } L10nKey::SettingsTabCompletion => "タブ補完", L10nKey::SettingsTabCompletionDesc => { @@ -887,6 +894,9 @@ pub fn translate_ja(key: L10nKey) -> Option<&'static str> { L10nKey::SettingsSearchLegiblePaletteKeywords => { "可読 コントラスト 明色 パレット パラメーター 修正 legible bright contrast palette parameter" } + L10nKey::SettingsSearchPromptEditorKeywords => { + "プロンプト エディター ネイティブ シェル 入力 行編集 キーバインド 貼り付け prompt editor native shell input zle readline" + } L10nKey::SettingsSearchTabBarPositionKeywords => { "タブ 垂直 サイドバー 左 上 レイアウト レール tab bar position tabs vertical sidebar left top rail" } diff --git a/src/ui/i18n/mod.rs b/src/ui/i18n/mod.rs index 355fa435..47119d6a 100644 --- a/src/ui/i18n/mod.rs +++ b/src/ui/i18n/mod.rs @@ -397,6 +397,9 @@ l10n_keys! { SettingsBellModeBoth, SettingsPrompt, SettingsPromptIntro, + SettingsPromptEditor, + SettingsPromptEditorDesc, + SettingsNeedsPromptEditor, SettingsTabCompletion, SettingsTabCompletionDesc, SettingsHistorySearch, @@ -621,6 +624,7 @@ l10n_keys! { SettingsSearchSmartSelectionKeywords, SettingsSearchStartInKeywords, SettingsSearchSyncWithSystemKeywords, + SettingsSearchPromptEditorKeywords, SettingsSearchTabBarPositionKeywords, SettingsSearchTabCompletionKeywords, SettingsSearchTerminalBellKeywords, diff --git a/src/ui/i18n/zh.rs b/src/ui/i18n/zh.rs index edb0ec19..6a8030fb 100644 --- a/src/ui/i18n/zh.rs +++ b/src/ui/i18n/zh.rs @@ -395,7 +395,14 @@ pub fn translate_zh(key: L10nKey) -> Option<&'static str> { L10nKey::SettingsBellModeBoth => "闪烁 + 声音", L10nKey::SettingsPrompt => "提示符", L10nKey::SettingsPromptIntro => { - "shell 提示符处的 tty7 自带菜单。关闭某项即可把按键交还给 shell。" + "shell 提示符处的 tty7 自带编辑器与菜单。关闭某项即可把这部分交还给 shell。" + } + L10nKey::SettingsPromptEditor => "提示符编辑器", + L10nKey::SettingsPromptEditorDesc => { + "由 tty7 接管你在 shell 提示符上敲的这一行:选择、撤销,以及下面这些菜单。关闭后,提示符处的每个按键、输入法上屏和粘贴都直接交给 shell,由它自己的行编辑器——zsh 的 ZLE、readline、fish——负责编辑,你在那里绑定的键位照常生效。两种模式下 shell 集成都保持开启。" + } + L10nKey::SettingsNeedsPromptEditor => { + "需要提示符编辑器:它关闭时,这个按键本就归 shell 所有。" } L10nKey::SettingsTabCompletion => "Tab 补全", L10nKey::SettingsTabCompletionDesc => { @@ -793,6 +800,9 @@ pub fn translate_zh(key: L10nKey) -> Option<&'static str> { L10nKey::SettingsSearchLegiblePaletteKeywords => { "颜色 低对比度 可读 纠偏 调色板 修正 contrast legible palette parameter" } + L10nKey::SettingsSearchPromptEditorKeywords => { + "提示符编辑器 提示符 编辑器 原生输入 行编辑 键位 快捷键 输入法 粘贴 prompt editor native shell input zle readline" + } L10nKey::SettingsSearchTabBarPositionKeywords => { "标签栏位置 标签栏 侧边栏 左侧 顶部 布局 tab bar position tabs sidebar left top" } diff --git a/src/ui/settings.rs b/src/ui/settings.rs index 33ad7cc4..bddb839f 100644 --- a/src/ui/settings.rs +++ b/src/ui/settings.rs @@ -513,6 +513,11 @@ fn settings_search_entries() -> &'static [SearchEntry] { title: OpenFilesWith, keywords: SettingsSearchOpenFilesWithKeywords, }, + SearchEntry { + section: Input, + title: SettingsPromptEditor, + keywords: SettingsSearchPromptEditorKeywords, + }, SearchEntry { section: Input, title: SettingsTabCompletion, @@ -1924,6 +1929,23 @@ impl Tty7App { desc: impl Into, control: AnyElement, cx: &Context, + ) -> Stateful
{ + self.settings_row_gated_when(label, desc, control, false, cx) + } + + /// The same row, greyed out when `gated` — for a control another setting + /// has switched off, whose own value is still there for when it comes back. + /// + /// Only the text dims. The control draws its own disabled state — a + /// switch's thumb is already down to 35% there — and dimming the whole row + /// on top of that leaves a pill with nothing visible inside it. + pub(crate) fn settings_row_gated_when( + &self, + label: impl Into, + desc: impl Into, + control: AnyElement, + gated: bool, + cx: &Context, ) -> Stateful
{ let theme = cx.theme(); let label = label.into(); @@ -1959,6 +1981,7 @@ impl Tty7App { let labels = v_flex() .gap_0p5() .min_w_0() + .when(gated, |col| col.opacity(0.45)) .child( div() .text_sm() @@ -5410,6 +5433,7 @@ impl Tty7App { fn render_settings_input(&self, cx: &mut Context) -> AnyElement { let cfg = cx.global::(); let option_as_alt = cfg.macos_option_as_alt; + let prompt_editor = cfg.prompt_editor; let tab_completion = cfg.tab_completion; let history_search = cfg.history_search; let per_pane_history = cfg.per_pane_history; @@ -5417,12 +5441,28 @@ impl Tty7App { let copy_on_select = cfg.copy_on_select; let clip_trim = cfg.clipboard_trim_trailing_spaces; + // Tab completion and history search are menus tty7 opens *inside* its + // own prompt editor. With the editor off, both keys already belong to + // the shell, so the switches have nothing left to switch: grey them + // out and say why, rather than leave two controls that quietly do + // nothing. Their stored values are untouched and come back with it. + let gated = |desc: L10nKey| match prompt_editor { + true => t(desc).to_string(), + false => format!("{} {}", t(desc), t(L10nKey::SettingsNeedsPromptEditor)), + }; + + let prompt_editor_switch = crate::ui::theme::switch("term-prompt-editor", cx) + .checked(prompt_editor) + .on_click(cx.listener(|this, on: &bool, _w, cx| this.set_prompt_editor(*on, cx))) + .into_any_element(); let tab_completion_switch = crate::ui::theme::switch("term-tab-completion", cx) .checked(tab_completion) + .disabled(!prompt_editor) .on_click(cx.listener(|this, on: &bool, _w, cx| this.set_tab_completion(*on, cx))) .into_any_element(); let history_search_switch = crate::ui::theme::switch("term-history-search", cx) .checked(history_search) + .disabled(!prompt_editor) .on_click(cx.listener(|this, on: &bool, _w, cx| this.set_history_search(*on, cx))) .into_any_element(); let per_pane_history_switch = crate::ui::theme::switch("term-per-pane-history", cx) @@ -5463,15 +5503,23 @@ impl Tty7App { cx, )) .child(self.settings_row( - t(L10nKey::SettingsTabCompletion), - t(L10nKey::SettingsTabCompletionDesc), - tab_completion_switch, + t(L10nKey::SettingsPromptEditor), + t(L10nKey::SettingsPromptEditorDesc), + prompt_editor_switch, cx, )) - .child(self.settings_row( + .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), - t(L10nKey::SettingsHistorySearchDesc), + gated(L10nKey::SettingsHistorySearchDesc), history_search_switch, + !prompt_editor, cx, )) .child(self.settings_row( @@ -8022,4 +8070,42 @@ mod gpui_tests { "the panel should still be on Appearance after two paint passes", ); } + + /// The Input page paints with the prompt editor off — that is the state + /// where two of its rows are greyed out and their switches disabled — and + /// the cascade only *disables* those two. It must not rewrite what they + /// hold, or turning the editor back on would hand the user a completion + /// menu they had switched off. + #[gpui::test] + fn the_prompt_editor_greys_its_dependants_without_rewriting_them(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::Input, window, cx); + app.set_history_search(false, cx); + app.set_prompt_editor(false, cx); + }); + vcx.simulate_resize(size(px(1100.), px(800.))); + vcx.run_until_parked(); + + let (prompt_editor, tab_completion, history_search) = vcx.update(|_, cx| { + let cfg = cx.global::(); + (cfg.prompt_editor, cfg.tab_completion, cfg.history_search) + }); + assert!(!prompt_editor, "the switch stuck"); + assert!( + tab_completion, + "a greyed-out row keeps its value for when the editor comes back" + ); + assert!(!history_search, "and one the user had turned off stays off"); + + app.update_in(&mut vcx, |app, _, cx| app.set_prompt_editor(true, cx)); + vcx.run_until_parked(); + let (prompt_editor, tab_completion) = vcx.update(|_, cx| { + let cfg = cx.global::(); + (cfg.prompt_editor, cfg.tab_completion) + }); + assert!(prompt_editor); + assert!(tab_completion, "the completion menu comes back with it"); + } }