diff --git a/docs/CHANGELOG.md b/docs/CHANGELOG.md index b6627e3..b12dd75 100644 --- a/docs/CHANGELOG.md +++ b/docs/CHANGELOG.md @@ -26,6 +26,10 @@ counting escape sequences as visible width or cutting color codes. - Host-key generation now uses the non-deprecated libssh PKI API on libssh 0.12+ while keeping compatibility with older libssh releases. +- INSERT mode now shows a lightweight first-use hint for sending, browsing, + and `:support`. +- `:support` is now task-oriented around common user goals, and mistyped + commands suggest the nearest known command when possible. ## 2026-05-18 - Interactive input polish diff --git a/src/commands.c b/src/commands.c index a67725c..dffd56f 100644 --- a/src/commands.c +++ b/src/commands.c @@ -44,6 +44,64 @@ static void append_highlighted(char *output, size_t buf_size, size_t *pos, } } +static int min3(int a, int b, int c) { + int m = a < b ? a : b; + return m < c ? m : c; +} + +static int command_edit_distance(const char *a, const char *b) { + size_t la = strlen(a); + size_t lb = strlen(b); + int prev[32]; + int curr[32]; + + if (la >= 32 || lb >= 32) { + return 99; + } + + for (size_t j = 0; j <= lb; j++) { + prev[j] = (int)j; + } + + for (size_t i = 1; i <= la; i++) { + curr[0] = (int)i; + for (size_t j = 1; j <= lb; j++) { + int cost = a[i - 1] == b[j - 1] ? 0 : 1; + curr[j] = min3(prev[j] + 1, curr[j - 1] + 1, + prev[j - 1] + cost); + } + for (size_t j = 0; j <= lb; j++) { + prev[j] = curr[j]; + } + } + + return prev[lb]; +} + +static const char *suggest_command(const char *cmd) { + static const char *commands[] = { + "list", "users", "who", "nick", "name", "msg", "w", "inbox", + "last", "search", "mute-joins", "mute", "support", "guide", + "help", "commands", "clear", "cls", "q", "quit", "exit" + }; + const char *best = NULL; + int best_distance = 99; + + if (!cmd || !*cmd) { + return NULL; + } + + for (size_t i = 0; i < sizeof(commands) / sizeof(commands[0]); i++) { + int distance = command_edit_distance(cmd, commands[i]); + if (distance < best_distance) { + best_distance = distance; + best = commands[i]; + } + } + + return best_distance <= 2 ? best : NULL; +} + void commands_dispatch(client_t *client) { char cmd_buf[256]; strncpy(cmd_buf, client->command_input, sizeof(cmd_buf) - 1); @@ -368,9 +426,15 @@ void commands_dispatch(client_t *client) { return; } else { + const char *suggestion = suggest_command(cmd); buffer_appendf(output, sizeof(output), &pos, - "Unknown command: %s\n" - "Type 'help' for available commands\n", cmd); + "Unknown command: %s\n", cmd); + if (suggestion) { + buffer_appendf(output, sizeof(output), &pos, + "Did you mean :%s?\n", suggestion); + } + buffer_appendf(output, sizeof(output), &pos, + "Type :support for guidance or :help for commands\n"); } cmd_done: diff --git a/src/support.c b/src/support.c index 376eced..3bc55bb 100644 --- a/src/support.c +++ b/src/support.c @@ -7,23 +7,25 @@ void support_append_interactive_panel(char *buffer, size_t buf_size, buffer_appendf(buffer, buf_size, pos, "\033[1;36m支持 · support\033[0m\n" "\n" - "\033[1;37m快速开始\033[0m\n" + "\033[1;37m第一次进来\033[0m\n" " INSERT 输入消息,Enter 发送,ESC 进入 NORMAL\n" " NORMAL 浏览消息,G 回到最新,i 继续输入\n" " COMMAND 按 : 输入命令,q/ESC 关闭当前面板\n" "\n" - "\033[1;37m常用动作\033[0m\n" - " :users 查看在线用户\n" - " :last 20 查看最近 20 条历史\n" - " :search 搜索聊天记录\n" - " :msg 私聊\n" - " :inbox 查看私聊收件箱\n" - " :mute-joins 静音加入/离开提示\n" + "\033[1;37m我想...\033[0m\n" + " 看谁在线 :users\n" + " 看最近历史 :last 20\n" + " 搜索聊天记录 :search \n" + " 回到最新消息 G 或 End\n" + " 私聊某个人 :msg \n" + " 查看私聊收件箱 :inbox\n" + " 静音进出提示 :mute-joins\n" "\n" "\033[1;37m遇到问题\033[0m\n" " 看不到新消息: 在 NORMAL 按 G 或 End 回到最新\n" " 粘贴多行文本: 直接粘贴,TNT 会等 Enter 后一次发送\n" " 输入太长: 状态行接近限制时会提示,超出会响铃\n" + " 命令不记得: 输入 :help 看列表,输入 :support 回到这里\n" " 连接断开: 可能是空闲超时、连接数限制或网络重连\n" "\n" "\033[2;37m更多: ? 打开完整按键帮助,:help 查看命令列表\033[0m\n"); diff --git a/src/tui_status.c b/src/tui_status.c index 5d6f48b..24e6d8b 100644 --- a/src/tui_status.c +++ b/src/tui_status.c @@ -7,7 +7,18 @@ void tui_status_append(char *buffer, size_t buf_size, size_t *pos, if (!buffer || !pos || !client) return; if (client->mode == MODE_INSERT) { - buffer_appendf(buffer, buf_size, pos, "\033[2;37m›\033[0m \033[K"); + if (client->width >= 58) { + buffer_appendf(buffer, buf_size, pos, + "\033[2;37m›\033[0m " + "\033[2;37mEnter send · Esc browse · :support\033[0m" + "\033[K"); + } else if (client->width >= 36) { + buffer_appendf(buffer, buf_size, pos, + "\033[2;37m›\033[0m " + "\033[2;37mEnter · Esc · :support\033[0m\033[K"); + } else { + buffer_appendf(buffer, buf_size, pos, "\033[2;37m›\033[0m \033[K"); + } } else if (client->mode == MODE_NORMAL) { int total = msg_count; int range_start = total == 0 ? 0 : start + 1; diff --git a/tests/test_interactive_input.sh b/tests/test_interactive_input.sh index 0f5facd..bc5cc2a 100755 --- a/tests/test_interactive_input.sh +++ b/tests/test_interactive_input.sh @@ -64,7 +64,7 @@ set timeout 10 spawn ssh $SSH_OPTS anonymous@127.0.0.1 sleep 1 send -- "tester\r" -expect "›" +expect ":support" send -- "\033\[200~" send -- "line1\nline2\nline3" send -- "\033\[201~" @@ -139,7 +139,7 @@ set timeout 10 spawn ssh $SSH_OPTS anonymous@127.0.0.1 sleep 1 send -- "supporter\r" -expect "›" +expect ":support" send -- "\033" expect "NORMAL" send -- ":" @@ -165,6 +165,38 @@ else FAIL=$((FAIL + 1)) fi +UNKNOWN_SCRIPT="$STATE_DIR/unknown-command.expect" +cat >"$UNKNOWN_SCRIPT" <"$STATE_DIR/unknown-command.log" 2>&1; then + echo "✓ mistyped command suggests nearest command" + PASS=$((PASS + 1)) +else + echo "x mistyped command suggestion failed" + sed -n '1,160p' "$STATE_DIR/unknown-command.log" + sed -n '1,120p' "$STATE_DIR/server.log" + FAIL=$((FAIL + 1)) +fi + echo "" echo "PASSED: $PASS" echo "FAILED: $FAIL"