# Themes

## Overview

This document covers the theme system in _Lilush Shell_: how themes are structured,
how they're discovered and applied, and how live theme switching works.

Related: [TSS](TSS.md) for the styling engine that consumes theme data.

The `theme` module (`src/theme/theme.lua`) provides a centralized palette for all
styled output: shell prompt, builtins (`ls`, `history`, `dig`, etc.), completion,
pager, wiki mode, markdown rendering, and LILPACK-provided sections (e.g. agent mode).

Theme data is structured as a set of **RSS tables** (raw style sheets) organized
into **sections**. The core sections are `shell` and `markdown`; additional sections
can be registered by LILPACKs at runtime. Each section feeds into `term.tss` to
produce styled terminal output.

At the core of the theme system is a **semantic palette** — a table of 26 named
color roles (e.g. `success`, `accent3`, `text`). All built-in style rules reference
these roles. Bundled and custom themes override the palette to change the entire
look, optionally adding per-element overrides for fine-tuning.

Themes can be switched at runtime — no restart required.

## Architecture

### Data flow

```
┌─────────────────┐   ┌───────────────┐
│ default palette │ → │ theme palette │
│ (26 roles)      │   │ (overrides)   │
└─────────────────┘   └───────────────┘
         │                    │
         └── merged palette ──┘
                    │
          build_defaults(palette)
             + LILPACK section builders
                    │
                    ▼
          palette-derived RSS tables
                    │
         ┌──────────┼──────────┐
         ▼          ▼          ▼
  ┌──────────┐ ┌────────┐ ┌────────┐
  │ theme    │ │ extra  │ │ merged │
  │ element  │ │ over-  │ │ RSS    │
  │ overrides│ │ rides  │ │ table  │
  │ (opt.)   │ │ (param)│ │        │
  └──────────┘ └────────┘ └────────┘
                    │
                    ▼
             tss = style.new(rss)
```

Each layer is deep-merged on top of the previous one. Later layers override
earlier ones at the leaf level.

### Palette

The default palette defines 26 semantic color roles used by all style rules:

| Group | Roles | Purpose |
|-------|-------|---------|
| Background | `bg_deep`, `bg_mid`, `bg_surface`, `bg_raised` | 4-tier depth gradient |
| Foreground | `fg1` .. `fg8` | 8-level text gradient (dim → bright) |
| Accent | `accent1` .. `accent4` | UI accent colors (highlight → deep) |
| Status | `success`, `error`, `warning`, `caution` | Semantic status colors |
| Auxiliary | `special1`, `special2`, `warm1`, `warm2`, `warm3` | Purple, amber, earth brown, sage tones |
| Text | `text` | Canonical body text color (default: same as `fg7`) |

The `text` role is the conventional main content color. All sections that display
body text should reference `p.text` rather than picking an `fg` level directly.
This ensures a consistent text color across LEM, markdown rendering, and any
LILPACK-provided sections.

Themes override this palette to change the entire color scheme. Style rules are
built from the palette via factory functions (`build_builtin(p)`, `build_prompt(p)`,
`build_markdown(p)`, etc.), so a palette override is sufficient to retheme
the entire shell.

### Sections

| Section | What it styles | Source |
|---------|----------------|--------|
| `shell` | Prompt, builtins, completion, pager, REPL separators, wiki mode, widgets | Built-in |
| `markdown` | Markdown renderer (headings, code blocks, borders, divs, tables, etc.) | Built-in |
| `agent` | Coding Agent display, prompts, tool output | Agent LILPACK |

Additional sections can be registered by LILPACKs via `theme.register_section()`.

## Theme Sources

Themes are discovered from two sources:

| Priority | Source | Description |
|----------|--------|-------------|
| 1 | LILPACK | Themes registered from installed LILPACK packages |
| 2 | Bundled | `theme.catalog` module (compiled into binary) |

If a theme name exists in both sources, the LILPACK theme takes priority.

### Bundled themes

The `theme.catalog` module (`src/theme/theme/catalog.lua`) contains curated themes
compiled directly into the binary. Each catalog theme defines a **palette** table
mapping the 26 semantic roles to theme-specific colors, plus optional per-element
overrides for styles that can't be derived from the palette alone.

Current bundled themes:

