mirror of
https://oauth2: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@github.com/m1ngsama/TNT.git
synced 2026-06-26 04:34:38 +08:00
tui: preserve ansi styling when truncating output
This commit is contained in:
parent
67d21ad0e9
commit
169ba1a150
5 changed files with 142 additions and 8 deletions
|
|
@ -22,6 +22,8 @@
|
|||
clients can discover common actions and troubleshooting paths in-product.
|
||||
- The GitHub workflow formerly named deploy now runs CI only; production
|
||||
deployment remains a manual operator action.
|
||||
- Command output rendering now truncates ANSI-styled UTF-8 text without
|
||||
counting escape sequences as visible width or cutting color codes.
|
||||
|
||||
## 2026-05-18 - Interactive input polish
|
||||
|
||||
|
|
|
|||
|
|
@ -15,9 +15,16 @@ uint32_t utf8_decode(const char *str, int *bytes_read);
|
|||
/* Calculate display width of a UTF-8 string (considering CJK double-width) */
|
||||
int utf8_string_width(const char *str);
|
||||
|
||||
/* Calculate display width while treating ANSI escape sequences as zero-width */
|
||||
int utf8_ansi_string_width(const char *str);
|
||||
|
||||
/* Truncate string to fit within max_width display characters */
|
||||
void utf8_truncate(char *str, int max_width);
|
||||
|
||||
/* Truncate ANSI-styled UTF-8 text without cutting escape sequences */
|
||||
void utf8_ansi_truncate(const char *src, char *dst, size_t dst_size,
|
||||
int max_width);
|
||||
|
||||
/* Count the number of UTF-8 characters in a string */
|
||||
int utf8_strlen(const char *str);
|
||||
|
||||
|
|
|
|||
14
src/tui.c
14
src/tui.c
|
|
@ -621,11 +621,14 @@ void tui_render_command_output(client_t *client) {
|
|||
|
||||
/* Title */
|
||||
const char *title = " COMMAND OUTPUT ";
|
||||
int title_width = strlen(title);
|
||||
char title_display[64];
|
||||
utf8_ansi_truncate(title, title_display, sizeof(title_display), rw);
|
||||
int title_width = utf8_ansi_string_width(title_display);
|
||||
int padding = rw - title_width;
|
||||
if (padding < 0) padding = 0;
|
||||
|
||||
buffer_appendf(buffer, sizeof(buffer), &pos, ANSI_REVERSE "%s", title);
|
||||
buffer_appendf(buffer, sizeof(buffer), &pos, ANSI_REVERSE "%s",
|
||||
title_display);
|
||||
for (int i = 0; i < padding; i++) {
|
||||
buffer_append_bytes(buffer, sizeof(buffer), &pos, " ", 1);
|
||||
}
|
||||
|
|
@ -642,12 +645,7 @@ void tui_render_command_output(client_t *client) {
|
|||
|
||||
while (line && line_count < max_lines) {
|
||||
char truncated[1024];
|
||||
strncpy(truncated, line, sizeof(truncated) - 1);
|
||||
truncated[sizeof(truncated) - 1] = '\0';
|
||||
|
||||
if (utf8_string_width(truncated) > rw) {
|
||||
utf8_truncate(truncated, rw);
|
||||
}
|
||||
utf8_ansi_truncate(line, truncated, sizeof(truncated), rw);
|
||||
|
||||
buffer_appendf(buffer, sizeof(buffer), &pos, "%s\r\n", truncated);
|
||||
line = strtok(NULL, "\n");
|
||||
|
|
|
|||
109
src/utf8.c
109
src/utf8.c
|
|
@ -96,6 +96,49 @@ int utf8_string_width(const char *str) {
|
|||
return width;
|
||||
}
|
||||
|
||||
static const char *skip_ansi_sequence(const char *p) {
|
||||
if (!p || *p != '\033') {
|
||||
return p;
|
||||
}
|
||||
|
||||
if (p[1] == '[') {
|
||||
const char *q = p + 2;
|
||||
while (*q) {
|
||||
unsigned char c = (unsigned char)*q;
|
||||
if (c >= 0x40 && c <= 0x7E) {
|
||||
return q + 1;
|
||||
}
|
||||
q++;
|
||||
}
|
||||
return p + 1;
|
||||
}
|
||||
|
||||
return p[1] ? p + 2 : p + 1;
|
||||
}
|
||||
|
||||
int utf8_ansi_string_width(const char *str) {
|
||||
int width = 0;
|
||||
int bytes_read;
|
||||
const char *p = str;
|
||||
|
||||
if (!str) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
while (*p != '\0') {
|
||||
if (*p == '\033') {
|
||||
p = skip_ansi_sequence(p);
|
||||
continue;
|
||||
}
|
||||
|
||||
uint32_t codepoint = utf8_decode(p, &bytes_read);
|
||||
width += utf8_char_width(codepoint);
|
||||
p += bytes_read;
|
||||
}
|
||||
|
||||
return width;
|
||||
}
|
||||
|
||||
/* Count the number of UTF-8 characters in a string */
|
||||
int utf8_strlen(const char *str) {
|
||||
int count = 0;
|
||||
|
|
@ -134,6 +177,72 @@ void utf8_truncate(char *str, int max_width) {
|
|||
*last_valid = '\0';
|
||||
}
|
||||
|
||||
void utf8_ansi_truncate(const char *src, char *dst, size_t dst_size,
|
||||
int max_width) {
|
||||
int width = 0;
|
||||
int bytes_read;
|
||||
size_t pos = 0;
|
||||
bool copied_ansi = false;
|
||||
bool last_ansi_was_reset = false;
|
||||
bool truncated = false;
|
||||
const char *p = src;
|
||||
|
||||
if (!dst || dst_size == 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
dst[0] = '\0';
|
||||
if (!src || max_width <= 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
while (*p != '\0') {
|
||||
if (*p == '\033') {
|
||||
const char *end = skip_ansi_sequence(p);
|
||||
size_t len = (size_t)(end - p);
|
||||
|
||||
if (pos + len >= dst_size) {
|
||||
truncated = true;
|
||||
break;
|
||||
}
|
||||
memcpy(dst + pos, p, len);
|
||||
pos += len;
|
||||
copied_ansi = true;
|
||||
last_ansi_was_reset =
|
||||
len == strlen(ANSI_RESET) && memcmp(p, ANSI_RESET, len) == 0;
|
||||
p = end;
|
||||
continue;
|
||||
}
|
||||
|
||||
uint32_t codepoint = utf8_decode(p, &bytes_read);
|
||||
int char_width = utf8_char_width(codepoint);
|
||||
|
||||
if (width + char_width > max_width) {
|
||||
truncated = true;
|
||||
break;
|
||||
}
|
||||
if (pos + (size_t)bytes_read >= dst_size) {
|
||||
truncated = true;
|
||||
break;
|
||||
}
|
||||
|
||||
memcpy(dst + pos, p, (size_t)bytes_read);
|
||||
pos += (size_t)bytes_read;
|
||||
width += char_width;
|
||||
p += bytes_read;
|
||||
}
|
||||
|
||||
if (truncated && copied_ansi && !last_ansi_was_reset) {
|
||||
size_t reset_len = strlen(ANSI_RESET);
|
||||
if (pos + reset_len < dst_size) {
|
||||
memcpy(dst + pos, ANSI_RESET, reset_len);
|
||||
pos += reset_len;
|
||||
}
|
||||
}
|
||||
|
||||
dst[pos] = '\0';
|
||||
}
|
||||
|
||||
/* Remove last UTF-8 character from string */
|
||||
void utf8_remove_last_char(char *str) {
|
||||
int len = strlen(str);
|
||||
|
|
|
|||
|
|
@ -114,6 +114,22 @@ TEST(utf8_string_width_cjk_only) {
|
|||
assert(utf8_string_width("中文字符") == 8);
|
||||
}
|
||||
|
||||
TEST(utf8_ansi_string_width_ignores_escape_sequences) {
|
||||
assert(utf8_ansi_string_width("\033[1;36mHello\033[0m") == 5);
|
||||
assert(utf8_ansi_string_width("\033[31m支持\033[0m") == 4);
|
||||
assert(utf8_ansi_string_width("A\033[7;33m中\033[0mB") == 4);
|
||||
}
|
||||
|
||||
TEST(utf8_ansi_truncate_preserves_escape_sequences) {
|
||||
char out[64];
|
||||
|
||||
utf8_ansi_truncate("\033[31mHello世界\033[0m", out, sizeof(out), 7);
|
||||
assert(strcmp(out, "\033[31mHello世\033[0m") == 0);
|
||||
|
||||
utf8_ansi_truncate("A\033[7;33m中文\033[0mB", out, sizeof(out), 5);
|
||||
assert(strcmp(out, "A\033[7;33m中文\033[0m") == 0);
|
||||
}
|
||||
|
||||
/* Test backspace handling */
|
||||
TEST(utf8_remove_last_char) {
|
||||
char buffer[256];
|
||||
|
|
@ -228,6 +244,8 @@ int main(void) {
|
|||
RUN_TEST(utf8_string_width_ascii);
|
||||
RUN_TEST(utf8_string_width_mixed);
|
||||
RUN_TEST(utf8_string_width_cjk_only);
|
||||
RUN_TEST(utf8_ansi_string_width_ignores_escape_sequences);
|
||||
RUN_TEST(utf8_ansi_truncate_preserves_escape_sequences);
|
||||
RUN_TEST(utf8_remove_last_char);
|
||||
RUN_TEST(utf8_remove_last_char_multibyte);
|
||||
RUN_TEST(utf8_remove_last_word);
|
||||
|
|
|
|||
Loading…
Reference in a new issue