Keyboard shortcuts

Press or to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

Introduction

siggy

A terminal-based Signal messenger client with an IRC aesthetic.

siggy screenshot

siggy wraps signal-cli via JSON-RPC, giving you a full-featured messaging interface that runs entirely in your terminal. Built with Ratatui, Crossterm, and Tokio.

Why siggy?

  • Lightweight – no Electron, no web browser, just your terminal
  • Vim keybindings – modal editing with Normal and Insert modes
  • Persistent – SQLite-backed message history that survives restarts
  • Private – incognito mode for ephemeral sessions with zero disk traces
  • Extensible – TOML configuration, slash commands, and a clean module architecture

Quick start

From crates.io:

cargo install siggy

Linux / macOS:

curl -fsSL https://raw.githubusercontent.com/johnsideserf/siggy/master/install.sh | bash

Windows (PowerShell):

irm https://raw.githubusercontent.com/johnsideserf/siggy/master/install.ps1 | iex

Then launch:

siggy

The setup wizard will guide you through linking your Signal account on first launch.

Try it without Signal

siggy --demo

Demo mode populates the UI with dummy conversations and messages so you can explore the interface without a Signal account or signal-cli installed.

License

GPL-3.0

Changelog

v1.3.3

Bug fixes

  • Kitty image ghosting – images that scrolled out of view persisted on screen. Now deletes all Kitty placements before each redraw (#180)
  • Kitty image stretching – partially visible images were scaled into fewer rows instead of being cropped. Now uses Kitty source-rect params to crop at the pixel level (#180)
  • Kitty image flickering – images were deleted and re-transmitted every frame even when unchanged. Now tracks the previous frame’s visible images and skips redundant redraws (#180)
  • Native image cache consistency – the first visible height would win in the cache, producing wrong results at different sizes. Images are now always encoded at full dimensions and cropped at display time (#180)

Enhancements

  • Synchronized updates – the entire render cycle (clear + text draw + image overlay) is now wrapped in a synchronized update, eliminating flash on conversation switch and during scroll (#180)
  • Suppressed Kitty responses – added q=2 to Kitty graphics commands to prevent response bytes from leaking into stdin (#180)

Thanks to @Dowsley for this release.


v1.3.2

Bug fixes

  • Cursor blink fix – the event loop was redrawing every 50ms regardless of state changes, resetting the terminal’s cursor blink timer. Now frames are only drawn when state actually changes (#177)

Enhancements

  • Proxy config – added a proxy field to config.toml for Signal TLS proxy URLs, passed through to signal-cli as --proxy. Useful for connecting in censored regions (#178)

Developer

  • Fuzz testing – added cargo-fuzz harnesses for JSON-RPC parsing, UTF-8 cursor operations, keybinding parsing, and slash command parsing (#179)

v1.3.1

Bug fixes

  • Image attachments – fixed image attachments not rendering after restart on Unix (#175)
  • Quit aliases – added :q and :quit as aliases for /quit (#174)

v1.3.0

Bug fixes

  • Unicode input crash – typing accented characters (á, ã, ó, é) caused a panic due to cursor arithmetic assuming single-byte characters. All cursor movement, insertion, and deletion now correctly handle multi-byte UTF-8 characters (#166)

Security

  • SQLite secure_delete – added PRAGMA secure_delete = ON so deleted message content is zeroed in database free pages rather than left recoverable (#161)
  • Debug log PII redaction--debug now masks phone numbers and message bodies in log output. Use --debug-full for unredacted output when needed (#163)
  • Security documentation – added a comprehensive security page to the docsite covering trust model, credential storage, encryption at rest, and privacy features (#164)

Enhancements

  • Notification preview in settings – the notification_preview setting (“full”, “sender”, “minimal”) is now accessible as a cycle toggle in the /settings overlay (#162)

v1.2.2

Bug fixes

  • Uppercase keybindings broken – Shift+J/K (focus next/prev message), G (scroll to bottom), N (prev search), Y (copy all), and other uppercase bindings stopped working after the keybindings refactor. crossterm sends uppercase chars with a SHIFT modifier that the lookup didn’t account for
  • j/k scroll snapping back – pressing j/k to scroll would briefly shift the viewport then immediately snap back to the bottom. The draw code was deriving a focus index that triggered a “keep focused message visible” adjustment on the next frame

v1.2.1

Bug fixes

  • Autocomplete popup crash – fixed a panic when opening the autocomplete popup (/ commands, @ mentions) in a small terminal window. The popup now clamps to available space and skips rendering if the terminal is too small

v1.2.0

New features

  • Configurable keybindings – keybindings are now fully configurable via three built-in profiles (Default, Emacs, Minimal), custom TOML profiles in ~/.config/siggy/keybindings/, and per-key overrides in ~/.config/siggy/keybindings.toml. Switch profiles and rebind keys live in the new /keybindings overlay. The help overlay dynamically reflects active bindings (#138)
  • Message history pagination – scrolling to the top of a conversation automatically loads older messages from the database, with a loading indicator while fetching (#158)
  • Multi-line message input – press Alt+Enter (or Shift+Enter) to insert newlines in your message before sending (#157)

v1.1.0

New features

  • Forward messages – press f in Normal mode to forward a message to another conversation via a filterable picker overlay (#139)
  • Scroll position memory – switching between conversations now remembers and restores your scroll position (#137)
  • Notification preview levels – new notification_preview config option with three levels: "full" (default), "sender", or "minimal" (#132)
  • Clipboard auto-clear – clipboard is automatically cleared 30 seconds after copying a message. Configurable via clipboard_clear_seconds (#131)
  • Tree-style connectors – quotes and link previews now use curved Unicode box-drawing characters (, , ) for visual clarity (#141)

Security

  • Identity verification fix/verify now uses verifiedSafetyNumber instead of trustAllKnownKeys, requiring explicit safety number confirmation before trusting a contact’s identity key (#144)
  • Secure debug log – debug log moved from CWD to ~/.cache/siggy/debug.log with 10MB rotation and a startup warning (#134)
  • Incognito attachment isolation--incognito mode now redirects attachment downloads to a temp directory that is auto-deleted on exit (#133)

v1.0.1

Security

  • OSC 8 escape injection fix – URLs in messages are now sanitized before being embedded in terminal hyperlink escape sequences, preventing crafted URLs from manipulating terminal state (title, colors, screen)
  • Attachment path traversal fix – attachment filenames from signal-cli are now sanitized by replacing path separators and .. traversal sequences, preventing writes outside the configured download directory

v1.0.0

Rename to siggy

  • Renamed from signal-tui to siggy – the binary, package, config paths, data paths, and database filename are all now “siggy” (#127)
  • Automatic migration – existing config directories, data directories, and database files are seamlessly migrated from the old “signal-tui” paths on first launch. No manual action required
  • Published to crates.io – install with cargo install siggy (closes #11)

Docsite

  • Brand theme – docsite color palette updated from gray mIRC to siggy’s navy-blue brand colors in both light and dark modes (#128)
  • Logo integration – siggy logo and favicon added to the docsite intro page and menu bar

Repo hygiene

  • Cargo.lock tracked – binary crate now correctly tracks its lockfile
  • .gitignore cleanup – added IDE directories and platform artifacts

v0.9.0

Pinned messages

  • Pin and unpin messages – press p in Normal mode or use the action menu to pin a message. Choose a pin duration (forever, 24h, 7d, 30d). Pinned messages show a banner at the top of the chat area. Unpin by pressing p again. Pin state syncs across devices (closes #65)
  • URL preview display – messages containing URLs now show link preview cards with title, description, and thumbnail image (when available). Toggle via /settings > “Link previews” (closes #63)

Polls

  • Create polls – use /poll "question" "opt1" "opt2" to create a poll. Add --single to restrict to single-select. Polls display as inline bar charts showing vote counts and percentages
  • Vote in polls – press Enter on a poll message to open the vote overlay. Select options with Space, confirm with Enter. Multi-select polls allow toggling multiple options (closes #64)

Identity verification

  • /verify command – verify the identity keys of your contacts. In 1:1 chats, shows the safety number and trust level. In groups, browse members and verify individually. Trust/untrust identity keys directly from the overlay (closes #70)

Profile editor

  • /profile command – edit your Signal profile directly from the TUI. Change your given name, family name, about text, and about emoji. Navigate with j/k, Enter to edit fields inline, and Save to push changes via updateProfile RPC (closes #69)

About overlay

  • /about command – shows app version, description, author, license, and repository link. Press any key to close
  • Left/right sidebar – new setting to place the sidebar on the right side instead of the default left. Toggle via /settings > “Sidebar on right” (closes #125)

Bug fixes

  • Mouse selection – fixed mouse click positioning in the input bar, right-click paste, and slow Ctrl+V behavior (#124)
  • Poll vote counting – votes now correctly use vote_count as a multiplier instead of always counting as 1 (#122)
  • Mention parsing – fixed mention field names to match signal-cli’s actual protocol (#108)

Internal

  • Test coverage – added unit tests for UI helpers and event handlers, migrated to rstest parameterized tests (#109, #113, #120)
  • Robustness – removed unsafe unwraps, surfaced DB errors in status bar, used binary search for message insertion (#118, #119)

v0.8.0

Disappearing messages

  • Timer support – siggy now honors disappearing message timers. Messages auto-expire after the configured duration, with a countdown shown in the chat area. Set the timer with /disappearing <duration> (alias /dm) using values like 30s, 5m, 1h, 1d, 1w, or off (closes #61)

Group management

  • /group command – manage groups directly from the TUI (alias /g). Opens a menu with options to view members, add/remove members, rename the group, create a new group, or leave a group. Add/remove members use a type-to-filter contact picker (closes #26)

Message requests

  • Unknown sender detection – messages from unknown senders (not in your contacts) are now flagged as message requests. A banner appears with options to accept (start chatting) or delete the conversation. Unaccepted conversations do not trigger notifications or send read receipts (closes #62)

Block and unblock

  • /block and /unblock commands – block or unblock the current conversation’s contact or group. Blocked conversations do not trigger notifications, read receipts, or typing indicators (closes #60)

Mouse support

  • Clickable sidebar – click conversations in the sidebar to switch
  • Scrollable messages – scroll wheel in the chat area
  • Overlay navigation – scroll wheel navigates lists in overlays
  • Click to position cursor – click in the input bar to place the cursor
  • Configurable via /settings > “Mouse support” (default: on) (closes #17)

Color themes

  • Selectable themes – open the theme picker with /theme (alias /t) or from /settings > Theme. Includes built-in themes with customizable sidebar, chat, status bar, and accent colors (closes #18)

Desktop notifications

  • OS-level notifications – cross-platform desktop notifications using notify-rust (Linux D-Bus, macOS NSNotification, Windows WinRT toast). Shows sender name and message preview. Toggle via /settings > “Desktop notifications” (default: off) (closes #19)

Bug fixes

  • Mouse capture on Windows – mouse support no longer breaks after signal-cli starts on Windows. Spawning signal-cli.bat (cmd.exe) was resetting console input mode flags (#105)

Database

  • Migration v7 – adds expiration_timer to conversations and expires_in_seconds, expiration_start_ms to messages (disappearing messages)
  • Migration v8 – adds accepted column to conversations (message requests)
  • Migration v9 – adds blocked column to conversations (block/unblock)

v0.7.0

Text styling

  • Rich text rendering – messages with Signal formatting now display with proper styling: bold, italic, strikethrough, monospace, and spoiler text. Spoiler content is hidden behind block characters (closes #66)

Sticker messages

  • Sticker display – incoming stickers are now shown as [Sticker: emoji] in the chat area instead of being silently dropped (closes #67)

View-once messages

  • View-once handling – view-once messages display as [View-once message] with attachments suppressed, respecting the ephemeral intent (closes #68)

Cross-device read sync

  • Read state sync – when you read messages on your phone or another linked device, siggy marks those conversations as read and updates unread counts automatically (closes #71)

System messages

  • Missed calls – missed voice and video calls now show as system messages
  • Safety number changes – a warning appears when a contact’s safety number changes
  • Group updates – group metadata changes (member adds/removes) display as system messages
  • Disappearing message timer – changes to the expiration timer show a human-readable message (e.g. “Disappearing messages set to 1 day”)

Message action menu

  • Enter key menu – press Enter in Normal mode on a focused message to open a contextual action menu. Available actions (shown with key hints): Reply (q), Edit (e), React (r), Copy (y), Delete (d). Navigate with j/k, press Enter to execute, or use the shortcut key directly (closes #85)

Bug fixes

  • “New messages” bar – the unread separator no longer persists after viewing a conversation with new messages (#90)

v0.6.1

Bug fixes

  • j/k scroll fixed – viewport no longer gets stuck when scrolling with j/k. The root cause was the message window expanding in lockstep with scroll offset, keeping the viewport position constant (#84)
  • J/K navigation in short conversationsJ/K message jumping now works even when all messages fit the viewport (no scroll offset needed) (#84)
  • Edit preserves quotes – editing a quoted message no longer strips the original quote on remote clients. The wire-format phone number is now preserved through display name resolution (#84)
  • Contact names no longer revert to phone numbers – conversations would permanently show phone numbers in the sidebar when messages arrived before the contact list synced. Fixed by preventing phone-number fallback names from overwriting real display names in the database (#84)
  • Contact name recovery on startup – 1:1 conversations still named as phone numbers (e.g. when signal-cli’s contact list has no cached profile name) now recover the correct name from stored message sender fields (#86)
  • Reaction sender names after reload – reaction senders no longer revert to phone numbers after restarting the app (#80)
  • Non-contact name resolution – display names for non-contacts in reactions and quotes are now resolved correctly (#83)
  • Mention placeholders in quotes – U+FFFC placeholder characters from @mentions are now stripped from quoted text (#79)

Improvements

  • Loading screen – a loading indicator now appears during startup while contacts and groups sync from signal-cli (#81, #82)
  • Install scripts updated – Windows and macOS install scripts now reference Java 25+ (required by signal-cli 0.14). The Windows script checks the actual Java version before installing signal-cli (#87)

v0.6.0

Reply, edit, and delete messages

  • Quote reply – press q in Normal mode on any message to reply with a quote. A reply indicator appears above the input box, and the sent message includes a quoted block showing the original author and text (closes #15)
  • Edit messages – press e on your own outgoing message to edit it. The original text is loaded into the input buffer for modification. Edited messages display an “(edited)” label. Edits sync across devices (closes #24)
  • Delete messages – press d on any message to open a delete confirmation. Outgoing messages offer “delete for everyone” (remote delete) or “delete locally”. Incoming messages can be deleted locally. Deleted messages show as “[deleted]” (closes #23)
  • /search command – search across all conversations with /search <query> (alias: /s). Results appear in a scrollable overlay showing sender, message snippet, and conversation name. Press Enter to jump directly to the message in context. Use n/N in Normal mode to cycle through matches (closes #14)
  • Highlight matches – search terms are highlighted in the result snippets

File attachments

  • /attach command – send files with /attach to open a file browser overlay. Navigate with j/k, Enter to select, Backspace to go up a directory. The selected file attaches to your next message, shown as a pending indicator in the input area (closes #54)

/join autocomplete

  • Contact and group autocomplete/join now offers Tab-completable suggestions from your contacts and groups. Type /join and see matching names, or keep typing to filter. Groups and contacts are distinguished by color (closes #21)

Send typing indicators

  • Outbound typing – siggy now sends typing indicators to your conversation partner while you type. Typing state starts on the first keypress, auto-stops after 5 seconds of inactivity, and stops immediately when you send or switch conversations (closes #58)

Send read receipts

  • Read receipt sending – when you view a conversation, read receipts are automatically sent to message senders, letting them know you’ve read their messages. Controlled by the “Send read receipts” toggle in /settings (closes #59)

Welcome screen

  • Getting started hints – the welcome screen now shows useful commands and navigation tips including Tab/Shift+Tab for cycling conversations

Bug fixes

  • Out-of-order messages – messages with delayed delivery timestamps are now inserted in correct chronological order (#56)
  • Link highlight – fixed background color bleeding on highlighted links and J/K message navigation edge cases (#55)

Database

  • Migration v5 – adds index on messages(conversation_id, timestamp_ms) for faster search queries
  • Migration v6 – adds is_edited, is_deleted, quote_author, quote_body, quote_ts_ms, and sender_id columns to the messages table

v0.5.0

Message reactions

  • Emoji reactions – react to any message with r in Normal mode to open the reaction picker. Navigate with h/l or 1-8, press Enter to send. Reactions display below messages as compact emoji badges (e.g. 👍 2 ❤️ 1) with an optional verbose mode showing sender names (closes #16)
  • Reaction sync – incoming reactions, sync reactions from other devices, and reaction removals are all handled in real time
  • Persistence – reactions are stored in the database (migration v4) and restored on startup

@mentions

  • Mention autocomplete – type @ in group chats to open a member autocomplete popup. Filter by name, press Tab to insert the mention. Works in 1:1 chats too (with the conversation partner)
  • Mention display – incoming mentions are highlighted in cyan+bold in the chat area

Visible message selection

  • Focus highlight – when scrolling in Normal mode, the focused message gets a subtle dark background highlight so you can see exactly which message reactions and copy will target
  • J/K navigation – Shift+j and Shift+k jump between actual messages, skipping date separators and system messages

Startup error handling

  • stderr capture – signal-cli startup errors (missing Java, bad config, etc.) are now captured and displayed in a TUI error screen instead of silently failing

Internal

  • Major refactoring across four PRs (#45-#48): extracted shared key handlers, data-driven settings system, split parse_receive_event into sub-functions, modernized test helpers, added persistent debug log and pending_requests TTL

v0.4.0

Contact list

  • /contacts command – new overlay for browsing all synced contacts, with j/k navigation, type-to-filter by name or number, and Enter to open a conversation (alias: /c) (closes #22)

Clipboard

  • Copy to clipboard – in Normal mode, y copies the selected message body and Y copies the full formatted line ([HH:MM] <sender> body) to the system clipboard (closes #28)
  • Full timestamp on scroll – when scrolling through messages in Normal mode, the status bar now shows the full date and time of the focused message (e.g. “Sun Mar 01, 2026 12:34:56 PM”) (closes #27)

v0.3.3

Bug fixes

  • Settings persistence – changes made in /settings are now saved to the config file and persist between sessions (fixes #40)
  • Input box scrolling – long messages no longer disappear when typing past the edge of the input box; text now scrolls horizontally to keep the cursor visible (fixes #39)
  • Image preview refresh – toggling “Inline image previews” in /settings now immediately re-renders or clears previews on existing messages (fixes #41)

Settings

  • Tab to toggle – Tab key now toggles settings items in the /settings overlay, alongside Space and Enter

v0.3.2

Read receipts and delivery status

  • Message status indicators – outgoing messages now show delivery lifecycle symbols: Sending → Sent → Delivered → Read → Viewed
  • Real-time updates – status symbols update live as recipients receive and read your messages
  • Group receipt support – delivery and read receipts work correctly in group conversations
  • Race condition handling – receipts that arrive before the server confirms the send are buffered and replayed automatically
  • Persistent status – message status is stored in the database and restored on reload (stale “Sending” messages are promoted to “Sent”)
  • Nerd Font icons – optional Nerd Font glyphs available via /settings > “Nerd Font icons”
  • Configurable – three new settings toggles: “Show read receipts” (on/off), “Receipt colors” (colored/monochrome), “Nerd Font icons” (unicode/nerd)

Debug logging

  • --debug flag – opt-in protocol logging to siggy-debug.log for diagnosing signal-cli communication issues

Database

  • Migration v3 – adds status and timestamp_ms columns to the messages table (automatic on first run)

v0.3.1

Image attachments

  • Embedded file links – attachment URIs are now hidden behind clickable bracket text (e.g. [image: photo.jpg]) instead of showing the raw file:/// path
  • Double extension fix – filenames like photo.jpg.jpg are stripped to photo.jpg when signal-cli duplicates the extension
  • Improved halfblock previews – increased height cap from 20 to 30 cell-rows for better inline image quality
  • Native image protocols – experimental support for Kitty and iTerm2 inline image rendering, off by default. Enable via /settings > “Native images (experimental)”
  • Pre-resized encoding – native protocol images are resized and cached as PNG before sending to the terminal, avoiding multi-megabyte raw file transfers every frame

Attachment lookup

  • MSYS/WSL path fixfind_signal_cli_attachment now checks both platform-native data dirs (AppData/Roaming) and POSIX-style (~/.local/share) where signal-cli stores files under MSYS or WSL. Fixes outgoing images sent from Signal desktop not displaying in the TUI.

Platform

  • Windows Ctrl+C fix – suppress the STATUS_CONTROL_C_EXIT error on exit by disabling the default Windows console handler (crossterm already captures Ctrl+C as a key event in raw mode)

Documentation

  • mdBook documentation site with custom mIRC/Win95 light theme and dark mode toggle

v0.3.0

Initial public release.

  • Terminal Signal client wrapping signal-cli via JSON-RPC
  • Vim-style modal input (Normal/Insert modes)
  • Sidebar with conversation list, unread counts, typing indicators
  • Inline halfblock image previews
  • OSC 8 clickable hyperlinks
  • SQLite persistence with WAL mode
  • Incognito mode (--incognito)
  • Demo mode (--demo)
  • First-run setup wizard with QR device linking
  • Slash commands: /join, /part, /quit, /sidebar, /help, /settings, /mute, /notify, /bell
  • Input history (Up/Down recall)
  • Autocomplete popup for commands and @mentions
  • Configurable notifications (direct/group) with terminal bell
  • Cross-platform: Linux, macOS, Windows

Installation

From crates.io

Requires Rust 1.70+.

cargo install siggy

Pre-built binaries

Download the latest release for your platform from the Releases page.

Linux / macOS (one-liner)

curl -fsSL https://raw.githubusercontent.com/johnsideserf/siggy/master/install.sh | bash

Windows (PowerShell)

irm https://raw.githubusercontent.com/johnsideserf/siggy/master/install.ps1 | iex

Both install scripts download the latest release binary and check for signal-cli.

Build from source

Requires Rust 1.70+.

Install directly from the repository:

cargo install --git https://github.com/johnsideserf/siggy.git

Or clone and build locally:

git clone https://github.com/johnsideserf/siggy.git
cd siggy
cargo build --release
# Binary is at target/release/siggy

signal-cli setup

siggy requires signal-cli as its messaging backend.

  1. Install signal-cli – follow the signal-cli installation guide. The install scripts above will check for it automatically.

  2. Make it accessible – signal-cli must be on your PATH, or you can set the full path in the config file:

    signal_cli_path = "/usr/local/bin/signal-cli"
    

    On Windows, point to signal-cli.bat if it isn’t in your PATH.

  3. Java runtime – signal-cli 0.14+ requires Java 25+. Make sure java is available in your shell. On Linux, the install script uses the native signal-cli build which does not require Java.

Supported platforms

PlatformBinaryNotes
Linux x86_64siggy-vX.Y.Z-x86_64-unknown-linux-gnu.tar.gz
macOS x86_64siggy-vX.Y.Z-x86_64-apple-darwin.tar.gzIntel Macs
macOS arm64siggy-vX.Y.Z-aarch64-apple-darwin.tar.gzApple Silicon
Windows x86_64siggy-vX.Y.Z-x86_64-pc-windows-msvc.zip

Getting Started

First launch

Run siggy with no arguments:

siggy

If no config file exists, the setup wizard starts automatically.

Setup wizard

The wizard walks through three steps:

  1. Locate signal-cli – siggy searches your PATH for signal-cli. If it can’t find it, you’ll be prompted to enter the full path.

  2. Enter your phone number – provide your Signal phone number in E.164 format (e.g. +15551234567). This is the account siggy will connect to.

  3. Link your device – a QR code is displayed in the terminal. Scan it with the Signal app on your phone:

    • Open Signal on your phone
    • Go to Settings > Linked Devices > Link New Device
    • Scan the QR code shown in the terminal

Once linked, siggy saves your config and starts the main interface.

Re-running setup

To re-run the setup wizard at any time:

siggy --setup

This is useful if you need to link a different account or reconfigure signal-cli.

Demo mode

Try the full UI without a Signal account or signal-cli:

siggy --demo

Demo mode populates the interface with dummy conversations and messages. It’s useful for exploring keybindings, commands, and the layout before committing to setup.

CLI options

FlagDescription
-a, --account <NUMBER>Phone number in E.164 format (overrides config)
-c, --config <PATH>Path to a custom config file
--setupRe-run the first-time setup wizard
--demoLaunch with dummy data (no signal-cli needed)
--incognitoIn-memory storage only; nothing persists after exit

Basic navigation

Once launched, the interface has three areas:

  • Sidebar (left) – lists your conversations; groups are prefixed with #
  • Chat area (center) – shows messages for the selected conversation
  • Input bar (bottom) – type messages and commands here

Use Tab / Shift+Tab to switch between conversations, or type /join <name> to jump to a specific contact or group.

Press Esc to enter Normal mode for vim-style scrolling and navigation. The default mode is Insert, where you can type messages immediately.

Configuration

Config file location

siggy loads its config from a TOML file at the platform-specific path:

PlatformPath
Linux / macOS~/.config/siggy/config.toml
Windows%APPDATA%\siggy\config.toml

You can override the path with the -c flag:

siggy -c /path/to/config.toml

Config fields

All fields are optional. Here is a complete example with defaults:

account = "+15551234567"
signal_cli_path = "signal-cli"
download_dir = "/home/user/signal-downloads"
notify_direct = true
notify_group = true
desktop_notifications = false
inline_images = true
native_images = false
show_receipts = true
color_receipts = true
nerd_fonts = false
reaction_verbose = false
send_read_receipts = true
mouse_enabled = true
theme = "Default"
keybinding_profile = "Default"
proxy = ""

Field reference

FieldTypeDefaultDescription
accountstring""Phone number in E.164 format
signal_cli_pathstring"signal-cli"Path to the signal-cli binary
download_dirstring~/signal-downloads/Directory for downloaded attachments
notify_directbooltrueTerminal bell on new direct messages
notify_groupbooltrueTerminal bell on new group messages
desktop_notificationsboolfalseOS-level desktop notifications for incoming messages
inline_imagesbooltrueRender image attachments as halfblock art
native_imagesboolfalseUse native terminal image protocols (Kitty/iTerm2)
show_receiptsbooltrueShow delivery/read receipt status symbols
color_receiptsbooltrueColored receipt status symbols (vs monochrome)
nerd_fontsboolfalseUse Nerd Font glyphs for status symbols
reaction_verboseboolfalseShow reaction sender names instead of counts
send_read_receiptsbooltrueSend read receipts when viewing conversations
mouse_enabledbooltrueEnable mouse support (click sidebar, scroll, etc.)
themestring"Default"Color theme name
keybinding_profilestring"Default"Keybinding profile (Default, Emacs, Minimal, or custom)
proxystring""Signal TLS proxy URL passed through to signal-cli

CLI flags

CLI flags override config file values for the current session:

FlagOverrides
-a +15551234567account
-c /path/to/config.tomlConfig file path
--incognitoUses in-memory database (no persistence)

Settings overlay

Settings overlay

Press /settings inside the app to open the settings overlay. This provides toggles for runtime settings:

  • Notification toggles (direct / group / desktop)
  • Sidebar visibility
  • Inline image previews / native images
  • Show read receipts / receipt colors / nerd font icons
  • Verbose reactions
  • Send read receipts
  • Mouse support
  • Theme selector
  • Keybinding profile selector

Changes made in the settings overlay are saved to the config file when you close the overlay, and persist across sessions.

Incognito mode

siggy --incognito

Incognito mode replaces the on-disk SQLite database with an in-memory database. No messages, conversations, or read markers are saved. When you exit, all data is gone. The status bar shows a bold magenta incognito indicator.

Commands

All commands start with /. Type / in Insert mode to open the autocomplete popup.

Command reference

CommandAliasArgumentsDescription
/join/j<name>Switch to a conversation by contact name, number, or group
/part/pLeave current conversation
/search/s<query>Search messages across all conversations
/attach/aOpen file browser to attach a file
/sidebar/sbToggle sidebar visibility
/bell/notify[type]Toggle notifications (direct, group, or both)
/muteMute/unmute current conversation
/blockBlock current contact or group
/unblockUnblock current contact or group
/disappearing/dm<duration>Set disappearing message timer (off, 30s, 5m, 1h, 1d, 1w)
/group/gOpen group management menu
/theme/tOpen theme picker
/keybindings/kbOpen keybindings overlay
/poll"q" "a" "b" [--single]Create a poll
/verify/vVerify contact identity keys
/profileEdit your Signal profile
/aboutShow app info (version, license, etc.)
/contacts/cBrowse synced contacts
/settingsOpen settings overlay
/help/hShow help overlay
/quit/qExit siggy

Autocomplete

Slash command autocomplete

When you type /, a popup appears showing matching commands. As you continue typing, the list filters down. Use:

  • Up/Down arrows to navigate the list
  • Tab to complete the selected command
  • Esc to dismiss the popup

/join autocomplete

After typing /join , a second autocomplete popup shows matching contacts and groups. Filter by name or phone number. Groups are shown in green. Press Tab to complete the selection.

Examples

Join a conversation by name:

/join Alice

Join by phone number:

/j +15551234567

Toggle direct message notifications off:

/bell direct

Toggle all notifications:

/bell

Mute the current conversation:

/mute

Search for a message:

/search hello

Attach a file:

/attach

This opens a file browser. Navigate with j/k, Enter to select a file or enter a directory, Backspace to go up. The selected file attaches to your next message.

Block the current conversation:

/block

Set disappearing messages to 1 day:

/disappearing 1d

Disable disappearing messages:

/dm off

Open group management:

/group

This opens a menu with options to view members, add/remove members, rename the group, create a new group, or leave. Only available in group conversations (except create, which works anywhere).

Switch color theme:

/theme

Create a poll:

/poll "Lunch?" "Pizza" "Sushi" "Tacos"

Create a single-select poll:

/poll "Best editor?" "Vim" "Emacs" --single

Verify a contact’s identity:

/verify

Edit your Signal profile:

/profile

Navigate fields with j/k, press Enter to edit a field inline, Enter again to confirm (or Esc to cancel). Move to Save and press Enter to push changes.

Show app info:

/about

Messaging a new contact

To start a conversation with someone not in your sidebar, use /join with their phone number in E.164 format:

/join +15551234567

The conversation will appear in your sidebar once the first message is exchanged.

Keybindings

siggy uses vim-style modal editing with two modes: Insert (default) and Normal. All keybindings are configurable via profiles and per-key overrides.

Profiles

Three built-in profiles are available:

ProfileDescription
DefaultVim-style modal editing (Normal / Insert modes)
EmacsNo modal concept; Ctrl-based shortcuts in Insert mode
MinimalArrow-key centric; F-key shortcuts for actions

Set the profile in your config file:

keybinding_profile = "Default"

Or switch profiles live in the app via /keybindings or /settings > Keybindings.

Customizing keybindings

Per-key overrides

Create ~/.config/siggy/keybindings.toml to override individual keys on top of your active profile:

[global]
quit = "ctrl+q"

[normal]
scroll_up = "ctrl+j"
react = "ctrl+r"

[insert]
send_message = "ctrl+enter"

Custom profiles

Create full profiles in ~/.config/siggy/keybindings/myprofile.toml:

name = "My Custom"

[global]
quit = "ctrl+c"
next_conversation = "tab"

[normal]
scroll_up = "k"
scroll_down = "j"

[insert]
exit_insert = "esc"
send_message = "enter"
insert_newline = ["shift+enter", "alt+enter"]

Arrays are supported for binding multiple keys to the same action.

In-app rebinding

Open the keybindings overlay with /keybindings (alias /kb). Navigate actions with j/k, press Enter to capture a new key, Backspace to reset to profile default. Changes are saved automatically.

Default keybindings

The tables below show the Default profile bindings.

Global (both modes)

KeyAction
Ctrl+CQuit
Tab / Shift+TabNext / previous conversation
PgUp / PgDnScroll messages (5 lines)
Ctrl+Left / Ctrl+RightResize sidebar

Normal mode

Press Esc to enter Normal mode. The cursor stops blinking and the mode indicator changes in the status bar.

Scrolling

KeyAction
j / kScroll down / up 1 line
J / KJump to previous / next message
Ctrl+D / Ctrl+UScroll down / up half page
g / GScroll to top / bottom

Actions

KeyAction
yCopy message body to clipboard
YCopy full line ([HH:MM] <sender> body) to clipboard
EnterOpen action menu on focused message
rOpen reaction picker on focused message
qReply to focused message (quote reply)
eEdit own outgoing message
fForward focused message
dDelete focused message
pPin / unpin focused message
nJump to next search result
NJump to previous search result
@Mention autocomplete (in Insert mode)

Cursor movement

KeyAction
h / lMove cursor left / right
w / bWord forward / back
0 / $Start / end of line

Editing

KeyAction
xDelete character at cursor
DDelete from cursor to end of line

Entering Insert mode

KeyAction
iInsert at cursor
aInsert after cursor
IInsert at start of line
AInsert at end of line
oInsert (clear buffer first)
/Insert with / pre-typed (for commands)

Insert mode (default)

Insert mode is the default on startup. You can type messages and commands directly.

KeyAction
EscSwitch to Normal mode
EnterSend message or execute command
Alt+Enter / Shift+EnterInsert newline (multi-line input)
Ctrl+WDelete word back
Backspace / DeleteDelete characters
Up / DownRecall input history
Left / RightMove cursor
Home / EndJump to start / end of line

Mouse

Mouse support is enabled by default (toggle in /settings).

ActionEffect
Click sidebar conversationSwitch to that conversation
Scroll wheel in chatScroll messages up/down
Click in input barPosition cursor
Scroll wheel in overlaysNavigate list items

Help overlay

Help overlay

Press /help (alias /h) to see all keybindings and commands at a glance. The help overlay dynamically reflects your active keybinding profile.

Input history

In Insert mode, press Up and Down to cycle through previously sent messages and commands. History is per-session (not persisted to disk). Your current draft is preserved while browsing history.

Features

Messaging

Send and receive 1:1 and group messages. Messages sent from your phone (or other linked devices) sync into the TUI automatically.

Attachments

  • Images – rendered inline as halfblock art when inline_images = true
  • Native image protocols – for terminals that support Kitty or iTerm2 graphics, enable /settings > “Native images” for higher-fidelity rendering with proper cropping and flicker-free scrolling
  • Other files – shown as [attachment: filename] with the download path
  • Send files – use /attach to open a file browser and attach a file to your next message

Received attachments are saved to the download_dir configured in your config file (default: ~/signal-downloads/).

URLs and file paths in messages are rendered as OSC 8 hyperlinks. In supported terminals (Windows Terminal, iTerm2, Kitty, etc.), you can click them to open in your browser.

Typing indicators

When someone is typing, their name appears below the chat area. Contact name resolution is used where available. siggy also sends typing indicators to your conversation partners while you type, so they can see when you’re composing a message.

Persistence

All conversations, messages, and read markers are stored in a SQLite database with WAL (Write-Ahead Logging) mode for safe concurrent access. Data survives app restarts.

The database is stored alongside the config file:

  • Linux / macOS: ~/.config/siggy/siggy.db
  • Windows: %APPDATA%\siggy\siggy.db

Unread tracking

The sidebar shows unread counts next to each conversation. When you open a conversation, a “new messages” separator line marks where you left off. Read markers persist across restarts.

Notifications

Terminal bell notifications fire when new messages arrive in background conversations. Configure them per type:

  • notify_direct – 1:1 messages (default: on)
  • notify_group – group messages (default: on)
  • desktop_notifications – OS-level desktop notifications (default: off)
  • /mute – per-conversation mute (persists in the database)
  • /bell – toggle notification types at runtime

Desktop notifications use notify-rust for cross-platform support (Linux D-Bus, macOS NSNotification, Windows WinRT toast). They show the sender name and a message preview, and respect the same mute/block/accept conditions as bell notifications.

Contact resolution

On startup, siggy requests your contact list and group list from signal-cli. Names from your Signal address book are used throughout the sidebar, chat area, and typing indicators.

Responsive layout

The sidebar auto-hides on narrow terminals (less than 60 columns). Use Ctrl+Left / Ctrl+Right to resize it, or /sidebar to toggle it.

Incognito mode

siggy --incognito

Uses an in-memory database instead of on-disk SQLite. No messages, conversations, or read markers are written to disk. The status bar shows a bold magenta incognito indicator. When you exit, everything is gone.

Message reactions

React to any message with r in Normal mode to open the emoji picker. Navigate with h/l or press 1-8 to jump directly, then Enter to send.

Reactions display below messages as compact badges:

👍 2  ❤️ 1

Enable “Verbose reactions” in /settings to show sender names instead of counts. Reactions sync across devices and persist in the database.

Reactions, quote reply, link preview, and poll

@mentions

In group chats, type @ to open a member autocomplete popup. Filter by name and press Tab to insert the mention. Works in 1:1 chats too (with the conversation partner). Incoming mentions are highlighted in cyan+bold.

Visible message selection

Focused message

When scrolling in Normal mode, the focused message gets a subtle dark background highlight. This makes it clear which message r (react) and y/Y (copy) will target. Use J/K (Shift+j/k) to jump between messages, skipping date separators and system messages.

Reply, edit, and delete

In Normal mode, act on the focused message:

  • q – Quote reply – reply with a quoted block showing the original message. A reply indicator appears above your input while composing.
  • e – Edit – edit your own outgoing messages. The original text is loaded into the input buffer. Edited messages display “(edited)”.
  • d – Delete – delete a message. Outgoing messages offer “delete for everyone” (remote delete) or “delete locally”. Incoming messages can be deleted locally. Deleted messages show as “[deleted]”.

All three features sync across devices and persist in the database.

Message search

Use /search <query> (alias /s) to search across all conversations. Results appear in a scrollable overlay with sender, snippet, and conversation name. Press Enter to jump to the message in context.

After searching, use n/N in Normal mode to cycle through matches without re-opening the overlay.

Text styling

Signal formatting is rendered in the chat area:

  • Bold – displayed with terminal bold
  • Italic – displayed with terminal italic
  • Strikethrough – displayed with terminal strikethrough
  • Monospace – displayed in gray
  • Spoiler – hidden behind block characters (████)

Styles compose correctly with @mentions and link highlighting.

Sticker messages

Incoming stickers display as [Sticker: emoji] in the chat area (e.g. [Sticker: 👍]). If the sticker has no associated emoji, it shows as [Sticker].

View-once messages

View-once messages display as [View-once message] with any attachments suppressed, respecting the sender’s ephemeral intent.

System messages

Certain Signal events display as system messages (dimmed, centered) in the chat:

  • Missed calls – “Missed voice call” / “Missed video call”
  • Safety number changes – warning when a contact’s safety number changes
  • Group updates – group metadata changes (member adds/removes)
  • Disappearing message timer – e.g. “Disappearing messages set to 1 day”

Message action menu

Press Enter in Normal mode on a focused message to open a contextual action menu. Available actions depend on the message type:

ActionKeyAvailable on
ReplyqNon-deleted messages
EditeYour own outgoing messages
ReactrAll messages
CopyyAll messages
ForwardfNon-deleted messages
DeletedNon-deleted messages

Navigate with j/k, press Enter to execute, or press the shortcut key directly. Press Esc to close.

Read receipts

siggy sends read receipts to message senders when you view a conversation, letting them know you’ve read their messages. This can be toggled off via /settings > “Send read receipts”.

Cross-device read sync

When you read messages on your phone or another linked device, siggy receives the read sync and marks those conversations as read. Unread counts update automatically.

Disappearing messages

siggy honors Signal’s disappearing message timers. When a conversation has a timer set, messages auto-expire after the configured duration. Set the timer with /disappearing <duration> (alias /dm):

  • 30s, 5m, 1h, 1d, 1w – set the timer
  • off – disable disappearing messages

Timer changes from other devices sync automatically.

Group management

Use /group (alias /g) to manage groups directly from the TUI:

  • View members – see all group members
  • Add member – type-to-filter contact picker to add members
  • Remove member – type-to-filter member picker to remove members
  • Rename – change the group name
  • Create – create a new group (available from any conversation)
  • Leave – leave the group with confirmation

Message requests

Messages from unknown senders (not in your contacts) are flagged as message requests. A banner appears at the top of the conversation with options to accept or delete. Unaccepted conversations do not trigger notifications or send read receipts.

Block and unblock

Use /block to block the current conversation’s contact or group, and /unblock to unblock. Blocked conversations do not trigger notifications, read receipts, or typing indicators.

Mouse support

Mouse support is enabled by default. Toggle via /settings > “Mouse support”.

  • Click sidebar – switch conversations by clicking
  • Scroll messages – scroll wheel in the chat area
  • Click input bar – position the cursor by clicking
  • Overlay scroll – scroll wheel navigates lists in overlays

Color themes

Open the theme picker with /theme (alias /t) or from /settings > Theme. Choose from built-in themes with customizable sidebar, chat, status bar, and accent colors.

Pinned messages

Pin important messages to the top of a conversation. Press p in Normal mode on a focused message (or use the action menu) to pin it. Choose a duration: forever, 24 hours, 7 days, or 30 days. Pinned messages show as a banner at the top of the chat area. Press p on an already-pinned message to unpin it. Pin state syncs across all linked devices.

Messages containing URLs display link preview cards with the page title, description, and thumbnail image (when available). Toggle via /settings > “Link previews” (enabled by default).

Polls

Create polls with /poll "question" "option1" "option2". Add --single to restrict voting to one option. Polls display inline as bar charts showing vote counts and percentages.

Press Enter on a poll message in Normal mode to open the vote overlay. Select options with Space (multi-select) or Enter (single-select), then confirm. Your votes sync across devices.

Identity verification

Use /verify to verify the identity keys of your contacts. In 1:1 chats, the overlay shows the contact’s safety number and current trust level. In group chats, browse members and verify individually. You can trust or untrust identity keys directly from the overlay.

Profile editor

Use /profile to edit your Signal profile. Change your given name, family name, about text, and about emoji. Navigate fields with j/k, press Enter to edit inline, and Save to push changes to Signal’s servers.

About

Use /about to see the app version, description, author, license, and repository link.

The sidebar can be placed on the left (default) or right side of the screen. Toggle via /settings > “Sidebar on right”.

Configurable keybindings

Keybindings overlay

All keybindings are fully configurable. Choose from three built-in profiles (Default, Emacs, Minimal) or create your own. Override individual keys via ~/.config/siggy/keybindings.toml, or rebind keys live in the app with /keybindings (alias /kb).

See Keybindings for full details on profiles, customization, and the TOML format.

Multi-line input

Press Alt+Enter or Shift+Enter in Insert mode to insert a newline. Compose multi-line messages before sending with Enter. The input area expands automatically to show all lines.

Message history pagination

Scrolling to the top of a conversation automatically loads older messages from the database. A loading indicator appears briefly while fetching. This lets you browse your full message history without loading everything upfront.

Forward messages

Press f in Normal mode on a focused message to forward it to another conversation. A filterable picker overlay lets you choose the destination.

Demo mode

siggy --demo

Launches with dummy conversations and messages. No signal-cli process is spawned. Useful for testing the UI, exploring keybindings, and taking screenshots.

Security

Trust model

siggy is a thin TUI layer over signal-cli. It does not implement any cryptographic protocols, manage credentials, or contact Signal servers directly. All security-critical operations are delegated to signal-cli, which implements the full Signal Protocol.

This means siggy inherits signal-cli’s security posture:

  • What signal-cli handles: key generation, key exchange, message encryption/decryption, identity verification, contact and group management, attachment encryption, and all network communication with Signal servers.
  • What siggy handles: rendering messages in a terminal, storing a local cache of conversations in SQLite, and forwarding user actions to signal-cli via JSON-RPC.

siggy never sees plaintext cryptographic keys or raw network traffic.

Credential storage

Signal credentials (identity keys, session keys, pre-keys) are stored by signal-cli in its own data directory (~/.local/share/signal-cli/ on Linux). siggy does not read, write, or manage these files. If credential storage security is a concern, it should be addressed at the signal-cli level or via OS-level protections (encrypted home directory, restrictive file permissions).

Encryption

In transit

All messages are end-to-end encrypted using the Signal Protocol, handled entirely by signal-cli. siggy communicates with signal-cli over a local stdin/stdout pipe using JSON-RPC – no network sockets are involved.

At rest

Messages are stored unencrypted in a local SQLite database (siggy.db). This is the same approach used by Signal Desktop and most other messaging clients. The rationale is that local storage protection is best handled at the OS level (full-disk encryption, screen lock, file permissions) rather than by individual applications.

The database uses PRAGMA secure_delete = ON, which zeroes out deleted content in the database file rather than leaving it recoverable in free pages.

Files on disk

FileContentsLocation
siggy.dbMessage history, contacts, groupsPlatform config directory
siggy.db-walRecent uncommitted writesSame directory
config.tomlPhone number, settingsPlatform config directory
debug.logDebug output (opt-in, PII redacted by default)~/.cache/siggy/
Download directoryReceived attachments~/signal-downloads/ or configured path

Platform config directories:

  • Linux / macOS: ~/.config/siggy/
  • Windows: %APPDATA%\siggy\

Privacy features

Incognito mode

siggy --incognito

Uses an in-memory database instead of on-disk SQLite. No messages, conversations, or read markers are written to disk. When you exit, everything is gone.

Notification previews

Desktop notification content is configurable via the notification_preview setting in /settings:

LevelTitleBody
full (default)Sender nameMessage content
senderSender name“New message”
minimal“New message”(empty)

Debug logging

Debug logging is opt-in only and disabled by default.

  • --debug – enables logging with PII redaction: phone numbers are masked (e.g. +4***...567), message bodies are replaced with [msg: 42 chars], and contact/group lists show only counts.
  • --debug-full – enables logging with full unredacted output. Only use this when you need actual message content for troubleshooting, and delete the log file afterwards.

Debug logs are written to ~/.cache/siggy/debug.log with 10 MB rotation. On Unix systems, the log file and directory are created with restrictive permissions (0600 / 0700).

Clipboard

Copied message content is automatically cleared from the system clipboard after 30 seconds (configurable via clipboard_clear_seconds in config).

Recommendations

  • Enable full-disk encryption on your device (BitLocker, LUKS, FileVault). This is the single most effective protection for data at rest.
  • Use --incognito mode for sensitive sessions where you don’t want any messages persisted to disk.
  • Set notification_preview = "sender" or "minimal" if you’re concerned about notification content being visible on lock screens or in screen recordings.
  • Use a screen lock to prevent physical access to your terminal session.
  • On shared systems, restrict file permissions on the config directory (chmod 700 ~/.config/siggy).

Reporting vulnerabilities

If you discover a security issue, please report it responsibly via GitHub Issues or contact the maintainer directly. We take security seriously and will respond promptly.

Troubleshooting

signal-cli not found

Symptom: setup wizard says it cannot find signal-cli.

Fix: ensure signal-cli is installed and on your PATH. You can also set the full path in your config:

signal_cli_path = "/usr/local/bin/signal-cli"

On Windows, use the full path to signal-cli.bat.

QR code doesn’t display properly

Symptom: the QR code appears garbled or too large during device linking.

Fix: make sure your terminal is at least 60 columns wide and supports Unicode block characters. Try a modern terminal emulator like Windows Terminal, iTerm2, Kitty, or Alacritty.

“Java not found” or class version errors

Symptom: signal-cli fails to start with Java-related errors, or you see UnsupportedClassVersionError mentioning “class file version 69.0”.

Fix: signal-cli 0.14+ requires Java 25+. Install a compatible JDK:

# Windows
winget install EclipseAdoptium.Temurin.25.JDK

# macOS
brew install --cask temurin@25

# Or download from https://adoptium.net/

Verify with java -version – you should see version 25 or higher. On Linux, the install script uses the native signal-cli build which does not require Java.

Messages not appearing

Symptom: the app starts but no messages show up.

Fix:

  1. Check that your device is properly linked in Signal’s settings on your phone (Settings > Linked Devices)
  2. Try re-running the setup wizard: siggy --setup
  3. Check signal-cli can communicate by running it directly:
    signal-cli -a +15551234567 receive
    

Images not rendering

Symptom: images show as [attachment: image.jpg] instead of inline previews.

Fix: make sure inline_images = true in your config (this is the default). Also check that your terminal supports 256 colors or truecolor. Halfblock rendering requires a terminal with proper Unicode support.

Symptom: the sidebar is not visible.

Fix: if your terminal is narrower than 60 columns, the sidebar auto-hides. Widen your terminal, or press /sidebar to force it on. You can also use Ctrl+Right to widen the sidebar.

Database errors

Symptom: errors about SQLite or the database file.

Fix: the database is stored alongside the config file. If it becomes corrupted, you can delete it and siggy will create a fresh one on next launch. You’ll lose message history but all conversations will re-populate from signal-cli.

As a workaround, you can also run in incognito mode:

siggy --incognito

FAQ

Does siggy replace the Signal phone app?

No. siggy runs as a linked device, just like Signal Desktop. Your phone remains the primary device and must stay registered. siggy connects through signal-cli, which registers as a secondary device on your account.

Can I use siggy without a phone?

No. Signal requires a phone number for registration and a primary device. siggy links to your existing account as a secondary device.

Is my data encrypted?

Messages are end-to-end encrypted in transit by the Signal protocol (handled by signal-cli). Locally, messages are stored in an unencrypted SQLite database – the same approach used by Signal Desktop. If you want zero local persistence, use --incognito mode. See the Security page for full details and recommendations.

Can I send files and images?

Yes. Use /attach to open a file browser and select a file to send. Received images are rendered inline, and other files are saved to your download directory.

Does it work on Windows?

Yes. Pre-built Windows binaries are provided in each release. Use a modern terminal like Windows Terminal for the best experience (clickable links, proper Unicode, truecolor support).

Does it work over SSH?

Yes. siggy is a terminal application and works perfectly over SSH sessions. Make sure signal-cli and Java are available on the remote machine.

Can I use multiple Signal accounts?

Yes. Use the -a flag or config file to specify which account to use:

siggy -a +15551234567
siggy -a +15559876543

Each account needs its own device linking via signal-cli.

How do I update siggy?

Re-run the install script, or download the latest binary from the Releases page.

If you installed from source:

cargo install --git https://github.com/johnsideserf/siggy.git --force

What license is siggy under?

GPL-3.0. This is a copyleft license – forks must remain open source under the same terms.

Architecture

Overview

siggy is a terminal Signal client that wraps signal-cli via JSON-RPC over stdin/stdout. It is built on a Tokio async runtime with Ratatui for rendering.

+------------+   mpsc channels   +----------------+
|  TUI       | <---------------> |  Signal        |
|  (main     |   SignalEvent     |  Backend       |
|  thread)   |   UserCommand     |  (tokio task)  |
+------------+                   +--------+-------+
                                          |
                                   stdin/stdout
                                          |
                                 +--------v-------+
                                 |  signal-cli    |
                                 |  (child proc)  |
                                 +----------------+

Async runtime

The application uses a multi-threaded Tokio runtime (via #[tokio::main]). The main thread runs the TUI event loop. signal-cli communication happens in spawned Tokio tasks that communicate back to the main thread via tokio::sync::mpsc channels.

Event loop

The main loop in main.rs runs on a 50ms tick:

  1. Poll keyboard – check for key events via Crossterm (non-blocking, 50ms timeout)
  2. Drain signal events – process all pending SignalEvent messages from the mpsc channel
  3. Render – call ui::draw() with the current App state

This keeps the UI responsive while processing backend events as they arrive.

Startup sequence

  1. Load config from TOML (or defaults)
  2. Check if setup is needed (account field empty)
  3. If needed: run the setup wizard (signal-cli detection, phone input, QR linking)
  4. Open SQLite database (or in-memory for --incognito)
  5. Spawn signal-cli child process
  6. Load conversations and contacts from database + signal-cli
  7. Enter the main event loop

Key dependencies

CratePurpose
ratatui 0.29Terminal UI framework
crossterm 0.28Cross-platform terminal I/O
tokio 1.xAsync runtime
serde / serde_jsonJSON serialization for signal-cli RPC
rusqlite 0.32SQLite database (bundled)
chrono 0.4Timestamp handling
qrcode 0.14QR code generation for device linking
image 0.25Image decoding for inline previews
anyhow 1.xError handling
toml 0.8Config file parsing
dirs 6.xPlatform-specific directory paths
uuid 1.xRPC request ID generation

Module Reference

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

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).

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, /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.

Data Flow

Outbound messages (user sends)

1. User types message + presses Enter
2. input::parse_input() -> InputAction::SendText
3. App sends JsonRpcRequest via mpsc to SignalClient
4. SignalClient writes JSON-RPC to signal-cli stdin
5. signal-cli transmits via Signal protocol

The request is a JSON-RPC call to the send method with the recipient and message body as parameters. Each request gets a unique UUID as its RPC ID.

Inbound messages (received)

1. signal-cli receives message via Signal protocol
2. signal-cli writes JSON-RPC notification to stdout
3. SignalClient stdout reader parses the JSON line
4. Notification has method = "receive" with message data
5. Parsed into SignalEvent::MessageReceived
6. Sent through mpsc channel to main thread
7. App::handle_signal_event() processes it:
   a. get_or_create_conversation() ensures the conversation exists
   b. Message is appended to the conversation's message list
   c. Message is inserted into SQLite
   d. Unread count is updated (if not the active conversation)
   e. Terminal bell fires (if notifications enabled and not muted)
8. Next render cycle shows the new message

RPC request/response correlation

signal-cli uses JSON-RPC 2.0. There are two types of messages:

Notifications (incoming)

Notifications arrive as JSON-RPC requests from signal-cli (they have a method field). These include:

  • receive – incoming message
  • receiveTyping – typing indicator
  • receiveReceipt – delivery/read receipt

These are unsolicited and do not have an id field matching any outbound request.

RPC responses

When siggy sends a request (e.g., listContacts, listGroups, send), signal-cli replies with a response that has a matching id field and a result (or error) field.

The pending_requests map in SignalClient stores id -> method pairs. When a response arrives, the client looks up the method by ID to know how to parse the result:

outbound:  { "jsonrpc": "2.0", "id": "abc-123", "method": "listContacts", ... }
inbound:   { "jsonrpc": "2.0", "id": "abc-123", "result": [...] }

pending_requests["abc-123"] = "listContacts"
-> parse result as Vec<Contact>
-> emit SignalEvent::ContactList

Sync messages

When you send a message from your phone, signal-cli receives a sync notification. These appear as SignalMessage with is_outgoing = true and a destination field indicating the recipient. The app routes these to the correct conversation and displays them as outgoing messages.

Channel architecture

                     mpsc::channel
SignalClient ───────────────────────> App (main thread)
  (tokio tasks)      SignalEvent

                     mpsc::channel
App (main thread) ──────────────────> SignalClient
                     JsonRpcRequest     (stdin writer task)

Both channels are unbounded tokio::sync::mpsc channels. The signal event channel carries SignalEvent variants. The command channel carries JsonRpcRequest structs to be serialized and written to signal-cli’s stdin.

Database Schema

siggy uses SQLite with WAL (Write-Ahead Logging) mode for safe concurrent reads/writes. The database file is stored alongside the config file.

Tables

schema_version

Tracks the current migration version.

CREATE TABLE schema_version (
    version INTEGER NOT NULL
);

conversations

One row per conversation (1:1 or group).

CREATE TABLE conversations (
    id                TEXT PRIMARY KEY,      -- phone number or group ID
    name              TEXT NOT NULL,         -- display name
    is_group          INTEGER NOT NULL DEFAULT 0,
    created_at        TEXT NOT NULL DEFAULT (datetime('now')),
    muted             INTEGER NOT NULL DEFAULT 0,  -- added in migration v2
    expiration_timer  INTEGER NOT NULL DEFAULT 0,  -- disappearing msg seconds (v7)
    accepted          INTEGER NOT NULL DEFAULT 1,  -- message request state (v8)
    blocked           INTEGER NOT NULL DEFAULT 0   -- blocked state (v9)
);

The id is a phone number (E.164 format) for 1:1 conversations or a base64-encoded group ID for groups.

messages

All messages, ordered by insertion rowid.

CREATE TABLE messages (
    rowid           INTEGER PRIMARY KEY AUTOINCREMENT,
    conversation_id TEXT NOT NULL REFERENCES conversations(id),
    sender          TEXT NOT NULL,       -- sender display name or empty for system
    timestamp       TEXT NOT NULL,       -- RFC 3339 timestamp
    body            TEXT NOT NULL,       -- message text
    is_system       INTEGER NOT NULL DEFAULT 0,
    status          INTEGER NOT NULL DEFAULT 0,    -- MessageStatus enum (v3)
    timestamp_ms    INTEGER NOT NULL DEFAULT 0,    -- server epoch ms (v3)
    is_edited       INTEGER NOT NULL DEFAULT 0,    -- edited flag (v6)
    is_deleted      INTEGER NOT NULL DEFAULT 0,    -- deleted flag (v6)
    quote_author    TEXT,                           -- quoted reply author (v6)
    quote_body      TEXT,                           -- quoted reply body (v6)
    quote_ts_ms     INTEGER,                        -- quoted reply timestamp (v6)
    sender_id            TEXT NOT NULL DEFAULT '',       -- sender phone number (v6)
    expires_in_seconds   INTEGER NOT NULL DEFAULT 0,    -- disappearing timer (v7)
    expiration_start_ms  INTEGER NOT NULL DEFAULT 0     -- timer start epoch ms (v7)
);

CREATE INDEX idx_messages_conv_ts ON messages(conversation_id, timestamp);
CREATE INDEX idx_messages_conv_ts_ms ON messages(conversation_id, timestamp_ms);

System messages (is_system = 1) are used for join/leave notifications and are excluded from unread counts.

reactions

Emoji reactions on messages. One reaction per sender per message, with the latest emoji replacing any previous one.

CREATE TABLE reactions (
    rowid           INTEGER PRIMARY KEY AUTOINCREMENT,
    conversation_id TEXT NOT NULL,
    target_ts_ms    INTEGER NOT NULL,     -- timestamp of the reacted-to message
    target_author   TEXT NOT NULL,         -- author of the reacted-to message
    emoji           TEXT NOT NULL,
    sender          TEXT NOT NULL,         -- who sent this reaction
    UNIQUE(conversation_id, target_ts_ms, target_author, sender)
);

CREATE INDEX idx_reactions_target ON reactions(conversation_id, target_ts_ms);

read_markers

Tracks the last-read message per conversation for unread counting.

CREATE TABLE read_markers (
    conversation_id TEXT PRIMARY KEY REFERENCES conversations(id),
    last_read_rowid INTEGER NOT NULL DEFAULT 0
);

Unread count = messages with rowid > last_read_rowid and is_system = 0.

Migrations

Migrations are version-based and run sequentially in Database::migrate():

VersionChanges
1Initial schema: conversations, messages, read_markers tables
2Add muted column to conversations
3Add status and timestamp_ms columns to messages (delivery status tracking)
4Create reactions table with unique constraint per sender per message
5Add index on messages(conversation_id, timestamp_ms) for search performance
6Add is_edited, is_deleted, quote_author, quote_body, quote_ts_ms, sender_id columns to messages
7Add expiration_timer to conversations and expires_in_seconds, expiration_start_ms to messages
8Add accepted column to conversations (message request tracking)
9Add blocked column to conversations (block/unblock state)

Each migration is wrapped in a transaction. The schema_version table tracks the current version.

WAL mode

WAL mode is enabled on every connection:

PRAGMA journal_mode=WAL;
PRAGMA foreign_keys=ON;

WAL allows concurrent readers while a writer is active, preventing database locks during normal operation.

In-memory mode

When running with --incognito, Database::open_in_memory() is used instead of Database::open(). The same schema and migrations apply, but everything lives in memory and is lost on exit.

signal-cli Protocol

siggy communicates with signal-cli using JSON-RPC 2.0 over stdin/stdout. signal-cli is spawned as a child process in jsonRpc mode.

Starting signal-cli

signal-cli is launched with:

signal-cli -a +15551234567 jsonRpc

This starts signal-cli in JSON-RPC mode, reading requests from stdin and writing responses/notifications to stdout. Each message is a single JSON line.

Request format

Requests sent from siggy to signal-cli:

{
    "jsonrpc": "2.0",
    "id": "550e8400-e29b-41d4-a716-446655440000",
    "method": "send",
    "params": {
        "recipient": ["+15551234567"],
        "message": "Hello!"
    }
}

Each request has a unique UUID id for response correlation.

Response format

Responses from signal-cli for RPC calls:

{
    "jsonrpc": "2.0",
    "id": "550e8400-e29b-41d4-a716-446655440000",
    "result": { ... }
}

Or on error:

{
    "jsonrpc": "2.0",
    "id": "550e8400-e29b-41d4-a716-446655440000",
    "error": {
        "code": -1,
        "message": "error description"
    }
}

Notification format

Notifications are unsolicited JSON-RPC requests from signal-cli (no matching outbound request). They have a method field but no id:

{
    "jsonrpc": "2.0",
    "method": "receive",
    "params": {
        "envelope": {
            "source": "+15559876543",
            "sourceDevice": 1,
            "timestamp": 1700000000000,
            "dataMessage": {
                "message": "Hey there!",
                "timestamp": 1700000000000
            }
        }
    }
}

Methods used

Outbound (siggy -> signal-cli)

MethodPurpose
sendSend a message (also used for edits via editTimestamp param)
listContactsRequest the contact address book
listGroupsRequest the list of groups
sendSyncRequestRequest a sync from the primary device
sendReactionSend an emoji reaction to a message
remoteDeleteDelete a message for all recipients
sendTypingIndicatorSend typing started/stopped indicator
sendReceiptSend a read receipt for one or more messages
updateGroupCreate/rename group, add/remove members
quitGroupLeave a group
blockBlock a contact or group
unblockUnblock a contact or group
setExpirationSet disappearing message timer
updateProfileUpdate own Signal profile (name, about, emoji)
listIdentitiesList known identity keys for contacts
trustTrust a contact’s identity key
sendMessageRequestResponseAccept or delete a message request

Inbound notifications (signal-cli -> siggy)

MethodPurposeMaps to
receiveIncoming messageSignalEvent::MessageReceived
receiveTypingTyping indicatorSignalEvent::TypingIndicator
receiveReceiptDelivery/read receiptSignalEvent::ReceiptReceived

Incoming receive envelopes may also contain:

Envelope fieldPurposeMaps to
dataMessage.reactionIncoming reactionSignalEvent::ReactionReceived
dataMessage.remoteDeleteRemote delete requestSignalEvent::RemoteDeleteReceived
dataMessage.quoteQuoted reply metadataquote field on SignalMessage
editMessageEdited messageSignalEvent::EditReceived
syncMessage.sentMessageOutgoing sync (own messages from other devices)Same as above, with is_outgoing = true
syncMessage.readMessagesRead sync from other devicesSignalEvent::ReadSyncReceived
dataMessage.stickerSticker messageBody set to [Sticker: emoji]
dataMessage.textStyles / bodyRangesText formatting (bold, italic, etc.)text_styles field on SignalMessage
dataMessage.expiresInSecondsDisappearing message timerexpires_in_seconds on SignalMessage
dataMessage.isViewOnceView-once message flagBody set to [View-once message]
callMessageMissed call notificationSignalEvent::SystemMessage

Parsing logic

The stdout reader in SignalClient determines the message type by checking which fields are present:

  1. If method is present -> it’s a notification, parse based on method name
  2. If id and result/error are present -> it’s a response, look up the method via pending_requests[id] and parse accordingly
  3. Unknown methods are logged and discarded

Sync messages

Messages sent from the primary device arrive as sync messages. They are identified by having is_outgoing = true in the parsed SignalMessage. The destination field indicates the recipient, and the message is routed to the appropriate conversation.

Testing

Running tests

Run the full test suite:

cargo test

Run tests for a specific module:

cargo test app::tests          # App module tests
cargo test signal::client::tests  # Signal client tests
cargo test db::tests           # Database tests
cargo test input::tests        # Input parsing tests

Run a single test by name:

cargo test test_name

rstest

Tests use rstest for fixtures and parameterization. The crate is declared in [dev-dependencies].

Fixtures

Two #[fixture] functions provide pre-built test objects:

  • app() in app.rs — returns an App with an in-memory DB and connected state.
  • db() in db.rs — returns an in-memory Database.

To use a fixture, mark the test #[rstest] and add the fixture as a parameter:

#![allow(unused)]
fn main() {
#[rstest]
fn my_test(mut app: App) {
    app.input_buffer = "/quit".to_string();
    // ...
}
}

Parameterized tests

When multiple tests share the same assertion logic but differ in inputs, use #[case] to collapse them into a single function:

#![allow(unused)]
fn main() {
#[rstest]
#[case("/quit", InputAction::Quit)]
#[case("/q",    InputAction::Quit)]
#[case("/help", InputAction::Help)]
fn command_returns_expected_action(#[case] input: &str, #[case] expected: InputAction) {
    assert_eq!(parse_input(input), expected);
}
}

Each #[case] produces a separate entry in cargo test output, so individual failures are easy to identify.

When to use what

SituationApproach
Test needs an App or DatabaseUse the fixture (#[rstest] + parameter)
3+ tests with identical structure, different dataParameterize with #[case]
2 tests with significantly different setupKeep them separate
Test doesn’t need a fixturePlain #[test] is fine

Best practices

  • Prefer #[case] over copy-paste. If you’re writing a new test and an existing parameterized test covers the same assertion pattern, add a #[case] instead of a new function.
  • Keep case data simple. Strings, numbers, and booleans work well in #[case] attributes. For complex types, build them inside the test body using a label parameter and match.
  • Add a _label parameter when case data alone doesn’t make the purpose obvious. This shows up in test names (e.g., my_test::case_basic).
  • Don’t over-parameterize. If merging tests requires a match with completely different setup logic per arm, separate tests are clearer.

Test modules

Tests are defined as #[cfg(test)] mod tests blocks within each source file.

db.rs tests

Database tests use Database::open_in_memory() for isolated, fast test instances. Coverage includes:

  • Schema migration and table creation
  • Conversation upsert and loading
  • Name updates on conflict
  • Message insertion and retrieval (ordering)
  • Unread count with read markers
  • System message exclusion from unread counts
  • Conversation ordering by most recent message
  • Mute flag round-trip
  • Last message rowid tracking

input.rs tests

Input parser tests cover:

  • Plain text passthrough
  • Empty and whitespace-only input
  • All commands and their aliases (/join, /j, /part, /p, etc.)
  • Commands with and without arguments
  • Unknown command handling

app.rs tests

Application state tests cover signal event handling, conversation management, and mode transitions.

signal/client.rs tests

Signal client tests cover JSON-RPC parsing and event routing.

Demo mode for manual testing

cargo run -- --demo

Demo mode populates the UI with dummy conversations and messages without requiring signal-cli. This is the easiest way to manually test UI changes, keybindings, and rendering.

Linting

The project enforces zero clippy warnings:

cargo clippy --tests -- -D warnings

CI runs this on every push and pull request. Fix all warnings before pushing.

Fuzz testing

The fuzz/ directory contains cargo-fuzz harnesses for external input boundaries. Fuzz testing requires nightly Rust and Linux or macOS (libfuzzer does not support Windows).

cargo install cargo-fuzz
cargo +nightly fuzz run <target>

Fuzz targets

TargetWhat it tests
fuzz_json_rpcJSON-RPC deserialization and parse_signal_event / parse_rpc_result
fuzz_input_editUTF-8 cursor navigation and string mutation at byte boundaries
fuzz_key_comboparse_key_combo with arbitrary strings from user TOML files
fuzz_command_parseparse_input with arbitrary slash commands

Run cargo fuzz list to see all available targets. Any panic found by the fuzzer is a bug to fix.

Contributing

Getting started

  1. Fork the repository and clone your fork
  2. Install prerequisites: Rust 1.70+ and signal-cli
  3. Build and run tests:
cargo build
cargo test

Use --demo mode to test the UI without a Signal account:

cargo run -- --demo

Making changes

  1. Create a feature branch from master:
git checkout -b feature/my-change
  1. Make your changes. Run checks before committing:
cargo clippy --tests -- -D warnings
cargo test
  1. Push your branch and open a pull request against master.

Branch naming

Use prefixed names:

PrefixUse case
feature/New functionality
fix/Bug fixes
refactor/Code restructuring
docs/Documentation changes

Examples: feature/dark-mode, fix/unread-count, docs/update-readme

Code style

  • Follow existing patterns in the codebase
  • Run cargo clippy with warnings-as-errors – CI enforces this
  • Keep commits focused: one logical change per commit
  • Write descriptive commit messages
  • Reference issue numbers in commits and PRs (e.g., closes #29)

Pull requests

  • Create a PR targeting master
  • Include a clear description of what changed and why
  • Reference the issue being addressed if applicable
  • Make sure CI passes (clippy + tests)
  • Trivial docs-only changes may be committed directly to master; all code changes must go through a PR

Reporting bugs

Use the bug report template. Include:

  • Your OS and terminal emulator
  • siggy version (siggy --version or the release tag)
  • Steps to reproduce the issue

Suggesting features

Use the feature request template. Describe the problem you’re trying to solve before proposing a solution.

License

By contributing, you agree that your contributions will be licensed under GPL-3.0.

CI & Releases

Continuous integration

CI runs automatically on every push and pull request via .github/workflows/ci.yml.

CI pipeline

StepCommand
Checkoutactions/checkout@v4
Rust toolchaindtolnay/rust-toolchain@stable
CacheSwatinem/rust-cache@v2
Lintcargo clippy --tests -- -D warnings
Testcargo test

CI must pass before merging any PR.

Release pipeline

Releases are triggered by pushing a version tag. The workflow is defined in .github/workflows/release.yml.

Triggering a release

# 1. Update version in Cargo.toml
# 2. Commit the version bump
# 3. Tag and push
git tag v0.3.0
git push origin v0.3.0

Release pipeline steps

  1. Lint & Test – same as CI (clippy + tests)
  2. Build – compiles release binaries for 4 targets:
TargetRunnerArchive
x86_64-unknown-linux-gnuubuntu-latest.tar.gz
x86_64-apple-darwinmacos-latest.tar.gz
aarch64-apple-darwinmacos-latest.tar.gz
x86_64-pc-windows-msvcwindows-latest.zip
  1. Package – creates archives (tar.gz on Unix, zip on Windows)
  2. Release – creates a GitHub Release with auto-generated changelog and attached archives (via softprops/action-gh-release@v2)

Version tags

Use semantic versioning: v0.1.0, v0.2.0, v1.0.0.

Remember to update the version field in Cargo.toml before creating the tag.

Install scripts

Two install scripts are provided in the repository root:

install.sh (Linux / macOS)

curl -fsSL https://raw.githubusercontent.com/johnsideserf/siggy/master/install.sh | bash

Downloads the latest release binary for the detected platform and checks for signal-cli.

install.ps1 (Windows)

irm https://raw.githubusercontent.com/johnsideserf/siggy/master/install.ps1 | iex

Downloads the latest Windows release binary and checks for signal-cli.

Documentation deployment

Documentation is built and deployed via .github/workflows/docs.yml. See the docs workflow for details on how changes to the docs/ directory trigger a rebuild and deployment to GitHub Pages.

Roadmap

Completed

  • Send and receive plain text messages (1:1 and group)

  • Receive file attachments (displayed as [attachment: filename])

  • Typing indicators (receive and send)

  • SQLite-backed message persistence with WAL mode

  • Unread message counts with persistent read markers

  • Vim-style modal editing (Normal / Insert modes)

  • Responsive layout with auto-hiding sidebar

  • First-run setup wizard with QR device linking

  • TUI error screens instead of stderr crashes

  • Commands: /join, /part, /quit, /sidebar, /help

  • Load contacts and groups on startup (name resolution, groups in sidebar)

  • Echo outgoing messages from other devices via sync messages

  • Contact name resolution from address book

  • Sync request at startup to refresh data from primary device

  • Inline image preview for attachments (halfblock rendering)

  • New message notifications (terminal bell, per-type toggles, per-chat mute)

  • Command autocomplete with Tab completion

  • Settings overlay

  • Input history (Up/Down to recall previous messages)

  • Incognito mode (--incognito)

  • Demo mode (--demo)

  • Delivery/read receipt display (status symbols on outgoing messages)

  • Contact list overlay (/contacts)

  • Copy to clipboard (y/Y in Normal mode)

  • Full timestamp on scroll (status bar shows date+time of focused message)

  • Message reactions (emoji picker, badge display, full lifecycle with DB persistence)

  • @mention autocomplete (type @ in group or 1:1 chats)

  • Visible message selection (focus highlight, J/K message-level navigation)

  • Startup error handling (signal-cli stderr captured in TUI error screen)

  • Reply to specific messages (quote reply with q key)

  • Edit own messages (e key, “(edited)” label, cross-device sync)

  • Delete messages (d key, remote delete + local delete)

  • Message search (/search, n/N navigation)

  • Send file attachments (/attach command with file browser)

  • /join autocomplete (contacts and groups with Tab completion)

  • Send typing indicators (auto-start/stop on keypress)

  • Send read receipts (automatic on conversation view, configurable)

  • System messages (missed calls, safety number changes, group updates, expiration timer)

  • Message action menu (Enter in Normal mode, contextual actions on focused message)

  • Text styling (bold, italic, strikethrough, monospace, spoiler rendering)

  • Display stickers (shown as [Sticker: emoji] in chat)

  • View-once messages (shown as [View-once message] placeholder)

  • Cross-device read sync (sync read state across linked devices)

  • Disappearing messages (honor timers, countdown display, /disappearing command)

  • Group management (/group command: view/add/remove members, rename, create, leave)

  • Message requests (detect unknown senders, accept/delete with banner UI)

  • Block/unblock contacts (/block, /unblock commands)

  • Mouse support (click sidebar, scroll messages, click input bar, overlay scroll)

  • Color themes (selectable themes via /theme or /settings)

  • Desktop notifications (OS-native via notify-rust, configurable toggle)

  • Link previews (URL preview cards with title, description, thumbnail)

  • Polls (create with /poll, vote overlay, inline bar charts)

  • Pinned messages (pin/unpin with p, duration picker, banner display)

  • Identity key verification (/verify overlay with trust management)

  • Profile editor (/profile overlay for Signal profile fields)

  • About overlay (/about command showing app info)

  • Sidebar position setting (left or right placement)

  • Publish to crates.io (cargo install siggy)

  • Rename to siggy (auto-migration from signal-tui paths)

  • Forward messages (f key, filterable picker overlay)

  • Scroll position memory per conversation

  • Multi-line message input (Alt+Enter / Shift+Enter for newlines)

  • Message history pagination (scroll-up to load older messages)

  • Configurable keybindings (profiles, in-app rebinding, TOML overrides)

Future

No planned features at this time. Have an idea? Open an issue.