diff --git a/include/ssh_server.h b/include/ssh_server.h index 9bb9d0f..8cbafc3 100644 --- a/include/ssh_server.h +++ b/include/ssh_server.h @@ -7,6 +7,16 @@ #include #include +/* One stored whisper. Kept per-recipient, not broadcast to the room + * and not persisted to messages.log. Inbox is bounded; oldest slides + * out FIFO. */ +#define WHISPER_INBOX_SIZE 16 +typedef struct { + time_t timestamp; + char from[MAX_USERNAME_LEN]; + char content[MAX_MESSAGE_LEN]; +} whisper_t; + /* Client connection structure */ typedef struct client { ssh_session session; /* SSH session */ @@ -37,6 +47,11 @@ typedef struct client { time_t last_active; atomic_bool redraw_pending; _Atomic int unread_mentions; /* @-mentions received since last reset */ + _Atomic int unread_whispers; /* whispers received since last :inbox view */ + /* Per-client whisper inbox. Pushes serialise on io_lock; readers are + * the client's own thread inside :inbox handling. */ + whisper_t whisper_inbox[WHISPER_INBOX_SIZE]; + int whisper_inbox_count; bool mute_joins; pthread_t thread; atomic_bool connected; diff --git a/src/commands.c b/src/commands.c index f9b59fc..4200463 100644 --- a/src/commands.c +++ b/src/commands.c @@ -112,7 +112,8 @@ void commands_dispatch(client_t *client) { "========================================\n" "list, users, who - Show online users\n" "nick/name - Change nickname\n" - "msg/w - Whisper to user\n" + "msg/w - Whisper to user (private)\n" + "inbox - Show whisper history\n" "last [N] - Show last N messages\n" "search - Search message history\n" "mute-joins - Toggle join/leave notices\n" @@ -155,12 +156,33 @@ void commands_dispatch(client_t *client) { pthread_rwlock_unlock(&g_room->lock); if (target) { - char whisper[MAX_MESSAGE_LEN]; - snprintf(whisper, sizeof(whisper), - "\r\n\033[35m[whisper from %s]: %s\033[0m\r\n", - client->username, rest); - client_send(target, whisper, strlen(whisper)); + /* Push into recipient's inbox. io_lock serialises so two + * senders to the same recipient don't tear the ring. */ + pthread_mutex_lock(&target->io_lock); + int slot; + if (target->whisper_inbox_count < WHISPER_INBOX_SIZE) { + slot = target->whisper_inbox_count++; + } else { + /* FIFO evict the oldest */ + memmove(&target->whisper_inbox[0], + &target->whisper_inbox[1], + (WHISPER_INBOX_SIZE - 1) * sizeof(whisper_t)); + slot = WHISPER_INBOX_SIZE - 1; + } + target->whisper_inbox[slot].timestamp = time(NULL); + snprintf(target->whisper_inbox[slot].from, + sizeof(target->whisper_inbox[slot].from), + "%s", client->username); + snprintf(target->whisper_inbox[slot].content, + sizeof(target->whisper_inbox[slot].content), + "%s", rest); + pthread_mutex_unlock(&target->io_lock); + + target->unread_whispers++; target->redraw_pending = true; + /* Audible nudge — the title bar ✉ counter (UX-11 style) + * carries the persistent signal. */ + client_send(target, "\a", 1); client_release(target); } @@ -173,6 +195,35 @@ void commands_dispatch(client_t *client) { } } + } else if (strcmp(cmd, "inbox") == 0) { + /* Snapshot the inbox under io_lock so a concurrent sender doesn't + * tear what we're rendering. Counter reset happens after copy. */ + whisper_t snapshot[WHISPER_INBOX_SIZE]; + int snap_count; + pthread_mutex_lock(&client->io_lock); + snap_count = client->whisper_inbox_count; + memcpy(snapshot, client->whisper_inbox, + snap_count * sizeof(whisper_t)); + pthread_mutex_unlock(&client->io_lock); + client->unread_whispers = 0; + + buffer_appendf(output, sizeof(output), &pos, + "\033[1;36m悄悄话 · whispers\033[0m " + "\033[2;37m· %d\033[0m\n", snap_count); + if (snap_count == 0) { + buffer_appendf(output, sizeof(output), &pos, + " \033[2;37m(空)\033[0m\n"); + } + 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 (strncmp(cmd, "nick ", 5) == 0 || strncmp(cmd, "name ", 5) == 0) { char *new_name = cmd + 5; while (*new_name == ' ') new_name++; diff --git a/src/tui.c b/src/tui.c index a50d3cb..78643f5 100644 --- a/src/tui.c +++ b/src/tui.c @@ -371,11 +371,22 @@ void tui_render_screen(client_t *client) { unread_width = utf8_string_width(unread_buf) + 2; /* leading " · " minus initial space accounted later */ } + /* Unread whispers chip — bright magenta envelope. Same priority as + * the mentions chip; both signal "you missed something". */ + int whisper_count = client->unread_whispers; + char whisper_buf[32] = ""; + int whisper_width = 0; + if (whisper_count > 0) { + snprintf(whisper_buf, sizeof(whisper_buf), "✉ %d", whisper_count); + whisper_width = utf8_string_width(whisper_buf) + 2; + } + /* 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_whisper = whisper_count > 0 ? 1 : 0; int show_chips = chip_count; while (show_chips > 1) { @@ -385,16 +396,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 */ + if (show_unread) left_w += unread_width + 1; + if (show_whisper) left_w += whisper_width + 1; 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. - * Unread is sticky — only dropped if everything else already is. */ + /* Drop priority: hint → mute → mode → online → whispers → mentions. */ if (show_hint) { show_hint = 0; continue; } if (show_mute) { show_mute = 0; continue; } if (show_chips > 1) { show_chips--; continue; } + if (show_whisper) { show_whisper = 0; continue; } if (show_unread) { show_unread = 0; continue; } break; } @@ -421,6 +433,11 @@ void tui_render_screen(client_t *client) { " \033[1;33m%s\033[0m", unread_buf); left_width += unread_width + 1; } + if (show_whisper) { + buffer_appendf(left, sizeof(left), &lpos, + " \033[1;35m%s\033[0m", whisper_buf); + left_width += whisper_width + 1; + } int gap = render_width - left_width - (show_hint ? hint_width + 2 : 1); if (gap < 1) gap = 1;