diff --git a/AGENTS.md b/AGENTS.md new file mode 100644 index 0000000..e98ec33 --- /dev/null +++ b/AGENTS.md @@ -0,0 +1,58 @@ +# Noise — Agent Instructions + +## What this is + +Noise is a Signal Messenger client for Emacs, written in Elisp. It communicates with a local `signal-cli` instance via JSON-RPC over HTTP (default: `localhost:9128`). Conversations are stored in SQLite via `emacs-sqlite3-api`. + +## Technology & Architecture + +| Layer | Technology | +|---|---| +| Language | Emacs Lisp (Elisp) | +| Message backend | `signal-cli` JSON-RPC (`signal-cli daemon`) | +| Local storage | SQLite (`emacs-sqlite3-api`) | +| Runtime | GNU Emacs | + +- All Signal protocol work is delegated to `signal-cli`. Noise is a *client* — it does not implement Signal crypto. +- The JSON-RPC interface is called over HTTP (not piped subprocess stdio), so requests are `POST http://localhost:9128/api/v1/rpc` with JSON-RPC 2.0 payloads. +- SQLite is the single source of truth for local state: contacts, conversations, messages, read/unread markers. +- The address is configurable via the `noise-signal-cli-address` defcustom. + +## User Experience Principles + +These come directly from the UI mockup (`signal-emacs-ui.html`) and README: + +- **Every action has a keybinding.** There are no mouse targets, no toolbars, no sidebars. +- **Conversation switching is minibuffer completion** (fuzzy narrowing, vertico-style). Press `C-x b` from anywhere to switch chats. +- **Compose inline at the prompt** in a conversation buffer. `RET` sends; `M-RET` inserts a newline. +- **Chat list is an ibuffer-style root buffer** (`*signal*`). `n`/`p` to move, `RET` to open a conversation. +- **Discoverability via which-key.** The keymap is the menu — `C-c s` shows available bindings. +- **Evil compatibility out of the box.** When evil is active, provide modal bindings for searching, creating conversations, sending, etc. +- **End-to-end encryption indicator** (`⌁ E2E`) is always visible in the modeline. + +## File Naming & Conventions + +- Emacs Lisp source files live in the project root or a package directory and end in `.el`. +- Package prefix is `noise-` for all symbols, functions, and variables. +- Follow Emacs Lisp conventions: `defcustom` for user options, `defvar` for internal state, `defun` for commands. +- Use `;;;` as the comment starter for top-level headings, `;;` for inline comments. + +## Key Dependencies + +- `signal-cli` — external program, must be installed and running (`signal-cli daemon --receive-mode=manual`) +- `emacs-sqlite3-api` — Elisp package for SQLite (used for read/write on conversations, contacts, messages) + +## UI Screens (from mockup) + +1. **Chat list** (`*signal*`) — root buffer showing all conversations, unread counts, last message preview, timestamps +2. **Conversation buffer** (`*signal:*`) — full message history with inline compose prompt, day separators, delivery receipts +3. **Minibuffer switcher** — fuzzy narrowing completion over all conversations, showing name, type (direct/group), unread count, last activity +4. **Which-key panel** — discoverable keybindings menu triggered by prefix keys + +## Development Approach + +- Build iteratively — get the JSON-RPC bridge working first, then the SQLite schema, then the buffers. +- Test with a real `signal-cli` daemon running locally. Verify registration/account linking before anything else. +- Elisp files should be byte-compilable with `emacs -batch -f batch-byte-compile`. +- Follow Emacs buffer-naming conventions and major-mode patterns. +- Always respect `noise-signal-cli-address` as the configurable JSON-RPC endpoint. diff --git a/README.org b/README.org new file mode 100644 index 0000000..9936d47 --- /dev/null +++ b/README.org @@ -0,0 +1,27 @@ +* Noise + +Noise is a Signal Messenger client in Elisp for Emacs powered by https://github.com/AsamK/signal-cli + +Its ultimate goal is to provide a fast and efficient user interfae for Signal chats. + +** Tech stack +The tech stack is Elisp and SQLite (for storing conversations) + +** Setup +The project assumes a working installation of signal-cli and interfaces with that via JSON-RPC. + +** Dependencies + +For SQLite access we use this library - https://github.com/pekingduck/emacs-sqlite3-api + +We communicate with signal-cli via JSON-RPC over HTTP on localhost port 9128 by default but this can be configured by specifying =noise-signal-cli-address= + +** User Experience + +We follow Emacs conventions for keybindings with the addition of evil support out of the box. When evil compatibility is enabled then we provide a modal interface for searching conversations/contacts, creating conversations, sending messages, etc. + +** User Interface + +See the file signal-emacs-ui.html for user interface mockups we're targetting. + + diff --git a/docs/plans/2026-06-09-noise-phase-1-3.md b/docs/plans/2026-06-09-noise-phase-1-3.md new file mode 100644 index 0000000..7828472 --- /dev/null +++ b/docs/plans/2026-06-09-noise-phase-1-3.md @@ -0,0 +1,126 @@ +# Noise Phase 1-3 Implementation Plan + +**Goal:** Build a minimal Emacs Signal client with JSON-RPC connectivity to signal-cli, SQLite persistence for contacts/conversations/messages, and a chat-list buffer with minibuffer conversation switcher. + +**Architecture:** Five Elisp files with ERT tests. Each file is independently testable. No external Emacs packages beyond built-ins (url, json, sqlite, ert, cl-lib). + +**Tech Stack:** Emacs Lisp 27+, signal-cli JSON-RPC over HTTP (localhost:9128) + +--- +### File Structure + +``` +noise.el - defgroup, defcustom, noise-mode, noise-check-connection +noise-rpc.el - JSON-RPC HTTP client: noise-rpc-call, noise-rpc-check +noise-db.el - SQLite schema (contacts, conversations, messages, members), CRUD +noise-chat-list.el - *signal* buffer, ibuffer-style rendering +noise-switcher.el - minibuffer completing-read fuzzy switcher +test/ + noise-rpc-test.el + noise-db-test.el + noise-chat-list-test.el + noise-switcher-test.el +``` + +--- +### Task 1: noise.el — Package Skeleton + +Create `noise.el` with: +- defgroup noise +- defcustom noise-signal-cli-address (default "http://localhost:9128") +- noise-mode minor mode (placeholder, message on enable) +- Byte-compile check +- Commit: "feat: add noise.el package skeleton with defcustom and minor mode" + +--- +### Task 2: noise-rpc.el — JSON-RPC Client + +Create `test/noise-rpc-test.el` first (5 tests): +- noise-rpc--build-request-no-params: valid JSON-RPC 2.0 without params +- noise-rpc--build-request-with-params: params included as alist +- noise-rpc--parse-response-success: parses result field +- noise-rpc--parse-response-error: signals noise-rpc-error on error +- noise-rpc--parse-response-malformed: signals noise-rpc-error on bad JSON + +Create `noise-rpc.el`: +- define-error noise-rpc-error +- noise-rpc--build-request: JSON-RPC 2.0 request with plist-to-alist conversion +- noise-rpc--plist-to-alist: convert :key val plist to alist +- noise-rpc-call: POST to {address}/api/v1/rpc, parse response, signal error on connection failure +- noise-rpc--parse-response: parse JSON, extract result or signal noise-rpc-error +- noise-rpc-check: call getVersion method + +Update noise.el: add (require 'noise-rpc), noise-check-connection command + +Test: 5 tests pass, byte-compiles. Commit. + +--- +### Task 3: noise-db.el — SQLite Database Layer + +Create `test/noise-db-test.el` first (9 tests): +- noise-db-init-creates-file: file exists + connection active +- noise-db-tables-exist: contacts, conversations, messages, conversation_members tables created +- noise-db-upsert-and-get-contact: insert/retrieve contact +- noise-db-upsert-contact-update: upsert updates existing +- noise-db-get-all-contacts: fetch all contacts +- noise-db-upsert-and-get-conversation: insert conversation with members +- noise-db-get-all-conversations-ordered: ordered by last_message_time DESC +- noise-db-insert-message: insert message, updates conversation preview +- noise-db-search-contacts: fuzzy search by name + +Create `noise-db.el`: +- defcustom noise-db-file (default ~/.emacs.d/noise/noise.db) +- Schema tables: contacts, conversations, messages, conversation_members (CREATE TABLE IF NOT EXISTS) +- Indexes: idx_messages_conversation_time, idx_conversations_time +- noise-db-init, noise-db--open, noise-db--close, noise-db--exec, noise-db--query +- CRUD: noise-db-upsert-contact, noise-db-get-contact, noise-db-get-all-contacts +- CRUD: noise-db-upsert-conversation, noise-db-get-conversation, noise-db-get-all-conversations +- CRUD: noise-db-insert-message, noise-db-get-messages +- Queries: noise-db-get-conversation-members, noise-db-conversations-for-contact +- Search: noise-db-search-contacts, noise-db-search-conversations + +Test: 9 tests pass, byte-compiles. Commit. + +--- +### Task 4: noise-chat-list.el — Chat List Buffer + +Create `test/noise-chat-list-test.el` first (5 tests): +- noise-chat-list--format-time-today: returns HH:MM pattern +- noise-chat-list--format-time-yesterday: returns Ddd pattern +- noise-chat-list--format-time-older: returns Mon DD pattern +- noise-chat-list--format-row-with-unread: includes fringe char and name +- noise-chat-list--format-row-no-unread: excludes fringe char + +Create `noise-chat-list.el`: +- noise-chat-list-mode derived from special-mode, read-only, header-line "U Name Last message When" +- Keymap: n (next-line), p (previous-line), RET (noise-chat-list-open), m (mark read), g (refresh), / (filter), q (quit-window) +- noise-chat-list entry point: init db, switch to *signal* buffer, enter mode, render +- noise-chat-list--render: iterate conversations from db, insert formatted rows, set mode-line with chat/unread counts +- noise-chat-list--format-row: format with: fringe glyph (● or space), unread count (right-aligned 3), bold name (18 chars truncated), sender-prefixed preview with "me:" for own messages (36 chars truncated), relative timestamp +- noise-chat-list--format-time: today HH:MM, this week Ddd, older Mon DD using format-time-string +- noise-chat-list-open: (placeholder) lookup conversation at point, message the name +- noise-chat-list-mark-read/refresh/filter: placeholder messages + +Update noise.el: (require 'noise-chat-list), noise-mode enables calls (noise-chat-list) + +Test: 5 tests pass, byte-compiles. Commit. + +--- +### Task 5: noise-switcher.el — Minibuffer Switcher + +Create `test/noise-switcher-test.el` first (5 tests): +- noise-switcher--collection-includes-conversations: collection returns conversation entries +- noise-switcher--collection-includes-contacts-without-conversations: contacts with no conversation shown +- noise-switcher--format-candidate-with-unread: includes unread count annotation +- noise-switcher--format-candidate-no-unread: annotation omits unread +- noise-switcher--filter-matches-fuzzy: candidates filtered by substring match + +Create `noise-switcher.el`: +- noise-switcher command: completes over conversations + contacts-without-conversations +- noise-switcher--collection: returns list of (display . id) cons cells for completing-read +- noise-switcher--format-candidate: "NAME TYPE · UNREAD · TIME" following mockup +- noise-switcher--filter: substring fuzzy match on input +- On selection: conversation exists → message name; contact only → create conversation stub, message contact name +- Annotation property on each candidate for completing-read display + +Test: 5 tests pass, byte-compiles. Commit. diff --git a/signal-emacs-ui.html b/signal-emacs-ui.html new file mode 100644 index 0000000..1bcd302 --- /dev/null +++ b/signal-emacs-ui.html @@ -0,0 +1,208 @@ + + + + + +signal.el — UI screens + + + +
+ +
+

