Improve TUI pager and search ergonomics

This commit is contained in:
m1ngsama 2026-05-27 19:24:55 +08:00
parent 1c451b7722
commit 797ecbb992
9 changed files with 380 additions and 89 deletions

View file

@ -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

View file

@ -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 断开连接

View file

@ -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;

View file

@ -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"
);

View file

@ -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);
}
}
}

View file

@ -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"
);

View file

@ -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);
}
}

View file

@ -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

View file

@ -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"