feat(prompt): let the shell's own line editor own the prompt (#633)

Closes #624

tty7's inline editor takes the prompt the moment OSC 133 reports one, and
until now the only way to keep it off was to hide the shell's own name
from tty7 so integration never armed — which costs the prompt boundaries,
cwd and exit codes as well. Someone who binds `history-beginning-search-
backward-end` to Up in their zshrc had no way to reach it, and the local
history the editor walks instead is per-view: a command run in one pane is
not in another's list, so the shell's shared history looked broken too.

The new `prompt_editor` switch (Settings -> Input -> Prompt, on by
default) hands the line back. Off, every key at the prompt goes to the
PTY, so ZLE / readline / fish do the editing and what the user bound
behaves as written. Shell integration is untouched by it.

The gate is one line in `input_inactive_reason`, which every path that
could take the prompt from the shell already asks: keys, IME commits,
paste, Tab, the completion and reverse-search menus, the input bar. That
is what makes this a mode rather than a special case per key.

`shell_owns_prompt` learns the flag too, and that half matters more than
it looks: the gap hold and the typeahead record both exist to feed the
local editor, and `flush_typeahead` sends ^U to erase the line before
moving it there — on a line only ZLE is editing, that erases the user's
work. Ctrl-R landing on the PTY also stops raising the missing-integration
notice: the shell owning it is what was asked for.

Turning it off mid-line hands what is typed to the shell the way an
unknown chord does, so the text is still on the prompt to finish. Live
panes follow the switch, including a hand edit of config.json in another
window.

Tab completion and history search are menus tty7 opens inside that editor,
so the page greys them out and says why while it is off. Only their text
dims — a switch already draws its thumb at 35% when disabled, and dimming
the row on top of that leaves a pill with nothing visible in it. Their
stored values are left alone and come back with the editor.

Co-authored-by: l0ng-ai <24760907+l0ng-ai@users.noreply.github.com>
This commit is contained in:
l0ng-ai
2026-08-14 14:46:22 +08:00
committed by GitHub
co-authored by l0ng-ai
parent b3698f4b59
commit 72db26d15a
12 changed files with 410 additions and 13 deletions
+15
View File
@@ -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, <kbd>⇥</kbd> and <kbd>⌃ R</kbd> —
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:
+17
View File
@@ -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"}"#,
+3 -2
View File
@@ -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 <kbd>⇥</kbd>. Off hands the key to the shell. |
| `history_search` | bool | `true` | tty7's fuzzy history on <kbd>⌃ R</kbd>. 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 <kbd></kbd>. Off hands the key to the shell. Needs `prompt_editor`. |
| `history_search` | bool | `true` | tty7's fuzzy history on <kbd>⌃ R</kbd>. 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` | |
+6
View File
@@ -32,6 +32,12 @@ If you already have an fzf, percol, atuin or McFly binding you like, turn off
<kbd>⌃ R</kbd> then goes to the shell, and whatever you bound there keeps
working.
That releases <kbd>⌃ R</kbd> and nothing else — <kbd>↑</kbd> still walks tty7's
own history list. To give the shell the whole prompt, including <kbd>↑</kbd> 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
+37 -2
View File
@@ -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** | <kbd>⇥</kbd> → your shell's completion |
| **Settings → Input → Prompt → History search** | <kbd>⌃ R</kbd> → 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 <kbd>⌃ R</kbd>, 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.
<Tip>
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
<kbd>↑</kbd> — or if a plugin you rely on (zsh-autosuggestions,
zsh-syntax-highlighting, atuin) should own the line instead.
</Tip>
+183 -1
View File
@@ -239,6 +239,12 @@ pub struct TerminalView {
git_status_cwd: Option<std::path::PathBuf>,
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<String>,
@@ -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::<Config>().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<Self>) {
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);
+21
View File
@@ -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>) {
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>) {
self.update_config(cx, |cfg| cfg.tab_completion = on);
}
@@ -5294,6 +5309,7 @@ impl Tty7App {
}
}
let report_mouse = cx.global::<Config>().mouse_reporting;
let prompt_editor = cx.global::<Config>().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);
});
}
}
+11 -1
View File
@@ -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"
}
+11 -1
View File
@@ -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"
}
+4
View File
@@ -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,
+11 -1
View File
@@ -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"
}
+91 -5
View File
@@ -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<String>,
control: AnyElement,
cx: &Context<Self>,
) -> Stateful<Div> {
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<String>,
desc: impl Into<String>,
control: AnyElement,
gated: bool,
cx: &Context<Self>,
) -> Stateful<Div> {
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<Self>) -> AnyElement {
let cfg = cx.global::<Config>();
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::<Config>();
(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::<Config>();
(cfg.prompt_editor, cfg.tab_completion)
});
assert!(prompt_editor);
assert!(tab_completion, "the completion menu comes back with it");
}
}