fix(tui): add composer_arrows_scroll config option for trackpad terminals
Instead of unconditionally changing Up/Down behavior, gate the
empty-composer-scroll path behind a new `tui.composer_arrows_scroll`
config option (default false). Users whose terminals map trackpad
gestures to arrow keys can opt in via:
[tui]
composer_arrows_scroll = true
When enabled, empty-composer Up/Down scroll the transcript; otherwise
plain arrows always navigate input history (preserving #1117 default).
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
@@ -388,6 +388,12 @@ pub struct TuiConfig {
|
||||
/// - `Never` — suppress all turn-completion notifications.
|
||||
/// - Unset (default) — fall back to the `[notifications]` defaults.
|
||||
pub notification_condition: Option<NotificationCondition>,
|
||||
/// When `true`, plain Up/Down on an empty composer scroll the
|
||||
/// transcript instead of recalling input history. Useful for
|
||||
/// terminals that map trackpad gestures to arrow keys. Default:
|
||||
/// `false` (plain arrows always navigate input history, #1117).
|
||||
#[serde(default)]
|
||||
pub composer_arrows_scroll: Option<bool>,
|
||||
}
|
||||
|
||||
/// High-level notification trigger override. See
|
||||
|
||||
@@ -4644,6 +4644,7 @@ mod terminal_mode_tests {
|
||||
terminal_probe_timeout_ms: None,
|
||||
status_items: None,
|
||||
osc8_links: None,
|
||||
composer_arrows_scroll: None,
|
||||
notification_condition: None,
|
||||
}),
|
||||
..Config::default()
|
||||
@@ -4717,6 +4718,7 @@ mod terminal_mode_tests {
|
||||
terminal_probe_timeout_ms: None,
|
||||
status_items: None,
|
||||
osc8_links: None,
|
||||
composer_arrows_scroll: None,
|
||||
notification_condition: None,
|
||||
}),
|
||||
..Config::default()
|
||||
@@ -4747,6 +4749,7 @@ mod terminal_mode_tests {
|
||||
terminal_probe_timeout_ms: None,
|
||||
status_items: None,
|
||||
osc8_links: None,
|
||||
composer_arrows_scroll: None,
|
||||
notification_condition: None,
|
||||
}),
|
||||
..Config::default()
|
||||
@@ -4828,6 +4831,7 @@ mod terminal_mode_tests {
|
||||
terminal_probe_timeout_ms: None,
|
||||
status_items: None,
|
||||
osc8_links: None,
|
||||
composer_arrows_scroll: None,
|
||||
notification_condition: None,
|
||||
}),
|
||||
..Config::default()
|
||||
|
||||
@@ -720,6 +720,9 @@ pub struct App {
|
||||
pub use_memory: bool,
|
||||
pub use_alt_screen: bool,
|
||||
pub use_mouse_capture: bool,
|
||||
/// When true, plain Up/Down on an empty composer scroll the transcript
|
||||
/// instead of navigating input history (#1117 opt-in).
|
||||
pub composer_arrows_scroll: bool,
|
||||
pub use_bracketed_paste: bool,
|
||||
pub use_paste_burst_detection: bool,
|
||||
#[allow(dead_code)]
|
||||
@@ -1440,6 +1443,11 @@ impl App {
|
||||
collapsed_cell_map: Vec::new(),
|
||||
edit_in_progress: false,
|
||||
lsp_enabled: config.lsp.as_ref().and_then(|l| l.enabled).unwrap_or(true),
|
||||
composer_arrows_scroll: config
|
||||
.tui
|
||||
.as_ref()
|
||||
.and_then(|tui| tui.composer_arrows_scroll)
|
||||
.unwrap_or(false),
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -3591,13 +3591,27 @@ fn handle_composer_history_arrow(
|
||||
return false;
|
||||
}
|
||||
|
||||
// When `composer_arrows_scroll` is enabled and the composer is empty,
|
||||
// plain Up/Down scroll the transcript. This helps terminals that map
|
||||
// trackpad gestures to arrow keys. Otherwise arrows always navigate
|
||||
// input history regardless of composer state (#1117).
|
||||
let scroll_on_empty = app.composer_arrows_scroll && app.input.trim().is_empty();
|
||||
|
||||
match key.code {
|
||||
KeyCode::Up => {
|
||||
app.history_up();
|
||||
if scroll_on_empty {
|
||||
app.scroll_up(1);
|
||||
} else {
|
||||
app.history_up();
|
||||
}
|
||||
true
|
||||
}
|
||||
KeyCode::Down => {
|
||||
app.history_down();
|
||||
if scroll_on_empty {
|
||||
app.scroll_down(1);
|
||||
} else {
|
||||
app.history_down();
|
||||
}
|
||||
true
|
||||
}
|
||||
_ => false,
|
||||
|
||||
@@ -1259,6 +1259,7 @@ fn terminal_probe_timeout_uses_tui_config_and_clamps() {
|
||||
status_items: None,
|
||||
osc8_links: None,
|
||||
notification_condition: None,
|
||||
composer_arrows_scroll: None,
|
||||
}),
|
||||
..Config::default()
|
||||
};
|
||||
@@ -4385,13 +4386,13 @@ fn history_arrow_handles_empty_input() {
|
||||
let mut app = create_test_app();
|
||||
app.input_history.push("previous prompt".to_string());
|
||||
|
||||
// Default: empty composer Up navigates input history (#1117).
|
||||
assert!(handle_composer_history_arrow(
|
||||
&mut app,
|
||||
KeyEvent::new(KeyCode::Up, KeyModifiers::NONE),
|
||||
false,
|
||||
false,
|
||||
));
|
||||
|
||||
assert_eq!(app.input, "previous prompt");
|
||||
}
|
||||
|
||||
@@ -4408,7 +4409,6 @@ fn history_arrow_handles_whitespace_input() {
|
||||
false,
|
||||
false,
|
||||
));
|
||||
|
||||
assert_eq!(app.input, "previous prompt");
|
||||
}
|
||||
|
||||
@@ -4429,6 +4429,54 @@ fn history_arrow_handles_nonempty_input() {
|
||||
assert_eq!(app.input, "previous prompt");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn composer_arrows_scroll_empty_up() {
|
||||
let mut app = create_test_app();
|
||||
app.composer_arrows_scroll = true;
|
||||
|
||||
// Opt-in: empty composer Up scrolls transcript.
|
||||
assert!(handle_composer_history_arrow(
|
||||
&mut app,
|
||||
KeyEvent::new(KeyCode::Up, KeyModifiers::NONE),
|
||||
false,
|
||||
false,
|
||||
));
|
||||
assert_eq!(app.viewport.pending_scroll_delta, -1);
|
||||
assert!(app.input.is_empty());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn composer_arrows_scroll_empty_down() {
|
||||
let mut app = create_test_app();
|
||||
app.composer_arrows_scroll = true;
|
||||
|
||||
assert!(handle_composer_history_arrow(
|
||||
&mut app,
|
||||
KeyEvent::new(KeyCode::Down, KeyModifiers::NONE),
|
||||
false,
|
||||
false,
|
||||
));
|
||||
assert_eq!(app.viewport.pending_scroll_delta, 1);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn composer_arrows_scroll_nonempty_still_navigates_history() {
|
||||
let mut app = create_test_app();
|
||||
app.composer_arrows_scroll = true;
|
||||
app.input = "hello".to_string();
|
||||
app.cursor_position = app.input.chars().count();
|
||||
app.input_history.push("previous prompt".to_string());
|
||||
|
||||
// Even with the option on, non-empty composer still navigates history.
|
||||
assert!(handle_composer_history_arrow(
|
||||
&mut app,
|
||||
KeyEvent::new(KeyCode::Up, KeyModifiers::NONE),
|
||||
false,
|
||||
false,
|
||||
));
|
||||
assert_eq!(app.input, "previous prompt");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn notification_settings_tui_always_keeps_configured_method_no_threshold() {
|
||||
let config = Config {
|
||||
|
||||
Reference in New Issue
Block a user