mirror of
https://oauth2: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@github.com/m1ngsama/TNT.git
synced 2026-06-26 05:44: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.
|
clients can discover common actions and troubleshooting paths in-product.
|
||||||
- The GitHub workflow formerly named deploy now runs CI only; production
|
- The GitHub workflow formerly named deploy now runs CI only; production
|
||||||
deployment remains a manual operator action.
|
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
|
## 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) */
|
/* Calculate display width of a UTF-8 string (considering CJK double-width) */
|
||||||
int utf8_string_width(const char *str);
|
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 */
|
/* Truncate string to fit within max_width display characters */
|
||||||
void utf8_truncate(char *str, int max_width);
|
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 */
|
/* Count the number of UTF-8 characters in a string */
|
||||||
int utf8_strlen(const char *str);
|
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 */
|
/* Title */
|
||||||
const char *title = " COMMAND OUTPUT ";
|
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;
|
int padding = rw - title_width;
|
||||||
if (padding < 0) padding = 0;
|
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++) {
|
for (int i = 0; i < padding; i++) {
|
||||||
buffer_append_bytes(buffer, sizeof(buffer), &pos, " ", 1);
|
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) {
|
while (line && line_count < max_lines) {
|
||||||
char truncated[1024];
|
char truncated[1024];
|
||||||
strncpy(truncated, line, sizeof(truncated) - 1);
|
utf8_ansi_truncate(line, truncated, sizeof(truncated), rw);
|
||||||
truncated[sizeof(truncated) - 1] = '\0';
|
|
||||||
|
|
||||||
if (utf8_string_width(truncated) > rw) {
|
|
||||||
utf8_truncate(truncated, rw);
|
|
||||||
}
|
|
||||||
|
|
||||||
buffer_appendf(buffer, sizeof(buffer), &pos, "%s\r\n", truncated);
|
buffer_appendf(buffer, sizeof(buffer), &pos, "%s\r\n", truncated);
|
||||||
line = strtok(NULL, "\n");
|
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;
|
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 */
|
/* Count the number of UTF-8 characters in a string */
|
||||||
int utf8_strlen(const char *str) {
|
int utf8_strlen(const char *str) {
|
||||||
int count = 0;
|
int count = 0;
|
||||||
|
|
@ -134,6 +177,72 @@ void utf8_truncate(char *str, int max_width) {
|
||||||
*last_valid = '\0';
|
*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 */
|
/* Remove last UTF-8 character from string */
|
||||||
void utf8_remove_last_char(char *str) {
|
void utf8_remove_last_char(char *str) {
|
||||||
int len = strlen(str);
|
int len = strlen(str);
|
||||||
|
|
|
||||||
|
|
@ -114,6 +114,22 @@ TEST(utf8_string_width_cjk_only) {
|
||||||
assert(utf8_string_width("中文字符") == 8);
|
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 backspace handling */
|
||||||
TEST(utf8_remove_last_char) {
|
TEST(utf8_remove_last_char) {
|
||||||
char buffer[256];
|
char buffer[256];
|
||||||
|
|
@ -228,6 +244,8 @@ int main(void) {
|
||||||
RUN_TEST(utf8_string_width_ascii);
|
RUN_TEST(utf8_string_width_ascii);
|
||||||
RUN_TEST(utf8_string_width_mixed);
|
RUN_TEST(utf8_string_width_mixed);
|
||||||
RUN_TEST(utf8_string_width_cjk_only);
|
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);
|
||||||
RUN_TEST(utf8_remove_last_char_multibyte);
|
RUN_TEST(utf8_remove_last_char_multibyte);
|
||||||
RUN_TEST(utf8_remove_last_word);
|
RUN_TEST(utf8_remove_last_word);
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue