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