Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 6 additions & 4 deletions AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,8 @@ tests/
syntect_tests.rs # Color, StyleModifier, Theme generation
egui_tests.rs # Color32, Visuals, widget visuals, selection
iced_tests.rs # Color, Palette mapping, Extended palette, Custom theme
theme_tests.rs # register_default_token / register_default_style semantics
theme_selector_tests.rs # Widget key handling, filter, Esc restore
docs/ # VitePress documentation site (SilkCircuit OKLCH theme)
```

Expand All @@ -58,7 +60,7 @@ docs/ # VitePress documentation site (SilkCircuit OKLCH theme)
```bash
cargo check # Fast type check
cargo clippy --all-targets --all-features # Pedantic lint gate
cargo test --all-features # Full test suite (210 tests)
cargo test --all-features # Full test suite
cargo doc --all-features --open # Generate docs
cd docs && pnpm dev # VitePress dev server
cd docs && pnpm build # Build docs for deployment
Expand All @@ -84,7 +86,7 @@ cd docs && pnpm build # Build docs for deployment

## Key Types

- `OpalineColor` — RGB color with hex, tuple, array, u32 conversions + lerp
- `OpalineColor` — RGB color with hex, tuple, array, u32 conversions + lerp + darken/lighten/desaturate
- `OpalineStyle` — Composed style (fg, bg, 9 modifiers) with builder pattern, `#[non_exhaustive]`
- `Gradient` — Multi-stop color interpolation (new() panics, try_new() returns Result)
- `Theme` — Fully resolved theme with `color()`, `style()`, `gradient()` + strict `try_*` variants; ratatui `span()`, `line()`, `text()`, `gradient_text()` (no trait import needed)
Expand Down Expand Up @@ -117,11 +119,11 @@ Solarized (Dark, Light), One (Dark, Light)

## Token Contract

Every builtin theme must define 26 core semantic tokens across these namespaces:
Every builtin theme must define 28 core semantic tokens across these namespaces:
`text.*`, `bg.*`, `accent.*`, `success/error/warning/info`,
`border.*`, `code.*`

Plus 13 required styles, 5 required gradients — enforced by contract tests.
Plus 14 required styles, 5 required gradients — enforced by contract tests.

App-specific semantics (git status, diff, mode indicators) are derived
by consuming apps via `register_default_token()`, not baked into the core.
Expand Down
2 changes: 1 addition & 1 deletion CONTRIBUTING.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ The easiest way to contribute — drop a `.toml` file in `src/builtins/`:

1. Copy an existing theme as a starting point
2. Fill in `[meta]`, `[palette]`, `[tokens]`, `[styles]`, `[gradients]`
3. Run `cargo test --all-features` — the contract tests enforce 26 tokens, 13 styles, 5 gradients
3. Run `cargo test --all-features` — the contract tests enforce 28 tokens, 14 styles, 5 gradients
4. Open a PR

