From a832fe703f542e6112ad8265c387dbec957abb02 Mon Sep 17 00:00:00 2001 From: m1ngsama Date: Sun, 19 Apr 2026 17:22:23 +0800 Subject: [PATCH] feat: add whisper messaging and command history - Add :msg/:w command for private whisper messages between users - Add command history with UP/DOWN arrow navigation in command mode - Store up to 16 commands in ring buffer per client session - Update help text to document new features - Fix unit test Makefile to link common.c --- include/ssh_server.h | 3 ++ src/ssh_server.c | 93 +++++++++++++++++++++++++++++++++++++++++++- tests/unit/Makefile | 3 +- 3 files changed, 97 insertions(+), 2 deletions(-) diff --git a/include/ssh_server.h b/include/ssh_server.h index bf77b9e..11df154 100644 --- a/include/ssh_server.h +++ b/include/ssh_server.h @@ -21,6 +21,9 @@ typedef struct client { int help_scroll_pos; bool show_help; char command_input[256]; + char command_history[16][256]; + int command_history_count; + int command_history_pos; char command_output[2048]; char exec_command[MAX_EXEC_COMMAND_LEN]; char ssh_login[MAX_USERNAME_LEN]; diff --git a/src/ssh_server.c b/src/ssh_server.c index 9f8b3d2..5c6cd98 100644 --- a/src/ssh_server.c +++ b/src/ssh_server.c @@ -1084,6 +1084,21 @@ static void execute_command(client_t *client) { } } + /* Save to command history */ + if (cmd[0] != '\0') { + int max_hist = 16; + if (client->command_history_count >= max_hist) { + memmove(&client->command_history[0], &client->command_history[1], + (max_hist - 1) * sizeof(client->command_history[0])); + client->command_history_count = max_hist - 1; + } + strncpy(client->command_history[client->command_history_count], + cmd, sizeof(client->command_history[0]) - 1); + client->command_history[client->command_history_count][sizeof(client->command_history[0]) - 1] = '\0'; + client->command_history_count++; + client->command_history_pos = client->command_history_count; + } + if (strcmp(cmd, "list") == 0 || strcmp(cmd, "users") == 0 || strcmp(cmd, "who") == 0) { buffer_appendf(output, sizeof(output), &pos, @@ -1116,10 +1131,52 @@ static void execute_command(client_t *client) { " Available Commands\n" "========================================\n" "list, users, who - Show online users\n" + "msg/w - Whisper to user\n" "help, commands - Show this help\n" "clear, cls - Clear command output\n" + "Up/Down arrows - Command history\n" "========================================\n"); + } else if (strncmp(cmd, "msg ", 4) == 0 || strncmp(cmd, "w ", 2) == 0) { + char *rest = (cmd[0] == 'w') ? cmd + 2 : cmd + 4; + while (*rest == ' ') rest++; + char target_name[MAX_USERNAME_LEN] = {0}; + int ti = 0; + while (*rest && *rest != ' ' && ti < MAX_USERNAME_LEN - 1) { + target_name[ti++] = *rest++; + } + while (*rest == ' ') rest++; + + if (target_name[0] == '\0' || rest[0] == '\0') { + buffer_appendf(output, sizeof(output), &pos, + "Usage: msg \n" + " w \n"); + } else { + bool found = false; + pthread_rwlock_rdlock(&g_room->lock); + for (int i = 0; i < g_room->client_count; i++) { + if (strcmp(g_room->clients[i]->username, target_name) == 0) { + char whisper[MAX_MESSAGE_LEN]; + snprintf(whisper, sizeof(whisper), + "\r\n\033[35m[whisper from %s]: %s\033[0m\r\n", + client->username, rest); + client_send(g_room->clients[i], whisper, strlen(whisper)); + g_room->clients[i]->redraw_pending = true; + found = true; + break; + } + } + pthread_rwlock_unlock(&g_room->lock); + + if (found) { + buffer_appendf(output, sizeof(output), &pos, + "Whisper sent to %s\n", target_name); + } else { + buffer_appendf(output, sizeof(output), &pos, + "User '%s' not found\n", target_name); + } + } + } else if (strcmp(cmd, "clear") == 0 || strcmp(cmd, "cls") == 0) { buffer_appendf(output, sizeof(output), &pos, "Command output cleared\n"); @@ -1290,7 +1347,39 @@ static bool handle_key(client_t *client, unsigned char key, char *input) { break; case MODE_COMMAND: - if (key == 27) { /* ESC */ + if (key == 27) { /* ESC - check for arrow key sequences */ + char seq[2]; + int n = ssh_channel_read_timeout(client->channel, seq, 1, 0, 50); + if (n == 1 && seq[0] == '[') { + n = ssh_channel_read_timeout(client->channel, &seq[1], 1, 0, 50); + if (n == 1) { + if (seq[1] == 'A') { /* Up arrow */ + if (client->command_history_count > 0 && + client->command_history_pos > 0) { + client->command_history_pos--; + strncpy(client->command_input, + client->command_history[client->command_history_pos], + sizeof(client->command_input) - 1); + client->command_input[sizeof(client->command_input) - 1] = '\0'; + tui_render_screen(client); + } + return true; + } else if (seq[1] == 'B') { /* Down arrow */ + if (client->command_history_pos < client->command_history_count - 1) { + client->command_history_pos++; + strncpy(client->command_input, + client->command_history[client->command_history_pos], + sizeof(client->command_input) - 1); + client->command_input[sizeof(client->command_input) - 1] = '\0'; + } else { + client->command_history_pos = client->command_history_count; + client->command_input[0] = '\0'; + } + tui_render_screen(client); + return true; + } + } + } client->mode = MODE_NORMAL; client->command_input[0] = '\0'; tui_render_screen(client); @@ -1339,6 +1428,8 @@ void* client_handle_session(void *arg) { client->mode = MODE_INSERT; client->help_lang = LANG_ZH; client->connected = true; + client->command_history_count = 0; + client->command_history_pos = 0; /* Check for exec command */ if (client->exec_command[0] != '\0') { diff --git a/tests/unit/Makefile b/tests/unit/Makefile index 0fe36d8..bc1496c 100644 --- a/tests/unit/Makefile +++ b/tests/unit/Makefile @@ -6,6 +6,7 @@ LDFLAGS = -pthread # Source files UTF8_SRC = ../../src/utf8.c MESSAGE_SRC = ../../src/message.c +COMMON_SRC = ../../src/common.c TESTS = test_utf8 test_message @@ -16,7 +17,7 @@ all: $(TESTS) test_utf8: test_utf8.c $(UTF8_SRC) $(CC) $(CFLAGS) -o $@ $^ $(LDFLAGS) -test_message: test_message.c $(MESSAGE_SRC) $(UTF8_SRC) +test_message: test_message.c $(MESSAGE_SRC) $(UTF8_SRC) $(COMMON_SRC) $(CC) $(CFLAGS) -o $@ $^ $(LDFLAGS) run: all