Whispers used to flash on the recipient's terminal and disappear with
the next redraw. No history, no record, no signal if you weren't
looking.
Now whispers are stored per-recipient in a bounded inbox (16 slots,
FIFO eviction):
typedef struct { time_t timestamp; char from[]; char content[]; } whisper_t;
whisper_t whisper_inbox[16];
int whisper_inbox_count;
_Atomic int unread_whispers;
Sender side (:msg / :w):
- resolves target as before
- pushes the whisper into target->whisper_inbox under target->io_lock
(so two simultaneous senders to the same recipient don't tear the
ring)
- bumps target->unread_whispers atomically
- sends a single \a bell + triggers redraw_pending
- no longer writes whisper text directly to the channel (which used
to get clobbered by the next redraw)
Recipient side:
- new :inbox command in COMMAND mode prints the snapshot under
io_lock, in M7 chat-list style:
悄悄话 · whispers · 3
05-17 13:42 alice: 一会儿要不要喝咖啡
...
- viewing :inbox resets unread_whispers to 0
Title bar (extends UX-11):
- bright magenta "✉ N" chip alongside the yellow "★ N" mention chip
- same priority / degradation rules as ★
Whispers remain private — they're never broadcast to the room and
never persisted to messages.log. The inbox lives only in client_t,
so disconnecting drops it.
The bell + brief yellow highlight on the chat line meant that if you
weren't looking at the screen the moment someone @-mentioned you, you
had no way to know.
Now the title bar carries a sticky chip:
tester · 在线 3 · NORMAL ★ 2 ? 帮助
- bright yellow "★ N" appears whenever client->unread_mentions > 0
- count is bumped atomically in notify_mentions() for each target
- cleared automatically when the user returns to attention:
* pressing 'i' in NORMAL to re-enter INSERT mode
* pressing 'G' in NORMAL to jump to the live tail
- never dropped by the narrow-terminal degradation (UX-6) unless
every other optional chip has already been shed — it's the highest
priority signal in the bar
Counter is _Atomic int so the cross-thread bump in notify_mentions
doesn't tear against the local thread's reads / resets.
ESC in INSERT mode used to switch straight to NORMAL. Now it follows
the same arrow-key probe COMMAND mode already does: if the next two
bytes are "[A" or "[B", treat as Up / Down and walk through the last
16 messages this client has sent. A plain ESC still falls through to
NORMAL — the 50 ms probe timeout keeps that path responsive.
Storage: new client_t fields
char insert_history[16][MAX_MESSAGE_LEN];
int insert_history_count;
int insert_history_pos;
Recording: every Enter that broadcasts a message pushes the input
buffer onto the ring (with FIFO eviction at 16) and resets pos.
Down at the bottom of the ring returns to an empty input.
This is the standard chat-client recall that vim users (and anyone
who's ever used a shell) expect.
The MOTD used to ride on tui_render_command_output's coattails — text
got stuffed into client->command_output prefixed by "=== 公告 / MOTD ==="
and rendered under a reverse-video " COMMAND OUTPUT " title bar. Two
different things (a transient command result vs. a one-shot service
notice) wearing the same chrome.
Now MOTD has its own renderer with its own aesthetic:
╭─ 公告 / MOTD ────────────────────────────────╮
欢迎来到 TNT 公共聊天室。
请互相尊重,不要刷屏。
管理员:m1ng
╰─ 按任意键继续 / press any key ────────────────╯
- title chip embedded in the top border (bright cyan)
- footer hint embedded in the bottom border (dim grey)
- 2-column left padding on body lines, blank top/bottom pad rows
- dim cyan borders, no full-line reverse anywhere
Wiring:
- new tui_render_motd() declared in tui.h
- new client_t.show_motd flag selects the renderer; command_output
remains the text storage (no extra buffer needed)
- input.c MOTD path sets show_motd = true and calls tui_render_motd()
- handle_key's dismiss path clears show_motd alongside command_output
- main-loop redraw dispatch checks show_motd before command_output[0]