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
73#[must_use]
75pub fn select(mode: SelectionMode) -> CellQuery {
76 CellQuery { mode, ..CellQuery::default() }
77}
78
79impl CellQuery {
80 #[must_use]
85 pub fn start(mut self, start: (u16, u16)) -> Self {
86 self.start = Some(start);
87 self
88 }
89
90 #[must_use]
95 pub fn end(mut self, end: (u16, u16)) -> Self {
96 self.end = Some(end);
97 self
98 }
99
100 #[must_use]
102 pub fn is_empty(&self) -> bool {
103 self.start.is_none() && self.end.is_none()
104 }
105
106 #[must_use]
111 pub fn range(&self) -> Option<((u16, u16), (u16, u16))> {
112 if let (Some(start), Some(end)) = (self.start, self.end) {
113 Some((
114 (start.0.min(end.0), start.1.min(end.1)),
115 (start.0.max(end.0), start.1.max(end.1)),
116 ))
117 } else {
118 None
119 }
120 }
121
122 #[must_use]
127 pub fn trim_trailing_whitespace(mut self, enabled: bool) -> Self {
128 self.trim_trailing_whitespace = enabled;
129 self
130 }
131
132 pub(crate) fn with_content_hash(mut self, hash: u64) -> Self {
137 self.content_hash = Some(hash);
138 self
139 }
140}
141
142impl Iterator for CellIterator {
143 type Item = (usize, bool); fn next(&mut self) -> Option<Self::Item> {
146 match self {
147 CellIterator::Block(iter) => iter.next(),
148 CellIterator::Linear(iter) => iter.next(),
149 }
150 }
151}
152
153impl Iterator for BlockCellIterator {
154 type Item = (usize, bool);
155
156 fn next(&mut self) -> Option<Self::Item> {
157 if self.finished || self.current.1 > self.end.1 {
158 return None;
159 }
160
161 let idx = self.current.1 as usize * self.cols as usize + self.current.0 as usize;
162
163 let is_end_of_row = self.current.0 == self.end.0;
165 let is_last_row = self.current.1 == self.end.1;
166 let needs_newline = is_end_of_row && !is_last_row;
167
168 if self.current.0 < self.end.0 {
170 self.current.0 += 1;
171 } else {
172 self.current.0 = self.start.0;
173 self.current.1 += 1;
174 if self.current.1 > self.end.1 {
175 self.finished = true;
176 }
177 }
178
179 Some((idx, needs_newline))
180 }
181}
182
183impl Iterator for LinearCellIterator {
184 type Item = (usize, bool);
185
186 fn next(&mut self) -> Option<Self::Item> {
187 if self.finished || self.current_idx > self.end_idx {
188 return None;
189 }
190
191 let idx = self.current_idx;
192
193 self.current_idx += 1;
194 if self.current_idx > self.end_idx {
195 self.finished = true;
196 }
197
198 let needs_newline_after = if self.current_idx <= self.end_idx {
200 self.current_idx
201 .is_multiple_of(self.cols as usize)
202 } else {
203 false
204 };
205
206 Some((idx, needs_newline_after))
207 }
208}
209
210impl BlockCellIterator {
211 fn new(cols: u16, start: (u16, u16), end: (u16, u16), max_cells: usize) -> Self {
215 let start = (
217 start.0.min(cols.saturating_sub(1)),
218 start
219 .1
220 .min((max_cells / cols as usize).saturating_sub(1) as u16),
221 );
222 let end = (
223 end.0.min(cols.saturating_sub(1)),
224 end.1
225 .min((max_cells / cols as usize).saturating_sub(1) as u16),
226 );
227 let (start, end) = if start > end { (end, start) } else { (start, end) };
228
229 Self {
230 cols,
231 start,
232 end,
233 current: start,
234 finished: start.1 > end.1,
235 }
236 }
237}
238
239impl LinearCellIterator {
240 fn new(cols: u16, start: (u16, u16), end: (u16, u16), max_cells: usize) -> Self {
244 let cols_usize = cols as usize;
245
246 let start = (
248 start.0.min(cols.saturating_sub(1)),
249 start
250 .1
251 .min((max_cells / cols_usize).saturating_sub(1) as u16),
252 );
253 let end = (
254 end.0.min(cols.saturating_sub(1)),
255 end.1
256 .min((max_cells / cols_usize).saturating_sub(1) as u16),
257 );
258 let (start, end) = if start > end { (end, start) } else { (start, end) };
259
260 let start_idx = start.1 as usize * cols_usize + start.0 as usize;
261 let end_idx = end.1 as usize * cols_usize + end.0 as usize;
262 let end_idx = end_idx.min(max_cells.saturating_sub(1));
263 let start_idx = start_idx.min(end_idx);
264
265 Self {
266 cols,
267 current_idx: start_idx,
268 end_idx,
269 finished: start_idx > end_idx,
270 }
271 }
272}
273
274impl TerminalGrid {
275 #[must_use]
283 pub fn cell_iter(&self, selection: CellQuery) -> CellIterator {
284 let cols = self.terminal_size().cols;
285 let max_cells = self.cell_count();
286
287 let (start, end) = selection.range().unwrap_or_default();
288
289 match selection.mode {
290 SelectionMode::Block => {
291 CellIterator::Block(BlockCellIterator::new(cols, start, end, max_cells))
292 },
293 SelectionMode::Linear => {
294 CellIterator::Linear(LinearCellIterator::new(cols, start, end, max_cells))
295 },
296 }
297 }
298
299 pub fn get_text(&self, selection: CellQuery) -> CompactString {
310 if selection.is_empty() {
311 return CompactString::const_new("");
312 }
313
314 let text = self.get_symbols(self.cell_iter(selection));
315
316 if selection.trim_trailing_whitespace {
317 text.lines().map(str::trim_end).join_compact("\n")
318 } else {
319 text
320 }
321 }
322}