Use underscores in filenames (e.g., `my_theme.toml` becomes id `my-theme`). Themes are auto-discovered at compile time via `build.rs`.
Expand Down
14 changes: 7 additions & 7 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@
<img src="https://img.shields.io/docsrs/opaline?style=for-the-badge&logo=docs.rs&logoColor=white&color=80ffea" alt="docs.rs">
</a>
<a href="https://github.com/hyperb1iss/opaline/actions">
<img src="https://img.shields.io/github/actions/workflow/status/hyperb1iss/opaline/ci.yml?style=for-the-badge&logo=github-actions&logoColor=white&color=4C566A" alt="CI">
<img src="https://img.shields.io/github/actions/workflow/status/hyperb1iss/opaline/cicd.yml?style=for-the-badge&logo=github-actions&logoColor=white&color=4C566A" alt="CI">
</a>
<a href="#-builtin-themes">
<img src="https://img.shields.io/badge/Themes-39_Built--in-ff6ac1?style=for-the-badge&logo=palette&logoColor=white" alt="39 Themes">
Expand Down Expand Up @@ -59,14 +59,14 @@ Opaline ships adapters for **ratatui**, **egui**, **iced**, **crossterm**, **owo
TOML file → ThemeFile (serde) → Resolver (palette → tokens → styles → gradients) → Theme
```

Opaline ships with **39 professionally crafted themes** spanning 17 colorscheme families, all enforced by a strict contract test suite that validates 26 core semantic tokens, 13 required styles, and 5 gradients per theme.
Opaline ships with **39 professionally crafted themes** spanning 17 colorscheme families, all enforced by a strict contract test suite that validates 28 core semantic tokens, 14 required styles, and 5 gradients per theme.

## ✦ Features

| Feature | Description |
| --- | --- |
| 🎨 **39 Builtin Themes** | SilkCircuit, Catppuccin, GitHub, Monokai Pro, Ayu, Night Owl, Flexoki, Palenight, Dracula, Nord, Rose Pine, Gruvbox, Solarized, Tokyo Night, Kanagawa, Everforest, One Dark/Light |
| 🔗 **Semantic Tokens** | 26 core tokens across generic `text.*`, `bg.*`, `accent.*`, `border.*`, and `code.*` namespaces |
| 🔗 **Semantic Tokens** | 28 core tokens across generic `text.*`, `bg.*`, `accent.*`, `border.*`, and `code.*` namespaces |
| 🌊 **Multi-Stop Gradients** | Smooth color interpolation with `gradient_bar()`, `gradient_text_line()`, and `gradient_spans()` |
| 🖥️ **Deep Ratatui Integration** | `From` impls, `Styled` trait, inherent `span()`, `line()`, `text()`, `gradient_text()` on `Theme` |
| 🎮 **egui Integration** | `Color32` conversion, full `Visuals` generation from theme tokens |
Expand Down Expand Up @@ -148,7 +148,7 @@ Browse all 39 themes, see every style and gradient rendered in real-time.
| **Solarized** | Dark, Light | Precision colors for machines and people |
| **One** | Dark, Light | Atom's iconic syntax palette |

Every theme is contract-tested: 26 core semantic tokens, 13 required styles, 5 required gradients.
Every theme is contract-tested: 28 core semantic tokens, 14 required styles, 5 required gradients.

## 🔮 Usage

Expand Down Expand Up @@ -244,11 +244,11 @@ purple = "#bb9af7"
"bg.base" = "bg"
"bg.selection" = "bg"
"accent.primary" = "blue"
# ... 26 required core tokens across text.*, bg.*, accent.*, border.*, code.*, etc.
# ... 28 required core tokens across text.*, bg.*, accent.*, border.*, code.*, etc.

[styles]
keyword = { fg = "accent.primary", bold = true }
# ... 13 required core styles
# ... 14 required core styles

[gradients]
primary = ["blue", "purple"]
Expand Down Expand Up @@ -306,7 +306,7 @@ TOML → ThemeFile (serde) → Resolver → Theme
```bash
cargo check # Fast type check
cargo clippy --all-targets --all-features # Pedantic lint gate
cargo test --all-features # Full test suite (210 tests)
cargo test --all-features # Full test suite
cargo doc --all-features --open # Generate docs
cargo run --example theme-showcase # Interactive TUI demo
```
Expand Down
4 changes: 4 additions & 0 deletions deny.toml
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,10 @@ all-features = true
ignore = [
# time 0.3.45 stack exhaustion — transitive dep from ratatui, fix requires MSRV > 1.85
"RUSTSEC-2026-0009",
# ttf-parser unmaintained notice — only reachable with the `egui` feature via
# egui 0.33 → epaint → ab_glyph → owned_ttf_parser. Drops out once egui ships
# a release that no longer depends on it.
"RUSTSEC-2026-0192",
]

# ── License compliance ────────────────────────────────────────
Expand Down
3 changes: 2 additions & 1 deletion docs/.vitepress/config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@ export default defineConfig({
'meta',
{
property: 'og:description',
content: 'Token-based theme engine with 39 builtin themes, gradients, and adapters for ratatui, egui, crossterm, syntect, and more',
content: 'Token-based theme engine with 39 builtin themes, gradients, and adapters for ratatui, egui, iced, crossterm, syntect, and more',
},
],
],
Expand Down Expand Up @@ -63,6 +63,7 @@ export default defineConfig({
{ text: 'CSS Adapter', link: '/guide/css' },
{ text: 'Syntect Adapter', link: '/guide/syntect' },
{ text: 'egui Adapter', link: '/guide/egui' },
{ text: 'iced Adapter', link: '/guide/iced' },
{ text: 'Color Manipulation', link: '/guide/color-manipulation' },
{ text: 'App-Level Derivation', link: '/guide/derivation' },
{ text: 'ThemeBuilder', link: '/guide/builder' },
Expand Down
6 changes: 3 additions & 3 deletions docs/getting-started/index.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
# Introduction

Opaline is a **token-based theme engine** for Rust applications. It gives your app a complete theming system: raw hex colors resolve to semantic tokens, which compose into styles, all driven by TOML configuration. While Opaline ships with first-class ratatui support, adapters for egui, crossterm, owo-colors, syntect, and CSS make it work across terminal, GUI, and web targets.
Opaline is a **token-based theme engine** for Rust applications. It gives your app a complete theming system: raw hex colors resolve to semantic tokens, which compose into styles, all driven by TOML configuration. While Opaline ships with first-class ratatui support, adapters for egui, iced, crossterm, owo-colors, syntect, and CSS make it work across terminal, GUI, and web targets.

## Why Opaline?

Expand Down Expand Up @@ -53,9 +53,9 @@ This separation means palette swaps propagate through the entire theme automatic
| Feature | Description |
| --------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| **39 builtin themes** | SilkCircuit, Catppuccin, GitHub, Monokai Pro, Ayu, Night Owl, Flexoki, Palenight, Rose Pine, Everforest, Tokyo Night, Kanagawa, Dracula, Nord, Gruvbox, Solarized, One |
| **Token system** | 26 core semantic tokens across generic namespaces |
| **Token system** | 28 core semantic tokens across generic namespaces |
| **Gradients** | Multi-stop color interpolation with `at(t)` and `generate(n)` |
| **7 adapters** | ratatui, egui, crossterm, owo-colors, syntect, CSS, colored |
| **8 adapters** | ratatui, egui, iced, crossterm, owo-colors, syntect, CSS, colored |
| **ThemeBuilder** | Programmatic theme construction without TOML |
| **Strict resolver** | Cycle detection, unresolvable reference errors |
| **Zero unsafe** | `unsafe_code = "forbid"`, no exceptions |
Expand Down
3 changes: 2 additions & 1 deletion docs/getting-started/installation.md
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,7 @@ opaline = { version = "0.4", features = ["global-state"] }
opaline = { version = "0.4", features = [
"builtin-themes", "gradients", "ratatui",
"cli", "crossterm", "owo-colors", "css",
"syntect", "egui",
"syntect", "egui", "iced",
"global-state", "discovery", "widgets"
] }
```
Expand All @@ -46,6 +46,7 @@ opaline = { version = "0.4", features = [
| `css` | no | CSS custom properties + classes generation |
| `syntect` | no | Syntax highlighting theme generation |
| `egui` | no | `Color32`/`Visuals` adapter for egui |
| `iced` | no | `Color`/`Palette`/`Custom` adapter for iced |
| `global-state` | no | Process-wide `current()`/`set_theme()` singleton |
| `discovery` | no | Load user themes from `~/.config/<app>/themes/` |
| `widgets` | no | Theme selector widget with live preview |
Expand Down
25 changes: 15 additions & 10 deletions docs/guide/css.md
Original file line number Diff line number Diff line change
Expand Up @@ -22,16 +22,18 @@ Output:

```css
:root {
--opaline-accent-primary: #cba6f7;
--opaline-bg-base: #1e1e2e;
--opaline-text-primary: #cdd6f4;
/* ... 39 token variables */
--opaline-accent-primary: #e135ff;
--opaline-bg-base: #121218;
--opaline-text-primary: #f8f8f2;
/* ... 28 token variables */
--opaline-gradient-primary: linear-gradient(to right, #e135ff, #80ffea);
--opaline-gradient-aurora: linear-gradient(
to right,
#e135ff,
#80ffea,
#ff6ac1
#f31bff,
#ff00ff,
#bf80f4,
#80ffea
);
}
```
Expand All @@ -53,16 +55,17 @@ Output:

```css
.opaline-keyword {
color: #cba6f7;
color: #e135ff;
font-weight: bold;
}

.opaline-error-style {
color: #f38ba8;
color: #ff6363;
}

.opaline-diff-added {
color: #a6e3a1;
.opaline-inline-code {
color: #50fa7b;
background-color: #1e1e28;
}
```

Expand All @@ -79,6 +82,8 @@ Style modifiers map to CSS properties:
| `crossed_out` | `text-decoration: line-through` |
| `hidden` | `visibility: hidden` |

`reversed`, `slow_blink`, and `rapid_blink` have no CSS equivalent and are skipped.

## Complete Stylesheet

Generate both variables and classes in one call:
Expand Down
9 changes: 6 additions & 3 deletions docs/guide/custom-themes.md
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,8 @@ orange = "#ffb86c"
"bg.panel" = "bg"
"bg.code" = "bg"
"bg.highlight" = "bg"
"bg.elevated" = "bg"
"bg.active" = "bg"
"bg.selection" = "bg"

"accent.primary" = "accent"
Expand All @@ -64,6 +66,7 @@ info = "blue"
[styles]
keyword = { fg = "accent.primary", bold = true }
line_number = { fg = "code.line_number" }
cursor_line = { bg = "bg.highlight" }
selected = { fg = "accent.secondary", bg = "bg.highlight" }
active_selected = { fg = "accent.primary", bg = "bg.highlight", bold = true }
focused_border = { fg = "border.focused" }
Expand All @@ -87,8 +90,8 @@ aurora = ["accent", "secondary", "green", "blue", "accent"]
## Loading Custom Themes

```rust
// From a file
let theme = opaline::load_from_file("~/.config/myapp/themes/custom.toml")?;
// From a file (paths are used as given; expand `~` yourself first)
let theme = opaline::load_from_file("themes/custom.toml")?;

// From a string (e.g., embedded or fetched)
let toml_str = std::fs::read_to_string("theme.toml")?;
Expand Down Expand Up @@ -123,7 +126,7 @@ The strict resolver catches issues at load time:
- **Circular reference**: tokens form a cycle (`a → b → a`)
- **Invalid hex**: a palette value isn't a valid hex color

If your theme loads without error, it's valid. For builtin-level quality, ensure it defines all required tokens, 13 required styles, and 5 required gradients.
If your theme loads without error, it's valid. For builtin-level quality, ensure it defines all 28 required tokens, 14 required styles, and 5 required gradients.

## Tips

Expand Down
5 changes: 3 additions & 2 deletions docs/guide/egui.md
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,7 @@ let visuals = to_egui_visuals(&theme);
ctx.set_visuals(visuals);
```

The function starts from `Visuals::dark()` or `Visuals::light()` based on the theme variant, then overrides all color properties. Non-color properties (corner radii, shadows, spacing) retain their sensible defaults.
The function starts from `Visuals::dark()` or `Visuals::light()` based on the theme variant, then overrides the main color properties. Text cursor, shadow, and text-edit colors keep egui's defaults, as do non-color properties (corner radii, shadows, spacing).

### Token → Visuals Mapping

Expand All @@ -45,7 +45,8 @@ The function starts from `Visuals::dark()` or `Visuals::light()` based on the th
| `bg.highlight` | `faint_bg_color`, `widgets.hovered.bg_fill` |
| `bg.code` | `code_bg_color` |
| `bg.selection` | `selection.bg_fill`, `widgets.active.bg_fill` |
| `text.primary` | `override_text_color` |
| `text.primary` | `widgets.open.fg_stroke` |
| `bg.base` ±0.5 | `extreme_bg_color` (darkened for dark themes, lightened for light) |
| `text.secondary` | `widgets.inactive.fg_stroke` |
| `text.muted` | `widgets.noninteractive.fg_stroke` |
| `accent.primary` | `hyperlink_color`, `selection.stroke`, `widgets.hovered.fg_stroke` |
Expand Down
78 changes: 78 additions & 0 deletions docs/guide/iced.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,78 @@
# iced Adapter

The `iced` feature maps Opaline themes onto [iced](https://iced.rs/)'s `Palette` and `Custom` theme types, so a single TOML theme drives both your terminal UI and your desktop GUI.

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

## Color Conversion

`OpalineColor` converts to `iced::Color`:

```rust
use opaline::OpalineColor;
use iced::Color;

let color = OpalineColor::new(225, 53, 255);
let iced_color: Color = color.into();
// → Color::from_rgb8(225, 53, 255)
```

## Palette Generation

Convert a full Opaline theme to an iced `Palette`:

```rust
use opaline::adapters::iced::to_iced_palette;

let theme = opaline::Theme::default();
let palette = to_iced_palette(&theme);
```

### Token → Palette Mapping

| Opaline Token | Palette Field |
| ---------------- | ------------- |
| `bg.base` | `background` |
| `text.primary` | `text` |
| `accent.primary` | `primary` |
| `success` | `success` |
| `warning` | `warning` |
| `error` | `danger` |

iced derives the rest of its widget colors (weak, strong, hover variants) from these six slots.

## Custom Theme

`to_iced_custom` wraps the palette in an iced `Custom` theme named after the Opaline theme. Drop it into `iced::Theme::Custom`:

```rust
use std::sync::Arc;
use iced::Theme;
use opaline::adapters::iced::to_iced_custom;

let theme = opaline::Theme::default();
let custom = to_iced_custom(&theme);
let iced_theme = Theme::Custom(Arc::new(custom));
```

Return `iced_theme` from your application's `theme` method and every widget picks it up.

If you need the derived tints directly, `to_iced_extended` returns the `Extended` palette that iced generates from the base palette.

## Runtime Theme Switching

```rust
use std::sync::Arc;
use iced::Theme;
use opaline::adapters::iced::to_iced_custom;

fn switch_theme(theme_name: &str) -> Theme {
let theme = opaline::load_by_name(theme_name).expect("valid theme");
Theme::Custom(Arc::new(to_iced_custom(&theme)))
}
```

All 39 builtin themes work with iced. iced decides between its dark and light widget styling from the lightness of `background`, which agrees with each builtin theme's declared variant.
5 changes: 2 additions & 3 deletions docs/guide/ratatui.md
Original file line number Diff line number Diff line change
Expand Up @@ -63,9 +63,8 @@ let bg = Style::default().bg(theme.color("bg.base").into());
`OpalineStyle` implements Ratatui's `Styled` trait:

```rust
use opaline::OpalineStyle;
use ratatui::style::Stylize;
use ratatui::text::Span;
use opaline::{OpalineColor, OpalineStyle};
use ratatui::style::Styled;

let style = OpalineStyle::fg(OpalineColor::new(225, 53, 255)).bold();
let rat_style: ratatui::style::Style = style.style();
Expand Down
3 changes: 2 additions & 1 deletion docs/guide/styles.md
Original file line number Diff line number Diff line change
Expand Up @@ -54,12 +54,13 @@ inline_code = { fg = "success", bg = "bg.code" }

## Required Styles

Every builtin theme must define these 13 styles:
Every builtin theme must define these 14 styles:

| Style | Purpose |
| ------------------ | ---------------------- |
| `keyword` | Language keywords |
| `line_number` | Code line numbers |
| `cursor_line` | Line under the cursor |
| `selected` | Selected item |
| `active_selected` | Active + selected item |
| `focused_border` | Focused panel border |
Expand Down
Loading