The Kalem Book GitHub

This Book describes Kalem's main branch, which may be ahead of the latest release.

Commands, scopes and keys

Everything Kalem does is a command: saving, making text bold, moving a table row, opening the file manager. Menus, toolbars, the command palette and keys all run commands, and a key binding is keys, a command, its arguments and a condition. This chapter describes the command registry, the scope that says which kinds of text a command serves, the when-clause language that decides where a command or binding applies, and the keymap files you write to change keys. All of it is available today, and the appendix Commands and keys lists every command with its scope and default keys, generated from the registry each time the Book is built.

The code is in crates/kalem-core/src: command.rs (the registry and scopes), builtin.rs (the built-in commands), when.rs (when-clauses), document.rs (the context keys), keys.rs (key syntax) and keymap.rs (keymaps); the design is §7.3 and §11.2 of the design document, and decisions D9 and D27.

1. Commands

Field What it is
id A dotted name: org.todo.cycle, table.insertRow; for a plugin, pluginId.action
title The name in the palette and menus, translated for built-in commands
category The group in the palette: Edit, Format, TODO, Table, File, View
default_keys The keys of the Word-like profile
scope The text types the command serves (below); required
when An optional when-clause for the rest: structure at the cursor, a selection, the file kind
handler What runs it: a Rust function, or a plugin's callback
args_schema A JSON schema of its arguments, if it takes any
source Who registered it: Kalem, a plugin by ID, or the user

An ID has two or more parts separated by dots, each a lower case ASCII letter followed by ASCII letters and digits: org.headline.setLevel is valid, org.set-level and save are not. The registry refuses an invalid or repeated ID and a command without a scope. A command's change to the document is one undo step, however many edits it makes. The palette (Ctrl+Shift+P, F1, Ctrl+G in terminals) shows the commands whose when-clause holds at the cursor, found by their title in your language, their English title or their ID; kalem commands prints every command, one a line, as ID, title, scope and default keys separated by tabs, and --type csv only those serving that type.

Some commands take arguments, as a JSON object: org.todo.set takes {"state": "NEXT"}, org.headline.setLevel {"level": 2}, org.insert.block {"type": "quote"}. A key binding can pass them (the args of a keymap entry); when a required argument is missing, the editor asks for it and reads the answer as the schema's type says: a whole number for integer; y, yes, true or t for a true boolean; words separated by blanks or colons for an array; the text as it is for a string. Some prompts start with a useful value, such as the current formula for table.setFormula.

2. Scopes (D27)

Every command says where it applies: its scope is all text types, or a list of them, less an optional list of exceptions; the when-clause stays for structure (inTable, onHeadline, hasSelection) and finer conditions.

2.1. Text types

A text type is the type of the text at the cursor, innermost first:

Where the cursor is Text type
In an Org source block Its language, in lower case: python, rust
In an Org export block Its back-end: html, latex
In a LaTeX fragment or environment in Org latex
Elsewhere in an Org document org
In a Markdown, CSV or LaTeX file markdown, csv, latex
In a plain text file with a language That language, in lower case
In a plain text file without one text
In the file manager directory

Inside a block the type is the block's only in its contents; on the #+BEGIN_SRC and #+END_SRC lines it is the document's. A plain text file's language is what Kalem detected it by, a mode line, the extension or a #! line, under that language's one name: script.py, a file starting with #!/usr/bin/env python3 and #+BEGIN_SRC python all have python; .js is javascript, .sh and bash are shell, .h is c. A scope never names a mode or an editor, because the type decides them.

In Rust, a scope is Scope::all(), Scope::only(&["csv"]) or Scope::except(&["org"]); when a command is registered, its scope is turned into a when-clause over textType (Scope::only(&["org"]) becomes textType == org) and joined to the command's own with &&, so the palette, menus, key bindings and the documentation all evaluate one expression. kalem commands and the appendix write a scope as all, a list such as org, or all except org.

Most built-in commands get their scope from their when-clause: commands for whole documents and files (export., file., app., view., stats., project.) serve all types; those requiring editorMode == directory serve directory; those requiring editorMode == org serve org, so Org's editing commands work on Org text and not on the source blocks in it (Ctrl+B makes text bold in Org and does nothing in a Python block); those requiring editorMode != org serve every type but org. The line commands (lines.duplicate, lines.moveUp, lines.moveDown) set their scope to every type but org, so in a source block they move lines of code while in Org text Alt+Up moves headings, items and rows.

Kalem knows the types org, markdown, csv, text, directory, latex, html, json, txt, log, tsv, diff, patch, bib, rtf, srt, ascii, and every language it has comment markers for (rust, python, shell, go, sql, yaml, toml and about a hundred more). A binding whose when-clause compares textType with an unknown type is reported as a problem of the keymap, since it would never apply.

3. When-clauses

A when-clause is a condition on the editor's context: editorFocus && inTable, editorMode == org, !hasSelection. Commands have them, and so do key bindings.

