mirror of
https://github.com/l0ng-ai/tty7.git
synced 2026-09-22 00:02:23 +00:00
fix(cli): clamp a tab title by columns, since a column is what it bounds
`clamp` is there so "one talkative tab cannot widen every column in the
table" -- its own comment. It counted characters, so a CJK title went
through at two columns each:
@1 工作工作...工作… tty7 1 <- 40 chars, 79 columns
and `tab ls` came out 98 columns wide, past an 80-column terminal, doing
the exact thing the clamp exists to prevent. Every locale this app ships
a UI for is affected, and none of the ASCII tests could see it.
The rest of the file was already careful here -- `width()` exists, and its
comment says padding by `len()` would push later columns out of line --
so this was one function measuring in different units from its caller.
Now it fills a column budget, dropping a wide character whole when it
would straddle the end: half of one is not a narrower character, it is a
different one. `saturating_sub` on the ellipsis's own column also retires
an underflow that `clamp(s, 0)` would have panicked on, unreachable as it
was with a single caller passing 40.
Measured on a live server before and after: 98 columns down to 58, with
every row still starting its fields at the same place.
This commit is contained in:
@@ -1,4 +1,4 @@
|
||||
use unicode_width::UnicodeWidthStr;
|
||||
use unicode_width::{UnicodeWidthChar, UnicodeWidthStr};
|
||||
|
||||
use tty7_core::core::machine::{Machine, PaneNode, Workspace};
|
||||
use tty7_core::core::session::WorkspaceId;
|
||||
@@ -36,12 +36,35 @@ pub fn tab_label(view: &TabView) -> String {
|
||||
}
|
||||
}
|
||||
|
||||
/// `max` characters at most, with an ellipsis in place of what was dropped.
|
||||
/// `max` display columns at most, with an ellipsis in place of what was
|
||||
/// dropped.
|
||||
///
|
||||
/// Columns rather than characters, because the caller is bounding a table:
|
||||
/// counting characters let a CJK title through at two columns each, so a
|
||||
/// clamp to 40 came out 79 wide and widened every column in `tab ls` — the
|
||||
/// one thing the clamp is there to stop.
|
||||
///
|
||||
/// A wide character that would straddle the limit is dropped whole. Half of
|
||||
/// one is not a narrower character, it is a different one.
|
||||
fn clamp(s: &str, max: usize) -> String {
|
||||
match s.chars().count() > max {
|
||||
true => s.chars().take(max - 1).chain(['…']).collect(),
|
||||
false => s.to_string(),
|
||||
if width(s) <= max {
|
||||
return s.to_string();
|
||||
}
|
||||
// The ellipsis has to fit inside `max` as well; `saturating_sub` because a
|
||||
// caller asking for zero columns should get the ellipsis, not a panic.
|
||||
let budget = max.saturating_sub(1);
|
||||
let mut out = String::new();
|
||||
let mut used = 0;
|
||||
for c in s.chars() {
|
||||
let w = c.width().unwrap_or(0);
|
||||
if used + w > budget {
|
||||
break;
|
||||
}
|
||||
out.push(c);
|
||||
used += w;
|
||||
}
|
||||
out.push('…');
|
||||
out
|
||||
}
|
||||
|
||||
/// The last segment of a path, for columns that have room for a word and not
|
||||
@@ -647,4 +670,37 @@ mod tests {
|
||||
"columns drifted between rows: {starts:?} in {rendered:?}"
|
||||
);
|
||||
}
|
||||
|
||||
/// The clamp exists to bound a column, so it has to count what a column
|
||||
/// is measured in.
|
||||
///
|
||||
/// Counting characters, a 40-char CJK title came out 79 columns wide and
|
||||
/// pushed `tab ls` to 98 columns — past an 80-column terminal, and exactly
|
||||
/// the runaway the clamp is there to prevent.
|
||||
#[test]
|
||||
fn a_clamp_bounds_columns_not_characters() {
|
||||
let ascii = "a".repeat(100);
|
||||
assert_eq!(width(&clamp(&ascii, 40)), 40, "ASCII fills the budget");
|
||||
|
||||
let cjk = "工作".repeat(50);
|
||||
let clamped = clamp(&cjk, 40);
|
||||
assert!(
|
||||
width(&clamped) <= 40,
|
||||
"{} columns from a 40-column clamp: {clamped}",
|
||||
width(&clamped)
|
||||
);
|
||||
assert!(clamped.ends_with('…'), "something was dropped: {clamped}");
|
||||
|
||||
// An odd budget cannot be filled exactly by two-column characters, and
|
||||
// half of a wide character is a different character, not a narrow one.
|
||||
let odd = clamp(&cjk, 9);
|
||||
assert!(width(&odd) <= 9, "{} columns: {odd}", width(&odd));
|
||||
|
||||
// Short enough to keep is returned whole, ellipsis and all.
|
||||
assert_eq!(clamp("工作", 40), "工作");
|
||||
assert_eq!(clamp("plain", 40), "plain");
|
||||
|
||||
// The old arithmetic underflowed here rather than returning anything.
|
||||
assert_eq!(clamp(&ascii, 0), "…");
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user