mirror of
https://oauth2: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@github.com/m1ngsama/TNT.git
synced 2026-06-26 10:04:39 +08:00
NORMAL mode status line: before: "-- NORMAL -- (3/100) ↓ 5 new" after : "[reverse-yellow NORMAL] 3 / 100 ▼ 5 new" - the mode is now a small reverse-video yellow chip so it visually echoes the mode colour in the title bar - position is dim grey with em-spaced slash, "▼" replaces "↓" so the unseen-message marker matches the same downward-triangle vocabulary the help screen uses INSERT mode prompt: before: "> " after : "› " (U+203A, dim grey) - single-glyph chevron is quieter than the ASCII ">", aligns with the thinner aesthetic of the new title bar - applied both in tui_render_screen() and tui_render_input() so per-keystroke redraws match the initial paint COMMAND mode prompt: before: ":foo" after : "[magenta :]foo" - the leading colon picks up the COMMAND mode colour so it reads as "you are in command mode now" rather than as part of the typed text
681 lines
26 KiB
C
681 lines
26 KiB
C
#include "tui.h"
|
||
#include "client.h"
|
||
#include "ssh_server.h"
|
||
#include "chat_room.h"
|
||
#include "utf8.h"
|
||
#include <unistd.h>
|
||
|
||
static bool is_join_leave_msg(const message_t *msg) {
|
||
if (strcmp(msg->username, "系统") != 0) return false;
|
||
return strstr(msg->content, "加入了聊天室") != NULL ||
|
||
strstr(msg->content, "离开了聊天室") != NULL;
|
||
}
|
||
|
||
static const char *username_color(const char *name) {
|
||
static const char *colors[] = {
|
||
"\033[31m", "\033[32m", "\033[33m",
|
||
"\033[34m", "\033[35m", "\033[36m",
|
||
};
|
||
unsigned int h = 5381;
|
||
for (const char *p = name; *p; p++)
|
||
h = h * 33 + (unsigned char)*p;
|
||
return colors[h % 6];
|
||
}
|
||
|
||
static void format_message_colored(const message_t *msg, char *buffer,
|
||
size_t buf_size, int width,
|
||
const char *my_username) {
|
||
struct tm tm_info;
|
||
localtime_r(&msg->timestamp, &tm_info);
|
||
char time_str[32];
|
||
strftime(time_str, sizeof(time_str), "%H:%M", &tm_info);
|
||
|
||
bool mentioned = false;
|
||
if (my_username && my_username[0] != '\0' &&
|
||
strcmp(msg->username, "系统") != 0) {
|
||
char mention[MAX_USERNAME_LEN + 2];
|
||
snprintf(mention, sizeof(mention), "@%s", my_username);
|
||
if (strstr(msg->content, mention) != NULL) {
|
||
mentioned = true;
|
||
}
|
||
}
|
||
const char *hl_start = mentioned ? "\033[1;33m" : "";
|
||
const char *hl_end = mentioned ? "\033[0m" : "";
|
||
|
||
if (strcmp(msg->username, "系统") == 0) {
|
||
snprintf(buffer, buf_size,
|
||
"\033[90m--> %s\033[0m", msg->content);
|
||
} else if (strcmp(msg->username, "*") == 0) {
|
||
snprintf(buffer, buf_size,
|
||
"\033[90m%s\033[0m \033[3;36m* %s\033[0m",
|
||
time_str, msg->content);
|
||
} else {
|
||
snprintf(buffer, buf_size,
|
||
"\033[90m%s\033[0m %s%s\033[0m: %s%s%s",
|
||
time_str, username_color(msg->username),
|
||
msg->username, hl_start, msg->content, hl_end);
|
||
}
|
||
|
||
/* Plain-text version for width calculation */
|
||
char plain[MAX_MESSAGE_LEN + 128];
|
||
if (strcmp(msg->username, "系统") == 0) {
|
||
snprintf(plain, sizeof(plain), "--> %s", msg->content);
|
||
} else if (strcmp(msg->username, "*") == 0) {
|
||
snprintf(plain, sizeof(plain), "%s * %s", time_str, msg->content);
|
||
} else {
|
||
snprintf(plain, sizeof(plain), "%s %s: %s",
|
||
time_str, msg->username, msg->content);
|
||
}
|
||
|
||
if (utf8_string_width(plain) > width) {
|
||
/* Rebuild with truncated content */
|
||
int prefix_width;
|
||
char prefix_plain[256];
|
||
if (strcmp(msg->username, "系统") == 0) {
|
||
snprintf(prefix_plain, sizeof(prefix_plain), "--> ");
|
||
} else if (strcmp(msg->username, "*") == 0) {
|
||
snprintf(prefix_plain, sizeof(prefix_plain), "%s * ", time_str);
|
||
} else {
|
||
snprintf(prefix_plain, sizeof(prefix_plain), "%s %s: ",
|
||
time_str, msg->username);
|
||
}
|
||
prefix_width = utf8_string_width(prefix_plain);
|
||
int content_width = width - prefix_width;
|
||
if (content_width < 4) content_width = 4;
|
||
|
||
char truncated_content[MAX_MESSAGE_LEN];
|
||
if (strcmp(msg->username, "系统") == 0) {
|
||
strncpy(truncated_content, msg->content, sizeof(truncated_content) - 1);
|
||
truncated_content[sizeof(truncated_content) - 1] = '\0';
|
||
} else if (strcmp(msg->username, "*") == 0) {
|
||
snprintf(truncated_content, sizeof(truncated_content), "* %s", msg->content);
|
||
} else {
|
||
strncpy(truncated_content, msg->content, sizeof(truncated_content) - 1);
|
||
truncated_content[sizeof(truncated_content) - 1] = '\0';
|
||
}
|
||
utf8_truncate(truncated_content, content_width);
|
||
|
||
if (strcmp(msg->username, "系统") == 0) {
|
||
snprintf(buffer, buf_size,
|
||
"\033[90m--> %s\033[0m", truncated_content);
|
||
} else if (strcmp(msg->username, "*") == 0) {
|
||
snprintf(buffer, buf_size,
|
||
"\033[90m%s\033[0m \033[3;36m%s\033[0m",
|
||
time_str, truncated_content);
|
||
} else {
|
||
snprintf(buffer, buf_size,
|
||
"\033[90m%s\033[0m %s%s\033[0m: %s%s%s",
|
||
time_str, username_color(msg->username),
|
||
msg->username, hl_start, truncated_content, hl_end);
|
||
}
|
||
}
|
||
}
|
||
|
||
/* Clear the screen */
|
||
void tui_clear_screen(client_t *client) {
|
||
if (!client || !client->connected) return;
|
||
const char *clear = ANSI_CLEAR ANSI_HOME;
|
||
client_send(client, clear, strlen(clear));
|
||
}
|
||
|
||
/* Render the pre-login welcome banner.
|
||
*
|
||
* Centred horizontally; vertically positioned about a third of the way down
|
||
* the available height so the user's eye lands naturally on it before the
|
||
* prompt below. Uses light box-drawing characters (U+256D / U+2570) so the
|
||
* frame matches the rest of the TUI's aesthetic instead of the older ASCII
|
||
* `==` rules. */
|
||
void tui_render_welcome(client_t *client) {
|
||
if (!client || !client->connected) return;
|
||
|
||
int rw = client->width;
|
||
int rh = client->height;
|
||
if (rw < 10) rw = 10;
|
||
if (rh < 4) rh = 4;
|
||
|
||
/* Lines, in display order. Width is computed in display columns. */
|
||
const char *line1 = "TNT · " TNT_VERSION;
|
||
const char *line2 = "匿名聊天室 · SSH";
|
||
const char *line3 = "Anonymous chat over SSH";
|
||
|
||
int inner_w = utf8_string_width(line1);
|
||
int w2 = utf8_string_width(line2);
|
||
int w3 = utf8_string_width(line3);
|
||
if (w2 > inner_w) inner_w = w2;
|
||
if (w3 > inner_w) inner_w = w3;
|
||
inner_w += 4; /* 2 columns padding on each side */
|
||
|
||
/* Fall back to plain prompt if the terminal is too narrow for the frame. */
|
||
if (inner_w + 2 > rw) {
|
||
char fallback[128];
|
||
int n = snprintf(fallback, sizeof(fallback),
|
||
ANSI_CLEAR ANSI_HOME
|
||
"TNT %s — anonymous chat over SSH\r\n\r\n",
|
||
TNT_VERSION);
|
||
if (n > 0) client_send(client, fallback, (size_t)n);
|
||
return;
|
||
}
|
||
|
||
int top_pad = rh / 3;
|
||
if (top_pad < 1) top_pad = 1;
|
||
int left_pad = (rw - (inner_w + 2)) / 2;
|
||
if (left_pad < 0) left_pad = 0;
|
||
|
||
/* ~5 KiB is plenty for the framed banner even on the largest terminals. */
|
||
char buf[4096];
|
||
size_t pos = 0;
|
||
|
||
buffer_appendf(buf, sizeof(buf), &pos, ANSI_CLEAR ANSI_HOME);
|
||
for (int i = 0; i < top_pad; i++) {
|
||
buffer_appendf(buf, sizeof(buf), &pos, "\r\n");
|
||
}
|
||
|
||
/* Top border: ╭───…───╮ */
|
||
for (int i = 0; i < left_pad; i++) buffer_append_bytes(buf, sizeof(buf), &pos, " ", 1);
|
||
buffer_append_bytes(buf, sizeof(buf), &pos, "\033[36m", 5);
|
||
buffer_append_bytes(buf, sizeof(buf), &pos, "╭", strlen("╭"));
|
||
for (int i = 0; i < inner_w; i++) buffer_append_bytes(buf, sizeof(buf), &pos, "─", strlen("─"));
|
||
buffer_append_bytes(buf, sizeof(buf), &pos, "╮", strlen("╮"));
|
||
buffer_append_bytes(buf, sizeof(buf), &pos, "\033[0m", 4);
|
||
buffer_appendf(buf, sizeof(buf), &pos, "\r\n");
|
||
|
||
/* Three content lines with surrounding │ borders, centred inside the frame. */
|
||
const char *lines[3] = {line1, line2, line3};
|
||
int widths[3] = {utf8_string_width(line1), w2, w3};
|
||
const char *line_color[3] = {"\033[1;36m", "\033[0m", "\033[2;37m"};
|
||
for (int li = 0; li < 3; li++) {
|
||
for (int i = 0; i < left_pad; i++) buffer_append_bytes(buf, sizeof(buf), &pos, " ", 1);
|
||
buffer_append_bytes(buf, sizeof(buf), &pos, "\033[36m", 5);
|
||
buffer_append_bytes(buf, sizeof(buf), &pos, "│", strlen("│"));
|
||
buffer_append_bytes(buf, sizeof(buf), &pos, "\033[0m", 4);
|
||
|
||
int pad_total = inner_w - widths[li];
|
||
int pad_left = pad_total / 2;
|
||
int pad_right = pad_total - pad_left;
|
||
for (int i = 0; i < pad_left; i++) buffer_append_bytes(buf, sizeof(buf), &pos, " ", 1);
|
||
buffer_appendf(buf, sizeof(buf), &pos, "%s%s\033[0m", line_color[li], lines[li]);
|
||
for (int i = 0; i < pad_right; i++) buffer_append_bytes(buf, sizeof(buf), &pos, " ", 1);
|
||
|
||
buffer_append_bytes(buf, sizeof(buf), &pos, "\033[36m", 5);
|
||
buffer_append_bytes(buf, sizeof(buf), &pos, "│", strlen("│"));
|
||
buffer_append_bytes(buf, sizeof(buf), &pos, "\033[0m", 4);
|
||
buffer_appendf(buf, sizeof(buf), &pos, "\r\n");
|
||
}
|
||
|
||
/* Bottom border: ╰───…───╯ */
|
||
for (int i = 0; i < left_pad; i++) buffer_append_bytes(buf, sizeof(buf), &pos, " ", 1);
|
||
buffer_append_bytes(buf, sizeof(buf), &pos, "\033[36m", 5);
|
||
buffer_append_bytes(buf, sizeof(buf), &pos, "╰", strlen("╰"));
|
||
for (int i = 0; i < inner_w; i++) buffer_append_bytes(buf, sizeof(buf), &pos, "─", strlen("─"));
|
||
buffer_append_bytes(buf, sizeof(buf), &pos, "╯", strlen("╯"));
|
||
buffer_append_bytes(buf, sizeof(buf), &pos, "\033[0m", 4);
|
||
buffer_appendf(buf, sizeof(buf), &pos, "\r\n\r\n");
|
||
|
||
client_send(client, buf, pos);
|
||
}
|
||
|
||
/* Render the main screen */
|
||
void tui_render_screen(client_t *client) {
|
||
if (!client || !client->connected) return;
|
||
|
||
int render_width = client->width;
|
||
int render_height = client->height;
|
||
if (render_width < 10) render_width = 10;
|
||
if (render_height < 4) render_height = 4;
|
||
|
||
const size_t buf_size = (size_t)(render_height + 10) * (MAX_MESSAGE_LEN + 64) + 2048;
|
||
char *buffer = malloc(buf_size);
|
||
if (!buffer) return;
|
||
size_t pos = 0;
|
||
buffer[0] = '\0';
|
||
|
||
/* First pass under lock: compute indices and counts */
|
||
pthread_rwlock_rdlock(&g_room->lock);
|
||
int online = g_room->client_count;
|
||
int msg_count = g_room->message_count;
|
||
pthread_rwlock_unlock(&g_room->lock);
|
||
|
||
/* Calculate which messages to show */
|
||
int msg_height = render_height - 3;
|
||
if (msg_height < 1) msg_height = 1;
|
||
|
||
int start = 0;
|
||
if (client->mode == MODE_NORMAL) {
|
||
start = client->scroll_pos;
|
||
if (start > msg_count - msg_height) {
|
||
start = msg_count - msg_height;
|
||
}
|
||
if (start < 0) start = 0;
|
||
} else {
|
||
/* INSERT mode: show latest */
|
||
if (msg_count > msg_height) {
|
||
start = msg_count - msg_height;
|
||
}
|
||
}
|
||
|
||
int end = start + msg_height;
|
||
if (end > msg_count) end = msg_count;
|
||
|
||
/* Allocate snapshot outside the lock to avoid blocking writers */
|
||
message_t *msg_snapshot = NULL;
|
||
int snapshot_count = end - start;
|
||
|
||
if (snapshot_count > 0) {
|
||
msg_snapshot = calloc(snapshot_count, sizeof(message_t));
|
||
}
|
||
|
||
/* Second pass under lock: copy messages */
|
||
if (msg_snapshot) {
|
||
pthread_rwlock_rdlock(&g_room->lock);
|
||
/* Re-clamp in case msg_count changed */
|
||
int actual_count = g_room->message_count;
|
||
int actual_end = (end <= actual_count) ? end : actual_count;
|
||
int actual_start = (start < actual_end) ? start : actual_end;
|
||
int actual_snapshot = actual_end - actual_start;
|
||
if (actual_snapshot > 0 && actual_snapshot <= snapshot_count) {
|
||
memcpy(msg_snapshot, &g_room->messages[actual_start],
|
||
actual_snapshot * sizeof(message_t));
|
||
snapshot_count = actual_snapshot;
|
||
} else {
|
||
snapshot_count = 0;
|
||
}
|
||
pthread_rwlock_unlock(&g_room->lock);
|
||
}
|
||
|
||
/* Now render using snapshot (no lock held) */
|
||
|
||
/* If mute_joins is set, remove join/leave messages from snapshot in place */
|
||
if (client->mute_joins && msg_snapshot) {
|
||
int filtered = 0;
|
||
for (int i = 0; i < snapshot_count; i++) {
|
||
if (!is_join_leave_msg(&msg_snapshot[i])) {
|
||
msg_snapshot[filtered++] = msg_snapshot[i];
|
||
}
|
||
}
|
||
snapshot_count = filtered;
|
||
}
|
||
|
||
/* Move to top (Home) - Do NOT clear screen to prevent flicker */
|
||
buffer_appendf(buffer, buf_size, &pos, ANSI_HOME);
|
||
|
||
/* Title bar — segmented chips on a single line, no full-line reverse.
|
||
*
|
||
* Segments (left to right):
|
||
* • bold username
|
||
* • online count
|
||
* • mode name (colour matches the mode itself: cyan/yellow/magenta)
|
||
* • mute marker, only when active
|
||
* • right-aligned hint
|
||
*
|
||
* `· ` separators are dim grey so the eye groups segments without
|
||
* mistaking them for content. */
|
||
struct title_chip { const char *value; const char *value_color; };
|
||
struct title_chip chips[3];
|
||
int chip_count = 0;
|
||
|
||
chips[chip_count].value = client->username;
|
||
chips[chip_count].value_color = "\033[1;37m";
|
||
chip_count++;
|
||
|
||
char online_buf[32];
|
||
snprintf(online_buf, sizeof(online_buf), "在线 %d", online);
|
||
chips[chip_count].value = online_buf;
|
||
chips[chip_count].value_color = "\033[37m";
|
||
chip_count++;
|
||
|
||
const char *mode_str;
|
||
const char *mode_color;
|
||
switch (client->mode) {
|
||
case MODE_INSERT: mode_str = "INSERT"; mode_color = "\033[36m"; break;
|
||
case MODE_NORMAL: mode_str = "NORMAL"; mode_color = "\033[33m"; break;
|
||
case MODE_COMMAND: mode_str = "COMMAND"; mode_color = "\033[35m"; break;
|
||
default: mode_str = "HELP"; mode_color = "\033[34m"; break;
|
||
}
|
||
chips[chip_count].value = mode_str;
|
||
chips[chip_count].value_color = mode_color;
|
||
chip_count++;
|
||
|
||
/* Compose left half. */
|
||
char left[256];
|
||
size_t lpos = 0;
|
||
int left_width = 0;
|
||
for (int i = 0; i < chip_count; i++) {
|
||
if (i > 0) {
|
||
buffer_appendf(left, sizeof(left), &lpos, "\033[2;37m · \033[0m");
|
||
left_width += 3;
|
||
}
|
||
buffer_appendf(left, sizeof(left), &lpos, "%s%s\033[0m",
|
||
chips[i].value_color, chips[i].value);
|
||
left_width += utf8_string_width(chips[i].value);
|
||
}
|
||
if (client->mute_joins) {
|
||
buffer_appendf(left, sizeof(left), &lpos, " \033[2;37m静音\033[0m");
|
||
left_width += 4;
|
||
}
|
||
|
||
const char *hint = "? 帮助";
|
||
int hint_width = utf8_string_width(hint);
|
||
int gap = render_width - left_width - hint_width - 2;
|
||
if (gap < 1) gap = 1;
|
||
|
||
buffer_appendf(buffer, buf_size, &pos, " %s", left);
|
||
for (int i = 0; i < gap; i++) {
|
||
buffer_append_bytes(buffer, buf_size, &pos, " ", 1);
|
||
}
|
||
buffer_appendf(buffer, buf_size, &pos, "\033[2;37m%s\033[0m \033[K\r\n", hint);
|
||
|
||
/* Render messages from snapshot */
|
||
if (msg_snapshot) {
|
||
for (int i = 0; i < snapshot_count; i++) {
|
||
char msg_line[2048];
|
||
format_message_colored(&msg_snapshot[i], msg_line, sizeof(msg_line),
|
||
render_width, client->username);
|
||
buffer_appendf(buffer, buf_size, &pos, "%s\033[K\r\n", msg_line);
|
||
}
|
||
free(msg_snapshot);
|
||
}
|
||
|
||
/* Fill empty lines and clear them */
|
||
for (int i = snapshot_count; i < msg_height; i++) {
|
||
buffer_appendf(buffer, buf_size, &pos, "\033[K\r\n");
|
||
}
|
||
|
||
/* Separator - use box drawing character */
|
||
for (int i = 0; i < render_width; i++) {
|
||
buffer_append_bytes(buffer, buf_size, &pos, "─", strlen("─"));
|
||
}
|
||
buffer_appendf(buffer, buf_size, &pos, "\033[K\r\n");
|
||
|
||
/* Status/Input line */
|
||
if (client->mode == MODE_INSERT) {
|
||
buffer_appendf(buffer, buf_size, &pos, "\033[2;37m›\033[0m \033[K");
|
||
} else if (client->mode == MODE_NORMAL) {
|
||
int total = msg_count;
|
||
int scroll_pos = client->scroll_pos + 1;
|
||
if (total == 0) scroll_pos = 0;
|
||
int unseen = msg_count - end;
|
||
/* mode reverse-video chip + dim position + optional unseen marker */
|
||
if (unseen > 0) {
|
||
buffer_appendf(buffer, buf_size, &pos,
|
||
"\033[7;33m NORMAL \033[0m"
|
||
" \033[2;37m%d / %d\033[0m"
|
||
" \033[33m▼ %d new\033[0m\033[K",
|
||
scroll_pos, total, unseen);
|
||
} else {
|
||
buffer_appendf(buffer, buf_size, &pos,
|
||
"\033[7;33m NORMAL \033[0m"
|
||
" \033[2;37m%d / %d\033[0m\033[K",
|
||
scroll_pos, total);
|
||
}
|
||
} else if (client->mode == MODE_COMMAND) {
|
||
buffer_appendf(buffer, buf_size, &pos,
|
||
"\033[35m:\033[0m%s\033[K", client->command_input);
|
||
}
|
||
|
||
client_send(client, buffer, pos);
|
||
free(buffer);
|
||
}
|
||
|
||
/* Render the input line */
|
||
void tui_render_input(client_t *client, const char *input) {
|
||
if (!client || !client->connected) return;
|
||
|
||
int rw = client->width;
|
||
int rh = client->height;
|
||
if (rw < 10) rw = 10;
|
||
if (rh < 4) rh = 4;
|
||
|
||
char buffer[2048];
|
||
int input_width = utf8_string_width(input);
|
||
int avail = rw - 3;
|
||
if (avail < 1) avail = 1;
|
||
|
||
/* Truncate from start if too long */
|
||
char display[MAX_MESSAGE_LEN];
|
||
strncpy(display, input, sizeof(display) - 1);
|
||
display[sizeof(display) - 1] = '\0';
|
||
|
||
if (input_width > avail) {
|
||
int excess = input_width - avail;
|
||
int skip_width = 0;
|
||
const char *p = input;
|
||
int bytes_read;
|
||
|
||
while (*p && skip_width < excess) {
|
||
uint32_t cp = utf8_decode(p, &bytes_read);
|
||
skip_width += utf8_char_width(cp);
|
||
p += bytes_read;
|
||
}
|
||
|
||
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);
|
||
|
||
client_send(client, buffer, strlen(buffer));
|
||
}
|
||
|
||
/* Render the command output screen */
|
||
void tui_render_command_output(client_t *client) {
|
||
if (!client || !client->connected) return;
|
||
|
||
int rw = client->width;
|
||
int rh = client->height;
|
||
if (rw < 10) rw = 10;
|
||
if (rh < 4) rh = 4;
|
||
|
||
char buffer[4096];
|
||
size_t pos = 0;
|
||
buffer[0] = '\0';
|
||
|
||
/* Clear screen */
|
||
buffer_appendf(buffer, sizeof(buffer), &pos, ANSI_CLEAR ANSI_HOME);
|
||
|
||
/* Title */
|
||
const char *title = " COMMAND OUTPUT ";
|
||
int title_width = strlen(title);
|
||
int padding = rw - title_width;
|
||
if (padding < 0) padding = 0;
|
||
|
||
buffer_appendf(buffer, sizeof(buffer), &pos, ANSI_REVERSE "%s", title);
|
||
for (int i = 0; i < padding; i++) {
|
||
buffer_append_bytes(buffer, sizeof(buffer), &pos, " ", 1);
|
||
}
|
||
buffer_appendf(buffer, sizeof(buffer), &pos, ANSI_RESET "\r\n");
|
||
|
||
/* Command output - use a copy to avoid strtok corruption */
|
||
char output_copy[2048];
|
||
strncpy(output_copy, client->command_output, sizeof(output_copy) - 1);
|
||
output_copy[sizeof(output_copy) - 1] = '\0';
|
||
|
||
char *line = strtok(output_copy, "\n");
|
||
int line_count = 0;
|
||
int max_lines = rh - 2;
|
||
|
||
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);
|
||
}
|
||
|
||
buffer_appendf(buffer, sizeof(buffer), &pos, "%s\r\n", truncated);
|
||
line = strtok(NULL, "\n");
|
||
line_count++;
|
||
}
|
||
|
||
client_send(client, buffer, pos);
|
||
}
|
||
|
||
/* Get help text based on language */
|
||
const char* tui_get_help_text(help_lang_t lang) {
|
||
if (lang == LANG_EN) {
|
||
return "TERMINAL CHAT ROOM - HELP\n"
|
||
"\n"
|
||
"OPERATING MODES:\n"
|
||
" INSERT - Type and send messages (default)\n"
|
||
" NORMAL - Browse message history\n"
|
||
" COMMAND - Execute commands\n"
|
||
"\n"
|
||
"INSERT MODE KEYS:\n"
|
||
" ESC - Enter NORMAL mode\n"
|
||
" Enter - Send message\n"
|
||
" Backspace - Delete character\n"
|
||
" Ctrl+W - Delete last word\n"
|
||
" Ctrl+U - Delete line\n"
|
||
" Ctrl+C - Enter NORMAL mode\n"
|
||
"\n"
|
||
"NORMAL MODE KEYS:\n"
|
||
" i - Return to INSERT mode\n"
|
||
" : - Enter COMMAND mode\n"
|
||
" j/k - Scroll down/up one line\n"
|
||
" Ctrl+D/U - Scroll half page down/up\n"
|
||
" Ctrl+F/B - Scroll full page down/up\n"
|
||
" g/G - Jump to top/bottom\n"
|
||
" ? - Show this help\n"
|
||
" Ctrl+C - Exit chat\n"
|
||
"\n"
|
||
"AVAILABLE COMMANDS:\n"
|
||
" :list, :users - Show online users\n"
|
||
" :nick <name> - Change nickname\n"
|
||
" :msg <user> <text> - Whisper to user\n"
|
||
" :w <user> <text> - Short alias for :msg\n"
|
||
" :last [N] - Show last N messages (max 50)\n"
|
||
" :search <keyword> - Search message history\n"
|
||
" :mute-joins - Toggle join/leave notices\n"
|
||
" :help - Show available commands\n"
|
||
" :clear - Clear command output\n"
|
||
" :q, :quit, :exit - Disconnect\n"
|
||
"\n"
|
||
"SPECIAL MESSAGES:\n"
|
||
" /me <action> - Send action (e.g. /me waves)\n"
|
||
" @username - Mention user (bell + highlight)\n"
|
||
"\n"
|
||
"HELP SCREEN KEYS:\n"
|
||
" q, ESC - Close help\n"
|
||
" j/k - Scroll down/up\n"
|
||
" g/G - Jump to top/bottom\n"
|
||
" e/z - Switch English/Chinese\n";
|
||
} else {
|
||
return "终端聊天室 - 帮助\n"
|
||
"\n"
|
||
"操作模式:\n"
|
||
" INSERT - 输入和发送消息(默认)\n"
|
||
" NORMAL - 浏览消息历史\n"
|
||
" COMMAND - 执行命令\n"
|
||
"\n"
|
||
"INSERT 模式按键:\n"
|
||
" ESC - 进入 NORMAL 模式\n"
|
||
" Enter - 发送消息\n"
|
||
" Backspace - 删除字符\n"
|
||
" Ctrl+W - 删除上个单词\n"
|
||
" Ctrl+U - 删除整行\n"
|
||
" Ctrl+C - 进入 NORMAL 模式\n"
|
||
"\n"
|
||
"NORMAL 模式按键:\n"
|
||
" i - 返回 INSERT 模式\n"
|
||
" : - 进入 COMMAND 模式\n"
|
||
" j/k - 向下/上滚动一行\n"
|
||
" Ctrl+D/U - 向下/上滚动半页\n"
|
||
" Ctrl+F/B - 向下/上滚动整页\n"
|
||
" g/G - 跳到顶部/底部\n"
|
||
" ? - 显示此帮助\n"
|
||
" Ctrl+C - 退出聊天\n"
|
||
"\n"
|
||
"可用命令:\n"
|
||
" :list, :users - 显示在线用户\n"
|
||
" :nick <名字> - 更改昵称\n"
|
||
" :msg <用户> <文本> - 私聊\n"
|
||
" :w <用户> <文本> - :msg 的简写\n"
|
||
" :last [N] - 显示最后 N 条消息(最多50)\n"
|
||
" :search <关键词> - 搜索消息历史\n"
|
||
" :mute-joins - 切换加入/离开提示\n"
|
||
" :help - 显示可用命令\n"
|
||
" :clear - 清空命令输出\n"
|
||
" :q, :quit, :exit - 断开连接\n"
|
||
"\n"
|
||
"特殊消息:\n"
|
||
" /me <动作> - 发送动作 (如 /me 挥手)\n"
|
||
" @用户名 - 提及用户 (响铃+高亮)\n"
|
||
"\n"
|
||
"帮助界面按键:\n"
|
||
" q, ESC - 关闭帮助\n"
|
||
" j/k - 向下/上滚动\n"
|
||
" g/G - 跳到顶部/底部\n"
|
||
" e/z - 切换英文/中文\n";
|
||
}
|
||
}
|
||
|
||
/* Render the help screen */
|
||
void tui_render_help(client_t *client) {
|
||
if (!client || !client->connected) return;
|
||
|
||
int rw = client->width;
|
||
int rh = client->height;
|
||
if (rw < 10) rw = 10;
|
||
if (rh < 4) rh = 4;
|
||
|
||
char buffer[8192];
|
||
size_t pos = 0;
|
||
buffer[0] = '\0';
|
||
|
||
/* Clear screen */
|
||
buffer_appendf(buffer, sizeof(buffer), &pos, ANSI_CLEAR ANSI_HOME);
|
||
|
||
/* Title */
|
||
const char *title = " HELP ";
|
||
int title_width = strlen(title);
|
||
int padding = rw - title_width;
|
||
if (padding < 0) padding = 0;
|
||
|
||
buffer_appendf(buffer, sizeof(buffer), &pos, ANSI_REVERSE "%s", title);
|
||
for (int i = 0; i < padding; i++) {
|
||
buffer_append_bytes(buffer, sizeof(buffer), &pos, " ", 1);
|
||
}
|
||
buffer_appendf(buffer, sizeof(buffer), &pos, ANSI_RESET "\r\n");
|
||
|
||
/* Help content */
|
||
const char *help_text = tui_get_help_text(client->help_lang);
|
||
char help_copy[8192];
|
||
strncpy(help_copy, help_text, sizeof(help_copy) - 1);
|
||
help_copy[sizeof(help_copy) - 1] = '\0';
|
||
|
||
/* Split into lines and display with scrolling */
|
||
char *lines[100];
|
||
int line_count = 0;
|
||
char *line = strtok(help_copy, "\n");
|
||
while (line && line_count < 100) {
|
||
lines[line_count++] = line;
|
||
line = strtok(NULL, "\n");
|
||
}
|
||
|
||
int content_height = rh - 2;
|
||
if (content_height < 1) content_height = 1;
|
||
int max_scroll = line_count - content_height + 1;
|
||
if (max_scroll < 0) max_scroll = 0;
|
||
int start = client->help_scroll_pos;
|
||
if (start > max_scroll) start = max_scroll;
|
||
int end = start + content_height - 1;
|
||
if (end > line_count) end = line_count;
|
||
|
||
for (int i = start; i < end && (i - start) < content_height - 1; i++) {
|
||
buffer_appendf(buffer, sizeof(buffer), &pos, "%s\r\n", lines[i]);
|
||
}
|
||
|
||
/* Fill remaining lines */
|
||
for (int i = end - start; i < content_height - 1; i++) {
|
||
buffer_append_bytes(buffer, sizeof(buffer), &pos, "\r\n", 2);
|
||
}
|
||
|
||
/* Status line */
|
||
buffer_appendf(buffer, sizeof(buffer), &pos,
|
||
"-- HELP -- (%d/%d) j/k:scroll g/G:top/bottom e/z:lang q:close",
|
||
start + 1, max_scroll + 1);
|
||
|
||
client_send(client, buffer, pos);
|
||
}
|