clause     = or
or         = and { "||" and }
and        = unary { "&&" unary }
unary      = "!" unary
           | "(" or ")"
           | "true" | "false"
           | key [ ( "==" | "!=" ) value ]
value      = string | number | "true" | "false" | word
key, word  = letter-or-"_" { letter | digit | "_" | "." | ":" | "-" }
string     = "'" { any character but "'" } "'"
           | '"' { any character but '"' } '"'
number     = [ "-" ] digit { digit | "." }

&& binds tighter than ||. Keys and words may contain ., : and - after the first character, so documentHasTag:board is one key. A bare word after a comparison is a string (editorMode == org and editorMode == 'org' are the same), strings have no escapes, and there are no other operators: no <, no regular expressions, no in. A clause that cannot be read is an error with a position ("Binding 3: when-clause: expected a value at 13"), and the binding is skipped.

A bare key is true when it is set to a true value: a boolean true, a non-empty string or a number other than zero; a key that is not set is false. key == value is true when the key is set and equal, key != value when it is not, including when the key is not set; a number and a string compare equal when the string reads as that number.

3.1. Context keys

Key Value Set by
editorMode org, markdown, csv, latex, text, binary, directory document
fileKind org; only in Org documents document
editorLanguage A plain text file's language, as detected (rs, python) document
editorLayer The layer serving the document, its plugin and its ID (graph.logseq; Layers over a mode) document
folderLayer The layer serving the folder of the document's file, or the folder the file manager shows, in any mode (graph.logseq) document
vcs The version control holding the document's file (git, hg, jj, =svn=…); without a file, the first project's folder's document
textType The text type at the cursor (see Text types) cursor
hasSelection Text is selected cursor
hasComments The text has a comment syntax (Toggle Comment); not in a viewer or the file manager document
narrowed The document is narrowed to a part, in any mode editor
modified The document has unsaved changes editor
wdired The file manager's listing is being edited editor
onHeadline The cursor is on a heading line (or an inline task) cursor, Org
inTable The cursor is on a row of a table cursor, Org
inList The cursor is in a list item cursor, Org
inBlock The cursor is in a block: source, example, export, comment, verse, quote, center, special or dynamic cursor, Org
inSrcBlock The element at the cursor is a source block cursor, Org
editorFocus The editor has the keys: always in the graphical editor; in the terminal, when no prompt is open frontend
sourceView The source view is shown instead of the rich view frontend
terminal Running in the terminal editor (not set in the graphical one) frontend
inProject The document belongs to a project frontend
vimMode With the Vim profile: normal, insert, visual, visualLine, visualBlock, replace frontend
vimCommand With the Vim profile: in normal or visual mode, with no command half typed and no : line open frontend
vimActive With the Vim profile: the Vim layer is on for the document (the Word-like keys with Alt are off) frontend
vimOwnsCtrl As vimActive, where Control is Vim's (not on macOS, where Command has the Word-like keys): the Word-like keys with Control are off frontend

The Org keys (onHeadline to inSrcBlock) are false outside Org documents, and come from the previous parse while a large document is parsed again. Keys the design names for later, such as readOnly or docLanguage, are not set yet and count as false.

3.2. Menus, toolbars and the palette

A command is offered only where it can work: a menu item, toolbar button or palette entry whose command cannot run in the document is not shown, not merely greyed out. Menus and toolbars ask with the document's keys alone (editorMode, fileKind, editorLanguage, vcs, and textType set to the document's own type, not the type at the cursor), and keep a command whose clause could hold with those keys: editorMode == org && onHeadline stays in an Org document's menus and leaves a LaTeX document's. So menus do not change as the cursor moves. The palette evaluates the whole clause at the cursor.

4. Keymap files

Your key bindings are in keymap.json in the settings directory ($KALEM_CONFIG_DIR, or kalem in $XDG_CONFIG_HOME, %APPDATA% on Windows, or ~/.config). Kalem reads the file when it starts; restart it after a change. There is no project keymap.

A keymap file is a JSON array of entries, with // and /* ... */ comments allowed outside strings:

Field Required What it is
keys yes The keys (below); key is accepted as well
command yes A command ID, or - and an ID to remove bindings
args no The command's arguments, a JSON value
when no A when-clause; it applies in addition to the command's own
terminalKeys no Other keys for terminals that cannot send keys

An entry whose command starts with - removes bindings of that command, those with the given keys or all of them when keys is left out, default keys, profile keys and your own earlier entries alike. An entry that cannot be read is skipped and the rest of the file applies; a file that is not a JSON array is ignored as a whole.