- `default` — Gruvbox Dark palette (defined in `src/theme/theme.lua`)
- `alabaster_dark` — Alabaster Dark palette (Nikita Prokopov's scheme, via kitty-alabaster)
- `kanagawa` — Kanagawa Wave palette (inspired by Hokusai's "The Great Wave")
- `1984` — Orwellian dystopian palette

## Merge Order

When `theme.set(name)` is called, the palette is resolved first:

1. **Default palette** — 26 built-in roles from `src/theme/theme.lua`
2. **Theme palette** — `palette` table from the active theme (if any)

The merged palette is then passed to factory functions to build the default RSS
tables. When `theme.get(section)` is called, additional layers are merged:

3. **Palette-derived defaults** — Built from the merged palette
4. **Theme element overrides** — Per-element overrides from the active theme
5. **Extra overrides** — Optional parameter passed to `get()`

## API

The `theme` module (`require("theme")`) exports the following functions:

### `get(section, extra_overrides)`

Returns a merged RSS table for the given section.

```lua
local theme = require("theme")
local shell_rss = theme.get("shell")
local tss = require("term.tss").new(shell_rss)
```

### `set(name)`

Activates a named theme. Pass `"default"` to return to the built-in palette.
Returns `true` on success, or `nil, error_message` if the theme is not found.

```lua
theme.set("alabaster_dark") -- activate alabaster_dark
theme.set("default")        -- back to built-in palette (gruvbox)
```

### `current()`

Returns the active theme name as a string (`"default"` when no named theme is active).

### `list()`

Returns a sorted array of `{ name = string, source = string }` entries for all
discovered themes. Source is one of `"bundled"` or `"lilpack"`.

### `palette()`

Returns a copy of the currently active palette table (26 color roles). Useful for
debugging or building palette-aware tools.

```lua
local pal = theme.palette()
-- pal.success, pal.accent3, pal.text, etc.
```

### `subscribe(section, sub_key)`

Returns a closure that tracks theme changes and lazily rebuilds a TSS instance.
Calling the closure returns the cached TSS, rebuilding it only when the theme
generation has changed (i.e., after a `set()` call).

```lua
local theme = require("theme")
local ensure_tss = theme.subscribe("shell")           -- full section
local ensure_completion_tss = theme.subscribe("shell", "completion")  -- sub-key
```

The optional `sub_key` parameter extracts a nested table from the section RSS
before passing it to `style.new()`. This is useful for modules that only need a
subset of a section (e.g., completion styles within the `shell` section).

Each subscriber gets its own independent TSS instance. The invalidation check is
a single integer comparison — negligible cost per call. Internally, `subscribe`
uses a generation counter that `set()` increments on every theme change.

### `register_section(name, builder_fn)`

Registers a LILPACK-provided theme section. The `builder_fn` receives the active
palette table and must return an RSS table for the section. The builder is called
immediately with the current palette and again whenever the theme changes.

```lua
local theme = require("theme")
theme.register_section("agent", function(p)
    return {
        agent = { error = { fg = p.error }, ... },
        prompts = { ... },
    }
end)
```

Called automatically during shell startup for LILPACKs that declare theme sections
in their manifest.

### `register_theme(name, data)`

Registers a LILPACK-provided theme. The `data` table follows the same format as
bundled catalog entries: a `palette` table plus optional per-section override tables.

```lua
theme.register_theme("solarized", {
    palette = { bg_deep = {0, 43, 54}, fg8 = {253, 246, 227}, ... },
    markdown = { heading = { fg = {181, 137, 0} } },
})
```

Called automatically during shell startup for LILPACKs that declare theme
definitions in their manifest.

## LILPACK Theme Extensions

LILPACKs can extend the theme system in two ways:

### Section builders

A LILPACK can provide a theme section (e.g., agent mode styles). The manifest
declares a builder module that receives the palette and returns section-specific
RSS data:

```json
{
    "theme": {
        "sections": [
            {"name": "agent", "builder": "agent.theme"}
        ]
    }
}
```

The builder module must return a function:

```lua
-- agent/theme.lua
return function(p)
    return {
        agent = { error = { fg = p.error }, ... },
        prompts = { ... },
    }
end
```

Section builders are called with the active palette each time the theme changes,
so palette-derived colors update automatically.

### Theme definitions

A LILPACK can provide a complete theme (palette + optional section overrides):

```json
{
    "theme": {
        "definition": {
            "name": "solarized",
            "module": "solarized.theme"
        }
    }
}
```

The module must return a table in the same format as bundled catalog entries:

```lua
-- solarized/theme.lua
return {
    palette = {
        bg_deep = {0, 43, 54},
        fg8 = {253, 246, 227},
        text = {238, 232, 213},
        -- ... remaining roles ...
    },
    -- Optional per-section overrides:
    markdown = {
        heading = { fg = {181, 137, 0} },
    },
}
```

A single LILPACK can provide both section builders and theme definitions.

## Shell Builtin

The `theme` command is a shell builtin with two forms:

```
theme                   # List available themes, highlighting the active one
theme <name>            # Switch to a named theme
```

Tab completion is supported for theme names.

## Live Reloading

Theme changes take effect immediately for all shell and agent components. This
works through two mechanisms:

### Subscribe-based TSS rebuild

Long-lived modules use `theme.subscribe()` to obtain a self-invalidating TSS
accessor. The returned closure rebuilds the TSS only when the theme generation
changes (after a `set()` call):

```lua
local theme_mod = require("theme")
local ensure_tss = theme_mod.subscribe("shell")

-- later, in any function:
local tss = ensure_tss()  -- cheap: returns cached TSS unless theme changed
```

For modules that need a sub-key of a section:

```lua
local ensure_tss = theme_mod.subscribe("shell", "completion")
```

Modules using this pattern:
- `shell/mode/shell.lua` — shell mode TLBs
- `shell/mode/shell.prompt.lua` — prompt rendering
- `shell/shell.lua` — shell input and completion TSS subscriptions
- `shell/mode/lua.lua` — Lua REPL separator
- `shell/mode/lua.prompt.lua` — Lua REPL prompt
- `shell/mode/wiki.lua` — wiki mode display
- `shell/mode/wiki.prompt.lua` — wiki mode prompt

### Per-invocation fetch

Builtins that create a fresh TSS for each execution fetch theme data at call time
rather than caching it at module level:

```lua
local theme_mod = require("theme")

local some_builtin = function(cmd, args)
    local tss = style.new(theme_mod.get("shell"))
    -- ... use tss ...
end
```

Since `theme_mod.get()` always returns data reflecting the active theme (and
results are cached internally per generation), these builtins automatically pick
up changes with minimal overhead.

Modules with this pattern:
- `shell/builtins/fs.lua` — `ls`, `cat`
- `shell/builtins/history.lua`
- `shell/builtins/dns.lua` — `dig`
- `shell/builtins/net.lua` — `netstat`
- `shell/builtins/env.lua`
- `shell/messages.lua`

## Persisting Themes

Theme selection is not automatically persisted. To start the shell with a specific
theme, add the `theme <name>` command to your init script:

```
# ~/.config/lilush/init.lsh
theme alabaster_dark
```

## Creating Custom Themes

### Adding a bundled theme

Add the theme data as a new key in `src/theme/theme/catalog.lua` and rebuild
with `./build.bash`. A minimal bundled theme only needs a `palette` table:

```lua
["mytheme"] = {
    palette = {
        bg_deep = {30, 25, 20}, bg_mid = {45, 40, 35},
        bg_surface = {60, 55, 50}, bg_raised = {80, 75, 70},
        fg1 = {100, 95, 90}, fg2 = {110, 105, 100},
        -- ... remaining roles ...
        text = {210, 200, 190},
        success = {140, 190, 80}, error = {210, 90, 80},
        warning = {220, 180, 70}, caution = {200, 140, 70},
    },
    -- Optional per-element overrides for styles that differ from palette:
    markdown = {
        heading = { fg = {220, 180, 100} },
    },
},
```

### Creating a theme LILPACK

Create a LILPACK package with a theme definition in its manifest. See the
[LILPACK](LILPACK.md) documentation for the full convention, and the
"LILPACK Theme Extensions" section above for the manifest format.

Colors can be specified as:
- Named ANSI colors: `"red"`, `"green"`, `"blue"`, etc.
- 256-color index: `42`
- RGB array: `{100, 200, 255}` (Lua) or `[100, 200, 255]` (JSON)
