tui: persistent @mention unread counter in title bar (UX-11)

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.
This commit is contained in:
m1ngsama 2026-05-17 14:27:46 +08:00
parent 70718482f3
commit ddcecbea81
3 changed files with 25 additions and 1 deletions

View file

@ -36,6 +36,7 @@ typedef struct client {
time_t connect_time;
time_t last_active;
atomic_bool redraw_pending;
_Atomic int unread_mentions; /* @-mentions received since last reset */
bool mute_joins;
pthread_t thread;
atomic_bool connected;

View file

@ -144,6 +144,7 @@ void notify_mentions(const char *content, const client_t *sender) {
for (int i = 0; i < target_count; i++) {
client_send(targets[i], "\a", 1);
targets[i]->unread_mentions++;
targets[i]->redraw_pending = true;
client_release(targets[i]);
}
@ -382,6 +383,7 @@ static bool handle_key(client_t *client, unsigned char key, char *input) {
if (key == 'i') {
client->mode = MODE_INSERT;
client->unread_mentions = 0;
tui_render_screen(client);
return true;
} else if (key == ':') {
@ -429,6 +431,7 @@ static bool handle_key(client_t *client, unsigned char key, char *input) {
return true;
} else if (key == 'G') {
client->scroll_pos = nm_max_scroll;
client->unread_mentions = 0;
tui_render_screen(client);
return true;
} else if (key == '?') {

View file

@ -360,10 +360,22 @@ void tui_render_screen(client_t *client) {
int hint_width = utf8_string_width(hint);
int mute_width = client->mute_joins ? 6 : 0; /* " 静音" = 2 + 4 */
/* Unread @-mentions chip — high-priority, gets a bright yellow star.
* Sits between mode and hint when present, and survives degradation
* longer than the hint / mute / mode chips. */
int unread_count = client->unread_mentions;
char unread_buf[32] = "";
int unread_width = 0;
if (unread_count > 0) {
snprintf(unread_buf, sizeof(unread_buf), "★ %d", unread_count);
unread_width = utf8_string_width(unread_buf) + 2; /* leading " · " minus initial space accounted later */
}
/* Decide what fits. Reserve at least 1 col of gap between left and
* right halves so they never visually touch. */
int show_hint = 1;
int show_mute = client->mute_joins ? 1 : 0;
int show_unread = unread_count > 0 ? 1 : 0;
int show_chips = chip_count;
while (show_chips > 1) {
@ -373,14 +385,17 @@ void tui_render_screen(client_t *client) {
left_w += utf8_string_width(chips[i].value);
}
if (show_mute) left_w += mute_width;
if (show_unread) left_w += unread_width + 1; /* + " " separator */
int right_w = (show_hint ? hint_width + 1 /*trailing space*/ : 0);
int needed = left_w + 1 /*min gap*/ + right_w;
if (needed <= render_width) break;
/* Drop in priority order: hint → mute → mode chip → online count. */
/* Drop in priority order: hint → mute → mode chip → online count.
* Unread is sticky only dropped if everything else already is. */
if (show_hint) { show_hint = 0; continue; }
if (show_mute) { show_mute = 0; continue; }
if (show_chips > 1) { show_chips--; continue; }
if (show_unread) { show_unread = 0; continue; }
break;
}
@ -401,6 +416,11 @@ void tui_render_screen(client_t *client) {
buffer_appendf(left, sizeof(left), &lpos, " \033[2;37m静音\033[0m");
left_width += mute_width;
}
if (show_unread) {
buffer_appendf(left, sizeof(left), &lpos,
" \033[1;33m%s\033[0m", unread_buf);
left_width += unread_width + 1;
}
int gap = render_width - left_width - (show_hint ? hint_width + 2 : 1);
if (gap < 1) gap = 1;