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

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
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=2to 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
proxyfield toconfig.tomlfor Signal TLS proxy URLs, passed through to signal-cli as--proxy. Useful for connecting in censored regions (#178)
Developer
- Fuzz testing – added
cargo-fuzzharnesses 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
:qand:quitas 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 = ONso deleted message content is zeroed in database free pages rather than left recoverable (#161) - Debug log PII redaction –
--debugnow masks phone numbers and message bodies in log output. Use--debug-fullfor 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_previewsetting (“full”, “sender”, “minimal”) is now accessible as a cycle toggle in the/settingsoverlay (#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/keybindingsoverlay. 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
fin 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_previewconfig 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 –
/verifynow usesverifiedSafetyNumberinstead oftrustAllKnownKeys, 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.logwith 10MB rotation and a startup warning (#134) - Incognito attachment isolation –
--incognitomode 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
pin 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 pressingpagain. Pin state syncs across devices (closes #65)
Link previews
- 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--singleto 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
/verifycommand – 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
/profilecommand – 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 viaupdateProfileRPC (closes #69)
About overlay
/aboutcommand – shows app version, description, author, license, and repository link. Press any key to close
Sidebar position
- 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_countas 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 like30s,5m,1h,1d,1w, oroff(closes #61)
Group management
/groupcommand – 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
/blockand/unblockcommands – 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_timertoconversationsandexpires_in_seconds,expiration_start_mstomessages(disappearing messages) - Migration v8 – adds
acceptedcolumn toconversations(message requests) - Migration v9 – adds
blockedcolumn toconversations(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 conversations –
J/Kmessage 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
qin 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
eon 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
don 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)
Message search
/searchcommand – 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. Usen/Nin Normal mode to cycle through matches (closes #14)- Highlight matches – search terms are highlighted in the result snippets
File attachments
/attachcommand – send files with/attachto open a file browser overlay. Navigate withj/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 –
/joinnow offers Tab-completable suggestions from your contacts and groups. Type/joinand 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, andsender_idcolumns to the messages table
v0.5.0
Message reactions
- Emoji reactions – react to any message with
rin Normal mode to open the reaction picker. Navigate withh/lor1-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/Knavigation – 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_eventinto sub-functions, modernized test helpers, added persistent debug log and pending_requests TTL
v0.4.0
Contact list
/contactscommand – 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,
ycopies the selected message body andYcopies the full formatted line ([HH:MM] <sender> body) to the system clipboard (closes #28)
Navigation
- 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
/settingsare 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
/settingsnow immediately re-renders or clears previews on existing messages (fixes #41)
Settings
- Tab to toggle – Tab key now toggles settings items in the
/settingsoverlay, 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
--debugflag – opt-in protocol logging tosiggy-debug.logfor diagnosing signal-cli communication issues
Database
- Migration v3 – adds
statusandtimestamp_mscolumns 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 rawfile:///path - Double extension fix – filenames like
photo.jpg.jpgare stripped tophoto.jpgwhen 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 fix –
find_signal_cli_attachmentnow 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_EXITerror 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.
-
Install signal-cli – follow the signal-cli installation guide. The install scripts above will check for it automatically.
-
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.batif it isn’t in yourPATH. -
Java runtime – signal-cli 0.14+ requires Java 25+. Make sure
javais available in your shell. On Linux, the install script uses the native signal-cli build which does not require Java.
Supported platforms
| Platform | Binary | Notes |
|---|---|---|
| Linux x86_64 | siggy-vX.Y.Z-x86_64-unknown-linux-gnu.tar.gz | |
| macOS x86_64 | siggy-vX.Y.Z-x86_64-apple-darwin.tar.gz | Intel Macs |
| macOS arm64 | siggy-vX.Y.Z-aarch64-apple-darwin.tar.gz | Apple Silicon |
| Windows x86_64 | siggy-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:
-
Locate signal-cli – siggy searches your
PATHforsignal-cli. If it can’t find it, you’ll be prompted to enter the full path. -
Enter your phone number – provide your Signal phone number in E.164 format (e.g.
+15551234567). This is the account siggy will connect to. -
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
| Flag | Description |
|---|---|
-a, --account <NUMBER> | Phone number in E.164 format (overrides config) |
-c, --config <PATH> | Path to a custom config file |
--setup | Re-run the first-time setup wizard |
--demo | Launch with dummy data (no signal-cli needed) |
--incognito | In-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:
| Platform | Path |
|---|---|
| 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
| Field | Type | Default | Description |
|---|---|---|---|
account | string | "" | Phone number in E.164 format |
signal_cli_path | string | "signal-cli" | Path to the signal-cli binary |
download_dir | string | ~/signal-downloads/ | Directory for downloaded attachments |
notify_direct | bool | true | Terminal bell on new direct messages |
notify_group | bool | true | Terminal bell on new group messages |
desktop_notifications | bool | false | OS-level desktop notifications for incoming messages |
inline_images | bool | true | Render image attachments as halfblock art |
native_images | bool | false | Use native terminal image protocols (Kitty/iTerm2) |
show_receipts | bool | true | Show delivery/read receipt status symbols |
color_receipts | bool | true | Colored receipt status symbols (vs monochrome) |
nerd_fonts | bool | false | Use Nerd Font glyphs for status symbols |
reaction_verbose | bool | false | Show reaction sender names instead of counts |
send_read_receipts | bool | true | Send read receipts when viewing conversations |
mouse_enabled | bool | true | Enable mouse support (click sidebar, scroll, etc.) |
theme | string | "Default" | Color theme name |
keybinding_profile | string | "Default" | Keybinding profile (Default, Emacs, Minimal, or custom) |
proxy | string | "" | Signal TLS proxy URL passed through to signal-cli |
CLI flags
CLI flags override config file values for the current session:
| Flag | Overrides |
|---|---|
-a +15551234567 | account |
-c /path/to/config.toml | Config file path |
--incognito | Uses in-memory database (no persistence) |
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
| Command | Alias | Arguments | Description |
|---|---|---|---|
/join | /j | <name> | Switch to a conversation by contact name, number, or group |
/part | /p | Leave current conversation | |
/search | /s | <query> | Search messages across all conversations |
/attach | /a | Open file browser to attach a file | |
/sidebar | /sb | Toggle sidebar visibility | |
/bell | /notify | [type] | Toggle notifications (direct, group, or both) |
/mute | Mute/unmute current conversation | ||
/block | Block current contact or group | ||
/unblock | Unblock current contact or group | ||
/disappearing | /dm | <duration> | Set disappearing message timer (off, 30s, 5m, 1h, 1d, 1w) |
/group | /g | Open group management menu | |
/theme | /t | Open theme picker | |
/keybindings | /kb | Open keybindings overlay | |
/poll | "q" "a" "b" [--single] | Create a poll | |
/verify | /v | Verify contact identity keys | |
/profile | Edit your Signal profile | ||
/about | Show app info (version, license, etc.) | ||
/contacts | /c | Browse synced contacts | |
/settings | Open settings overlay | ||
/help | /h | Show help overlay | |
/quit | /q | Exit siggy |
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:
| Profile | Description |
|---|---|
| Default | Vim-style modal editing (Normal / Insert modes) |
| Emacs | No modal concept; Ctrl-based shortcuts in Insert mode |
| Minimal | Arrow-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)
| Key | Action |
|---|---|
Ctrl+C | Quit |
Tab / Shift+Tab | Next / previous conversation |
PgUp / PgDn | Scroll messages (5 lines) |
Ctrl+Left / Ctrl+Right | Resize sidebar |
Normal mode
Press Esc to enter Normal mode. The cursor stops blinking and the mode indicator
changes in the status bar.
Scrolling
| Key | Action |
|---|---|
j / k | Scroll down / up 1 line |
J / K | Jump to previous / next message |
Ctrl+D / Ctrl+U | Scroll down / up half page |
g / G | Scroll to top / bottom |
Actions
| Key | Action |
|---|---|
y | Copy message body to clipboard |
Y | Copy full line ([HH:MM] <sender> body) to clipboard |
Enter | Open action menu on focused message |
r | Open reaction picker on focused message |
q | Reply to focused message (quote reply) |
e | Edit own outgoing message |
f | Forward focused message |
d | Delete focused message |
p | Pin / unpin focused message |
n | Jump to next search result |
N | Jump to previous search result |
@ | Mention autocomplete (in Insert mode) |
Cursor movement
| Key | Action |
|---|---|
h / l | Move cursor left / right |
w / b | Word forward / back |
0 / $ | Start / end of line |
Editing
| Key | Action |
|---|---|
x | Delete character at cursor |
D | Delete from cursor to end of line |
Entering Insert mode
| Key | Action |
|---|---|
i | Insert at cursor |
a | Insert after cursor |
I | Insert at start of line |
A | Insert at end of line |
o | Insert (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.
| Key | Action |
|---|---|
Esc | Switch to Normal mode |
Enter | Send message or execute command |
Alt+Enter / Shift+Enter | Insert newline (multi-line input) |
Ctrl+W | Delete word back |
Backspace / Delete | Delete characters |
Up / Down | Recall input history |
Left / Right | Move cursor |
Home / End | Jump to start / end of line |
Mouse
Mouse support is enabled by default (toggle in /settings).
| Action | Effect |
|---|---|
| Click sidebar conversation | Switch to that conversation |
| Scroll wheel in chat | Scroll messages up/down |
| Click in input bar | Position cursor |
| Scroll wheel in overlays | Navigate list items |
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
/attachto 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/).
Clickable links
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.

@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

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:
| Action | Key | Available on |
|---|---|---|
| Reply | q | Non-deleted messages |
| Edit | e | Your own outgoing messages |
| React | r | All messages |
| Copy | y | All messages |
| Forward | f | Non-deleted messages |
| Delete | d | Non-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 timeroff– 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.
Link previews
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.
Sidebar position
The sidebar can be placed on the left (default) or right side of the screen.
Toggle via /settings > “Sidebar on right”.
Configurable keybindings

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
| File | Contents | Location |
|---|---|---|
siggy.db | Message history, contacts, groups | Platform config directory |
siggy.db-wal | Recent uncommitted writes | Same directory |
config.toml | Phone number, settings | Platform config directory |
debug.log | Debug output (opt-in, PII redacted by default) | ~/.cache/siggy/ |
| Download directory | Received 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:
| Level | Title | Body |
|---|---|---|
full (default) | Sender name | Message content |
sender | Sender 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
--incognitomode 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:
- Check that your device is properly linked in Signal’s settings on your phone (Settings > Linked Devices)
- Try re-running the setup wizard:
siggy --setup - 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.
Sidebar disappeared
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:
- Poll keyboard – check for key events via Crossterm (non-blocking, 50ms timeout)
- Drain signal events – process all pending
SignalEventmessages from the mpsc channel - Render – call
ui::draw()with the currentAppstate
This keeps the UI responsive while processing backend events as they arrive.
Startup sequence
- Load config from TOML (or defaults)
- Check if setup is needed (
accountfield empty) - If needed: run the setup wizard (signal-cli detection, phone input, QR linking)
- Open SQLite database (or in-memory for
--incognito) - Spawn signal-cli child process
- Load conversations and contacts from database + signal-cli
- Enter the main event loop
Key dependencies
| Crate | Purpose |
|---|---|
ratatui 0.29 | Terminal UI framework |
crossterm 0.28 | Cross-platform terminal I/O |
tokio 1.x | Async runtime |
serde / serde_json | JSON serialization for signal-cli RPC |
rusqlite 0.32 | SQLite database (bundled) |
chrono 0.4 | Timestamp handling |
qrcode 0.14 | QR code generation for device linking |
image 0.25 | Image decoding for inline previews |
anyhow 1.x | Error handling |
toml 0.8 | Config file parsing |
dirs 6.x | Platform-specific directory paths |
uuid 1.x | RPC 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
SignalEventvariants, and sends them through the mpsc channel - stdin writer – receives
JsonRpcRequeststructs 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 stylesTextStyle/StyleType– text formatting ranges (bold, italic, strikethrough, monospace, spoiler)Attachment– file metadata (content type, filename, local path)JsonRpcRequest/JsonRpcResponse– JSON-RPC protocol structsContact/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 messagereceiveTyping– typing indicatorreceiveReceipt– 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():
| Version | Changes |
|---|---|
| 1 | Initial schema: conversations, messages, read_markers tables |
| 2 | Add muted column to conversations |
| 3 | Add status and timestamp_ms columns to messages (delivery status tracking) |
| 4 | Create reactions table with unique constraint per sender per message |
| 5 | Add index on messages(conversation_id, timestamp_ms) for search performance |
| 6 | Add is_edited, is_deleted, quote_author, quote_body, quote_ts_ms, sender_id columns to messages |
| 7 | Add expiration_timer to conversations and expires_in_seconds, expiration_start_ms to messages |
| 8 | Add accepted column to conversations (message request tracking) |
| 9 | Add 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)
| Method | Purpose |
|---|---|
send | Send a message (also used for edits via editTimestamp param) |
listContacts | Request the contact address book |
listGroups | Request the list of groups |
sendSyncRequest | Request a sync from the primary device |
sendReaction | Send an emoji reaction to a message |
remoteDelete | Delete a message for all recipients |
sendTypingIndicator | Send typing started/stopped indicator |
sendReceipt | Send a read receipt for one or more messages |
updateGroup | Create/rename group, add/remove members |
quitGroup | Leave a group |
block | Block a contact or group |
unblock | Unblock a contact or group |
setExpiration | Set disappearing message timer |
updateProfile | Update own Signal profile (name, about, emoji) |
listIdentities | List known identity keys for contacts |
trust | Trust a contact’s identity key |
sendMessageRequestResponse | Accept or delete a message request |
Inbound notifications (signal-cli -> siggy)
| Method | Purpose | Maps to |
|---|---|---|
receive | Incoming message | SignalEvent::MessageReceived |
receiveTyping | Typing indicator | SignalEvent::TypingIndicator |
receiveReceipt | Delivery/read receipt | SignalEvent::ReceiptReceived |
Incoming receive envelopes may also contain:
| Envelope field | Purpose | Maps to |
|---|---|---|
dataMessage.reaction | Incoming reaction | SignalEvent::ReactionReceived |
dataMessage.remoteDelete | Remote delete request | SignalEvent::RemoteDeleteReceived |
dataMessage.quote | Quoted reply metadata | quote field on SignalMessage |
editMessage | Edited message | SignalEvent::EditReceived |
syncMessage.sentMessage | Outgoing sync (own messages from other devices) | Same as above, with is_outgoing = true |
syncMessage.readMessages | Read sync from other devices | SignalEvent::ReadSyncReceived |
dataMessage.sticker | Sticker message | Body set to [Sticker: emoji] |
dataMessage.textStyles / bodyRanges | Text formatting (bold, italic, etc.) | text_styles field on SignalMessage |
dataMessage.expiresInSeconds | Disappearing message timer | expires_in_seconds on SignalMessage |
dataMessage.isViewOnce | View-once message flag | Body set to [View-once message] |
callMessage | Missed call notification | SignalEvent::SystemMessage |
Parsing logic
The stdout reader in SignalClient determines the message type by checking
which fields are present:
- If
methodis present -> it’s a notification, parse based on method name - If
idandresult/errorare present -> it’s a response, look up the method viapending_requests[id]and parse accordingly - 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()inapp.rs— returns anAppwith an in-memory DB and connected state.db()indb.rs— returns an in-memoryDatabase.
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
| Situation | Approach |
|---|---|
Test needs an App or Database | Use the fixture (#[rstest] + parameter) |
| 3+ tests with identical structure, different data | Parameterize with #[case] |
| 2 tests with significantly different setup | Keep them separate |
| Test doesn’t need a fixture | Plain #[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 andmatch. - Add a
_labelparameter 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
matchwith 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
| Target | What it tests |
|---|---|
fuzz_json_rpc | JSON-RPC deserialization and parse_signal_event / parse_rpc_result |
fuzz_input_edit | UTF-8 cursor navigation and string mutation at byte boundaries |
fuzz_key_combo | parse_key_combo with arbitrary strings from user TOML files |
fuzz_command_parse | parse_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
- Fork the repository and clone your fork
- Install prerequisites: Rust 1.70+ and signal-cli
- Build and run tests:
cargo build
cargo test
Use --demo mode to test the UI without a Signal account:
cargo run -- --demo
Making changes
- Create a feature branch from
master:
git checkout -b feature/my-change
- Make your changes. Run checks before committing:
cargo clippy --tests -- -D warnings
cargo test
- Push your branch and open a pull request against
master.
Branch naming
Use prefixed names:
| Prefix | Use 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 clippywith 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 --versionor 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
| Step | Command |
|---|---|
| Checkout | actions/checkout@v4 |
| Rust toolchain | dtolnay/rust-toolchain@stable |
| Cache | Swatinem/rust-cache@v2 |
| Lint | cargo clippy --tests -- -D warnings |
| Test | cargo 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
- Lint & Test – same as CI (clippy + tests)
- Build – compiles release binaries for 4 targets:
| Target | Runner | Archive |
|---|---|---|
x86_64-unknown-linux-gnu | ubuntu-latest | .tar.gz |
x86_64-apple-darwin | macos-latest | .tar.gz |
aarch64-apple-darwin | macos-latest | .tar.gz |
x86_64-pc-windows-msvc | windows-latest | .zip |
- Package – creates archives (
tar.gzon Unix,zipon Windows) - 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/Yin 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/Kmessage-level navigation) -
Startup error handling (signal-cli stderr captured in TUI error screen)
-
Reply to specific messages (quote reply with
qkey) -
Edit own messages (
ekey, “(edited)” label, cross-device sync) -
Delete messages (
dkey, remote delete + local delete) -
Message search (
/search,n/Nnavigation) -
Send file attachments (
/attachcommand with file browser) -
/joinautocomplete (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,
/disappearingcommand) -
Group management (
/groupcommand: view/add/remove members, rename, create, leave) -
Message requests (detect unknown senders, accept/delete with banner UI)
-
Block/unblock contacts (
/block,/unblockcommands) -
Mouse support (click sidebar, scroll messages, click input bar, overlay scroll)
-
Color themes (selectable themes via
/themeor/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 (
/verifyoverlay with trust management) -
Profile editor (
/profileoverlay for Signal profile fields) -
About overlay (
/aboutcommand 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 (
fkey, 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.