signal.el — screens

+

Four frames. No mouse targets, no chrome. Every action is a binding; conversation switching is a minibuffer completion, never a sidebar.

+
+ + +
+

Chat list

*signal* — ibuffer-style root buffer, n/p to move, RET to open
+
+
U Name Last message When
+
3 Family Dad: see you sunday then 09:41
1 Sarah photo: IMG_2041.jpg 09:12
Mark Okafor me: merged, cutting the release now 08:55
cycling crew Priya: saturday 7am, usual spot? Mon
Hawat alerts backup completed: zendo (4.2 GiB) Mon
Elena V. thanks! that fixed it Sun
school parents Reminder: early pickup friday Sun
Tom me: voice message (0:42) Jun 5
Note to Self uv pip compile --universal Jun 4
+
*signal* 9 chats · 4 unread (Signal Chats) ─ U:%%- ─ Top ────────────
+
C-x b switch chat RET open m mark read g refresh / filter
+
+
+ + +
+

Conversation

*signal:Sarah* — compose inline at the prompt, RET sends, M-RET newline
+
+
───────────────────────────── Tue, Jun 9 ─────────────────────────────
+
[08:58] Sarah> are you taking the girls to the pool after school?
[09:02] me> yeah, leaving around 4. want me to grab dinner on the way back? ✓✓
[09:03] Sarah> yes please. thai?
[09:10] me> done. ordering the usual ✓✓
[09:12] Sarah> [photo: IMG_2041.jpg — RET to view]
───────────────────────────────────────────────────────────────────────
> looks great. see you toni
+
⌁ E2E *signal:Sarah* Sarah is typing… (Signal Chat Fill) ─ U:%%- ─ Bot ───
+
RET send C-c C-a attach C-c C-r reply-to C-c C-e emoji react M-p edit last
+
+
+ + +
+

Minibuffer switcher

the core interaction — C-x b from anywhere, fuzzy narrowing, C-n/C-p, RET
+
+
+
[09:10] me> done. ordering the usual ✓✓
[09:12] Sarah> [photo: IMG_2041.jpg — RET to view]
───────────────────────────────────────────────────────────────────────
>
+
⌁ E2E *signal:Sarah* (Signal Chat Fill) ─ U:%%- ─ Bot ───
+
+
sarah direct · 1 unread · 09:12
school parents and carpool group · Sun
Samir Darwish direct · May 30
Switch to chat (3/47): sar
+
+
+ + +
+

Discoverability

which-key panel after C-c s — the keymap is the menu
+
+
+
[09:03] Sarah> yes please. thai?
[09:10] me> done. ordering the usual ✓✓
───────────────────────────────────────────────────────────────────────
>
+
⌁ E2E *signal:Sarah* (Signal Chat Fill) ─ U:%%- ─ Bot ───
+
+
C-c s
a attach file m mute chat v verify safety number
d disappearing msgs n new chat x delete message
g new group p pin chat / search in chat
i chat info r mark all read ? describe bindings
+
+
+ +
+ +