aboutsummaryrefslogtreecommitdiff
path: root/src/app/input/clipboard.rs
diff options
context:
space:
mode:
authorCan Celik <ogulcancelik@gmail.com>2026-09-01 15:48:22 +0300
committerGitHub <noreply@github.com>2026-09-01 15:48:22 +0300
commit207be3c771d281baae6e5fa0fb74be9a056e97a2 (patch)
treed7b037ecdf629224a048c841d11659efdc23507d /src/app/input/clipboard.rs
parent8a6d697308162874a236c84aac0b0f8c7ac01f83 (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.rs370
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"
- );
- }
-}