parent
659badf106
commit
a9ff4012bd
|
|
@ -3,6 +3,7 @@
|
|||
## Unreleased
|
||||
|
||||
### Added
|
||||
- Added `ui.mouse_scroll_lines` to configure how many pane scrollback lines each mouse wheel notch scrolls. The default remains 3. (#236)
|
||||
- Added `--remote-keybindings local|server` for `herdr --remote`. Remote attach now uses the launching client's local keybindings by default without copying config files to the remote host; use `--remote-keybindings server` to keep the remote server's keybindings. The client/server protocol is now version 9.
|
||||
- Added `experimental.reveal_hidden_cursor_for_cjk_ime = false` (opt-in), `experimental.cjk_ime_agents = []` (optional allow-list), and `experimental.cjk_ime_cursor_shape = "steady_block"` to expose the focused pane's cursor anchor to the outer terminal even when the pane requested `?25l`, restoring macOS IME candidate-window tracking for TUIs that paint their own cursor (Claude Code, pi, codex). When `cjk_ime_agents` is non-empty, the reveal applies only to focused panes whose detected agent matches one of the listed names. When the pane reports no cursor position, the anchor falls back to the pane's top-left so a stable IME hint is always available. Trade-off when enabled: an extra hardware cursor may appear in the outer terminal for apps that hide the cursor without painting a replacement. (#149, thanks @ChihGodlee)
|
||||
|
||||
|
|
|
|||
|
|
@ -183,6 +183,7 @@ sidebar_width = 32
|
|||
sidebar_min_width = 18
|
||||
sidebar_max_width = 36
|
||||
mouse_capture = true
|
||||
mouse_scroll_lines = 3
|
||||
confirm_close = true
|
||||
prompt_new_tab_name = true
|
||||
show_agent_labels_on_pane_borders = false
|
||||
|
|
@ -198,6 +199,8 @@ accent = "cyan"
|
|||
|
||||
Set `mouse_capture = false` if you want your terminal to handle normal clicks, such as command-clicking URLs.
|
||||
|
||||
Set `mouse_scroll_lines` to change how many pane scrollback lines each mouse wheel notch scrolls. The default is 3. Pane apps that request mouse reporting still receive wheel events directly.
|
||||
|
||||
Set `show_agent_labels_on_pane_borders = true` if you want detected agent labels in split pane borders when no manual pane label is set.
|
||||
|
||||
## Notifications
|
||||
|
|
|
|||
|
|
@ -1133,7 +1133,7 @@ impl AppState {
|
|||
terminal_runtimes: &TerminalRuntimeRegistry,
|
||||
mouse: MouseEvent,
|
||||
) {
|
||||
const LINES_PER_NOTCH: usize = 3;
|
||||
let lines_per_notch = self.mouse_scroll_lines;
|
||||
|
||||
if let Some(info) = self.pane_at(mouse.column, mouse.row).cloned() {
|
||||
self.focus_pane(info.id);
|
||||
|
|
@ -1142,10 +1142,10 @@ impl AppState {
|
|||
}
|
||||
match mouse.kind {
|
||||
MouseEventKind::ScrollUp => {
|
||||
self.scroll_pane_up(terminal_runtimes, info.id, LINES_PER_NOTCH)
|
||||
self.scroll_pane_up(terminal_runtimes, info.id, lines_per_notch)
|
||||
}
|
||||
MouseEventKind::ScrollDown => {
|
||||
self.scroll_pane_down(terminal_runtimes, info.id, LINES_PER_NOTCH)
|
||||
self.scroll_pane_down(terminal_runtimes, info.id, lines_per_notch)
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
|
|
@ -1156,10 +1156,10 @@ impl AppState {
|
|||
self.focus_pane(info.id);
|
||||
match mouse.kind {
|
||||
MouseEventKind::ScrollUp => {
|
||||
self.scroll_pane_up(terminal_runtimes, info.id, LINES_PER_NOTCH)
|
||||
self.scroll_pane_up(terminal_runtimes, info.id, lines_per_notch)
|
||||
}
|
||||
MouseEventKind::ScrollDown => {
|
||||
self.scroll_pane_down(terminal_runtimes, info.id, LINES_PER_NOTCH)
|
||||
self.scroll_pane_down(terminal_runtimes, info.id, lines_per_notch)
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
|
|
@ -1169,8 +1169,8 @@ impl AppState {
|
|||
if let Some(ws_idx) = self.active {
|
||||
if let Some(rt) = self.focused_runtime_in_workspace(terminal_runtimes, ws_idx) {
|
||||
match mouse.kind {
|
||||
MouseEventKind::ScrollUp => rt.scroll_up(LINES_PER_NOTCH),
|
||||
MouseEventKind::ScrollDown => rt.scroll_down(LINES_PER_NOTCH),
|
||||
MouseEventKind::ScrollUp => rt.scroll_up(lines_per_notch),
|
||||
MouseEventKind::ScrollDown => rt.scroll_down(lines_per_notch),
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
|
|
@ -1385,7 +1385,8 @@ mod tests {
|
|||
use ratatui::layout::{Direction, Rect};
|
||||
|
||||
use super::super::{
|
||||
app_for_mouse_test, capture_snapshot, handle_context_menu_key, mouse, root_layout_ratio,
|
||||
app_for_mouse_test, capture_snapshot, handle_context_menu_key, mouse, numbered_lines_bytes,
|
||||
root_layout_ratio,
|
||||
};
|
||||
use super::*;
|
||||
use crate::{
|
||||
|
|
@ -1394,6 +1395,44 @@ mod tests {
|
|||
workspace::Workspace,
|
||||
};
|
||||
|
||||
#[tokio::test]
|
||||
async fn terminal_wheel_uses_configured_mouse_scroll_lines() {
|
||||
let mut app = app_for_mouse_test();
|
||||
let mut ws = Workspace::test_new("test");
|
||||
let pane_id = ws.tabs[0].root_pane;
|
||||
let pane_infos = ws.tabs[0].layout.panes(Rect::new(26, 2, 80, 18));
|
||||
let info = pane_infos[0].clone();
|
||||
ws.tabs[0].runtimes.insert(
|
||||
pane_id,
|
||||
crate::terminal::TerminalRuntime::test_with_scrollback_bytes(
|
||||
info.inner_rect.width,
|
||||
info.inner_rect.height,
|
||||
16 * 1024,
|
||||
&numbered_lines_bytes(64),
|
||||
),
|
||||
);
|
||||
|
||||
app.state.workspaces = vec![ws];
|
||||
app.state.active = Some(0);
|
||||
app.state.selected = 0;
|
||||
app.state.mode = Mode::Terminal;
|
||||
app.state.view.pane_infos = pane_infos;
|
||||
app.state.mouse_scroll_lines = 7;
|
||||
|
||||
app.handle_mouse(mouse(
|
||||
MouseEventKind::ScrollUp,
|
||||
info.inner_rect.x + 1,
|
||||
info.inner_rect.y + 1,
|
||||
));
|
||||
|
||||
let metrics = app
|
||||
.state
|
||||
.runtime_for_pane_in_workspace(&app.terminal_runtimes, 0, pane_id)
|
||||
.and_then(crate::terminal::TerminalRuntime::scroll_metrics)
|
||||
.expect("scroll metrics after wheel");
|
||||
assert_eq!(metrics.offset_from_bottom, 7);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn hovering_context_menu_updates_highlight() {
|
||||
let mut app = app_for_mouse_test();
|
||||
|
|
|
|||
|
|
@ -144,7 +144,7 @@ impl AppState {
|
|||
terminal_runtimes: &TerminalRuntimeRegistry,
|
||||
mouse: MouseEvent,
|
||||
) -> bool {
|
||||
const LINES_PER_NOTCH: usize = 3;
|
||||
let lines_per_notch = self.mouse_scroll_lines;
|
||||
|
||||
let Some(selection) = self.selection.as_ref() else {
|
||||
return false;
|
||||
|
|
@ -156,10 +156,10 @@ impl AppState {
|
|||
self.focus_pane(pane_id);
|
||||
match mouse.kind {
|
||||
MouseEventKind::ScrollUp => {
|
||||
self.scroll_pane_up(terminal_runtimes, pane_id, LINES_PER_NOTCH)
|
||||
self.scroll_pane_up(terminal_runtimes, pane_id, lines_per_notch)
|
||||
}
|
||||
MouseEventKind::ScrollDown => {
|
||||
self.scroll_pane_down(terminal_runtimes, pane_id, LINES_PER_NOTCH)
|
||||
self.scroll_pane_down(terminal_runtimes, pane_id, lines_per_notch)
|
||||
}
|
||||
_ => return false,
|
||||
}
|
||||
|
|
|
|||
|
|
@ -424,6 +424,7 @@ impl App {
|
|||
sidebar_section_split,
|
||||
agent_panel_scope,
|
||||
mouse_capture: config.ui.mouse_capture,
|
||||
mouse_scroll_lines: config.ui.mouse_scroll_lines(),
|
||||
confirm_close: config.ui.confirm_close,
|
||||
prompt_new_tab_name: config.ui.prompt_new_tab_name,
|
||||
show_agent_labels_on_pane_borders: config.ui.show_agent_labels_on_pane_borders,
|
||||
|
|
@ -903,6 +904,7 @@ impl App {
|
|||
.sidebar_width
|
||||
.clamp(self.state.sidebar_min_width, self.state.sidebar_max_width);
|
||||
self.state.mouse_capture = config.ui.mouse_capture;
|
||||
self.state.mouse_scroll_lines = config.ui.mouse_scroll_lines();
|
||||
self.state.confirm_close = config.ui.confirm_close;
|
||||
self.state.prompt_new_tab_name = config.ui.prompt_new_tab_name;
|
||||
self.state.show_agent_labels_on_pane_borders =
|
||||
|
|
|
|||
|
|
@ -960,6 +960,7 @@ pub struct AppState {
|
|||
/// Capture mouse input for Herdr's own mouse UI. When false, Herdr only
|
||||
/// captures mouse while the focused pane app requests mouse reporting.
|
||||
pub mouse_capture: bool,
|
||||
pub mouse_scroll_lines: usize,
|
||||
pub confirm_close: bool,
|
||||
pub prompt_new_tab_name: bool,
|
||||
pub show_agent_labels_on_pane_borders: bool,
|
||||
|
|
@ -1239,6 +1240,7 @@ impl AppState {
|
|||
sidebar_section_split: 0.5,
|
||||
agent_panel_scope: AgentPanelScope::AllWorkspaces,
|
||||
mouse_capture: true,
|
||||
mouse_scroll_lines: crate::config::DEFAULT_MOUSE_SCROLL_LINES,
|
||||
confirm_close: true,
|
||||
prompt_new_tab_name: true,
|
||||
show_agent_labels_on_pane_borders: false,
|
||||
|
|
|
|||
|
|
@ -29,6 +29,7 @@ pub(crate) use self::io::upsert_top_level_bool;
|
|||
|
||||
pub const CONFIG_PATH_ENV_VAR: &str = "HERDR_CONFIG_PATH";
|
||||
pub const DEFAULT_SCROLLBACK_LIMIT_BYTES: usize = 10_000_000;
|
||||
pub const DEFAULT_MOUSE_SCROLL_LINES: usize = 3;
|
||||
|
||||
#[cfg(test)]
|
||||
pub(crate) fn app_dir_name() -> &'static str {
|
||||
|
|
|
|||
|
|
@ -1,7 +1,10 @@
|
|||
use std::num::NonZeroUsize;
|
||||
|
||||
use serde::{Deserialize, Deserializer, Serialize};
|
||||
|
||||
use super::{
|
||||
BindingConfig, CommandKeybindConfig, SoundConfig, ThemeConfig, DEFAULT_SCROLLBACK_LIMIT_BYTES,
|
||||
BindingConfig, CommandKeybindConfig, SoundConfig, ThemeConfig, DEFAULT_MOUSE_SCROLL_LINES,
|
||||
DEFAULT_SCROLLBACK_LIMIT_BYTES,
|
||||
};
|
||||
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq, Deserialize, Serialize, Default)]
|
||||
|
|
@ -193,6 +196,8 @@ pub struct UiConfig {
|
|||
pub sidebar_max_width: u16,
|
||||
/// Capture mouse input for Herdr's mouse UI. Default: true.
|
||||
pub mouse_capture: bool,
|
||||
/// Lines to scroll per mouse wheel notch. Default: 3.
|
||||
pub mouse_scroll_lines: Option<NonZeroUsize>,
|
||||
/// Ask for confirmation before closing a workspace. Default: true.
|
||||
pub confirm_close: bool,
|
||||
/// Ask for a tab name before creating a new tab. Default: true.
|
||||
|
|
@ -328,6 +333,7 @@ impl Default for UiConfig {
|
|||
sidebar_min_width: 18,
|
||||
sidebar_max_width: 36,
|
||||
mouse_capture: true,
|
||||
mouse_scroll_lines: None,
|
||||
confirm_close: true,
|
||||
prompt_new_tab_name: true,
|
||||
show_agent_labels_on_pane_borders: false,
|
||||
|
|
@ -339,6 +345,14 @@ impl Default for UiConfig {
|
|||
}
|
||||
}
|
||||
|
||||
impl UiConfig {
|
||||
pub fn mouse_scroll_lines(&self) -> usize {
|
||||
self.mouse_scroll_lines
|
||||
.map(NonZeroUsize::get)
|
||||
.unwrap_or(DEFAULT_MOUSE_SCROLL_LINES)
|
||||
}
|
||||
}
|
||||
|
||||
impl Default for ToastConfig {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
|
|
@ -516,6 +530,31 @@ mouse_capture = false
|
|||
assert!(!config.ui.mouse_capture);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn mouse_scroll_lines_defaults_to_three_and_parses() {
|
||||
let default_config = Config::default();
|
||||
assert_eq!(
|
||||
default_config.ui.mouse_scroll_lines(),
|
||||
DEFAULT_MOUSE_SCROLL_LINES
|
||||
);
|
||||
|
||||
let toml = r#"
|
||||
[ui]
|
||||
mouse_scroll_lines = 1
|
||||
"#;
|
||||
let config: Config = toml::from_str(toml).unwrap();
|
||||
assert_eq!(config.ui.mouse_scroll_lines(), 1);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn mouse_scroll_lines_rejects_zero() {
|
||||
let toml = r#"
|
||||
[ui]
|
||||
mouse_scroll_lines = 0
|
||||
"#;
|
||||
assert!(toml::from_str::<Config>(toml).is_err());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn toast_config_parses() {
|
||||
let toml = r#"
|
||||
|
|
|
|||
|
|
@ -159,6 +159,9 @@ const DEFAULT_CONFIG: &str = r##"# herdr configuration
|
|||
# Pane apps like lazygit and btop can still receive mouse when they request it.
|
||||
# mouse_capture = true
|
||||
|
||||
# Pane scrollback lines to scroll per mouse wheel notch.
|
||||
# mouse_scroll_lines = 3
|
||||
|
||||
# Ask for confirmation before closing a workspace
|
||||
# confirm_close = true
|
||||
|
||||
|
|
|
|||
Loading…
Reference in New Issue