[
  // Ctrl+Shift+W sets the tag "work" on a heading.
  { "keys": "ctrl+shift+w", "command": "org.tags.set",
    "args": { "tags": ["work"] }, "when": "onHeadline" },
  // Two-key sequences: Ctrl+Alt+X, then N marks a task NEXT, then Q
  // inserts a quote block.
  { "keys": "ctrl+alt+x n", "command": "org.todo.set", "args": { "state": "NEXT" } },
  { "keys": "ctrl+alt+x q", "command": "org.insert.block", "args": { "type": "quote" } },
  // Duplicate lines in Python only, in files and in source blocks.
  { "keys": "ctrl+alt+l", "command": "lines.duplicate",
    "when": "textType == python" },
  // No Ctrl+B for bold; no keys at all for Quit.
  { "keys": "ctrl+b", "command": "-org.emphasis.bold" },
  { "command": "-app.quit" },
  // A leader binding for the Vim profile; a key only the terminal editor gets;
  // a terminal variant for a chord most terminals cannot send.
  { "keys": "leader t d", "command": "org.todo.done", "when": "vimCommand" },
  { "keys": "f7", "command": "view.outline", "when": "terminal" },
  { "keys": "ctrl+enter", "command": "org.todo.cycle", "terminalKeys": "ctrl+t" }
]

4.1. Keys

A chord is modifiers and a key joined by +: ctrl+s, ctrl+shift+t, alt+up, f5; case does not matter. The modifiers are ctrl (control), alt (option, meta), shift and cmd (super, win). A key is a single character (a final + is the plus key, as in ctrl++), a function key f1 to f24, or enter, tab, escape, space, backspace, delete, insert, home, end, pageup, pagedown, up, down, left, right (ret, esc, spc, del, bs, pgup, pgdn are read as those). Emacs style is accepted too: C- for Control, M- for Alt, S- for Shift, s- for Command, and a named key in angle brackets: C-c, C-M-x, S-<up>.

A sequence is chords separated by spaces: C-c C-t, or leader p f. After the first chord Kalem waits for the next one and shows what may follow. leader (or <leader>) stands for the Vim leader, the key of editor.vim.leader. On macOS the graphical editor swaps Control and Command, so ctrl+s is Cmd+S and cmd+s is Control+S; the terminal editor never receives Command. In the graphical editor a key with Shift that types punctuation arrives as the character it types: write ctrl+? rather than ctrl+shift+/.

4.2. Precedence

editor.keymap_profile chooses word (the default) or vim. The keymap is built from the default keys of every command, then the profile's own bindings (for vim, also the leader bindings, which apply when vimCommand holds), then your keymap.json, entry by entry; a removal takes effect where it stands. When keys are pressed, among the bindings whose conditions hold, a longer binding that starts with the keys makes Kalem wait for the next key; otherwise the last binding of exactly those keys wins: yours over the profile's, the profile's over the defaults, a later entry of yours over an earlier one. So put general bindings first and special ones after; docs/keymaps/emacs.json is a complete keymap with Emacs's Org keys written this way. Take care with prefixes: a binding of C-c C-t makes Ctrl+C a prefix wherever it applies, and Copy on Ctrl+C never runs there (the keymap reports it as shadowed by a prefix), which is why that file removes Copy from Ctrl+C first.

In Vim's insert mode, Enter, Tab and Backspace are the mode's own (a new line indented as the last, the text's tab stops), before any key binding, except a binding written for insert mode: one whose condition looks at vimMode (vimMode = insert=) comes first, as a plugin's Enter in a file of its language does (the plain text accounting plugin aligns a posting's amount, then the new line).

4.3. Terminals

A terminal without an enhanced keyboard protocol cannot send every chord: Control with Shift, with a digit, with Enter or Tab, ctrl+i, ctrl+m, ctrl+h and anything with Command arrive as other keys or not at all. For such a binding the terminal editor uses its terminalKeys if the terminal can send them; else, when no other binding uses it, Alt with the same letter, digit or dot (ctrl+shift+t becomes alt+t) and ctrl+_ for ctrl+/; else the binding is dropped in that terminal. Terminals with the kitty keyboard protocol (kitty, WezTerm, Ghostty, foot) send every chord as it is.

4.4. Problems

Problem What it means
Invalid entry An entry could not be read; it is skipped
Unknown command The command does not exist; the binding is skipped
Shadowed A later binding of the same keys with the same condition always wins
Shadowed by a prefix A longer binding starts with these keys, so Kalem always waits
Unknown text type The when-clause compares textType with a type Kalem does not know
No terminal key The terminal cannot send the keys and there is no variant

They are written to the log (kalem.log in the state directory) at level info, and both editors say at startup how many problems the settings and the keymap have. The built-in profiles have none; a test keeps it so.

5. Commands of plugins

Plugins register commands with kalem::command(spec, handler), the spec holding the ID, the title, the scope, the when-clause and default keys, and bindings with kalem::keymap; they appear in the same palette, menus and keymap as the built-ins (see Plugins). kalem run PLUGIN COMMAND [FILE...] runs them without a window. Planned: key bindings in a plugin's manifest, and the inspection panel showing the registry of the running editor (T3.2.1).