fix: add ellipsis for truncated status indicator (#12540)

#### What

- Add ellipsis truncation of the status indicator, similar to equivalent
truncation done in the footer.
- Extract truncation helpers into separate file



https://github.com/user-attachments/assets/a2d5f22f-8adc-456e-8059-97359194c25c


#### Tests
Updated relevant snapshot tests
This commit is contained in:
sayan-oai
2026-02-23 11:45:46 -08:00
committed by GitHub
Unverified
parent 7f75e74201
commit bfe622f495
10 changed files with 113 additions and 98 deletions
@@ -110,10 +110,10 @@
//! overall state machine, since it affects which transitions are even possible from a given UI
//! state.
use crate::bottom_pane::footer::mode_indicator_line;
use crate::bottom_pane::selection_popup_common::truncate_line_with_ellipsis_if_overflow;
use crate::key_hint;
use crate::key_hint::KeyBinding;
use crate::key_hint::has_ctrl_or_alt;
use crate::line_truncation::truncate_line_with_ellipsis_if_overflow;
use crate::ui_consts::FOOTER_INDENT_COLS;
use crossterm::event::KeyCode;
use crossterm::event::KeyEvent;
+1 -1
View File
@@ -990,7 +990,7 @@ const SHORTCUTS: &[ShortcutDescriptor] = &[
#[cfg(test)]
mod tests {
use super::*;
use crate::bottom_pane::selection_popup_common::truncate_line_with_ellipsis_if_overflow;
use crate::line_truncation::truncate_line_with_ellipsis_if_overflow;
use crate::test_backend::VT100Backend;
use insta::assert_snapshot;
use pretty_assertions::assert_eq;
@@ -46,8 +46,8 @@ use crate::bottom_pane::bottom_pane_view::BottomPaneView;
use crate::bottom_pane::popup_consts::MAX_POPUP_ROWS;
use crate::bottom_pane::scroll_state::ScrollState;
use crate::bottom_pane::selection_popup_common::render_rows_single_line;
use crate::bottom_pane::selection_popup_common::truncate_line_with_ellipsis_if_overflow;
use crate::key_hint;
use crate::line_truncation::truncate_line_with_ellipsis_if_overflow;
use crate::render::Insets;
use crate::render::RectExt;
use crate::render::renderable::ColumnRenderable;
@@ -14,6 +14,7 @@ use unicode_width::UnicodeWidthChar;
use unicode_width::UnicodeWidthStr;
use crate::key_hint::KeyBinding;
use crate::line_truncation::truncate_line_with_ellipsis_if_overflow;
use crate::render::Insets;
use crate::render::RectExt as _;
use crate::style::user_message_style;
@@ -105,12 +106,6 @@ pub(crate) fn wrap_styled_line<'a>(line: &'a Line<'a>, width: u16) -> Vec<Line<'
word_wrap_line(line, opts)
}
fn line_width(line: &Line<'_>) -> usize {
line.iter()
.map(|span| UnicodeWidthStr::width(span.content.as_ref()))
.sum()
}
fn line_to_owned(line: Line<'_>) -> Line<'static> {
Line {
style: line.style,
@@ -126,91 +121,6 @@ fn line_to_owned(line: Line<'_>) -> Line<'static> {
}
}
pub(crate) fn truncate_line_to_width(line: Line<'static>, max_width: usize) -> Line<'static> {
if max_width == 0 {
return Line::from(Vec::<Span<'static>>::new());
}
let Line {
style,
alignment,
spans,
} = line;
let mut used = 0usize;
let mut spans_out: Vec<Span<'static>> = Vec::new();
for span in spans {
let text = span.content.into_owned();
let style = span.style;
let span_width = UnicodeWidthStr::width(text.as_str());
if span_width == 0 {
spans_out.push(Span::styled(text, style));
continue;
}
if used >= max_width {
break;
}
if used + span_width <= max_width {
used += span_width;
spans_out.push(Span::styled(text, style));
continue;
}
let mut truncated = String::new();
for ch in text.chars() {
let ch_width = UnicodeWidthChar::width(ch).unwrap_or(0);
if used + ch_width > max_width {
break;
}
truncated.push(ch);
used += ch_width;
}
if !truncated.is_empty() {
spans_out.push(Span::styled(truncated, style));
}
break;
}
Line {
style,
alignment,
spans: spans_out,
}
}
pub(crate) fn truncate_line_with_ellipsis_if_overflow(
line: Line<'static>,
max_width: usize,
) -> Line<'static> {
if max_width == 0 {
return Line::from(Vec::<Span<'static>>::new());
}
let width = line_width(&line);
if width <= max_width {
return line;
}
let truncated = truncate_line_to_width(line, max_width.saturating_sub(1));
let Line {
style,
alignment,
mut spans,
} = truncated;
let ellipsis_style = spans.last().map(|span| span.style).unwrap_or_default();
spans.push(Span::styled("", ellipsis_style));
Line {
style,
alignment,
spans,
}
}
fn compute_desc_col(
rows_all: &[GenericDisplayRow],
start_idx: usize,
@@ -2,7 +2,7 @@
source: tui/src/bottom_pane/mod.rs
expression: "render_snapshot(&pane, area)"
---
• Working (0s • esc to interru
• Working (0s • esc to interr
Ask Codex to do anything
@@ -3,7 +3,7 @@ source: tui/src/chatwidget/tests.rs
expression: terminal.backend()
---
" "
"• Working (0s • esc to interrupt) · 1 background terminal running · /ps to view "
"• Working (0s • esc to interrupt) · 1 background terminal running · /ps to view"
" "
" "
" Ask Codex to do anything "
+1
View File
@@ -80,6 +80,7 @@ mod get_git_diff;
mod history_cell;
pub mod insert_history;
mod key_hint;
mod line_truncation;
pub mod live_wrap;
mod markdown;
mod markdown_render;
+100
View File
@@ -0,0 +1,100 @@
use ratatui::text::Line;
use ratatui::text::Span;
use unicode_width::UnicodeWidthChar;
use unicode_width::UnicodeWidthStr;
pub(crate) fn line_width(line: &Line<'_>) -> usize {
line.iter()
.map(|span| UnicodeWidthStr::width(span.content.as_ref()))
.sum()
}
pub(crate) fn truncate_line_to_width(line: Line<'static>, max_width: usize) -> Line<'static> {
if max_width == 0 {
return Line::from(Vec::<Span<'static>>::new());
}
let Line {
style,
alignment,
spans,
} = line;
let mut used = 0usize;
let mut spans_out: Vec<Span<'static>> = Vec::with_capacity(spans.len());
for span in spans {
let span_width = UnicodeWidthStr::width(span.content.as_ref());
if span_width == 0 {
spans_out.push(span);
continue;
}
if used >= max_width {
break;
}
if used + span_width <= max_width {
used += span_width;
spans_out.push(span);
continue;
}
let style = span.style;
let text = span.content.as_ref();
let mut end_idx = 0usize;
for (idx, ch) in text.char_indices() {
let ch_width = UnicodeWidthChar::width(ch).unwrap_or(0);
if used + ch_width > max_width {
break;
}
end_idx = idx + ch.len_utf8();
used += ch_width;
}
if end_idx > 0 {
spans_out.push(Span::styled(text[..end_idx].to_string(), style));
}
break;
}
Line {
style,
alignment,
spans: spans_out,
}
}
/// Truncate a styled line to `max_width` and append an ellipsis on overflow.
///
/// Intended for short UI rows. This preserves a fast no-overflow path (width
/// pre-scan + return original line unchanged) and uses `truncate_line_to_width`
/// for the overflow case.
/// Performance should be reevaluated if using this method in loops/over larger content in the future.
pub(crate) fn truncate_line_with_ellipsis_if_overflow(
line: Line<'static>,
max_width: usize,
) -> Line<'static> {
if max_width == 0 {
return Line::from(Vec::<Span<'static>>::new());
}
if line_width(&line) <= max_width {
return line;
}
let truncated = truncate_line_to_width(line, max_width.saturating_sub(1));
let Line {
style,
alignment,
mut spans,
} = truncated;
let ellipsis_style = spans.last().map(|span| span.style).unwrap_or_default();
spans.push(Span::styled("", ellipsis_style));
Line {
style,
alignment,
spans,
}
}
@@ -2,5 +2,5 @@
source: tui/src/status_indicator_widget.rs
expression: terminal.backend()
---
"• Working (0s • esc "
"• Working (0s • esc"
" "
+5 -1
View File
@@ -23,6 +23,7 @@ use crate::app_event::AppEvent;
use crate::app_event_sender::AppEventSender;
use crate::exec_cell::spinner;
use crate::key_hint;
use crate::line_truncation::truncate_line_with_ellipsis_if_overflow;
use crate::render::renderable::Renderable;
use crate::shimmer::shimmer_spans;
use crate::text_formatting::capitalize_first;
@@ -253,7 +254,10 @@ impl Renderable for StatusIndicatorWidget {
}
let mut lines = Vec::new();
lines.push(Line::from(spans));
lines.push(truncate_line_with_ellipsis_if_overflow(
Line::from(spans),
usize::from(area.width),
));
if area.height > 1 {
// If there is enough space, add the details lines below the header.
let details = self.wrapped_details_lines(area.width);