
This document covers the Lilush interactive shell: startup sequence, configuration, modes, prompt, builtins, and key bindings.
Related docs:
Lilush Shell requires a terminal that supports the
Kitty keyboard protocol(refered to as KKBP in code and hereafter).
Compatible terminals include kitty, ghostty, foot, alacritty, and konsole.
The shell enables both KKBP and bracketed paste mode on startup and refuses to launch if KKBP is not detected.
The shell creates these directories on first launch if they don't exist:
| Directory | Purpose |
|---|---|
~/.config/lilush/ | User configuration |
~/.config/lilush/snippets/ | User snippet files |
~/.local/share/lilush/ | User data (MNEME databases, packages) |
On startup, the shell mode executes init scripts in this order:
/etc/lilush/init.lsh — system-wide (if exists)
~/.config/lilush/init.lsh — user (if exists)
Both files use the LSH scripting format. Execution stops on the first non-zero exit code within each file.
A typical init.lsh:
# Set prompt blocks
setenv LILUSH_PROMPT=user,dir,git
# Set theme
theme gruvbox
# Define aliases
alias ll ls -la
# Python venvs directory
setenv LILUSH_PYTHON_VENVS_DIR=/home/user/.venvs
Custom modes are loaded from ~/.config/lilush/modes.json. This file maps
shortcut keys to LILPACK package names:
{
"F5": "my-package-name"
}
The mode module, prompt, completion, and sources are discovered from the
LILPACK's manifest. Built-in mode names (shell, lua, wiki) cannot be
overridden.
If a mode's package is not installed, the shell prints a warning and skips that mode instead of failing to start.
Shortcut conflicts are resolved automatically: if a user mode requests a reserved
shortcut (F1–F3) or one already taken, it is reassigned to the next free key
in the F4–F12 pool.
The full initialization sequence for lilush (interactive shell):
start_shell.luaSeeds the RNG, registers SIGINT handler, starts a LEV event loop (lev.run()),
then within the event loop calls shell.new() and shell:run().
shell.lua:new()Sets SHELL=/usr/bin/lilush
Initializes LILPACK (lilpack.init()), which replaces default Lua
searchers with LILPACK and script-local searchers and clears package.path
Creates a MNEME-backed history store (see MNEME for storage details)
Loads built-in modes in order: shell → lua → wiki.
For each mode, load_mode() creates:
Prompt object (from the mode's prompt module)
Completion object (with all configured sources)
History object (backed by the shared store)
Input object (wiring prompt, completion, and history together)
Mode object via the mode module's new(input, config)
Binds mode shortcuts: F1 (shell), F2 (lua), F3 (wiki)
Reads ~/.config/lilush/modes.json and loads user modes from LILPACKs
shell/mode/shell.lua:new()The shell mode constructor runs during step 4 above:
check_config_dirs() — creates ~/.config/lilush/ and
~/.local/share/lilush/ if missing
load_config() — executes /etc/lilush/init.lsh then
~/.config/lilush/init.lsh
Re-applies LILUSH_PROMPT — if init.lsh set the variable via setenv,
the prompt blocks are updated to match (the prompt object was created before
init.lsh ran)
Sets initial prompt state — populates home, user, hostname, pwd
Updates completion — registers aliases as a completion source
shell.lua:run()Verifies TTY, sets raw mode
Probes terminal text-sizing support
Checks and enables Kitty keyboard protocol
Enables bracketed paste mode
Clears screen, displays initial prompt
Enters the main loop, processing "execute" and "combo" events
The shell is a mode-based system. Each mode provides its own prompt, completion, history, and command execution logic.
| Mode | Shortcut | Description |
|---|---|---|
shell | F1 | Standard shell — runs commands, pipelines, builtins |
lua | F2 | Lua REPL — evaluates Lua expressions and statements |
wiki | F3 | Wiki viewer — browse and search MNEME wiki databases (see WIKIDB) |
Built-in shortcuts (F1–F3) are reserved and cannot be reassigned.
Press a mode's shortcut key to switch to it. CTRL+d from a non-shell mode
returns to the shell mode. Each mode preserves its own input state (content,
cursor position, history position) independently.
Every mode must expose these methods:
run() — execute the current input
get_input() — return the mode's input object
can_handle_combo(combo) — return true if the mode handles the given key combo
handle_combo(combo) — process a key combo, return true to trigger redraw
The shell prompt is composed of configurable blocks rendered left to right.
| Block | What it shows |
|---|---|
user | username@hostname (root highlighted in red) |
dir | Current directory (with ~ substitution, clipped at 25 chars) |
git | Branch, modified/staged/untracked counts, ahead/behind, tags |
python | Active Python venv name |
secrets | Local secrets encryption/fetch status icon |
Blocks are always rendered in this fixed order regardless of the order specified
in the configuration. A trailing $ is always appended.
Via LILUSH_PROMPT env var — comma-separated list of block names:
setenv LILUSH_PROMPT=user,dir,git
The default if unset is user,dir.
Via the prompt builtin — toggle individual blocks at runtime:
prompt git # toggle git block on/off
prompt git secrets # toggle multiple blocks
prompt # show enabled/disabled blocks
LILUSH_PROMPT resolution orderPrompt object is created — reads LILUSH_PROMPT from environment, falls back
to "user,dir"
init.lsh runs — may setenv LILUSH_PROMPT=...
After init.lsh, LILUSH_PROMPT is re-read and prompt blocks are updated
This means all three scenarios work:
No config, no env var → defaults to user,dir
LILUSH_PROMPT set in parent shell → read at prompt creation
LILUSH_PROMPT set in init.lsh → re-applied after config loading
| Key | Action |
|---|---|
CTRL+d | Exit shell (or deactivate venv if active, or return to shell mode if in another mode) |
CTRL+l | Clear screen |
F1–F3 | Switch to built-in mode |
F4–F12 | Switch to user mode (if assigned) |
These are handled directly by the shell mode because they need access to the mode's internal state:
| Command | Description |
|---|---|
alias <name> <value> | Define an alias (no args: list all) |
unalias <name> | Remove an alias |
rehash | Rescan PATH for completions |
prompt [blocks...] | Toggle prompt blocks (no args: show status) |
theme [name] | Theme management (no args: list all; with name: set theme) |
calm <subcommand> | CALM model management |
pyvenv <name\|exit> | Activate/deactivate Python venv (no args: list available) |
secrets <subcommand> | Local secrets management (status, set, get, del, list, fetch, clear) |
run_script <path> | Execute an LSH script |
Dispatched through the builtins registry (shell.builtins):
Filesystem:
ls (-a -t -l), cd, mkdir (-p), rm (-r -f), rmrf,
../.../etc. (shorthand for cd ../../...),
files_matching (execute over matching files)
File viewing:
kat — file viewer with pager and markdown rendering (-m --raw --pager)
Environment:
setenv/export, unsetenv, envlist (optional regex filter)
Process management:
ps (-a -o -x --detailed --json --text -k -p -u --sort),
exec (replace shell process), job (list start kill attach reap)
Networking:
dig (see dig --help), netstat (-l --source --destination), wgcli (up down)
History:
history (-n --compact --time-only)
Snippets:
zx (snippet launcher)
MNEME:
mneme — MNEME database management
Packages:
lilpack — LILPACK package management (list, modules, install, verify)
Desktop:
notify — desktop notification via Kitty protocol (--after --title)
Each builtin declares a fork mode that controls how it executes:
| Mode | Behavior |
|---|---|
"always" | Fork a child process (default for most builtins) |
"never" | Run directly in the shell process — required for builtins that modify shell state |
"pipe" | Run in the shell process when standalone; fork when in a pipeline or with I/O redirects |
Builtins may also declare:
extra — receives the shell's jobs context as a third argument (used by kat, job)
pre_fork — a hook that runs in the parent before forking, can set environment
variables or short-circuit execution
Fork modes by builtin:
| fork = "never" | fork = "pipe" |
|---|---|
cd, ../... | kat |
setenv, export, unsetenv | mneme |
job, zx, theme | lilpack |
Aliases are expanded at:
Start of input
After | (pipe)
After &&, ||, ; operators
Aliases are simple text substitution — the alias name is replaced with the alias value before parsing.
When you press Enter:
Input text is retrieved and aliases are expanded
The command list is parsed via pipeline.parse_list() (handles pipes,
&&/||/; operators, I/O redirects (<, >, >>, err>, err>>,
all>, all>>), and quoting)
All commands are validated — each must be a known builtin, a binary on PATH,
a top-level builtin, or a path starting with ./ or /
The command list is executed:
Top-level builtins run in the shell process
Regular builtins with fork = "never" run in the shell process
Regular builtins with fork = "pipe" run in the shell process
unless they are in a pipeline or have I/O redirects (file input,
output, or stderr redirection), in which case they fork
Everything else is forked
Fetched local secrets are cleared after execution (per persist mode)
The following env vars are set around each command execution:
| Variable | Set when | Value |
|---|---|---|
LILUSH_EXEC_CWD | Before execution | Working directory |
LILUSH_EXEC_START | Before execution | Unix timestamp |
LILUSH_EXEC_END | After execution | Unix timestamp |
LILUSH_EXEC_STATUS | After execution | Exit code as string |
Environment variables whose values start with mneme://secrets/ reference
secrets stored in the shell's encrypted MNEME keyspace
(~/.local/share/lilush/shell.mneme, keyspace secrets). The secrets
builtin manages storage and fetching of these secrets.
mneme://secrets/key_name
key_name — the key in the encrypted secrets keyspace
Example:
setenv DB_PASSWORD=mneme://secrets/db_password
Secrets are encrypted at rest using an Ed25519 SSH key. Set the
LILUSH_ENCRYPTION_KEY environment variable to the path of your SSH key:
setenv LILUSH_ENCRYPTION_KEY=~/.ssh/id_ed25519
Without this key, secrets set/get/fetch operations will fail with an
error. Unencrypted keyspaces (history, session) are unaffected.
secrets builtin| Subcommand | Description |
|---|---|
secrets status | Show encryption status, managed vars, and persist mode |
secrets set <key> [value] | Store a secret (prompts interactively if value omitted) |
secrets get <key> | Retrieve and print a secret |
secrets del <key> | Delete a secret |
secrets list [prefix] | List stored secret keys |
secrets fetch [--persist N\|manual] [VARS...] | Fetch secrets and replace env var values |
secrets clear | Immediately reset all fetched secrets to their mneme:// URIs |
secrets fetch resolves each mneme://secrets/ URI, reads the secret from the
encrypted keyspace, and overwrites the environment variable with the actual
value. You can fetch specific variables by name or omit the argument to fetch
all secret-backed vars.
When using secrets set, omitting the value argument opens an interactive
prompt with hidden input, avoiding the secret appearing in shell history.
After fetching, secrets remain in the environment for a limited number of
commands before being automatically cleared (restored to mneme:// URIs).
--persist value | Behavior |
|---|---|
manual | Secrets persist until secrets clear is run |
| Integer N | Secrets persist for N commands, then auto-clear |
| (omitted) | Uses LILUSH_SECRETS_PERSIST env var, falls back to 1 |
Fetched secrets are inherited by child processes, including LSH scripts run via
shebang. Scripts do not re-run init.lsh, so the inherited environment
(with fetched secrets) is preserved as-is.
lilush -c <command> (mini mode)Creates a minimal shell with no prompt, no completion, and no history. Executes
the given command and exits. Does not load init.lsh.
lsh <script> (script mode)If lilush is invoked as lsh via symlink, it executes an LSH script file.
Script arguments are exposed as env vars:
${0} (script path), ${1}..${N} (positional args), ${#} (arg count).
Does not load init.lsh. See LSH for the scripting format.
| Variable | Purpose |
|---|---|
SHELL | Set to /usr/bin/lilush on startup |
LUA_PATH | Set to empty string (LILPACK handles module loading) |
PWD | Current working directory (updated by cd) |
LILUSH_EXEC_CWD | CWD before last command execution |
LILUSH_EXEC_START | Timestamp when last command started |
LILUSH_EXEC_END | Timestamp when last command ended |
LILUSH_EXEC_STATUS | Exit status of last command |
VIRTUAL_ENV | Active Python venv path (set by pyvenv) |
| Variable | Purpose | Default |
|---|---|---|
LILUSH_PROMPT | Comma-separated prompt block names | user,dir |
LILUSH_PYTHON_VENVS_DIR | Directory containing Python venvs | — |
LILUSH_TERM_TITLE_PREFIX | Prefix for dynamic terminal title | — |
LILUSH_TERM_TITLE_STATIC | Static terminal title (overrides dynamic) | — |
LILUSH_CALM_MODEL | Path to CALM model weights file | (resolved via registry, e.g. ~/.local/share/lilush/calm/shell.cwgt) |
| Variable | Purpose | Default |
|---|---|---|
LILUSH_ENCRYPTION_KEY | Path to Ed25519 SSH key for secret encryption | — |
LILUSH_SECRETS_PERSIST | Default persist mode for secrets fetch (manual or integer) | 1 |
Command history is stored in MNEME (~/.local/share/lilush/shell.mneme).
Each mode maintains its own history namespace as a sorted set within a
keyspace named after the mode (e.g. shell, lua). History is always
persisted across sessions.
The history builtin displays past commands:
history # show recent history
history -n 50 # show last 50 entries
history --compact # compact format
history --time-only # show only timestamps