forked from armin/amanda
a very tiny IRC client
- C 99.4%
- Makefile 0.6%
| .gitignore | update gitignore; tiny changes | |
| AGENTS.md | ||
| amanda.c | show mode changes properly in status bar | |
| Makefile | add Makefile | |
| README | ||
amanda — minimalist IRC console client ======================================== Amanda is a lightweight, single-file IRC client for the Unix terminal. It has no external dependencies beyond OpenSSL (for TLS), and compiles in under a second with a single `gcc` invocation. The entire application is roughly 1500 lines of C with no build system, no test framework, and no runtime library beyond libc and OpenSSL. gcc -o amanda amanda.c -lssl -lcrypto The binary is stateless — there are no config files, no plugins, no scripts. Everything is controlled via command-line flags at startup and slash commands while running. Design and architecture ----------------------- Amanda is built around a **poll(2)-driven event loop** that multiplexes between two file descriptors: the IRC server socket and stdin. I/O is non-blocking; the loop runs every 100ms (`POLL_MS`), checking for incoming network data and keyboard input in the same tick. The program uses **raw terminal mode** (`term_raw()`) to intercept every keystroke, including escape sequences for arrow keys, Page Up/Down, function keys, and Alt+key combinations. A custom `read_key()` function parses ANSI escape sequences into internal keycodes, handles multi-byte UTF-8 input, and translates control characters into symbolic constants. The screen is redrawn from scratch on every iteration by `ui_draw()`. There is no double-buffering or incremental update — the entire terminal content is regenerated each frame. This simplifies the code enormously and is fast enough for a text protocol like IRC. Windows ------- Amanda supports up to 32 windows (`MAX_WINDOWS`). Each window is an independent scrollable buffer with its own: - Ring buffer of text lines (`LineBuf`, max 4096 lines) - Nick list (`NickList`, max 512 nicks) - Scroll offset - Topic string (for channels) - Unread-message flag - Clear-at marker (for `/clear`) Window 0 is always the **status window**, created at startup. It receives server notices, errors, whois output, CTCP events, and nick change announcements. It cannot be closed. Channel windows are created automatically when you join (`/join`) or when you receive a message from a channel you're already in. Private message (PM) windows are created on incoming whispers or explicitly via `/query`. A window's name is the channel or nick it represents. The status bar at the bottom of the screen shows every window in a compact format: [HH:MM] (nick) [*0:status 1:#chan !2:friend] - `*` marks the active window - `!` marks windows with unread messages Switching windows is done with `Ctrl+N` / `Ctrl+P` or `/window N`. Closing a channel window sends a PART command to the server. Networking and TLS ------------------ TCP connection is handled by `net_connect()`, which resolves the host with `getaddrinfo()` (IPv4/IPv6 dual-stack) and tries each address until one succeeds. If the port is 6697 or 7000, TLS is enabled automatically even without the `-ssl` flag. TLS uses OpenSSL's `TLS_client_method()` with system CA bundle (`SSL_CTX_set_default_verify_paths`). Certificate verification is on by default (`SSL_VERIFY_PEER` with SNI and hostname matching). Pass `-noverify` to disable verification — this is inherently insecure and should never be used on untrusted networks. The socket is put into **non-blocking mode** after the TLS handshake (which itself is done blocking to keep the handshake path simple). All reads and writes after connect use the non-blocking variants, and the main loop relies on `poll()` to signal data availability. IRC protocol handling --------------------- Incoming data is accumulated in a 16KB ring buffer (`rbuf`) and split on `\r\n` boundaries by `irc_feed()`. Each complete line is dispatched to `irc_handle()`, which parses the IRC message format (prefix, command, parameters) and reacts to numeric replies and commands. Handled server messages include: | Code | Description | |---------|---------------------------------| | PING | Responds with PONG | | 001 | Marks session as connected | | 353 | NAMES list — populates nicklist | | 366 | End of NAMES — prints nicklist | | 372/375 | MOTD header / body | | 376/422 | End of MOTD | | 311-318 | WHOIS responses (user, server, | | | channels, end) | | 332 | Channel topic | | 432/433 | Nickname errors (erroneous / | | /436 | in use / conflict) | | 464 | Password rejected — quits | | JOIN | Tracks users joining channels | | PART | Tracks users leaving | | QUIT | Removes user from all nicklists | | NICK | Renames user in all nicklists | | KICK | Removes user from channel | | PRIVMSG | Prints message in channel/PM | | | window; handles CTCP ACTION and | | | VERSION queries | | NOTICE | Prints notice in PM or status; | | | handles CTCP replies | | MODE | Prints mode change | | TOPIC | Updates topic and prints change | | ERROR | Displays server error | Unrecognized commands and numerics are silently ignored. Command processing ------------------ User input is processed by `input_process()`. Lines starting with `/` are parsed as commands (the first word up to a space is the command name, everything after is the argument). Unknown commands produce an error message in the current window. Non-command input is sent as a PRIVMSG to the current window's channel or nick. The command table: | Command | Alias | Description | |----------|----------|--------------------------------------| | `/quit` | `/q` | Send QUIT and exit | | `/join` | `/j` | Join channel (prepends `#` if needed)| | `/part` | | Leave current or specified channel | | `/msg` | | Send private message to a nick | | `/query` | | Open (or switch to) PM window; | | | | optionally send a message | | `/nick` | | Change nickname | | `/me` | | Send CTCP ACTION to current channel | | `/whois` | | Query user information | | `/ctcp` | | Send arbitrary CTCP query | | `/window`| `/w` | Switch to window by index | | `/wc` | `/close` | Close window (parts if it's a | | | | channel) | | `/clear` | | Clear current window's text | | `/names` | `/n` | Show nicklist for current window | | `/mode` | | Set channel/user mode | | `/op` | | Give operator status in current chan | | `/kick` | | Kick user from channel | | `/ban` | | Ban user in current channel | | `/topic` | `/t` | Query or set channel topic | | `/help` | `/h` | Show command summary in window | Tab completion -------------- The `Tab` key triggers `input_tab()`, which has two modes: 1. **Command completion**: If the cursor is at the start of the input and the first character is `/`, Amanda cycles through a built-in list of command names matching the current prefix. 2. **Nick completion**: Otherwise, Amanda searches the current window's nicklist for matching nicks. For PM windows, the remote nick and your own nick are also candidates. If the completed word is at the start of the line, a colon and space are appended (`nick: `). Cycling wraps around. Pressing Tab repeatedly rotates through all matching candidates. Command history --------------- Up to 64 commands are kept in a ring buffer. The Up and Down arrows navigate the history (most recent first). When the input line is empty, Page Up / Page Down also browse history instead of scrolling the window — this lets you quickly recall past commands without leaving the input area. The history is not persisted between sessions. Paste detection --------------- Amanda implements a simple heuristic to detect bulk pastes: if two or more lines are entered within 300ms of each other (tracked via `CLOCK_MONOTONIC`), the lines are accumulated into a paste buffer and the user is prompted: > Paste 3 lines? (y/n) Pressing `y` sends all lines as if typed individually. Pressing `n` or `Esc` discards them. This prevents accidentally flooding a channel with a multi-line paste. Key bindings ------------ | Key | Action | |-----------------|---------------------------------------| | Ctrl+N | Next window | | Ctrl+P | Previous window | | Ctrl+L | Clear current window | | Ctrl+C | Quit (sends QUIT, then exits) | | Ctrl+D | Delete character under cursor | | Ctrl+U | Clear input line | | Backspace | Delete character before cursor | | Delete | Delete character at cursor | | Left / Right | Move cursor | | Home | Scroll to top of window | | End | Scroll to bottom of window | | Page Up | Scroll up / browse history (on empty) | | Page Down | Scroll down / browse history (on empty)| | Up / Down | Browse command history | | Tab | Command / nick completion | | Alt+Backspace | Delete word backwards | | Alt+B | Jump one word backwards | | Alt+D | Delete next word | | Enter | Send input line | Usage and arguments ------------------- Positional arguments: ./amanda [server] [+port] [nick] Flags: -server host IRC server (default: irc.libera.chat) -port p Port (default: 6667) -nick n Nickname (default: tiny<PID>) -pass p Server password (PASS command) -ssl Enable TLS -noverify Skip TLS certificate verification -h / --help Show usage TLS is automatically enabled for ports 6697 and 7000. Port numbers prefixed with `+` (e.g., `+6697`) also enable TLS. When no nick is provided, a default is generated from `tiny` and the last four digits of the PID (e.g., `tiny1234`). Implementation notes -------------------- - **Ring buffers**: The line buffer (`LineBuf`) uses a fixed-capacity circular buffer. Adding a line beyond capacity overwrites the oldest line. The nicklist is a dynamic array that doubles when full. - **Window flags**: Each window has a `flag` field that is set to 1 when new output arrives while the window is not active or is scrolled away. The status bar shows a `!` next to flagged windows. - **Clear**: `/clear` does not destroy lines — it sets a `clear_at` marker to the current line count. The display logic skips lines before this marker, creating the visual effect of a cleared window. - **UTF-8**: The input handler reads multi-byte UTF-8 sequences and stores them atomically. Backspace and cursor movement respect UTF-8 continuation bytes (0x80–0xBF) to avoid breaking character encoding. - **Scroll**: Each window has an independent scroll offset. Scrolling backwards past the buffer's oldest line clamps to the top. Home and End jump to extreme positions. - **MOTD**: The MOTD is displayed line-by-line in the status window. Because only numerics 372 (MOTD body) and 375 (MOTD start) carry the message text, and 376/422 (end of MOTD) produce a termination line, the MOTD appears naturally in the scrollback. - **Nick mode prefixes**: Nicknames may carry mode prefixes (`@`, `+`, `%`, `~`, `&`). These are stripped for comparison in `nick_raw()` but preserved in the stored nicklist so the display shows modes. - **WHOIS**: Five numerics are parsed (311, 312, 319, 671, 318) and printed with `-WHOIS-` prefix. The target nick's PM window is used if it exists; otherwise output goes to status. Building and dependencies ------------------------- The sole build command: gcc -o amanda amanda.c -lssl -lcrypto OpenSSL development headers are required (`libssl-dev` on Debian, `openssl-devel` on Fedora, etc.). Without them, the TLS symbols (`SSL_*`, `SSL_CTX_*`, etc.) will cause linker errors. There is no `configure`, `makefile`, `cmake`, `meson`, or any other build infrastructure. There are no tests, no CI configuration, and no package definition files. The binary is listed in `.gitignore`. Security -------- - TLS certificate verification is enabled by default via `SSL_VERIFY_PEER` with hostname matching (`SSL_set1_host`). The system CA store is loaded with `SSL_CTX_set_default_verify_paths`. - The `-noverify` flag disables all certificate checks. This provides encryption but **no authentication** — a man-in-the-middle can trivially intercept the connection. Only use this on trusted networks (e.g., a LAN you control) or for testing. - No passwords are logged or stored beyond the lifetime of the `pass` field in the `Session` struct. - The terminal is restored to cooked mode on exit via `atexit()`.