diff --git a/docs/next/CHANGELOG.md b/docs/next/CHANGELOG.md index 5ec122d5c7..022705ff62 100644 --- a/docs/next/CHANGELOG.md +++ b/docs/next/CHANGELOG.md @@ -8,6 +8,7 @@ - The plugin marketplace now discovers valid manifests at repository roots and subdirectories, groups multiple plugins under each repository, and publishes their versions and exact default-branch commits. ### Fixed +- Prefix-mode keybindings can use Escape as the RHS key (for example `copy_mode = "prefix+esc"`, matching tmux `bind Escape copy-mode`). Unbound Escape still cancels prefix mode. - Configs containing the retired Herdr-written `ui.agent_panel_scope` setting no longer report it as an unknown key after upgrades. (#2292) - Claude Code confirmation prompts using `Enter to confirm · Esc to cancel` now report `blocked` instead of `idle`. (#2268) - Sidebar agent lists keep scrolling when differently sized clients are attached to the same session. (#2255, thanks @aiworkflowpro) diff --git a/docs/next/website/src/content/docs/configuration.mdx b/docs/next/website/src/content/docs/configuration.mdx index ce4db4e2be..7819cd259e 100644 --- a/docs/next/website/src/content/docs/configuration.mdx +++ b/docs/next/website/src/content/docs/configuration.mdx @@ -139,7 +139,7 @@ next_tab = ["prefix+n", "ctrl+alt+]"] Optional actions are unset by default. Bind them with `prefix+` for prefix-mode behavior, or use an explicit modified chord when you intentionally want a direct shortcut. -Key strings accept plain keys, modifier combinations such as `ctrl+a`, `shift+n`, `alt+1`, `cmd+k`, and special keys such as `enter`, `tab`, `esc`, `left`, `right`, `up`, and `down`. Named punctuation such as `minus`, `comma`, `ampersand`, `plus`, and `backtick` is also accepted. Plain direct printable keys such as `n` are unsafe because they intercept typing; use `prefix+n` unless you intentionally want a direct binding. The `navigate_workspace_*` and `navigate_pane_*` fields are navigate-mode-only and may use plain keys such as `j` or `k`; they must not use `prefix+`, `esc`, `enter`, `tab`, `shift+tab`, `left`, `right`, or unmodified `1` through `9`. Left and right arrows are permanent aliases for pane-left and pane-right navigation. These navigate-mode shortcuts are independent from general action bindings such as `focus_pane_down = "prefix+j"`; when both use the same key, the navigate-mode shortcut wins while navigate mode is open. Alt, Cmd/Super, and punctuation with modifiers depend on your terminal and tmux settings. +Key strings accept plain keys, modifier combinations such as `ctrl+a`, `shift+n`, `alt+1`, `cmd+k`, and special keys such as `enter`, `tab`, `esc`/`escape`, `left`, `right`, `up`, and `down`. Named punctuation such as `minus`, `comma`, `ampersand`, `plus`, and `backtick` is also accepted. Plain direct printable keys such as `n` are unsafe because they intercept typing; use `prefix+n` unless you intentionally want a direct binding. Escape may be used as a prefix-mode RHS key (for example `copy_mode = "prefix+esc"`, like tmux `bind Escape copy-mode`); when Escape is not bound, it still cancels prefix mode. The `navigate_workspace_*` and `navigate_pane_*` fields are navigate-mode-only and may use plain keys such as `j` or `k`; they must not use `prefix+`, `esc`, `enter`, `tab`, `shift+tab`, `left`, `right`, or unmodified `1` through `9`. Left and right arrows are permanent aliases for pane-left and pane-right navigation. These navigate-mode shortcuts are independent from general action bindings such as `focus_pane_down = "prefix+j"`; when both use the same key, the navigate-mode shortcut wins while navigate mode is open. Alt, Cmd/Super, and punctuation with modifiers depend on your terminal and tmux settings. If you have old custom keybindings and want the new defaults, run `herdr config reset-keys`. Herdr backs up `config.toml`, removes `[keys]` and `[[keys.command]]`, and uses built-in v2 defaults after restart or `herdr server reload-config`. diff --git a/docs/next/website/src/content/docs/ja/configuration.mdx b/docs/next/website/src/content/docs/ja/configuration.mdx index 9f7c0e5e2a..dc6c09fc3d 100644 --- a/docs/next/website/src/content/docs/ja/configuration.mdx +++ b/docs/next/website/src/content/docs/ja/configuration.mdx @@ -139,7 +139,7 @@ next_tab = ["prefix+n", "ctrl+alt+]"] 任意のアクションはデフォルトでは未設定です。プレフィックスモードの挙動には `prefix+` でバインドし、意図的に直接ショートカットにする場合は明示的な修飾キーコードを使ってください。 -キー文字列には、通常のキー、`ctrl+a`、`shift+n`、`alt+1`、`cmd+k` のような修飾キーの組み合わせ、`enter`、`tab`、`esc`、`left`、`right`、`up`、`down` のような特殊キーが使えます。`minus`、`comma`、`ampersand`、`plus`、`backtick` のような名前付き記号も使えます。`n` のような通常の印字可能キーの直接バインドは入力を妨げるため危険です。意図的に直接バインドするのでなければ `prefix+n` を使ってください。`navigate_workspace_*` と `navigate_pane_*` のフィールドはナビゲートモード専用で、`j` や `k` のような通常のキーを使えます。これらには `prefix+`、`esc`、`enter`、`tab`、`shift+tab`、`left`、`right`、修飾なしの `1` から `9` は使えません。左右の矢印キーは、左ペインと右ペインへのナビゲーションの恒久的なエイリアスです。これらのナビゲートモードショートカットは、`focus_pane_down = "prefix+j"` のような一般アクションのバインドから独立しています。両方に同じキーが使われている場合、ナビゲートモードが開いている間はナビゲートモードのショートカットが優先されます。Alt、Cmd/Super、修飾キー付き記号は、ターミナルと tmux の設定に依存します。 +キー文字列には、通常のキー、`ctrl+a`、`shift+n`、`alt+1`、`cmd+k` のような修飾キーの組み合わせ、`enter`、`tab`、`esc`/`escape`、`left`、`right`、`up`、`down` のような特殊キーが使えます。`minus`、`comma`、`ampersand`、`plus`、`backtick` のような名前付き記号も使えます。`n` のような通常の印字可能キーの直接バインドは入力を妨げるため危険です。意図的に直接バインドするのでなければ `prefix+n` を使ってください。Escape はプレフィックスモードの右側キーとしても使えます(例: `copy_mode = "prefix+esc"`。tmux の `bind Escape copy-mode` に相当)。バインドされていない場合、Escape はこれまでどおりプレフィックスモードを解除します。`navigate_workspace_*` と `navigate_pane_*` のフィールドはナビゲートモード専用で、`j` や `k` のような通常のキーを使えます。これらには `prefix+`、`esc`、`enter`、`tab`、`shift+tab`、`left`、`right`、修飾なしの `1` から `9` は使えません。左右の矢印キーは、左ペインと右ペインへのナビゲーションの恒久的なエイリアスです。これらのナビゲートモードショートカットは、`focus_pane_down = "prefix+j"` のような一般アクションのバインドから独立しています。両方に同じキーが使われている場合、ナビゲートモードが開いている間はナビゲートモードのショートカットが優先されます。Alt、Cmd/Super、修飾キー付き記号は、ターミナルと tmux の設定に依存します。 古いカスタムキーバインドを使っていて新しいデフォルトが欲しい場合は、`herdr config reset-keys` を実行してください。Herdr は `config.toml` をバックアップし、`[keys]` と `[[keys.command]]` を削除し、再起動または `herdr server reload-config` の後に組み込みの v2 デフォルトを使います。 diff --git a/docs/next/website/src/content/docs/zh-cn/configuration.mdx b/docs/next/website/src/content/docs/zh-cn/configuration.mdx index 0cb9ce4222..74e58b9ba3 100644 --- a/docs/next/website/src/content/docs/zh-cn/configuration.mdx +++ b/docs/next/website/src/content/docs/zh-cn/configuration.mdx @@ -139,7 +139,7 @@ next_tab = ["prefix+n", "ctrl+alt+]"] 可选动作默认不设置。使用 `prefix+` 可获得前缀模式行为;当你确实需要直接快捷键时,请使用显式的修饰组合键。 -按键字符串支持普通按键、`ctrl+a`、`shift+n`、`alt+1`、`cmd+k` 等修饰组合键,以及 `enter`、`tab`、`esc`、`left`、`right`、`up`、`down` 等特殊键。也支持 `minus`、`comma`、`ampersand`、`plus`、`backtick` 等命名标点。直接绑定 `n` 这样的普通可打印键并不安全,因为它会拦截输入;除非你有意设置直接绑定,否则请使用 `prefix+n`。`navigate_workspace_*` 和 `navigate_pane_*` 字段仅在导航模式中生效,可以使用 `j` 或 `k` 等普通按键;它们不得使用 `prefix+`、`esc`、`enter`、`tab`、`shift+tab`、`left`、`right`,也不得使用无修饰键的 `1` 到 `9`。左右方向键是向左和向右导航窗格的永久别名。这些导航模式快捷键独立于 `focus_pane_down = "prefix+j"` 等通用动作绑定;两者使用同一按键时,打开导航模式后,导航模式快捷键优先。Alt、Cmd/Super 以及带修饰键的标点取决于你的终端和 tmux 设置。 +按键字符串支持普通按键、`ctrl+a`、`shift+n`、`alt+1`、`cmd+k` 等修饰组合键,以及 `enter`、`tab`、`esc`/`escape`、`left`、`right`、`up`、`down` 等特殊键。也支持 `minus`、`comma`、`ampersand`、`plus`、`backtick` 等命名标点。直接绑定 `n` 这样的普通可打印键并不安全,因为它会拦截输入;除非你有意设置直接绑定,否则请使用 `prefix+n`。Escape 可以用作前缀模式的右侧按键(例如 `copy_mode = "prefix+esc"`,类似 tmux 的 `bind Escape copy-mode`);未绑定时,Escape 仍会取消前缀模式。`navigate_workspace_*` 和 `navigate_pane_*` 字段仅在导航模式中生效,可以使用 `j` 或 `k` 等普通按键;它们不得使用 `prefix+`、`esc`、`enter`、`tab`、`shift+tab`、`left`、`right`,也不得使用无修饰键的 `1` 到 `9`。左右方向键是向左和向右导航窗格的永久别名。这些导航模式快捷键独立于 `focus_pane_down = "prefix+j"` 等通用动作绑定;两者使用同一按键时,打开导航模式后,导航模式快捷键优先。Alt、Cmd/Super 以及带修饰键的标点取决于你的终端和 tmux 设置。 如果你已有旧的自定义按键绑定,并想使用新的默认值,请运行 `herdr config reset-keys`。Herdr 会备份 `config.toml`,移除 `[keys]` 和 `[[keys.command]]`,并在重启或执行 `herdr server reload-config` 后使用内置的 v2 默认值。 diff --git a/src/app/input/navigate.rs b/src/app/input/navigate.rs index 68941e663f..eb9436a10a 100644 --- a/src/app/input/navigate.rs +++ b/src/app/input/navigate.rs @@ -75,11 +75,6 @@ impl App { return; } - if key.code == KeyCode::Esc { - leave_command_mode(&mut self.state); - return; - } - if let Some(action) = non_indexed_action_for_key(&self.state, &raw_key, BindingDispatch::Prefix) { @@ -3033,6 +3028,81 @@ navigate_pane_down = "ctrl+j" assert_eq!(app.state.mode, Mode::Terminal); } + #[tokio::test] + async fn unbound_prefix_escape_exits_prefix_mode() { + let mut app = app_with_test_workspaces(&["test"]); + app.state.mode = Mode::Terminal; + + app.handle_key(TerminalKey::new( + app.state.prefix_code, + app.state.prefix_mods, + )) + .await; + assert_eq!(app.state.mode, Mode::Prefix); + + app.handle_key(TerminalKey::new(KeyCode::Esc, KeyModifiers::empty())) + .await; + + assert_eq!(app.state.mode, Mode::Terminal); + assert!(app.state.copy_mode.is_none()); + } + + #[tokio::test] + async fn prefix_escape_copy_mode_binding_enters_copy_mode() { + let mut app = app_with_test_workspaces(&["test"]); + app.state.view.pane_infos = app.state.workspaces[0].tabs[0] + .layout + .panes(ratatui::layout::Rect::new(0, 0, 80, 24)); + let config: Config = toml::from_str( + r#" +[keys] +copy_mode = "prefix+esc" +"#, + ) + .unwrap(); + app.state.keybinds.copy_mode = config.keybinds().copy_mode; + app.state.mode = Mode::Terminal; + + app.handle_key(TerminalKey::new( + app.state.prefix_code, + app.state.prefix_mods, + )) + .await; + app.handle_key(TerminalKey::new(KeyCode::Esc, KeyModifiers::empty())) + .await; + + assert_eq!(app.state.mode, Mode::Copy); + assert!(app.state.copy_mode.is_some()); + } + + #[tokio::test] + async fn prefix_escape_alias_copy_mode_binding_enters_copy_mode() { + let mut app = app_with_test_workspaces(&["test"]); + app.state.view.pane_infos = app.state.workspaces[0].tabs[0] + .layout + .panes(ratatui::layout::Rect::new(0, 0, 80, 24)); + let config: Config = toml::from_str( + r#" +[keys] +copy_mode = "prefix+escape" +"#, + ) + .unwrap(); + app.state.keybinds.copy_mode = config.keybinds().copy_mode; + app.state.mode = Mode::Terminal; + + app.handle_key(TerminalKey::new( + app.state.prefix_code, + app.state.prefix_mods, + )) + .await; + app.handle_key(TerminalKey::new(KeyCode::Esc, KeyModifiers::empty())) + .await; + + assert_eq!(app.state.mode, Mode::Copy); + assert!(app.state.copy_mode.is_some()); + } + #[tokio::test] async fn prefix_help_matches_enhanced_shifted_question_mark() { let (_api_tx, api_rx) = tokio::sync::mpsc::unbounded_channel(); diff --git a/src/config/keybinds.rs b/src/config/keybinds.rs index 199076f03c..f1a6b28512 100644 --- a/src/config/keybinds.rs +++ b/src/config/keybinds.rs @@ -1591,6 +1591,33 @@ next_tab = "prefix+n" ); } + #[test] + fn copy_mode_accepts_prefix_escape_aliases() { + for raw in ["prefix+esc", "prefix+escape", "prefix+Esc", "prefix+Escape"] { + let config: Config = toml::from_str(&format!( + r#" +[keys] +copy_mode = "{raw}" +"# + )) + .unwrap(); + let kb = config.keybinds(); + assert!( + config.collect_diagnostics().is_empty(), + "unexpected diagnostics for {raw:?}: {:?}", + config.collect_diagnostics() + ); + assert_eq!( + binding_triggers(&kb.copy_mode), + vec![BindingTrigger::Prefix(( + KeyCode::Esc, + KeyModifiers::empty() + ))], + "failed for {raw:?}" + ); + } + } + #[test] fn back_and_forth_keybinds_are_unset_by_default() { let kb = Config::default().keybinds();