From c66491d4f80a7292de663b43cf85ae16ed718ddd Mon Sep 17 00:00:00 2001 From: m1ngsama Date: Sun, 17 May 2026 13:47:00 +0800 Subject: [PATCH] tui: byte-budget gauge in INSERT input line (UX-4) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The chat input is bounded at MAX_MESSAGE_LEN (1024 bytes). Past that limit the input loop silently drops further keystrokes — which is fine mechanically but leaves the user typing into the void. tui_render_input() now appends a right-aligned gauge to the input line once the buffer crosses 80 % full: › some long message that goes on and on … 187 B (dim grey) › this one is almost at the limit … 12 B (bold yellow > 95 %) Below 80 % the prompt is unchanged. The gauge eats display width from the available content area so the existing horizontal scroll-truncate logic keeps working — long input still scrolls cleanly past the gauge. (Paste handling, which is the other half of the long-input UX gap, lands separately as UX-13.) --- src/tui.c | 44 ++++++++++++++++++++++++++++++++++++++------ 1 file changed, 38 insertions(+), 6 deletions(-) diff --git a/src/tui.c b/src/tui.c index 0ee9bc0..9778719 100644 --- a/src/tui.c +++ b/src/tui.c @@ -467,7 +467,11 @@ void tui_render_screen(client_t *client) { free(buffer); } -/* Render the input line */ +/* Render the input line. + * + * Format: "› " with optional right-aligned length indicator + * once the buffer is past 80% full. The indicator turns bold-yellow + * past 95% so users can see further keystrokes will be dropped. */ void tui_render_input(client_t *client, const char *input) { if (!client || !client->connected) return; @@ -478,7 +482,25 @@ void tui_render_input(client_t *client, const char *input) { char buffer[2048]; int input_width = utf8_string_width(input); - int avail = rw - 3; + size_t input_bytes = strlen(input); + + /* Decide whether to show the length gauge and how loud. */ + int gauge_width = 0; + char gauge[64] = ""; + if (input_bytes > (MAX_MESSAGE_LEN * 8) / 10) { /* > 80 % */ + size_t remaining = (input_bytes < MAX_MESSAGE_LEN) + ? (MAX_MESSAGE_LEN - 1 - input_bytes) : 0; + const char *color = + (input_bytes > (MAX_MESSAGE_LEN * 95) / 100) ? "\033[1;33m" + : "\033[2;37m"; + snprintf(gauge, sizeof(gauge), "%s… %zu B\033[0m", color, remaining); + /* Plain-text width: " … 1234 B" → 4 + len(digits) + 2 */ + char digits[12]; + snprintf(digits, sizeof(digits), "%zu", remaining); + gauge_width = 4 + (int)strlen(digits) + 2; /* "… ", digits, " B" + leading space */ + } + + int avail = rw - 3 - (gauge_width > 0 ? gauge_width + 1 : 0); if (avail < 1) avail = 1; /* Truncate from start if too long */ @@ -501,10 +523,20 @@ void tui_render_input(client_t *client, const char *input) { strncpy(display, p, sizeof(display) - 1); } - /* Move to input line and clear it, then write input */ - snprintf(buffer, sizeof(buffer), - "\033[%d;1H" ANSI_CLEAR_LINE "\033[2;37m›\033[0m %s", - rh, display); + /* Compose: cursor to input row, clear line, "› " prompt, input. + * If a gauge is active, append it right-aligned. */ + if (gauge_width > 0) { + int displayed_width = utf8_string_width(display); + int padding = rw - 2 - displayed_width - gauge_width; + if (padding < 1) padding = 1; + snprintf(buffer, sizeof(buffer), + "\033[%d;1H" ANSI_CLEAR_LINE "\033[2;37m›\033[0m %s%*s%s", + rh, display, padding, "", gauge); + } else { + snprintf(buffer, sizeof(buffer), + "\033[%d;1H" ANSI_CLEAR_LINE "\033[2;37m›\033[0m %s", + rh, display); + } client_send(client, buffer, strlen(buffer)); }