
LEM is the built-in text editor for Lilush Shell. It provides Vi-inspired modal editing with syntax highlighting, multiple buffers, and a piece table buffer for efficient editing with unlimited undo.
LEM is also a tribute to Stanisław Lem, the Polish science fiction author.
Related docs:
SHELL for the shell mode system and builtin contract
COMPLETION for the completion engine architecture
CALM for language model integration
TSS for the styling engine
THEMES for the theme system
LEM runs inside the lilush process as a shell builtin, sharing the event loop, theme system, and loaded modules. It uses the alternate screen buffer and the Kitty keyboard protocol for full modifier and key-event support.
Key characteristics:
Modal editing with normal, insert, visual, and command modes
Piece table buffer implemented in C for fast insert/delete and unlimited linear undo/redo with edit grouping
Multiple buffers switchable via commands or keybindings (no splits)
Syntax highlighting via Lua-scriptable per-filetype highlighters
Bracket pair matching -- highlights both brackets when cursor sits on one
Inline diagnostics for JSON and Lua -- syntax errors highlighted in real time
Ghost-text completion -- buffer-word and Lua (keywords + _G symbols)
CALM prose completion -- secondary ghost text driven by a CALM language model, for markdown and plain-text buffers (manual trigger)
TSS-styled rendering -- all chrome inherits the shell's theme system
lem # open empty buffer
lem file.lua # open file
lem file1.lua file2.c # open multiple files (first is active)
lem +42 file.lua # open file, jump to line 42
The +N syntax jumps to line N on startup.
LEM requires a terminal (refuses to run without a TTY). It registers
as a shell builtin with fork = "never", so it runs in the current
process without forking.
LEM has four modes. The active mode determines how keystrokes are interpreted.
ESC / operation complete
+------------------------------+
| |
v i/a/o/O/I/A |
+---------+ ------------------> +---------+
| NORMAL | | INSERT |
| | <------------------ | |
+----+----+ ESC +---------+
|
| v/V
|
v
+---------+
| VISUAL |
| (char/ | ---- ESC ----> NORMAL
| line) |
+---------+
: (from NORMAL)
|
v
+---------+
| COMMAND | ---- ESC/ENTER ----> NORMAL
+---------+
The default mode. Keystrokes are interpreted as motions, operators,
or commands. Supports the [count]operator[count]motion/text-object
grammar. In operator-pending form, counts multiply, so 2d3w acts like
d6w, and line-target motions follow the same rule (2d3G targets
line 6).
| Key | Description |
|---|---|
d | Delete the motion/text-object range, yank to register |
c | Delete + enter insert mode |
y | Copy (yank) the range to register |
> | Indent lines covered by motion |
< | Dedent lines covered by motion |
Operators wait for a motion or text object. Doubling the operator key
applies it to the current line (dd deletes line, yy yanks line,
>> indents line).
All motions accept an optional count prefix: 3w moves three words
forward, 5j moves five lines down.
| Key | Description |
|---|---|
h / l | Cursor left / right |
j / k | Cursor down / up (preserves target column) |
w / b / e | Word forward / backward / end |
W / B / E | WORD forward / backward / end (whitespace-delimited) |
0 | First column |
^ | First non-whitespace character |
$ | End of line |
gg | First line (or line N with count) |
G | Last line (or line N with count) |
{ / } | Previous / next blank line (paragraph motion) |
f{c} / F{c} | Find char forward / backward on line (count finds the Nth match) |
t{c} / T{c} | Till char forward / backward on line (count targets the Nth match) |
; / , | Repeat / reverse last f/F/t/T (count repeats N times) |
% | Matching bracket ()[]{} |
| Arrow keys | Cursor movement (also works in insert mode) |
HOME / END | First non-blank / end of line |
PAGE_UP / PAGE_DOWN | Scroll by screen height |
CTRL+f / CTRL+b | Same as PAGE_DOWN / PAGE_UP |
Used after an operator, prefixed with i (inner) or a (around):
| Object | Inner (i) | Around (a) |
|---|---|---|
w | Word characters | Word + surrounding space |
W | WORD characters | WORD + surrounding space |
" / ' / ` | Inside quotes | Including quotes |
( / ) / b | Inside parentheses | Including parens |
[ / ] | Inside brackets | Including brackets |
{ / } / B | Inside braces | Including braces |
< / > | Inside angle brackets | Including brackets |
| Key | Description |
|---|---|
x | Delete character under cursor |
X | Delete character before cursor |
r{c} | Replace character under cursor with {c} |
J | Join current line with next line |
p / P | Paste after / before cursor |
u | Undo |
CTRL+r | Redo |
. | Repeat last edit operation |
~ | Toggle case of character under cursor |
* / # | Search forward / backward for word under cursor |
n / N | Next / previous search match |
m{a-z} | Set mark |
`{a-z} | Jump to mark (exact position) |
'{a-z} | Jump to mark (start of line) |
ZZ | Save and quit |
ZQ | Quit without saving |
CTRL+p | Open file chooser |
CTRL+SHIFT+p | Open file chooser and insert chosen file after cursor (like :r) |
CTRL+n | Next buffer |
| Key | Description |
|---|---|
i | Insert before cursor |
I | Insert at first non-blank of line |
a | Insert after cursor |
A | Insert at end of line |
o | Open new line below, enter insert |
O | Open new line above, enter insert |
Keystrokes are inserted as text. Special keys:
| Key | Action |
|---|---|
ESC | Return to normal mode, seal undo group |
BACKSPACE / CTRL+h | Delete character before cursor |
DELETE | Delete character under cursor |
ENTER | Insert newline (with autoindent if enabled) |
TAB | Insert configured indentation (spaces or tab) |
CTRL+w | Delete word before cursor |
CTRL+u | Delete to start of line |
CTRL+n / CTRL+DOWN | Scroll completion candidates forward (secondary if active with >1, else primary) |
CTRL+p / CTRL+UP | Scroll completion candidates backward (secondary if active with >1, else primary) |
CTRL+space | Trigger CALM prose completion (secondary ghost) |
CTRL+l | Accept active CALM ghost in full |
CTRL+RIGHT | Accept next word from active CALM ghost |
| Arrow keys | Move cursor without leaving insert mode |
All typing within an insert session (from entering insert mode to pressing ESC) is grouped as a single undo entry.
Entered from normal mode with v (character-wise) or V (line-wise).
Motions extend the selection from the anchor (entry point) to the cursor.
| Key | Action |
|---|---|
| Motions | Extend selection |
o | Swap cursor and anchor |
d / x | Delete selection |
c | Change selection (delete + insert) |
y | Yank selection |
> / < | Indent / dedent selected lines |
~ | Toggle case of selection |
v / V | Switch visual sub-mode or cancel |
ESC | Cancel selection, return to normal |
Entered by pressing :, /, or ? in normal mode. A command line
appears at the bottom of the screen.
| Command | Description |
|---|---|
:w [path] | Save (optionally to a different path) |
:q | Quit (fails if unsaved changes) |
:q! | Force quit, discard changes |
:wq | Save and quit |
:x | Save if modified, then quit |
:e <path> | Open file in new buffer |
:bn / :bp | Next / previous buffer |
:bd | Close current buffer |
:b <N\|name> | Switch to buffer by number or partial name match |
:ls | List open buffers |
:r <path> | Read file and insert contents below cursor |
:<N> | Jump to line N |
| Command | Description |
|---|---|
/<pattern> | Search forward (plain text, not regex) |
?<pattern> | Search backward |
:s/pat/rep/[flags] | Substitute on current line |
:%s/pat/rep/[flags] | Substitute in entire file |
:<range>s/pat/rep/[flags] | Substitute in line range |
Search and substitute patterns use Lua patterns. Flag g replaces
all occurrences on each line (without it, only the first match per
line is replaced).
Range syntax: N (line N), . (current line), $ (last line),
% (entire file = 1,$), N,M (lines N through M).
| Command | Description |
|---|---|
:set number / :set nonumber | Toggle line numbers |
:set wrap / :set nowrap | Toggle line wrapping |
:set tabstop=N | Tab display width |
:set shiftwidth=N | Indentation width for > / < |
:set expandtab / :set noexpandtab | Tabs as spaces or literal tabs |
:set autoindent / :set noautoindent | Auto-indent new lines |
:set scrolloff=N | Minimum lines above/below cursor |
:set page_scroll_overlap=N | Lines of overlap kept when PAGE_DOWN/UP scrolls |
:set filetype=X | Override filetype detection |
:set | Show all current settings |
| Command | Description |
|---|---|
:calm / :calm status | Show active CALM model, enable state, path |
:calm on / :calm off | Enable / disable the CALM source |
:calm model <name> | Switch to a registry-named model (session override) |
:calm model auto | Clear override, return to mode-map mapping |
The piece table is the core data structure for text storage, implemented
as a C module (lem.piece_table) for performance.
A piece table represents a buffer as a sequence of pieces, each referencing a contiguous span in one of two backing buffers:
Original buffer: The file content as read from disk. Immutable.
Add buffer: All inserted text, appended sequentially. Append-only.
Editing operations modify only the piece list, never the backing data. This gives efficient insert/delete regardless of position and preserves the original file content until an explicit save.
The piece table maintains a line index -- an array of byte offsets for each newline. The index is rebuilt on each edit. This provides O(1) line-to-offset and offset-to-line translation.
Undo is unlimited and linear (no branching history). Each edit pushes a snapshot to the undo stack. Redo is populated when undo is invoked; any new edit after undo clears the redo stack.
Edit grouping: Consecutive inserts in insert mode are grouped into
a single undo entry. The group is sealed when the user leaves insert
mode. This matches vi behavior: u undoes the entire insert session.
local pt = require("lem.piece_table")
local p = pt.new(content_string) -- or pt.new() for empty buffer
p:insert(offset, text) -- 0-based byte offset
p:delete(offset, length)
local text = p:get_text(offset, length)
local total = p:length()
-- Line index (1-based line numbers)
local count = p:line_count()
local off = p:line_offset(line)
local len = p:line_length(line)
local line = p:offset_to_line(offset)
-- Undo/redo
p:undo() -- returns true/false
p:redo()
p:group_open()
p:group_close()
-- Full content for saving
local content = p:snapshot()
-- GC releases resources, or explicit:
p:close()
A buffer wraps a piece table with file metadata and editor state (cursor position, scroll state, marks).
| Field | Description |
|---|---|
cfg.path | File path (nil for unnamed/scratch) |
cfg.filetype | Detected filetype (for highlighting) |
cfg.readonly | Prevent modifications |
__cursor | Cursor position {line, col} (1-based) |
__scroll | Viewport scroll state {top_line, left_col} |
__marks | Named marks {name = {line, col}} |
__version | Monotonic counter bumped on every edit |
Modification state is exposed through buf:is_modified(), which
returns true whenever the buffer is not at the last-saved position
in its undo history.
Filetype is detected from the file extension, with a fallback to shebang inspection. Extensions with a bundled highlighter:
| Extensions | Filetype |
|---|---|
.lua | lua |
.c, .h | c |
.md | markdown |
.json | json |
.sh, .bash | sh |
.lsh | lsh |
.py | python |
Filenames Makefile / GNUmakefile and Dockerfile (case-insensitive)
are recognized as make and dockerfile respectively. Shebang lines
(#!/usr/bin/env lua, #!/bin/sh, #!/usr/bin/env python, etc.)
provide detection when the extension is absent; recognized interpreters
include lua, luajit, python/python3, ruby, sh/bash/dash/
zsh, node, and perl.
Additional extensions (.go, .rs, .js, .ts, .yaml/.yml,
.toml, .rb, .html, .css, .xml, .conf, .ini) are detected
and assigned a filetype label, but there is no bundled highlighter for
them. Register one via highlight.register(filetype, module_name) to
enable highlighting.
The buffer list manages open buffers with :e, :bn, :bp, :bd,
:b, and :ls. CTRL+n cycles to the next buffer from normal mode.
When the last buffer is closed, the editor exits.
Closing a modified buffer requires :bd! or saving first.
LEM has a minimal register system for yank/paste operations.
| Register | Description |
|---|---|
"" | Unnamed -- default target for all yank/delete |
"0 | Yank register -- last yanked text (not affected by deletes) |
"_ | Black hole -- discard (delete without storing) |
"+ | System clipboard via OSC 52 |
Access: "<register><operator> -- for example, "+y yanks to the
system clipboard, "0p pastes the last yank.
Linewise vs. character-wise paste is determined by how the text was originally yanked or deleted.
The "+ register uses OSC 52 terminal escapes for clipboard writes.
This works over SSH sessions and in terminals that support OSC 52
(kitty, foot, alacritty, ghostty, etc.). Paste from the system
clipboard is delivered via bracketed paste mode.
Syntax highlighting uses Lua-scriptable per-filetype highlighters. Each highlighter processes one line at a time and returns an array of styled spans.
Selective, not exhaustive. Most code stays in base text color. Highlighting is reserved for things that benefit from visual separation.
Minimal palette. The default theme targets five semantic colors.
Highlight the rare, leave the common. Constants and definitions get color. Variable usage and function calls stay in base text.
Language keywords are scaffolding. if, for, local render
in base text. The names and values beside them matter more.
Comments deserve attention. Explanatory comments get a visible, distinct color -- not greyed out.
Punctuation steps back. Brackets and delimiters are dimmed below base text.
Red is for errors. No ordinary token type uses red.
| Token type | TSS path | What it covers |
|---|---|---|
literal | syntax.literal | Strings, numbers, booleans, nil/null |
definition | syntax.definition | Names being defined (functions, variables) |
comment | syntax.comment | Explanatory comments |
disabled | syntax.disabled | Commented-out code, inactive preprocessor branches |
punctuation | syntax.punctuation | Brackets, semicolons, commas, dots |
operator | syntax.operator | Arithmetic, logical, comparison operators |
directive | syntax.directive | Preprocessor directives, shebangs |
error | syntax.error | Syntax errors, unmatched brackets |
Untagged text renders in the base text color. This is the default for variable references, function calls, and language keywords.
Constructs that span multiple lines (block comments, multiline strings, heredocs) are tracked via line state -- a value passed from each line to the next indicating whether a multiline construct is open.
| Filetype | Module | Notes |
|---|---|---|
lua | lem.highlight.lua | Long brackets, block comments with nesting |
c | lem.highlight.c | Block comments, preprocessor directives |
markdown | lem.highlight.markdown | Fenced code blocks, headings, inline code |
json | lem.highlight.json | Object keys vs. string values |
sh | lem.highlight.sh | Heredocs, variable expansion |
lsh | lem.highlight.lsh | Reuses sh highlighter |
python | lem.highlight.python | Triple-quoted strings, decorators, string prefixes |
A highlighter module exports new() and returns a table with:
local new = function()
return {
-- Returns {from, to, token_type} spans (1-based byte positions)
-- and the new line state for multiline tracking.
highlight_line = function(self, text, prev_state)
-- ...
return spans, new_state
end,
-- Reset state (e.g., on file reload).
reset = function(self) end,
}
end
Register custom highlighters via highlight.register(filetype, module_name)
before the editor starts.
When the cursor is on a bracket character ((, ), [, ], {, }),
LEM highlights both the bracket under the cursor and its matching
counterpart. The highlight uses the text.bracket_match TSS path.
The matching uses the same depth-tracking algorithm as the % motion:
nested brackets are handled correctly across lines. If no match exists
(unmatched bracket), no highlight is shown.
The highlight is computed on every render and appears in all modes.
LEM provides real-time syntax validation for JSON and Lua files. When a syntax error is detected, the editor shows three indicators:
Gutter marker: The line number on the error line is rendered
with the gutter.diagnostic_error style (bold red by default).
Inline highlight: The character at the error position is
styled with text.diagnostic (underlined red by default).
Message bar: When the cursor is on the error line, the error message is shown in the message area at the bottom of the screen.
| Filetype | Validation method | Error granularity |
|---|---|---|
json | cjson.safe.decode() | Line and column |
lua | loadstring() (compile only) | Line |
Only the first error is reported (single-error model).
Validation runs only when the buffer content changes (tracked via an internal version counter), not on cursor-only movements.
LEM shows completion suggestions as dim ghost text after the cursor while typing in insert mode. There is no popup list -- completions are end-of-line only.
A session auto-triggers on every insert-mode edit whenever the cursor is at end-of-line and the prefix immediately before the cursor is at least two characters long. Typing more characters narrows the active session; deleting characters or moving the cursor refreshes it. Leaving insert mode, moving the cursor away from end-of-line, or reducing the prefix below the threshold dismisses the session.
When the prefix and cursor position are unchanged between refreshes, the currently chosen candidate is preserved so scrolling doesn't reset as you keep typing the same word.
For .lua buffers the session merges three sources in this order:
Lua keywords (and, break, do, ...)
Lua symbols from an enriched global table: every name visible in
_G plus the modules configured in cfg.lua_preloads (by default
std, crypto, dns, term, lev, mneme, http, json,
wg; the json binding loads cjson.safe and wg loads
wireguard). Dotted access like std.fs.r walks the table path
and lists matching members. Add extra modules by overriding
lua_preloads in your LEM config.
Unique identifiers harvested from all open buffers (active buffer first).
For every other filetype only the buffer-word source runs.
The buffer-word source scans each buffer's piece table once per
__version change and caches the token set, so repeated completions
on an unchanged buffer do not rescan.
| Key | Action |
|---|---|
CTRL+n / CTRL+DOWN | Next candidate |
CTRL+p / CTRL+UP | Previous candidate |
TAB | Accept the ghost |
TAB falls through to its normal indent behavior when no session is
active. Any other key ends the session implicitly by changing the
prefix or cursor position; the ghost disappears on the next refresh.
Ghost text uses the single completion TSS path from the lem theme
section (gray italic by default). The secondary (CALM) ghost uses
completion_secondary (accent-color italic by default).
For prose filetypes (markdown, text, and unnamed buffers), LEM can
draw a second ghost fed by a CALM language model. The CALM
ghost is secondary: it coexists with the regular buffer-word / Lua
ghost and never replaces it. Both ghosts render simultaneously -- the
primary at the cursor, the CALM ghost immediately after.
CALM is manual-only: CTRL+space in insert mode runs inference and
installs the secondary ghost. There is no keystroke-by-keystroke
auto-trigger -- prose writing has natural pauses, and running the model
on every keypress would stall the editor.
Any subsequent edit or cursor move dismisses the secondary ghost (its anchor becomes stale); the user can re-trigger at the new position.
CALM activates only for filetypes in cfg.calm_filetypes. The default
is {markdown, text, ""} (markdown, plain text, and buffers with no
detected filetype). Override programmatically before startup:
local lem_cfg = require("lem.config").new({
calm_filetypes = { markdown = true, text = true },
})
The source resolves its model in this order:
LILUSH_LEM_MODEL environment variable (absolute path override)
calm.registry lookup for mode_map["lem"]
Registry default (same fallback the shell uses)
To use a dedicated prose model, add an entry to
~/.config/lilush/calm/registry.json:
{ "default": "shell",
"mode_map": { "lem": "prose" } }
:calm model <name> installs a runtime override; :calm model auto
clears it and returns to the mode-map mapping.
The source builds a prose-domain sequence directly, without frame tokens:
<BOS> <ATN> [lookback text as byte tokens...]
Lookback is the text from the start of the buffer up to the cursor,
capped at the model's l_max budget (minus BOS, ATN, and generation
headroom). The trim walks backward to the nearest paragraph boundary
(double newline), then sentence boundary (.!? + space/newline), then
word boundary -- never splits mid-word.
Sampler parameters come from the CWGT metadata (sampler_defaults) if
present, with fallbacks: temperature=0.8, top_k=8, max_tokens=40,
num_candidates=1.
| Key | Effect |
|---|---|
CTRL+l | Accept full ghost text, advance cursor to its end |
CTRL+RIGHT | Accept leading whitespace + next word + trailing space; remainder stays as ghost, cursor advances |
CTRL+n / CTRL+DOWN | Next candidate (when num_candidates > 1) |
CTRL+p / CTRL+UP | Previous candidate (when num_candidates > 1) |
| Any other insert-mode key | Dismiss the ghost |
When the secondary ghost has only one candidate, CTRL+n/CTRL+p
fall through to scrolling the primary ghost as usual.
Accepting a CALM ghost also dismisses any active primary session so the next keystroke refreshes cleanly.
The source tracks the model file's mtime. Overwriting the .cwgt on
disk causes the next trigger to reload the weights without restarting
LEM -- useful during training-and-test cycles.
All rendering goes through TSS. LEM obtains its stylesheet via
theme.subscribe("shell", "lem"), which returns a lazy getter that
refreshes when the theme changes.
+-----+----------------------------------------+
| gut | text area | row 1
| ter | |
| | |
| | |
+-----+----------------------------------------+
| status line | row H-1
+----------------------------------------------+
| command line / message area | row H
+----------------------------------------------+
| Path | Purpose |
|---|---|
gutter.line_number | Non-current line numbers |
gutter.current_line | Current line number |
gutter.diagnostic_error | Line number on a line with a diagnostic error |
| Path | Purpose |
|---|---|
status | Status line background |
status.filename | File name |
status.modified | Modified indicator [+] |
status.position | Line:col display |
status.filetype | Filetype label |
status.mode | Mode indicator (normal) |
status.mode_insert | Mode indicator (insert) |
status.mode_visual | Mode indicator (visual) |
| Path | Purpose |
|---|---|
message.info | Informational messages |
message.warning | Warning messages |
message.error | Error messages |
| Path | Purpose |
|---|---|
text | Base text color |
text.selection | Visual mode selection overlay |
text.bracket_match | Matching bracket highlight |
text.diagnostic | Inline diagnostic error highlight |
completion | Primary ghost text (buffer-word / Lua) |
completion_secondary | CALM prose ghost text |
syntax.* | Syntax token styles (see Token types) |
| Setting | Default | Description |
|---|---|---|
number | true | Show line numbers |
relativenumber | true | Show relative line numbers (hybrid with number) |
wrap | false | Soft-wrap long lines |
tabstop | 4 | Tab display width |
shiftwidth | 4 | Indentation width for > / < |
expandtab | true | Insert spaces instead of tab characters |
autoindent | true | Copy indentation from current line on ENTER |
scrolloff | 5 | Minimum visible lines above/below cursor |
page_scroll_overlap | 2 | Lines of overlap kept when PAGE_DOWN/UP scrolls |
lua_preloads | table of 9 modules | Modules merged into the Lua completion environment (see Completion) |
calm_filetypes | {markdown, text, ""} | Filetypes eligible for CALM prose completion |
Scalar settings can be changed at runtime with :set. Table-valued
settings like lua_preloads are set programmatically by overriding
config.new(overrides) before starting the editor.
| Path | Role |
|---|---|
src/lem/lem_piece_table.c | C piece table implementation |
src/lem/lem_piece_table.h | C piece table header |
src/lem/lem.lua | Editor core: startup, main loop, key dispatch |
src/lem/lem/buffer.lua | Buffer object: wraps piece table + metadata |
src/lem/lem/buffer_list.lua | Multiple buffer management |
src/lem/lem/mode.lua | Mode machine: normal, insert, visual, command |
src/lem/lem/keymap.lua | Key binding registry and dispatch |
src/lem/lem/motion.lua | Motion definitions and cursor math |
src/lem/lem/text_object.lua | Text object definitions |
src/lem/lem/operator.lua | Operator definitions and register system |
src/lem/lem/command.lua | Ex-mode command parser |
src/lem/lem/viewport.lua | Viewport: scroll state, cursor positioning |
src/lem/lem/render.lua | Screen rendering via TSS |
src/lem/lem/diagnostic.lua | Inline diagnostic validation engine |
src/lem/lem/highlight.lua | Syntax highlighting engine |
src/lem/lem/highlight/ | Per-filetype highlighter modules |
src/lem/lem/completion.lua | Ghost-text completion controller (primary + secondary) |
src/lem/lem/completion/buffer_words.lua | Buffer-word completion source |
src/lem/lem/completion/calm.lua | CALM prose completion source |
src/lem/lem/config.lua | Configuration defaults and filetype detection |
src/shell/shell/builtins/lem.lua | Shell builtin entry point |
| Module | Purpose |
|---|---|
term | Terminal I/O: alt screen, keyboard input, cursor, output |
term.tss | Styling engine for all visual output |
term.widgets | file_chooser() for CTRL+p |
theme | Theme lazy getter via theme.subscribe() |
std.fs | File I/O |
std.utf | UTF-8 text operations |
std.txt | Text utilities, binary detection |
crypto | b64_encode() for OSC 52 clipboard |
shell.completion.source.lua_keywords | Lua keyword list for completion |
shell.completion.source.lua_symbols | _G walker reused for Lua completion |
The following features are designed but not yet implemented:
Auto-trigger for CALM: an opt-in debounced mode that fires CALM
after a typing pause (:calm auto), in addition to the current manual
CTRL+space trigger
Async inference: move model:complete() off the main thread via
lev so slow models don't freeze the editor during a CALM call
Session persistence: save/restore buffers across sessions via MNEME
Visual lines navigation: gj/gk display-line navigation
File watcher: detect external file changes via inotify