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