beamterm_renderer/gl/
cell_query.rs

1use compact_str::{CompactString, CompactStringExt};
2
3use crate::gl::TerminalGrid;
4
5/// Configuration for querying and extracting text from terminal cells.
6///
7/// Defines the selection mode, coordinate range, and text processing options
8/// for extracting content from the terminal grid.
9#[derive(Debug, Clone, Copy, Default)]
10pub struct CellQuery {
11    pub(crate) mode: SelectionMode,
12    pub(super) start: Option<(u16, u16)>,
13    pub(super) end: Option<(u16, u16)>,
14    pub(super) trim_trailing_whitespace: bool,
15    /// Hash of the selected content when selection was completed.
16    /// Used to detect if underlying content has changed.
17    pub(crate) content_hash: Option<u64>,
18}
19
20/// Defines how cells are selected in the terminal grid.
21#[derive(Debug, Clone, Copy, Default)]
22pub enum SelectionMode {
23    /// Rectangular selection of cells.
24    ///
25    /// Selects all cells within the rectangle defined by start and end points.
26    #[default]
27    Block,
28    /// Linear selection following text flow.
29    ///
30    /// Selects cells from start to end following line wrapping, similar to
31    /// standard text selection in terminals.
32    Linear,
33}
34
35/// Zero-allocation iterator over terminal cell indices.
36///
37/// Provides efficient iteration over selected cells without allocating
38/// intermediate collections.
39#[derive(Debug)]
40pub enum CellIterator {
41    /// Iterator for block (rectangular) selections.
42    Block(BlockCellIterator),
43    /// Iterator for linear (text-flow) selections.
44    Linear(LinearCellIterator),
45}
46
47/// Iterator for block (rectangular) cell selection.
48///
49/// Iterates over cells row by row within the rectangular region defined
50/// by start and end coordinates.
51#[derive(Debug)]
52pub struct BlockCellIterator {
53    cols: u16,
54    start: (u16, u16),
55    end: (u16, u16),
56    current: (u16, u16),
57    finished: bool,
58}
59
60/// Iterator for linear cell selection.
61///
62/// Iterates over cells following text flow from start to end position,
63/// wrapping at line boundaries like standard text selection.
64#[derive(Debug)]
65pub struct LinearCellIterator {
66    cols: u16,
67    current_idx: usize,
68    end_idx: usize,
69    finished: bool,
70}
71
72/// Creates a new cell query with the specified selection mode.
73///
74/// # Example
75/// ```
76/// use beamterm_renderer::{select, SelectionMode};
77///
78/// let query = select(SelectionMode::Block)
79///     .start((0, 0))
80///     .end((10, 5))
81///     .trim_trailing_whitespace(true);
82/// ```
83pub fn select(mode: SelectionMode) -> CellQuery {
84    CellQuery { mode, ..CellQuery::default() }
85}
86
87impl CellQuery {
88    /// Sets the starting position for the selection.
89    ///
90    /// # Arguments
91    /// * `start` - Starting coordinates as (column, row)
92    pub fn start(mut self, start: (u16, u16)) -> Self {
93        self.start = Some(start);
94        self
95    }
96
97    /// Sets the ending position for the selection.
98    ///
99    /// # Arguments
100    /// * `end` - Ending coordinates as (column, row)
101    pub fn end(mut self, end: (u16, u16)) -> Self {
102        self.end = Some(end);
103        self
104    }
105
106    /// Checks if the query has no selection range defined.
107    pub fn is_empty(&self) -> bool {
108        self.start.is_none() && self.end.is_none()
109    }
110
111    /// Returns the normalized selection range if both start and end are defined.
112    ///
113    /// The returned range has coordinates ordered so that the first tuple
114    /// contains the minimum coordinates and the second contains the maximum.
115    pub fn range(&self) -> Option<((u16, u16), (u16, u16))> {
116        if let (Some(start), Some(end)) = (self.start, self.end) {
117            Some((
118                (start.0.min(end.0), start.1.min(end.1)),
119                (start.0.max(end.0), start.1.max(end.1)),
120            ))
121        } else {
122            None
123        }
124    }
125
126    /// Configures whether to remove trailing whitespace from each line.
127    ///
128    /// When enabled, spaces at the end of each selected line are removed
129    /// from the extracted text.
130    pub fn trim_trailing_whitespace(mut self, enabled: bool) -> Self {
131        self.trim_trailing_whitespace = enabled;
132        self
133    }
134
135    /// Sets the content hash for this query.
136    ///
137    /// The hash is computed from the selected cells when the selection
138    /// is completed, and used to detect if the underlying content changes.
139    pub(crate) fn with_content_hash(mut self, hash: u64) -> Self {
140        self.content_hash = Some(hash);
141        self
142    }
143}
144
145impl Iterator for CellIterator {
146    type Item = (usize, bool); // (cell_index, needs_newline_after)
147
148    fn next(&mut self) -> Option<Self::Item> {
149        match self {
150            CellIterator::Block(iter) => iter.next(),
151            CellIterator::Linear(iter) => iter.next(),
152        }
153    }
154}
155
156impl Iterator for BlockCellIterator {
157    type Item = (usize, bool);
158
159    fn next(&mut self) -> Option<Self::Item> {
160        if self.finished || self.current.1 > self.end.1 {
161            return None;
162        }
163
164        let idx = self.current.1 as usize * self.cols as usize + self.current.0 as usize;
165
166        // Check if we need a newline after this cell
167        let is_end_of_row = self.current.0 == self.end.0;
168        let is_last_row = self.current.1 == self.end.1;
169        let needs_newline = is_end_of_row && !is_last_row;
170
171        // Advance to next position
172        if self.current.0 < self.end.0 {
173            self.current.0 += 1;
174        } else {
175            self.current.0 = self.start.0;
176            self.current.1 += 1;
177            if self.current.1 > self.end.1 {
178                self.finished = true;
179            }
180        }
181
182        Some((idx, needs_newline))
183    }
184}
185
186impl Iterator for LinearCellIterator {
187    type Item = (usize, bool);
188
189    fn next(&mut self) -> Option<Self::Item> {
190        if self.finished || self.current_idx > self.end_idx {
191            return None;
192        }
193
194        let idx = self.current_idx;
195
196        // Check if we need a newline before this cell (except for the first cell)
197        let is_row_start = idx.is_multiple_of(self.cols as usize);
198        let is_first_cell = idx == self.current_idx;
199        let needs_newline_before = is_row_start && !is_first_cell;
200
201        self.current_idx += 1;
202        if self.current_idx > self.end_idx {
203            self.finished = true;
204        }
205
206        // Check if NEXT cell will need a newline before it
207        let needs_newline_after = if self.current_idx <= self.end_idx {
208            self.current_idx
209                .is_multiple_of(self.cols as usize)
210        } else {
211            false
212        };
213
214        Some((idx, needs_newline_after))
215    }
216}
217
218impl BlockCellIterator {
219    /// Creates a new block iterator with bounds checking.
220    ///
221    /// Ensures coordinates are within terminal bounds and properly ordered.
222    fn new(cols: u16, start: (u16, u16), end: (u16, u16), max_cells: usize) -> Self {
223        // Bounds checking and coordinate ordering
224        let start = (
225            start.0.min(cols.saturating_sub(1)),
226            start
227                .1
228                .min((max_cells / cols as usize).saturating_sub(1) as u16),
229        );
230        let end = (
231            end.0.min(cols.saturating_sub(1)),
232            end.1
233                .min((max_cells / cols as usize).saturating_sub(1) as u16),
234        );
235        let (start, end) = if start > end { (end, start) } else { (start, end) };
236
237        Self {
238            cols,
239            start,
240            end,
241            current: start,
242            finished: start.1 > end.1,
243        }
244    }
245}
246
247impl LinearCellIterator {
248    /// Creates a new linear iterator with bounds checking.
249    ///
250    /// Converts coordinates to linear indices and ensures they're within bounds.
251    fn new(cols: u16, start: (u16, u16), end: (u16, u16), max_cells: usize) -> Self {
252        let cols_usize = cols as usize;
253
254        // Bounds checking and coordinate ordering
255        let start = (
256            start.0.min(cols.saturating_sub(1)),
257            start
258                .1
259                .min((max_cells / cols_usize).saturating_sub(1) as u16),
260        );
261        let end = (
262            end.0.min(cols.saturating_sub(1)),
263            end.1
264                .min((max_cells / cols_usize).saturating_sub(1) as u16),
265        );
266        let (start, end) = if start > end { (end, start) } else { (start, end) };
267
268        let start_idx = start.1 as usize * cols_usize + start.0 as usize;
269        let end_idx = end.1 as usize * cols_usize + end.0 as usize;
270        let end_idx = end_idx.min(max_cells.saturating_sub(1));
271        let start_idx = start_idx.min(end_idx);
272
273        Self {
274            cols,
275            current_idx: start_idx,
276            end_idx,
277            finished: start_idx > end_idx,
278        }
279    }
280}
281
282impl TerminalGrid {
283    /// Zero-allocation iterator over cell indices for a given selection range and mode.
284    ///
285    /// Creates an efficient iterator that yields cell indices and newline indicators
286    /// without allocating intermediate collections.
287    ///
288    /// # Returns
289    /// Iterator yielding (cell_index, needs_newline_after) tuples.
290    pub fn cell_iter(&self, selection: CellQuery) -> CellIterator {
291        let cols = self.terminal_size().0;
292        let max_cells = self.cell_count();
293
294        let (start, end) = selection.range().unwrap_or_default();
295
296        match selection.mode {
297            SelectionMode::Block => {
298                CellIterator::Block(BlockCellIterator::new(cols, start, end, max_cells))
299            },
300            SelectionMode::Linear => {
301                CellIterator::Linear(LinearCellIterator::new(cols, start, end, max_cells))
302            },
303        }
304    }
305
306    /// Extracts text content from the terminal based on the selection query.
307    ///
308    /// Retrieves the text within the selection range, optionally trimming
309    /// trailing whitespace from each line based on the query configuration.
310    ///
311    /// # Arguments
312    /// * `selection` - Query defining the selection range and options
313    ///
314    /// # Returns
315    /// The selected text as a `CompactString`, or empty string if no selection.
316    pub fn get_text(&self, selection: CellQuery) -> CompactString {
317        if selection.is_empty() {
318            return CompactString::const_new("");
319        }
320
321        let text = self.get_symbols(self.cell_iter(selection));
322
323        if selection.trim_trailing_whitespace {
324            text.lines().map(str::trim_end).join_compact("\n")
325        } else {
326            text
327        }
328    }
329}