1use compact_str::{CompactString, CompactStringExt};
2
3use crate::gl::TerminalGrid;
4
5#[derive(Debug, Clone, Copy, Default)]
10pub struct CellQuery {
11 pub(crate) mode: SelectionMode,
12 pub(crate) start: Option<(u16, u16)>,
13 pub(crate) end: Option<(u16, u16)>,
14 pub(crate) trim_trailing_whitespace: bool,
15 pub(crate) content_hash: Option<u64>,
18}
19
20#[derive(Debug, Clone, Copy, Default)]
22#[non_exhaustive]
23pub enum SelectionMode {
24 #[default]
28 Block,
29 Linear,
34}
35
36#[derive(Debug)]
41pub enum CellIterator {
42 Block(BlockCellIterator),
44 Linear(LinearCellIterator),
46}
47
48#[derive(Debug)]
53pub struct BlockCellIterator {
54 cols: u16,
55 start: (u16, u16),
56 end: (u16, u16),
57 current: (u16, u16),
58 finished: bool,
59}
60
61#[derive(Debug)]
66pub struct LinearCellIterator {
67 cols: u16,
68 current_idx: usize,
69 end_idx: usize,
70 finished: bool,
71}
72
73pub fn select(mode: SelectionMode) -> CellQuery {
75 CellQuery { mode, ..CellQuery::default() }
76}
77
78impl CellQuery {
79 #[must_use]
84 pub fn start(mut self, start: (u16, u16)) -> Self {
85 self.start = Some(start);
86 self
87 }
88
89 #[must_use]
94 pub fn end(mut self, end: (u16, u16)) -> Self {
95 self.end = Some(end);
96 self
97 }
98
99 pub fn is_empty(&self) -> bool {
101 self.start.is_none() && self.end.is_none()
102 }
103
104 pub fn range(&self) -> Option<((u16, u16), (u16, u16))> {
109 if let (Some(start), Some(end)) = (self.start, self.end) {
110 Some((
111 (start.0.min(end.0), start.1.min(end.1)),
112 (start.0.max(end.0), start.1.max(end.1)),
113 ))
114 } else {
115 None
116 }
117 }
118
119 #[must_use]
124 pub fn trim_trailing_whitespace(mut self, enabled: bool) -> Self {
125 self.trim_trailing_whitespace = enabled;
126 self
127 }
128
129 pub(crate) fn with_content_hash(mut self, hash: u64) -> Self {
134 self.content_hash = Some(hash);
135 self
136 }
137}
138
139impl Iterator for CellIterator {
140 type Item = (usize, bool); fn next(&mut self) -> Option<Self::Item> {
143 match self {
144 CellIterator::Block(iter) => iter.next(),
145 CellIterator::Linear(iter) => iter.next(),
146 }
147 }
148}
149
150impl Iterator for BlockCellIterator {
151 type Item = (usize, bool);
152
153 fn next(&mut self) -> Option<Self::Item> {
154 if self.finished || self.current.1 > self.end.1 {
155 return None;
156 }
157
158 let idx = self.current.1 as usize * self.cols as usize + self.current.0 as usize;
159
160 let is_end_of_row = self.current.0 == self.end.0;
162 let is_last_row = self.current.1 == self.end.1;
163 let needs_newline = is_end_of_row && !is_last_row;
164
165 if self.current.0 < self.end.0 {
167 self.current.0 += 1;
168 } else {
169 self.current.0 = self.start.0;
170 self.current.1 += 1;
171 if self.current.1 > self.end.1 {
172 self.finished = true;
173 }
174 }
175
176 Some((idx, needs_newline))
177 }
178}
179
180impl Iterator for LinearCellIterator {
181 type Item = (usize, bool);
182
183 fn next(&mut self) -> Option<Self::Item> {
184 if self.finished || self.current_idx > self.end_idx {
185 return None;
186 }
187
188 let idx = self.current_idx;
189
190 self.current_idx += 1;
191 if self.current_idx > self.end_idx {
192 self.finished = true;
193 }
194
195 let needs_newline_after = if self.current_idx <= self.end_idx {
197 self.current_idx
198 .is_multiple_of(self.cols as usize)
199 } else {
200 false
201 };
202
203 Some((idx, needs_newline_after))
204 }
205}
206
207impl BlockCellIterator {
208 fn new(cols: u16, start: (u16, u16), end: (u16, u16), max_cells: usize) -> Self {
212 let start = (
214 start.0.min(cols.saturating_sub(1)),
215 start
216 .1
217 .min((max_cells / cols as usize).saturating_sub(1) as u16),
218 );
219 let end = (
220 end.0.min(cols.saturating_sub(1)),
221 end.1
222 .min((max_cells / cols as usize).saturating_sub(1) as u16),
223 );
224 let (start, end) = if start > end { (end, start) } else { (start, end) };
225
226 Self {
227 cols,
228 start,
229 end,
230 current: start,
231 finished: start.1 > end.1,
232 }
233 }
234}
235
236impl LinearCellIterator {
237 fn new(cols: u16, start: (u16, u16), end: (u16, u16), max_cells: usize) -> Self {
241 let cols_usize = cols as usize;
242
243 let start = (
245 start.0.min(cols.saturating_sub(1)),
246 start
247 .1
248 .min((max_cells / cols_usize).saturating_sub(1) as u16),
249 );
250 let end = (
251 end.0.min(cols.saturating_sub(1)),
252 end.1
253 .min((max_cells / cols_usize).saturating_sub(1) as u16),
254 );
255 let (start, end) = if start > end { (end, start) } else { (start, end) };
256
257 let start_idx = start.1 as usize * cols_usize + start.0 as usize;
258 let end_idx = end.1 as usize * cols_usize + end.0 as usize;
259 let end_idx = end_idx.min(max_cells.saturating_sub(1));
260 let start_idx = start_idx.min(end_idx);
261
262 Self {
263 cols,
264 current_idx: start_idx,
265 end_idx,
266 finished: start_idx > end_idx,
267 }
268 }
269}
270
271impl TerminalGrid {
272 pub fn cell_iter(&self, selection: CellQuery) -> CellIterator {
280 let cols = self.terminal_size().cols;
281 let max_cells = self.cell_count();
282
283 let (start, end) = selection.range().unwrap_or_default();
284
285 match selection.mode {
286 SelectionMode::Block => {
287 CellIterator::Block(BlockCellIterator::new(cols, start, end, max_cells))
288 },
289 SelectionMode::Linear => {
290 CellIterator::Linear(LinearCellIterator::new(cols, start, end, max_cells))
291 },
292 }
293 }
294
295 pub fn get_text(&self, selection: CellQuery) -> CompactString {
306 if selection.is_empty() {
307 return CompactString::const_new("");
308 }
309
310 let text = self.get_symbols(self.cell_iter(selection));
311
312 if selection.trim_trailing_whitespace {
313 text.lines().map(str::trim_end).join_compact("\n")
314 } else {
315 text
316 }
317 }
318}