Skip to main content

ratatui_toolkit/vt100_term/
copy_mode.rs

1//! Copy mode implementation with frozen screen snapshots
2//!
3//! This implements mprocs-style copy mode where entering copy mode
4//! freezes the terminal screen, allowing stable selection even as
5//! new output arrives.
6
7use super::Screen;
8
9/// Position in terminal (can be negative for scrollback)
10#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
11pub struct Pos {
12    pub x: i32,
13    pub y: i32,
14}
15
16impl Pos {
17    /// Create a new position
18    pub fn new(x: i32, y: i32) -> Self {
19        Self { x, y }
20    }
21
22    /// Check if position is within selection range
23    pub fn within(start: &Pos, end: &Pos, pos: &Pos) -> bool {
24        let (low, high) = Self::to_low_high(start, end);
25
26        if pos.y < low.y || pos.y > high.y {
27            return false;
28        }
29
30        if pos.y == low.y && pos.y == high.y {
31            // Single line
32            pos.x >= low.x && pos.x <= high.x
33        } else if pos.y == low.y {
34            // First line
35            pos.x >= low.x
36        } else if pos.y == high.y {
37            // Last line
38            pos.x <= high.x
39        } else {
40            // Middle lines
41            true
42        }
43    }
44
45    /// Convert two positions to normalized (low, high) order
46    pub fn to_low_high(a: &Pos, b: &Pos) -> (Pos, Pos) {
47        if a.y < b.y || (a.y == b.y && a.x <= b.x) {
48            (*a, *b)
49        } else {
50            (*b, *a)
51        }
52    }
53}
54
55/// Copy mode state
56///
57/// This is the key feature from mprocs: when entering copy mode,
58/// we clone the screen state so selection coordinates remain stable
59/// even as new terminal output arrives.
60#[derive(Debug, Clone)]
61pub enum CopyMode {
62    /// Not in copy mode, optionally storing last mouse position
63    None,
64
65    /// Active copy mode with frozen screen snapshot
66    Active {
67        /// Frozen screen state at time of entering copy mode
68        screen: Screen,
69
70        /// Selection start position
71        start: Pos,
72
73        /// Selection end position (None until user sets it)
74        end: Option<Pos>,
75    },
76}
77
78impl CopyMode {
79    /// Check if currently in copy mode
80    pub fn is_active(&self) -> bool {
81        matches!(self, CopyMode::Active { .. })
82    }
83
84    /// Get selection range if active
85    pub fn get_selection(&self) -> Option<(Pos, Pos)> {
86        match self {
87            CopyMode::Active { start, end, .. } => Some((*start, end.unwrap_or(*start))),
88            CopyMode::None => None,
89        }
90    }
91
92    /// Get frozen screen if in copy mode
93    pub fn screen(&self) -> Option<&Screen> {
94        match self {
95            CopyMode::Active { screen, .. } => Some(screen),
96            CopyMode::None => None,
97        }
98    }
99
100    /// Enter copy mode with frozen screen
101    pub fn enter(screen: Screen, start: Pos) -> Self {
102        CopyMode::Active {
103            screen,
104            start,
105            end: None,
106        }
107    }
108
109    /// Exit copy mode
110    pub fn exit() -> Self {
111        CopyMode::None
112    }
113
114    /// Move cursor in copy mode
115    pub fn move_cursor(&mut self, dx: i32, dy: i32) {
116        if let CopyMode::Active { screen, start, end } = self {
117            let pos = end.as_mut().unwrap_or(start);
118
119            // Apply movement
120            pos.x = (pos.x + dx).max(0).min(screen.size().cols as i32 - 1);
121            pos.y = (pos.y + dy)
122                .max(-(screen.scrollback_len() as i32))
123                .min(screen.size().rows as i32 - 1);
124        }
125    }
126
127    /// Set end position (start range selection)
128    pub fn set_end(&mut self) {
129        if let CopyMode::Active { start, end, .. } = self {
130            if end.is_none() {
131                *end = Some(*start);
132            }
133        }
134    }
135
136    /// Get selected text from frozen screen
137    pub fn get_selected_text(&self) -> Option<String> {
138        match self {
139            CopyMode::Active { screen, start, end } => {
140                let end_pos = end.unwrap_or(*start);
141                let (low, high) = Pos::to_low_high(start, &end_pos);
142                Some(screen.get_selected_text(low.x, low.y, high.x, high.y))
143            }
144            CopyMode::None => None,
145        }
146    }
147}
148
149/// Direction for cursor movement in copy mode
150#[derive(Debug, Clone, Copy)]
151pub enum CopyMoveDir {
152    Up,
153    Down,
154    Left,
155    Right,
156}
157
158impl CopyMoveDir {
159    /// Get delta for this direction
160    pub fn delta(&self) -> (i32, i32) {
161        match self {
162            CopyMoveDir::Up => (0, -1),
163            CopyMoveDir::Down => (0, 1),
164            CopyMoveDir::Left => (-1, 0),
165            CopyMoveDir::Right => (1, 0),
166        }
167    }
168}