Editor

Shortcuts

Shortcuts

Every keyboard shortcut outl ships with, across every client, in one table per concern.

Source of truth

Why one catalog owns every chord, and why the desktop has no character cursor: RFC 0070.

The desktop and TUI both pull their chord catalog from crates/outl-shortcuts (src/defaults.rs::default_bindings()). The mobile app doesn’t expose a keyboard surface (touch + on-screen keyboard); the rows below leave its column blank when there’s nothing to bind.

One catalog, two consumers — and only one of them resolves through it. The desktop calls outl_shortcuts::lookup(mode, chord) → Action for every keystroke, so a chord change in defaults.rs reaches it on the next build. The TUI does not. It dispatches Normal-mode keys through its own match in input/normal.rs, and input/chord_adapter.rs exists only to match plugin-contributed chords. The catalog and those match arms can disagree without anything failing to compile; a handful of reminder chords are pinned by reminder_chord_tests, and the rest are not. Treat a TUI cell below as documentation of the match, not of the catalog.

If a row below disagrees with what you observe in the app, the code is right and this doc is stale — file an issue or fix the row.

Which client performs which action is not this doc’s to answer, and the three times it tried it was wrong (see the rows fixed in the same commit as this note). That fact lives in outl_shortcuts::support and is rendered in client-parity.md, which is generated and test-pinned. This table says which key; that one says whether anything happens.

How to read the tables

  • Chrome / Global chords fire in every mode.
  • Normal / Insert / Visual / Overlay mirror the vim modes. The desktop subscribes to Normal/Visual only while editor.vim_mode = true (see docs/config.md); chrome and Insert chords are always live.
  • Cmd is the macOS modifier; Ctrl is the same chord on Linux / Windows / TUI. We list one form per row to keep the table readable.
  • A chord in the form q q is a vim-style two-key sequence: press the first, then the second within ~1 s.

Chrome (Global) — works in any mode

