Skip to main content

par_term/copy_mode/
cursor.rs

1//! Cursor movement methods for the copy mode state machine.
2
3use super::types::CopyModeState;
4
5impl CopyModeState {
6    // ========================================================================
7    // Basic motions
8    // ========================================================================
9
10    /// Move cursor left by count
11    pub fn move_left(&mut self) {
12        let count = self.effective_count();
13        self.cursor_col = self.cursor_col.saturating_sub(count);
14    }
15
16    /// Move cursor right by count
17    pub fn move_right(&mut self) {
18        let count = self.effective_count();
19        self.cursor_col = (self.cursor_col + count).min(self.cols.saturating_sub(1));
20    }
21
22    /// Move cursor up by count
23    pub fn move_up(&mut self) {
24        let count = self.effective_count();
25        self.cursor_absolute_line = self.cursor_absolute_line.saturating_sub(count);
26    }
27
28    /// Move cursor down by count
29    pub fn move_down(&mut self) {
30        let count = self.effective_count();
31        self.cursor_absolute_line = (self.cursor_absolute_line + count).min(self.max_line());
32    }
33
34    /// Move cursor to start of line
35    pub fn move_to_line_start(&mut self) {
36        self.cursor_col = 0;
37    }
38
39    /// Move cursor to end of line
40    pub fn move_to_line_end(&mut self) {
41        self.cursor_col = self.cols.saturating_sub(1);
42    }
43
44    /// Move cursor to first non-blank character on the line
45    pub fn move_to_first_non_blank(&mut self, line_text: &str) {
46        let first_non_blank = line_text
47            .chars()
48            .position(|c| !c.is_whitespace())
49            .unwrap_or(0);
50        self.cursor_col = first_non_blank.min(self.cols.saturating_sub(1));
51    }
52
53    // ========================================================================
54    // Page motions
55    // ========================================================================
56
57    /// Move half page up
58    pub fn half_page_up(&mut self) {
59        let half = self.rows / 2;
60        let count = self.effective_count();
61        self.cursor_absolute_line = self.cursor_absolute_line.saturating_sub(half * count);
62    }
63
64    /// Move half page down
65    pub fn half_page_down(&mut self) {
66        let half = self.rows / 2;
67        let count = self.effective_count();
68        self.cursor_absolute_line = (self.cursor_absolute_line + half * count).min(self.max_line());
69    }
70
71    /// Move full page up
72    pub fn page_up(&mut self) {
73        let count = self.effective_count();
74        self.cursor_absolute_line = self.cursor_absolute_line.saturating_sub(self.rows * count);
75    }
76
77    /// Move full page down
78    pub fn page_down(&mut self) {
79        let count = self.effective_count();
80        self.cursor_absolute_line =
81            (self.cursor_absolute_line + self.rows * count).min(self.max_line());
82    }
83
84    /// Go to top of buffer (line 0)
85    pub fn goto_top(&mut self) {
86        self.cursor_absolute_line = 0;
87    }
88
89    /// Go to bottom of buffer (last line)
90    pub fn goto_bottom(&mut self) {
91        self.cursor_absolute_line = self.max_line();
92    }
93
94    /// Go to specific absolute line (for count+G)
95    pub fn goto_line(&mut self, line: usize) {
96        self.cursor_absolute_line = line.min(self.max_line());
97    }
98
99    // ========================================================================
100    // Viewport helpers
101    // ========================================================================
102
103    /// Get the cursor position in screen coordinates, if visible.
104    ///
105    /// Returns `(col, row)` where row is relative to the viewport top.
106    /// Returns `None` if the cursor is outside the visible viewport.
107    pub fn screen_cursor_pos(&self, scroll_offset: usize) -> Option<(usize, usize)> {
108        let viewport_top = self.scrollback_len.saturating_sub(scroll_offset);
109        let viewport_bottom = viewport_top + self.rows;
110
111        if self.cursor_absolute_line >= viewport_top && self.cursor_absolute_line < viewport_bottom
112        {
113            let screen_row = self.cursor_absolute_line - viewport_top;
114            Some((self.cursor_col, screen_row))
115        } else {
116            None
117        }
118    }
119
120    /// Calculate the scroll offset needed to make the cursor visible.
121    ///
122    /// Returns `Some(new_offset)` if scrolling is needed, `None` if cursor is already visible.
123    pub fn required_scroll_offset(&self, current_offset: usize) -> Option<usize> {
124        let viewport_top = self.scrollback_len.saturating_sub(current_offset);
125        let viewport_bottom = viewport_top + self.rows;
126
127        if self.cursor_absolute_line < viewport_top {
128            // Cursor is above viewport — scroll up
129            let new_offset = self
130                .scrollback_len
131                .saturating_sub(self.cursor_absolute_line);
132            Some(new_offset)
133        } else if self.cursor_absolute_line >= viewport_bottom {
134            // Cursor is below viewport — scroll down
135            let lines_below = self.cursor_absolute_line - viewport_top;
136            let needed_offset = current_offset
137                .saturating_sub(lines_below.saturating_sub(self.rows.saturating_sub(1)));
138            // Alternatively: the viewport top should be cursor_line - (rows - 1)
139            let target_viewport_top = self
140                .cursor_absolute_line
141                .saturating_sub(self.rows.saturating_sub(1));
142            let new_offset = self.scrollback_len.saturating_sub(target_viewport_top);
143            // Clamp to valid range
144            let _ = needed_offset; // suppress unused
145            Some(new_offset.min(self.scrollback_len))
146        } else {
147            None
148        }
149    }
150}