string-util,format-table: measure console width with unichar_console_width()
ellipsize_mem() and align_string_mem() open coded the width of a character as
unichar_iswide(c) ? 2 : 1, but unichar_console_width() counts a tab as eight
cells. The two disagreed, and format-table.c uses both: it decides a field is too
wide with utf8_console_width(), then asks ellipsize() to fit it into that many
columns, and ellipsize() believed a tab was one cell. The ellipsized cell then
still overflowed the column, measured by the very function that had just asked
for it to be truncated. A table pinned to table_set_width(20) emitted lines of
25, 44 and 39 columns. align_string_mem() undercounted the same way and so padded
already overlong cells further.
Use unichar_console_width(), split out for this purpose in 5c0d4b5061. While at
it, let align_string_mem() decode with utf8_char_console_width() as well, so that
the second pass shares both the width model and the validity check with the
measuring pass, instead of keeping a more permissive decoder of its own around
for the next caller to trip over. Its "count invalid chars as 1" fallback now
skips the whole rejected sequence rather than one byte, so that the continuation
bytes are not charged a cell each of their own; the branch should be unreachable
anyway, since our caller only gets there after utf8_console_width() succeeded on
the same string.
Note that unichar_console_width() charges a tab a flat eight cells, while a
terminal advances to the next multiple of eight, so this is deliberately an upper
bound: the two agree only when the tab itself starts on a multiple of eight. A
per character helper cannot do better, because a tab's width is a function of the
column it starts in, and neither ellipsize_mem() nor align_string_mem() is told
what column its string starts at. Overestimating is the safe direction: it can
leave a column padded short, but it cannot overflow one.
ascii_ellipsize_mem() equates bytes with character cells, which holds for ASCII
except for the tab, so keep tabs off that fast path. Like the ANSI sequence case
just above it, such strings now take the generic path, which means they get the
Unicode ellipsis and one reserved cell rather than "..." and three, even in a non
UTF-8 locale.
This commit is contained in:
committed by
Luca Boccassi
parent
cd51add717
commit
0834378fb9
@@ -414,8 +414,10 @@ char* ellipsize_mem(const char *s, size_t old_length, size_t new_length, unsigne
|
||||
|
||||
bool has_ansi_seq = string_has_ansi_sequence(s, old_length);
|
||||
|
||||
/* If no multibyte characters or ANSI sequences, use ascii_ellipsize_mem for speed */
|
||||
if (!has_ansi_seq && ascii_is_valid_n(s, old_length))
|
||||
/* If no multibyte characters or ANSI sequences, use ascii_ellipsize_mem for speed. It equates
|
||||
* bytes with character cells, which holds for ASCII except for the tab, which
|
||||
* unichar_console_width() counts as 8 cells. */
|
||||
if (!has_ansi_seq && ascii_is_valid_n(s, old_length) && !memchr(s, '\t', old_length))
|
||||
return ascii_ellipsize_mem(s, old_length, new_length, percent);
|
||||
|
||||
x = (new_length - 1) * percent / 100;
|
||||
@@ -436,7 +438,7 @@ char* ellipsize_mem(const char *s, size_t old_length, size_t new_length, unsigne
|
||||
char32_t c;
|
||||
assert_se(utf8_encoded_to_unichar(i, &c) == r);
|
||||
|
||||
int w = unichar_iswide(c) ? 2 : 1;
|
||||
int w = unichar_console_width(c);
|
||||
if (k + w > x)
|
||||
break;
|
||||
|
||||
@@ -467,7 +469,7 @@ char* ellipsize_mem(const char *s, size_t old_length, size_t new_length, unsigne
|
||||
if (!tt)
|
||||
return NULL;
|
||||
|
||||
w = unichar_iswide(c) ? 2 : 1;
|
||||
w = unichar_console_width(c);
|
||||
if (k + w > new_length)
|
||||
break;
|
||||
|
||||
|
||||
Reference in New Issue
Block a user