NORMAL mode status line:
before: "-- NORMAL -- (3/100) ↓ 5 new"
after : "[reverse-yellow NORMAL] 3 / 100 ▼ 5 new"
- the mode is now a small reverse-video yellow chip so it visually
echoes the mode colour in the title bar
- position is dim grey with em-spaced slash, "▼" replaces "↓" so the
unseen-message marker matches the same downward-triangle vocabulary
the help screen uses
INSERT mode prompt:
before: "> "
after : "› " (U+203A, dim grey)
- single-glyph chevron is quieter than the ASCII ">", aligns with the
thinner aesthetic of the new title bar
- applied both in tui_render_screen() and tui_render_input() so
per-keystroke redraws match the initial paint
COMMAND mode prompt:
before: ":foo"
after : "[magenta :]foo"
- the leading colon picks up the COMMAND mode colour so it reads as
"you are in command mode now" rather than as part of the typed text
Replace the full-width reverse-video title
▍ tester | 在线: 3 | 模式: INSERT | ? 帮助 ▎ (reversed)
with a single line of small chips on a normal background:
tester · 在线 3 · INSERT ? 帮助
- username is bold white, online count is plain
- mode name carries its own colour, so glance + colour disambiguates
it (INSERT cyan, NORMAL yellow, COMMAND magenta, HELP blue) without
having to read the word
- separators are dim grey middle-dots so the eye groups segments
- mute marker shrinks from "[静音]" to a dim "静音" suffix when active
- hint "? 帮助" is right-aligned in dim grey, far from content
The horizontal rule below the chat already provides the visual divider
that the reverse-video bar used to provide, so the bar can drop the
heaviness without losing structure.
Replace the four lines of `==` rules + bilingual title with a centred
rounded box so the eye lands on a single visual element instead of two.
The new tui_render_welcome():
- centres horizontally and vertically (about a third of the way down)
- shows TNT + version, then 中文 subtitle, then English caption with
decreasing visual weight (bold cyan / default / dim grey)
- falls back to a plain "TNT $version — anonymous chat over SSH" line
when the terminal is too narrow for the frame
- declared in tui.h, called from input.c read_username()
read_username()'s prompt becomes a single short bilingual hint
("请输入用户名 (留空 anonymous): ") that lives below the box.
Move client_t I/O and lifecycle out of ssh_server.c into a dedicated
module. ssh_server.c is now down to the listening socket, host key
setup, ssh_server_init / ssh_server_start, and the accept loop.
Migrated to client.{c,h}:
- client_send, client_printf
- client_addref, client_release (and the ssh_session / ssh_channel /
channel_cb teardown that fires on the final release)
- client_install_channel_callbacks
- client_channel_window_change / _eof / _close (the post-bootstrap
callbacks that target the client_t)
The client_t struct definition stays in ssh_server.h so we don't have
to revisit every existing #include chain. bootstrap, commands, exec,
input, and tui pick up the new client.h alongside their existing
ssh_server.h include.
ssh_server.c shrinks from 402 to 249 lines (-153).
Final per-module size after PR2:
ssh_server.c 249 accept loop + ssh_server_init/start + host key
bootstrap.c 493 per-connection SSH handshake / auth / channel
client.c 162 client_t I/O + lifecycle + channel callbacks
input.c 637 username read + handle_key + main loop +
notify_mentions + idle timeout
commands.c 269 vim ':' command dispatcher
exec.c 453 SSH exec subcommand dispatcher
ratelimit.c 197 IP rate limit + connection counters
tui.c 614 screen rendering
chat_room.c 151 room + client list
message.c 350 message log load/save/search
utf8.c 250 UTF-8 width / validation
common.c 133 buffer_*, env_int, is_valid_username,
sanitize_terminal_size, tnt_state_*
main.c 109 process entry
Total: ~4 000 lines of C across 13 files, no file over 650 lines, every
file is single-purpose. Behaviour is preserved: the migrated code is
byte-for-byte identical.
Fixes:
- message_load() now holds g_message_file_lock for the read, so :last [N]
can no longer observe a half-written line while message_save() is
flushing.
- constant_time_strcmp() accumulates the length difference in size_t.
The old code truncated to unsigned char, which collapsed pairs whose
lengths differed by a multiple of 256 down to 0 and lost the signal.
Refactor:
- buffer_appendf() / buffer_append_bytes() moved to common.c; the two
identical copies in ssh_server.c and tui.c have been removed.
Docs / cleanup:
- README clarifies that exec 'post' uses the SSH login name as the
author and that anonymous mode performs no identity check.
- Removed TODO.md (both items completed) and docs/README.old.
- Trimmed the auto-generated 2025 entry block from docs/CHANGELOG.md
and added a 2026-05-16 entry summarising this change.
- :last [N]: show last N messages from log (max 50, default 10)
- :search <keyword>: case-insensitive full-text search across log history
- :mute-joins: per-client toggle to silence join/leave system messages,
indicated by [静音] in the title bar
- MOTD: display motd.txt from state directory on connect before entering chat
- Add message_search() to message.c/h for log file scanning
- Update :help and tui help screen (EN/ZH) with new commands
- @mention: typing @username in a message sends bell char to that user
and highlights the message content in bold yellow for them
- Idle timeout: disconnect inactive clients after TNT_IDLE_TIMEOUT
seconds (default 1800 = 30min, 0 to disable)
- :list now shows connection duration per user (e.g. "alice (12m)")
- Document all three features in help text, manpage, and README
Closes#46
- Color-code usernames using hash-based ANSI color assignment (6 colors)
- Style system messages (join/leave/rename) in gray with --> prefix
- Style /me action messages in italic cyan
- Show "↓ N new" indicator in yellow when scrolled up in NORMAL mode
- Compact timestamps from full date to HH:MM for more message space
Closes#44
- Add /me action command to all help surfaces (EN/ZH help screen,
:help output, exec help, manpage, README)
- Add Ctrl+D/U/F/B page scrolling keys to help text (were only in manpage)
- Add :q/:quit/:exit disconnect command to help text
- Update README COMMAND mode section with all current commands
(:nick, :msg, :w were missing)
- Remove redundant COMMAND MODE KEYS section from help text
(merged into AVAILABLE COMMANDS for clarity)
- Compact help screen layout (j/k on one line, g/G on one line)
- Replace all direct client->width/height reads with local variables
clamped to minimums (width>=10, height>=4) across tui_render_screen,
tui_render_input, tui_render_command_output, and tui_render_help
- Fix tui_render_input underflow when width < 3 (avail = max(rw-3, 1))
- Show username in title bar instead of generic label
- Add /me action message support in exec_command_post for scripting parity
- Reject empty usernames when loading messages from log file
- :nick/:name <name>: change username in-session with full validation,
thread-safe update under write lock, and system broadcast
- /me <action>: IRC-style action messages displayed as "* user action"
- Updated help text (EN/ZH) and manpage with new commands
Closes#38
Bug fixes:
- Fix data race on client->width/height (now _Atomic int)
- Persist join/leave system messages via message_save()
- Make room_add_message static to enforce lock contract
- Fix execute_command mutating command_input directly
- Increase help_copy buffer from 4096 to 8192 for CJK safety
New features:
- Add :msg/:w whisper command for private messaging
- Add command history with UP/DOWN arrows in command mode
- Add Ctrl+D/U/F/B page scrolling in normal mode
- Add :q/:quit/:exit Vim-style disconnect
Unix community:
- Add tnt.1 manpage (roff format) with full documentation
- Add manpage install/uninstall to Makefile
- Fix use-after-free/double-free on install_client_channel_callbacks
failure: nullify session/channel ownership before releasing refs so
cleanup_failed_session does not double-free resources
- Fix constant_time_strcmp to always iterate over the full secret length,
preventing timing leak of token length
- Fix data race on client->width/height by protecting window-change
callback writes with io_lock
- Fix potential UTF-8 mid-sequence truncation in tui_render_input by
sizing display buffer to MAX_MESSAGE_LEN
Critical fixes:
- C-1: Use atomic_bool for client->connected and redraw_pending to prevent
data races between callback and main threads
- C-2: Add reference counting for channel callbacks to prevent use-after-free
when callbacks fire during client cleanup
- C-3/M-7: Use ssh_channel_read_timeout (5s) for UTF-8 continuation bytes
to prevent thread blocking and stream desynchronization
High-severity fixes:
- H-1: Replace non-thread-safe setenv/tzset with timegm() in parse_rfc3339_utc
- H-2: Change room_get_message to return by value copy instead of interior pointer
- H-3: Log warning when rate-limit table evicts active IP entry
- H-4: Replace strcmp with constant-time comparison for access token validation
- H-5: Check signature_state in auth_pubkey to reject unsigned key offers
Medium/low fixes:
- M-1: Replace all atoi() with strtol() for proper error detection
- M-3: Move calloc outside rwlock in tui_render_screen to avoid blocking writers
- M-8: Fix off-by-one in rate limit threshold (> to >=)
- M-9: Trim partial UTF-8 sequences after snprintf truncation in message_format
- L-1: Validate continuation byte mask (0xC0==0x80) in utf8_decode
- D-3: Remove vestigial client_t.fd field
- L-3: Remove unreachable pthread_attr_destroy after infinite loop
Fixes#10.
Five bugs that caused the server to crash or become unresponsive:
1. Signal handler deadlock (main.c)
signal_handler called room_destroy (pthread_rwlock + free) and printf —
neither is async-signal-safe. If SIGTERM arrived while any thread held
g_room->lock, the process deadlocked permanently.
Fix: handler now only writes a message via write(2) and calls _exit(0).
Also remove close(g_listen_fd) which was closing stdin (fd 0), since
ssh_server_init returns 0 on success, not a real file descriptor.
2. NULL dereference in room_broadcast when room is empty (chat_room.c)
calloc(0, n) may return NULL per POSIX; memcpy on NULL is undefined.
Also: no NULL check after calloc for the OOM case.
Fix: early return if count == 0; check calloc return value.
3. Stack buffer overflow in tui_render_screen (tui.c)
char buffer[8192] overflows with tall terminals: 197 visible lines *
~1031 bytes/message ≈ 203 KiB. Title padding loop also lacked a
bounds check (buffer[pos++] = ' ' with no guard).
Fix: switch to malloc(65536) with buf_size used consistently.
Add bounds check to the title padding loop.
4. sleep() inside libssh auth callback (ssh_server.c)
auth_password is called from ssh_event_dopoll in the main thread.
sleep(2) there blocks the entire accept loop — one attacker with
repeated wrong passwords stalls all incoming connections.
IP blocking via record_auth_failure already handles brute force.
Fix: remove sleep(2) from auth_password.
5. Spurious sleep() calls in the main accept loop (ssh_server.c)
sleep(1/2) after rejecting rate-limited or over-limit connections
delays accepting the next legitimate connection for no benefit.
Fix: remove all sleep() from the accept loop error paths.
This PR addresses critical performance bottlenecks, improves UX, and eliminates technical debt.
### Key Changes
**1. Performance Optimization:**
- **Startup**: Rewrote `message_load` to scan `messages.log` backwards from the end
- Complexity reduced from O(FileSize) to O(MaxMessages)
- Large log file startup: seconds → milliseconds
- **Rendering**: Optimized TUI rendering to use line clearing (`\033[K`) instead of full-screen clearing (`\033[2J`)
- Eliminated visual flicker
**2. libssh API Migration:**
- Replaced deprecated message-based API with callback-based server implementation
- Removed `#pragma GCC diagnostic ignored "-Wdeprecated-declarations"`
- Ensures future libssh compatibility
**3. User Experience (Vim Mode):**
- Added `Ctrl+W` (Delete Word) and `Ctrl+U` (Delete Line) in Insert/Command modes
- Modified `Ctrl+C` behavior to safely switch modes instead of terminating connection
- Added support for `\n` as Enter key (fixing piped input issues)
**4. Project Structure:**
- Moved all test scripts to `tests/` directory
- Added `make test` target
- Updated CI/CD to run comprehensive test suite
### Verification
- ✅ All tests passing (17/17)
- ✅ CI passing on Ubuntu and macOS
- ✅ AddressSanitizer clean
- ✅ Valgrind clean (no memory leaks)
- ✅ Zero compilation warnings
### Code Quality
**Rating:** 🟢 Good Taste
- Algorithm-driven optimization (not hacks)
- Simplified architecture (callback-based API)
- Zero breaking changes (all tests pass)
- Enhance room_broadcast() reference counting:
* Check client state (connected, show_help, command_output) before rendering
* Perform state check while holding client ref_lock
* Prevents rendering to disconnected/invalid clients
* Ensures safe cleanup when ref count reaches zero
- Fix tui_render_screen() message array TOCTOU:
* Acquire all data (online count, message count, messages) in single lock
* Create snapshot of messages to display
* Calculate message range while holding lock
* Render from snapshot without holding lock
* Prevents inconsistencies from concurrent message additions
* Eliminates race between two separate lock acquisitions
- Fix handle_key() scroll position TOCTOU:
* Get message count atomically when calculating scroll bounds
* Calculate max_scroll properly accounting for message height
* Apply consistent bounds checking for 'j' (down) and 'G' (bottom)
* Prevents out-of-bounds access from concurrent message changes
These changes address:
- Race condition in broadcast rendering to disconnecting clients
- TOCTOU between message count read and message access
- Scroll position bounds check race conditions
Prevents:
- Use-after-free in client cleanup
- Array out-of-bounds access
- Inconsistent UI rendering
- Crashes from concurrent message list modifications
Improves thread safety without introducing deadlocks by:
- Using snapshot approach to avoid long lock holds
- Acquiring data in consistent lock order
- Minimizing critical sections
Fixes three critical bugs that caused crashes after long-running:
1. Use-after-free race condition in room_broadcast()
- Added reference counting to client_t structure
- Increment ref_count before using client outside lock
- Decrement and free only when ref_count reaches 0
- Prevents accessing freed client memory during broadcast
2. strtok() data corruption in tui_render_command_output()
- strtok() modifies original string by replacing delimiters
- Now use a local copy before calling strtok()
- Prevents corruption of client->command_output
3. Improved handle_key() consistency
- Return bool to indicate if key was consumed
- Fixes issue where mode-switch keys were processed twice
Thread safety changes:
- Added client->ref_count and client->ref_lock
- Added client_release() for safe cleanup
- room_broadcast() now properly increments/decrements refs
This fixes the primary cause of crashes during extended operation.
- Allow SSH_AUTH_METHOD_NONE for passwordless authentication
- Replace all \n with \r\n in TUI rendering for proper line breaks
- Fixes messages appearing misaligned on terminal
- Implement SSH server using libssh for secure connections
- Replace insecure telnet with encrypted SSH protocol
- Add automatic terminal size detection via PTY requests
- Support dynamic window resize (SIGWINCH handling)
- Fix UI display bug by using SSH channel instead of fd
- Update tui_clear_screen to work with SSH connections
- Add RSA host key auto-generation on first run
- Update README with SSH instructions and security notes
- Add libssh dependency to Makefile with auto-detection
- Remove all telnet-related code
Security improvements:
- All traffic now encrypted
- Host key authentication
- No more plaintext transmission