ActionTUIDesktopMobile
Quick switcher (fuzzy pages + journals)Ctrl+PCmd/Ctrl+Ptap toolbar
Open today’s journalt / HomeCmd/Ctrl+Jtoolbar
Cycle task state on focused or selected block — (none) → TODO → DOING → DONE → (none), one stop per press (T for task)Ctrl+T / Ctrl+EnterCmd/Ctrl+T / Cmd/Ctrl+Entertap checkbox
Run code block under cursor / selected block (X for execute). TUI: when the block isn’t code, g x instead opens the markdown link [text](url) under the cursor — an asset link in the OS default app, anything else in the browser (issue #183)g x chord / :runCmd/Ctrl+Shift+X (inside a textarea the Insert-mode strikethrough wins — commit first or use the Run button; plain Cmd+X is the OS cut / block cut)tap “Run” button
Previous journal day[Cmd/Ctrl+[swipe right
Next journal day]Cmd/Ctrl+]swipe left
Toggle sidebarCtrl+ECmd/Ctrl+Shift+E(single pane)
Toggle backlinks panelCtrl+BCmd/Ctrl+Shift+Binline below outline
Toggle backlinks order (newest/oldest)Ctrl+Odirection button in the backlinks headerdirection button in the backlinks header
Open settingsvia :settingsCmd/Ctrl+,gear icon
Toggle help overlay?Cmd/Ctrl+/help button
Quitq q (chord) / Z Z (vim alias) / Ctrl+CCmd/Ctrl+Q (OS)

Why Cmd+J and not Cmd+T for today’s journal? Every outliner ecosystem uses T for task / TODO — TUI’s Ctrl+T, Logseq’s Cmd+T, the universal Markdown checkbox shortcut. Re-training that muscle memory would be hostile. J for journal is unambiguous and lines up with the TUI’s g j chord.

Defaults the user often asks about. Both clients ship with sidebar and backlinks panel HIDDEN (show_sidebar: false, show_backlinks: false). Editor-hero on first launch — the user opts the panels in with the chord. This matches Bear / Ulysses on the desktop and outl-tui’s historical behaviour.

ChordWhy not
Cmd+BReserved for bold in Insert mode — every popular markdown editor (Notion, Obsidian, Discord, Slack, Typora) treats it that way. Hijacking would be hostile.
Cmd+\macOS 1Password global autofill. Stealing it breaks every 1Password user.

So Cmd+Shift+E (VS Code’s “Show Explorer”) and Cmd+Shift+B are the canonical chrome chords on the desktop. The TUI mirrors the spirit with Ctrl+E / Ctrl+B (most terminals collapse Ctrl+Shift+letter into Ctrl+letter, so both forms work identically).


Inline markdown — Insert mode (textarea focused)

Wrap the current selection (or insert the delimiter pair around the caret). Mirrors the convention every markdown editor on the planet ships.

ActionTUIDesktopMobile
Bold (**…**)type **Cmd/Ctrl+Btoolbar B
Italic (_…_)type _Cmd/Ctrl+Itoolbar I
Inline code (`…`)type `Cmd/Ctrl+Etoolbar <>
Strikethrough (~~…~~)type ~~Cmd/Ctrl+Shift+Xtoolbar S
Link ([label](url))type [Cmd/Ctrl+Ktoolbar 🔗

outl ships _…_ as the canonical italic. The parser still accepts *…* for compatibility, but .md projections emit underscores.


Outline navigation — Normal mode

The desktop honours Normal/Visual only while editor.vim_mode = true. The TUI is vim-style by definition.

ActionTUIDesktop (vim on)Mobile
Selection downj / j / tap block
Selection upk / k / tap block
Enter Insert at end of blockiitap block
Enter Insert at start of blockIItap at start
Enter Insert one char past cursor (vim append)aa (= i, no char cursor)
Enter Insert at end of block (vim A)AA
Substitute block (clear + Insert at col 0; S / cc)SS
Substitute char under cursor (= xi)s(char cursor only)
Yank current block to register + OS clipboard (Y, alias of y y)YYlong-press → “Copy text”
Paste OS clipboard with formatting (outline structure / multi-paragraph split)pCmd/Ctrl+Vpaste
Paste OS clipboard without formatting (raw text, single block)PCmd/Ctrl+Shift+V
Open [[ref]] / #tag / ((blk-…)) under cursorEnterEntertap
New block below + Insertoo / Cmd/Ctrl+Shift+Enter (no vim needed)toolbar +
New block above + Insert (creates a sibling before the selected block)OOlong-press → “New block above”
Indent blockTabTabdrag right
Outdent blockShift+TabShift+Tabdrag left
Move block up among siblingsKCmd/Ctrl+Shift+↑drag
Move block down among siblingsJCmd/Ctrl+Shift+↓drag
Cut block + subtree (move-by-id; paste keeps ((blk-…)) refs)Cmd/Ctrl+Xlong-press → “Cut block” (fresh id on paste, see note below)
Copy block + subtree (paste duplicates with fresh ids)Cmd/Ctrl+Clong-press → “Copy block”
Paste block after the selection (cut → move, copy → duplicate)Cmd/Ctrl+Vlong-press → “Paste block”
Cancel a pending cutEsc
Delete block (chord)d dd dswipe left
Fold / unfold (toggle collapsed)cctap bullet
Unfold all on the page (chord)z Rz Rtap header “Unfold all” button
Fold all on the page (chord)z Mz Mtap header “Fold all” button
Center viewport on cursor (chord)z zz z
Zoom in on block (make it the outline root)z iz i / Cmd/Ctrl+Shift+]tap bullet
Zoom out (back up one level toward the page)z oz o / Cmd/Ctrl+Shift+[tap breadcrumb / back
Last block (jump)GG
First block (chord)g gg g
Reselect last Visual range (chord)g vg v
Search workspace for word / block text — forward** (seeds picker)page switcher “Blocks” tab (no hit-stepping)
Search workspace for word / block text — backward## (seeds picker)page switcher “Blocks” tab (no hit-stepping)
Undo last committed block mutationuu / Cmd/Ctrl+Zkeyboard toolbar
RedoCtrl+RCtrl+R / Cmd/Ctrl+Shift+Zkeyboard toolbar
Yank block ref → clipboard (chord)y r(chord is in the catalog, no handler — #parity)long-press → “Copy block ref”
Enter Visualvv
Open command palette:(chord is in the catalog, no handler — #parity)
Open slash menu///

About a / * / # on the desktop. The desktop’s Normal mode has only a selected block id — no character cursor inside the block. So a collapses to i (the textarea’s own caret takes over), and * / # seed the picker with the first few words of the selected block’s text instead of doing a word-under-cursor search. The catalog still ships these chords so muscle memory from the TUI carries over.

Cmd+X / Cmd+C / Cmd+V are mode-aware on the desktop. Inside a block editor (Insert mode, a <textarea> is focused) they are the OS-native text cut / copy / paste — the chords aren’t in the catalog there, so the keystroke reaches the webview untouched. In view mode (Normal, nothing focused) they act on the whole selected block + its subtree: cut marks it to move by id (the paste emits a single Op::Move, so ((blk-…)) refs and backlinks survive — and the target may live on another page, moving the block across pages), copy snapshots it as markdown (the paste duplicates with fresh ids). This is also why run code block moved off Cmd+X to Cmd+Shift+X (view mode): a text-editing app has to let the OS-wide cut win.

Mobile’s “Cut block” mints a fresh id, unlike the desktop’s Cmd/Ctrl+X. The desktop’s block clipboard tags a cut with {kind: "cut", nodeId} and pastes it as a single Op::Move, so the block keeps its identity and every ((blk-…)) ref to it stays valid. Mobile’s long-press “Cut block” instead renders the subtree to markdown and deletes the source in one round-trip, then pastes it back via the same fresh-id path its “Copy block” already uses — a ((blk-…)) ref pointing at the cut block goes stale across the round-trip. Use “Copy block ref” first if something else in the workspace links to the block you’re about to cut.

Cmd/Ctrl+Shift+Enter works without vim mode. Unlike o, the chord is not vim-gated: with no textarea focused the desktop falls into Normal dispatch regardless of the vim_mode setting, so every user can append a block from view mode. Inside a block editor the same chord commits the current edit first (Insert-mode CommitAndContinue). Both Cmd+Shift+Enter (macOS) and Ctrl+Shift+Enter (Windows / Linux) are bound in each mode.

Cursor inside a block (Normal)

These rely on a character cursor inside the selected block. The TUI ships it natively; the desktop has only a selected block id, so the char-cursor ops surface a status-line nudge instead of firing.

ActionTUIDesktop (vim on)
Char left / righth / l (or arrows)h / l
Word right / leftw / bw / b
Word end forward (vim e)e
Start / end of block text0 / $ (or Home/End)0 / $
Find char forward / backward (next typed char)f{ch} / F{ch}
Delete char under / before cursorx / X
Delete to end of block (D) / change to end (C)D / C
Replace char under cursor with next typed charr{ch}
Toggle case of char under cursor; advance~

Insert mode (text editing)

ActionTUIDesktopMobile
Commit + exit InsertEscEsc / blurblur
Newline inside the block (multi-line text)Shift+EnterShift+EnterEnter
Split block at cursor into two (caret at end → empty sibling below; caret at start → empty block above; caret mid-text → head stays, tail becomes the new sibling, caret lands at its start)EnterEnterEnter
Commit + new block, caret-aware (caret at col 0 → before the block / vim O; past col 0 → below)Cmd/Ctrl+Shift+Enter
Indent (stay in Insert)TabTabdrag
Outdent (stay in Insert)Shift+TabShift+Tabdrag
Delete block on emptyBackspace on emptyBackspace on empty
Auto-pair ( [ { [[ ((yesyesyes
Ref autocomplete[[ triggers picker[[ triggers picker[[ triggers picker
Tag autocomplete# triggers picker# triggers picker
Block ref autocomplete(( triggers picker(( triggers picker
Slash command autocomplete//
Cycle task state on current (TODODOINGDONE → none)Ctrl+T / Ctrl+EnterCmd/Ctrl+T / Cmd/Ctrl+Entertap checkbox / long-press menu
Cut / copy / paste text (native)Cmd/Ctrl+X / Cmd/Ctrl+C / Cmd/Ctrl+Vnative
Run code blockg x chord(commit with Esc, then Cmd/Ctrl+Shift+X)tap “Run”

Visual mode (range)

TUI + desktop; mobile has no Visual equivalent yet.

On the desktop, Shift+↑ / Shift+↓ start (and keep growing) a contiguous selection without vim mode — the non-vim multi-select entry. It flips the client into Visual and pops a floating batch toolbar (N selected + Indent / Outdent / Move up / Move down / Delete / Done) so the range ops are reachable by mouse; the toolbar fires the same actions the chords do. Only the toolbar’s Delete confirms before erasing a range that contains nested children; the keyboard delete (d / x / Delete / Backspace) and the TUI delete without a prompt, matching vim.

ActionTUIDesktop
Start / extend selection downj / Shift+↓ (any mode) · j / (vim)
Start / extend selection upk / Shift+↑ (any mode) · k / (vim)
Yank rangeyy
Delete range (toolbar Delete confirms if a block has children; the keys don’t)d / xd / x · Delete / Backspace · toolbar Delete
Indent range (vim >)Tab / >> · toolbar Indent
Outdent range (vim <)Shift+Tab / << · toolbar Outdent
Move range up among siblingsAlt+↑Cmd/Ctrl+Shift+↑ · toolbar
Move range down among siblingsAlt+↓Cmd/Ctrl+Shift+↓ · toolbar
Leave Visual (captures range so a follow-up g v restores it)EscEsc · toolbar Done

Page operations

g d (Normal mode, “go delete”) is the canonical chord for page deletion. It lives in the shared outl-shortcuts catalog, same g<action> family as g j (today) / g x (execute) / g p (properties). The chord deletes the focused page (sidebar-highlighted row when the sidebar has focus on the TUI, otherwise the current page). Each client confirms before invoking outl_actions::page::delete. Journals are refused everywhere. Clients also expose the action through their native page-list affordance.

ActionTUIDesktopMobile
Delete the focused page (with confirmation)g d (Normal mode) deletes the current page; sidebar d deletes the focused sidebar rowg d (Normal mode) + hover × button on the sidebar rowlong-press the row in the page switcher

The desktop routes g d through the same DeletePage handler in action-handlers.ts (same window.confirm + deletePage(slug) flow as the × button). Mobile has no keyboard surface. Long-press in the page switcher remains the only trigger on touch devices.


Reminders (remind::)

Authoring a reminder and inspecting the schedule are chorded on every client; delivering it is per-OS and never a chord. Every client delivers, the TUI included (an OSC 9 notification plus a toast, on its event-loop tick). What the TUI can’t do is deliver while it’s closed, since a terminal session has no background presence — see reminders.md.

ActionTUIDesktopMobile
Add a reminder to the selected blockg r (writes remind:: 9am; reports the existing rule instead of overwriting)Cmd+R, or g r in Normallong-press the block → Remind me…
”Nag me” preset (remind:: now every 1h until DONE)g R (overwrites — escalating is explicit)g R in Normal
Open the reminders listg nCmd+Shift+R / Ctrl+Shift+Rbell icon in the header
In the list: open the reminder’s pageEnterEnter / click the rowtap the row
Snooze the selected block 1h, without opening the listg sg s in Normalthe chips in the sheet (no chord surface)
In the list: snooze 1hsthe 1h / tomorrow / next week buttonsthe 1h / Tomorrow / Next week chips
In the list: closeEsc / qEscdrag down / tap outside

Why g n in the TUI and not Ctrl+R? Ctrl+R is already Redo in Normal mode, and a terminal cannot distinguish Ctrl+R from Ctrl+Shift+R. The g family (g j, g x, g d) is the honest home for it there; only the desktop takes the Cmd/Ctrl+Shift+R spelling. Ctrl+R on the desktop likewise stays Redo on Linux / Windows, which is why authoring is Cmd+R only.

Snoozing writes Op::SnoozeRemind, so it converges: silencing a nag on the phone silences the same block on the laptop.


Properties (key:: value)

remind:: above is one property with its own chords; this is the generic door. Creating the first property on a block used to be impossible in either GUI client — you could edit a chip that already existed and nothing more (issue #13).

ActionTUIDesktopMobile
Open the property editor for the selected blockg pg p in Normal — the chip row is always visible, so this opens the blank pair (same as Cmd+Shift+P)long-press the block → Properties…
Add a property to the selected blocko inside g p, or /prop <key> <value>Cmd+Shift+P / Ctrl+Shift+P in Normal, or the + prop button at the end of the chip row (visible on block hover)+ Add property at the top of the sheet
Edit a property’s valueEnter inside g p, or /prop <key> <value>click the chiptap the chip
Delete a propertyd d inside g p, or /prop <key> (empty value)the × on the chipswipe the row left, or Delete inside the editor
Add / edit a page propertyp inside g p flips to page scope, or /prop-page <key> <value>the chip row under the page title (always visible)long-press the title, the Block ⇄ Page switch in the sheet, or the Properties button on an empty page
Complete the key from the workspace catalogueTab in the key fieldtypeahead on the key fieldthe most-used keys as tappable chips, plus Other… for the keyboard
Pick a [[page]] as the value[[ / # in the value field[[ in the value field opens the page list (↑/↓, Enter or click)the [[ picker
Close the editorEsc / qEscswipe the sheet down, or ✕

The key field completes from the workspace’s own key catalogue (outl_actions::known_keys), most-used first, skipping keys the block already carries. Deleting and “empty the value” are the same backend call — the × exists because a gesture nobody can see is not an affordance.

The TUI gets an overlay rather than a form. It is modal and vim, so tabbing between a key field and a value field fights the mode: Tab there means complete, and Enter is what advances key → value → save. /prop and /prop-page stay for muscle memory; the overlay is for when you don’t remember the key.

g p used to mean “pin”. The pinned:: toggle moved to g P (/pin is unchanged). Pinning is a once-per-page act with a second door already; editing properties is a daily one — and pinned:: is itself one of the page properties g p now edits.


Overlays (picker, palette, settings, help)

ActionTUIDesktop
Highlight next / Tab / Ctrl+J
Highlight previous / Shift+Tab / Ctrl+K
ConfirmEnterEnter
Close overlayEscEsc

The picker (Cmd+P / Ctrl+P) fuzzy-matches pages and journals together; type a date in ISO (2026-06-04) or natural (today, yesterday) to jump.


Where each chord lives in the code

LayerFileWhat it owns
Canonical catalogcrates/outl-shortcuts/src/defaults.rsEvery (mode, chord, action, description) row.
Action enumcrates/outl-shortcuts/src/action.rsThe named operation each chord resolves to.
TUI key dispatchcrates/outl-tui/src/input/normal.rsIts own match on crossterm::KeyEvent. Does not call lookup.
TUI chord adaptercrates/outl-tui/src/input/chord_adapter.rscrossterm::KeyEvent → Chord, for plugin keybindings only.
Desktop input adaptercrates/outl-desktop/src/lib/shortcuts.tsKeyboardEvent → Chord.
Desktop dispatchercrates/outl-desktop/src/lib/action-handlers.tsAction → Tauri command.
Mobile toolbar / gesturescrates/outl-mobile/src/components/Per-component on-screen handlers.
Per-client supportcrates/outl-shortcuts/src/support.rsWhether each client performs each action, and the sentence shown when it can’t. Rendered in client-parity.md.

A chord change is a single line in defaults.rs plus, if the action is new, a row in action.rs and a handler in each client. See crates/outl-shortcuts/CLAUDE.md for the full add-a-binding checklist.


Help overlay vs. this doc

In the TUI, press ? (Normal mode) to see the live chord table baked into the binary — it’s generated from the same default_bindings() table this doc describes. In the desktop, Cmd+/ opens the same overlay. If you need to look something up while typing, the in-app overlay is faster than this page.

This doc exists so a contributor (or a user shopping for outl) can see every shortcut without launching the app.