ux: collapse help surface around manual

This commit is contained in:
m1ngsama 2026-05-24 10:17:25 +08:00
parent 15aac7134f
commit a693d281f8
27 changed files with 236 additions and 368 deletions

View file

@ -82,7 +82,7 @@ Ctrl+F/B - Scroll full page down/up
PgDn/PgUp - Scroll full page down/up
End/Home - Jump to bottom/top
g/G - Jump to top/bottom
? - Show help
? - Show full key reference
Ctrl+C - Exit chat
```
@ -92,12 +92,12 @@ Ctrl+C - Exit chat
:nick <name> - Change nickname
:msg <user> <text> - Whisper to user
:w <user> <text> - Short alias for :msg
:inbox - Show whispers
:last [N] - Show last N messages from history (max 50, default 10)
:search <keyword> - Search full message history (case-insensitive)
:mute-joins - Toggle join/leave system notifications
:support - Show quick support guide
:lang <en|zh> - Switch UI language for this session
:help - Show available commands
:help - Show concise manual
:clear - Clear command output
:q, :quit, :exit - Disconnect
Up/Down - Browse command history
@ -176,7 +176,6 @@ TNT also exposes a small non-interactive SSH surface for scripts:
ssh -p 2222 chat.m1ng.space health
ssh -p 2222 chat.m1ng.space stats --json
ssh -p 2222 chat.m1ng.space users
ssh -p 2222 chat.m1ng.space support
ssh -p 2222 chat.m1ng.space "tail -n 20"
ssh -p 2222 operator@chat.m1ng.space post "service notice"
ssh -p 2222 chat.m1ng.space post "/me deploys v2.0"
@ -256,8 +255,9 @@ TNT/
│ ├── chat_room.c # chat room logic
│ ├── message.c # message persistence
│ ├── history_view.c # message viewport and scroll state
│ ├── help_text.c # full-screen and command help content
│ ├── support_text.c # quick support guide content
│ ├── help_text.c # full-screen key reference content
│ ├── manual.c # concise manual panel rendering
│ ├── manual_text.c # concise manual content
│ ├── i18n.c # language selection and shared UI text
│ ├── ratelimit.c # connection limits and rate limiting
│ ├── tui.c # terminal UI rendering

View file

@ -1,5 +1,16 @@
# Changelog
## Unreleased
### Changed
- Collapsed the interactive help surface around a concise Unix-style `:help`
manual and the `?` full key reference; `:support` is no longer a user-facing
command.
- First-use hints and unknown-command guidance now point users to `:help`
instead of the removed support entry.
- The concise manual module is now named `manual_text`, and the redundant
interactive `:commands` entrypoint was removed.
## 1.0.1 - 2026-05-24 - Release candidate hardening
### Added

View file

@ -58,7 +58,7 @@ Deployments are operator-driven:
3. Install the new binary.
4. Restart the service.
5. Run black-box checks (`health`, `stats --json`, `users --json`,
`support`, and a post/tail smoke test).
and a post/tail smoke test).
The installer can still be used manually on a server:
curl -sSL https://raw.githubusercontent.com/m1ngsama/TNT/main/install.sh | sh

View file

@ -97,7 +97,7 @@ Place a `motd.txt` file in the state directory. TNT displays it to each user on
# Systemd deployment (state dir is /var/lib/tnt)
sudo tee /var/lib/tnt/motd.txt <<'EOF'
Welcome! Be respectful. No spam.
Type :help for available commands.
Type :help for a concise manual, or ? for the full key reference.
EOF
sudo chown tnt:tnt /var/lib/tnt/motd.txt

View file

@ -79,8 +79,9 @@ src/
├── tui.c - Terminal UI rendering (ANSI escape codes)
├── tui_status.c - Mode/status/input-line rendering
├── i18n.c - Language selection and shared UI text
├── help_text.c - Full-screen and command help text
├── support_text.c - Quick support guide text
├── help_text.c - Full-screen key reference text
├── manual.c - Concise manual panel rendering
├── manual_text.c - Concise manual text
├── system_message.c - Localized join/leave/nick system messages
├── ratelimit.c - Per-IP and global connection limits
└── utf8.c - UTF-8 character handling
@ -98,8 +99,9 @@ include/
├── history_view.h - Scroll-state helpers
├── tui.h - TUI rendering functions
├── i18n.h - Language and shared text IDs
├── help_text.h - Help text interface
├── support_text.h - Support guide text interface
├── help_text.h - Key reference text interface
├── manual.h - Concise manual panel interface
├── manual_text.h - Concise manual text interface
├── ratelimit.h - Connection limit interface
└── utf8.h - UTF-8 utilities
```
@ -363,7 +365,7 @@ if (strcmp(cmd, "newcmd") == 0) {
3. **Move user-facing strings through `src/i18n.c` when they need localization or are reused.**
4. **Update help text in `src/help_text.c`:**
4. **Update user help text in `src/manual_text.c` and `src/help_text.c`:**
```c
"AVAILABLE COMMANDS:\n"
" newcmd - Description of new command\n"

View file

@ -27,11 +27,12 @@ COMMANDS (COMMAND mode, prefix with :)
nick <name> change nickname
msg <user> <text> whisper to user
w <user> <text> alias for msg
inbox show whispers
last [N] last N messages from log (default 10, max 50)
search <keyword> search full history (case-insensitive, 15 results)
mute-joins toggle join/leave notifications
support quick support guide
help show all commands
help concise manual
lang [en|zh] show or switch UI language
clear clear output
q / quit / exit disconnect
@ -52,8 +53,9 @@ STRUCTURE
src/exec.c SSH exec command dispatch
src/message.c persistence, search
src/history_view.c message viewport / scroll state
src/help_text.c full-screen and command help text
src/support_text.c quick support guide content
src/help_text.c full-screen key reference text
src/manual.c concise manual panel rendering
src/manual_text.c concise manual content
src/i18n.c language selection and shared text
src/ratelimit.c connection limits and rate limiting
src/tui.c rendering

View file

@ -4,7 +4,5 @@
#include "common.h"
const char *help_text_full(help_lang_t lang);
void help_text_append_commands(char *output, size_t buf_size, size_t *pos,
help_lang_t lang);
#endif /* HELP_TEXT_H */

9
include/manual.h Normal file
View file

@ -0,0 +1,9 @@
#ifndef MANUAL_H
#define MANUAL_H
#include "common.h"
void manual_append_interactive_panel(char *buffer, size_t buf_size,
size_t *pos, help_lang_t lang);
#endif /* MANUAL_H */

8
include/manual_text.h Normal file
View file

@ -0,0 +1,8 @@
#ifndef MANUAL_TEXT_H
#define MANUAL_TEXT_H
#include "common.h"
const char *manual_text_interactive(help_lang_t lang);
#endif /* MANUAL_TEXT_H */

View file

@ -1,11 +0,0 @@
#ifndef SUPPORT_H
#define SUPPORT_H
#include "common.h"
void support_append_interactive_panel(char *buffer, size_t buf_size,
size_t *pos, help_lang_t lang);
void support_append_exec_panel(char *buffer, size_t buf_size, size_t *pos,
help_lang_t lang);
#endif /* SUPPORT_H */

View file

@ -1,9 +0,0 @@
#ifndef SUPPORT_TEXT_H
#define SUPPORT_TEXT_H
#include "common.h"
const char *support_text_interactive(help_lang_t lang);
const char *support_text_exec(help_lang_t lang);
#endif /* SUPPORT_TEXT_H */

View file

@ -8,10 +8,9 @@
#include "chat_room.h"
#include "client.h"
#include "common.h"
#include "help_text.h"
#include "i18n.h"
#include "manual.h"
#include "message.h"
#include "support.h"
#include "system_message.h"
#include "tui.h"
#include "utf8.h"
@ -84,8 +83,8 @@ static int command_edit_distance(const char *a, const char *b) {
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",
"lang", "language", "help", "commands", "clear", "cls",
"last", "search", "mute-joins", "mute", "lang", "language",
"help", "clear", "cls",
"q", "quit", "exit"
};
const char *best = NULL;
@ -168,13 +167,9 @@ void commands_dispatch(client_t *client) {
}
pthread_rwlock_unlock(&g_room->lock);
} else if (strcmp(cmd, "help") == 0 || strcmp(cmd, "commands") == 0) {
help_text_append_commands(output, sizeof(output), &pos,
client->help_lang);
} else if (strcmp(cmd, "support") == 0 || strcmp(cmd, "guide") == 0) {
support_append_interactive_panel(output, sizeof(output), &pos,
client->help_lang);
} else if (strcmp(cmd, "help") == 0) {
manual_append_interactive_panel(output, sizeof(output), &pos,
client->help_lang);
} else if (strcmp(cmd, "lang") == 0 || strcmp(cmd, "language") == 0 ||
strncmp(cmd, "lang ", 5) == 0 ||

View file

@ -6,7 +6,6 @@
#include "input.h"
#include "message.h"
#include "ratelimit.h"
#include "support.h"
#include "utf8.h"
#include <ctype.h>
#include <stdio.h>
@ -122,14 +121,6 @@ static int exec_command_help(client_t *client) {
return client_send(client, help_text, strlen(help_text)) == 0 ? 0 : 1;
}
static int exec_command_support(client_t *client) {
char output[2048] = {0};
size_t pos = 0;
support_append_exec_panel(output, sizeof(output), &pos, client->help_lang);
return client_send(client, output, pos) == 0 ? 0 : 1;
}
static int exec_command_health(client_t *client) {
static const char ok[] = "ok\n";
return client_send(client, ok, sizeof(ok) - 1) == 0 ? 0 : 1;
@ -429,9 +420,6 @@ int exec_dispatch(client_t *client) {
if (strcmp(cmd, "help") == 0 || strcmp(cmd, "--help") == 0) {
return exec_command_help(client);
}
if (strcmp(cmd, "support") == 0 || strcmp(cmd, "guide") == 0) {
return exec_command_support(client);
}
if (strcmp(cmd, "health") == 0) {
return exec_command_health(client);
}

View file

@ -1,60 +1,8 @@
#include "help_text.h"
void help_text_append_commands(char *output, size_t buf_size, size_t *pos,
help_lang_t lang) {
if (lang == LANG_ZH) {
buffer_appendf(output, buf_size, pos,
"========================================\n"
" 可用命令\n"
"========================================\n"
"list, users, who - 显示在线用户\n"
"nick/name <name> - 修改昵称\n"
"msg/w <user> <text> - 私聊用户\n"
"inbox - 查看私聊历史\n"
"last [N] - 查看最近 N 条消息\n"
"search <keyword> - 搜索消息历史\n"
"mute-joins - 切换加入/离开提示\n"
"support - 显示快速支持指南\n"
"lang [en|zh] - 查看或切换界面语言\n"
"help, commands - 显示此帮助\n"
"clear, cls - 清空命令输出\n"
"q, quit, exit - 断开连接\n"
"上/下方向键 - 命令历史\n"
"========================================\n"
"INSERT 模式:\n"
" /me <action> - 发送动作消息\n"
" @username - 提及用户并响铃提示\n"
"========================================\n");
return;
}
buffer_appendf(output, buf_size, pos,
"========================================\n"
" Available Commands\n"
"========================================\n"
"list, users, who - Show online users\n"
"nick/name <name> - Change nickname\n"
"msg/w <user> <text> - Whisper to user (private)\n"
"inbox - Show whisper history\n"
"last [N] - Show last N messages\n"
"search <keyword> - Search message history\n"
"mute-joins - Toggle join/leave notices\n"
"support - Show quick support guide\n"
"lang [en|zh] - Show or switch UI language\n"
"help, commands - Show this help\n"
"clear, cls - Clear command output\n"
"q, quit, exit - Disconnect\n"
"Up/Down arrows - Command history\n"
"========================================\n"
"In INSERT mode:\n"
" /me <action> - Send action message\n"
" @username - Mention (bell notify)\n"
"========================================\n");
}
const char *help_text_full(help_lang_t lang) {
if (lang == LANG_EN) {
return "TERMINAL CHAT ROOM - HELP\n"
return "TNT KEY REFERENCE\n"
"\n"
"OPERATING MODES:\n"
" INSERT - Type and send messages (default)\n"
@ -80,7 +28,7 @@ const char *help_text_full(help_lang_t lang) {
" PgDn/PgUp - Scroll full page down/up\n"
" End/Home - Jump to bottom/top\n"
" g/G - Jump to top/bottom\n"
" ? - Show this help\n"
" ? - Show full key reference\n"
" Ctrl+C - Exit chat\n"
"\n"
"AVAILABLE COMMANDS:\n"
@ -88,12 +36,12 @@ const char *help_text_full(help_lang_t lang) {
" :nick <name> - Change nickname\n"
" :msg <user> <text> - Whisper to user\n"
" :w <user> <text> - Short alias for :msg\n"
" :inbox - Show whispers\n"
" :last [N] - Show last N messages (max 50)\n"
" :search <keyword> - Search message history\n"
" :mute-joins - Toggle join/leave notices\n"
" :support - Show quick support guide\n"
" :help - Show concise manual\n"
" :lang <en|zh> - Switch UI language\n"
" :help - Show available commands\n"
" :clear - Clear command output\n"
" :q, :quit, :exit - Disconnect\n"
"\n"
@ -110,7 +58,7 @@ const char *help_text_full(help_lang_t lang) {
" e/z - Switch English/Chinese\n";
}
return "终端聊天室 - 帮助\n"
return "TNT 按键参考\n"
"\n"
"操作模式:\n"
" INSERT - 输入和发送消息(默认)\n"
@ -136,7 +84,7 @@ const char *help_text_full(help_lang_t lang) {
" PgDn/PgUp - 向下/上滚动整页\n"
" End/Home - 跳到底部/顶部\n"
" g/G - 跳到顶部/底部\n"
" ? - 显示此帮助\n"
" ? - 显示完整按键参考\n"
" Ctrl+C - 退出聊天\n"
"\n"
"可用命令:\n"
@ -144,12 +92,12 @@ const char *help_text_full(help_lang_t lang) {
" :nick <名字> - 更改昵称\n"
" :msg <用户> <文本> - 私聊\n"
" :w <用户> <文本> - :msg 的简写\n"
" :inbox - 查看私聊\n"
" :last [N] - 显示最后 N 条消息(最多50)\n"
" :search <关键词> - 搜索消息历史\n"
" :mute-joins - 切换加入/离开提示\n"
" :support - 显示快速支持指南\n"
" :help - 显示简明手册\n"
" :lang <en|zh> - 切换界面语言\n"
" :help - 显示可用命令\n"
" :clear - 清空命令输出\n"
" :q, :quit, :exit - 断开连接\n"
"\n"

View file

@ -104,17 +104,17 @@ const char *i18n_text(help_lang_t lang, i18n_text_id_t id) {
case I18N_WELCOME_FALLBACK_FORMAT:
return "TNT %s - SSH 匿名聊天室\r\n\r\n";
case I18N_INSERT_HINT_WIDE:
return "Enter 发送 · Esc 浏览 · :support";
return "Enter 发送 · Esc 浏览 · :help";
case I18N_INSERT_HINT_NARROW:
return "Enter · Esc · :support";
return "Enter · Esc · :help";
case I18N_NORMAL_LATEST:
return "G 最新";
case I18N_NORMAL_NEW_MESSAGES:
return "新消息";
case I18N_HELP_TITLE:
return " 帮助 ";
return " 按键 ";
case I18N_HELP_STATUS_FORMAT:
return "-- 帮助 -- (%d/%d) j/k:滚动 g/G:首尾 e/z:语言 q:关闭";
return "-- 按键参考 -- (%d/%d) j/k:滚动 g/G:首尾 e/z:语言 q:关闭";
case I18N_COMMAND_OUTPUT_TITLE:
return " 命令输出 ";
case I18N_MOTD_TITLE:
@ -126,7 +126,7 @@ const char *i18n_text(help_lang_t lang, i18n_text_id_t id) {
case I18N_TITLE_MUTED:
return "静音";
case I18N_TITLE_HELP_HINT:
return "? 帮助";
return "? 按键";
case I18N_IDLE_TIMEOUT_FORMAT:
return "\r\n\033[33m已断开: 空闲超时 (%d 分钟)\033[0m\r\n";
case I18N_SYSTEM_USERNAME:
@ -189,7 +189,7 @@ const char *i18n_text(help_lang_t lang, i18n_text_id_t id) {
case I18N_DID_YOU_MEAN_FORMAT:
return "你是想输入 :%s 吗?\n";
case I18N_UNKNOWN_GUIDANCE:
return "输入 :support 查看引导,或 :help 查看命令\n";
return "输入 :help 查看帮助\n";
case I18N_EXEC_HELP:
return "TNT exec 接口\n"
"命令:\n"
@ -201,7 +201,6 @@ const char *i18n_text(help_lang_t lang, i18n_text_id_t id) {
" tail -n N 输出最近消息\n"
" post MESSAGE 非交互发送消息\n"
" post \"/me act\" 发送动作消息\n"
" support 显示快速支持指南\n"
" exit 成功退出\n";
case I18N_EXEC_USERS_USAGE:
return "users: 用法: users [--json]\n";
@ -236,17 +235,17 @@ const char *i18n_text(help_lang_t lang, i18n_text_id_t id) {
case I18N_WELCOME_FALLBACK_FORMAT:
return "TNT %s - anonymous chat over SSH\r\n\r\n";
case I18N_INSERT_HINT_WIDE:
return "Enter send · Esc browse · :support";
return "Enter send · Esc browse · :help";
case I18N_INSERT_HINT_NARROW:
return "Enter · Esc · :support";
return "Enter · Esc · :help";
case I18N_NORMAL_LATEST:
return "G latest";
case I18N_NORMAL_NEW_MESSAGES:
return "new";
case I18N_HELP_TITLE:
return " HELP ";
return " KEYS ";
case I18N_HELP_STATUS_FORMAT:
return "-- HELP -- (%d/%d) j/k:scroll g/G:top/bottom e/z:lang q:close";
return "-- KEY REFERENCE -- (%d/%d) j/k:scroll g/G:top/bottom e/z:lang q:close";
case I18N_COMMAND_OUTPUT_TITLE:
return " COMMAND OUTPUT ";
case I18N_MOTD_TITLE:
@ -258,7 +257,7 @@ const char *i18n_text(help_lang_t lang, i18n_text_id_t id) {
case I18N_TITLE_MUTED:
return "muted";
case I18N_TITLE_HELP_HINT:
return "? help";
return "? keys";
case I18N_IDLE_TIMEOUT_FORMAT:
return "\r\n\033[33mDisconnected: idle timeout (%d min)\033[0m\r\n";
case I18N_SYSTEM_USERNAME:
@ -321,7 +320,7 @@ const char *i18n_text(help_lang_t lang, i18n_text_id_t id) {
case I18N_DID_YOU_MEAN_FORMAT:
return "Did you mean :%s?\n";
case I18N_UNKNOWN_GUIDANCE:
return "Type :support for guidance or :help for commands\n";
return "Type :help for help\n";
case I18N_EXEC_HELP:
return "TNT exec interface\n"
"Commands:\n"
@ -333,7 +332,6 @@ const char *i18n_text(help_lang_t lang, i18n_text_id_t id) {
" tail -n N Print recent messages\n"
" post MESSAGE Post a message non-interactively\n"
" post \"/me act\" Post an action message\n"
" support Show quick support guide\n"
" exit Exit successfully\n";
case I18N_EXEC_USERS_USAGE:
return "users: usage: users [--json]\n";

10
src/manual.c Normal file
View file

@ -0,0 +1,10 @@
#include "manual.h"
#include "manual_text.h"
void manual_append_interactive_panel(char *buffer, size_t buf_size,
size_t *pos, help_lang_t lang) {
if (!buffer || !pos) return;
buffer_appendf(buffer, buf_size, pos, "%s",
manual_text_interactive(lang));
}

63
src/manual_text.c Normal file
View file

@ -0,0 +1,63 @@
#include "manual_text.h"
const char *manual_text_interactive(help_lang_t lang) {
if (lang == LANG_ZH) {
return "\033[1;36mTNT(1) 帮助\033[0m\n"
"\n"
"\033[1;37m名称\033[0m\n"
" TNT - SSH 终端聊天室\n"
"\n"
"\033[1;37m快速开始\033[0m\n"
" 直接输入消息Enter 发送\n"
" Esc 进入 NORMAL 浏览历史G 回到最新i 继续输入\n"
" : 输入命令q 或 Esc 关闭输出面板\n"
"\n"
"\033[1;37m命令\033[0m\n"
" :users 在线用户\n"
" :last [N] 最近消息,默认 10最多 50\n"
" :search <关键词> 搜索历史\n"
" :msg <用户> <文本> 私聊\n"
" :inbox 私聊收件箱\n"
" :nick <名字> 修改昵称\n"
" :mute-joins 静音/开启进出提示\n"
" :clear 清空命令输出\n"
" :q 断开连接\n"
"\n"
"\033[1;37m语言\033[0m\n"
" :lang 显示当前语言\n"
" :lang zh 切换中文\n"
" :lang en 切换英文\n"
"\n"
"\033[1;37m参见\033[0m\n"
" ? 完整按键参考\n";
}
return "\033[1;36mTNT(1) help\033[0m\n"
"\n"
"\033[1;37mName\033[0m\n"
" TNT - SSH terminal chat room\n"
"\n"
"\033[1;37mQuick start\033[0m\n"
" Type a message and press Enter to send\n"
" Esc enters NORMAL history browsing; G jumps latest; i types again\n"
" : enters COMMAND mode; q or Esc closes output panels\n"
"\n"
"\033[1;37mCommands\033[0m\n"
" :users show online users\n"
" :last [N] show recent messages, default 10, max 50\n"
" :search <keyword> search history\n"
" :msg <user> <text> whisper privately\n"
" :inbox show whispers\n"
" :nick <name> change nickname\n"
" :mute-joins toggle join/leave notices\n"
" :clear clear command output\n"
" :q disconnect\n"
"\n"
"\033[1;37mLanguage\033[0m\n"
" :lang show current language\n"
" :lang en switch to English\n"
" :lang zh switch to Chinese\n"
"\n"
"\033[1;37mSee also\033[0m\n"
" ? full key reference\n";
}

View file

@ -1,17 +0,0 @@
#include "support.h"
#include "support_text.h"
void support_append_interactive_panel(char *buffer, size_t buf_size,
size_t *pos, help_lang_t lang) {
if (!buffer || !pos) return;
buffer_appendf(buffer, buf_size, pos, "%s",
support_text_interactive(lang));
}
void support_append_exec_panel(char *buffer, size_t buf_size, size_t *pos,
help_lang_t lang) {
if (!buffer || !pos) return;
buffer_appendf(buffer, buf_size, pos, "%s", support_text_exec(lang));
}

View file

@ -1,97 +0,0 @@
#include "support_text.h"
const char *support_text_interactive(help_lang_t lang) {
if (lang == LANG_ZH) {
return "\033[1;36m支持 · support\033[0m\n"
"\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\n"
" 搜索聊天记录 :search <keyword>\n"
" 回到最新消息 G 或 End\n"
" 私聊某个人 :msg <user> <text>\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";
}
return "\033[1;36mSupport\033[0m\n"
"\n"
"\033[1;37mFirst minute\033[0m\n"
" INSERT Type messages, Enter sends, ESC enters NORMAL\n"
" NORMAL Browse history, G jumps latest, i continues typing\n"
" COMMAND Press : for commands, q/ESC closes this panel\n"
"\n"
"\033[1;37mI want to...\033[0m\n"
" See who is online :users\n"
" See recent history :last 20\n"
" Search history :search <keyword>\n"
" Return to latest G or End\n"
" Whisper someone :msg <user> <text>\n"
" Read whispers :inbox\n"
" Mute join notices :mute-joins\n"
"\n"
"\033[1;37mTroubleshooting\033[0m\n"
" Missing new messages: press G or End in NORMAL\n"
" Pasting many lines: paste normally, then Enter sends once\n"
" Message too long: the status line warns near the limit\n"
" Forgot a command: type :help or return here with :support\n"
" Disconnected: check idle timeout, limits, or reconnect\n"
"\n"
"\033[2;37mMore: ? opens full key help, :help lists commands\033[0m\n";
}
const char *support_text_exec(help_lang_t lang) {
if (lang == LANG_ZH) {
return "TNT 支持\n"
"\n"
"交互使用:\n"
" ssh -p 2222 HOST\n"
" INSERT: 输入消息并按 Enter 发送\n"
" NORMAL: G 回到最新k/PageUp 查看更早消息\n"
" COMMAND: 按 : 后可运行 users, last, search, msg, inbox\n"
"\n"
"非交互检查:\n"
" ssh -p 2222 HOST health\n"
" ssh -p 2222 HOST stats --json\n"
" ssh -p 2222 HOST users --json\n"
" ssh -p 2222 HOST 'tail -n 20'\n"
" ssh -p 2222 USER@HOST post 'message'\n"
"\n"
"排查:\n"
" 连接过早关闭: 检查限流、空闲超时、连接容量、\n"
" 单 IP 限制和防火墙规则。\n";
}
return "TNT support\n"
"\n"
"Interactive use:\n"
" ssh -p 2222 HOST\n"
" INSERT: type and press Enter to send\n"
" NORMAL: press G for latest, k/PageUp for older messages\n"
" COMMAND: press : then run users, last, search, msg, inbox\n"
"\n"
"Non-interactive checks:\n"
" ssh -p 2222 HOST health\n"
" ssh -p 2222 HOST stats --json\n"
" ssh -p 2222 HOST users --json\n"
" ssh -p 2222 HOST 'tail -n 20'\n"
" ssh -p 2222 USER@HOST post 'message'\n"
"\n"
"Troubleshooting:\n"
" Connection closes early: check rate limits, idle timeout,\n"
" global connection capacity, per-IP limits, and firewall rules.\n";
}

View file

@ -75,18 +75,6 @@ else
FAIL=$((FAIL + 1))
fi
SUPPORT_OUTPUT=$(ssh $SSH_OPTS localhost support 2>/dev/null || true)
printf '%s\n' "$SUPPORT_OUTPUT" | grep -Eq '^TNT (support|支持)$' &&
printf '%s\n' "$SUPPORT_OUTPUT" | grep -Eq '^(Troubleshooting|排查):'
if [ $? -eq 0 ]; then
echo "✓ support returns quick guide"
PASS=$((PASS + 1))
else
echo "✗ support output unexpected"
printf '%s\n' "$SUPPORT_OUTPUT"
FAIL=$((FAIL + 1))
fi
HELP_OUTPUT=$(ssh $SSH_OPTS localhost help 2>/dev/null || true)
printf '%s\n' "$HELP_OUTPUT" | grep -q '^TNT exec 接口$' &&
printf '%s\n' "$HELP_OUTPUT" | grep -q '^命令:$'

View file

@ -64,7 +64,7 @@ set timeout 10
spawn ssh $SSH_OPTS anonymous@127.0.0.1
sleep 1
send -- "tester\r"
expect ":support"
expect ":help"
send -- "\033\[200~"
send -- "line1\nline2\nline3"
send -- "\033\[201~"
@ -133,19 +133,19 @@ else
FAIL=$((FAIL + 1))
fi
SUPPORT_SCRIPT="$STATE_DIR/support.expect"
cat >"$SUPPORT_SCRIPT" <<EOF
HELP_SCRIPT="$STATE_DIR/help.expect"
cat >"$HELP_SCRIPT" <<EOF
set timeout 10
spawn ssh $SSH_OPTS anonymous@127.0.0.1
sleep 1
send -- "supporter\r"
expect ":support"
send -- "helper\r"
expect ":help"
send -- "\033"
expect "NORMAL"
send -- ":"
expect ":"
send -- "support\r"
expect "支持"
send -- "help\r"
expect "TNT\\(1\\) 帮助"
expect "按任意键"
send -- "q"
expect "NORMAL"
@ -162,12 +162,12 @@ send -- "\003"
expect eof
EOF
if expect "$SUPPORT_SCRIPT" >"$STATE_DIR/support.log" 2>&1; then
echo "✓ :support renders quick guide"
if expect "$HELP_SCRIPT" >"$STATE_DIR/help.log" 2>&1; then
echo "✓ :help renders concise manual"
PASS=$((PASS + 1))
else
echo "x :support command failed"
sed -n '1,160p' "$STATE_DIR/support.log"
echo "x :help command failed"
sed -n '1,160p' "$STATE_DIR/help.log"
sed -n '1,120p' "$STATE_DIR/server.log"
FAIL=$((FAIL + 1))
fi
@ -178,13 +178,13 @@ set timeout 10
spawn ssh $SSH_OPTS anonymous@127.0.0.1
sleep 1
send -- "mistype\r"
expect ":support"
expect ":help"
send -- "\033"
expect "NORMAL"
send -- ":"
expect ":"
send -- "suport\r"
expect "你是想输入 :support 吗?"
send -- "hlep\r"
expect "你是想输入 :help 吗?"
expect "按任意键"
send -- "q"
sleep 0.2
@ -210,7 +210,7 @@ set timeout 10
spawn ssh $SSH_OPTS anonymous@127.0.0.1
sleep 1
send -- "localized\r"
expect ":support"
expect ":help"
send -- "\033"
expect "NORMAL"
send -- ":"
@ -260,7 +260,7 @@ set timeout 10
spawn ssh $SSH_OPTS anonymous@127.0.0.1
sleep 1
send -- "usageuser\r"
expect ":support"
expect ":help"
send -- "\033"
expect "NORMAL"
send -- ":"
@ -328,7 +328,7 @@ set timeout 10
spawn ssh $SSH_OPTS anonymous@127.0.0.1
sleep 1
send -- "systemuser\r"
expect ":support"
expect ":help"
send -- "\033"
expect "NORMAL"
send -- ":"

View file

@ -19,10 +19,10 @@ HISTORY_VIEW_SRC = ../../src/history_view.c
I18N_SRC = ../../src/i18n.c
SYSTEM_MESSAGE_SRC = ../../src/system_message.c
HELP_TEXT_SRC = ../../src/help_text.c
SUPPORT_TEXT_SRC = ../../src/support_text.c
MANUAL_TEXT_SRC = ../../src/manual_text.c
RATELIMIT_SRC = ../../src/ratelimit.c
TESTS = test_utf8 test_message test_chat_room test_history_view test_i18n test_system_message test_help_text test_support_text test_cli_text test_ratelimit
TESTS = test_utf8 test_message test_chat_room test_history_view test_i18n test_system_message test_help_text test_manual_text test_cli_text test_ratelimit
.PHONY: all clean run
@ -49,7 +49,7 @@ test_system_message: test_system_message.c $(SYSTEM_MESSAGE_SRC) $(I18N_SRC)
test_help_text: test_help_text.c $(HELP_TEXT_SRC) $(COMMON_SRC)
$(CC) $(CFLAGS) -o $@ $^ $(LDFLAGS)
test_support_text: test_support_text.c $(SUPPORT_TEXT_SRC)
test_manual_text: test_manual_text.c $(MANUAL_TEXT_SRC)
$(CC) $(CFLAGS) -o $@ $^ $(LDFLAGS)
test_cli_text: test_cli_text.c $(CLI_TEXT_SRC) $(COMMON_SRC)
@ -80,8 +80,8 @@ run: all
@echo "=== Running Help Text Tests ==="
./test_help_text
@echo ""
@echo "=== Running Support Text Tests ==="
./test_support_text
@echo "=== Running Manual Text Tests ==="
./test_manual_text
@echo ""
@echo "=== Running CLI Text Tests ==="
./test_cli_text

View file

@ -19,39 +19,25 @@ TEST(full_help_matches_language) {
const char *en = help_text_full(LANG_EN);
const char *zh = help_text_full(LANG_ZH);
assert(strstr(en, "TERMINAL CHAT ROOM - HELP") != NULL);
assert(strstr(en, "TNT KEY REFERENCE") != NULL);
assert(strstr(en, "AVAILABLE COMMANDS") != NULL);
assert(strstr(en, ":inbox") != NULL);
assert(strstr(en, ":support") == NULL);
assert(strstr(en, ":commands") == NULL);
assert(strstr(en, "Switch English/Chinese") != NULL);
assert(strstr(zh, "终端聊天室 - 帮助") != NULL);
assert(strstr(zh, "TNT 按键参考") != NULL);
assert(strstr(zh, "可用命令") != NULL);
assert(strstr(zh, ":inbox") != NULL);
assert(strstr(zh, ":support") == NULL);
assert(strstr(zh, ":commands") == NULL);
assert(strstr(zh, "切换英文/中文") != NULL);
}
TEST(command_help_matches_language) {
char out[2048];
size_t pos;
out[0] = '\0';
pos = 0;
help_text_append_commands(out, sizeof(out), &pos, LANG_EN);
assert(strstr(out, "Available Commands") != NULL);
assert(strstr(out, "Show online users") != NULL);
assert(pos == strlen(out));
out[0] = '\0';
pos = 0;
help_text_append_commands(out, sizeof(out), &pos, LANG_ZH);
assert(strstr(out, "可用命令") != NULL);
assert(strstr(out, "显示在线用户") != NULL);
assert(pos == strlen(out));
}
int main(void) {
printf("Running help text unit tests...\n\n");
RUN_TEST(full_help_matches_language);
RUN_TEST(command_help_matches_language);
printf("\n✓ All %d tests passed!\n", tests_passed);
return 0;

View file

@ -81,9 +81,9 @@ TEST(text_lookup_matches_language) {
assert(strstr(i18n_text(LANG_ZH, I18N_WELCOME_SUBTITLE),
"匿名聊天室") != NULL);
assert(strstr(i18n_text(LANG_EN, I18N_HELP_STATUS_FORMAT),
"HELP") != NULL);
"KEY REFERENCE") != NULL);
assert(strstr(i18n_text(LANG_ZH, I18N_HELP_STATUS_FORMAT),
"帮助") != NULL);
"按键参考") != NULL);
assert(strstr(i18n_text(LANG_EN, I18N_COMMAND_OUTPUT_TITLE),
"COMMAND") != NULL);
assert(strstr(i18n_text(LANG_ZH, I18N_COMMAND_OUTPUT_TITLE),
@ -118,8 +118,12 @@ TEST(text_lookup_matches_language) {
"未知命令") != NULL);
assert(strstr(i18n_text(LANG_EN, I18N_EXEC_HELP),
"TNT exec interface") != NULL);
assert(strstr(i18n_text(LANG_EN, I18N_EXEC_HELP),
"support") == NULL);
assert(strstr(i18n_text(LANG_ZH, I18N_EXEC_HELP),
"TNT exec 接口") != NULL);
assert(strstr(i18n_text(LANG_ZH, I18N_EXEC_HELP),
"support") == NULL);
assert(strstr(i18n_text(LANG_EN, I18N_EXEC_POST_EMPTY),
"message cannot be empty") != NULL);
assert(strstr(i18n_text(LANG_ZH, I18N_EXEC_POST_EMPTY),

View file

@ -0,0 +1,44 @@
/* Unit tests for concise manual text language selection */
#include "../../include/manual_text.h"
#include <assert.h>
#include <stdio.h>
#include <string.h>
#define TEST(name) static void test_##name()
#define RUN_TEST(name) do { \
printf("Running %s... ", #name); \
test_##name(); \
printf("\n"); \
tests_passed++; \
} while(0)
static int tests_passed = 0;
TEST(interactive_manual_matches_language) {
const char *en = manual_text_interactive(LANG_EN);
const char *zh = manual_text_interactive(LANG_ZH);
assert(strstr(en, "TNT(1) help") != NULL);
assert(strstr(en, "Quick start") != NULL);
assert(strstr(en, "Commands") != NULL);
assert(strstr(en, ":mute-joins") != NULL);
assert(strstr(en, ":support") == NULL);
assert(strstr(en, ":commands") == NULL);
assert(strstr(zh, "TNT(1) 帮助") != NULL);
assert(strstr(zh, "快速开始") != NULL);
assert(strstr(zh, "命令") != NULL);
assert(strstr(zh, ":mute-joins") != NULL);
assert(strstr(zh, ":support") == NULL);
assert(strstr(zh, ":commands") == NULL);
}
int main(void) {
printf("Running manual text unit tests...\n\n");
RUN_TEST(interactive_manual_matches_language);
printf("\n✓ All %d tests passed!\n", tests_passed);
return 0;
}

View file

@ -1,52 +0,0 @@
/* Unit tests for support text language selection */
#include "../../include/support_text.h"
#include <assert.h>
#include <stdio.h>
#include <string.h>
#define TEST(name) static void test_##name()
#define RUN_TEST(name) do { \
printf("Running %s... ", #name); \
test_##name(); \
printf("\n"); \
tests_passed++; \
} while(0)
static int tests_passed = 0;
TEST(interactive_support_matches_language) {
const char *en = support_text_interactive(LANG_EN);
const char *zh = support_text_interactive(LANG_ZH);
assert(strstr(en, "Support") != NULL);
assert(strstr(en, "First minute") != NULL);
assert(strstr(en, ":mute-joins") != NULL);
assert(strstr(zh, "支持") != NULL);
assert(strstr(zh, "第一次进来") != NULL);
assert(strstr(zh, ":mute-joins") != NULL);
}
TEST(exec_support_matches_language) {
const char *en = support_text_exec(LANG_EN);
const char *zh = support_text_exec(LANG_ZH);
assert(strstr(en, "TNT support") != NULL);
assert(strstr(en, "Non-interactive checks") != NULL);
assert(strstr(en, "stats --json") != NULL);
assert(strstr(zh, "TNT 支持") != NULL);
assert(strstr(zh, "非交互检查") != NULL);
assert(strstr(zh, "stats --json") != NULL);
}
int main(void) {
printf("Running support text unit tests...\n\n");
RUN_TEST(interactive_support_matches_language);
RUN_TEST(exec_support_matches_language);
printf("\n✓ All %d tests passed!\n", tests_passed);
return 0;
}

16
tnt.1
View file

@ -1,5 +1,5 @@
.\" tnt(1) - Terminal Network Talk
.TH TNT 1 "May 2026" "TNT 1.0.1" "User Commands"
.TH TNT 1 "2026-05-24" "TNT 1.0.1" "User Commands"
.SH NAME
tnt \- anonymous SSH chat server with Vim\-style TUI
.SH SYNOPSIS
@ -15,7 +15,8 @@ tnt \- anonymous SSH chat server with Vim\-style TUI
is a multi\-user anonymous chat server accessed over SSH.
It provides a Vim\-style terminal user interface with INSERT, NORMAL, and
COMMAND modes.
Users connect with any standard SSH client; no account or registration is needed.
Users connect with any standard SSH client; no account or registration is
needed.
.PP
Messages are persisted to a log file and restored on server restart.
The server supports CJK and emoji input, rate limiting, access tokens, and
@ -44,7 +45,6 @@ Print version and exit.
.BR \-h ", " \-\-help
Print a short usage summary and exit.
.SH CONNECTING
.PP
.nf
ssh any\-username@hostname \-p 2222
.fi
@ -70,7 +70,7 @@ to return to INSERT,
.B :
to enter COMMAND mode,
.B ?
to open the help screen.
to open the full key reference.
.TP
.B COMMAND
Execute commands prefixed with
@ -102,7 +102,7 @@ End/Home Jump to bottom/top
g/G Jump to top/bottom
i Switch to INSERT
: Enter COMMAND mode
? Open help screen
? Open full key reference
Ctrl+C Disconnect
.TE
.PP
@ -117,12 +117,12 @@ l l.
:name \fIname\fR Alias for :nick
:msg \fIuser text\fR Send private whisper
:w \fIuser text\fR Short alias for :msg
:inbox Show private whispers
:last [\fIN\fR] Show last N messages from history (1\-50, default 10)
:search \fIkeyword\fR Case\-insensitive search across full message history
:mute\-joins Toggle join/leave system notifications on/off
:support Show quick support guide
:lang \fIen|zh\fR Switch UI language for this session
:help Show available commands
:help Show concise manual
:clear Clear command output
:q, :quit, :exit Disconnect
Up/Down Browse command history
@ -133,7 +133,6 @@ Commands can be run non\-interactively for scripting:
.PP
.nf
ssh host \-p 2222 help
ssh host \-p 2222 support
ssh host \-p 2222 users \-\-json
ssh host \-p 2222 stats \-\-json
ssh host \-p 2222 tail 20
@ -246,6 +245,7 @@ m1ngsama <contact@m1ng.space>
.SH BUGS
Report bugs at
.UR https://github.com/m1ngsama/TNT/issues
the project issue tracker
.UE .
.SH SEE ALSO
.BR ssh (1),