diff options
Diffstat (limited to 'src/app/input/terminal.rs')
| -rw-r--r-- | src/app/input/terminal.rs | 390 |
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] |
