1use crate::termtui::screen::Screen;
8
9#[derive(Clone, Copy, Debug, PartialEq, Eq)]
11pub struct CopyPos {
12 pub x: i32,
13 pub y: i32,
14}
15
16impl CopyPos {
17 pub fn new(x: i32, y: i32) -> Self {
18 Self { x, y }
19 }
20
21 pub fn to_low_high(start: &CopyPos, end: &CopyPos) -> (CopyPos, CopyPos) {
23 if start.y < end.y || (start.y == end.y && start.x <= end.x) {
24 (*start, *end)
25 } else {
26 (*end, *start)
27 }
28 }
29}
30
31#[derive(Clone, Copy, Debug)]
33pub enum CopyMoveDir {
34 Up,
35 Down,
36 Left,
37 Right,
38 LineStart,
39 LineEnd,
40 PageUp,
41 PageDown,
42 Top,
43 Bottom,
44 WordLeft,
45 WordRight,
46}
47
48impl CopyMoveDir {
49 pub fn delta(&self) -> (i32, i32) {
51 match self {
52 CopyMoveDir::Up => (0, -1),
53 CopyMoveDir::Down => (0, 1),
54 CopyMoveDir::Left => (-1, 0),
55 CopyMoveDir::Right => (1, 0),
56 _ => (0, 0), }
58 }
59}
60
61#[derive(Clone)]
63pub enum CopyMode {
64 None,
66 Active {
68 frozen_screen: Screen,
70 cursor: CopyPos,
72 anchor: Option<CopyPos>,
74 screen_height: i32,
76 screen_width: i32,
78 scrollback_available: i32,
80 },
81}
82
83impl Default for CopyMode {
84 fn default() -> Self {
85 Self::None
86 }
87}
88
89impl CopyMode {
90 pub fn is_active(&self) -> bool {
92 matches!(self, CopyMode::Active { .. })
93 }
94
95 pub fn enter(screen: Screen, start: CopyPos) -> Self {
97 let size = screen.size();
98 let scrollback = screen.primary_grid().scrollback_available() as i32;
99
100 CopyMode::Active {
101 frozen_screen: screen,
102 cursor: start,
103 anchor: None,
104 screen_height: size.rows as i32,
105 screen_width: size.cols as i32,
106 scrollback_available: scrollback,
107 }
108 }
109
110 pub fn move_cursor(&mut self, dx: i32, dy: i32) {
112 if let CopyMode::Active {
113 cursor,
114 screen_width,
115 screen_height,
116 scrollback_available,
117 ..
118 } = self
119 {
120 let new_x = (cursor.x + dx).clamp(0, *screen_width - 1);
121 let new_y = (cursor.y + dy).clamp(-*scrollback_available, *screen_height - 1);
122
123 cursor.x = new_x;
124 cursor.y = new_y;
125 }
126 }
127
128 pub fn move_dir(&mut self, dir: CopyMoveDir) {
130 if let CopyMode::Active {
131 cursor,
132 screen_width,
133 screen_height,
134 scrollback_available,
135 ..
136 } = self
137 {
138 match dir {
139 CopyMoveDir::Up | CopyMoveDir::Down | CopyMoveDir::Left | CopyMoveDir::Right => {
140 let (dx, dy) = dir.delta();
141 self.move_cursor(dx, dy);
142 }
143 CopyMoveDir::LineStart => {
144 cursor.x = 0;
145 }
146 CopyMoveDir::LineEnd => {
147 cursor.x = *screen_width - 1;
148 }
149 CopyMoveDir::PageUp => {
150 let page = *screen_height / 2;
151 cursor.y = (cursor.y - page).max(-*scrollback_available);
152 }
153 CopyMoveDir::PageDown => {
154 let page = *screen_height / 2;
155 cursor.y = (cursor.y + page).min(*screen_height - 1);
156 }
157 CopyMoveDir::Top => {
158 cursor.y = -*scrollback_available;
159 cursor.x = 0;
160 }
161 CopyMoveDir::Bottom => {
162 cursor.y = *screen_height - 1;
163 cursor.x = *screen_width - 1;
164 }
165 CopyMoveDir::WordLeft | CopyMoveDir::WordRight => {
166 let delta = if matches!(dir, CopyMoveDir::WordLeft) {
168 -5
169 } else {
170 5
171 };
172 cursor.x = (cursor.x + delta).clamp(0, *screen_width - 1);
173 }
174 }
175 }
176 }
177
178 pub fn set_anchor(&mut self) {
180 if let CopyMode::Active { cursor, anchor, .. } = self {
181 if anchor.is_some() {
182 *anchor = None;
184 } else {
185 *anchor = Some(*cursor);
187 }
188 }
189 }
190
191 pub fn set_end(&mut self) {
193 if let CopyMode::Active { cursor, anchor, .. } = self {
194 if anchor.is_none() {
195 *anchor = Some(*cursor);
197 }
198 }
199 }
200
201 pub fn get_selection(&self) -> Option<(CopyPos, CopyPos)> {
203 if let CopyMode::Active { cursor, anchor, .. } = self {
204 anchor.map(|a| (a, *cursor))
205 } else {
206 None
207 }
208 }
209
210 pub fn get_selected_text(&self) -> Option<String> {
212 if let CopyMode::Active {
213 frozen_screen,
214 cursor,
215 anchor,
216 ..
217 } = self
218 {
219 let anchor = (*anchor)?;
220 let (low, high) = CopyPos::to_low_high(&anchor, cursor);
221
222 Some(frozen_screen.get_selected_text(low.x, low.y, high.x, high.y))
223 } else {
224 None
225 }
226 }
227
228 pub fn frozen_screen(&self) -> Option<&Screen> {
230 if let CopyMode::Active { frozen_screen, .. } = self {
231 Some(frozen_screen)
232 } else {
233 None
234 }
235 }
236
237 pub fn cursor(&self) -> Option<CopyPos> {
239 if let CopyMode::Active { cursor, .. } = self {
240 Some(*cursor)
241 } else {
242 None
243 }
244 }
245}
246
247#[cfg(test)]
248mod tests {
249 use super::*;
250
251 fn make_test_screen() -> Screen {
252 Screen::new(24, 80, 1000)
253 }
254
255 #[test]
256 fn test_copy_mode_enter() {
257 let screen = make_test_screen();
258 let mode = CopyMode::enter(screen, CopyPos::new(0, 0));
259
260 assert!(mode.is_active());
261 }
262
263 #[test]
264 fn test_copy_mode_move_cursor() {
265 let screen = make_test_screen();
266 let mut mode = CopyMode::enter(screen, CopyPos::new(10, 10));
267
268 mode.move_cursor(5, 2);
269
270 if let CopyMode::Active { cursor, .. } = mode {
271 assert_eq!(cursor.x, 15);
272 assert_eq!(cursor.y, 12);
273 } else {
274 panic!("Expected active mode");
275 }
276 }
277
278 #[test]
279 fn test_copy_mode_bounds() {
280 let screen = make_test_screen();
281 let mut mode = CopyMode::enter(screen, CopyPos::new(0, 0));
282
283 mode.move_cursor(-10, -10);
285
286 if let CopyMode::Active { cursor, .. } = mode {
287 assert_eq!(cursor.x, 0);
288 assert!(cursor.y <= 0);
289 } else {
290 panic!("Expected active mode");
291 }
292 }
293
294 #[test]
295 fn test_copy_mode_selection() {
296 let screen = make_test_screen();
297 let mut mode = CopyMode::enter(screen, CopyPos::new(5, 5));
298
299 assert!(mode.get_selection().is_none());
301
302 mode.set_anchor();
304
305 mode.move_cursor(10, 0);
307
308 let selection = mode.get_selection();
310 assert!(selection.is_some());
311
312 let (start, end) = selection.unwrap();
313 assert_eq!(start.x, 5);
314 assert_eq!(end.x, 15);
315 }
316
317 #[test]
318 fn test_copy_pos_to_low_high() {
319 let a = CopyPos::new(10, 5);
320 let b = CopyPos::new(5, 10);
321
322 let (low, high) = CopyPos::to_low_high(&a, &b);
323 assert_eq!(low.y, 5);
324 assert_eq!(high.y, 10);
325 }
326}