aboutsummaryrefslogtreecommitdiff
path: root/src/app/input/terminal.rs
diff options
context:
space:
mode:
Diffstat (limited to 'src/app/input/terminal.rs')
-rw-r--r--src/app/input/terminal.rs390
1 files changed, 263 insertions, 127 deletions
diff --git a/src/app/input/terminal.rs b/src/app/input/terminal.rs
index 3e99d0fc..f0e10d9c 100644
--- a/src/app/input/terminal.rs
+++ b/src/app/input/terminal.rs
@@ -41,7 +41,7 @@ impl App {
source_id: InputSourceId,
key: TerminalKey,
) -> Option<TerminalInputTarget> {
- match self.prepare_popup_key_forward(key) {
+ match self.prepare_popup_key_forward(key.clone()) {
PreparedPopupInput::NotOpen => {}
PreparedPopupInput::Consumed => return None,
PreparedPopupInput::Bytes { target, bytes } => {
@@ -66,7 +66,7 @@ impl App {
key: TerminalKey,
) -> Option<PreparedPaneInput> {
let key_event = key.as_key_event();
- if self.try_copy_retained_selection(source_id, key) {
+ if self.try_copy_retained_selection(source_id, key.clone()) {
return None;
}
@@ -74,7 +74,8 @@ impl App {
self.selection_autoscroll_deadline = None;
self.state.update_dismissed = true;
- if let Some(action) = super::terminal_direct_non_indexed_navigation_action(&self.state, key)
+ if let Some(action) =
+ super::terminal_direct_non_indexed_navigation_action(&self.state, &key)
{
debug!(
code = ?key_event.code,
@@ -93,7 +94,7 @@ impl App {
if let Some(binding) = super::navigate::command_for_key(
&self.state,
- key,
+ &key,
super::navigate::BindingDispatch::Direct,
) {
debug!(
@@ -107,7 +108,7 @@ impl App {
return None;
}
- if let Some(action) = super::terminal_direct_indexed_navigation_action(&self.state, key) {
+ if let Some(action) = super::terminal_direct_indexed_navigation_action(&self.state, &key) {
debug!(
code = ?key_event.code,
modifiers = ?key_event.modifiers,
@@ -119,7 +120,7 @@ impl App {
return None;
}
- if self.state.is_prefix_key(key) {
+ if self.state.is_prefix_key(&key) {
self.state.mode = Mode::Prefix;
return None;
}
@@ -189,7 +190,7 @@ impl App {
rt.scroll_reset();
let protocol = rt.keyboard_protocol();
- let bytes = rt.encode_terminal_key(key);
+ let bytes = rt.encode_terminal_key(key.clone());
if matches!(key_event.code, KeyCode::Esc)
|| key_event
@@ -251,7 +252,7 @@ impl App {
return PreparedPopupInput::Consumed;
};
rt.scroll_reset();
- let bytes = rt.encode_terminal_key(key);
+ let bytes = rt.encode_terminal_key(key.clone());
self.state.mode = Mode::Terminal;
if bytes.is_empty() {
PreparedPopupInput::Consumed
@@ -316,7 +317,7 @@ impl App {
let Some(runtime) = self.terminal_input_runtime(target) else {
return false;
};
- let bytes = runtime.encode_terminal_key(key);
+ let bytes = runtime.encode_terminal_key(key.clone());
bytes.is_empty() || runtime.try_send_bytes(Bytes::from(bytes)).is_ok()
}
@@ -328,25 +329,24 @@ impl App {
let Some(runtime) = self.terminal_input_runtime(target) else {
return false;
};
- let bytes = runtime.encode_terminal_key(key);
+ let bytes = runtime.encode_terminal_key(key.clone());
bytes.is_empty() || runtime.send_bytes(Bytes::from(bytes)).await.is_ok()
}
fn take_pressed_keys_for_source(
&mut self,
source_id: crate::app::InputSourceId,
- ) -> Vec<crate::app::PressedTerminalKey> {
- let pressed = self
- .pressed_terminal_keys
- .iter()
- .filter(|((id, _), _)| *id == source_id)
- .map(|(_, pressed)| pressed.clone())
- .collect();
- self.pressed_terminal_keys
- .retain(|(id, _), _| *id != source_id);
- self.suppressed_repeat_keys
- .retain(|(id, _)| *id != source_id);
- pressed
+ ) -> Vec<super::ForwardedInputLease> {
+ self.input_leases.remove_source(source_id)
+ }
+
+ pub(crate) fn release_input_target_headless(&mut self, target: &TerminalInputTarget) {
+ for pressed in self.input_leases.remove_target(target) {
+ let release = pressed
+ .key
+ .with_kind(crossterm::event::KeyEventKind::Release);
+ let _ = self.forward_terminal_key_to_target_headless(&pressed.target, release);
+ }
}
pub(crate) fn release_input_source_headless(&mut self, source_id: crate::app::InputSourceId) {
@@ -375,7 +375,7 @@ impl App {
&mut self,
key: TerminalKey,
) -> Option<TerminalInputTarget> {
- match self.prepare_popup_key_forward(key) {
+ match self.prepare_popup_key_forward(key.clone()) {
PreparedPopupInput::NotOpen => {}
PreparedPopupInput::Consumed => return None,
PreparedPopupInput::Bytes { target, bytes } => {
@@ -1579,65 +1579,155 @@ mod tests {
assert!(rx.try_recv().is_err());
}
- #[tokio::test]
- async fn page_up_scrolls_plain_shell_pane() {
+ fn app_with_plain_scrollback(
+ line_count: usize,
+ ) -> (App, crate::layout::PaneId, crate::layout::PaneInfo) {
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(
+ let mut workspace = Workspace::test_new("test");
+ let pane_id = workspace.tabs[0].root_pane;
+ let pane_infos = workspace.tabs[0].layout.panes(Rect::new(26, 2, 80, 18));
+ let pane_info = pane_infos[0].clone();
+ workspace.tabs[0].runtimes.insert(
pane_id,
crate::terminal::TerminalRuntime::test_with_scrollback_bytes(
- info.inner_rect.width,
- info.inner_rect.height,
+ pane_info.inner_rect.width,
+ pane_info.inner_rect.height,
16 * 1024,
- &numbered_lines_bytes(64),
+ &numbered_lines_bytes(line_count),
),
);
-
- app.state.workspaces = vec![ws];
+ app.state.workspaces = vec![workspace];
app.state.active = Some(0);
app.state.selected = 0;
app.state.mode = Mode::Terminal;
app.state.view.pane_infos = pane_infos;
+ (app, pane_id, pane_info)
+ }
- let start_metrics = app
- .state
+ fn pane_scroll_offset(app: &App, pane_id: crate::layout::PaneId) -> usize {
+ app.state
.runtime_for_pane_in_workspace(&app.terminal_runtimes, 0, pane_id)
.and_then(crate::terminal::TerminalRuntime::scroll_metrics)
- .expect("initial scroll metrics");
- assert_eq!(start_metrics.offset_from_bottom, 0);
+ .expect("pane scroll metrics")
+ .offset_from_bottom
+ }
+
+ fn physical_page_up(repeat_count: u16) -> TerminalKey {
+ TerminalKey::new(KeyCode::PageUp, KeyModifiers::empty()).with_windows_record(
+ crate::input::WindowsKeyRecord {
+ key_down: true,
+ repeat_count,
+ virtual_key_code: 0x21,
+ virtual_scan_code: 0x49,
+ unicode: 0,
+ control_key_state: 0x0100,
+ },
+ )
+ }
+
+ #[tokio::test]
+ async fn page_up_scrolls_plain_shell_pane() {
+ let (mut app, pane_id, pane_info) = app_with_plain_scrollback(64);
+ assert_eq!(pane_scroll_offset(&app, pane_id), 0);
app.handle_terminal_key_headless(TerminalKey::new(KeyCode::PageUp, KeyModifiers::empty()));
- let end_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 PageUp");
assert_eq!(
- end_metrics.offset_from_bottom,
- info.inner_rect.height as usize
+ pane_scroll_offset(&app, pane_id),
+ pane_info.inner_rect.height as usize
);
}
#[tokio::test]
- async fn page_down_returns_to_bottom_after_page_up() {
+ async fn consumed_page_up_preserves_separate_and_grouped_repeats() {
+ let (mut app, pane_id, pane_info) = app_with_plain_scrollback(256);
+ let page_up = physical_page_up(1);
+ app.route_client_events(
+ vec![
+ crate::raw_input::RawInputEvent::Key(page_up.clone()),
+ crate::raw_input::RawInputEvent::Key(
+ page_up.clone().with_kind(KeyEventKind::Repeat),
+ ),
+ crate::raw_input::RawInputEvent::Key(
+ page_up.clone().with_kind(KeyEventKind::Release),
+ ),
+ ],
+ false,
+ );
+
+ assert_eq!(
+ pane_scroll_offset(&app, pane_id),
+ pane_info.inner_rect.height as usize * 2
+ );
+
+ app.route_client_events(
+ vec![crate::raw_input::RawInputEvent::Key(physical_page_up(3))],
+ false,
+ );
+
+ assert_eq!(
+ pane_scroll_offset(&app, pane_id),
+ pane_info.inner_rect.height as usize * 5
+ );
+ }
+
+ #[tokio::test]
+ async fn runtime_consumed_page_up_preserves_separate_and_grouped_repeats() {
+ let (mut app, pane_id, pane_info) = app_with_plain_scrollback(256);
+ let page_up = physical_page_up(1);
+ for key in [
+ page_up.clone(),
+ page_up.clone().with_kind(KeyEventKind::Repeat),
+ page_up.clone().with_kind(KeyEventKind::Release),
+ ] {
+ app.handle_raw_input_event(crate::raw_input::RawInputEvent::Key(key))
+ .await;
+ }
+
+ assert_eq!(
+ pane_scroll_offset(&app, pane_id),
+ pane_info.inner_rect.height as usize * 2
+ );
+
+ app.handle_raw_input_event(crate::raw_input::RawInputEvent::Key(physical_page_up(3)))
+ .await;
+
+ assert_eq!(
+ pane_scroll_offset(&app, pane_id),
+ pane_info.inner_rect.height as usize * 5
+ );
+ }
+
+ #[tokio::test]
+ async fn consumed_repeat_that_becomes_forwarded_acquires_pane_ownership() {
let mut app = app_for_mouse_test();
let mut ws = Workspace::test_new("test");
- let pane_id = ws.tabs[0].root_pane;
+ let first_pane = ws.tabs[0].root_pane;
+ let second_pane = ws.test_split(ratatui::layout::Direction::Horizontal);
let pane_infos = ws.tabs[0].layout.panes(Rect::new(26, 2, 80, 18));
- let info = pane_infos[0].clone();
+ let first_info = pane_infos
+ .iter()
+ .find(|info| info.id == first_pane)
+ .expect("first pane info");
ws.tabs[0].runtimes.insert(
- pane_id,
+ first_pane,
crate::terminal::TerminalRuntime::test_with_scrollback_bytes(
- info.inner_rect.width,
- info.inner_rect.height,
+ first_info.inner_rect.width,
+ first_info.inner_rect.height,
16 * 1024,
- &numbered_lines_bytes(64),
+ &numbered_lines_bytes(128),
),
);
+ let (second_runtime, mut second_rx) =
+ crate::terminal::TerminalRuntime::test_with_channel_and_scrollback_bytes(
+ 80,
+ 24,
+ 0,
+ b"\x1b[?1h\x1b[>15u",
+ 3,
+ );
+ ws.tabs[0].runtimes.insert(second_pane, second_runtime);
+ ws.tabs[0].layout.focus_pane(first_pane);
app.state.workspaces = vec![ws];
app.state.active = Some(0);
@@ -1645,107 +1735,153 @@ mod tests {
app.state.mode = Mode::Terminal;
app.state.view.pane_infos = pane_infos;
- app.handle_terminal_key_headless(TerminalKey::new(KeyCode::PageUp, KeyModifiers::empty()));
- let after_up = app
- .state
- .runtime_for_pane_in_workspace(&app.terminal_runtimes, 0, pane_id)
- .and_then(crate::terminal::TerminalRuntime::scroll_metrics)
- .expect("scroll metrics after PageUp");
- assert!(after_up.offset_from_bottom > 0);
+ let page_up = physical_page_up(1);
+ app.route_client_events(
+ vec![crate::raw_input::RawInputEvent::Key(page_up.clone())],
+ false,
+ );
+ assert!(app.state.focus_pane_in_workspace(0, second_pane));
+ app.route_client_events(
+ vec![crate::raw_input::RawInputEvent::Key(
+ page_up.clone().with_kind(KeyEventKind::Repeat),
+ )],
+ false,
+ );
+ assert!(app.state.focus_pane_in_workspace(0, first_pane));
+ app.route_client_events(
+ vec![
+ crate::raw_input::RawInputEvent::Key(
+ page_up.clone().with_kind(KeyEventKind::Repeat),
+ ),
+ crate::raw_input::RawInputEvent::Key(page_up.with_kind(KeyEventKind::Release)),
+ ],
+ false,
+ );
- app.handle_terminal_key_headless(TerminalKey::new(
- KeyCode::PageDown,
- KeyModifiers::empty(),
- ));
- let after_down = app
- .state
- .runtime_for_pane_in_workspace(&app.terminal_runtimes, 0, pane_id)
- .and_then(crate::terminal::TerminalRuntime::scroll_metrics)
- .expect("scroll metrics after PageDown");
- assert_eq!(after_down.offset_from_bottom, 0);
+ let first_repeat = second_rx.try_recv().expect("first forwarded repeat");
+ let second_repeat = second_rx.try_recv().expect("owned repeat");
+ let release = second_rx.try_recv().expect("owned release");
+ assert_eq!(first_repeat, second_repeat);
+ assert_ne!(second_repeat, release);
+ assert!(second_rx.try_recv().is_err());
+ assert!(app.input_leases.is_empty());
}
#[tokio::test]
- async fn page_up_release_does_not_scroll_plain_shell_pane_again() {
+ async fn missing_popup_runtime_suppresses_grouped_and_later_repeats() {
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),
- ),
- );
-
+ let (runtime, mut input_rx) = crate::terminal::TerminalRuntime::test_with_channel(80, 24);
+ ws.tabs[0].runtimes.insert(pane_id, runtime);
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.handle_terminal_key_headless(TerminalKey::new(KeyCode::PageUp, KeyModifiers::empty()));
- let after_press = app
+ let (popup_runtime, _popup_rx) =
+ crate::terminal::TerminalRuntime::test_with_channel(40, 12);
+ app.install_test_popup_runtime(popup_runtime);
+ let popup_terminal_id = app
.state
- .runtime_for_pane_in_workspace(&app.terminal_runtimes, 0, pane_id)
- .and_then(crate::terminal::TerminalRuntime::scroll_metrics)
- .expect("scroll metrics after PageUp press");
- assert_eq!(
- after_press.offset_from_bottom,
- info.inner_rect.height as usize
+ .popup_pane
+ .as_ref()
+ .expect("popup installed")
+ .terminal_id
+ .clone();
+ app.terminal_runtimes.remove(&popup_terminal_id);
+
+ let key = TerminalKey::new(KeyCode::Char('x'), KeyModifiers::empty()).with_repeat_count(3);
+ app.route_client_events(
+ vec![
+ crate::raw_input::RawInputEvent::Key(key.clone()),
+ crate::raw_input::RawInputEvent::Key(
+ key.clone()
+ .with_kind(KeyEventKind::Repeat)
+ .with_repeat_count(1),
+ ),
+ crate::raw_input::RawInputEvent::Key(key.with_kind(KeyEventKind::Release)),
+ ],
+ false,
);
+ assert!(app.state.popup_pane.is_none());
+ assert!(input_rx.try_recv().is_err());
+ assert!(app.input_leases.is_empty());
+ }
+
+ #[tokio::test]
+ async fn consumed_repeat_stays_suppressed_after_context_returns() {
+ let (mut app, pane_id, _pane_info) = app_with_plain_scrollback(128);
+ let page_up = physical_page_up(1);
+ app.route_client_events(
+ vec![crate::raw_input::RawInputEvent::Key(page_up.clone())],
+ false,
+ );
+ let after_press = pane_scroll_offset(&app, pane_id);
+
+ let (popup_runtime, mut popup_rx) =
+ crate::terminal::TerminalRuntime::test_with_channel(40, 12);
+ app.install_test_popup_runtime(popup_runtime);
+ app.route_client_events(
+ vec![crate::raw_input::RawInputEvent::Key(
+ page_up.clone().with_kind(KeyEventKind::Repeat),
+ )],
+ false,
+ );
+ app.close_popup_pane();
+ app.route_client_events(
+ vec![
+ crate::raw_input::RawInputEvent::Key(
+ page_up.clone().with_kind(KeyEventKind::Repeat),
+ ),
+ crate::raw_input::RawInputEvent::Key(page_up.with_kind(KeyEventKind::Release)),
+ ],
+ false,
+ );
+
+ assert_eq!(pane_scroll_offset(&app, pane_id), after_press);
+ assert!(popup_rx.try_recv().is_err());
+ assert!(app.input_leases.is_empty());
+ }
+
+ #[tokio::test]
+ async fn page_down_returns_to_bottom_after_page_up() {
+ let (mut app, pane_id, _pane_info) = app_with_plain_scrollback(64);
+
+ app.handle_terminal_key_headless(TerminalKey::new(KeyCode::PageUp, KeyModifiers::empty()));
+ assert!(pane_scroll_offset(&app, pane_id) > 0);
+
+ app.handle_terminal_key_headless(TerminalKey::new(
+ KeyCode::PageDown,
+ KeyModifiers::empty(),
+ ));
+ assert_eq!(pane_scroll_offset(&app, pane_id), 0);
+ }
+
+ #[tokio::test]
+ async fn page_up_release_does_not_scroll_plain_shell_pane_again() {
+ let (mut app, pane_id, pane_info) = app_with_plain_scrollback(64);
+
+ app.handle_terminal_key_headless(TerminalKey::new(KeyCode::PageUp, KeyModifiers::empty()));
+ let after_press = pane_scroll_offset(&app, pane_id);
+ assert_eq!(after_press, pane_info.inner_rect.height as usize);
+
app.handle_terminal_key_headless(
TerminalKey::new(KeyCode::PageUp, KeyModifiers::empty())
.with_kind(KeyEventKind::Release),
);
- let after_release = app
- .state
- .runtime_for_pane_in_workspace(&app.terminal_runtimes, 0, pane_id)
- .and_then(crate::terminal::TerminalRuntime::scroll_metrics)
- .expect("scroll metrics after PageUp release");
- assert_eq!(
- after_release.offset_from_bottom,
- after_press.offset_from_bottom
- );
+ assert_eq!(pane_scroll_offset(&app, pane_id), after_press);
}
#[tokio::test]
async fn modified_page_up_does_not_host_scroll_plain_shell_pane() {
- 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;
+ let (mut app, pane_id, _pane_info) = app_with_plain_scrollback(64);
app.handle_terminal_key_headless(TerminalKey::new(KeyCode::PageUp, KeyModifiers::CONTROL));
- 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 modified PageUp");
- assert_eq!(metrics.offset_from_bottom, 0);
+ assert_eq!(pane_scroll_offset(&app, pane_id), 0);
}
#[tokio::test]