chore: add project docs, readme, and UI mockup
This commit is contained in:
@@ -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:<name>*`) — 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.
|
||||
+27
@@ -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.
|
||||
|
||||
|
||||
@@ -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.
|
||||
@@ -0,0 +1,208 @@
|
||||
<!doctype html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||
<title>signal.el — UI screens</title>
|
||||
<style>
|
||||
:root{
|
||||
--page:#15171c;
|
||||
--bg:#282c34;
|
||||
--bg-dim:#21252b;
|
||||
--fg:#bbc2cf;
|
||||
--fg-dim:#5b6268;
|
||||
--fg-faint:#3f4450;
|
||||
--modeline-bg:#1c1f24;
|
||||
--modeline-fg:#9da5b4;
|
||||
--modeline-inactive:#5b6268;
|
||||
--hl-line:#2c323c;
|
||||
--blue:#3a76f0; /* Signal blue: prompt, matches, selection */
|
||||
--blue-soft:#6f9bf5;
|
||||
--green:#98be65; /* delivered, e2e */
|
||||
--yellow:#ecbe7b; /* unread */
|
||||
--red:#ff6c6b;
|
||||
--mono:"Iosevka","JetBrains Mono","Fira Code",ui-monospace,"SF Mono",Menlo,Consolas,monospace;
|
||||
}
|
||||
*{box-sizing:border-box;margin:0;padding:0}
|
||||
body{
|
||||
background:var(--page);
|
||||
color:var(--fg);
|
||||
font-family:var(--mono);
|
||||
font-size:14px;
|
||||
line-height:1.5;
|
||||
padding:56px 20px 96px;
|
||||
}
|
||||
.wrap{max-width:900px;margin:0 auto}
|
||||
|
||||
header.page{margin-bottom:56px}
|
||||
header.page h1{
|
||||
font-size:15px;font-weight:600;letter-spacing:.04em;color:var(--fg);
|
||||
}
|
||||
header.page h1 .ext{color:var(--blue-soft)}
|
||||
header.page p{
|
||||
margin-top:6px;color:var(--fg-dim);font-size:13px;max-width:62ch;
|
||||
}
|
||||
kbd{
|
||||
color:var(--blue-soft);
|
||||
font-family:inherit;font-size:inherit;
|
||||
}
|
||||
|
||||
section.screen{margin-bottom:72px}
|
||||
.caption{
|
||||
display:flex;align-items:baseline;gap:14px;margin-bottom:12px;
|
||||
}
|
||||
.caption h2{
|
||||
font-size:12px;font-weight:600;letter-spacing:.14em;text-transform:uppercase;
|
||||
color:var(--modeline-fg);
|
||||
}
|
||||
.caption span{font-size:12px;color:var(--fg-dim)}
|
||||
|
||||
/* ——— the Emacs frame ——— */
|
||||
.frame{
|
||||
background:var(--bg);
|
||||
border:1px solid #0c0d10;
|
||||
border-radius:6px;
|
||||
overflow:hidden;
|
||||
box-shadow:0 14px 40px rgba(0,0,0,.45);
|
||||
}
|
||||
.buffer{
|
||||
padding:10px 0 14px;
|
||||
min-height:330px;
|
||||
white-space:pre;
|
||||
overflow-x:auto;
|
||||
tab-size:4;
|
||||
}
|
||||
.ln{padding:0 16px 0 22px;position:relative}
|
||||
.fringe{position:absolute;left:8px;color:var(--yellow)}
|
||||
.hl{background:var(--hl-line)}
|
||||
.header-line{
|
||||
background:var(--bg-dim);
|
||||
color:var(--fg-dim);
|
||||
padding:2px 16px 2px 22px;
|
||||
border-bottom:1px solid #1a1d22;
|
||||
white-space:pre;overflow-x:auto;
|
||||
}
|
||||
.dim{color:var(--fg-dim)}
|
||||
.faint{color:var(--fg-faint)}
|
||||
.name{color:var(--fg);font-weight:600}
|
||||
.unread{color:var(--yellow);font-weight:600}
|
||||
.blue{color:var(--blue-soft)}
|
||||
.green{color:var(--green)}
|
||||
.me{color:var(--blue-soft);font-weight:600}
|
||||
.sep{color:var(--fg-faint)}
|
||||
|
||||
.modeline{
|
||||
background:var(--modeline-bg);
|
||||
color:var(--modeline-fg);
|
||||
padding:2px 16px;
|
||||
white-space:pre;overflow-x:auto;
|
||||
border-top:1px solid #14161a;
|
||||
}
|
||||
.modeline .mode{color:var(--blue-soft)}
|
||||
.modeline .e2e{color:var(--green)}
|
||||
.echo{
|
||||
background:var(--bg);
|
||||
color:var(--fg-dim);
|
||||
padding:3px 16px 5px;
|
||||
white-space:pre;overflow-x:auto;
|
||||
}
|
||||
.echo kbd{color:var(--blue-soft)}
|
||||
|
||||
.cursor{
|
||||
display:inline-block;width:.62em;height:1.25em;
|
||||
background:var(--fg);vertical-align:text-bottom;
|
||||
animation:blink 1.1s steps(1) infinite;
|
||||
}
|
||||
@media (prefers-reduced-motion:reduce){.cursor{animation:none}}
|
||||
@keyframes blink{50%{opacity:0}}
|
||||
|
||||
/* minibuffer completion (vertico-style) */
|
||||
.dimmed{opacity:.38}
|
||||
.mini{
|
||||
background:var(--bg);
|
||||
border-top:1px solid #14161a;
|
||||
padding:4px 0 6px;
|
||||
white-space:pre;overflow-x:auto;
|
||||
}
|
||||
.cand{padding:0 16px 0 22px}
|
||||
.cand.sel{background:var(--hl-line)}
|
||||
.cand .m{color:var(--blue-soft);font-weight:700}
|
||||
.cand .ann{color:var(--fg-faint)}
|
||||
.prompt{padding:2px 16px 0 22px}
|
||||
.prompt .p{color:var(--blue-soft);font-weight:600}
|
||||
.prompt .count{color:var(--fg-dim)}
|
||||
|
||||
/* which-key panel */
|
||||
.whichkey{
|
||||
background:var(--bg-dim);
|
||||
border-top:1px solid #14161a;
|
||||
padding:8px 0 10px;
|
||||
white-space:pre;overflow-x:auto;
|
||||
}
|
||||
.wk{padding:0 16px 0 22px}
|
||||
.wk kbd{color:var(--blue-soft);font-weight:600}
|
||||
.wk .arr{color:var(--fg-faint)}
|
||||
|
||||
@media (max-width:560px){
|
||||
body{font-size:12px;padding:32px 8px 64px}
|
||||
.buffer{min-height:240px}
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div class="wrap">
|
||||
|
||||
<header class="page">
|
||||
<h1>signal<span class="ext">.el</span> — screens</h1>
|
||||
<p>Four frames. No mouse targets, no chrome. Every action is a binding; conversation switching is a minibuffer completion, never a sidebar.</p>
|
||||
</header>
|
||||
|
||||
<!-- ============ SCREEN 1: chat list ============ -->
|
||||
<section class="screen">
|
||||
<div class="caption"><h2>Chat list</h2><span>*signal* — ibuffer-style root buffer, <kbd>n</kbd>/<kbd>p</kbd> to move, <kbd>RET</kbd> to open</span></div>
|
||||
<div class="frame">
|
||||
<div class="header-line"> U Name Last message When</div>
|
||||
<div class="buffer"><div class="ln"><span class="fringe">●</span><span class="unread"> 3</span> <span class="name">Family</span> <span class="dim">Dad: see you sunday then</span> <span class="faint">09:41</span></div><div class="ln hl"><span class="fringe">●</span><span class="unread"> 1</span> <span class="name">Sarah</span> <span class="dim">photo: IMG_2041.jpg</span> <span class="faint">09:12</span></div><div class="ln"> <span class="name">Mark Okafor</span> <span class="dim">me: merged, cutting the release now</span> <span class="faint">08:55</span></div><div class="ln"> <span class="name">cycling crew</span> <span class="dim">Priya: saturday 7am, usual spot?</span> <span class="faint">Mon</span></div><div class="ln"> <span class="name">Hawat alerts</span> <span class="dim">backup completed: zendo (4.2 GiB)</span> <span class="faint">Mon</span></div><div class="ln"> <span class="name">Elena V.</span> <span class="dim">thanks! that fixed it</span> <span class="faint">Sun</span></div><div class="ln"> <span class="name">school parents</span> <span class="dim">Reminder: early pickup friday</span> <span class="faint">Sun</span></div><div class="ln"> <span class="name">Tom</span> <span class="dim">me: voice message (0:42)</span> <span class="faint">Jun 5</span></div><div class="ln"> <span class="name">Note to Self</span> <span class="dim">uv pip compile --universal</span> <span class="faint">Jun 4</span></div></div>
|
||||
<div class="modeline"> <span class="e2e">⌁</span> <b>*signal*</b> 9 chats · 4 unread <span class="mode">(Signal Chats)</span> ─ U:%%- ─ Top ────────────</div>
|
||||
<div class="echo"><kbd>C-x b</kbd> switch chat <kbd>RET</kbd> open <kbd>m</kbd> mark read <kbd>g</kbd> refresh <kbd>/</kbd> filter</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- ============ SCREEN 2: conversation buffer ============ -->
|
||||
<section class="screen">
|
||||
<div class="caption"><h2>Conversation</h2><span>*signal:Sarah* — compose inline at the prompt, <kbd>RET</kbd> sends, <kbd>M-RET</kbd> newline</span></div>
|
||||
<div class="frame">
|
||||
<div class="buffer"><div class="ln"><span class="sep">───────────────────────────── Tue, Jun 9 ─────────────────────────────</span></div>
|
||||
<div class="ln"><span class="faint">[08:58]</span> <span class="name">Sarah</span><span class="dim">></span> are you taking the girls to the pool after school?</div><div class="ln"><span class="faint">[09:02]</span> <span class="me">me</span><span class="dim">></span> yeah, leaving around 4. want me to grab dinner on the way back? <span class="green">✓✓</span></div><div class="ln"><span class="faint">[09:03]</span> <span class="name">Sarah</span><span class="dim">></span> yes please. thai?</div><div class="ln"><span class="faint">[09:10]</span> <span class="me">me</span><span class="dim">></span> done. ordering the usual <span class="green">✓✓</span></div><div class="ln"><span class="faint">[09:12]</span> <span class="name">Sarah</span><span class="dim">></span> <span class="blue">[photo: IMG_2041.jpg — RET to view]</span></div><div class="ln"> </div><div class="ln"><span class="sep">───────────────────────────────────────────────────────────────────────</span></div><div class="ln"><span class="blue">></span> looks great. see you toni<span class="cursor"></span></div></div>
|
||||
<div class="modeline"> <span class="e2e">⌁ E2E</span> <b>*signal:Sarah*</b> Sarah is typing… <span class="mode">(Signal Chat Fill)</span> ─ U:%%- ─ Bot ───</div>
|
||||
<div class="echo"><kbd>RET</kbd> send <kbd>C-c C-a</kbd> attach <kbd>C-c C-r</kbd> reply-to <kbd>C-c C-e</kbd> emoji react <kbd>M-p</kbd> edit last</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- ============ SCREEN 3: minibuffer switcher ============ -->
|
||||
<section class="screen">
|
||||
<div class="caption"><h2>Minibuffer switcher</h2><span>the core interaction — <kbd>C-x b</kbd> from anywhere, fuzzy narrowing, <kbd>C-n</kbd>/<kbd>C-p</kbd>, <kbd>RET</kbd></span></div>
|
||||
<div class="frame">
|
||||
<div class="dimmed">
|
||||
<div class="buffer" style="min-height:200px"><div class="ln"><span class="faint">[09:10]</span> <span class="me">me</span><span class="dim">></span> done. ordering the usual <span class="green">✓✓</span></div><div class="ln"><span class="faint">[09:12]</span> <span class="name">Sarah</span><span class="dim">></span> <span class="blue">[photo: IMG_2041.jpg — RET to view]</span></div><div class="ln"> </div><div class="ln"><span class="sep">───────────────────────────────────────────────────────────────────────</span></div><div class="ln"><span class="blue">></span> </div></div>
|
||||
<div class="modeline"> <span class="e2e">⌁ E2E</span> <b>*signal:Sarah*</b> <span class="mode">(Signal Chat Fill)</span> ─ U:%%- ─ Bot ───</div>
|
||||
</div>
|
||||
<div class="mini"><div class="cand sel"><span class="name"><span class="m">s</span><span class="m">a</span><span class="m">r</span>ah</span> <span class="ann">direct · 1 unread · 09:12</span></div><div class="cand">school parent<span class="m">s</span> <span class="m">a</span>nd ca<span class="m">r</span>pool <span class="ann">group · Sun</span></div><div class="cand"><span class="m">S</span>amir D<span class="m">a</span><span class="m">r</span>wish <span class="ann">direct · May 30</span></div><div class="prompt"><span class="p">Switch to chat</span> <span class="count">(3/47)</span><span class="p">:</span> sar<span class="cursor"></span></div></div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- ============ SCREEN 4: which-key ============ -->
|
||||
<section class="screen">
|
||||
<div class="caption"><h2>Discoverability</h2><span>which-key panel after <kbd>C-c s</kbd> — the keymap is the menu</span></div>
|
||||
<div class="frame">
|
||||
<div class="dimmed">
|
||||
<div class="buffer" style="min-height:170px"><div class="ln"><span class="faint">[09:03]</span> <span class="name">Sarah</span><span class="dim">></span> yes please. thai?</div><div class="ln"><span class="faint">[09:10]</span> <span class="me">me</span><span class="dim">></span> done. ordering the usual <span class="green">✓✓</span></div><div class="ln"> </div><div class="ln"><span class="sep">───────────────────────────────────────────────────────────────────────</span></div><div class="ln"><span class="blue">></span> </div></div>
|
||||
<div class="modeline"> <span class="e2e">⌁ E2E</span> <b>*signal:Sarah*</b> <span class="mode">(Signal Chat Fill)</span> ─ U:%%- ─ Bot ───</div>
|
||||
</div>
|
||||
<div class="whichkey"><div class="wk"><kbd>C-c s</kbd></div><div class="wk"> </div><div class="wk"><kbd>a</kbd> <span class="arr">→</span> attach file <kbd>m</kbd> <span class="arr">→</span> mute chat <kbd>v</kbd> <span class="arr">→</span> verify safety number</div><div class="wk"><kbd>d</kbd> <span class="arr">→</span> disappearing msgs <kbd>n</kbd> <span class="arr">→</span> new chat <kbd>x</kbd> <span class="arr">→</span> delete message</div><div class="wk"><kbd>g</kbd> <span class="arr">→</span> new group <kbd>p</kbd> <span class="arr">→</span> pin chat <kbd>/</kbd> <span class="arr">→</span> search in chat</div><div class="wk"><kbd>i</kbd> <span class="arr">→</span> chat info <kbd>r</kbd> <span class="arr">→</span> mark all read <kbd>?</kbd> <span class="arr">→</span> describe bindings</div></div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
||||
Reference in New Issue
Block a user