1use std::{borrow::Cow, marker::PhantomData};
4
5use papergrid::{
6 records::{empty::EmptyRecords, Records, RecordsMut},
7 util::cut_str,
8 width::{CfgWidthFunction, WidthFunc},
9 Entity, GridConfig,
10};
11
12use crate::{
13 peaker::{Peaker, PriorityNone},
14 width::{count_borders, get_table_widths, get_table_widths_with_total, Measurment},
15 CellOption, Table, TableOption, Width,
16};
17
18#[derive(Debug)]
37pub struct Truncate<'a, W = usize, P = PriorityNone> {
38 width: W,
39 suffix: Option<TruncateSuffix<'a>>,
40 _priority: PhantomData<P>,
41}
42
43#[derive(Debug)]
44struct TruncateSuffix<'a> {
45 text: Cow<'a, str>,
46 limit: SuffixLimit,
47 #[cfg(feature = "color")]
48 try_color: bool,
49}
50
51impl Default for TruncateSuffix<'_> {
52 fn default() -> Self {
53 Self {
54 text: Cow::default(),
55 limit: SuffixLimit::Cut,
56 #[cfg(feature = "color")]
57 try_color: false,
58 }
59 }
60}
61
62#[derive(Debug, Clone, Copy)]
64pub enum SuffixLimit {
65 Cut,
67 Ignore,
69 Replace(char),
71}
72
73impl<W> Truncate<'static, W>
74where
75 W: Measurment<Width>,
76{
77 pub fn new(width: W) -> Truncate<'static, W> {
79 Self {
80 width,
81 suffix: None,
82 _priority: PhantomData::default(),
83 }
84 }
85}
86
87impl<'a, W, P> Truncate<'a, W, P> {
88 pub fn suffix<S: Into<Cow<'a, str>>>(self, suffix: S) -> Truncate<'a, W, P> {
97 let mut suff = self.suffix.unwrap_or_default();
98 suff.text = suffix.into();
99
100 Truncate {
101 width: self.width,
102 suffix: Some(suff),
103 _priority: PhantomData::default(),
104 }
105 }
106
107 pub fn suffix_limit(self, limit: SuffixLimit) -> Truncate<'a, W, P> {
109 let mut suff = self.suffix.unwrap_or_default();
110 suff.limit = limit;
111
112 Truncate {
113 width: self.width,
114 suffix: Some(suff),
115 _priority: PhantomData::default(),
116 }
117 }
118
119 #[cfg(feature = "color")]
120 pub fn suffix_try_color(self, color: bool) -> Truncate<'a, W, P> {
122 let mut suff = self.suffix.unwrap_or_default();
123 suff.try_color = color;
124
125 Truncate {
126 width: self.width,
127 suffix: Some(suff),
128 _priority: PhantomData::default(),
129 }
130 }
131}
132
133impl<'a, W, P> Truncate<'a, W, P> {
134 pub fn priority<PP: Peaker>(self) -> Truncate<'a, W, PP> {
143 Truncate {
144 width: self.width,
145 suffix: self.suffix,
146 _priority: PhantomData::default(),
147 }
148 }
149}
150
151impl<W, P, R> CellOption<R> for Truncate<'_, W, P>
152where
153 W: Measurment<Width>,
154 R: Records + RecordsMut<String>,
155{
156 fn change_cell(&mut self, table: &mut Table<R>, entity: Entity) {
157 let width_ctrl = CfgWidthFunction::from_cfg(table.get_config());
158 let set_width = self.width.measure(table.get_records(), table.get_config());
159
160 let mut width = set_width;
161 let suffix = match self.suffix.as_ref() {
162 Some(suffix) => {
163 let suffix_length = width_ctrl.width(&suffix.text);
164 if width > suffix_length {
165 width -= suffix_length;
166 Cow::Borrowed(suffix.text.as_ref())
167 } else {
168 match suffix.limit {
169 SuffixLimit::Ignore => Cow::Borrowed(""),
170 SuffixLimit::Cut => {
171 width = 0;
172 cut_str(&suffix.text, set_width)
173 }
174 SuffixLimit::Replace(c) => {
175 width = 0;
176 Cow::Owned(std::iter::repeat(c).take(set_width).collect())
177 }
178 }
179 }
180 }
181 None => Cow::Borrowed(""),
182 };
183
184 let (count_rows, count_cols) = table.shape();
185 for pos in entity.iter(count_rows, count_cols) {
186 let cell_width = table.get_records().get_width(pos, &width_ctrl);
187 if set_width >= cell_width {
188 continue;
189 }
190
191 let suffix_color_try_keeping;
192 #[cfg(not(feature = "color"))]
193 {
194 suffix_color_try_keeping = false;
195 }
196 #[cfg(feature = "color")]
197 {
198 suffix_color_try_keeping = self.suffix.as_ref().map_or(false, |s| s.try_color);
199 }
200
201 let records = table.get_records();
202 let text = records.get_text(pos);
203 let text = papergrid::util::replace_tab(text, table.get_config().get_tab_width());
207 let text = truncate_text(&text, width, set_width, &suffix, suffix_color_try_keeping)
208 .into_owned();
209
210 let records = table.get_records_mut();
211 records.set(pos, text, &width_ctrl);
212 }
213
214 table.destroy_width_cache();
215 }
216}
217
218impl<W, P, R> TableOption<R> for Truncate<'_, W, P>
219where
220 W: Measurment<Width>,
221 P: Peaker,
222 R: Records + RecordsMut<String>,
223{
224 fn change(&mut self, table: &mut Table<R>) {
225 if table.is_empty() {
226 return;
227 }
228
229 let width = self.width.measure(table.get_records(), table.get_config());
230 let (widths, total_width) =
231 get_table_widths_with_total(table.get_records(), table.get_config());
232 if total_width <= width {
233 return;
234 }
235
236 let suffix = self.suffix.as_ref().map(|s| TruncateSuffix {
237 limit: s.limit,
238 text: Cow::Borrowed(&s.text),
239 #[cfg(feature = "color")]
240 try_color: s.try_color,
241 });
242
243 truncate_total_width(table, widths, total_width, width, suffix, P::create());
244 }
245}
246
247fn truncate_text<'a>(
248 content: &'a str,
249 width: usize,
250 original_width: usize,
251 suffix: &'a str,
252 _suffix_color_try_keeping: bool,
253) -> Cow<'a, str> {
254 if width == 0 {
255 if original_width == 0 {
256 Cow::Borrowed("")
257 } else {
258 Cow::Borrowed(suffix)
259 }
260 } else {
261 let content = cut_str(content, width);
262
263 if suffix.is_empty() {
264 content
265 } else {
266 #[cfg(feature = "color")]
267 {
268 if _suffix_color_try_keeping {
269 if let Some(clr) = ansi_str::get_blocks(&content).last() {
270 if clr.has_ansi() {
271 Cow::Owned(format!("{}{}{}{}", content, clr.start(), suffix, clr.end()))
272 } else {
273 let mut content = content.into_owned();
274 content.push_str(suffix);
275 Cow::Owned(content)
276 }
277 } else {
278 let mut content = content.into_owned();
279 content.push_str(suffix);
280 Cow::Owned(content)
281 }
282 } else {
283 let mut content = content.into_owned();
284 content.push_str(suffix);
285 Cow::Owned(content)
286 }
287 }
288
289 #[cfg(not(feature = "color"))]
290 {
291 let mut content = content.into_owned();
292 content.push_str(suffix);
293 Cow::Owned(content)
294 }
295 }
296 }
297}
298
299pub(crate) fn get_decrease_cell_list(
300 cfg: &GridConfig,
301 widths: &[usize],
302 min_widths: &[usize],
303 (count_rows, count_cols): (usize, usize),
304) -> Vec<((usize, usize), usize)> {
305 let mut points = Vec::new();
306 (0..count_cols).for_each(|col| {
307 (0..count_rows)
308 .filter(|&row| cfg.is_cell_visible((row, col), (count_rows, count_cols)))
309 .for_each(|row| {
310 let (width, width_min) =
311 match cfg.get_column_span((row, col), (count_rows, count_cols)) {
312 Some(span) => {
313 let width = (col..col + span).map(|i| widths[i]).sum::<usize>();
314 let min_width = (col..col + span).map(|i| min_widths[i]).sum::<usize>();
315 let count_borders = count_borders(cfg, col, col + span, count_cols);
316 (width + count_borders, min_width + count_borders)
317 }
318 None => (widths[col], min_widths[col]),
319 };
320
321 if width >= width_min {
322 let padding = cfg.get_padding((row, col).into());
323 let width = width.saturating_sub(padding.left.size + padding.right.size);
324
325 points.push(((row, col), width));
326 }
327 });
328 });
329
330 points
331}
332
333pub(crate) fn decrease_widths<F>(
334 widths: &mut [usize],
335 min_widths: &[usize],
336 total_width: usize,
337 mut width: usize,
338 mut peeaker: F,
339) where
340 F: Peaker,
341{
342 let mut empty_list = 0;
343 for col in 0..widths.len() {
344 if widths[col] == 0 || widths[col] <= min_widths[col] {
345 empty_list += 1;
346 }
347 }
348
349 while width != total_width {
350 if empty_list == widths.len() {
351 break;
352 }
353
354 let col = match peeaker.peak(min_widths, widths) {
355 Some(col) => col,
356 None => break,
357 };
358
359 if widths[col] == 0 || widths[col] <= min_widths[col] {
360 continue;
361 }
362
363 widths[col] -= 1;
364
365 if widths[col] == 0 || widths[col] <= min_widths[col] {
366 empty_list += 1;
367 }
368
369 width += 1;
370 }
371}
372
373fn truncate_total_width<P, R>(
374 table: &mut Table<R>,
375 mut widths: Vec<usize>,
376 widths_total: usize,
377 width: usize,
378 suffix: Option<TruncateSuffix<'_>>,
379 priority: P,
380) where
381 P: Peaker,
382 R: Records + RecordsMut<String>,
383{
384 let (count_rows, count_cols) = table.shape();
385 let cfg = table.get_config();
386 let min_widths = get_table_widths(EmptyRecords::new(count_rows, count_cols), cfg);
387
388 decrease_widths(&mut widths, &min_widths, widths_total, width, priority);
389
390 let points = get_decrease_cell_list(cfg, &widths, &min_widths, (count_rows, count_cols));
391
392 let mut truncate = Truncate::new(0);
393 truncate.suffix = suffix;
394 for ((row, col), width) in points {
395 truncate.width = width;
396 truncate.change_cell(table, (row, col).into());
397 }
398
399 table.destroy_width_cache();
400 table.destroy_height_cache();
401 table.cache_width(widths);
402}
403
404#[cfg(feature = "color")]
405#[cfg(test)]
406mod tests {
407 use owo_colors::{colors::Yellow, OwoColorize};
408 use papergrid::util::cut_str;
409
410 #[test]
411 fn test_color_strip() {
412 let s = "Collored string"
413 .fg::<Yellow>()
414 .on_truecolor(12, 200, 100)
415 .blink()
416 .to_string();
417 assert_eq!(
418 cut_str(&s, 1),
419 "\u{1b}[5m\u{1b}[48;2;12;200;100m\u{1b}[33mC\u{1b}[25m\u{1b}[39m\u{1b}[49m"
420 )
421 }
422}