Files
noise/docs/plans/2026-06-09-noise-phase-1-3.md

6.2 KiB

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.