Skip to main content

supercode_frontend_tui/foundation/
line_truncation.rs

1// Derived from OpenAI Codex: codex-rs/tui/src/line_truncation.rs
2// Pinned source: 8604689ec5e3437eb79802d8d72249b7722fbf5b
3// Copyright 2025 OpenAI
4// Licensed under the Apache License, Version 2.0.
5// Modified by the Supercode contributors; see docs/legal/codex-frontend-extraction.toml.
6
7use ratatui::text::Line;
8use ratatui::text::Span;
9use unicode_width::UnicodeWidthChar;
10use unicode_width::UnicodeWidthStr;
11
12pub fn line_width(line: &Line<'_>) -> usize {
13    line.iter()
14        .map(|span| UnicodeWidthStr::width(span.content.as_ref()))
15        .sum()
16}
17
18pub fn truncate_line_to_width(line: Line<'static>, max_width: usize) -> Line<'static> {
19    if max_width == 0 {
20        return Line::from(Vec::<Span<'static>>::new());
21    }
22
23    let Line {
24        style,
25        alignment,
26        spans,
27    } = line;
28    let mut used = 0usize;
29    let mut spans_out: Vec<Span<'static>> = Vec::with_capacity(spans.len());
30
31    for span in spans {
32        let span_width = UnicodeWidthStr::width(span.content.as_ref());
33
34        if span_width == 0 {
35            spans_out.push(span);
36            continue;
37        }
38
39        if used >= max_width {
40            break;
41        }
42
43        if used + span_width <= max_width {
44            used += span_width;
45            spans_out.push(span);
46            continue;
47        }
48
49        let style = span.style;
50        let text = span.content.as_ref();
51        let mut end_idx = 0usize;
52        for (idx, ch) in text.char_indices() {
53            let ch_width = UnicodeWidthChar::width(ch).unwrap_or(0);
54            if used + ch_width > max_width {
55                break;
56            }
57            end_idx = idx + ch.len_utf8();
58            used += ch_width;
59        }
60
61        if end_idx > 0 {
62            spans_out.push(Span::styled(text[..end_idx].to_string(), style));
63        }
64
65        break;
66    }
67
68    Line {
69        style,
70        alignment,
71        spans: spans_out,
72    }
73}
74
75/// Truncate a styled line to `max_width` and append an ellipsis on overflow.
76///
77/// Intended for short UI rows. This preserves a fast no-overflow path (width
78/// pre-scan + return original line unchanged) and uses `truncate_line_to_width`
79/// for the overflow case.
80/// Performance should be reevaluated if using this method in loops/over larger content in the future.
81pub fn truncate_line_with_ellipsis_if_overflow(
82    line: Line<'static>,
83    max_width: usize,
84) -> Line<'static> {
85    if max_width == 0 {
86        return Line::from(Vec::<Span<'static>>::new());
87    }
88
89    if line_width(&line) <= max_width {
90        return line;
91    }
92
93    let truncated = truncate_line_to_width(line, max_width.saturating_sub(1));
94    let Line {
95        style,
96        alignment,
97        mut spans,
98    } = truncated;
99    let ellipsis_style = spans.last().map(|span| span.style).unwrap_or_default();
100    spans.push(Span::styled("…", ellipsis_style));
101    Line {
102        style,
103        alignment,
104        spans,
105    }
106}