Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions docs/next/CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
## Unreleased

### Added
- Optional `keys.resize_pane_left`, `keys.resize_pane_down`, `keys.resize_pane_up`, and `keys.resize_pane_right` bindings now resize the focused pane in one keystroke without entering resize mode.
- Devin CLI, Cursor Agent CLI, MastraCode, Hermes Agent, and Grok CLI integrations now install and run natively on Windows.
- Panes can now route normal right-click gestures to mouse-reporting applications through the pane menu, `herdr pane input`, `pane.input.set`, or the `pane split --right-click pane` launch option.
- `theme.custom.sidebar_bg` can now give the desktop sidebar its own background without changing built-in theme defaults.
Expand Down
10 changes: 9 additions & 1 deletion docs/next/website/src/content/docs/configuration.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -137,7 +137,15 @@ A binding may also be an array when one action needs multiple shortcuts:
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.
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. For example, tmux-style one-keystroke pane resizing without entering resize mode:

```toml
[keys]
resize_pane_left = "ctrl+shift+alt+left"
resize_pane_down = "ctrl+shift+alt+down"
resize_pane_up = "ctrl+shift+alt+up"
resize_pane_right = "ctrl+shift+alt+right"
```

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.

Expand Down
24 changes: 24 additions & 0 deletions docs/next/website/src/data/config-reference.json
Original file line number Diff line number Diff line change
Expand Up @@ -523,6 +523,30 @@
"default": "\"prefix+r\"",
"description": "Enter resize mode."
},
{
"key": "keys.resize_pane_left",
"type": "keybinding",
"default": "unset",
"description": "Resize the focused pane toward the left. Unset by default."
},
{
"key": "keys.resize_pane_down",
"type": "keybinding",
"default": "unset",
"description": "Resize the focused pane downward. Unset by default."
},
{
"key": "keys.resize_pane_up",
"type": "keybinding",
"default": "unset",
"description": "Resize the focused pane upward. Unset by default."
},
{
"key": "keys.resize_pane_right",
"type": "keybinding",
"default": "unset",
"description": "Resize the focused pane toward the right. Unset by default."
},
{
"key": "keys.toggle_sidebar",
"type": "keybinding",
Expand Down
89 changes: 89 additions & 0 deletions src/app/input/navigate.rs
Original file line number Diff line number Diff line change
Expand Up @@ -383,6 +383,22 @@ impl App {
leave_navigate_mode(&mut self.state);
}
NavigateAction::EnterResizeMode => self.state.mode = Mode::Resize,
NavigateAction::ResizePaneLeft => {
self.resize_pane_direction_via_api(NavDirection::Left);
leave_navigate_mode(&mut self.state);
}
NavigateAction::ResizePaneDown => {
self.resize_pane_direction_via_api(NavDirection::Down);
leave_navigate_mode(&mut self.state);
}
NavigateAction::ResizePaneUp => {
self.resize_pane_direction_via_api(NavDirection::Up);
leave_navigate_mode(&mut self.state);
}
NavigateAction::ResizePaneRight => {
self.resize_pane_direction_via_api(NavDirection::Right);
leave_navigate_mode(&mut self.state);
}
NavigateAction::ToggleSidebar => {
self.state.sidebar_collapsed = !self.state.sidebar_collapsed;
leave_navigate_mode(&mut self.state);
Expand Down Expand Up @@ -537,6 +553,17 @@ impl App {
}
}

pub(crate) fn resize_pane_direction_via_api(&mut self, direction: NavDirection) {
self.runtime_pane_resize(
"tui.pane.resize",
crate::api::schema::PaneResizeParams {
pane_id: None,
direction: api_pane_direction(direction),
amount: None,
},
);
}

pub(crate) fn swap_pane_direction_via_api(&mut self, direction: NavDirection) {
if let Some((ws_idx, source, target)) = self.directional_pane_swap_from_view(direction) {
let source_pane_id = self.public_pane_id(ws_idx, source);
Expand Down Expand Up @@ -1367,6 +1394,10 @@ pub(crate) enum NavigateAction {
CopyMode,
Zoom,
EnterResizeMode,
ResizePaneLeft,
ResizePaneDown,
ResizePaneUp,
ResizePaneRight,
ToggleSidebar,
CyclePaneNext,
CyclePanePrevious,
Expand Down Expand Up @@ -1511,6 +1542,10 @@ fn non_indexed_action_for_key(
(&kb.close_pane, NavigateAction::ClosePane),
(&kb.zoom, NavigateAction::Zoom),
(&kb.resize_mode, NavigateAction::EnterResizeMode),
(&kb.resize_pane_left, NavigateAction::ResizePaneLeft),
(&kb.resize_pane_down, NavigateAction::ResizePaneDown),
(&kb.resize_pane_up, NavigateAction::ResizePaneUp),
(&kb.resize_pane_right, NavigateAction::ResizePaneRight),
(&kb.toggle_sidebar, NavigateAction::ToggleSidebar),
(&kb.reload_config, NavigateAction::ReloadConfig),
(
Expand Down Expand Up @@ -1742,6 +1777,22 @@ pub(super) fn execute_navigate_action_in_context(
leave_navigate_mode(state);
}
NavigateAction::EnterResizeMode => state.mode = Mode::Resize,
NavigateAction::ResizePaneLeft => {
state.resize_pane(NavDirection::Left);
leave_navigate_mode(state);
}
NavigateAction::ResizePaneDown => {
state.resize_pane(NavDirection::Down);
leave_navigate_mode(state);
}
NavigateAction::ResizePaneUp => {
state.resize_pane(NavDirection::Up);
leave_navigate_mode(state);
}
NavigateAction::ResizePaneRight => {
state.resize_pane(NavDirection::Right);
leave_navigate_mode(state);
}
NavigateAction::ToggleSidebar => {
state.sidebar_collapsed = !state.sidebar_collapsed;
leave_navigate_mode(state);
Expand Down Expand Up @@ -2569,6 +2620,44 @@ navigate_pane_right = "ctrl+l"
assert_eq!(action, Some(NavigateAction::SwapPaneRight));
}

#[test]
fn terminal_direct_resize_pane_shortcut_maps_to_navigation_action() {
let mut state = state_with_workspaces(&["test"]);
state.keybinds.resize_pane_right =
crate::config::ActionKeybinds::direct("ctrl+shift+alt+right");

let action = terminal_direct_navigation_action(
&state,
TerminalKey::new(
KeyCode::Right,
KeyModifiers::CONTROL | KeyModifiers::SHIFT | KeyModifiers::ALT,
),
);

assert_eq!(action, Some(NavigateAction::ResizePaneRight));
}

#[test]
fn prefix_resize_pane_binding_maps_to_navigation_action() {
let config: Config = toml::from_str(
r#"
[keys]
resize_pane_left = "prefix+shift+left"
"#,
)
.unwrap();
let mut state = state_with_workspaces(&["test"]);
state.keybinds = config.keybinds();

let action = action_for_key(
&state,
TerminalKey::new(KeyCode::Left, KeyModifiers::SHIFT),
BindingDispatch::Prefix,
);

assert_eq!(action, Some(NavigateAction::ResizePaneLeft));
}

#[test]
fn terminal_direct_last_pane_shortcut_maps_to_navigation_action() {
let mut state = state_with_workspaces(&["test"]);
Expand Down
12 changes: 12 additions & 0 deletions src/config/keybinds.rs
Original file line number Diff line number Diff line change
Expand Up @@ -350,6 +350,10 @@ pub struct Keybinds {
pub close_pane: ActionKeybinds,
pub zoom: ActionKeybinds,
pub resize_mode: ActionKeybinds,
pub resize_pane_left: ActionKeybinds,
pub resize_pane_down: ActionKeybinds,
pub resize_pane_up: ActionKeybinds,
pub resize_pane_right: ActionKeybinds,
pub toggle_sidebar: ActionKeybinds,
pub custom_commands: Vec<CustomCommandKeybind>,
}
Expand Down Expand Up @@ -512,6 +516,10 @@ impl Config {
close_pane: empty_action!(),
zoom: empty_action!(),
resize_mode: empty_action!(),
resize_pane_left: empty_action!(),
resize_pane_down: empty_action!(),
resize_pane_up: empty_action!(),
resize_pane_right: empty_action!(),
toggle_sidebar: empty_action!(),
custom_commands: Vec::new(),
};
Expand Down Expand Up @@ -653,6 +661,10 @@ impl Config {
apply_action!(keybinds.close_pane, close_pane, source);
apply_action!(keybinds.zoom, zoom, source);
apply_action!(keybinds.resize_mode, resize_mode, source);
apply_action!(keybinds.resize_pane_left, resize_pane_left, source);
apply_action!(keybinds.resize_pane_down, resize_pane_down, source);
apply_action!(keybinds.resize_pane_up, resize_pane_up, source);
apply_action!(keybinds.resize_pane_right, resize_pane_right, source);
apply_action!(keybinds.toggle_sidebar, toggle_sidebar, source);

if source == field_source!(indexed) {
Expand Down
28 changes: 28 additions & 0 deletions src/config/model.rs
Original file line number Diff line number Diff line change
Expand Up @@ -438,6 +438,14 @@ pub struct KeysConfig {
pub zoom: BindingConfig,
/// Enter resize mode. Default: "prefix+r"
pub resize_mode: BindingConfig,
/// Resize the focused pane toward the left. Unset by default.
pub resize_pane_left: BindingConfig,
/// Resize the focused pane downward. Unset by default.
pub resize_pane_down: BindingConfig,
/// Resize the focused pane upward. Unset by default.
pub resize_pane_up: BindingConfig,
/// Resize the focused pane toward the right. Unset by default.
pub resize_pane_right: BindingConfig,
/// Toggle sidebar collapse. Default: "prefix+b"
pub toggle_sidebar: BindingConfig,
/// Optional indexed shortcuts expanded over number keys 1-9.
Expand Down Expand Up @@ -557,6 +565,14 @@ pub(crate) struct KeysConfigOverlay {
#[serde(skip_serializing_if = "Option::is_none")]
resize_mode: Option<BindingConfig>,
#[serde(skip_serializing_if = "Option::is_none")]
resize_pane_left: Option<BindingConfig>,
#[serde(skip_serializing_if = "Option::is_none")]
resize_pane_down: Option<BindingConfig>,
#[serde(skip_serializing_if = "Option::is_none")]
resize_pane_up: Option<BindingConfig>,
#[serde(skip_serializing_if = "Option::is_none")]
resize_pane_right: Option<BindingConfig>,
#[serde(skip_serializing_if = "Option::is_none")]
toggle_sidebar: Option<BindingConfig>,
#[serde(skip_serializing_if = "Option::is_none")]
indexed: Option<IndexedKeysConfig>,
Expand Down Expand Up @@ -633,6 +649,10 @@ impl<'de> Deserialize<'de> for KeysConfig {
apply_field!(close_pane);
apply_field!(zoom);
apply_field!(resize_mode);
apply_field!(resize_pane_left);
apply_field!(resize_pane_down);
apply_field!(resize_pane_up);
apply_field!(resize_pane_right);
apply_field!(toggle_sidebar);
apply_field!(indexed);
apply_field!(command);
Expand Down Expand Up @@ -731,6 +751,10 @@ impl KeysConfig {
copy_effective_action_field!(close_pane, keybinds.close_pane);
copy_effective_action_field!(zoom, keybinds.zoom);
copy_effective_action_field!(resize_mode, keybinds.resize_mode);
copy_effective_action_field!(resize_pane_left, keybinds.resize_pane_left);
copy_effective_action_field!(resize_pane_down, keybinds.resize_pane_down);
copy_effective_action_field!(resize_pane_up, keybinds.resize_pane_up);
copy_effective_action_field!(resize_pane_right, keybinds.resize_pane_right);
copy_effective_action_field!(toggle_sidebar, keybinds.toggle_sidebar);
copy_user_field!(indexed);

Expand Down Expand Up @@ -1019,6 +1043,10 @@ impl Default for KeysConfig {
close_pane: BindingConfig::one("prefix+x"),
zoom: BindingConfig::one("prefix+z"),
resize_mode: BindingConfig::one("prefix+r"),
resize_pane_left: BindingConfig::empty(),
resize_pane_down: BindingConfig::empty(),
resize_pane_up: BindingConfig::empty(),
resize_pane_right: BindingConfig::empty(),
toggle_sidebar: BindingConfig::one("prefix+b"),
indexed: IndexedKeysConfig::default(),
command: Vec::new(),
Expand Down
4 changes: 4 additions & 0 deletions src/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -213,6 +213,10 @@ const DEFAULT_CONFIG: &str = r##"# herdr configuration
# close_pane = "prefix+x"
# zoom = "prefix+z" # legacy alias: fullscreen
# resize_mode = "prefix+r"
# resize_pane_left = "" # optional, e.g. "ctrl+shift+alt+left" resizes without entering resize mode
# resize_pane_down = "" # optional, e.g. "ctrl+shift+alt+down"
# resize_pane_up = "" # optional, e.g. "ctrl+shift+alt+up"
# resize_pane_right = "" # optional, e.g. "ctrl+shift+alt+right"
# toggle_sidebar = "prefix+b"

# Navigate-mode movement. These local shortcuts win while navigate mode is open.
Expand Down
4 changes: 4 additions & 0 deletions src/ui/keybind_help.rs
Original file line number Diff line number Diff line change
Expand Up @@ -145,6 +145,10 @@ pub(super) fn keybind_help_groups(app: &AppState) -> Vec<HelpGroup> {
help_entry(keybind_label(&kb.copy_mode), "copy mode"),
help_entry(keybind_label(&kb.zoom), "zoom pane"),
help_entry(keybind_label(&kb.resize_mode), "resize mode"),
help_entry(keybind_label(&kb.resize_pane_left), "resize pane left"),
help_entry(keybind_label(&kb.resize_pane_down), "resize pane down"),
help_entry(keybind_label(&kb.resize_pane_up), "resize pane up"),
help_entry(keybind_label(&kb.resize_pane_right), "resize pane right"),
help_entry(keybind_label(&kb.toggle_sidebar), "toggle sidebar"),
help_entry(keybind_label(&kb.focus_pane_left), "focus pane left"),
help_entry(keybind_label(&kb.focus_pane_down), "focus pane down"),
Expand Down
Loading