opaline 0.4.1

A token-based theme engine for Rust applications
Documentation
# ThemeSelector Widget

Opaline ships a ready-to-use theme picker widget for Ratatui apps. It provides a searchable, scrollable theme list with a live color preview pane so users see exactly how each theme looks before committing.

## Feature Flag

The widget requires the `widgets` feature:

```toml
[dependencies]
opaline = { version = "0.4", features = ["widgets"] }
```

This enables `global-state` and `builtin-themes` automatically, and pulls in full `ratatui` (with crossterm) rather than just `ratatui-core`.
It also makes file-backed themes shadow builtin ids during discovery and live preview, so a local `dracula.toml` will win over the shipped `dracula` theme.

## Quick Start

```rust
use opaline::{ThemeSelector, ThemeSelectorState, ThemeSelectorAction};

// 1. Create state when opening the picker
let mut state = ThemeSelectorState::with_current_selected();

// 2. In your key handler
match state.handle_key(key_event) {
    ThemeSelectorAction::Select(id) => {
        // Theme already applied; just save the preference
        save_user_preference(&id);
        close_picker();
    }
    ThemeSelectorAction::Cancel => {
        // Original theme automatically restored
        close_picker();
    }
    _ => {} // Navigate, FilterChanged, Noop
}

// 3. In your render function
frame.render_stateful_widget(ThemeSelector::new(), area, &mut state);
```

## Types

### `ThemeSelectorAction`

Returned by `handle_key()`:

| Variant          | Meaning                                           |
| ---------------- | ------------------------------------------------- |
| `Navigate`       | Cursor moved, live preview applied                |
| `Select(String)` | Enter pressed; contains the theme's kebab-case ID |
| `Cancel`         | Esc pressed; original theme restored              |
| `FilterChanged`  | Filter text changed, list recomputed              |
| `Noop`           | Key not handled by the selector                   |

### `ThemeSelectorState`

Owns all widget state. Create on open, drop on close.

```rust
// All themes, no pre-selection
let state = ThemeSelectorState::new();

// Pre-selects the currently active global theme
let state = ThemeSelectorState::with_current_selected();

// With app-level token derivation for live preview
let state = ThemeSelectorState::with_current_selected()
    .with_derive(my_app::derive_tokens);
```

**Methods:**

- `handle_key(KeyEvent) -> ThemeSelectorAction`: process keyboard input
- `selected_theme() -> Option<&ThemeInfo>`: currently highlighted theme's metadata
- `filter() -> &str`: current filter text

### `ThemeSelector`

The stateful widget. Implements `StatefulWidget`.

```rust
let widget = ThemeSelector::new();
let widget = ThemeSelector::new().title("Pick a Color Scheme");
```

## Layout

The widget renders a two-pane layout:

```
┌─ Select Theme ──────────────────────────────────────────────┐
│ Filter: cat                                                 │
│─────────────────────────────┬───────────────────────────────│
│ ▌ Dark Themes ▐             │ Catppuccin Mocha              │
│   Catppuccin Mocha          │ by Catppuccin                 │
│ > Dracula                   │                               │
│   Gruvbox Dark              │ Soothing pastel theme for the │
│   Kanagawa Wave             │ high-spirited!                │
│ ▌ Light Themes ▐            │                               │
│   Catppuccin Latte          │ ████████ ████████ ████████    │
│   Everforest Light          │ primary  secondary tertiary   │
│                             │ ████████ ████████ ████████    │
│                             │ success  warning  error       │
│                             │                               │
│                             │ ▓▓▒▒░░██▓▓▒▒░░██▓▓▒▒░░██    │
│ ↑↓ Navigate    Enter OK    │ primary gradient              │
│ Esc Cancel                  │                               │
└─────────────────────────────┴───────────────────────────────┘
```

- **Left pane (55%)**: filter input + scrollable theme list with dark/light section headers
- **Right pane (45%)**: theme name, author, description, 6 color swatches, gradient bar

## Keyboard Controls

| Key           | Action                          |
| ------------- | ------------------------------- |
| ``           | Move cursor up                  |
| ``           | Move cursor down                |
| `Enter`       | Confirm selection               |
| `Esc`         | Cancel (restore original theme) |
| Any character | Append to filter                |
| `Backspace`   | Delete last filter character    |

The filter accepts all printable characters, including lowercase `j` and `k`.

## Live Preview

The widget applies each theme to the global state as you navigate. Your entire app re-renders with the previewed theme in real-time. On cancel (`Esc`), the original theme is restored from a snapshot taken at construction time.

## With App Derivation

If your app uses [derived tokens](./derivation.md), pass your derivation function so previews include your app-specific tokens:

```rust
fn derive_tokens(theme: &mut opaline::Theme) {
    let primary = theme.color("accent.primary");
    theme.register_default_token("sidebar.bg", primary.darken(0.85));
}

let state = ThemeSelectorState::with_current_selected()
    .with_derive(derive_tokens);
```

Without this, live preview would show the raw theme without your computed tokens, potentially missing colors or incorrect styling.

## Integration Example

A minimal integration into a Ratatui app with a modal theme picker:

```rust
use crossterm::event::{self, KeyCode};
use opaline::{ThemeSelector, ThemeSelectorAction, ThemeSelectorState};

struct App {
    theme_picker: Option<ThemeSelectorState>,
}

impl App {
    fn handle_key(&mut self, key: crossterm::event::KeyEvent) {
        if let Some(picker) = &mut self.theme_picker {
            match picker.handle_key(key) {
                ThemeSelectorAction::Select(id) => {
                    // Theme is already applied; persist the choice
                    self.save_theme_preference(&id);
                    self.theme_picker = None;
                }
                ThemeSelectorAction::Cancel => {
                    // Original theme was restored automatically
                    self.theme_picker = None;
                }
                _ => {}
            }
        } else if key.code == KeyCode::Char('t') {
            // Open theme picker
            self.theme_picker = Some(
                ThemeSelectorState::with_current_selected()
            );
        }
    }

    fn render(&mut self, frame: &mut ratatui::Frame) {
        if let Some(state) = &mut self.theme_picker {
            frame.render_stateful_widget(
                ThemeSelector::new(),
                frame.area(),
                state,
            );
        }
    }
}
```