siggy 1.8.0

Terminal-based Signal messenger client with vim keybindings
Documentation
# Module Reference

siggy is organized into a flat module structure under `src/`.

## Module dependency graph

```mermaid
graph TD
    MAIN["main.rs<br/><i>entry point + event loop</i>"]
    APP["app.rs<br/><i>all application state</i>"]
    UI["ui.rs<br/><i>stateless rendering</i>"]
    CLIENT["signal/client.rs<br/><i>signal-cli process</i>"]
    TYPES["signal/types.rs<br/><i>shared types</i>"]
    DB["db.rs<br/><i>SQLite persistence</i>"]
    CONFIG["config.rs<br/><i>TOML config</i>"]
    INPUT["input.rs<br/><i>command parsing</i>"]
    SETUP["setup.rs<br/><i>first-run wizard</i>"]
    LINK["link.rs<br/><i>device linking</i>"]

    MAIN --> APP
    MAIN --> UI
    MAIN --> CLIENT
    MAIN --> CONFIG
    MAIN --> SETUP
    MAIN --> DB
    SETUP --> LINK
    APP --> DB
    APP --> TYPES
    APP --> INPUT
    APP --> CONFIG
    CLIENT --> TYPES
    UI --> APP
```

## Source files

### `main.rs`

Entry point. Parses CLI arguments, runs the setup wizard if needed, opens the
database, spawns signal-cli, and runs the main event loop. Orchestrates the
startup sequence: setup wizard -> device linking -> app startup.

The event loop polls keyboard input (50ms timeout), drains signal events from
the mpsc channel, and renders each frame with `ui::draw()`.

### `app.rs`

All application state lives in the `App` struct. Owns conversations (stored in
a `HashMap` with an ordered `Vec` for sidebar ordering), the input buffer, and
the current mode (Normal / Insert).

Key entry point: `handle_signal_event()` processes all backend events -- incoming
messages, typing indicators, contact lists, group lists, and errors. This is the
single place where signal-cli events modify application state.

`get_or_create_conversation()` is the single point for ensuring a conversation
exists. It upserts to both the in-memory `HashMap` and SQLite. New conversations
append to `conversation_order`; existing ones are no-ops.

### `signal/client.rs`

Spawns the signal-cli child process and manages communication. Two Tokio tasks:

- **stdout reader** -- reads lines from signal-cli stdout, parses JSON-RPC into
  `SignalEvent` variants, and sends them through the mpsc channel
- **stdin writer** -- receives `JsonRpcRequest` structs and writes them as JSON
  lines to signal-cli stdin

The `pending_requests` map tracks RPC call IDs to correlate responses with their
original method (e.g., mapping a response ID back to `listContacts`).

### `signal/types.rs`

Shared types for signal-cli communication:

- `SignalEvent` -- enum of all events the backend can produce (messages, receipts, typing, read sync, system messages)
- `SignalMessage` -- a message with source, timestamp, body, attachments, group info, text styles
- `TextStyle` / `StyleType` -- text formatting ranges (bold, italic, strikethrough, monospace, spoiler)
- `Attachment` -- file metadata (content type, filename, local path)
- `JsonRpcRequest` / `JsonRpcResponse` -- JSON-RPC protocol structs
- `Contact` / `Group` -- address book and group info

### `ui.rs`

Stateless rendering. The `draw()` function takes an immutable `&App` reference and
renders the full UI: sidebar, chat area, input bar, and status bar.

Sender colors are hash-based (8 colors). Groups are prefixed with `#` in the sidebar.
OSC 8 hyperlinks are injected in a post-render pass (written directly to the terminal
after Ratatui's draw to avoid width calculation issues).

### `db.rs`

SQLite database layer with WAL mode. Four tables: `conversations`, `messages`,
`read_markers`, `reactions`. Schema migration is version-based (currently at v9,
see [Database Schema](database.md)).

Provides `open()` for disk-backed storage and `open_in_memory()` for incognito mode.

### `config.rs`

TOML configuration. The `Config` struct is serialized/deserialized with serde.
Fields: `account`, `signal_cli_path`, `download_dir`, `notify_direct`,
`notify_group`, `desktop_notifications`, `inline_images`, `native_images`,
`show_receipts`, `color_receipts`, `nerd_fonts`, `reaction_verbose`,
`send_read_receipts`, `mouse_enabled`, `theme`. All fields have defaults.

`Config::load()` reads from the platform-specific path (or a custom path).
`Config::save()` writes the current config back to disk.

### `input.rs`

Input parsing. Converts text input into an `InputAction` enum. Handles all
slash commands (`/join`, `/part`, `/quit`, `/sidebar`, `/bell`, `/mute`,
`/block`, `/unblock`, `/attach`, `/paste`, `/search`, `/contacts`, `/settings`,
`/disappearing`, `/group`, `/theme`, `/poll`, `/verify`, `/profile`,
`/about`, `/help`) and their aliases.

Also defines `CommandInfo` and the `COMMANDS` constant used for autocomplete.

### `setup.rs`

Multi-step first-run wizard. Handles signal-cli detection (searching PATH),
phone number input with validation, and triggers the device linking flow.

### `link.rs`

Device linking flow. Runs signal-cli's `link` command, captures the QR code URI,
renders it in the terminal, and waits for the user to scan it with their phone.
Checks for successful account registration afterward.