# Lilush Shell

![Lilush Shell logo](../images/logo.png)

## Overview

This document covers the Lilush interactive shell: startup sequence, configuration,
modes, prompt, builtins, and key bindings.

Related docs:

- [LSH](LSH.md) for the scripting format
- [THEMES](THEMES.md) for the theme system
- [TSS](TSS.md) for the styling engine

## Terminal Requirements

Lilush Shell requires a terminal that supports the
[Kitty keyboard protocol](https://sw.kovidgoyal.net/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.

## Configuration

### Directories

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) |

### Init scripts

On startup, the shell mode executes init scripts in this order:

1. `/etc/lilush/init.lsh` — system-wide (if exists)
2. `~/.config/lilush/init.lsh` — user (if exists)

Both files use the [LSH](LSH.md) 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 (default is gruvbox; switch to another bundled theme if desired)
theme alabaster_dark

# Define aliases
alias ll ls -la

# Python venvs directory
setenv LILUSH_PYTHON_VENVS_DIR=/home/user/.venvs
```

### User modes

Custom modes are loaded from `~/.config/lilush/modes.json`. This file maps
shortcut keys to LILPACK package names:

```json
{
    "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.

## Startup Sequence

The full initialization sequence for `lilush` (interactive shell):

### 1. Entry `start_shell.lua`

Seeds the RNG, registers SIGINT handler, starts a LEV event loop (`lev.run()`),
then within the event loop calls `shell.new()` and `shell:run()`.

### 2. Shell constructor `shell.lua:new()`

1. Sets `SHELL=/usr/bin/lilush`
2. Initializes LILPACK (`lilpack.init()`), which replaces default Lua
   searchers with LILPACK and script-local searchers and clears `package.path`
3. Creates a MNEME-backed history store (see [MNEME](MNEME.md) for storage details)
4. 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)`
5. Binds mode shortcuts: `F1` (shell), `F2` (lua), `F3` (wiki)
6. Reads `~/.config/lilush/modes.json` and loads user modes from LILPACKs

### 3. Shell mode initialization `shell/mode/shell.lua:new()`

The shell mode constructor runs during step 4 above:

1. **`check_config_dirs()`** — creates `~/.config/lilush/` and
   `~/.local/share/lilush/` if missing
2. **`load_config()`** — executes `/etc/lilush/init.lsh` then
   `~/.config/lilush/init.lsh`
3. **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)
4. **Sets initial prompt state** — populates home, user, hostname, pwd
5. **Updates completion** — registers aliases as a completion source

### 4. Main event loop `shell.lua:run()`

1. Verifies TTY, sets raw mode
2. Probes terminal text-sizing support
3. Checks and enables Kitty keyboard protocol
4. Enables bracketed paste mode
5. Clears screen, displays initial prompt
6. Enters the main loop, processing `"execute"` and `"combo"` events

## Modes

The shell is a **mode-based** system. Each mode provides its own prompt,
completion, history, and command execution logic.

### Built-in modes

| 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](WIKIDB.md)) |

Built-in shortcuts (`F1`–`F3`) are reserved and cannot be reassigned.

### Mode switching

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.

### Mode contract

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

## Prompt

The shell prompt is composed of configurable **blocks** rendered left to right.

### Available blocks

| 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.

### Configuring prompt blocks

**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 order

1. Prompt object is created — reads `LILUSH_PROMPT` from environment, falls back
   to `"user,dir"`
2. `init.lsh` runs — may `setenv LILUSH_PROMPT=...`
3. 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 Bindings

### Global (all modes)

| 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) |

## Builtins

### Top-level builtins

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) |
| `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 |

### Regular builtins

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`)

### Builtin fork modes

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` |

### Alias expansion

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.

## Command Execution

When you press Enter:

1. Input text is retrieved and aliases are expanded
2. The command list is parsed via `pipeline.parse_list()` (handles pipes,
   `&&`/`||`/`;` operators, I/O redirects (`<`, `>`, `>>`, `err>`, `err>>`,
   `all>`, `all>>`), and quoting)
3. All commands are validated — each must be a known builtin, a binary on PATH,
   a top-level builtin, or a path starting with `./` or `/`
4. 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
5. 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 |

## Local Secrets

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.

### Secret URI format

```
mneme://secrets/key_name
```

- `key_name` — the key in the encrypted `secrets` keyspace

Example:

```
setenv DB_PASSWORD=mneme://secrets/db_password
```

### Encryption

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.

### The `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.

### Persist modes

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` |

### Secrets in scripts

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.

## Non-interactive Modes

### `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](LSH.md) for the scripting format.

## Environment Variables

### Shell-managed

| 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`) |

### User-configurable

| 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) | — |

### Secrets

| 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` |

## History

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
```
