diff options
| author | Can Celik <ogulcancelik@gmail.com> | 2026-09-01 15:48:22 +0300 |
|---|---|---|
| committer | GitHub <noreply@github.com> | 2026-09-01 15:48:22 +0300 |
| commit | 207be3c771d281baae6e5fa0fb74be9a056e97a2 (patch) | |
| tree | d7b037ecdf629224a048c841d11659efdc23507d /src/app/input/clipboard.rs | |
| parent | 8a6d697308162874a236c84aac0b0f8c7ac01f83 (diff) | |
refactor: render the shell in the client (#3487)
* feat: add opt-in client-rendered shell
* feat: add client-owned shell mouse controls
* feat: complete client-owned shell chrome
* feat: add client-owned shell settings and notifications
* feat: run client shell custom commands
* feat: render popup terminals in client shell
* feat: add client-owned scrollback and copy mode
* fix: preserve client worktree trust defaults
* feat: render startup diagnostics in client shell
* feat: render onboarding in client shell
* feat: render product announcements in client shell
* feat: render release notes in client shell
* feat: render mobile switcher in client shell
* feat: render kitty graphics in client shell
* fix: preserve client shell lifecycle coherence
* refactor: route client shell commands through endpoint connections
* refactor: make the client-rendered shell the default
* refactor: remove monolithic launch mode
* refactor: complete client-rendered shell cutover
* perf: retain client pane surface updates
* perf: scale retained pane surface updates
* perf: remove remote pane input latency
* test: make client shell checks platform-independent
* fix: prevent client timer starvation
* test: fix client shell platform gates
* test: accept retained surfaces in cross-area checks
* test: make client mode assertions platform-neutral
* fix: badge only outdated integrations
Diffstat (limited to 'src/app/input/clipboard.rs')
| -rw-r--r-- | src/app/input/clipboard.rs | 370 |
1 files changed, 0 insertions, 370 deletions
diff --git a/src/app/input/clipboard.rs b/src/app/input/clipboard.rs deleted file mode 100644 index 095b944d..00000000 --- a/src/app/input/clipboard.rs +++ /dev/null @@ -1,370 +0,0 @@ -use crossterm::event::{KeyCode, KeyModifiers}; - -use crate::{ - app::{App, InputSourceId}, - input::TerminalKey, -}; - -use super::{ConsumedInputLease, InputLeaseKey}; - -fn is_retained_selection_copy_key(key: &TerminalKey) -> bool { - matches!(key.code, KeyCode::Char('c' | 'C')) - && matches!(key.modifiers, KeyModifiers::CONTROL | KeyModifiers::SUPER) -} - -impl App { - pub(super) fn dispatch_pending_clipboard_write(&mut self) -> bool { - let Some(content) = self.state.request_clipboard_write.take() else { - return false; - }; - if self - .event_tx - .try_send(crate::events::AppEvent::ClipboardWrite { content }) - .is_err() - { - tracing::warn!("failed to queue clipboard write event"); - } - true - } - - pub(super) fn try_copy_retained_selection( - &mut self, - source_id: InputSourceId, - key: TerminalKey, - ) -> bool { - if self.state.copy_on_select - || !is_retained_selection_copy_key(&key) - || !self - .state - .selection - .as_ref() - .is_some_and(crate::selection::Selection::is_visible) - { - return false; - } - - self.state.copy_selection(&self.terminal_runtimes); - self.selection_autoscroll_deadline = None; - if !self.dispatch_pending_clipboard_write() { - return false; - } - - self.input_leases.insert_consumed( - InputLeaseKey::new(source_id, &key), - ConsumedInputLease::SuppressRepeats, - ); - true - } -} - -#[cfg(test)] -mod tests { - use bytes::Bytes; - use crossterm::event::{KeyCode, KeyEventKind, KeyModifiers, MouseButton, MouseEventKind}; - use ratatui::layout::Rect; - - use super::super::{app_for_mouse_test, mouse}; - use super::*; - use crate::{app::Mode, events::AppEvent, workspace::Workspace}; - - fn app_with_screen_bytes_and_input( - bytes: &[u8], - ) -> ( - App, - crate::layout::PaneInfo, - tokio::sync::mpsc::Receiver<Bytes>, - ) { - 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(); - let (runtime, input_rx) = - crate::terminal::TerminalRuntime::test_with_channel_and_scrollback_bytes( - info.inner_rect.width, - info.inner_rect.height, - 0, - bytes, - 4, - ); - ws.insert_test_runtime(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, info, input_rx) - } - - fn drag_select_range( - app: &mut App, - info: &crate::layout::PaneInfo, - start_col: u16, - end_col: u16, - ) { - let row = info.inner_rect.y; - let start_col = info.inner_rect.x + start_col; - let end_col = info.inner_rect.x + end_col; - app.handle_mouse(mouse( - MouseEventKind::Down(MouseButton::Left), - start_col, - row, - )); - app.handle_mouse(mouse(MouseEventKind::Drag(MouseButton::Left), end_col, row)); - app.handle_mouse(mouse(MouseEventKind::Up(MouseButton::Left), end_col, row)); - } - - fn clipboard_write_content(app: &mut App) -> Vec<u8> { - match app.event_rx.try_recv().expect("clipboard write event") { - AppEvent::ClipboardWrite { content } => content, - event => panic!("unexpected event: {event:?}"), - } - } - - fn assert_visible_selection(app: &App) { - assert!(app - .state - .selection - .as_ref() - .is_some_and(crate::selection::Selection::is_visible)); - } - - #[tokio::test] - async fn copy_on_select_disabled_ctrl_c_copies_and_clears_retained_selection() { - let (mut app, info, mut input_rx) = app_with_screen_bytes_and_input(b"alpha beta"); - app.state.copy_on_select = false; - drag_select_range(&mut app, &info, 0, 4); - assert_visible_selection(&app); - assert!(app.event_rx.try_recv().is_err()); - - let ctrl_c = TerminalKey::new(KeyCode::Char('c'), KeyModifiers::CONTROL) - .with_windows_record(crate::input::WindowsKeyRecord { - key_down: true, - repeat_count: 1, - virtual_key_code: 0x43, - virtual_scan_code: 0x2e, - unicode: 'c' as u16, - control_key_state: 0x0008, - }); - let source_id = 41; - app.route_client_events_from( - source_id, - vec![crate::raw_input::RawInputEvent::Key(ctrl_c.clone())], - false, - ); - - let content = clipboard_write_content(&mut app); - assert_eq!(content, b"alpha"); - assert!(app.state.selection.is_none()); - assert!(input_rx.try_recv().is_err()); - - app.handle_internal_event(AppEvent::ClipboardWrite { content }); - assert_eq!( - app.state - .copy_feedback - .as_ref() - .map(|feedback| feedback.message.as_str()), - Some("copied to clipboard") - ); - - app.route_client_events_from( - source_id, - vec![ - crate::raw_input::RawInputEvent::Key(ctrl_c.clone()), - crate::raw_input::RawInputEvent::Key( - ctrl_c.clone().with_kind(KeyEventKind::Repeat), - ), - ], - false, - ); - assert_eq!(app.input_leases.len(), 1); - assert!(app.event_rx.try_recv().is_err()); - assert!(input_rx.try_recv().is_err()); - - app.route_client_events_from( - source_id, - vec![crate::raw_input::RawInputEvent::Key( - ctrl_c.clone().with_kind(KeyEventKind::Release), - )], - false, - ); - assert!(app.input_leases.is_empty()); - app.route_client_events_from( - source_id, - vec![crate::raw_input::RawInputEvent::Key(ctrl_c.clone())], - false, - ); - let expected = if cfg!(windows) { - b"\x1b[67;46;99;1;8;1_".as_slice() - } else { - b"\x03".as_slice() - }; - assert_eq!( - input_rx.try_recv().expect("forwarded Ctrl-C").as_ref(), - expected - ); - assert!(app.event_rx.try_recv().is_err()); - } - - #[tokio::test] - async fn copy_on_select_disabled_cmd_c_copies_retained_selection() { - let (mut app, info, mut input_rx) = app_with_screen_bytes_and_input(b"alpha beta"); - app.state.copy_on_select = false; - drag_select_range(&mut app, &info, 0, 4); - - app.handle_terminal_key_headless(TerminalKey::new(KeyCode::Char('c'), KeyModifiers::SUPER)); - - assert_eq!(clipboard_write_content(&mut app), b"alpha"); - assert!(app.state.selection.is_none()); - assert!(input_rx.try_recv().is_err()); - } - - #[tokio::test] - async fn copy_shortcut_before_delayed_mouse_up_copies_in_progress_selection() { - let (mut app, info, mut input_rx) = app_with_screen_bytes_and_input(b"alpha beta"); - app.state.copy_on_select = false; - let source_id = 41; - let row = info.inner_rect.y; - let start_col = info.inner_rect.x; - let end_col = info.inner_rect.x + 4; - app.route_client_events_from( - source_id, - vec![ - crate::raw_input::RawInputEvent::Mouse(mouse( - MouseEventKind::Down(MouseButton::Left), - start_col, - row, - )), - crate::raw_input::RawInputEvent::Mouse(mouse( - MouseEventKind::Drag(MouseButton::Left), - end_col, - row, - )), - ], - false, - ); - assert_visible_selection(&app); - assert!(app - .state - .selection - .as_ref() - .is_some_and(crate::selection::Selection::is_in_progress)); - assert!(app.state.selection_autoscroll.is_some()); - assert!(app.selection_autoscroll_deadline.is_some()); - - let cmd_c = TerminalKey::new(KeyCode::Char('c'), KeyModifiers::SUPER); - app.route_client_events_from( - source_id, - vec![crate::raw_input::RawInputEvent::Key(cmd_c.clone())], - false, - ); - - assert_eq!(clipboard_write_content(&mut app), b"alpha"); - assert!(app.event_rx.try_recv().is_err()); - assert!(app.state.selection.is_none()); - assert!(app.state.selection_autoscroll.is_none()); - assert!(app.selection_autoscroll_deadline.is_none()); - assert!(app.selection_highlight_clear_deadline.is_none()); - assert_eq!(app.input_leases.len(), 1); - assert!(input_rx.try_recv().is_err()); - - app.route_client_events_from( - source_id, - vec![crate::raw_input::RawInputEvent::Key( - cmd_c.with_kind(KeyEventKind::Release), - )], - false, - ); - assert!(app.input_leases.is_empty()); - - app.route_client_events_from( - source_id, - vec![crate::raw_input::RawInputEvent::Mouse(mouse( - MouseEventKind::Up(MouseButton::Left), - end_col, - row, - ))], - false, - ); - - assert!(app.state.selection.is_none()); - assert!(app.state.selection_autoscroll.is_none()); - assert!(app.selection_autoscroll_deadline.is_none()); - assert!(app.selection_highlight_clear_deadline.is_none()); - assert!(app.input_leases.is_empty()); - assert!(app.event_rx.try_recv().is_err()); - assert!(input_rx.try_recv().is_err()); - } - - #[tokio::test] - async fn retained_selection_copy_shortcut_is_disabled_with_copy_on_select() { - let (mut app, _info, mut input_rx) = app_with_screen_bytes_and_input(b"alpha beta"); - let pane_id = app.state.workspaces[0].tabs[0].root_pane; - let mut selection = crate::selection::Selection::range( - pane_id, - 0, - 0, - 4, - app.state - .pane_scroll_metrics(&app.terminal_runtimes, pane_id), - ); - assert!(selection.finish()); - app.state.selection = Some(selection); - app.state.copy_on_select = true; - - app.handle_terminal_key_headless(TerminalKey::new( - KeyCode::Char('c'), - KeyModifiers::CONTROL, - )); - - assert!(app.state.selection.is_none()); - assert!(app.event_rx.try_recv().is_err()); - assert_eq!( - input_rx.try_recv().expect("forwarded Ctrl-C").as_ref(), - b"\x03" - ); - } - - #[tokio::test] - async fn retained_selection_copy_shortcut_forwards_when_selection_text_is_empty() { - let (mut app, info, mut input_rx) = app_with_screen_bytes_and_input(b""); - app.state.copy_on_select = false; - drag_select_range(&mut app, &info, 0, 4); - assert_visible_selection(&app); - - app.handle_terminal_key_headless(TerminalKey::new( - KeyCode::Char('c'), - KeyModifiers::CONTROL, - )); - - assert!(app.state.selection.is_none()); - assert!(app.event_rx.try_recv().is_err()); - assert_eq!( - input_rx.try_recv().expect("forwarded Ctrl-C").as_ref(), - b"\x03" - ); - } - - #[tokio::test] - async fn retained_selection_copy_shortcut_requires_exact_modifiers() { - let (mut app, info, mut input_rx) = app_with_screen_bytes_and_input(b"alpha beta"); - app.state.copy_on_select = false; - drag_select_range(&mut app, &info, 0, 4); - - app.handle_terminal_key_headless(TerminalKey::new( - KeyCode::Char('C'), - KeyModifiers::CONTROL | KeyModifiers::SHIFT, - )); - - assert!(app.state.selection.is_none()); - assert!(app.event_rx.try_recv().is_err()); - assert_eq!( - input_rx - .try_recv() - .expect("forwarded Ctrl-Shift-C") - .as_ref(), - b"\x03" - ); - } -} |
