From f3217de36b322012741e638bc3482ed0abfbe806 Mon Sep 17 00:00:00 2001 From: m1ngsama Date: Sun, 17 May 2026 13:43:15 +0800 Subject: [PATCH] input: Up/Down in INSERT mode walks sent-message history (UX-2) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ESC in INSERT mode used to switch straight to NORMAL. Now it follows the same arrow-key probe COMMAND mode already does: if the next two bytes are "[A" or "[B", treat as Up / Down and walk through the last 16 messages this client has sent. A plain ESC still falls through to NORMAL — the 50 ms probe timeout keeps that path responsive. Storage: new client_t fields char insert_history[16][MAX_MESSAGE_LEN]; int insert_history_count; int insert_history_pos; Recording: every Enter that broadcasts a message pushes the input buffer onto the ring (with FIFO eviction at 16) and resets pos. Down at the bottom of the ring returns to an empty input. This is the standard chat-client recall that vim users (and anyone who's ever used a shell) expect. --- include/ssh_server.h | 5 +++++ src/input.c | 53 ++++++++++++++++++++++++++++++++++++++++++-- 2 files changed, 56 insertions(+), 2 deletions(-) diff --git a/include/ssh_server.h b/include/ssh_server.h index f13d97e..10b24bc 100644 --- a/include/ssh_server.h +++ b/include/ssh_server.h @@ -24,6 +24,11 @@ typedef struct client { char command_history[16][256]; int command_history_count; int command_history_pos; + /* INSERT mode chat-message history. Last 16 messages this client + * sent, oldest first. Up/Down in INSERT mode walks through it. */ + char insert_history[16][MAX_MESSAGE_LEN]; + int insert_history_count; + int insert_history_pos; char command_output[2048]; bool show_motd; /* command_output holds MOTD text */ char exec_command[MAX_EXEC_COMMAND_LEN]; diff --git a/src/input.c b/src/input.c index fda2c98..9bc55f9 100644 --- a/src/input.c +++ b/src/input.c @@ -206,13 +206,62 @@ static bool handle_key(client_t *client, unsigned char key, char *input) { /* Mode-specific handling */ switch (client->mode) { case MODE_INSERT: - if (key == 27) { /* ESC */ + if (key == 27) { /* ESC — may also be the start of an arrow seq */ + 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 — walk back through sent history */ + if (client->insert_history_count > 0 && + client->insert_history_pos > 0) { + client->insert_history_pos--; + strncpy(input, + client->insert_history[client->insert_history_pos], + MAX_MESSAGE_LEN - 1); + input[MAX_MESSAGE_LEN - 1] = '\0'; + tui_render_input(client, input); + } + return true; + } else if (seq[1] == 'B') { /* Down — walk forward */ + if (client->insert_history_pos < + client->insert_history_count - 1) { + client->insert_history_pos++; + strncpy(input, + client->insert_history[client->insert_history_pos], + MAX_MESSAGE_LEN - 1); + input[MAX_MESSAGE_LEN - 1] = '\0'; + } else { + client->insert_history_pos = + client->insert_history_count; + input[0] = '\0'; + } + tui_render_input(client, input); + return true; + } + } + } + /* Plain ESC — fall through to NORMAL mode */ client->mode = MODE_NORMAL; client->scroll_pos = 0; tui_render_screen(client); - return true; /* Key consumed */ + return true; } else if (key == '\r' || key == '\n') { /* Enter */ if (input[0] != '\0') { + /* Record into the per-client INSERT history ring */ + int max_hist = (int)(sizeof(client->insert_history) / + sizeof(client->insert_history[0])); + if (client->insert_history_count >= max_hist) { + memmove(&client->insert_history[0], + &client->insert_history[1], + (max_hist - 1) * sizeof(client->insert_history[0])); + client->insert_history_count = max_hist - 1; + } + snprintf(client->insert_history[client->insert_history_count], + sizeof(client->insert_history[0]), "%s", input); + client->insert_history_count++; + client->insert_history_pos = client->insert_history_count; + message_t msg = { .timestamp = time(NULL), };