From 207be3c771d281baae6e5fa0fb74be9a056e97a2 Mon Sep 17 00:00:00 2001 From: Can Celik Date: Tue, 1 Sep 2026 15:48:22 +0300 Subject: 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 --- src/copy_mode.rs | 86 ++++++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 86 insertions(+) create mode 100644 src/copy_mode.rs (limited to 'src/copy_mode.rs') diff --git a/src/copy_mode.rs b/src/copy_mode.rs new file mode 100644 index 00000000..8f84b9a8 --- /dev/null +++ b/src/copy_mode.rs @@ -0,0 +1,86 @@ +use crossterm::event::{KeyCode, KeyModifiers}; + +use crate::input::TerminalKey; + +pub(crate) fn first_non_blank_col(text: &str) -> Option { + let mut col = 0u16; + for ch in text.chars() { + if !ch.is_whitespace() { + return Some(col); + } + col = col.saturating_add(char_cell_width(ch)); + } + None +} + +pub(crate) fn last_character_col(text: &str) -> Option { + let mut col = 0u16; + let mut last_col = None; + for ch in text.chars() { + let width = u16::from(crate::ghostty::unicode_codepoint_width(ch as u32)); + if width > 0 { + last_col = Some(col); + col = col.saturating_add(width); + } + } + last_col +} + +fn char_cell_width(ch: char) -> u16 { + u16::from(crate::ghostty::unicode_codepoint_width(ch as u32)).max(1) +} + +pub(crate) fn copy_mode_page_lines(height: u16, half_page: bool) -> usize { + if height <= 2 { + 1 + } else if half_page { + usize::from(height / 2) + } else { + usize::from(height - 2) + } +} + +pub(crate) fn copy_mode_command_char(key: TerminalKey) -> Option { + if !key.modifiers.difference(KeyModifiers::SHIFT).is_empty() { + return None; + } + if let Some(ch) = key.shifted_codepoint.and_then(char::from_u32) { + return Some(ch); + } + let KeyCode::Char(ch) = key.code else { + return None; + }; + if key.modifiers.contains(KeyModifiers::SHIFT) { + Some(shifted_ascii_char(ch).unwrap_or(ch)) + } else { + Some(ch) + } +} + +fn shifted_ascii_char(ch: char) -> Option { + match ch { + 'a'..='z' => Some(ch.to_ascii_uppercase()), + '1' => Some('!'), + '2' => Some('@'), + '3' => Some('#'), + '4' => Some('$'), + '5' => Some('%'), + '6' => Some('^'), + '7' => Some('&'), + '8' => Some('*'), + '9' => Some('('), + '0' => Some(')'), + '-' => Some('_'), + '=' => Some('+'), + '[' => Some('{'), + ']' => Some('}'), + '\\' => Some('|'), + ';' => Some(':'), + '\'' => Some('"'), + ',' => Some('<'), + '.' => Some('>'), + '/' => Some('?'), + '`' => Some('~'), + _ => None, + } +} -- cgit v1.2.3