Commands, scopes and keys
Table of Contents
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).