mirror of
https://oauth2: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@github.com/m1ngsama/TNT.git
synced 2026-06-26 04:34:38 +08:00
Improve TUI pager and search ergonomics
This commit is contained in:
parent
1c451b7722
commit
797ecbb992
9 changed files with 380 additions and 89 deletions
|
|
@ -28,6 +28,8 @@
|
|||
- Added a VHS tape draft for recording the core TNT terminal-chat experience.
|
||||
- Added live `:inbox` refresh behavior: `r` refreshes the inbox manually, and
|
||||
an open inbox refreshes when a new private message arrives.
|
||||
- Added `/` in NORMAL mode as a fast history-search entrypoint backed by the
|
||||
existing `:search` command.
|
||||
- Added `make slow-client-test`, an opt-in regression for an unread
|
||||
interactive SSH client under backpressure while health, stats, post, tail,
|
||||
and server survival stay responsive.
|
||||
|
|
@ -73,6 +75,12 @@
|
|||
contract.
|
||||
- The two-user lifecycle test now covers opening `:inbox` before a private
|
||||
message arrives, matching the way users often leave an inbox page open.
|
||||
- Help and command-output pagers now accept arrow keys, PgUp/PgDn, Home/End,
|
||||
and Space/`b` in addition to the existing Vim-style keys.
|
||||
- Pre-login username entry now handles Ctrl+C/Ctrl+D cancel, Ctrl+U clear
|
||||
line, and Ctrl+W delete-word before the user joins the room.
|
||||
- Long COMMAND-mode input is now left-truncated with a visible marker in the
|
||||
status line instead of wrapping and damaging the TUI.
|
||||
- Private-message inbox access now uses its own mutex instead of sharing the
|
||||
SSH channel write lock, reducing unrelated contention on slow clients.
|
||||
- Client writes now check the SSH channel's remote window before writing and
|
||||
|
|
|
|||
|
|
@ -64,7 +64,10 @@ Esc enter NORMAL mode
|
|||
i return to INSERT mode
|
||||
: enter COMMAND mode
|
||||
? open the full key reference
|
||||
/ search message history
|
||||
G or End jump to latest messages
|
||||
Up/Down recall sent messages in INSERT mode
|
||||
Tab complete @mention in INSERT mode
|
||||
Ctrl+C disconnect from NORMAL mode
|
||||
```
|
||||
|
||||
|
|
@ -209,7 +212,10 @@ Esc 进入 NORMAL 模式
|
|||
i 回到 INSERT 模式
|
||||
: 输入命令
|
||||
? 查看完整按键参考
|
||||
/ 搜索消息历史
|
||||
G 或 End 回到最新消息
|
||||
Up/Down 在 INSERT 模式调出已发送消息
|
||||
Tab 在 INSERT 模式补全 @mention
|
||||
:help 查看简明手册
|
||||
:lang en|zh 切换界面语言
|
||||
:q 断开连接
|
||||
|
|
|
|||
|
|
@ -11,8 +11,9 @@ The product path should stay short:
|
|||
4. User lands in INSERT mode at the live tail and can type immediately.
|
||||
5. User presses Esc to browse history with Vim-style movement.
|
||||
6. User uses `:help` for the concise manual or `?` for the full key reference.
|
||||
7. User uses commands only when needed: `:users`, `:msg`, `:inbox`, `:last`,
|
||||
`:search`, `:nick`, `:mute-joins`, and `:q`.
|
||||
7. User searches from NORMAL with `/term`, or uses commands when needed:
|
||||
`:users`, `:msg`, `:inbox`, `:last`, `:search`, `:nick`, `:mute-joins`,
|
||||
and `:q`.
|
||||
8. Scripts and operators use `tntctl` or SSH exec commands for `health`,
|
||||
`stats`, `users`, `tail`, `dump`, and `post`.
|
||||
|
||||
|
|
@ -23,6 +24,10 @@ The product path should stay short:
|
|||
- INSERT mode is the default because most users arrive to send a message.
|
||||
- NORMAL mode opens at the latest messages, not the oldest history. Users can
|
||||
move upward for older context and use `G` or End to return to live chat.
|
||||
- NORMAL mode accepts `/` as the fast path for history search, matching a
|
||||
common terminal-reader habit while reusing the existing `:search` command.
|
||||
- INSERT mode keeps a small per-session sent-message history on Up/Down and
|
||||
completes trailing `@mention` prefixes with Tab.
|
||||
- `:help` is a compact manual, while `?` is a full key reference. Do not add
|
||||
parallel support commands for the same task.
|
||||
- Command syntax stays ASCII even in localized UI text. Translations explain;
|
||||
|
|
|
|||
|
|
@ -19,6 +19,8 @@ void help_text_append_full(char *buffer, size_t buf_size, size_t *pos,
|
|||
" Backspace - Delete character\n"
|
||||
" Ctrl+W - Delete last word\n"
|
||||
" Ctrl+U - Delete line\n"
|
||||
" Up/Down - Recall sent messages\n"
|
||||
" Tab - Complete @mention\n"
|
||||
" Ctrl+C - Enter NORMAL mode\n"
|
||||
"\n"
|
||||
"NORMAL MODE KEYS:\n"
|
||||
|
|
@ -26,6 +28,7 @@ void help_text_append_full(char *buffer, size_t buf_size, size_t *pos,
|
|||
" Follows latest until you scroll up\n"
|
||||
" i - Return to INSERT mode\n"
|
||||
" : - Enter COMMAND mode\n"
|
||||
" / - Search message history\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"
|
||||
|
|
@ -49,6 +52,8 @@ void help_text_append_full(char *buffer, size_t buf_size, size_t *pos,
|
|||
" Backspace - 删除字符\n"
|
||||
" Ctrl+W - 删除上个单词\n"
|
||||
" Ctrl+U - 删除整行\n"
|
||||
" Up/Down - 调出已发送消息\n"
|
||||
" Tab - 补全 @mention\n"
|
||||
" Ctrl+C - 进入 NORMAL 模式\n"
|
||||
"\n"
|
||||
"NORMAL 模式按键:\n"
|
||||
|
|
@ -56,6 +61,7 @@ void help_text_append_full(char *buffer, size_t buf_size, size_t *pos,
|
|||
" 未向上翻阅时自动跟随最新消息\n"
|
||||
" i - 返回 INSERT 模式\n"
|
||||
" : - 进入 COMMAND 模式\n"
|
||||
" / - 搜索消息历史\n"
|
||||
" j/k - 向下/上滚动一行\n"
|
||||
" Ctrl+D/U - 向下/上滚动半页\n"
|
||||
" Ctrl+F/B - 向下/上滚动整页\n"
|
||||
|
|
@ -71,9 +77,12 @@ void help_text_append_full(char *buffer, size_t buf_size, size_t *pos,
|
|||
"\n"
|
||||
"COMMAND OUTPUT KEYS:\n"
|
||||
" q, ESC - Close output\n"
|
||||
" j/k - Scroll down/up\n"
|
||||
" j/k, arrows - Scroll down/up\n"
|
||||
" Ctrl+D/U - Scroll half page down/up\n"
|
||||
" Ctrl+F/B - Scroll full page down/up\n"
|
||||
" Space/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"
|
||||
" r - Refresh live output (:inbox)\n"
|
||||
"\n"
|
||||
|
|
@ -83,17 +92,23 @@ void help_text_append_full(char *buffer, size_t buf_size, size_t *pos,
|
|||
"\n"
|
||||
"HELP SCREEN KEYS:\n"
|
||||
" q, ESC - Close help\n"
|
||||
" j/k - Scroll down/up\n"
|
||||
" j/k, arrows - Scroll down/up\n"
|
||||
" Ctrl+D/U - Scroll half page down/up\n"
|
||||
" Ctrl+F/B - Scroll full page down/up\n"
|
||||
" Space/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"
|
||||
" l - Cycle UI language\n",
|
||||
"\n"
|
||||
"命令输出按键:\n"
|
||||
" q, ESC - 关闭输出\n"
|
||||
" j/k - 向下/上滚动\n"
|
||||
" j/k, arrows - 向下/上滚动\n"
|
||||
" Ctrl+D/U - 向下/上滚动半页\n"
|
||||
" Ctrl+F/B - 向下/上滚动整页\n"
|
||||
" Space/b - 向下/上滚动整页\n"
|
||||
" PgDn/PgUp - 向下/上滚动整页\n"
|
||||
" End/Home - 跳到底部/顶部\n"
|
||||
" g/G - 跳到顶部/底部\n"
|
||||
" r - 刷新动态输出 (:inbox)\n"
|
||||
"\n"
|
||||
|
|
@ -103,9 +118,12 @@ void help_text_append_full(char *buffer, size_t buf_size, size_t *pos,
|
|||
"\n"
|
||||
"帮助界面按键:\n"
|
||||
" q, ESC - 关闭帮助\n"
|
||||
" j/k - 向下/上滚动\n"
|
||||
" j/k, arrows - 向下/上滚动\n"
|
||||
" Ctrl+D/U - 向下/上滚动半页\n"
|
||||
" Ctrl+F/B - 向下/上滚动整页\n"
|
||||
" Space/b - 向下/上滚动整页\n"
|
||||
" PgDn/PgUp - 向下/上滚动整页\n"
|
||||
" End/Home - 跳到底部/顶部\n"
|
||||
" g/G - 跳到顶部/底部\n"
|
||||
" l - 切换界面语言\n"
|
||||
);
|
||||
|
|
|
|||
225
src/input.c
225
src/input.c
|
|
@ -32,10 +32,10 @@ static int read_username(client_t *client) {
|
|||
char username[MAX_USERNAME_LEN] = {0};
|
||||
int pos = 0;
|
||||
char buf[4];
|
||||
const char *prompt = i18n_text(client->ui_lang, I18N_USERNAME_PROMPT);
|
||||
|
||||
tui_render_welcome(client);
|
||||
client_printf(client, "%s", i18n_text(client->ui_lang,
|
||||
I18N_USERNAME_PROMPT));
|
||||
client_printf(client, "%s", prompt);
|
||||
|
||||
while (1) {
|
||||
int n = ssh_channel_read_timeout(client->channel, buf, 1, 0, 60000); /* 60 sec timeout */
|
||||
|
|
@ -54,6 +54,18 @@ static int read_username(client_t *client) {
|
|||
|
||||
if (b == '\r' || b == '\n') {
|
||||
break;
|
||||
} else if (b == 3 || b == 4) { /* Ctrl+C / Ctrl+D */
|
||||
return -1;
|
||||
} else if (b == 21) { /* Ctrl+U: clear line */
|
||||
username[0] = '\0';
|
||||
pos = 0;
|
||||
client_printf(client, "\r\033[K%s", prompt);
|
||||
} else if (b == 23) { /* Ctrl+W: delete word */
|
||||
if (username[0] != '\0') {
|
||||
utf8_remove_last_word(username);
|
||||
pos = (int)strlen(username);
|
||||
client_printf(client, "\r\033[K%s%s", prompt, username);
|
||||
}
|
||||
} else if (b == 127 || b == 8) { /* Backspace */
|
||||
if (pos > 0) {
|
||||
/* Compute width of the last character before removing it */
|
||||
|
|
@ -230,6 +242,110 @@ static void dismiss_command_output(client_t *client) {
|
|||
tui_render_screen(client);
|
||||
}
|
||||
|
||||
typedef enum {
|
||||
PAGER_ACTION_NONE,
|
||||
PAGER_ACTION_SCROLL,
|
||||
PAGER_ACTION_CLOSE,
|
||||
PAGER_ACTION_REFRESH
|
||||
} pager_action_t;
|
||||
|
||||
static int pager_page_height(client_t *client) {
|
||||
int page = client->height - 2;
|
||||
if (page < 1) page = 1;
|
||||
return page;
|
||||
}
|
||||
|
||||
static void pager_scroll_by(int *scroll_pos, int delta) {
|
||||
*scroll_pos += delta;
|
||||
if (*scroll_pos < 0) {
|
||||
*scroll_pos = 0;
|
||||
}
|
||||
}
|
||||
|
||||
static pager_action_t pager_apply_key(client_t *client, unsigned char key,
|
||||
int *scroll_pos, bool allow_refresh) {
|
||||
int page = pager_page_height(client);
|
||||
int half = page / 2;
|
||||
if (half < 1) half = 1;
|
||||
|
||||
if (key == 'q') {
|
||||
return PAGER_ACTION_CLOSE;
|
||||
} else if (key == 'j') {
|
||||
pager_scroll_by(scroll_pos, 1);
|
||||
return PAGER_ACTION_SCROLL;
|
||||
} else if (key == 'k') {
|
||||
pager_scroll_by(scroll_pos, -1);
|
||||
return PAGER_ACTION_SCROLL;
|
||||
} else if (key == 4) { /* Ctrl+D: half page down */
|
||||
pager_scroll_by(scroll_pos, half);
|
||||
return PAGER_ACTION_SCROLL;
|
||||
} else if (key == 21) { /* Ctrl+U: half page up */
|
||||
pager_scroll_by(scroll_pos, -half);
|
||||
return PAGER_ACTION_SCROLL;
|
||||
} else if (key == 6 || key == ' ') { /* Ctrl+F / Space: page down */
|
||||
pager_scroll_by(scroll_pos, page);
|
||||
return PAGER_ACTION_SCROLL;
|
||||
} else if (key == 2 || key == 'b') { /* Ctrl+B / b: page up */
|
||||
pager_scroll_by(scroll_pos, -page);
|
||||
return PAGER_ACTION_SCROLL;
|
||||
} else if (key == 'g') {
|
||||
*scroll_pos = 0;
|
||||
return PAGER_ACTION_SCROLL;
|
||||
} else if (key == 'G') {
|
||||
*scroll_pos = 999;
|
||||
return PAGER_ACTION_SCROLL;
|
||||
} else if ((key == 'r' || key == 'R') && allow_refresh) {
|
||||
return PAGER_ACTION_REFRESH;
|
||||
} else if (key == 27) {
|
||||
char seq[3];
|
||||
int n = ssh_channel_read_timeout(client->channel, seq, 1, 0, 50);
|
||||
if (n != 1) {
|
||||
return PAGER_ACTION_CLOSE;
|
||||
}
|
||||
if (seq[0] != '[') {
|
||||
return PAGER_ACTION_NONE;
|
||||
}
|
||||
|
||||
n = ssh_channel_read_timeout(client->channel, &seq[1], 1, 0, 50);
|
||||
if (n != 1) {
|
||||
return PAGER_ACTION_NONE;
|
||||
}
|
||||
|
||||
if (seq[1] == 'A') { /* Up arrow */
|
||||
pager_scroll_by(scroll_pos, -1);
|
||||
return PAGER_ACTION_SCROLL;
|
||||
} else if (seq[1] == 'B') { /* Down arrow */
|
||||
pager_scroll_by(scroll_pos, 1);
|
||||
return PAGER_ACTION_SCROLL;
|
||||
} else if (seq[1] == 'H') { /* Home */
|
||||
*scroll_pos = 0;
|
||||
return PAGER_ACTION_SCROLL;
|
||||
} else if (seq[1] == 'F') { /* End */
|
||||
*scroll_pos = 999;
|
||||
return PAGER_ACTION_SCROLL;
|
||||
} else if (seq[1] >= '1' && seq[1] <= '6') {
|
||||
n = ssh_channel_read_timeout(client->channel, &seq[2], 1, 0, 50);
|
||||
if (n == 1 && seq[2] == '~') {
|
||||
if (seq[1] == '5') { /* PageUp */
|
||||
pager_scroll_by(scroll_pos, -page);
|
||||
return PAGER_ACTION_SCROLL;
|
||||
} else if (seq[1] == '6') { /* PageDown */
|
||||
pager_scroll_by(scroll_pos, page);
|
||||
return PAGER_ACTION_SCROLL;
|
||||
} else if (seq[1] == '1') { /* Home */
|
||||
*scroll_pos = 0;
|
||||
return PAGER_ACTION_SCROLL;
|
||||
} else if (seq[1] == '4') { /* End */
|
||||
*scroll_pos = 999;
|
||||
return PAGER_ACTION_SCROLL;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return PAGER_ACTION_NONE;
|
||||
}
|
||||
|
||||
/* Handle a single key press. Returns true if the key was fully consumed
|
||||
* (no further character buffering needed). */
|
||||
static bool handle_key(client_t *client, unsigned char key, char *input) {
|
||||
|
|
@ -257,44 +373,20 @@ static bool handle_key(client_t *client, unsigned char key, char *input) {
|
|||
|
||||
/* Handle help screen */
|
||||
if (client->show_help) {
|
||||
/* Page size: roughly the visible help body region. */
|
||||
int page = client->height - 2;
|
||||
if (page < 1) page = 1;
|
||||
int half = page / 2;
|
||||
if (half < 1) half = 1;
|
||||
pager_action_t action;
|
||||
|
||||
if (key == 'q' || key == 27) {
|
||||
client->show_help = false;
|
||||
tui_render_screen(client);
|
||||
} else if (key == 'l' || key == 'L') {
|
||||
if (key == 'l' || key == 'L') {
|
||||
client->ui_lang = i18n_next_ui_lang(client->ui_lang);
|
||||
client->help_scroll_pos = 0;
|
||||
tui_render_help(client);
|
||||
} else if (key == 'j') {
|
||||
client->help_scroll_pos++;
|
||||
tui_render_help(client);
|
||||
} else if (key == 'k' && client->help_scroll_pos > 0) {
|
||||
client->help_scroll_pos--;
|
||||
tui_render_help(client);
|
||||
} else if (key == 4) { /* Ctrl+D: half page down */
|
||||
client->help_scroll_pos += half;
|
||||
tui_render_help(client);
|
||||
} else if (key == 21) { /* Ctrl+U: half page up */
|
||||
client->help_scroll_pos -= half;
|
||||
if (client->help_scroll_pos < 0) client->help_scroll_pos = 0;
|
||||
tui_render_help(client);
|
||||
} else if (key == 6) { /* Ctrl+F: full page down */
|
||||
client->help_scroll_pos += page;
|
||||
tui_render_help(client);
|
||||
} else if (key == 2) { /* Ctrl+B: full page up */
|
||||
client->help_scroll_pos -= page;
|
||||
if (client->help_scroll_pos < 0) client->help_scroll_pos = 0;
|
||||
tui_render_help(client);
|
||||
} else if (key == 'g') {
|
||||
client->help_scroll_pos = 0;
|
||||
tui_render_help(client);
|
||||
} else if (key == 'G') {
|
||||
client->help_scroll_pos = 999; /* Large number */
|
||||
return true;
|
||||
}
|
||||
|
||||
action = pager_apply_key(client, key, &client->help_scroll_pos, false);
|
||||
if (action == PAGER_ACTION_CLOSE) {
|
||||
client->show_help = false;
|
||||
tui_render_screen(client);
|
||||
} else if (action == PAGER_ACTION_SCROLL) {
|
||||
tui_render_help(client);
|
||||
}
|
||||
return true; /* Key consumed */
|
||||
|
|
@ -303,56 +395,23 @@ static bool handle_key(client_t *client, unsigned char key, char *input) {
|
|||
/* Handle command output / MOTD display. MOTD remains a simple notice;
|
||||
* command output behaves like a small pager so long results can be read. */
|
||||
if (client->command_output[0] != '\0') {
|
||||
int page = client->height - 2;
|
||||
int half;
|
||||
pager_action_t action;
|
||||
|
||||
if (client->show_motd) {
|
||||
dismiss_command_output(client);
|
||||
return true;
|
||||
}
|
||||
|
||||
if (page < 1) page = 1;
|
||||
half = page / 2;
|
||||
if (half < 1) half = 1;
|
||||
|
||||
if (key == 'q' || key == 27) {
|
||||
action = pager_apply_key(client, key, &client->command_output_scroll,
|
||||
true);
|
||||
if (action == PAGER_ACTION_CLOSE) {
|
||||
dismiss_command_output(client);
|
||||
} else if (key == 'j') {
|
||||
client->command_output_scroll++;
|
||||
} else if (action == PAGER_ACTION_SCROLL) {
|
||||
tui_render_command_output(client);
|
||||
} else if (key == 'k') {
|
||||
client->command_output_scroll--;
|
||||
if (client->command_output_scroll < 0) {
|
||||
client->command_output_scroll = 0;
|
||||
} else if (action == PAGER_ACTION_REFRESH) {
|
||||
if (commands_refresh_active_output(client)) {
|
||||
tui_render_command_output(client);
|
||||
}
|
||||
tui_render_command_output(client);
|
||||
} else if (key == 4) { /* Ctrl+D: half page down */
|
||||
client->command_output_scroll += half;
|
||||
tui_render_command_output(client);
|
||||
} else if (key == 21) { /* Ctrl+U: half page up */
|
||||
client->command_output_scroll -= half;
|
||||
if (client->command_output_scroll < 0) {
|
||||
client->command_output_scroll = 0;
|
||||
}
|
||||
tui_render_command_output(client);
|
||||
} else if (key == 6) { /* Ctrl+F: full page down */
|
||||
client->command_output_scroll += page;
|
||||
tui_render_command_output(client);
|
||||
} else if (key == 2) { /* Ctrl+B: full page up */
|
||||
client->command_output_scroll -= page;
|
||||
if (client->command_output_scroll < 0) {
|
||||
client->command_output_scroll = 0;
|
||||
}
|
||||
tui_render_command_output(client);
|
||||
} else if (key == 'g') {
|
||||
client->command_output_scroll = 0;
|
||||
tui_render_command_output(client);
|
||||
} else if (key == 'G') {
|
||||
client->command_output_scroll = 999;
|
||||
tui_render_command_output(client);
|
||||
} else if ((key == 'r' || key == 'R') &&
|
||||
commands_refresh_active_output(client)) {
|
||||
tui_render_command_output(client);
|
||||
}
|
||||
return true; /* Key consumed */
|
||||
}
|
||||
|
|
@ -571,6 +630,12 @@ static bool handle_key(client_t *client, unsigned char key, char *input) {
|
|||
client->command_input[0] = '\0';
|
||||
tui_render_screen(client);
|
||||
return true;
|
||||
} else if (key == '/') {
|
||||
client->mode = MODE_COMMAND;
|
||||
snprintf(client->command_input, sizeof(client->command_input),
|
||||
"search ");
|
||||
tui_render_screen(client);
|
||||
return true;
|
||||
} else if (key == 'j') {
|
||||
normal_scroll_by(client, 1);
|
||||
tui_render_screen(client);
|
||||
|
|
@ -953,6 +1018,8 @@ main_loop:
|
|||
client->command_input[len] = b;
|
||||
client->command_input[len + 1] = '\0';
|
||||
tui_render_screen(client);
|
||||
} else {
|
||||
client_send(client, "\a", 1);
|
||||
}
|
||||
} else if (b >= 128) { /* UTF-8 multi-byte */
|
||||
int char_len = utf8_byte_length(b);
|
||||
|
|
@ -965,10 +1032,12 @@ main_loop:
|
|||
}
|
||||
if (!utf8_is_valid_sequence(buf, char_len)) continue;
|
||||
size_t len = strlen(client->command_input);
|
||||
if (len + (size_t)char_len < sizeof(client->command_input) - 1) {
|
||||
if (len + (size_t)char_len <= sizeof(client->command_input) - 1) {
|
||||
memcpy(client->command_input + len, buf, char_len);
|
||||
client->command_input[len + char_len] = '\0';
|
||||
tui_render_screen(client);
|
||||
} else {
|
||||
client_send(client, "\a", 1);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -12,8 +12,8 @@ void manual_text_append_interactive(char *buffer, size_t buf_size,
|
|||
" 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"
|
||||
" Type, Enter sends; Up/Down recalls; Tab completes @mentions\n"
|
||||
" Esc browses; / searches; G latest; i types; : commands; ? keys\n"
|
||||
"\n"
|
||||
"\033[1;37mCommands\033[0m\n",
|
||||
"\033[1;36mTNT(1) 帮助\033[0m\n"
|
||||
|
|
@ -22,8 +22,8 @@ void manual_text_append_interactive(char *buffer, size_t buf_size,
|
|||
" TNT - SSH 终端聊天室\n"
|
||||
"\n"
|
||||
"\033[1;37m使用\033[0m\n"
|
||||
" 输入消息并 Enter 发送;Esc 浏览历史;G 最新;i 输入\n"
|
||||
" : 运行命令;? 打开完整按键参考\n"
|
||||
" 输入并 Enter 发送;Up/Down 调出消息;Tab 补全 @mention\n"
|
||||
" Esc 浏览;/ 搜索;G 最新;i 输入;: 命令;? 按键\n"
|
||||
"\n"
|
||||
"\033[1;37m命令\033[0m\n"
|
||||
);
|
||||
|
|
|
|||
|
|
@ -1,6 +1,54 @@
|
|||
#include "tui_status.h"
|
||||
#include "i18n.h"
|
||||
#include "ssh_server.h"
|
||||
#include "utf8.h"
|
||||
|
||||
static void format_command_input_tail(const char *input, int avail_width,
|
||||
char *display, size_t display_size) {
|
||||
if (!input || !display || display_size == 0) return;
|
||||
|
||||
display[0] = '\0';
|
||||
if (avail_width < 1) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (utf8_string_width(input) <= avail_width) {
|
||||
strncpy(display, input, display_size - 1);
|
||||
display[display_size - 1] = '\0';
|
||||
return;
|
||||
}
|
||||
|
||||
const char *marker = "<";
|
||||
int marker_width = 1;
|
||||
int tail_width = avail_width - marker_width;
|
||||
if (tail_width < 1) {
|
||||
snprintf(display, display_size, "%s", marker);
|
||||
return;
|
||||
}
|
||||
|
||||
const char *p = input + strlen(input);
|
||||
const char *tail = p;
|
||||
int width = 0;
|
||||
|
||||
while (p > input && width < tail_width) {
|
||||
const char *q = p - 1;
|
||||
while (q > input && ((*q & 0xC0) == 0x80)) {
|
||||
q--;
|
||||
}
|
||||
|
||||
int bytes_read = 0;
|
||||
uint32_t cp = utf8_decode(q, &bytes_read);
|
||||
int char_width = utf8_char_width(cp);
|
||||
if (width + char_width > tail_width) {
|
||||
break;
|
||||
}
|
||||
width += char_width;
|
||||
tail = q;
|
||||
p = q;
|
||||
}
|
||||
|
||||
snprintf(display, display_size, "%s%s", marker, tail);
|
||||
}
|
||||
|
||||
void tui_status_append(char *buffer, size_t buf_size, size_t *pos,
|
||||
const struct client *client, int msg_count,
|
||||
|
|
@ -48,7 +96,12 @@ void tui_status_append(char *buffer, size_t buf_size, size_t *pos,
|
|||
i18n_text(client->ui_lang, I18N_NORMAL_LATEST));
|
||||
}
|
||||
} else if (client->mode == MODE_COMMAND) {
|
||||
char display[sizeof(client->command_input) + 2];
|
||||
int avail = client->width - 1;
|
||||
if (avail < 1) avail = 1;
|
||||
format_command_input_tail(client->command_input, avail, display,
|
||||
sizeof(display));
|
||||
buffer_appendf(buffer, buf_size, pos,
|
||||
"\033[35m:\033[0m%s\033[K", client->command_input);
|
||||
"\033[35m:\033[0m%s\033[K", display);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -58,6 +58,51 @@ else
|
|||
exit 1
|
||||
fi
|
||||
|
||||
USERNAME_CANCEL_SCRIPT="$STATE_DIR/username-cancel.expect"
|
||||
cat >"$USERNAME_CANCEL_SCRIPT" <<EOF
|
||||
set timeout 10
|
||||
spawn ssh $SSH_OPTS anonymous@127.0.0.1
|
||||
sleep 1
|
||||
send -- "\003"
|
||||
expect eof
|
||||
EOF
|
||||
|
||||
if expect "$USERNAME_CANCEL_SCRIPT" >"$STATE_DIR/username-cancel.log" 2>&1; then
|
||||
echo "✓ Ctrl+C cancels before username join"
|
||||
PASS=$((PASS + 1))
|
||||
else
|
||||
echo "x Ctrl+C before username failed"
|
||||
sed -n '1,120p' "$STATE_DIR/username-cancel.log"
|
||||
sed -n '1,120p' "$STATE_DIR/server.log"
|
||||
FAIL=$((FAIL + 1))
|
||||
fi
|
||||
|
||||
USERNAME_EDIT_SCRIPT="$STATE_DIR/username-edit.expect"
|
||||
cat >"$USERNAME_EDIT_SCRIPT" <<EOF
|
||||
set timeout 10
|
||||
spawn ssh $SSH_OPTS anonymous@127.0.0.1
|
||||
sleep 1
|
||||
send -- "wrong\025editeduser\r"
|
||||
expect ":help"
|
||||
send -- "\003"
|
||||
sleep 0.2
|
||||
send -- "\003"
|
||||
expect eof
|
||||
EOF
|
||||
|
||||
if expect "$USERNAME_EDIT_SCRIPT" >"$STATE_DIR/username-edit.log" 2>&1 &&
|
||||
grep -q 'editeduser' "$STATE_DIR/messages.log" &&
|
||||
! grep -q 'wrongediteduser' "$STATE_DIR/messages.log"; then
|
||||
echo "✓ Ctrl+U edits username before join"
|
||||
PASS=$((PASS + 1))
|
||||
else
|
||||
echo "x username line editing failed"
|
||||
sed -n '1,120p' "$STATE_DIR/username-edit.log" 2>/dev/null || true
|
||||
cat "$STATE_DIR/messages.log" 2>/dev/null || true
|
||||
sed -n '1,120p' "$STATE_DIR/server.log"
|
||||
FAIL=$((FAIL + 1))
|
||||
fi
|
||||
|
||||
EXPECT_SCRIPT="$STATE_DIR/bracketed-paste.expect"
|
||||
cat >"$EXPECT_SCRIPT" <<EOF
|
||||
set timeout 10
|
||||
|
|
@ -146,14 +191,17 @@ send -- ":"
|
|||
expect ":"
|
||||
send -- "help\r"
|
||||
expect "TNT\\(1\\) 帮助"
|
||||
expect "Tab 补全 @mention"
|
||||
expect "q:关闭"
|
||||
send -- "q"
|
||||
expect "NORMAL"
|
||||
send -- "?"
|
||||
expect "TNT 按键参考"
|
||||
expect "Tab - 补全 @mention"
|
||||
expect "l:语言"
|
||||
send -- "l"
|
||||
expect "TNT KEY REFERENCE"
|
||||
expect "Complete @mention"
|
||||
expect "l:lang"
|
||||
send -- "q"
|
||||
expect "NORMAL"
|
||||
|
|
@ -180,6 +228,45 @@ else
|
|||
FAIL=$((FAIL + 1))
|
||||
fi
|
||||
|
||||
HELP_PAGER_KEYS_SCRIPT="$STATE_DIR/help-pager-keys.expect"
|
||||
cat >"$HELP_PAGER_KEYS_SCRIPT" <<EOF
|
||||
set timeout 10
|
||||
stty rows 8 columns 80
|
||||
spawn ssh $SSH_OPTS anonymous@127.0.0.1
|
||||
sleep 1
|
||||
send -- "helppager\r"
|
||||
expect ":help"
|
||||
send -- "\033"
|
||||
expect "NORMAL"
|
||||
send -- "?"
|
||||
expect -re {\(1/[2-9][0-9]*\)}
|
||||
send -- "\033\[6~"
|
||||
expect -re {\([2-9][0-9]*/[2-9][0-9]*\)}
|
||||
send -- "\033\[5~"
|
||||
expect -re {\(1/[2-9][0-9]*\)}
|
||||
send -- "\033\[F"
|
||||
expect -re {\([2-9][0-9]*/[2-9][0-9]*\)}
|
||||
send -- "\033\[H"
|
||||
expect -re {\(1/[2-9][0-9]*\)}
|
||||
send -- "q"
|
||||
expect "NORMAL"
|
||||
sleep 0.2
|
||||
send -- "\003"
|
||||
sleep 0.2
|
||||
send -- "\003"
|
||||
expect eof
|
||||
EOF
|
||||
|
||||
if expect "$HELP_PAGER_KEYS_SCRIPT" >"$STATE_DIR/help-pager-keys.log" 2>&1; then
|
||||
echo "✓ help pager accepts terminal paging keys"
|
||||
PASS=$((PASS + 1))
|
||||
else
|
||||
echo "x help pager terminal keys failed"
|
||||
sed -n '1,220p' "$STATE_DIR/help-pager-keys.log"
|
||||
sed -n '1,120p' "$STATE_DIR/server.log"
|
||||
FAIL=$((FAIL + 1))
|
||||
fi
|
||||
|
||||
UNKNOWN_SCRIPT="$STATE_DIR/unknown-command.expect"
|
||||
cat >"$UNKNOWN_SCRIPT" <<EOF
|
||||
set timeout 10
|
||||
|
|
@ -371,6 +458,14 @@ expect "j/k:滚动"
|
|||
expect -re {\(1/[2-9][0-9]*\)}
|
||||
send -- "j"
|
||||
expect -re {\(2/[2-9][0-9]*\)}
|
||||
send -- "\033\[6~"
|
||||
expect -re {\([3-9][0-9]*/[2-9][0-9]*\)}
|
||||
send -- "\033\[5~"
|
||||
expect -re {\([1-9][0-9]*/[2-9][0-9]*\)}
|
||||
send -- "\033\[F"
|
||||
expect -re {\([2-9][0-9]*/[2-9][0-9]*\)}
|
||||
send -- "\033\[H"
|
||||
expect -re {\(1/[2-9][0-9]*\)}
|
||||
send -- "q"
|
||||
expect "NORMAL"
|
||||
sleep 0.2
|
||||
|
|
@ -390,6 +485,37 @@ else
|
|||
FAIL=$((FAIL + 1))
|
||||
fi
|
||||
|
||||
COMMAND_INPUT_WRAP_SCRIPT="$STATE_DIR/command-input-wrap.expect"
|
||||
cat >"$COMMAND_INPUT_WRAP_SCRIPT" <<EOF
|
||||
set timeout 10
|
||||
stty rows 10 columns 40
|
||||
spawn ssh $SSH_OPTS anonymous@127.0.0.1
|
||||
sleep 1
|
||||
send -- "wrapcmd\r"
|
||||
expect ":help"
|
||||
send -- "\033"
|
||||
expect "NORMAL"
|
||||
send -- ":"
|
||||
expect ":"
|
||||
send -- "search aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaatail"
|
||||
expect -re {<a+tail}
|
||||
send -- "\003"
|
||||
expect "NORMAL"
|
||||
sleep 0.2
|
||||
send -- "\003"
|
||||
expect eof
|
||||
EOF
|
||||
|
||||
if expect "$COMMAND_INPUT_WRAP_SCRIPT" >"$STATE_DIR/command-input-wrap.log" 2>&1; then
|
||||
echo "✓ long command input stays on one status line"
|
||||
PASS=$((PASS + 1))
|
||||
else
|
||||
echo "x long command input display failed"
|
||||
sed -n '1,220p' "$STATE_DIR/command-input-wrap.log"
|
||||
sed -n '1,120p' "$STATE_DIR/server.log"
|
||||
FAIL=$((FAIL + 1))
|
||||
fi
|
||||
|
||||
SYSTEM_MESSAGES_SCRIPT="$STATE_DIR/system-messages.expect"
|
||||
cat >"$SYSTEM_MESSAGES_SCRIPT" <<EOF
|
||||
set timeout 10
|
||||
|
|
|
|||
|
|
@ -185,6 +185,12 @@ expect "alpha"
|
|||
expect "q:关闭"
|
||||
send -- "q"
|
||||
expect "NORMAL"
|
||||
send -- "/alpha\r"
|
||||
expect "搜索"
|
||||
expect "alpha"
|
||||
expect "q:关闭"
|
||||
send -- "q"
|
||||
expect "NORMAL"
|
||||
send -- ":"
|
||||
expect ":"
|
||||
send -- "mute-joins\r"
|
||||
|
|
|
|||
Loading…
Reference in a new issue