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 pub fn start(mut self, start: (u16, u16)) -> Self {
84 self.start = Some(start);
85 self
86 }
87
88 pub fn end(mut self, end: (u16, u16)) -> Self {
93 self.end = Some(end);
94 self
95 }
96
97 pub fn is_empty(&self) -> bool {
99 self.start.is_none() && self.end.is_none()
100 }
101
102 pub fn range(&self) -> Option<((u16, u16), (u16, u16))> {
107 if let (Some(start), Some(end)) = (self.start, self.end) {
108 Some((
109 (start.0.min(end.0), start.1.min(end.1)),
110 (start.0.max(end.0), start.1.max(end.1)),
111 ))
112 } else {
113 None
114 }
115 }
116
117 pub fn trim_trailing_whitespace(mut self, enabled: bool) -> Self {
122 self.trim_trailing_whitespace = enabled;
123 self
124 }
125
126 pub(crate) fn with_content_hash(mut self, hash: u64) -> Self {
131 self.content_hash = Some(hash);
132 self
133 }
134}
135
136impl Iterator for CellIterator {
137 type Item = (usize, bool); fn next(&mut self) -> Option<Self::Item> {
140 match self {
141 CellIterator::Block(iter) => iter.next(),
142 CellIterator::Linear(iter) => iter.next(),
143 }
144 }
145}
146
147impl Iterator for BlockCellIterator {
148 type Item = (usize, bool);
149
150 fn next(&mut self) -> Option<Self::Item> {
151 if self.finished || self.current.1 > self.end.1 {
152 return None;
153 }
154
155 let idx = self.current.1 as usize * self.cols as usize + self.current.0 as usize;
156
157 let is_end_of_row = self.current.0 == self.end.0;
159 let is_last_row = self.current.1 == self.end.1;
160 let needs_newline = is_end_of_row && !is_last_row;
161
162 if self.current.0 < self.end.0 {
164 self.current.0 += 1;
165 } else {
166 self.current.0 = self.start.0;
167 self.current.1 += 1;
168 if self.current.1 > self.end.1 {
169 self.finished = true;
170 }
171 }
172
173 Some((idx, needs_newline))
174 }
175}
176
177impl Iterator for LinearCellIterator {
178 type Item = (usize, bool);
179
180 fn next(&mut self) -> Option<Self::Item> {
181 if self.finished || self.current_idx > self.end_idx {
182 return None;
183 }
184
185 let idx = self.current_idx;
186
187 self.current_idx += 1;
188 if self.current_idx > self.end_idx {
189 self.finished = true;
190 }
191
192 let needs_newline_after = if self.current_idx <= self.end_idx {
194 self.current_idx
195 .is_multiple_of(self.cols as usize)
196 } else {
197 false
198 };
199
200 Some((idx, needs_newline_after))
201 }
202}
203
204impl BlockCellIterator {
205 fn new(cols: u16, start: (u16, u16), end: (u16, u16), max_cells: usize) -> Self {
209 let start = (
211 start.0.min(cols.saturating_sub(1)),
212 start
213 .1
214 .min((max_cells / cols as usize).saturating_sub(1) as u16),
215 );
216 let end = (
217 end.0.min(cols.saturating_sub(1)),
218 end.1
219 .min((max_cells / cols as usize).saturating_sub(1) as u16),
220 );
221 let (start, end) = if start > end { (end, start) } else { (start, end) };
222
223 Self {
224 cols,
225 start,
226 end,
227 current: start,
228 finished: start.1 > end.1,
229 }
230 }
231}
232
233impl LinearCellIterator {
234 fn new(cols: u16, start: (u16, u16), end: (u16, u16), max_cells: usize) -> Self {
238 let cols_usize = cols as usize;
239
240 let start = (
242 start.0.min(cols.saturating_sub(1)),
243 start
244 .1
245 .min((max_cells / cols_usize).saturating_sub(1) as u16),
246 );
247 let end = (
248 end.0.min(cols.saturating_sub(1)),
249 end.1
250 .min((max_cells / cols_usize).saturating_sub(1) as u16),
251 );
252 let (start, end) = if start > end { (end, start) } else { (start, end) };
253
254 let start_idx = start.1 as usize * cols_usize + start.0 as usize;
255 let end_idx = end.1 as usize * cols_usize + end.0 as usize;
256 let end_idx = end_idx.min(max_cells.saturating_sub(1));
257 let start_idx = start_idx.min(end_idx);
258
259 Self {
260 cols,
261 current_idx: start_idx,
262 end_idx,
263 finished: start_idx > end_idx,
264 }
265 }
266}
267
268impl TerminalGrid {
269 pub fn cell_iter(&self, selection: CellQuery) -> CellIterator {
277 let cols = self.terminal_size().0;
278 let max_cells = self.cell_count();
279
280 let (start, end) = selection.range().unwrap_or_default();
281
282 match selection.mode {
283 SelectionMode::Block => {
284 CellIterator::Block(BlockCellIterator::new(cols, start, end, max_cells))
285 },
286 SelectionMode::Linear => {
287 CellIterator::Linear(LinearCellIterator::new(cols, start, end, max_cells))
288 },
289 }
290 }
291
292 pub fn get_text(&self, selection: CellQuery) -> CompactString {
303 if selection.is_empty() {
304 return CompactString::const_new("");
305 }
306
307 let text = self.get_symbols(self.cell_iter(selection));
308
309 if selection.trim_trailing_whitespace {
310 text.lines().map(str::trim_end).join_compact("\n")
311 } else {
312 text
313 }
314 }
315}