mirror of
https://oauth2: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@github.com/m1ngsama/TNT.git
synced 2026-06-26 06:54:38 +08:00
Polish live inbox command output
This commit is contained in:
parent
f3e2762f30
commit
e603a55cb3
16 changed files with 129 additions and 38 deletions
|
|
@ -105,6 +105,10 @@ Up/Down - Browse command history
|
||||||
ESC - Return to NORMAL mode
|
ESC - Return to NORMAL mode
|
||||||
```
|
```
|
||||||
|
|
||||||
|
Command output pages use `j/k`, `Ctrl+D/U`, and `g/G` for paging. `:inbox`
|
||||||
|
is live: press `r` to refresh it manually, and it refreshes when a new private
|
||||||
|
message arrives while the inbox is open.
|
||||||
|
|
||||||
**Special messages (INSERT mode)**
|
**Special messages (INSERT mode)**
|
||||||
```
|
```
|
||||||
/me <action> - Send action (e.g. /me waves)
|
/me <action> - Send action (e.g. /me waves)
|
||||||
|
|
|
||||||
|
|
@ -17,6 +17,8 @@
|
||||||
the main onboarding, chat, help, history, search, private-message, nickname,
|
the main onboarding, chat, help, history, search, private-message, nickname,
|
||||||
action-message, and exit paths.
|
action-message, and exit paths.
|
||||||
- Added a VHS tape draft for recording the core TNT terminal-chat experience.
|
- 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.
|
||||||
|
|
||||||
### Changed
|
### Changed
|
||||||
- `make install-systemd` now rewrites the installed unit's `ExecStart` to match
|
- `make install-systemd` now rewrites the installed unit's `ExecStart` to match
|
||||||
|
|
@ -40,6 +42,8 @@
|
||||||
- Interactive client writes now pass through a bounded per-client outbox and
|
- Interactive client writes now pass through a bounded per-client outbox and
|
||||||
flush against the remote SSH window from that client's session loop. Exec
|
flush against the remote SSH window from that client's session loop. Exec
|
||||||
sessions still write synchronously to preserve script output ordering.
|
sessions still write synchronously to preserve script output ordering.
|
||||||
|
- The two-user lifecycle test now covers opening `:inbox` before a private
|
||||||
|
message arrives, matching the way users often leave an inbox page open.
|
||||||
- Private-message inbox access now uses its own mutex instead of sharing the
|
- Private-message inbox access now uses its own mutex instead of sharing the
|
||||||
SSH channel write lock, reducing unrelated contention on slow clients.
|
SSH channel write lock, reducing unrelated contention on slow clients.
|
||||||
- Client writes now check the SSH channel's remote window before writing and
|
- Client writes now check the SSH channel's remote window before writing and
|
||||||
|
|
|
||||||
|
|
@ -29,6 +29,9 @@ The product path should stay short:
|
||||||
they do not change the command language.
|
they do not change the command language.
|
||||||
- Private messages are visible only in the recipient inbox and are not written
|
- Private messages are visible only in the recipient inbox and are not written
|
||||||
to `messages.log`.
|
to `messages.log`.
|
||||||
|
- `:inbox` is live enough for normal chat use: it can be refreshed with `r`
|
||||||
|
and refreshes automatically when a new private message arrives while the
|
||||||
|
inbox is open.
|
||||||
- Long command output uses a small pager so `:last` and `:search` are readable
|
- Long command output uses a small pager so `:last` and `:search` are readable
|
||||||
on small terminals.
|
on small terminals.
|
||||||
|
|
||||||
|
|
@ -41,7 +44,8 @@ The product path should stay short:
|
||||||
`:last` and `:search`
|
`:last` and `:search`
|
||||||
- first user toggles `:mute-joins`, sends `:msg`, changes nickname, sends
|
- first user toggles `:mute-joins`, sends `:msg`, changes nickname, sends
|
||||||
`/me`, and exits
|
`/me`, and exits
|
||||||
- second user reads `:inbox`
|
- second user opens `:inbox` before the private message arrives and sees it
|
||||||
|
auto-refresh after delivery
|
||||||
- exec `tail` sees public messages
|
- exec `tail` sees public messages
|
||||||
- `messages.log` contains public history and excludes private-message content
|
- `messages.log` contains public history and excludes private-message content
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -19,4 +19,9 @@
|
||||||
* path; callers must not hold client->io_lock before dispatching. */
|
* path; callers must not hold client->io_lock before dispatching. */
|
||||||
void commands_dispatch(client_t *client);
|
void commands_dispatch(client_t *client);
|
||||||
|
|
||||||
|
/* Rebuild the currently visible command output when it is backed by live
|
||||||
|
* client state, such as :inbox. Returns true if output changed and the caller
|
||||||
|
* should render it again. */
|
||||||
|
bool commands_refresh_active_output(client_t *client);
|
||||||
|
|
||||||
#endif /* COMMANDS_H */
|
#endif /* COMMANDS_H */
|
||||||
|
|
|
||||||
|
|
@ -25,6 +25,7 @@ typedef enum {
|
||||||
I18N_HELP_STATUS_FORMAT,
|
I18N_HELP_STATUS_FORMAT,
|
||||||
I18N_COMMAND_OUTPUT_TITLE,
|
I18N_COMMAND_OUTPUT_TITLE,
|
||||||
I18N_COMMAND_OUTPUT_STATUS_FORMAT,
|
I18N_COMMAND_OUTPUT_STATUS_FORMAT,
|
||||||
|
I18N_COMMAND_OUTPUT_REFRESH_STATUS_FORMAT,
|
||||||
I18N_MOTD_TITLE,
|
I18N_MOTD_TITLE,
|
||||||
I18N_MOTD_CONTINUE_HINT,
|
I18N_MOTD_CONTINUE_HINT,
|
||||||
I18N_TITLE_ONLINE_FORMAT,
|
I18N_TITLE_ONLINE_FORMAT,
|
||||||
|
|
|
||||||
|
|
@ -17,6 +17,12 @@ typedef struct {
|
||||||
char content[MAX_MESSAGE_LEN];
|
char content[MAX_MESSAGE_LEN];
|
||||||
} whisper_t;
|
} whisper_t;
|
||||||
|
|
||||||
|
typedef enum {
|
||||||
|
TNT_COMMAND_OUTPUT_NONE,
|
||||||
|
TNT_COMMAND_OUTPUT_GENERIC,
|
||||||
|
TNT_COMMAND_OUTPUT_INBOX
|
||||||
|
} tnt_command_output_kind_t;
|
||||||
|
|
||||||
/* Client connection structure */
|
/* Client connection structure */
|
||||||
typedef struct client {
|
typedef struct client {
|
||||||
ssh_session session; /* SSH session */
|
ssh_session session; /* SSH session */
|
||||||
|
|
@ -42,6 +48,7 @@ typedef struct client {
|
||||||
int insert_history_pos;
|
int insert_history_pos;
|
||||||
char command_output[MAX_COMMAND_OUTPUT_LEN];
|
char command_output[MAX_COMMAND_OUTPUT_LEN];
|
||||||
int command_output_scroll;
|
int command_output_scroll;
|
||||||
|
tnt_command_output_kind_t command_output_kind;
|
||||||
bool show_motd; /* command_output holds MOTD text */
|
bool show_motd; /* command_output holds MOTD text */
|
||||||
char exec_command[MAX_EXEC_COMMAND_LEN];
|
char exec_command[MAX_EXEC_COMMAND_LEN];
|
||||||
bool exec_command_too_long;
|
bool exec_command_too_long;
|
||||||
|
|
|
||||||
|
|
@ -52,12 +52,60 @@ static void append_command_usage(char *output, size_t buf_size, size_t *pos,
|
||||||
command_catalog_append_usage(output, buf_size, pos, id, lang);
|
command_catalog_append_usage(output, buf_size, pos, id, lang);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
static void append_inbox_output(client_t *client, char *output,
|
||||||
|
size_t buf_size, size_t *pos) {
|
||||||
|
whisper_t snapshot[WHISPER_INBOX_SIZE];
|
||||||
|
int snap_count;
|
||||||
|
|
||||||
|
pthread_mutex_lock(&client->whisper_lock);
|
||||||
|
snap_count = client->whisper_inbox_count;
|
||||||
|
memcpy(snapshot, client->whisper_inbox,
|
||||||
|
snap_count * sizeof(whisper_t));
|
||||||
|
client->unread_whispers = 0;
|
||||||
|
pthread_mutex_unlock(&client->whisper_lock);
|
||||||
|
|
||||||
|
buffer_appendf(output, buf_size, pos,
|
||||||
|
"\033[1;36m%s\033[0m \033[2;37m· %d\033[0m\n",
|
||||||
|
i18n_text(client->ui_lang, I18N_INBOX_TITLE),
|
||||||
|
snap_count);
|
||||||
|
if (snap_count == 0) {
|
||||||
|
buffer_appendf(output, buf_size, pos,
|
||||||
|
" \033[2;37m%s\033[0m\n",
|
||||||
|
i18n_text(client->ui_lang, I18N_INBOX_EMPTY));
|
||||||
|
}
|
||||||
|
for (int i = 0; i < snap_count; i++) {
|
||||||
|
char ts[20];
|
||||||
|
struct tm tmi;
|
||||||
|
localtime_r(&snapshot[i].timestamp, &tmi);
|
||||||
|
strftime(ts, sizeof(ts), "%m-%d %H:%M", &tmi);
|
||||||
|
buffer_appendf(output, buf_size, pos,
|
||||||
|
" \033[90m%s\033[0m \033[35m%s\033[0m: %s\n",
|
||||||
|
ts, snapshot[i].from, snapshot[i].content);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
bool commands_refresh_active_output(client_t *client) {
|
||||||
|
char output[MAX_COMMAND_OUTPUT_LEN] = {0};
|
||||||
|
size_t pos = 0;
|
||||||
|
|
||||||
|
if (!client || client->command_output_kind != TNT_COMMAND_OUTPUT_INBOX) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
append_inbox_output(client, output, sizeof(output), &pos);
|
||||||
|
snprintf(client->command_output, sizeof(client->command_output), "%s",
|
||||||
|
output);
|
||||||
|
client->command_output_scroll = 0;
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
void commands_dispatch(client_t *client) {
|
void commands_dispatch(client_t *client) {
|
||||||
char cmd_buf[256];
|
char cmd_buf[256];
|
||||||
strncpy(cmd_buf, client->command_input, sizeof(cmd_buf) - 1);
|
strncpy(cmd_buf, client->command_input, sizeof(cmd_buf) - 1);
|
||||||
cmd_buf[sizeof(cmd_buf) - 1] = '\0';
|
cmd_buf[sizeof(cmd_buf) - 1] = '\0';
|
||||||
char *cmd = cmd_buf;
|
char *cmd = cmd_buf;
|
||||||
char output[MAX_COMMAND_OUTPUT_LEN] = {0};
|
char output[MAX_COMMAND_OUTPUT_LEN] = {0};
|
||||||
|
tnt_command_output_kind_t output_kind = TNT_COMMAND_OUTPUT_GENERIC;
|
||||||
size_t pos = 0;
|
size_t pos = 0;
|
||||||
|
|
||||||
/* Trim whitespace */
|
/* Trim whitespace */
|
||||||
|
|
@ -219,9 +267,9 @@ void commands_dispatch(client_t *client) {
|
||||||
snprintf(target->whisper_inbox[slot].content,
|
snprintf(target->whisper_inbox[slot].content,
|
||||||
sizeof(target->whisper_inbox[slot].content),
|
sizeof(target->whisper_inbox[slot].content),
|
||||||
"%s", rest);
|
"%s", rest);
|
||||||
|
target->unread_whispers++;
|
||||||
pthread_mutex_unlock(&target->whisper_lock);
|
pthread_mutex_unlock(&target->whisper_lock);
|
||||||
|
|
||||||
target->unread_whispers++;
|
|
||||||
/* Audible nudge — the title bar ✉ counter (UX-11 style)
|
/* Audible nudge — the title bar ✉ counter (UX-11 style)
|
||||||
* carries the persistent signal. */
|
* carries the persistent signal. */
|
||||||
client_queue_bell(target);
|
client_queue_bell(target);
|
||||||
|
|
@ -242,35 +290,8 @@ void commands_dispatch(client_t *client) {
|
||||||
}
|
}
|
||||||
|
|
||||||
} else if (command_id == TNT_COMMAND_INBOX) {
|
} else if (command_id == TNT_COMMAND_INBOX) {
|
||||||
/* Snapshot the inbox under whisper_lock so a concurrent sender doesn't
|
output_kind = TNT_COMMAND_OUTPUT_INBOX;
|
||||||
* tear what we're rendering. Counter reset happens after copy. */
|
append_inbox_output(client, output, sizeof(output), &pos);
|
||||||
whisper_t snapshot[WHISPER_INBOX_SIZE];
|
|
||||||
int snap_count;
|
|
||||||
pthread_mutex_lock(&client->whisper_lock);
|
|
||||||
snap_count = client->whisper_inbox_count;
|
|
||||||
memcpy(snapshot, client->whisper_inbox,
|
|
||||||
snap_count * sizeof(whisper_t));
|
|
||||||
pthread_mutex_unlock(&client->whisper_lock);
|
|
||||||
client->unread_whispers = 0;
|
|
||||||
|
|
||||||
buffer_appendf(output, sizeof(output), &pos,
|
|
||||||
"\033[1;36m%s\033[0m \033[2;37m· %d\033[0m\n",
|
|
||||||
i18n_text(client->ui_lang, I18N_INBOX_TITLE),
|
|
||||||
snap_count);
|
|
||||||
if (snap_count == 0) {
|
|
||||||
buffer_appendf(output, sizeof(output), &pos,
|
|
||||||
" \033[2;37m%s\033[0m\n",
|
|
||||||
i18n_text(client->ui_lang, I18N_INBOX_EMPTY));
|
|
||||||
}
|
|
||||||
for (int i = 0; i < snap_count; i++) {
|
|
||||||
char ts[20];
|
|
||||||
struct tm tmi;
|
|
||||||
localtime_r(&snapshot[i].timestamp, &tmi);
|
|
||||||
strftime(ts, sizeof(ts), "%m-%d %H:%M", &tmi);
|
|
||||||
buffer_appendf(output, sizeof(output), &pos,
|
|
||||||
" \033[90m%s\033[0m \033[35m%s\033[0m: %s\n",
|
|
||||||
ts, snapshot[i].from, snapshot[i].content);
|
|
||||||
}
|
|
||||||
|
|
||||||
} else if (command_id == TNT_COMMAND_NICK) {
|
} else if (command_id == TNT_COMMAND_NICK) {
|
||||||
const char *new_name = arg;
|
const char *new_name = arg;
|
||||||
|
|
@ -414,6 +435,7 @@ void commands_dispatch(client_t *client) {
|
||||||
cmd_done:
|
cmd_done:
|
||||||
snprintf(client->command_output, sizeof(client->command_output), "%s", output);
|
snprintf(client->command_output, sizeof(client->command_output), "%s", output);
|
||||||
client->command_output_scroll = 0;
|
client->command_output_scroll = 0;
|
||||||
|
client->command_output_kind = output_kind;
|
||||||
client->command_input[0] = '\0';
|
client->command_input[0] = '\0';
|
||||||
tui_render_command_output(client);
|
tui_render_command_output(client);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -75,6 +75,7 @@ void help_text_append_full(char *buffer, size_t buf_size, size_t *pos,
|
||||||
" Ctrl+D/U - Scroll half page down/up\n"
|
" Ctrl+D/U - Scroll half page down/up\n"
|
||||||
" Ctrl+F/B - Scroll full page down/up\n"
|
" Ctrl+F/B - Scroll full page down/up\n"
|
||||||
" g/G - Jump to top/bottom\n"
|
" g/G - Jump to top/bottom\n"
|
||||||
|
" r - Refresh live output (:inbox)\n"
|
||||||
"\n"
|
"\n"
|
||||||
"SPECIAL MESSAGES:\n"
|
"SPECIAL MESSAGES:\n"
|
||||||
" /me <action> - Send action (e.g. /me waves)\n"
|
" /me <action> - Send action (e.g. /me waves)\n"
|
||||||
|
|
@ -94,6 +95,7 @@ void help_text_append_full(char *buffer, size_t buf_size, size_t *pos,
|
||||||
" Ctrl+D/U - 向下/上滚动半页\n"
|
" Ctrl+D/U - 向下/上滚动半页\n"
|
||||||
" Ctrl+F/B - 向下/上滚动整页\n"
|
" Ctrl+F/B - 向下/上滚动整页\n"
|
||||||
" g/G - 跳到顶部/底部\n"
|
" g/G - 跳到顶部/底部\n"
|
||||||
|
" r - 刷新动态输出 (:inbox)\n"
|
||||||
"\n"
|
"\n"
|
||||||
"特殊消息:\n"
|
"特殊消息:\n"
|
||||||
" /me <action> - 发送动作 (如 /me waves)\n"
|
" /me <action> - 发送动作 (如 /me waves)\n"
|
||||||
|
|
|
||||||
|
|
@ -57,6 +57,10 @@ static const i18n_string_t text_catalog[I18N_TEXT_COUNT] = {
|
||||||
"-- COMMAND OUTPUT -- (%d/%d) j/k:scroll Ctrl-D/U:half g/G:top/bottom q:close",
|
"-- COMMAND OUTPUT -- (%d/%d) j/k:scroll Ctrl-D/U:half g/G:top/bottom q:close",
|
||||||
"-- 命令输出 -- (%d/%d) j/k:滚动 Ctrl-D/U:半页 g/G:首尾 q:关闭"
|
"-- 命令输出 -- (%d/%d) j/k:滚动 Ctrl-D/U:半页 g/G:首尾 q:关闭"
|
||||||
),
|
),
|
||||||
|
[I18N_COMMAND_OUTPUT_REFRESH_STATUS_FORMAT] = I18N_STRING(
|
||||||
|
"-- COMMAND OUTPUT -- (%d/%d) j/k:scroll Ctrl-D/U:half g/G:top/bottom r:refresh q:close",
|
||||||
|
"-- 命令输出 -- (%d/%d) j/k:滚动 Ctrl-D/U:半页 g/G:首尾 r:刷新 q:关闭"
|
||||||
|
),
|
||||||
[I18N_MOTD_TITLE] = I18N_STRING(
|
[I18N_MOTD_TITLE] = I18N_STRING(
|
||||||
" NOTICE ",
|
" NOTICE ",
|
||||||
" 公告 "
|
" 公告 "
|
||||||
|
|
|
||||||
13
src/input.c
13
src/input.c
|
|
@ -221,6 +221,7 @@ static void dismiss_command_output(client_t *client) {
|
||||||
was_motd = client->show_motd;
|
was_motd = client->show_motd;
|
||||||
client->command_output[0] = '\0';
|
client->command_output[0] = '\0';
|
||||||
client->command_output_scroll = 0;
|
client->command_output_scroll = 0;
|
||||||
|
client->command_output_kind = TNT_COMMAND_OUTPUT_NONE;
|
||||||
client->show_motd = false;
|
client->show_motd = false;
|
||||||
client->mode = MODE_NORMAL;
|
client->mode = MODE_NORMAL;
|
||||||
if (was_motd) {
|
if (was_motd) {
|
||||||
|
|
@ -349,6 +350,9 @@ static bool handle_key(client_t *client, unsigned char key, char *input) {
|
||||||
} else if (key == 'G') {
|
} else if (key == 'G') {
|
||||||
client->command_output_scroll = 999;
|
client->command_output_scroll = 999;
|
||||||
tui_render_command_output(client);
|
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 */
|
return true; /* Key consumed */
|
||||||
}
|
}
|
||||||
|
|
@ -735,6 +739,7 @@ void input_run_session(client_t *client) {
|
||||||
client->command_history_count = 0;
|
client->command_history_count = 0;
|
||||||
client->command_history_pos = 0;
|
client->command_history_pos = 0;
|
||||||
client->command_output_scroll = 0;
|
client->command_output_scroll = 0;
|
||||||
|
client->command_output_kind = TNT_COMMAND_OUTPUT_NONE;
|
||||||
client->connect_time = time(NULL);
|
client->connect_time = time(NULL);
|
||||||
client->last_active = time(NULL);
|
client->last_active = time(NULL);
|
||||||
|
|
||||||
|
|
@ -788,6 +793,7 @@ void input_run_session(client_t *client) {
|
||||||
sizeof(client->command_output),
|
sizeof(client->command_output),
|
||||||
"%s", motd_buf);
|
"%s", motd_buf);
|
||||||
client->command_output_scroll = 0;
|
client->command_output_scroll = 0;
|
||||||
|
client->command_output_kind = TNT_COMMAND_OUTPUT_NONE;
|
||||||
client->show_motd = true;
|
client->show_motd = true;
|
||||||
tui_render_motd(client);
|
tui_render_motd(client);
|
||||||
seen_update_seq = room_get_update_seq(g_room);
|
seen_update_seq = room_get_update_seq(g_room);
|
||||||
|
|
@ -836,6 +842,13 @@ main_loop:
|
||||||
room_updated = true;
|
room_updated = true;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (client->command_output_kind == TNT_COMMAND_OUTPUT_INBOX &&
|
||||||
|
client->command_output[0] != '\0' &&
|
||||||
|
client->unread_whispers > 0) {
|
||||||
|
commands_refresh_active_output(client);
|
||||||
|
client->redraw_pending = true;
|
||||||
|
}
|
||||||
|
|
||||||
if (client->redraw_pending ||
|
if (client->redraw_pending ||
|
||||||
(room_updated && !client->show_help &&
|
(room_updated && !client->show_help &&
|
||||||
client->command_output[0] == '\0')) {
|
client->command_output[0] == '\0')) {
|
||||||
|
|
|
||||||
|
|
@ -677,7 +677,10 @@ void tui_render_command_output(client_t *client) {
|
||||||
|
|
||||||
buffer_appendf(buffer, sizeof(buffer), &pos,
|
buffer_appendf(buffer, sizeof(buffer), &pos,
|
||||||
i18n_text(client->ui_lang,
|
i18n_text(client->ui_lang,
|
||||||
I18N_COMMAND_OUTPUT_STATUS_FORMAT),
|
client->command_output_kind ==
|
||||||
|
TNT_COMMAND_OUTPUT_INBOX
|
||||||
|
? I18N_COMMAND_OUTPUT_REFRESH_STATUS_FORMAT
|
||||||
|
: I18N_COMMAND_OUTPUT_STATUS_FORMAT),
|
||||||
start + 1, max_scroll + 1);
|
start + 1, max_scroll + 1);
|
||||||
|
|
||||||
client_send(client, buffer, pos);
|
client_send(client, buffer, pos);
|
||||||
|
|
|
||||||
|
|
@ -304,6 +304,9 @@ expect ":"
|
||||||
send -- "inbox\r"
|
send -- "inbox\r"
|
||||||
expect "Private messages"
|
expect "Private messages"
|
||||||
expect "(empty)"
|
expect "(empty)"
|
||||||
|
expect "r:refresh"
|
||||||
|
send -- "r"
|
||||||
|
expect "Private messages"
|
||||||
expect "q:close"
|
expect "q:close"
|
||||||
send -- "q"
|
send -- "q"
|
||||||
expect "NORMAL"
|
expect "NORMAL"
|
||||||
|
|
|
||||||
|
|
@ -36,7 +36,7 @@ fi
|
||||||
SSH_OPTS="-e none -tt -o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null -o LogLevel=ERROR -p $PORT"
|
SSH_OPTS="-e none -tt -o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null -o LogLevel=ERROR -p $PORT"
|
||||||
SSH_EXEC_OPTS="-n -o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null -o BatchMode=yes -p $PORT"
|
SSH_EXEC_OPTS="-n -o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null -o BatchMode=yes -p $PORT"
|
||||||
BOB_READY="$STATE_DIR/bob.ready"
|
BOB_READY="$STATE_DIR/bob.ready"
|
||||||
ALICE_DONE="$STATE_DIR/alice.done"
|
PRIVATE_SENT="$STATE_DIR/private.sent"
|
||||||
|
|
||||||
wait_for_health() {
|
wait_for_health() {
|
||||||
out=""
|
out=""
|
||||||
|
|
@ -80,14 +80,17 @@ spawn ssh $SSH_OPTS bob@127.0.0.1
|
||||||
sleep 1
|
sleep 1
|
||||||
send -- "bob\r"
|
send -- "bob\r"
|
||||||
expect ":help"
|
expect ":help"
|
||||||
exec touch "$BOB_READY"
|
|
||||||
exec sh -c "while \[ ! -f '$ALICE_DONE' \]; do sleep 1; done"
|
|
||||||
send -- "\033"
|
send -- "\033"
|
||||||
expect "NORMAL"
|
expect "NORMAL"
|
||||||
send -- ":"
|
send -- ":"
|
||||||
expect ":"
|
expect ":"
|
||||||
send -- "inbox\r"
|
send -- "inbox\r"
|
||||||
expect "私信"
|
expect "私信"
|
||||||
|
expect "(空)"
|
||||||
|
expect "r:刷新"
|
||||||
|
exec touch "$BOB_READY"
|
||||||
|
exec sh -c "while \[ ! -f '$PRIVATE_SENT' \]; do sleep 1; done"
|
||||||
|
expect "私信"
|
||||||
expect "alice"
|
expect "alice"
|
||||||
expect "private lifecycle ping"
|
expect "private lifecycle ping"
|
||||||
expect "q:关闭"
|
expect "q:关闭"
|
||||||
|
|
@ -194,6 +197,7 @@ send -- ":"
|
||||||
expect ":"
|
expect ":"
|
||||||
send -- "msg bob private lifecycle ping\r"
|
send -- "msg bob private lifecycle ping\r"
|
||||||
expect "私信已发送给 bob"
|
expect "私信已发送给 bob"
|
||||||
|
exec touch "$PRIVATE_SENT"
|
||||||
expect "q:关闭"
|
expect "q:关闭"
|
||||||
send -- "q"
|
send -- "q"
|
||||||
expect "NORMAL"
|
expect "NORMAL"
|
||||||
|
|
@ -208,7 +212,6 @@ send -- "i"
|
||||||
expect ":help"
|
expect ":help"
|
||||||
send -- "/me ships lifecycle\r"
|
send -- "/me ships lifecycle\r"
|
||||||
sleep 1
|
sleep 1
|
||||||
exec touch "$ALICE_DONE"
|
|
||||||
send -- "\003"
|
send -- "\003"
|
||||||
sleep 0.2
|
sleep 0.2
|
||||||
send -- "\003"
|
send -- "\003"
|
||||||
|
|
@ -222,11 +225,11 @@ else
|
||||||
echo "✗ primary user lifecycle failed"
|
echo "✗ primary user lifecycle failed"
|
||||||
sed -n '1,240p' "$STATE_DIR/alice.log"
|
sed -n '1,240p' "$STATE_DIR/alice.log"
|
||||||
FAIL=$((FAIL + 1))
|
FAIL=$((FAIL + 1))
|
||||||
touch "$ALICE_DONE"
|
touch "$PRIVATE_SENT"
|
||||||
fi
|
fi
|
||||||
|
|
||||||
if wait "$BOB_PID" 2>/dev/null; then
|
if wait "$BOB_PID" 2>/dev/null; then
|
||||||
echo "✓ recipient read private-message inbox"
|
echo "✓ recipient inbox auto-refreshed after private message"
|
||||||
PASS=$((PASS + 1))
|
PASS=$((PASS + 1))
|
||||||
else
|
else
|
||||||
echo "✗ recipient inbox journey failed"
|
echo "✗ recipient inbox journey failed"
|
||||||
|
|
|
||||||
|
|
@ -29,6 +29,7 @@ TEST(full_help_matches_language) {
|
||||||
assert(strstr(en, "AVAILABLE COMMANDS") != NULL);
|
assert(strstr(en, "AVAILABLE COMMANDS") != NULL);
|
||||||
assert(strstr(en, "COMMAND OUTPUT KEYS") != NULL);
|
assert(strstr(en, "COMMAND OUTPUT KEYS") != NULL);
|
||||||
assert(strstr(en, ":inbox") != NULL);
|
assert(strstr(en, ":inbox") != NULL);
|
||||||
|
assert(strstr(en, "Refresh live output") != NULL);
|
||||||
assert(strstr(en, ":support") == NULL);
|
assert(strstr(en, ":support") == NULL);
|
||||||
assert(strstr(en, ":commands") == NULL);
|
assert(strstr(en, ":commands") == NULL);
|
||||||
assert(strstr(en, "Cycle UI language") != NULL);
|
assert(strstr(en, "Cycle UI language") != NULL);
|
||||||
|
|
@ -38,6 +39,7 @@ TEST(full_help_matches_language) {
|
||||||
assert(strstr(zh, "可用命令") != NULL);
|
assert(strstr(zh, "可用命令") != NULL);
|
||||||
assert(strstr(zh, "命令输出按键") != NULL);
|
assert(strstr(zh, "命令输出按键") != NULL);
|
||||||
assert(strstr(zh, ":inbox") != NULL);
|
assert(strstr(zh, ":inbox") != NULL);
|
||||||
|
assert(strstr(zh, "刷新动态输出") != NULL);
|
||||||
assert(strstr(zh, "/me <action>") != NULL);
|
assert(strstr(zh, "/me <action>") != NULL);
|
||||||
assert(strstr(zh, "@username") != NULL);
|
assert(strstr(zh, "@username") != NULL);
|
||||||
assert(strstr(zh, "<动作>") == NULL);
|
assert(strstr(zh, "<动作>") == NULL);
|
||||||
|
|
|
||||||
|
|
@ -111,6 +111,12 @@ TEST(text_lookup_matches_language) {
|
||||||
"q:close") != NULL);
|
"q:close") != NULL);
|
||||||
assert(strstr(i18n_text(UI_LANG_ZH, I18N_COMMAND_OUTPUT_STATUS_FORMAT),
|
assert(strstr(i18n_text(UI_LANG_ZH, I18N_COMMAND_OUTPUT_STATUS_FORMAT),
|
||||||
"q:关闭") != NULL);
|
"q:关闭") != NULL);
|
||||||
|
assert(strstr(i18n_text(UI_LANG_EN,
|
||||||
|
I18N_COMMAND_OUTPUT_REFRESH_STATUS_FORMAT),
|
||||||
|
"r:refresh") != NULL);
|
||||||
|
assert(strstr(i18n_text(UI_LANG_ZH,
|
||||||
|
I18N_COMMAND_OUTPUT_REFRESH_STATUS_FORMAT),
|
||||||
|
"r:刷新") != NULL);
|
||||||
assert(strstr(i18n_text(UI_LANG_EN, I18N_MOTD_CONTINUE_HINT),
|
assert(strstr(i18n_text(UI_LANG_EN, I18N_MOTD_CONTINUE_HINT),
|
||||||
"Press any key") != NULL);
|
"Press any key") != NULL);
|
||||||
assert(strstr(i18n_text(UI_LANG_ZH, I18N_MOTD_CONTINUE_HINT),
|
assert(strstr(i18n_text(UI_LANG_ZH, I18N_MOTD_CONTINUE_HINT),
|
||||||
|
|
|
||||||
8
tnt.1
8
tnt.1
|
|
@ -199,6 +199,14 @@ l l.
|
||||||
Up/Down Browse command history
|
Up/Down Browse command history
|
||||||
ESC Cancel and return to NORMAL
|
ESC Cancel and return to NORMAL
|
||||||
.TE
|
.TE
|
||||||
|
.PP
|
||||||
|
Command output pages use j/k, Ctrl+D/Ctrl+U, and g/G for paging.
|
||||||
|
The
|
||||||
|
.B :inbox
|
||||||
|
page can also be refreshed with
|
||||||
|
.B r
|
||||||
|
and refreshes automatically when a new private message arrives while it is
|
||||||
|
open.
|
||||||
.SH EXEC INTERFACE
|
.SH EXEC INTERFACE
|
||||||
Commands can be run non\-interactively for scripting:
|
Commands can be run non\-interactively for scripting:
|
||||||
.PP
|
.PP
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue