Skip to main content

par_term/copy_mode/
types.rs

1//! Type definitions for the vi-style copy mode state machine.
2
3use std::collections::HashMap;
4
5/// Visual selection mode in copy mode
6#[derive(Debug, Clone, Copy, PartialEq, Eq)]
7pub enum VisualMode {
8    /// No visual selection active
9    None,
10    /// Character-wise selection (v)
11    Char,
12    /// Line-wise selection (V)
13    Line,
14    /// Block/rectangular selection (Ctrl+V)
15    Block,
16}
17
18/// Pending operator waiting for a motion
19#[derive(Debug, Clone, Copy, PartialEq, Eq)]
20pub enum PendingOperator {
21    /// Yank (copy) operator
22    Yank,
23}
24
25/// Search direction
26#[derive(Debug, Clone, Copy, PartialEq, Eq)]
27pub enum SearchDirection {
28    Forward,
29    Backward,
30}
31
32/// A named mark position
33#[derive(Debug, Clone, Copy)]
34pub struct Mark {
35    pub col: usize,
36    pub absolute_line: usize,
37}
38
39/// Copy mode state machine.
40///
41/// Uses absolute line indexing:
42/// - Line 0 = oldest scrollback line
43/// - Line `scrollback_len - 1` = newest scrollback line
44/// - Line `scrollback_len` = top of visible screen (at scroll_offset=0)
45/// - Line `scrollback_len + rows - 1` = bottom of visible screen
46pub struct CopyModeState {
47    /// Whether copy mode is active
48    pub active: bool,
49    /// Cursor column position
50    pub cursor_col: usize,
51    /// Cursor absolute line position
52    pub cursor_absolute_line: usize,
53    /// Current visual selection mode
54    pub visual_mode: VisualMode,
55    /// Selection anchor point (absolute_line, col) - set when entering visual mode
56    pub selection_anchor: Option<(usize, usize)>,
57    /// Count prefix for motions (e.g., 5j moves down 5 lines)
58    pub count: Option<usize>,
59    /// Pending operator waiting for a motion
60    pub pending_operator: Option<PendingOperator>,
61    /// Named marks (a-z)
62    pub marks: HashMap<char, Mark>,
63    /// Terminal columns
64    pub cols: usize,
65    /// Terminal rows
66    pub rows: usize,
67    /// Scrollback buffer length
68    pub scrollback_len: usize,
69    /// Current search query
70    pub search_query: String,
71    /// Search direction
72    pub search_direction: SearchDirection,
73    /// Whether search input mode is active
74    pub is_searching: bool,
75    /// Waiting for second 'g' in 'gg'
76    pub(crate) pending_g: bool,
77    /// Waiting for mark name after 'm'
78    pub(crate) pending_mark_set: bool,
79    /// Waiting for mark name after "'"
80    pub(crate) pending_mark_goto: bool,
81}
82
83impl Default for CopyModeState {
84    fn default() -> Self {
85        Self::new()
86    }
87}
88
89impl CopyModeState {
90    /// Create a new inactive copy mode state
91    pub fn new() -> Self {
92        Self {
93            active: false,
94            cursor_col: 0,
95            cursor_absolute_line: 0,
96            visual_mode: VisualMode::None,
97            selection_anchor: None,
98            count: None,
99            pending_operator: None,
100            marks: HashMap::new(),
101            cols: 80,
102            rows: 24,
103            scrollback_len: 0,
104            search_query: String::new(),
105            search_direction: SearchDirection::Forward,
106            is_searching: false,
107            pending_g: false,
108            pending_mark_set: false,
109            pending_mark_goto: false,
110        }
111    }
112
113    /// Enter copy mode at the given cursor position
114    pub fn enter(
115        &mut self,
116        cursor_col: usize,
117        cursor_row: usize,
118        cols: usize,
119        rows: usize,
120        scrollback_len: usize,
121    ) {
122        self.active = true;
123        self.cols = cols;
124        self.rows = rows;
125        self.scrollback_len = scrollback_len;
126        // Convert screen row to absolute line
127        self.cursor_absolute_line = scrollback_len + cursor_row;
128        self.cursor_col = cursor_col.min(cols.saturating_sub(1));
129        self.visual_mode = VisualMode::None;
130        self.selection_anchor = None;
131        self.count = None;
132        self.pending_operator = None;
133        self.search_query.clear();
134        self.is_searching = false;
135        self.pending_g = false;
136        self.pending_mark_set = false;
137        self.pending_mark_goto = false;
138    }
139
140    /// Exit copy mode, clearing all state
141    pub fn exit(&mut self) {
142        self.active = false;
143        self.visual_mode = VisualMode::None;
144        self.selection_anchor = None;
145        self.count = None;
146        self.pending_operator = None;
147        self.is_searching = false;
148        self.pending_g = false;
149        self.pending_mark_set = false;
150        self.pending_mark_goto = false;
151    }
152
153    /// Total number of lines (scrollback + screen)
154    pub(super) fn total_lines(&self) -> usize {
155        self.scrollback_len + self.rows
156    }
157
158    /// Maximum valid absolute line index
159    pub(super) fn max_line(&self) -> usize {
160        self.total_lines().saturating_sub(1)
161    }
162
163    /// Push a digit to the count prefix
164    pub fn push_count_digit(&mut self, digit: u8) {
165        let current = self.count.unwrap_or(0);
166        self.count = Some(current * 10 + digit as usize);
167    }
168
169    /// Get the effective count (defaults to 1 if no count set)
170    pub fn effective_count(&mut self) -> usize {
171        let c = self.count.unwrap_or(1);
172        self.count = None;
173        c
174    }
175
176    /// Update the scrollback length (call when terminal state changes)
177    pub fn update_dimensions(&mut self, cols: usize, rows: usize, scrollback_len: usize) {
178        self.cols = cols;
179        self.rows = rows;
180        self.scrollback_len = scrollback_len;
181        // Clamp cursor to valid range
182        self.cursor_col = self.cursor_col.min(cols.saturating_sub(1));
183        self.cursor_absolute_line = self.cursor_absolute_line.min(self.max_line());
184    }
185}