mirror of
https://oauth2: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@github.com/m1ngsama/TNT.git
synced 2026-06-26 05:34:39 +08:00
commands: centralize interactive command catalog
This commit is contained in:
parent
8eb311e54b
commit
57bf3cfc67
18 changed files with 606 additions and 274 deletions
1
.gitignore
vendored
1
.gitignore
vendored
|
|
@ -14,6 +14,7 @@ tests/unit/test_chat_room
|
|||
tests/unit/test_history_view
|
||||
tests/unit/test_i18n
|
||||
tests/unit/test_system_message
|
||||
tests/unit/test_command_catalog
|
||||
tests/unit/test_help_text
|
||||
tests/unit/test_manual_text
|
||||
tests/unit/test_support_text
|
||||
|
|
|
|||
|
|
@ -250,6 +250,7 @@ TNT/
|
|||
├── src/ # source code
|
||||
│ ├── main.c # entry point
|
||||
│ ├── cli_text.c # startup CLI help and option text
|
||||
│ ├── command_catalog.c # command metadata
|
||||
│ ├── ssh_server.c # SSH server implementation
|
||||
│ ├── bootstrap.c # SSH authentication and session bootstrap
|
||||
│ ├── chat_room.c # chat room logic
|
||||
|
|
|
|||
|
|
@ -3,6 +3,8 @@
|
|||
## Unreleased
|
||||
|
||||
### Changed
|
||||
- Command names, aliases, help summaries, concise-manual command rows, and
|
||||
unknown-command suggestions now share a dedicated `command_catalog` module.
|
||||
- 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.
|
||||
|
|
|
|||
|
|
@ -73,6 +73,7 @@ src/
|
|||
├── bootstrap.c - SSH authentication/session bootstrap
|
||||
├── input.c - Interactive session loop and key handling
|
||||
├── commands.c - COMMAND-mode command dispatch
|
||||
├── command_catalog.c - COMMAND-mode names, aliases, and help summaries
|
||||
├── exec.c - SSH exec command dispatch
|
||||
├── chat_room.c - Chat room logic and message broadcasting
|
||||
├── message.c - Message persistence (RFC3339 format)
|
||||
|
|
@ -97,6 +98,7 @@ include/
|
|||
├── bootstrap.h - SSH session bootstrap interface
|
||||
├── chat_room.h - Chat room interface
|
||||
├── message.h - Message structure and persistence
|
||||
├── command_catalog.h - COMMAND-mode command metadata interface
|
||||
├── history_view.h - Scroll-state helpers
|
||||
├── tui.h - TUI rendering functions
|
||||
├── i18n.h - Language and shared text IDs
|
||||
|
|
|
|||
|
|
@ -46,6 +46,7 @@ INSERT MODE
|
|||
STRUCTURE
|
||||
src/main.c entry, signals
|
||||
src/cli_text.c startup CLI text
|
||||
src/command_catalog.c command metadata
|
||||
src/ssh_server.c SSH listener and server setup
|
||||
src/bootstrap.c SSH auth/session bootstrap
|
||||
src/chat_room.c broadcast and room state
|
||||
|
|
|
|||
37
include/command_catalog.h
Normal file
37
include/command_catalog.h
Normal file
|
|
@ -0,0 +1,37 @@
|
|||
#ifndef COMMAND_CATALOG_H
|
||||
#define COMMAND_CATALOG_H
|
||||
|
||||
#include "common.h"
|
||||
|
||||
typedef enum {
|
||||
TNT_COMMAND_USERS,
|
||||
TNT_COMMAND_HELP,
|
||||
TNT_COMMAND_LANG,
|
||||
TNT_COMMAND_MSG,
|
||||
TNT_COMMAND_INBOX,
|
||||
TNT_COMMAND_NICK,
|
||||
TNT_COMMAND_LAST,
|
||||
TNT_COMMAND_SEARCH,
|
||||
TNT_COMMAND_MUTE_JOINS,
|
||||
TNT_COMMAND_QUIT,
|
||||
TNT_COMMAND_CLEAR,
|
||||
TNT_COMMAND_COUNT
|
||||
} tnt_command_id_t;
|
||||
|
||||
typedef struct {
|
||||
tnt_command_id_t id;
|
||||
const char *canonical;
|
||||
const char *names[4];
|
||||
bool accepts_args;
|
||||
} tnt_command_spec_t;
|
||||
|
||||
const tnt_command_spec_t *command_catalog_get(tnt_command_id_t id);
|
||||
bool command_catalog_match(const char *line, tnt_command_id_t *id,
|
||||
const char **args);
|
||||
const char *command_catalog_suggest(const char *name);
|
||||
void command_catalog_append_full(char *buffer, size_t buf_size, size_t *pos,
|
||||
help_lang_t lang);
|
||||
void command_catalog_append_manual(char *buffer, size_t buf_size, size_t *pos,
|
||||
help_lang_t lang);
|
||||
|
||||
#endif /* COMMAND_CATALOG_H */
|
||||
|
|
@ -3,6 +3,7 @@
|
|||
|
||||
#include "common.h"
|
||||
|
||||
const char *help_text_full(help_lang_t lang);
|
||||
void help_text_append_full(char *buffer, size_t buf_size, size_t *pos,
|
||||
help_lang_t lang);
|
||||
|
||||
#endif /* HELP_TEXT_H */
|
||||
|
|
|
|||
|
|
@ -3,6 +3,7 @@
|
|||
|
||||
#include "common.h"
|
||||
|
||||
const char *manual_text_interactive(help_lang_t lang);
|
||||
void manual_text_append_interactive(char *buffer, size_t buf_size,
|
||||
size_t *pos, help_lang_t lang);
|
||||
|
||||
#endif /* MANUAL_TEXT_H */
|
||||
|
|
|
|||
259
src/command_catalog.c
Normal file
259
src/command_catalog.c
Normal file
|
|
@ -0,0 +1,259 @@
|
|||
#include "command_catalog.h"
|
||||
|
||||
#include <string.h>
|
||||
|
||||
typedef struct {
|
||||
tnt_command_spec_t spec;
|
||||
const char *full_usage_en;
|
||||
const char *full_usage_zh;
|
||||
const char *summary_en;
|
||||
const char *summary_zh;
|
||||
const char *manual_usage_en;
|
||||
const char *manual_usage_zh;
|
||||
int manual_group;
|
||||
} command_catalog_entry_t;
|
||||
|
||||
static const command_catalog_entry_t entries[] = {
|
||||
{
|
||||
{TNT_COMMAND_USERS, "users", {"users", "list", "who", NULL}, false},
|
||||
":users, :list, :who", ":users, :list, :who",
|
||||
"Show online users", "显示在线用户",
|
||||
":users", ":users", 1
|
||||
},
|
||||
{
|
||||
{TNT_COMMAND_MSG, "msg", {"msg", "w", NULL}, true},
|
||||
":msg <user> <text>, :w <user> <text>",
|
||||
":msg <用户> <文本>, :w <用户> <文本>",
|
||||
"Whisper to user", "私聊",
|
||||
":msg <user> <text>", ":msg <用户> <文本>", 2
|
||||
},
|
||||
{
|
||||
{TNT_COMMAND_INBOX, "inbox", {"inbox", NULL}, false},
|
||||
":inbox", ":inbox",
|
||||
"Show whispers", "查看私聊",
|
||||
":inbox", ":inbox", 2
|
||||
},
|
||||
{
|
||||
{TNT_COMMAND_NICK, "nick", {"nick", "name", NULL}, true},
|
||||
":nick <name>, :name <name>", ":nick <名字>, :name <名字>",
|
||||
"Change nickname", "更改昵称",
|
||||
":nick <name>", ":nick <名字>", 2
|
||||
},
|
||||
{
|
||||
{TNT_COMMAND_LAST, "last", {"last", NULL}, true},
|
||||
":last [N]", ":last [N]",
|
||||
"Show last N messages (max 50)", "显示最后 N 条消息(最多50)",
|
||||
":last [N]", ":last [N]", 1
|
||||
},
|
||||
{
|
||||
{TNT_COMMAND_SEARCH, "search", {"search", NULL}, true},
|
||||
":search <keyword>", ":search <关键词>",
|
||||
"Search message history", "搜索消息历史",
|
||||
":search <keyword>", ":search <词>", 1
|
||||
},
|
||||
{
|
||||
{TNT_COMMAND_MUTE_JOINS, "mute-joins", {"mute-joins", "mute", NULL}, false},
|
||||
":mute-joins, :mute", ":mute-joins, :mute",
|
||||
"Toggle join/leave notices", "切换加入/离开提示",
|
||||
":mute-joins", ":mute-joins", 3
|
||||
},
|
||||
{
|
||||
{TNT_COMMAND_HELP, "help", {"help", NULL}, false},
|
||||
":help", ":help",
|
||||
"Show concise manual", "显示简明手册",
|
||||
NULL, NULL, 0
|
||||
},
|
||||
{
|
||||
{TNT_COMMAND_LANG, "lang", {"lang", "language", NULL}, true},
|
||||
":lang <en|zh>", ":lang <en|zh>",
|
||||
"Switch UI language", "切换界面语言",
|
||||
NULL, NULL, 0
|
||||
},
|
||||
{
|
||||
{TNT_COMMAND_CLEAR, "clear", {"clear", "cls", NULL}, false},
|
||||
":clear, :cls", ":clear, :cls",
|
||||
"Clear command output", "清空命令输出",
|
||||
":clear", ":clear", 3
|
||||
},
|
||||
{
|
||||
{TNT_COMMAND_QUIT, "q", {"q", "quit", "exit", NULL}, false},
|
||||
":q, :quit, :exit", ":q, :quit, :exit",
|
||||
"Disconnect", "断开连接",
|
||||
":q", ":q", 3
|
||||
}
|
||||
};
|
||||
|
||||
static const command_catalog_entry_t *entry_for_id(tnt_command_id_t id) {
|
||||
for (size_t i = 0; i < sizeof(entries) / sizeof(entries[0]); i++) {
|
||||
if (entries[i].spec.id == id) {
|
||||
return &entries[i];
|
||||
}
|
||||
}
|
||||
return NULL;
|
||||
}
|
||||
|
||||
static const char *skip_spaces(const char *value) {
|
||||
while (value && *value == ' ') {
|
||||
value++;
|
||||
}
|
||||
return value;
|
||||
}
|
||||
|
||||
static bool name_matches(const char *line, const char *name,
|
||||
const char **args) {
|
||||
size_t len;
|
||||
|
||||
if (!line || !name) {
|
||||
return false;
|
||||
}
|
||||
|
||||
len = strlen(name);
|
||||
if (strncmp(line, name, len) != 0) {
|
||||
return false;
|
||||
}
|
||||
if (line[len] != '\0' && line[len] != ' ') {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (args) {
|
||||
*args = skip_spaces(line + len);
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
static int min3(int a, int b, int c) {
|
||||
int m = a < b ? a : b;
|
||||
return m < c ? m : c;
|
||||
}
|
||||
|
||||
static int 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];
|
||||
}
|
||||
|
||||
const tnt_command_spec_t *command_catalog_get(tnt_command_id_t id) {
|
||||
const command_catalog_entry_t *entry = entry_for_id(id);
|
||||
return entry ? &entry->spec : NULL;
|
||||
}
|
||||
|
||||
bool command_catalog_match(const char *line, tnt_command_id_t *id,
|
||||
const char **args) {
|
||||
line = skip_spaces(line);
|
||||
if (!line || line[0] == '\0') {
|
||||
return false;
|
||||
}
|
||||
|
||||
for (size_t i = 0; i < sizeof(entries) / sizeof(entries[0]); i++) {
|
||||
const tnt_command_spec_t *spec = &entries[i].spec;
|
||||
for (size_t n = 0; n < sizeof(spec->names) / sizeof(spec->names[0]); n++) {
|
||||
const char *candidate_args = NULL;
|
||||
if (!spec->names[n]) {
|
||||
break;
|
||||
}
|
||||
if (!name_matches(line, spec->names[n], &candidate_args)) {
|
||||
continue;
|
||||
}
|
||||
if (candidate_args && candidate_args[0] != '\0' &&
|
||||
!spec->accepts_args) {
|
||||
continue;
|
||||
}
|
||||
if (id) {
|
||||
*id = spec->id;
|
||||
}
|
||||
if (args) {
|
||||
*args = candidate_args ? candidate_args : "";
|
||||
}
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
const char *command_catalog_suggest(const char *name) {
|
||||
const char *best = NULL;
|
||||
int best_distance = 99;
|
||||
|
||||
if (!name || !*name) {
|
||||
return NULL;
|
||||
}
|
||||
|
||||
for (size_t i = 0; i < sizeof(entries) / sizeof(entries[0]); i++) {
|
||||
const tnt_command_spec_t *spec = &entries[i].spec;
|
||||
for (size_t n = 0; n < sizeof(spec->names) / sizeof(spec->names[0]); n++) {
|
||||
int distance;
|
||||
if (!spec->names[n]) {
|
||||
break;
|
||||
}
|
||||
distance = edit_distance(name, spec->names[n]);
|
||||
if (distance < best_distance) {
|
||||
best_distance = distance;
|
||||
best = spec->canonical;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return best_distance <= 2 ? best : NULL;
|
||||
}
|
||||
|
||||
void command_catalog_append_full(char *buffer, size_t buf_size, size_t *pos,
|
||||
help_lang_t lang) {
|
||||
for (size_t i = 0; i < sizeof(entries) / sizeof(entries[0]); i++) {
|
||||
const char *usage = lang == LANG_ZH ? entries[i].full_usage_zh
|
||||
: entries[i].full_usage_en;
|
||||
const char *summary = lang == LANG_ZH ? entries[i].summary_zh
|
||||
: entries[i].summary_en;
|
||||
buffer_appendf(buffer, buf_size, pos, " %-40s - %s\n",
|
||||
usage, summary);
|
||||
}
|
||||
}
|
||||
|
||||
void command_catalog_append_manual(char *buffer, size_t buf_size, size_t *pos,
|
||||
help_lang_t lang) {
|
||||
for (int group = 1; group <= 3; group++) {
|
||||
bool first = true;
|
||||
|
||||
buffer_appendf(buffer, buf_size, pos, " ");
|
||||
for (size_t i = 0; i < sizeof(entries) / sizeof(entries[0]); i++) {
|
||||
const char *usage;
|
||||
if (entries[i].manual_group != group) {
|
||||
continue;
|
||||
}
|
||||
usage = lang == LANG_ZH ? entries[i].manual_usage_zh
|
||||
: entries[i].manual_usage_en;
|
||||
if (!usage) {
|
||||
continue;
|
||||
}
|
||||
if (!first) {
|
||||
buffer_appendf(buffer, buf_size, pos, ", ");
|
||||
}
|
||||
buffer_appendf(buffer, buf_size, pos, "%s", usage);
|
||||
first = false;
|
||||
}
|
||||
buffer_appendf(buffer, buf_size, pos, "\n");
|
||||
}
|
||||
}
|
||||
151
src/commands.c
151
src/commands.c
|
|
@ -7,6 +7,7 @@
|
|||
#include "commands.h"
|
||||
#include "chat_room.h"
|
||||
#include "client.h"
|
||||
#include "command_catalog.h"
|
||||
#include "common.h"
|
||||
#include "i18n.h"
|
||||
#include "manual.h"
|
||||
|
|
@ -46,65 +47,6 @@ 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", "lang", "language",
|
||||
"help", "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);
|
||||
|
|
@ -138,8 +80,34 @@ void commands_dispatch(client_t *client) {
|
|||
client->command_history_pos = client->command_history_count;
|
||||
}
|
||||
|
||||
if (strcmp(cmd, "list") == 0 || strcmp(cmd, "users") == 0 ||
|
||||
strcmp(cmd, "who") == 0) {
|
||||
if (cmd[0] == '\0') {
|
||||
/* Empty command */
|
||||
client->mode = MODE_NORMAL;
|
||||
client->command_input[0] = '\0';
|
||||
tui_render_screen(client);
|
||||
return;
|
||||
}
|
||||
|
||||
tnt_command_id_t command_id;
|
||||
const char *arg = "";
|
||||
if (!command_catalog_match(cmd, &command_id, &arg)) {
|
||||
const char *suggestion = command_catalog_suggest(cmd);
|
||||
buffer_appendf(output, sizeof(output), &pos,
|
||||
i18n_text(client->help_lang,
|
||||
I18N_UNKNOWN_COMMAND_FORMAT),
|
||||
cmd);
|
||||
if (suggestion) {
|
||||
buffer_appendf(output, sizeof(output), &pos,
|
||||
i18n_text(client->help_lang,
|
||||
I18N_DID_YOU_MEAN_FORMAT),
|
||||
suggestion);
|
||||
}
|
||||
buffer_appendf(output, sizeof(output), &pos, "%s",
|
||||
i18n_text(client->help_lang, I18N_UNKNOWN_GUIDANCE));
|
||||
goto cmd_done;
|
||||
}
|
||||
|
||||
if (command_id == TNT_COMMAND_USERS) {
|
||||
pthread_rwlock_rdlock(&g_room->lock);
|
||||
int total = g_room->client_count;
|
||||
buffer_appendf(output, sizeof(output), &pos,
|
||||
|
|
@ -167,22 +135,13 @@ void commands_dispatch(client_t *client) {
|
|||
}
|
||||
pthread_rwlock_unlock(&g_room->lock);
|
||||
|
||||
} else if (strcmp(cmd, "help") == 0) {
|
||||
} else if (command_id == TNT_COMMAND_HELP) {
|
||||
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 ||
|
||||
strncmp(cmd, "language ", 9) == 0) {
|
||||
char *arg = NULL;
|
||||
} else if (command_id == TNT_COMMAND_LANG) {
|
||||
help_lang_t next_lang;
|
||||
|
||||
if (strncmp(cmd, "lang ", 5) == 0) {
|
||||
arg = cmd + 5;
|
||||
} else if (strncmp(cmd, "language ", 9) == 0) {
|
||||
arg = cmd + 9;
|
||||
}
|
||||
|
||||
if (!arg || arg[0] == '\0') {
|
||||
buffer_appendf(output, sizeof(output), &pos,
|
||||
i18n_text(client->help_lang,
|
||||
|
|
@ -201,9 +160,8 @@ void commands_dispatch(client_t *client) {
|
|||
arg);
|
||||
}
|
||||
|
||||
} else if (strcmp(cmd, "msg") == 0 || strcmp(cmd, "w") == 0 ||
|
||||
strncmp(cmd, "msg ", 4) == 0 || strncmp(cmd, "w ", 2) == 0) {
|
||||
char *rest = (cmd[0] == 'w') ? cmd + 1 : cmd + 3;
|
||||
} else if (command_id == TNT_COMMAND_MSG) {
|
||||
const char *rest = arg;
|
||||
while (*rest == ' ') rest++;
|
||||
char target_name[MAX_USERNAME_LEN] = {0};
|
||||
int ti = 0;
|
||||
|
|
@ -273,7 +231,7 @@ void commands_dispatch(client_t *client) {
|
|||
}
|
||||
}
|
||||
|
||||
} else if (strcmp(cmd, "inbox") == 0) {
|
||||
} else if (command_id == TNT_COMMAND_INBOX) {
|
||||
/* Snapshot the inbox under io_lock so a concurrent sender doesn't
|
||||
* tear what we're rendering. Counter reset happens after copy. */
|
||||
whisper_t snapshot[WHISPER_INBOX_SIZE];
|
||||
|
|
@ -304,9 +262,8 @@ void commands_dispatch(client_t *client) {
|
|||
ts, snapshot[i].from, snapshot[i].content);
|
||||
}
|
||||
|
||||
} else if (strcmp(cmd, "nick") == 0 || strcmp(cmd, "name") == 0 ||
|
||||
strncmp(cmd, "nick ", 5) == 0 || strncmp(cmd, "name ", 5) == 0) {
|
||||
char *new_name = cmd + 4;
|
||||
} else if (command_id == TNT_COMMAND_NICK) {
|
||||
const char *new_name = arg;
|
||||
while (*new_name == ' ') new_name++;
|
||||
|
||||
if (new_name[0] == '\0') {
|
||||
|
|
@ -367,8 +324,7 @@ void commands_dispatch(client_t *client) {
|
|||
}
|
||||
}
|
||||
|
||||
} else if (strncmp(cmd, "last", 4) == 0 && (cmd[4] == ' ' || cmd[4] == '\0')) {
|
||||
char *arg = cmd + 4;
|
||||
} else if (command_id == TNT_COMMAND_LAST) {
|
||||
while (*arg == ' ') arg++;
|
||||
int n = 10;
|
||||
if (*arg != '\0') {
|
||||
|
|
@ -397,8 +353,8 @@ void commands_dispatch(client_t *client) {
|
|||
}
|
||||
free(last_msgs);
|
||||
|
||||
} else if (strcmp(cmd, "search") == 0 || strncmp(cmd, "search ", 7) == 0) {
|
||||
char *query = cmd + 6;
|
||||
} else if (command_id == TNT_COMMAND_SEARCH) {
|
||||
const char *query = arg;
|
||||
while (*query == ' ') query++;
|
||||
if (*query == '\0') {
|
||||
buffer_appendf(output, sizeof(output), &pos, "%s",
|
||||
|
|
@ -427,7 +383,7 @@ void commands_dispatch(client_t *client) {
|
|||
free(found);
|
||||
}
|
||||
|
||||
} else if (strcmp(cmd, "mute-joins") == 0 || strcmp(cmd, "mute") == 0) {
|
||||
} else if (command_id == TNT_COMMAND_MUTE_JOINS) {
|
||||
client->mute_joins = !client->mute_joins;
|
||||
buffer_appendf(output, sizeof(output), &pos,
|
||||
i18n_text(client->help_lang, I18N_MUTE_JOINS_FORMAT),
|
||||
|
|
@ -436,36 +392,13 @@ void commands_dispatch(client_t *client) {
|
|||
I18N_MUTE_JOINS_MUTED :
|
||||
I18N_MUTE_JOINS_UNMUTED));
|
||||
|
||||
} else if (strcmp(cmd, "q") == 0 || strcmp(cmd, "quit") == 0 ||
|
||||
strcmp(cmd, "exit") == 0) {
|
||||
} else if (command_id == TNT_COMMAND_QUIT) {
|
||||
client->connected = false;
|
||||
return;
|
||||
|
||||
} else if (strcmp(cmd, "clear") == 0 || strcmp(cmd, "cls") == 0) {
|
||||
} else if (command_id == TNT_COMMAND_CLEAR) {
|
||||
buffer_appendf(output, sizeof(output), &pos, "%s",
|
||||
i18n_text(client->help_lang, I18N_CLEAR_DONE));
|
||||
|
||||
} else if (cmd[0] == '\0') {
|
||||
/* Empty command */
|
||||
client->mode = MODE_NORMAL;
|
||||
client->command_input[0] = '\0';
|
||||
tui_render_screen(client);
|
||||
return;
|
||||
|
||||
} else {
|
||||
const char *suggestion = suggest_command(cmd);
|
||||
buffer_appendf(output, sizeof(output), &pos,
|
||||
i18n_text(client->help_lang,
|
||||
I18N_UNKNOWN_COMMAND_FORMAT),
|
||||
cmd);
|
||||
if (suggestion) {
|
||||
buffer_appendf(output, sizeof(output), &pos,
|
||||
i18n_text(client->help_lang,
|
||||
I18N_DID_YOU_MEAN_FORMAT),
|
||||
suggestion);
|
||||
}
|
||||
buffer_appendf(output, sizeof(output), &pos, "%s",
|
||||
i18n_text(client->help_lang, I18N_UNKNOWN_GUIDANCE));
|
||||
}
|
||||
|
||||
cmd_done:
|
||||
|
|
|
|||
204
src/help_text.c
204
src/help_text.c
|
|
@ -1,115 +1,101 @@
|
|||
#include "help_text.h"
|
||||
|
||||
const char *help_text_full(help_lang_t lang) {
|
||||
#include "command_catalog.h"
|
||||
|
||||
void help_text_append_full(char *buffer, size_t buf_size, size_t *pos,
|
||||
help_lang_t lang) {
|
||||
if (lang == LANG_EN) {
|
||||
return "TNT KEY REFERENCE\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"
|
||||
" Opens at latest messages\n"
|
||||
" Follows latest until you scroll up\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"
|
||||
" PgDn/PgUp - Scroll full page down/up\n"
|
||||
" End/Home - Jump to bottom/top\n"
|
||||
" g/G - Jump to top/bottom\n"
|
||||
" ? - Show full key reference\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"
|
||||
" :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"
|
||||
" :help - Show concise manual\n"
|
||||
" :lang <en|zh> - Switch UI language\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"
|
||||
" 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"
|
||||
" e/z - Switch English/Chinese\n";
|
||||
buffer_appendf(buffer, buf_size, pos,
|
||||
"TNT KEY REFERENCE\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"
|
||||
" Opens at latest messages\n"
|
||||
" Follows latest until you scroll up\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"
|
||||
" PgDn/PgUp - Scroll full page down/up\n"
|
||||
" End/Home - Jump to bottom/top\n"
|
||||
" g/G - Jump to top/bottom\n"
|
||||
" ? - Show full key reference\n"
|
||||
" Ctrl+C - Exit chat\n"
|
||||
"\n"
|
||||
"AVAILABLE COMMANDS:\n");
|
||||
command_catalog_append_full(buffer, buf_size, pos, lang);
|
||||
buffer_appendf(buffer, buf_size, pos,
|
||||
"\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"
|
||||
" 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"
|
||||
" e/z - Switch English/Chinese\n");
|
||||
return;
|
||||
}
|
||||
|
||||
return "TNT 按键参考\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"
|
||||
" 默认停在最新消息\n"
|
||||
" 未向上翻阅时自动跟随最新消息\n"
|
||||
" i - 返回 INSERT 模式\n"
|
||||
" : - 进入 COMMAND 模式\n"
|
||||
" j/k - 向下/上滚动一行\n"
|
||||
" Ctrl+D/U - 向下/上滚动半页\n"
|
||||
" Ctrl+F/B - 向下/上滚动整页\n"
|
||||
" PgDn/PgUp - 向下/上滚动整页\n"
|
||||
" End/Home - 跳到底部/顶部\n"
|
||||
" g/G - 跳到顶部/底部\n"
|
||||
" ? - 显示完整按键参考\n"
|
||||
" Ctrl+C - 退出聊天\n"
|
||||
"\n"
|
||||
"可用命令:\n"
|
||||
" :list, :users - 显示在线用户\n"
|
||||
" :nick <名字> - 更改昵称\n"
|
||||
" :msg <用户> <文本> - 私聊\n"
|
||||
" :w <用户> <文本> - :msg 的简写\n"
|
||||
" :inbox - 查看私聊\n"
|
||||
" :last [N] - 显示最后 N 条消息(最多50)\n"
|
||||
" :search <关键词> - 搜索消息历史\n"
|
||||
" :mute-joins - 切换加入/离开提示\n"
|
||||
" :help - 显示简明手册\n"
|
||||
" :lang <en|zh> - 切换界面语言\n"
|
||||
" :clear - 清空命令输出\n"
|
||||
" :q, :quit, :exit - 断开连接\n"
|
||||
"\n"
|
||||
"特殊消息:\n"
|
||||
" /me <动作> - 发送动作 (如 /me 挥手)\n"
|
||||
" @用户名 - 提及用户 (响铃+高亮)\n"
|
||||
"\n"
|
||||
"帮助界面按键:\n"
|
||||
" q, ESC - 关闭帮助\n"
|
||||
" j/k - 向下/上滚动\n"
|
||||
" Ctrl+D/U - 向下/上滚动半页\n"
|
||||
" Ctrl+F/B - 向下/上滚动整页\n"
|
||||
" g/G - 跳到顶部/底部\n"
|
||||
" e/z - 切换英文/中文\n";
|
||||
buffer_appendf(buffer, buf_size, pos,
|
||||
"TNT 按键参考\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"
|
||||
" 默认停在最新消息\n"
|
||||
" 未向上翻阅时自动跟随最新消息\n"
|
||||
" i - 返回 INSERT 模式\n"
|
||||
" : - 进入 COMMAND 模式\n"
|
||||
" j/k - 向下/上滚动一行\n"
|
||||
" Ctrl+D/U - 向下/上滚动半页\n"
|
||||
" Ctrl+F/B - 向下/上滚动整页\n"
|
||||
" PgDn/PgUp - 向下/上滚动整页\n"
|
||||
" End/Home - 跳到底部/顶部\n"
|
||||
" g/G - 跳到顶部/底部\n"
|
||||
" ? - 显示完整按键参考\n"
|
||||
" Ctrl+C - 退出聊天\n"
|
||||
"\n"
|
||||
"可用命令:\n");
|
||||
command_catalog_append_full(buffer, buf_size, pos, lang);
|
||||
buffer_appendf(buffer, buf_size, pos,
|
||||
"\n"
|
||||
"特殊消息:\n"
|
||||
" /me <动作> - 发送动作 (如 /me 挥手)\n"
|
||||
" @用户名 - 提及用户 (响铃+高亮)\n"
|
||||
"\n"
|
||||
"帮助界面按键:\n"
|
||||
" q, ESC - 关闭帮助\n"
|
||||
" j/k - 向下/上滚动\n"
|
||||
" Ctrl+D/U - 向下/上滚动半页\n"
|
||||
" Ctrl+F/B - 向下/上滚动整页\n"
|
||||
" g/G - 跳到顶部/底部\n"
|
||||
" e/z - 切换英文/中文\n");
|
||||
}
|
||||
|
|
|
|||
|
|
@ -5,6 +5,5 @@ 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));
|
||||
manual_text_append_interactive(buffer, buf_size, pos, lang);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,47 +1,51 @@
|
|||
#include "manual_text.h"
|
||||
|
||||
const char *manual_text_interactive(help_lang_t lang) {
|
||||
#include "command_catalog.h"
|
||||
|
||||
void manual_text_append_interactive(char *buffer, size_t buf_size,
|
||||
size_t *pos, 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 发送;Esc 浏览历史;G 最新;i 输入\n"
|
||||
" : 运行命令;? 打开完整按键参考\n"
|
||||
"\n"
|
||||
"\033[1;37m命令\033[0m\n"
|
||||
" :users, :last [N], :search <词>\n"
|
||||
" :msg <用户> <文本>, :inbox, :nick <名字>\n"
|
||||
" :mute-joins, :clear, :q\n"
|
||||
"\n"
|
||||
"\033[1;37m语言\033[0m\n"
|
||||
" :lang 显示当前语言\n"
|
||||
" :lang en|zh 切换语言\n"
|
||||
"\n"
|
||||
"\033[1;37m参见\033[0m\n"
|
||||
" ? 完整按键参考\n";
|
||||
buffer_appendf(buffer, buf_size, pos,
|
||||
"\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 发送;Esc 浏览历史;G 最新;i 输入\n"
|
||||
" : 运行命令;? 打开完整按键参考\n"
|
||||
"\n"
|
||||
"\033[1;37m命令\033[0m\n");
|
||||
command_catalog_append_manual(buffer, buf_size, pos, lang);
|
||||
buffer_appendf(buffer, buf_size, pos,
|
||||
"\n"
|
||||
"\033[1;37m语言\033[0m\n"
|
||||
" :lang 显示当前语言\n"
|
||||
" :lang en|zh 切换语言\n"
|
||||
"\n"
|
||||
"\033[1;37m参见\033[0m\n"
|
||||
" ? 完整按键参考\n");
|
||||
return;
|
||||
}
|
||||
|
||||
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;37mUse\033[0m\n"
|
||||
" Type a message and press Enter; Esc browses; G latest; i types\n"
|
||||
" : runs commands; ? opens the full key reference\n"
|
||||
"\n"
|
||||
"\033[1;37mCommands\033[0m\n"
|
||||
" :users, :last [N], :search <keyword>\n"
|
||||
" :msg <user> <text>, :inbox, :nick <name>\n"
|
||||
" :mute-joins, :clear, :q\n"
|
||||
"\n"
|
||||
"\033[1;37mLanguage\033[0m\n"
|
||||
" :lang show current language\n"
|
||||
" :lang en|zh switch language\n"
|
||||
"\n"
|
||||
"\033[1;37mSee also\033[0m\n"
|
||||
" ? full key reference\n";
|
||||
buffer_appendf(buffer, buf_size, pos,
|
||||
"\033[1;36mTNT(1) help\033[0m\n"
|
||||
"\n"
|
||||
"\033[1;37mName\033[0m\n"
|
||||
" TNT - SSH terminal chat room\n"
|
||||
"\n"
|
||||
"\033[1;37mUse\033[0m\n"
|
||||
" Type a message and press Enter; Esc browses; G latest; i types\n"
|
||||
" : runs commands; ? opens the full key reference\n"
|
||||
"\n"
|
||||
"\033[1;37mCommands\033[0m\n");
|
||||
command_catalog_append_manual(buffer, buf_size, pos, lang);
|
||||
buffer_appendf(buffer, buf_size, pos,
|
||||
"\n"
|
||||
"\033[1;37mLanguage\033[0m\n"
|
||||
" :lang show current language\n"
|
||||
" :lang en|zh switch language\n"
|
||||
"\n"
|
||||
"\033[1;37mSee also\033[0m\n"
|
||||
" ? full key reference\n");
|
||||
}
|
||||
|
|
|
|||
|
|
@ -777,11 +777,11 @@ void tui_render_help(client_t *client) {
|
|||
}
|
||||
buffer_appendf(buffer, sizeof(buffer), &pos, ANSI_RESET "\r\n");
|
||||
|
||||
/* Help content */
|
||||
const char *help_text = help_text_full(client->help_lang);
|
||||
char help_copy[8192];
|
||||
strncpy(help_copy, help_text, sizeof(help_copy) - 1);
|
||||
help_copy[sizeof(help_copy) - 1] = '\0';
|
||||
size_t help_pos = 0;
|
||||
help_copy[0] = '\0';
|
||||
help_text_append_full(help_copy, sizeof(help_copy), &help_pos,
|
||||
client->help_lang);
|
||||
|
||||
/* Split into lines and display with scrolling */
|
||||
char *lines[100];
|
||||
|
|
|
|||
|
|
@ -13,6 +13,7 @@ endif
|
|||
UTF8_SRC = ../../src/utf8.c
|
||||
MESSAGE_SRC = ../../src/message.c
|
||||
COMMON_SRC = ../../src/common.c
|
||||
COMMAND_CATALOG_SRC = ../../src/command_catalog.c
|
||||
CLI_TEXT_SRC = ../../src/cli_text.c
|
||||
CHAT_ROOM_SRC = ../../src/chat_room.c
|
||||
HISTORY_VIEW_SRC = ../../src/history_view.c
|
||||
|
|
@ -22,7 +23,7 @@ HELP_TEXT_SRC = ../../src/help_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_manual_text test_cli_text test_ratelimit
|
||||
TESTS = test_utf8 test_message test_chat_room test_history_view test_i18n test_system_message test_command_catalog test_help_text test_manual_text test_cli_text test_ratelimit
|
||||
|
||||
.PHONY: all clean run
|
||||
|
||||
|
|
@ -46,10 +47,13 @@ test_i18n: test_i18n.c $(I18N_SRC)
|
|||
test_system_message: test_system_message.c $(SYSTEM_MESSAGE_SRC) $(I18N_SRC)
|
||||
$(CC) $(CFLAGS) -o $@ $^ $(LDFLAGS)
|
||||
|
||||
test_help_text: test_help_text.c $(HELP_TEXT_SRC) $(COMMON_SRC)
|
||||
test_command_catalog: test_command_catalog.c $(COMMAND_CATALOG_SRC) $(COMMON_SRC)
|
||||
$(CC) $(CFLAGS) -o $@ $^ $(LDFLAGS)
|
||||
|
||||
test_manual_text: test_manual_text.c $(MANUAL_TEXT_SRC)
|
||||
test_help_text: test_help_text.c $(HELP_TEXT_SRC) $(COMMAND_CATALOG_SRC) $(COMMON_SRC)
|
||||
$(CC) $(CFLAGS) -o $@ $^ $(LDFLAGS)
|
||||
|
||||
test_manual_text: test_manual_text.c $(MANUAL_TEXT_SRC) $(COMMAND_CATALOG_SRC) $(COMMON_SRC)
|
||||
$(CC) $(CFLAGS) -o $@ $^ $(LDFLAGS)
|
||||
|
||||
test_cli_text: test_cli_text.c $(CLI_TEXT_SRC) $(COMMON_SRC)
|
||||
|
|
@ -77,6 +81,9 @@ run: all
|
|||
@echo "=== Running System Message Tests ==="
|
||||
./test_system_message
|
||||
@echo ""
|
||||
@echo "=== Running Command Catalog Tests ==="
|
||||
./test_command_catalog
|
||||
@echo ""
|
||||
@echo "=== Running Help Text Tests ==="
|
||||
./test_help_text
|
||||
@echo ""
|
||||
|
|
|
|||
86
tests/unit/test_command_catalog.c
Normal file
86
tests/unit/test_command_catalog.c
Normal file
|
|
@ -0,0 +1,86 @@
|
|||
/* Unit tests for command catalog names, aliases, and generated help text */
|
||||
|
||||
#include "../../include/command_catalog.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(matches_canonical_names_and_aliases) {
|
||||
tnt_command_id_t id;
|
||||
const char *args;
|
||||
|
||||
assert(command_catalog_match("users", &id, &args));
|
||||
assert(id == TNT_COMMAND_USERS);
|
||||
assert(strcmp(args, "") == 0);
|
||||
|
||||
assert(command_catalog_match("list", &id, &args));
|
||||
assert(id == TNT_COMMAND_USERS);
|
||||
|
||||
assert(command_catalog_match("msg alice hello", &id, &args));
|
||||
assert(id == TNT_COMMAND_MSG);
|
||||
assert(strcmp(args, "alice hello") == 0);
|
||||
|
||||
assert(command_catalog_match("w alice hello", &id, &args));
|
||||
assert(id == TNT_COMMAND_MSG);
|
||||
assert(strcmp(args, "alice hello") == 0);
|
||||
|
||||
assert(command_catalog_match("language zh", &id, &args));
|
||||
assert(id == TNT_COMMAND_LANG);
|
||||
assert(strcmp(args, "zh") == 0);
|
||||
}
|
||||
|
||||
TEST(rejects_arguments_for_no_arg_commands) {
|
||||
tnt_command_id_t id;
|
||||
const char *args;
|
||||
|
||||
assert(!command_catalog_match("users extra", &id, &args));
|
||||
assert(!command_catalog_match("help now", &id, &args));
|
||||
assert(!command_catalog_match("q now", &id, &args));
|
||||
}
|
||||
|
||||
TEST(suggests_from_catalog_aliases) {
|
||||
assert(strcmp(command_catalog_suggest("hlep"), "help") == 0);
|
||||
assert(strcmp(command_catalog_suggest("usres"), "users") == 0);
|
||||
assert(strcmp(command_catalog_suggest("laguage"), "lang") == 0);
|
||||
assert(command_catalog_suggest("not-even-close") == NULL);
|
||||
}
|
||||
|
||||
TEST(generates_localized_help_sections) {
|
||||
char en[4096] = {0};
|
||||
char zh[4096] = {0};
|
||||
size_t en_pos = 0;
|
||||
size_t zh_pos = 0;
|
||||
|
||||
command_catalog_append_full(en, sizeof(en), &en_pos, LANG_EN);
|
||||
command_catalog_append_full(zh, sizeof(zh), &zh_pos, LANG_ZH);
|
||||
|
||||
assert(strstr(en, ":users, :list, :who") != NULL);
|
||||
assert(strstr(en, "Show online users") != NULL);
|
||||
assert(strstr(en, ":support") == NULL);
|
||||
|
||||
assert(strstr(zh, ":users, :list, :who") != NULL);
|
||||
assert(strstr(zh, "显示在线用户") != NULL);
|
||||
assert(strstr(zh, ":support") == NULL);
|
||||
}
|
||||
|
||||
int main(void) {
|
||||
printf("Running command catalog unit tests...\n\n");
|
||||
|
||||
RUN_TEST(matches_canonical_names_and_aliases);
|
||||
RUN_TEST(rejects_arguments_for_no_arg_commands);
|
||||
RUN_TEST(suggests_from_catalog_aliases);
|
||||
RUN_TEST(generates_localized_help_sections);
|
||||
|
||||
printf("\n✓ All %d tests passed!\n", tests_passed);
|
||||
return 0;
|
||||
}
|
||||
|
|
@ -16,8 +16,13 @@
|
|||
static int tests_passed = 0;
|
||||
|
||||
TEST(full_help_matches_language) {
|
||||
const char *en = help_text_full(LANG_EN);
|
||||
const char *zh = help_text_full(LANG_ZH);
|
||||
char en[8192] = {0};
|
||||
char zh[8192] = {0};
|
||||
size_t en_pos = 0;
|
||||
size_t zh_pos = 0;
|
||||
|
||||
help_text_append_full(en, sizeof(en), &en_pos, LANG_EN);
|
||||
help_text_append_full(zh, sizeof(zh), &zh_pos, LANG_ZH);
|
||||
|
||||
assert(strstr(en, "TNT KEY REFERENCE") != NULL);
|
||||
assert(strstr(en, "AVAILABLE COMMANDS") != NULL);
|
||||
|
|
|
|||
|
|
@ -29,14 +29,20 @@ static int count_lines(const char *text) {
|
|||
}
|
||||
|
||||
TEST(interactive_manual_matches_language) {
|
||||
const char *en = manual_text_interactive(LANG_EN);
|
||||
const char *zh = manual_text_interactive(LANG_ZH);
|
||||
char en[4096] = {0};
|
||||
char zh[4096] = {0};
|
||||
size_t en_pos = 0;
|
||||
size_t zh_pos = 0;
|
||||
|
||||
manual_text_append_interactive(en, sizeof(en), &en_pos, LANG_EN);
|
||||
manual_text_append_interactive(zh, sizeof(zh), &zh_pos, LANG_ZH);
|
||||
|
||||
assert(strstr(en, "TNT(1) help") != NULL);
|
||||
assert(strstr(en, "Use") != NULL);
|
||||
assert(strstr(en, "Commands") != NULL);
|
||||
assert(strstr(en, ":lang en|zh") != NULL);
|
||||
assert(strstr(en, ":mute-joins") != NULL);
|
||||
assert(strstr(en, ":mute-joins, :clear, :q") != NULL);
|
||||
assert(strstr(en, ":support") == NULL);
|
||||
assert(strstr(en, ":commands") == NULL);
|
||||
assert(count_lines(en) <= 20);
|
||||
|
|
@ -46,6 +52,7 @@ TEST(interactive_manual_matches_language) {
|
|||
assert(strstr(zh, "命令") != NULL);
|
||||
assert(strstr(zh, ":lang en|zh") != NULL);
|
||||
assert(strstr(zh, ":mute-joins") != NULL);
|
||||
assert(strstr(zh, ":mute-joins, :clear, :q") != NULL);
|
||||
assert(strstr(zh, ":support") == NULL);
|
||||
assert(strstr(zh, ":commands") == NULL);
|
||||
assert(count_lines(zh) <= 20);
|
||||
|
|
|
|||
Loading…
Reference in a new issue