Introduction
Litmus test your terminal themes — preview them across all your apps before you commit.
Try it live at litmus.edger.dev
The Problem
Switching terminal themes is a frustrating loop:
- You find a theme that looks nice in a preview
- You edit configs for kitty, wezterm, neovim, zellij, delta…
- You discover
git diffis unreadable, or cargo warnings blend into the background - You revert everything and try the next theme
- Repeat
The core issue: you can’t see how a theme actually looks across your real workflow until after you’ve fully set it up. A terminal’s built-in theme preview shows color swatches, but that doesn’t tell you whether a git diff or a cargo build will be readable. The decision is visual, but the evaluation process is mechanical and slow — especially on NixOS where config changes require a home-manager rebuild.
The Solution
A web app that lets you preview any theme across realistic terminal scenarios instantly — before touching a single config file.
Pick a theme. See exactly how git diff, cargo build output, bat syntax highlighting, and more will look across different terminal emulators. Litmus captures real screenshots from real terminals, so what you see is exactly what you’d get.
What’s In the Box
- 58 themes across 30+ families (Catppuccin, Tokyo Night, Gruvbox, Dracula, Nord, Rose Pine, and many more)
- 13 fixtures simulating real terminal output (git diff, cargo build, bat, ripgrep, shell prompt, ls, htop, and more)
- 2 providers — kitty and wezterm, with real per-provider screenshots
- Side-by-side comparison of any two themes
- Accessibility tooling — WCAG contrast checking, APCA readability scoring, color vision deficiency simulation (protanopia, deuteranopia, tritanopia)
- Config export — generate kitty.conf, TOML, or Nix attribute set for any theme
How It Works
Litmus captures real terminal screenshots by running actual commands in headless terminal emulators, then serves them through a web app where you can browse, compare, and evaluate themes. It also parses the ANSI output from those same commands to provide simulated rendering — enabling features like contrast analysis and CVD simulation that can’t work on raster images alone.
Read The Model to understand the conceptual framework that makes this work.
The Model
Terminal apps don’t all get their colors the same way. Understanding this is the key insight behind litmus — and the reason a simple color swatch preview doesn’t cut it.
Why Switching Themes Is Broken
When you change your terminal theme, some apps change with it and some don’t. git diff picks up the new colors immediately. Your neovim colorscheme stays exactly the same. And lazygit might use its own built-in palette or fall back to terminal colors depending on how you configured it.
This is confusing because it’s invisible. You change one config file and expect a uniform result, but the actual outcome depends on a hidden property of each app: where it gets its colors from.
Litmus makes this visible. It models terminal color relationships explicitly, so you can preview exactly what will change — and what won’t — when you switch themes.
Three Roles
Every terminal app falls into one of three roles:
- Providers — apps that define a complete color palette (terminal emulators like kitty, wezterm)
- Consumers — apps that inherit colors from their provider (git diff, cargo, bat, ls)
- Silos — apps that bring their own independent palette (neovim with a colorscheme) (roadmap)
Ecosystems
The provider/consumer relationship creates natural ecosystems. A terminal emulator (provider) plus all the CLI tools running inside it (consumers) form a group where colors flow in one direction: from provider to consumers.
When you preview a theme in litmus, you’re seeing an entire ecosystem. The 13 fixtures represent different consumer apps — git diff, cargo build, bat, ripgrep, ls, and more — all rendered with the same provider palette. This is the core insight: a useful theme preview shows the ecosystem, not just the palette.
Switching between providers (kitty vs wezterm) in litmus shows you that the same theme can look subtly different depending on which terminal you use — because providers sometimes interpret theme definitions differently.
Dual-Mode Apps
Some apps can operate as either a consumer or a silo. lazygit can use terminal ANSI colors (consumer mode) or its own built-in theme (silo mode). jjui behaves similarly.
Litmus will support showing both modes for dual-mode apps in a future version, making it clear what changes when you switch themes and what stays the same.
Providers
A provider is an app that defines a complete color palette. Terminal emulators are providers — they own the 16 ANSI colors plus foreground, background, cursor, and selection colors. Everything running inside a provider inherits this palette.
What a Provider Defines
A provider’s theme sets:
- Foreground / background — the default text and canvas colors
- Cursor / selection — visual feedback colors
- 16 ANSI colors — black, red, green, yellow, blue, magenta, cyan, white, plus their bright variants (indices 0–15)
These 21 colors are the foundation. Every consumer app inside the provider references them by index, not by RGB value. When the provider’s theme changes, all consumer output changes with it.
Why Providers Matter for Previewing
The same theme definition can produce different colors depending on the provider. Kitty and wezterm each ship their own theme collections, and even when a theme shares a name across both, the actual RGB values may differ — different curators, different source formats, different rounding.
Litmus captures screenshots separately for each provider, so you see the actual colors your terminal would render, not an idealized version.
Supported Providers
| Provider | Status | Notes |
|---|---|---|
| kitty | Supported | Full capture and color extraction |
| wezterm | Supported | Full capture and color extraction |
| alacritty | Planned | Next major version |
| foot | Planned | Next major version |
| ghostty | Planned | Next major version |
How Litmus Models Providers
Each theme in litmus has a theme definition (name, variant, slug) plus one or more provider color files that contain the actual RGB values as extracted from that provider’s theme system.
themes/
catppuccin/
mocha.toml # theme definition
mocha.kitty.toml # colors as kitty renders them
mocha.wezterm.toml # colors as wezterm renders them
The theme definition maps to provider-specific theme names:
name = "Catppuccin Mocha"
variant = "dark"
[providers]
kitty = "Catppuccin-Mocha"
wezterm = "catppuccin-mocha"
The provider color files are auto-generated by litmus extract-colors, which reads each provider’s vendored theme data and writes the resolved palette. See Adding Providers for details.
Consumers
A consumer is an app that inherits colors from its provider. It doesn’t define its own palette — it references colors by their ANSI index. What those colors actually look like depends entirely on the provider’s theme.
How Consumers Use Color
When git diff displays an addition in green, it emits ANSI escape code 32 (foreground color 2). The terminal emulator — the provider — maps color index 2 to an RGB value from its current theme. The consumer never knows what “green” actually looks like.
This is why the same git diff output looks completely different under Tokyo Night (muted teal green) vs Gruvbox (warm olive green) vs Catppuccin Mocha (pastel mint). The diff is identical; the provider’s palette is different.
Consumers in Litmus
Litmus represents consumers through fixtures — reproducible terminal scenarios that exercise specific tools. Each fixture runs a real command (git diff, cargo build, bat, etc.) and captures the output.
| Fixture | Consumer | What it demonstrates |
|---|---|---|
| git-diff | git diff | Diff colors: additions, deletions, context |
| cargo-build | cargo build | Compiler output: warnings, errors, notes |
| bat-syntax | bat | Syntax-highlighted source with line numbers |
| ripgrep-search | rg | Match highlighting, filenames, line numbers |
| ls-color | ls --color | File type colors: dirs, executables, symlinks |
| git-log | git log --graph | Graph colors and branch decorations |
| shell-prompt | bash session | Prompt colors and command output |
| python-repl | python3 | REPL output, tracebacks |
| htop | top | CPU, memory, process table |
| log-viewer | app logs | Structured logs: INFO/WARN/ERROR/DEBUG |
| color-swatch | ANSI palette | Reference palette: 16 ANSI + 256-color |
| color-showcase | CI dashboard | Simulated deploy pipeline using all 16 ANSI colors |
| editor-ui | text editor | Syntax highlighting, status bar, line numbers |
Each fixture is captured once per (provider, theme) combination, giving you the exact pixels your terminal would display.
Why Swatches Aren’t Enough
A 16-color swatch tells you the palette. It doesn’t tell you:
- Whether
git diffadditions are distinguishable from context lines - Whether cargo warnings are readable against the background
- Whether
batline numbers have enough contrast - Whether
lsdirectory colors clash with executable colors
These are consumer-specific questions that depend on how each app maps ANSI indices to semantic meaning. Litmus answers them by showing the actual consumer output, not abstract swatches.
Silos (Roadmap)
Silo support is planned for litmus’s next major version. This page describes the design direction, not current functionality.
A silo is an app that defines its own color palette, independent of the terminal provider. Changing your terminal theme has no effect on a silo — it controls its own colors entirely.
Examples
- Neovim with a colorscheme (e.g. tokyonight.nvim) — highlight groups map to arbitrary RGB, not ANSI indices
- Helix with a theme — same idea, editor-internal palette
- lazygit in built-in theme mode — uses its own color definitions instead of terminal ANSI
Why Silos Need Special Treatment
Silo themes are structurally different from provider themes. A neovim colorscheme defines highlight groups (Normal, Comment, String, Error, etc.) with arbitrary RGB values. These don’t map to the 16 ANSI palette — they exist in a completely separate color space.
This means:
- Silo themes can’t be previewed using provider color data
- A silo’s appearance is unaffected by provider theme changes
- The ecosystem view needs to visually distinguish “this changes with your theme” from “this stays the same”
Planned Features
- Silo theme definitions — a separate type modeling app-specific palettes (e.g. neovim highlight groups)
- Silo scene rendering — fixtures that use the silo’s own palette instead of the provider’s
- Dual-mode toggle — for apps like lazygit that can operate as consumer or silo, show both modes side by side
- Ecosystem view integration — clear visual separation between provider-affected consumers and independent silos
Using Litmus
Litmus is a web app for previewing terminal color themes. This section walks through its main features.
- Browsing Themes — find themes by name, variant, or readability
- Theme Detail — inspect a single theme across all fixtures
- Comparing Themes — side-by-side comparison with contrast analysis
- Exporting Config — generate config files for your terminal
Browsing Themes
The home page displays all 58 themes grouped by family. Each theme card shows a color swatch strip for quick visual comparison.
Filters
- Search — filter by theme or family name
- Variant — toggle between All, Dark, and Light themes
- Readability — set a minimum contrast threshold to surface only themes that pass your accessibility requirements
Filters combine — you can search for “catppuccin” while filtering to dark-only themes with high readability.
Provider Switching
The sidebar includes a provider toggle to switch between kitty and wezterm. This changes which color data and screenshots are displayed throughout the app.
Themes that aren’t available in the selected provider appear greyed out. Not every theme has been ported to every provider — some exist only in kitty or only in wezterm.
Favorites
Star any theme to add it to your favorites (up to 20). Favorites persist in the sidebar for quick access and are independent of the compare feature.
Navigation
Click a theme card to open its detail page. The sidebar also shows your last 5 visited themes for quick backtracking.
Theme Detail
The detail page shows a single theme across all 13 fixtures. This is where you evaluate whether a theme works for your workflow.
Dual Display
Each fixture is shown in two modes:
- Screenshot — a real capture from a headless terminal emulator, showing exactly what you’d see. These are served from a CDN and lazy-loaded as you scroll.
- Simulated — the same terminal output rendered from parsed ANSI codes. This enables features that can’t work on raster images: contrast analysis and CVD simulation.
Contrast Analysis
Below the header, contrast issue chips summarize readability problems found across all fixtures. Click a chip to jump to the specific fixture and span where the issue occurs. Issues are identified by checking every foreground/background color pair against WCAG contrast thresholds.
Fixture Minimap
The sidebar shows a visual minimap of all 13 fixtures. Click any fixture to scroll directly to it. Fixtures with contrast issues display a badge indicating the number of problems.
CVD Simulation
The sidebar includes a color vision deficiency simulation toggle. Switch between normal vision, protanopia, deuteranopia, and tritanopia to see how the theme appears under different vision conditions. This applies to the simulated rendering — it transforms the entire color palette in real time.
Comparing Themes
The compare page shows two themes side by side, fixture by fixture. This is the most effective way to decide between similar themes.
Selecting Themes
There are several ways to start a comparison:
- Click the Compare button on any theme card or detail page
- Use the “vs.” button next to a favorite in the sidebar — this compares the current theme against that favorite
- The sidebar remembers your last comparison partner, so “vs.” is a quick way to keep comparing against the same reference theme
Side-by-Side View
Each fixture renders for both themes, aligned horizontally. Inline theme pickers at the top let you swap either side without leaving the page.
Contrast Issues
The fixture minimap in the sidebar shows per-theme issue dots — a quick way to spot which fixtures have readability problems in each theme. Interactive contrast chips on the page let you click through individual issues.
Feel Lucky
The sidebar includes a “Feel Lucky” button that picks two random themes for comparison — useful for discovering themes you wouldn’t have considered.
Exporting Config
Once you’ve found a theme you like, litmus can generate the config for your terminal.
Supported Formats
- kitty.conf — color definitions for kitty’s configuration file
- TOML — portable theme definition
- Nix — attribute set for use in home-manager or NixOS configuration
How to Export
On the theme detail page, the export buttons generate the config for the currently selected provider. Copy to clipboard or download the file.
Planned Formats
Future versions will add export for additional providers:
- Alacritty (TOML)
- Wezterm (Lua)
- Foot (INI)
- Ghostty (config)
- Windows Terminal (JSON)
- iTerm2 (XML plist)
Under the Hood
This section covers litmus’s technical design — how themes go from definition files to pixels on screen, and the systems that enable accessibility analysis.
- Capture Pipeline — headless terminal screenshots, ANSI parsing, CDN delivery
- Rendering — the TermOutput model, color resolution, dual-mode display
- Accessibility — WCAG contrast, APCA scoring, CVD simulation
- Architecture — four crates, data flow, deployment
Capture Pipeline
Litmus captures real screenshots from real terminal emulators. This is the system that makes litmus’s previews trustworthy — you see the actual pixels your terminal would render, not a simulation.
Why Real Captures
Simulated rendering can reproduce colors accurately, but it can’t capture everything a terminal emulator does: font rendering, ligatures, cursor styles, padding, actual app behavior. A real screenshot of git diff running in kitty with Tokyo Night is the definitive answer to “what will this look like.”
Litmus uses simulated rendering too (for contrast analysis and CVD simulation), but the screenshots are the ground truth.
The litmus-capture Crate
The capture pipeline lives in crates/litmus-capture/. It’s a native binary (not WASM) that requires a headless Wayland compositor with GPU access.
Capture Flow
For each (provider, theme, fixture) combination:
- Configure — write a temporary provider config file with the theme’s colors and a fixed terminal geometry (80 columns × 32 rows, 12pt FiraCode, 1280×960px)
- Setup — run the fixture’s
setup.shto create any required state (git repos, source files, etc.) in a temp directory - Launch — start the terminal emulator headlessly with the fixture’s
command.sh - Capture — wait for the command to finish, take a screenshot with
grim - Convert — PNG → WebP for smaller file sizes
- Checksum — compute SHA-256 for cache-busting
The high-resolution capture gives crisp text when displayed at smaller sizes in the web app.
ANSI Parsing
litmus-capture also parses raw ANSI escape sequences from terminal command output into structured data. This produces TermOutput — the same format used for simulated rendering in the web app. The parser handles:
- Standard ANSI color codes (16 colors)
- 256-color extended palette
- 24-bit truecolor (RGB)
- SGR attributes: bold, italic, dim, underline, inverse
Each fixture’s parsed output is saved as output.json alongside its scripts.
Batch Capture
capture-all runs captures for every combination of provider × theme × fixture in parallel, using half the available CPU cores. With 2 providers × 58 themes × 13 fixtures = ~1,500 screenshots per full run.
Manifest and CDN
Screenshots are stored in a Cloudflare R2 bucket. A manifest.json file indexes every screenshot with metadata:
{
"providers": [{"slug": "kitty", "name": "Kitty", "version": "..."}],
"fixtures": [{"id": "git-diff", "name": "Git Diff", "description": "..."}],
"screenshots": [{
"provider": "kitty",
"theme": "tokyo-night",
"fixture": "git-diff",
"width": 1280,
"height": 960,
"url": "kitty/tokyo-night/git-diff.webp",
"format": "webp",
"sha256": "..."
}]
}
The web app fetches the manifest once at startup, then lazy-loads individual screenshots as the user scrolls. URLs include a checksum query parameter (?v=abc12345) for cache-busting, allowing aggressive caching: 1-year TTL on images, 1-minute TTL on the manifest.
Color Extraction
litmus extract-colors reads each provider’s vendored theme data (e.g. kitty’s themes/ directory, wezterm’s built-in theme registry) and writes .kitty.toml / .wezterm.toml files with the resolved RGB palette. This is how litmus gets the actual colors each provider uses — not a hand-transcribed approximation, but the values parsed from the provider’s own theme definitions.
Rendering
Litmus displays theme previews in two modes: real screenshots and simulated rendering. Both start from the same data — the difference is whether you’re looking at captured pixels or reconstructed output.
The TermOutput Model
The simulated rendering path uses TermOutput, defined in crates/litmus-model/src/term_output.rs. This replaced an earlier handcrafted scene system with a model that faithfully represents real terminal output.
TermColor
#![allow(unused)]
fn main() {
enum TermColor {
Default, // theme foreground or background (context-dependent)
Ansi(u8), // 0–15, resolved from provider's ANSI palette
Indexed(u8), // 16–255, fixed xterm-256 color palette
Rgb(u8, u8, u8) // 24-bit truecolor, used as-is
}
}
TermColor covers the full terminal color space. ANSI colors (0–15) are theme-dependent — they resolve differently per provider and theme. Indexed colors (16–255) and RGB colors are fixed regardless of theme.
TermSpan
#![allow(unused)]
fn main() {
struct TermSpan {
text: String,
fg: TermColor,
bg: TermColor,
bold: bool,
italic: bool,
dim: bool,
underline: bool,
}
}
A span is a run of text with uniform styling. A line of terminal output is a vector of spans.
Color Resolution
TermColor::resolve() takes a ProviderColors (the 21-color palette from a provider’s theme: 16 ANSI + fg/bg/cursor/selection_bg/selection_fg) and maps semantic references to concrete RGB:
Default→ provider’s foreground or background colorAnsi(n)→ the nth color in the provider’s paletteIndexed(n)→ looked up from the fixed xterm-256 color tableRgb(r,g,b)→ passed through unchanged
This indirection is what makes a single fixture definition render correctly across every theme. The fixture data says “use ANSI color 2 for additions” — what that looks like depends on the provider.
Dual Display
The theme detail page shows both rendering modes for each fixture:
- Screenshot — the WebP image captured from a real terminal, loaded from R2. This is the ground truth — what you’ll actually see.
- Simulated — the fixture’s
output.json(parsed ANSI) rendered as styled HTML. EachTermSpanbecomes a<span>with inlinecolorandbackground-colorCSS properties resolved from the current theme.
Simulated rendering exists because raster screenshots can’t support:
- CVD simulation — transforming colors to show how the theme appears under color vision deficiency requires access to the individual color values, not pixels
- Contrast analysis — checking every foreground/background pair against WCAG thresholds requires the structured span data
- Theme switching without re-capture — simulated rendering updates instantly when you change themes; screenshots require a full capture cycle
Web Renderer
crates/litmus-web/src/term_renderer.rs converts TermOutput to Dioxus HTML. For each span:
- Resolve
fgandbgviaTermColor::resolve()with the activeProviderColors - Apply bold, italic, dim as CSS
font-weight,font-style,opacity - Emit a
<span>with inline styles inside a<pre>block
The result is a monospace rendering that closely matches the real terminal output, minus font-specific details like ligatures and glyph spacing.
Accessibility
Litmus includes built-in tools for evaluating theme accessibility: contrast checking, readability scoring, and color vision deficiency simulation.
WCAG Contrast Checking
crates/litmus-model/src/contrast.rs implements WCAG 2.1 contrast ratio calculation.
The process:
- Convert sRGB to linear luminance using the standard gamma transfer function
- Compute relative luminance for each color
- Calculate the contrast ratio between foreground and background
- Compare against thresholds: AA (4.5:1 for normal text) and AAA (7.0:1)
Litmus checks every TermSpan in every fixture against these thresholds. This catches issues that a palette-level check would miss — a theme might have good overall contrast but produce unreadable output in specific contexts (e.g. dim text on a colored background in htop).
APCA Readability Scoring
In addition to WCAG ratios, litmus uses the Advanced Perceptual Contrast Algorithm (APCA) for readability scoring. APCA is more perceptually accurate than WCAG 2.1 contrast ratios — it accounts for the fact that dark text on a light background and light text on a dark background are not equally readable at the same ratio.
APCA scores are used in the readability filter on the browse page and in the per-fixture analysis on the detail page.
Per-Fixture Contrast Analysis
On the theme detail and compare pages, contrast issues surface as interactive chips:
- Each chip represents a foreground/background pair that falls below the contrast threshold
- Click a chip to jump to the specific fixture and span where the issue occurs
- The fixture minimap in the sidebar shows issue badges, giving you a bird’s-eye view of which fixtures have problems
This per-fixture analysis is only possible with simulated rendering — it requires access to the structured span data, not raster screenshots.
CVD Simulation
crates/litmus-model/src/cvd.rs simulates color vision deficiency using Machado et al. 2009 transformation matrices. Three conditions are supported:
- Protanopia — reduced red sensitivity
- Deuteranopia — reduced green sensitivity
- Tritanopia — reduced blue sensitivity
The simulation:
- Converts sRGB to linear RGB
- Applies a 3×3 transformation matrix specific to the condition
- Converts back to sRGB
The transform is applied at the ProviderColors level — it produces a new set of colors that flows through the existing rendering pipeline. This means contrast checking automatically works with simulated CVD colors too, so you can evaluate a theme’s accessibility for users with color vision deficiency.
The CVD toggle in the sidebar switches between normal vision and the three simulation modes in real time.
Architecture
Litmus is a Rust workspace with four crates.
Crates
| Crate | Role | Target |
|---|---|---|
litmus-model | Shared types and logic | any |
litmus-web | Web frontend | wasm32 |
litmus-cli | TUI prototype | native |
litmus-capture | Screenshot capture pipeline | native (Wayland) |
litmus-model
The foundation crate — no UI dependencies, compiles for any target. Contains:
- Theme types —
ThemeDefinition,ProviderColors,Color,AnsiColors - Screenshot model —
Provider,Fixture,ScreenshotKey,ScreenshotManifest - TermOutput —
TermColor,TermSpan, and color resolution logic - Contrast — WCAG 2.1 ratio calculation, APCA scoring, per-fixture validation
- CVD — color vision deficiency simulation matrices
- Parsers — TOML theme format, kitty.conf, base16 YAML, wezterm
- Export — kitty.conf, TOML, Nix output formatters
litmus-web
Dioxus WASM application — the primary interface. Depends on litmus-model. Themes and fixture data are embedded at compile time via include_str!.
litmus-cli
The original ratatui + crossterm TUI prototype from milestone 0. Still maintained but secondary to the web app.
litmus-capture
Native binary for headless screenshot capture. Requires a Wayland compositor with GPU access. Contains the ANSI parser, capture orchestration, color extraction, and manifest builder. See Capture Pipeline.
Data Flow
Theme TOML files
├─ theme definition (name, variant, providers map)
└─ provider color files (.kitty.toml, .wezterm.toml)
↓
litmus-web embeds at compile time
↓
ThemeDefinition + ProviderColors (in-memory)
↓
TermColor::resolve() maps semantic refs → RGB
↓
CSS inline styles (simulated rendering)
Fixture scripts (setup.sh + command.sh)
├─ litmus-capture runs them in headless terminals → WebP screenshots → R2
└─ litmus-capture parses ANSI output → output.json → embedded in litmus-web
↓
TermOutput + ProviderColors → simulated rendering
Screenshot manifest → lazy-loaded images
Routing
All routes are provider-scoped:
| Route | Page | Purpose |
|---|---|---|
/:provider/ | ThemeList | Home — filterable theme grid |
/:provider/theme/:slug | ThemeDetail | Single theme, all fixtures |
/:provider/scene/:scene_id | SceneAcrossThemes | One fixture across all themes |
/:provider/compare/:slugs | CompareThemes | Two themes side by side |
The provider prefix ensures that switching between kitty and wezterm changes the URL, making provider-specific views linkable and bookmarkable.
Deployment
The web app deploys to Cloudflare Pages via GitHub Actions:
- Build with
dx build --release(Dioxus CLI) - Inject critical CSS to prevent flash of unstyled content during WASM load
- Deploy with Wrangler to
litmus.edger.dev
Screenshots deploy separately to Cloudflare R2 at screenshots.litmus.edger.dev. The manifest and images are synced via rclone with appropriate cache headers.
State Management
The web app uses Dioxus signals for reactive state:
ActiveProvider— selected terminal emulator (kitty/wezterm)Favorites— starred themes (up to 20)VisitHistory— last 5 viewed themesLastComparedSlug— previous compare partner for the “vs.” buttonFilterState— search query, variant filter, readability thresholdCvdSimulation— active CVD mode (none/protanopia/deuteranopia/tritanopia)ManifestState— cached screenshot manifest from CDNAppThemeSlug— the theme used for the app’s own UI chrome
Contributing
Litmus welcomes contributions — new themes, new fixtures, and new provider support are the highest-impact areas.
- Dev Setup — prerequisites, tools, development workflow
- Adding Themes — add a new color theme
- Adding Fixtures — add a new terminal scenario
- Adding Providers — add support for a new terminal emulator
Dev Setup
Prerequisites
- Rust toolchain — stable channel with the
wasm32-unknown-unknowntarget, as defined inrust-toolchain.toml - mise — task runner for all dev commands
- Nix (optional) —
flake.nixprovides a reproducible dev shell with all dependencies
For screenshot capture only (not needed for web development):
- Wayland compositor with GPU access
grimfor screenshots- kitty and/or wezterm installed
Quick Start
# Start the web app (Dioxus dev server, port 8883)
mise run dev
# Start with local screenshot serving (port 8884 for images, 8883 for app)
mise run dev-screenshots
# Build for production
mise run build-web
Development Workflow
Bacon Diagnostics
The recommended workflow uses bacon for continuous compilation feedback:
- Start bacon in a terminal pane:
mise run bacon-claude-diagnostics - Edit code
- Bacon watches for changes and writes diagnostics to
.bacon-claude-diagnostics - Read that file for errors with exact file/line/column locations
Each line uses a pipe-delimited format:
level|:|file|:|line_start|:|line_end|:|message|:|rendered
This is faster than running cargo check (bacon is already watching) and machine-parseable.
Useful mise Tasks
| Task | Description |
|---|---|
dev | Start Dioxus dev server (port 8883) |
dev-screenshots | Local screenshot server + web app |
build-web | Build WASM release |
build-cli | Build CLI release |
check | cargo check across workspace |
fmt | Format with cargo fmt |
docs-serve | Serve mdbook with live reload (port 8882) |
docs-build | Build static docs |
capture-kitty | Capture all kitty screenshots |
capture-wezterm | Capture all wezterm screenshots |
screenshots-deploy | Build manifest + sync to R2 |
Work Tracking
Litmus uses beans, a file-based issue tracker. Beans are Markdown files with YAML frontmatter, managed via the beans CLI. Work is organized into milestones with parent/child and blocking relationships.
beans list --ready # see what's available to work on
beans show <id> # read a bean's full spec
beans create "Title" -t task -d "Description"
beans update <id> -s in-progress
Adding Themes
Each theme in litmus consists of a theme definition file and one or more provider color files.
1. Create the Theme Definition
Theme definitions live in themes/. Standalone themes go directly in the directory; family members go in a subdirectory:
themes/dracula.toml # standalone
themes/catppuccin/mocha.toml # family member
A theme definition is minimal — just a name, variant, and provider mappings:
name = "Dracula"
variant = "dark"
[providers]
kitty = "Dracula"
wezterm = "Dracula (Gogh)"
- name — human-readable display name
- variant —
"dark"or"light" - providers — maps each supported provider to the theme name in that provider’s theme registry. Not every theme needs to support every provider.
2. Extract Provider Colors
Provider color files are auto-generated — don’t write them by hand. Run:
# Extract colors for all themes from all providers
mise run extract-colors
This reads each provider’s vendored theme data and writes .kitty.toml / .wezterm.toml files with the resolved RGB palette:
themes/catppuccin/mocha.kitty.toml # auto-generated
themes/catppuccin/mocha.wezterm.toml # auto-generated
If your theme’s provider name doesn’t match anything in the vendored data, the extraction will skip it — check that the [providers] mapping matches the provider’s actual theme registry names.
3. Register in themes.rs
Open crates/litmus-web/src/themes.rs and add entries to both arrays.
In DEFINITION_DATA:
#![allow(unused)]
fn main() {
("dracula", include_str!("../../../themes/dracula.toml")),
}
In PROVIDER_COLORS_DATA (one entry per provider color file):
#![allow(unused)]
fn main() {
("dracula", include_str!("../../../themes/dracula.kitty.toml")),
("dracula", include_str!("../../../themes/dracula.wezterm.toml")),
}
The first element is the theme slug (must match the slug used in the definition entry). Keep both arrays sorted alphabetically.
4. Register the Family (if new)
If your theme belongs to a family not yet registered, add the family prefix in crates/litmus-web/src/family.rs:
#![allow(unused)]
fn main() {
static FAMILIES: &[&str] = &[
"Ayu",
"Catppuccin",
// ... add your family name here ...
];
}
Family grouping uses prefix matching — “Catppuccin Mocha” matches the “Catppuccin” prefix.
5. Capture Screenshots
After adding the theme, capture screenshots for it:
mise run capture-kitty # if kitty provider is mapped
mise run capture-wezterm # if wezterm provider is mapped
6. Verify
- Check
.bacon-claude-diagnosticsfor compilation errors - Start the web app with
mise run dev - Confirm the theme appears in the theme list
- Check that all fixtures render on the detail page
- Verify family grouping if applicable
Adding Fixtures
Fixtures are reproducible terminal scenarios used for capturing screenshots and generating simulated rendering data. Each fixture lives in its own subdirectory under fixtures/.
Directory Structure
fixtures/
{fixture-id}/
setup.sh # (required) Creates files/state in $FIXTURE_WORK_DIR
command.sh # (required) Runs the actual command to be captured
teardown.sh # (optional) Cleanup after capture
output.json # (generated) Parsed ANSI output for simulated rendering
Script Interface
The capture tool sets FIXTURE_WORK_DIR to an existing temp directory before running scripts.
setup.sh
Creates any files, git repos, or state the command needs. Must be idempotent. Runs once per capture.
#!/usr/bin/env bash
set -euo pipefail
cd "$FIXTURE_WORK_DIR"
git init -b main -q
git config user.email "demo@litmus.dev"
git config user.name "Litmus Demo"
# ... create files, make commits, etc.
command.sh
Produces the terminal output that gets screenshotted. Must exit when done — the terminal emulator is kept open via --hold.
#!/usr/bin/env bash
set -euo pipefail
cd "$FIXTURE_WORK_DIR"
git --no-pager diff --color=always HEAD
Colors must use ANSI escape codes, not hardcoded RGB. Use --color=always for tools that auto-detect.
output.json
Auto-generated by litmus-capture parse-fixtures. Contains the parsed ANSI output as structured TermOutput data. Don’t edit by hand.
Writing Good Fixtures
Pin versions and timestamps. Use fixed author names, dates, and deterministic data so output is reproducible:
GIT_AUTHOR_DATE="2024-01-15T10:00:00" \
GIT_COMMITTER_DATE="2024-01-15T10:00:00" \
git commit -q -m "Initial commit"
Exercise many ANSI colors. Good fixtures use a range of the 16 ANSI colors so themes can be compared meaningfully.
Stay within 80×32. The capture terminal is 80 columns × 32 rows. Output that wraps or scrolls past this boundary will be cut off.
Test locally:
export FIXTURE_WORK_DIR=$(mktemp -d)
bash fixtures/{id}/setup.sh
cd "$FIXTURE_WORK_DIR" && bash /path/to/fixtures/{id}/command.sh
rm -rf "$FIXTURE_WORK_DIR"
Registration
After creating the fixture scripts:
- Parse the ANSI output:
litmus-capture parse-fixtures - Register the fixture in
crates/litmus-web/src/fixtures.rs - Capture screenshots:
mise run capture-kitty && mise run capture-wezterm - Rebuild the manifest:
mise run capture-manifest
The fixture will automatically appear in all views — theme detail, scene-across-themes, and compare.
Adding Providers
Adding a new terminal emulator as a litmus provider involves three pieces: color extraction, capture configuration, and theme registration.
Overview
A provider in litmus needs:
- Vendored theme data — the provider’s theme definitions, checked into the repo
- Color extractor — code that parses the vendored data into litmus’s provider color format
- Capture config generator — code that writes the provider’s config file for headless screenshot capture
- Theme mappings — entries in each theme’s definition file mapping to this provider’s theme names
1. Vendor Theme Data
Check the provider’s theme collection into crates/litmus-capture/src/providers/ or a similar location. This is the source of truth for what colors the provider assigns to each theme.
For example, kitty’s themes are vendored from its official themes repo, and wezterm’s are extracted from its built-in theme registry.
2. Write a Color Extractor
Add a module in crates/litmus-capture/src/providers/ that reads the vendored theme data and emits ProviderColors — the standard 21-color palette (16 ANSI + fg/bg/cursor/selection_bg/selection_fg).
The extractor is called by litmus-capture extract-colors and writes .{provider}.toml files:
# AUTO-GENERATED by litmus extract-colors — do not edit
provider = "my-terminal"
source_version = "vendored"
[colors]
background = "#1E1E2E"
foreground = "#CDD6F4"
cursor = "#F5E0DC"
selection_background = "#F5E0DC"
selection_foreground = "#1E1E2E"
[colors.ansi]
black = "#45475A"
red = "#F38BA8"
# ... all 16 ANSI colors
3. Write a Capture Config Generator
Add a capture config generator in the same providers module. This writes a temporary config file that sets:
- The theme’s colors
- Terminal geometry (80×32 cells, 12pt FiraCode, 1280×960px)
- Any provider-specific settings needed for headless operation
See crates/litmus-capture/src/providers/kitty.rs and wezterm.rs for reference implementations.
4. Register the Provider
Register the new provider in crates/litmus-capture/src/providers/mod.rs so the capture tool and color extractor know about it.
5. Add Theme Mappings
For each theme that supports the new provider, add a mapping in the theme definition:
[providers]
kitty = "Catppuccin-Mocha"
wezterm = "catppuccin-mocha"
my-terminal = "Catppuccin Mocha" # add this
Then run litmus-capture extract-colors to generate the provider color files.
6. Capture and Deploy
# Capture screenshots for the new provider
mise run capture-{provider}
# Build manifest and sync to R2
mise run screenshots-deploy
The web app will automatically pick up the new provider — it discovers available providers from the embedded theme data at compile time.
Roadmap
What’s Shipped
Litmus has gone through five releases, evolving from a TUI prototype to a full-featured web app:
v0.1 — Initial release. Rust workspace with three crates, 19 curated themes, scene-based rendering, Dioxus WASM web app with theme browsing and detail views.
v0.2 — Major expansion. Image-backed screenshot system with headless capture for kitty and wezterm. APCA readability scoring, CVD simulation, config export (kitty.conf/TOML/Nix), sidebar layout, app theming, theme count expanded to 60.
v0.3 — Architecture shift. Migrated from handcrafted scenes to TermOutput model based on real ANSI capture. Provider-scoped routing (/:provider/), R2 screenshot deployment, interactive contrast issue navigation, fixture anchor deep-links.
v0.4 — Compare redesign. Strict 2-theme side-by-side comparison with inline pickers. Favorites (star toggle, 20 cap), visit history, per-fixture contrast issue dots in sidebar minimap.
v0.5 — Polish. Compare history tracking, sidebar label improvements.
See CHANGELOG.md for full details.
Next Major Version
The next major version extends the provider/consumer model with silo support and broadens provider coverage.
Silo Support
Apps like neovim and helix define their own color palettes independent of the terminal. The silo model will:
- Introduce a separate theme type for app-specific palettes (e.g. neovim highlight groups)
- Render silo fixtures using the silo’s own palette
- Support dual-mode apps (lazygit, jjui) that can operate as consumer or silo
- Visually distinguish provider-affected output from silo output in an ecosystem view
See Silos (Roadmap) for design details.
Ecosystem View
A new page showing a complete provider ecosystem: the terminal emulator plus all consumer apps rendered together. This makes it visible which apps change when you switch themes and which don’t.
Extended Export
Config generation for additional providers:
- Alacritty (TOML)
- Wezterm (Lua)
- Foot (INI)
- Ghostty (config)
- Windows Terminal (JSON)
- iTerm2 (XML plist)
Consumer Configuration Preview
Show how consumer-specific settings interact with themes — e.g. how LS_COLORS or delta config affects the appearance under different themes. Starting with presets rather than full interactivity.
Not in Scope
These are explicitly out of scope for litmus:
- Live terminal capture in browser — all rendering is from pre-captured data, not live terminal sessions
- Theme editor — litmus is a previewer, not a theme creation tool
- Plugin system — new providers, consumers, and fixtures are added to the codebase directly
- User accounts — no server-side state; everything runs client-side
Active Tasks
In Progress
No tasks
Todo
No tasks
Done (1)
Write first Litmus blogpost: What If You Could Test Drive a Terminal Theme? (litmus-f6mk)
Task · normal
All Tasks
Epics
- Fixtures iteration — more realistic, better color showcase (litmus-49jz) — Done ·
archived- Research existing terminal output datasets and ANSI test suites (litmus-3lcp) — Done ·
archived - Generate and curate first batch of candidate fixtures (litmus-52qn) — Done ·
archived - Build color swatch and color showcase fixtures (litmus-c55s) — Done ·
archived - Set up fixtures/candidates/ staging directory and review workflow (litmus-feex) — Done ·
archived - Audit existing fixtures against quality criteria (litmus-sk2k) — Done ·
archived
- Research existing terminal output datasets and ANSI test suites (litmus-3lcp) — Done ·
- Unify scenes and fixtures (litmus-coma) — Done ·
archived- Update litmus-cli to render TermOutput (litmus-0uoe) — Done ·
archived - Build ANSI-to-spans parser using VTE (litmus-28sq) — Done ·
archived - Integrate ANSI capture into fixture pipeline (litmus-9eg8) — Done ·
archived - Update contrast validation for TermColor (litmus-bcel) — Done ·
archived - Remove old Scene, ThemeColor, StyledSpan types and hand-written scenes (litmus-kbzo) — Done ·
archived - Update litmus-web to render TermOutput instead of Scene (litmus-lm76) — Done ·
archived - Add TermColor, TermSpan, TermLine, TermOutput types to litmus-model (litmus-q9lp) — Done ·
archived
- Update litmus-cli to render TermOutput (litmus-0uoe) — Done ·
- Iteration 2: Bug fixes + design feedback (litmus-jvkn) — Done ·
archived - Provider-based theme definition (litmus-knrz) — Done ·
archived- Update litmus-cli to load new theme format (litmus-dv2l) — Done ·
archived - Remove old Theme struct and hand-curated color sections (litmus-i4kf) — Done ·
archived - Add ThemeDefinition and ProviderColors types to litmus-model (litmus-jmna) — Done ·
archived - Convert existing themes to ThemeDefinition format (litmus-o4w9) — Done ·
archived - Vendor provider theme data (kitty-themes, wezterm color schemes) (litmus-vkne) — Done ·
archived - Update litmus-web to provider-scoped theme rendering (litmus-y6dc) — Done ·
archived - Build litmus extract-colors command (litmus-z20l) — Done ·
archived
- Update litmus-cli to load new theme format (litmus-dv2l) — Done ·
- Deploy screenshots to Cloudflare R2 (litmus-v2g1) — Done ·
archived- Set up rclone config and upload script (litmus-6gbb) — Done ·
archived - Create R2 bucket and API token (litmus-7k5y) — Done ·
archived - Configure R2 cache rules (litmus-exy0) — Done ·
archived - Configure custom domain for R2 bucket (litmus-mclx) — Done ·
archived - Build production manifest and deploy (litmus-wrol) — Done ·
archived - Smoke test: verify live screenshots end-to-end (litmus-zg8b) — Done ·
archived
- Set up rclone config and upload script (litmus-6gbb) — Done ·
- UI/UX improvement: shortlist, side-by-side, and contrast issues (litmus-ysy5) — Done ·
archived- Cap compare at 3 themes (litmus-4ai1) — Done ·
archived - Add compact issue chips per theme on compare page (litmus-72xs) — Done ·
archived - Add readability badges and contrast markers to compare page (litmus-962t) — Done ·
archived - New compare entry flow (litmus-k3fj) — Done ·
archived - Rename shortlist to favorites and decouple from compare (litmus-nqee) — Done ·
archived - Per-theme issue dots in sidebar fixture minimap (litmus-p5xo) — Done ·
archived
- Cap compare at 3 themes (litmus-4ai1) — Done ·
Features
- Compact scene rendering mode (litmus-1vws) — Done ·
archived - Light/dark and contrast quality filters (litmus-1wbl) — Done ·
archived - Live terminal capture (litmus-2mjz) — Done ·
archived - Provider ecosystem view (litmus-2w3r) — Done ·
archived - Provider-scoped URL routing with fixture anchors (litmus-3svg) — Done ·
archived - Show all themes with availability feedback (litmus-4uyp) — Done ·
archived - Theme-first vs provider-first navigation (litmus-5fj8) — Done ·
archived - Graceful provider switch when theme unavailable (litmus-5www) — Done ·
archived - Compare tray (persistent bottom bar) (litmus-65nt) — Done ·
archived - Tabbed scene navigation on detail page (litmus-73hr) — Done ·
archived - Shortlist UX improvements (litmus-84j7) — Done ·
archived - kitty.conf export (litmus-bpp9) — Done ·
archived - TOML and Nix config export (litmus-ct1q) — Done ·
archived - Theme search by name (litmus-dbel) — Done ·
archived - Compare accumulator with floating bar (litmus-e29y) — Done ·
archived - ANSI color swatches display (litmus-g6c0) — Done ·
archived - Side-by-side theme comparison (litmus-gjmg) — Done ·
archived - Theme listing page with family grouping (litmus-gyq0) — Done ·
archived - GitHub star button + Cloudflare Pages deployment prep (litmus-gzd5) — Done ·
archived - Grid layout for scene-across-themes (litmus-ibhf) — Done ·
archived - Contrast issues feature improvements (litmus-jzjb) — Done ·
archived - Interactive contrast issue navigation with footnotes (litmus-mm3f) — Done ·
archived - TUI navigation (litmus-oonb) — Done ·
archived - Color diff overlay on compare (litmus-p07t) — Done ·
archived - Core HTML renderer (litmus-q2my) — Done ·
archived - Web UI/UX overhaul: sidebar + full-width + app theming (litmus-qm77) — Done ·
archived - App Theme UI Improvements (litmus-r6e1) — Done ·
archived - Mini scene preview on theme cards (litmus-s6e2) — Done ·
archived - Hardcoded app mock-ups (litmus-t78w) — Done ·
archived - Sidebar simplification & shortlist redesign (litmus-t8hr) — Done ·
archived - Single-theme detail page (litmus-v3cn) — Done ·
archived - Unified scene navigation & sticky filters (litmus-x2wd) — Done ·
archived - Sticky toolbar for page-level controls (litmus-ywps) — Done ·
archived - Create terminal scenes (litmus-yx5o) — Done ·
archived - Multi-theme compare route (litmus-yy2r) — Done ·
archived
Tasks
- Update litmus-cli to render TermOutput (litmus-0uoe) — Done ·
archived - Build ANSI-to-spans parser using VTE (litmus-28sq) — Done ·
archived - Curate 10-20 high-quality themes (litmus-2ixq) — Done ·
archived - Visual polish and edge case handling (litmus-2xrw) — Done ·
archived - Compact expandable palette display (litmus-3fax) — Done ·
archived - Research existing terminal output datasets and ANSI test suites (litmus-3lcp) — Done ·
archived - Improve compare view layout (litmus-3px1) — Done ·
archived - Set up GitHub Pages for docs.litmus.edger.dev (litmus-3zkh) — Done ·
archived - Cap compare at 3 themes (litmus-4ai1) — Done ·
archived - Define canonical theme file format (litmus-4t4e) — Done ·
archived - Fix sidebar, shortlist, palette, and compare view issues (litmus-4tgw) — Done ·
archived - Generate and curate first batch of candidate fixtures (litmus-52qn) — Done ·
archived - Cache-bust manifest.json fetch to prevent stale CDN cache (litmus-5osv) — Done ·
archived - Theme quality checks (litmus-5tj3) — Done ·
archived - Set up rclone config and upload script (litmus-6gbb) — Done ·
archived - M6: CI Automation for Screenshots (litmus-6y50) — Done ·
archived - Add compact issue chips per theme on compare page (litmus-72xs) — Done ·
archived - Create R2 bucket and API token (litmus-7k5y) — Done ·
archived - Diagnose: identify exact failing spans for light/dark theme pairs (litmus-7qql) — Done ·
archived - Litmus repo skeleton setup (litmus-7rix) — Done ·
archived - Implement kitty.conf and base16 YAML parsers (litmus-8dqh) — Done ·
archived - Migrate from just to mise (litmus-8sd8) — Done ·
archived - Add readability badges and contrast markers to compare page (litmus-962t) — Done ·
archived - Integrate ANSI capture into fixture pipeline (litmus-9eg8) — Done ·
archived - Update README with usage instructions (litmus-9uzr) — Done ·
archived - M1: Screenshot Data Model (litmus-b10b) — Done ·
archived - Update contrast validation for TermColor (litmus-bcel) — Done ·
archived - Share link and decision flow (litmus-bo4e) — Done ·
archived - Build color swatch and color showcase fixtures (litmus-c55s) — Done ·
archived - DNS: CNAME litmus.edger.dev → litmus.pages.dev (litmus-dcjz) — Done ·
archived - Update litmus-cli to load new theme format (litmus-dv2l) — Done ·
archived - Change screenshot capture ratio from 16:9 to 4:3 (litmus-dvjb) — Done ·
archived - Reorder fixtures for better first impression (litmus-e9d6) — Done ·
archived - Configure R2 cache rules (litmus-exy0) — Done ·
archived - Write first Litmus blogpost: What If You Could Test Drive a Terminal Theme? (litmus-f6mk) — Done
- Set up fixtures/candidates/ staging directory and review workflow (litmus-feex) — Done ·
archived - M5: Web App Integration (litmus-fgts) — Done ·
archived - Parse kitty.conf theme files (litmus-fp9r) — Done ·
archived - Define scene format (litmus-h4yq) — Done ·
archived - Design internal theme representation (litmus-i1bi) — Done ·
archived - Remove old Theme struct and hand-curated color sections (litmus-i4kf) — Done ·
archived - Update mdbook documentation to reflect current project state (M6-M13) (litmus-iqfq) — Done ·
archived - M4: Cloudflare R2 Storage Setup (litmus-j0d6) — Done ·
archived - Add ThemeDefinition and ProviderColors types to litmus-model (litmus-jmna) — Done ·
archived - Expand themes collection to ~60 themes (litmus-jr2u) — Done ·
archived - New compare entry flow (litmus-k3fj) — Done ·
archived - Docs update and spec brainstorming (litmus-k6ah) — Done ·
archived - Remove old Scene, ThemeColor, StyledSpan types and hand-written scenes (litmus-kbzo) — Done ·
archived - Fix scene color choices that bias against light themes (litmus-ktsr) — Done ·
archived - M3: Capture Tool (litmus-capture) (litmus-lizo) — Done ·
archived - Update litmus-web to render TermOutput instead of Scene (litmus-lm76) — Done ·
archived - Cloudflare Pages dashboard setup (litmus-lwju) — Done ·
archived - Configure custom domain for R2 bucket (litmus-mclx) — Done ·
archived - Convert themes to canonical format (litmus-mpo6) — Done ·
archived - Rename shortlist to favorites and decouple from compare (litmus-nqee) — Done ·
archived - Simplify nav bar (litmus-o3j2) — Done ·
archived - Convert existing themes to ThemeDefinition format (litmus-o4w9) — Done ·
archived - Create CHANGELOG.md (litmus-otas) — Done ·
archived - Per-theme issue dots in sidebar fixture minimap (litmus-p5xo) — Done ·
archived - Add TermColor, TermSpan, TermLine, TermOutput types to litmus-model (litmus-q9lp) — Done ·
archived - Contrast and readability validation in scenes (litmus-r363) — Done ·
archived - M2: Fixture System (litmus-rz7q) — Done ·
archived - Audit existing fixtures against quality criteria (litmus-sk2k) — Done ·
archived - Responsive layout with monospace font rendering (litmus-smve) — Done ·
archived - Keyboard navigation for detail page (litmus-ti3e) — Done ·
archived - Implement APCA algorithm for light theme readability scoring (litmus-u02e) — Done ·
archived - Web app shell (litmus-u1cy) — Done ·
archived - Scene selector tabs on comparison page (litmus-udmz) — Done ·
archived - Organize themes by family (litmus-vas0) — Done ·
archived - Vendor provider theme data (kitty-themes, wezterm color schemes) (litmus-vkne) — Done ·
archived - Theme validation and error handling (litmus-vvye) — Done ·
archived - Unit tests for theme parsing (litmus-wl4v) — Done ·
archived - Write comprehensive contrast tests for light and dark themes (litmus-wp56) — Done ·
archived - Build production manifest and deploy (litmus-wrol) — Done ·
archived - Screenshot in side-by-side view (litmus-x2vo) — Done ·
archived - Update litmus-web to provider-scoped theme rendering (litmus-y6dc) — Done ·
archived - Set up mdbook docs site (litmus-yesx) — Done ·
archived - Upgrade bacon export for richer Claude Code diagnostics (litmus-yte1) — Done ·
archived - Build litmus extract-colors command (litmus-z20l) — Done ·
archived - Integration test skeleton for essential features (litmus-z7p4) — Draft
- Smoke test: verify live screenshots end-to-end (litmus-zg8b) — Done ·
archived
Bugs
- Investigate Cloudflare cache rules not applying to R2 custom domain (litmus-7zy0) — Done ·
archived - Fix: Missing Cargo.lock for crane/nix build (litmus-el0x) — Done ·
archived - Fix headless screenshot capture: kitty EGL + config keys (litmus-haaf) — Done ·
archived - Remove provider dropdown from detail page (litmus-lywi) — Done ·
archived - Fix wasm-bindgen-cli version mismatch (litmus-r5xi) — Done ·
archived - Inline contrast issue markers with hover tooltips (litmus-rs0b) — Done ·
archived - Fix minimap placement, scoring consistency, duplicate scenes (litmus-vryv) — Done ·
archived
Drafts
Epics
- Fixtures iteration — more realistic, better color showcase (litmus-49jz) — Done ·
archived - Unify scenes and fixtures (litmus-coma) — Done ·
archived - Iteration 2: Bug fixes + design feedback (litmus-jvkn) — Done ·
archived - Provider-based theme definition (litmus-knrz) — Done ·
archived - Deploy screenshots to Cloudflare R2 (litmus-v2g1) — Done ·
archived - UI/UX improvement: shortlist, side-by-side, and contrast issues (litmus-ysy5) — Done ·
archived
Fixtures iteration — more realistic, better color showcase (litmus-49jz)
Goal
Establish a repeatable workflow to improve fixtures over time. Not a one-shot redesign — a process for ongoing iteration with human curation.
Quality Criteria
Every fixture must meet:
- Color variety — uses ≥4 distinct ANSI colors naturally (not forced)
- Instant recognition — a developer recognizes the scenario within 2 seconds
- Fits 80x24 — no truncation, no scrolling needed
- Deterministic — no timestamps, PIDs, paths that vary between runs (use fixed dates, fake PIDs, $FIXTURE_WORK_DIR)
- Self-contained — setup.sh creates all needed state, no external dependencies beyond the tool itself
Two Special Fixtures
Color swatch (raw reference):
- Small program that prints all 16 ANSI colors as labeled fg+bg blocks, 256-color palette grid, truecolor gradients
- Pure validation artifact, not a real scenario
fixtures/color-swatch/
Color showcase (themed):
- Synthetic but real-looking scenario designed to hit every ANSI color naturally
- E.g. a status dashboard: green OK, yellow WARN, red FAIL, blue info, magenta debug, cyan links, bright variants for headers, bg colors for status bars
fixtures/color-showcase/
Content Sources (priority order)
- Existing datasets/collections — terminal output samples, ANSI test suites, terminal emulator test fixtures
- Agent-generated — Claude writes fixture scripts for specific scenarios, human reviews
- Real tool output — capture from popular dev tools (ripgrep, delta, bat, docker, kubectl)
Staging & Review Workflow
fixtures/
candidates/ # staging area
bat-syntax/
setup.sh
command.sh
REVIEW.md # source, colors used, quality assessment
git-diff/ # promoted fixtures
ls-color/
...
Flow:
- Generate or discover candidate → write to
fixtures/candidates/{name}/ - Run capture pipeline → inspect screenshot + parsed output
- Evaluate against quality criteria (document in REVIEW.md)
- Accepted → move to
fixtures/{name}/, delete REVIEW.md - Rejected → delete or note why for future reference
Candidate Scenarios to Research
High-value fixtures that don’t exist yet:
- ripgrep/grep — search results with filename, line number, match highlighting
- bat/cat — syntax-highlighted source code (Rust, Python, YAML)
- docker ps / kubectl — tabular colored output with status indicators
- journalctl / log viewer — severity-colored log lines (DEBUG, INFO, WARN, ERROR)
- tmux/zellij status bar — TUI chrome with background colors
- neovim/helix — editor UI (already a scene, no fixture yet)
- diff (delta) — enhanced diff with syntax highlighting
Relationship to Other Epics
Independent of but complementary to litmus-coma (unify scenes/fixtures). New fixtures created here automatically benefit from the unified pipeline once built.
Subtasks (in dependency order)
Unblocked (can run in parallel):
litmus-c55s— Build color swatch and color showcase fixtureslitmus-feex— Set up fixtures/candidates/ staging directory and review workflowlitmus-3lcp— Research existing terminal output datasets and ANSI test suiteslitmus-sk2k— Audit existing fixtures against quality criteria
Blocked by 2 + 3:
5. litmus-52qn — Generate and curate first batch of candidate fixtures
Summary of Changes
All 5 subtasks completed. The fixtures system now has 12 fixtures covering common terminal scenarios: git-diff, git-log, ls-color, cargo-build, shell-prompt, python-repl, htop, color-showcase, ripgrep-search, bat-syntax, log-viewer, and editor-ui. A candidates/ staging workflow with quality criteria review process is established. Research identified key external resources (tinted-theming/schemes, Gogh, shell-color-scripts) for future expansion.
Unify scenes and fixtures (litmus-coma)
Goal
Replace hand-written scenes with parsed real ANSI output from fixtures. One source of truth (fixture scripts) drives both the simulated view (spans) and screenshots. Simulated view becomes the primary rendering, screenshots become validation.
Key decisions:
- Fixtures → Scenes direction (real output is ground truth, not hand-written)
- Span-based storage, not cell grid (compact, ligature-friendly, fast HTML rendering)
- TermColor supports full spectrum: Default, Ansi(0-15), Indexed(16-255), Rgb(r,g,b)
- Output files are per-provider (aligns with litmus-knrz provider-scoped rendering)
- Capture pipeline tees ANSI stream and parses it alongside screenshot capture
Data Model
#![allow(unused)]
fn main() {
enum TermColor {
Default, // theme fg or bg
Ansi(u8), // 0-15, resolved from theme
Indexed(u8), // 16-255, fixed RGB
Rgb(u8, u8, u8), // 24-bit truecolor, literal
}
struct TermSpan {
text: String,
fg: TermColor,
bg: TermColor,
bold: bool,
italic: bool,
dim: bool,
underline: bool,
}
struct TermLine {
spans: Vec<TermSpan>,
}
struct TermOutput {
id: String, // fixture id
name: String, // display name
cols: u16,
rows: u16,
lines: Vec<TermLine>,
}
}
Parsing Pipeline
fixture command.sh → raw ANSI stream → VTE parser → intermediate cell grid → collapse to spans → TermOutput → output.{provider}.json
File Layout
fixtures/git-diff/
setup.sh # existing, unchanged
command.sh # existing, unchanged
output.kitty.json # generated — parsed TermOutput from kitty capture
output.wezterm.json # generated — parsed TermOutput from wezterm capture
Web App Rendering
- TermOutput replaces Scene as the renderable unit
- TermColor resolution: Default → theme fg/bg, Ansi(n) → ProviderColors palette, Indexed(n) → fixed 256-color table, Rgb → literal CSS
- Provider selector switches both simulated view (output.{provider}.json) and screenshot
- Contrast validation updated for TermColor (theme-dependent pairs validated, fixed colors checked against theme bg)
Subtasks (in dependency order)
Unblocked:
litmus-q9lp— Add TermColor, TermSpan, TermLine, TermOutput types to litmus-model
Blocked by 1:
2. litmus-28sq — Build ANSI-to-spans parser using VTE
Blocked by 2:
3. litmus-9eg8 — Integrate ANSI capture into fixture pipeline
Blocked by 1 + 3 (can run in parallel):
4. litmus-lm76 — Update litmus-web to render TermOutput instead of Scene
5. litmus-0uoe — Update litmus-cli to render TermOutput
Blocked by 1 + 4:
6. litmus-bcel — Update contrast validation for TermColor
Blocked by 4 + 5 + 6:
7. litmus-kbzo — Remove old Scene, ThemeColor, StyledSpan types and hand-written scenes
Summary of Changes
All 7 subtasks completed. The codebase now uses TermOutput (parsed from real ANSI output) as the sole rendering model. Scene/ThemeColor/StyledSpan types have been fully removed. The TermOutput data model supports Default, Ansi(0-15), Indexed(16-255), and Rgb colors with bold/italic/dim/underline modifiers. Both litmus-cli and litmus-web render TermOutput fixtures, and contrast validation uses the new TermColor-based analysis.
Iteration 2: Bug fixes + design feedback (litmus-jvkn)
8 items: fix app theme switcher, filter navigation, remove scene sidebar, separate filters, readability score, clickable contrast issues, move CVD, richer sidebar items
Summary of Changes
All 8 items implemented:
- Fix app theme switcher (
shell.rs): Movedapp_theme.read()insideuse_effectso Dioxus tracks the dependency - Filter navigation (
sidebar.rs): Addeduse_navigator()— search, variant, and readability filter changes pushRoute::ThemeList - Remove scene tabs from sidebar (
sidebar.rs): Removed Scenes section entirely - Separate variant/contrast filters (
sidebar.rs): All/Dark/Light now show count badges; readability is a separate dropdown below - Readability score (
contrast.rs,state.rs,sidebar.rs,theme_list.rs,theme_detail.rs): Addedreadability_score(), replacedgood_contrast: boolwithmin_readability: Option<u8>, shown on sidebar items, theme cards, and detail header - Clickable contrast issues (
theme_detail.rs): Issues text is now a toggle button; expands to show per-scene issue list with fg/bg chips and ratios - Move CVD (
sidebar.rs): CVD section moved below Compare, just above App Theme - Richer sidebar items (
sidebar.rs,style.css): Items use theme bg/fg colors, show readability badge, subtle borders between items
Scene tabs moved to top of ThemeDetail (before palette, above scene content).
Provider-based theme definition (litmus-knrz)
Goal
Flip the theme model from hand-curated RGB colors to provider-first definitions. Themes are defined as provider → theme name mappings. Colors are extracted from vendored provider theme data at build time, per provider. Simulated scenes, contrast validation, and screenshots are all scoped to a provider.
Priorities: Accuracy (provider colors are source of truth) > Simplicity (minimal authored data) > Coverage (start with popular built-in themes from kitty/wezterm)
Design
Data Model
- ThemeDefinition (authored): name, variant (dark/light), providers map (slug → provider theme name)
- ProviderColors (generated per-provider): provider slug, source_version, all 22 color fields
- No single ResolvedTheme — everything is per-provider
File Layout
themes/gruvbox/
gruvbox-dark.toml # authored: name, variant, provider mappings
gruvbox-dark.kitty.toml # generated: colors extracted from kitty
gruvbox-dark.wezterm.toml # generated: colors extracted from wezterm
Authored file format
name = "Gruvbox Dark"
variant = "dark"
[providers]
kitty = "Gruvbox Dark"
wezterm = "Gruvbox Dark (Gogh)"
Generated file format (e.g. gruvbox-dark.kitty.toml)
# AUTO-GENERATED by litmus extract-colors — do not edit
provider = "kitty"
source_version = "0.37.0"
[colors]
background = "#282828"
foreground = "#ebdbb2"
cursor = "#ebdbb2"
selection_background = "#ebdbb2"
selection_foreground = "#282828"
[colors.ansi]
black = "#282828"
red = "#cc241d"
# ... remaining 14 colors
Extraction Pipeline
- Vendored theme data in vendor/ (kitty-themes, wezterm-colorschemes — git subtree or submodule)
- Command:
litmus extract-colors [--provider kitty] [--theme gruvbox-dark] - Reads ThemeDefinition, looks up provider theme name in vendored data, parses native format, writes per-provider color files
- Reuses existing parsers (kitty.rs, base16.rs)
Web App
- load_embedded_themes() returns Vec
+ map of (theme_slug, provider_slug) → ProviderColors - Theme only listed if it has ≥1 ProviderColors file
- Provider selector switches both screenshots AND simulated scenes
- Contrast validation scoped to selected provider
- Theme list cards show available provider badges
CLI
- Loads ThemeDefinition + picks one ProviderColors (first available or –provider flag)
Subtasks (in dependency order)
Unblocked (can start in parallel):
litmus-jmna— Add ThemeDefinition + ProviderColors types to litmus-modellitmus-vkne— Vendor provider theme data (kitty-themes, wezterm color schemes)
Blocked by 1 + 2:
3. litmus-z20l — Build litmus extract-colors command
Blocked by 3:
4. litmus-o4w9 — Convert existing themes to ThemeDefinition format
Blocked by 1 + 4 (can run in parallel):
5. litmus-y6dc — Update litmus-web to provider-scoped theme rendering
6. litmus-dv2l — Update litmus-cli to load new theme format
Blocked by 5 + 6:
7. litmus-i4kf — Remove old Theme struct and hand-curated color sections
Summary of Changes
All 7 subtasks completed. Themes are now defined as ThemeDefinition (metadata + base16 colors) + ProviderColors (per-provider ANSI palette extraction). The extract-colors CLI command extracts colors from vendored provider theme data (kitty-themes, wezterm schemes). Both litmus-cli and litmus-web render themes using provider-scoped colors with a global provider selector.
Deploy screenshots to Cloudflare R2 (litmus-v2g1)
End-to-end setup to serve screenshot images from Cloudflare R2 at screenshots.litmus.edger.dev. Includes bucket creation, custom domain, rclone-based upload sync, cache headers, and manifest deployment. Approach: local capture → rclone sync to R2 → served via custom domain with aggressive caching on images (immutable, 1yr) and short TTL (60s) on manifest.json.
Infrastructure Setup
- R2 Bucket:
litmus-screenshots - Endpoint URL:
https://537f79be922377f50fb4ed655f6ab6b7.r2.cloudflarestorage.com/litmus-screenshots - Account ID env var:
CLOUDFLARE_APP_ID - API Token env var:
SCREENSHOTS_UPLOADING_API_TOKEN - Target domain:
https://screenshots.litmus.edger.dev
Summary of Changes
All 1560 screenshots (117.9 MiB) deployed to Cloudflare R2 and serving live at https://screenshots.litmus.edger.dev. Version bumped to 0.3.0.
What was done
- R2 bucket
litmus-screenshotscreated with custom domain + CORS policy - rclone added to devShell for checksum-based sync
- mise tasks:
screenshots-sync(sync only) andscreenshots-deploy(manifest + sync) - Production manifest built and deployed
- Web app verified working end-to-end
Known issue
- Cloudflare cache rules showing
cf-cache-status: DYNAMIC— may need zone-level investigation. Not blocking; content serves correctly.
UI/UX improvement: shortlist, side-by-side, and contrast issues (litmus-ysy5)
The side-by-side compare page is missing contrast issue indicators — one of the project’s killer features. The current shortlist/compare coupling creates UX friction: shortlist is limited to 5, compare gets messy with many themes, and users may confuse the two concepts.
Problems
- Missing contrast issues in side-by-side view — the most valuable analysis feature is absent from the comparison page
- Compare page scales poorly — works well for 2 themes, OK for 3, unusable for more without a very wide monitor
- Shortlist/compare coupling is confusing — shortlist limited to 5 because it’s tied to compare; users may not understand the relationship
- Count badges get too cluttered — too many themes make the contrast issue badges unreadable
Current Thinking
- Convert shortlist to favorites or history (decouple from compare)
- Improve side-by-side UX: limit max compare count, or enforce minimum width per theme
- Needs a larger design rethink to make these features work smoothly together
Status
Refined — ready for implementation. See subtasks.
Brainstorming Session (2026-03-26)
Current State Analysis
Compare page (compare.rs):
- Shows 2-4 themes side-by-side in a grid
- Two view modes: simulated terminal output + screenshots
- Color palette section at bottom
- No contrast issues shown —
TermOutputViewreceives emptyissue_details - URL:
/:provider/compare/slug1,slug2,...
Contrast issue system (fully built on detail page):
validate_fixtures_contrast()→Vec<TermContrastIssue>per themebuild_issue_registry()→ deduped rules (C1, C2…) by TermColor pair- Interactive chips with click-to-cycle navigation between affected fixtures
- Span-level markers (dotted outlines), tooltips, merged footnotes
- Readability score (0-100%) per theme
- All CSS classes and components exist and are reusable
Shortlist (state.rs, sidebar.rs):
MAX_SHORTLIST = 5, stored asVec<String>of slugs- Coupled to compare: sidebar builds compare URL from app theme + shortlist
- If < 2 themes, fills with random themes
- FIFO overflow when full
Design Proposal A: “Focused Compare” — Limit to 2-3, decouple shortlist
Core idea: Compare works best with 2-3 themes. Optimize for that and stop pretending 4+ works.
Changes:
-
Hard cap compare at 3 themes (down from 4)
- 2 themes: each gets ~50% width — plenty of room for contrast markers
- 3 themes: each gets ~33% — still workable with compact chips
-
Add contrast issues to compare page:
- Readability % badge in each column header (ScoreRing, already exists)
- Compact issue chips below each theme name (reuse
detail-issue-chip) - Span-level markers + tooltips on terminal output (pass
issue_detailstoTermOutputView) - Footnotes at line ends
- Per-fixture issue count badge on fixture headers
-
Rename shortlist → “Favorites”:
- Remove the 5-item limit (or raise to 20)
- Favorites persist in localStorage, shown in sidebar
- Star icon on cards and detail page
- Not directly tied to compare URL
-
New compare entry point:
- “Compare” button in sidebar picks first 2-3 favorites
- Or: checkboxes on browse page to select specific themes to compare
- Compare URL remains the same, just built differently
Pros: Simple, focused, contrast issues fit naturally. Clear separation of concerns. Cons: Loses ability to compare 4 themes. Favorites redesign is extra work.
Design Proposal B: “Compare with Contrast Overlay” — Enhance existing, minimal structural change
Core idea: Add contrast issues to the existing compare page with minimal UX disruption.
Changes:
-
Keep 2-4 theme support as-is
-
Add contrast summary row below column headers:
- Readability % (ScoreRing) + total issue count per theme
- Color-coded: green (≥85%), orange (70-84%), red (<70%)
- Compact — one line per theme
-
Add “Show issues” toggle (default off to avoid clutter):
- When ON: span markers + tooltips appear on all columns
- Footnotes shown inline
- Compact chip strip per theme (scrollable if many rules)
-
Cross-theme issue highlighting:
- Click an issue chip on theme A → highlight same TermColor pair on all themes
- Makes it easy to see “red-on-default is bad in theme A but fine in theme B”
-
Shortlist tweaks (minimal):
- Rename to “Pinned themes” for clarity
- Keep limit at 5 (matches max compare of 4 + app theme)
- Add tooltip explaining purpose
Pros: Least disruptive. Adds contrast without changing navigation model. Cross-theme highlighting is a unique differentiator. Cons: 4-theme compare + contrast markers may still be too cramped. Shortlist confusion only partially addressed.
Design Proposal C: “Split Model” — Favorites + Focused Compare
Core idea: Completely decouple browsing (favorites) from analysis (compare). Compare becomes a deliberate, deep-analysis tool for exactly 2 themes.
Changes:
-
Replace shortlist with “Favorites”:
- Unlimited (practical limit ~50), persisted to localStorage
- Star toggle on cards, detail page, scene-across page
- Sidebar section: expandable list of favorited themes
- No connection to compare
-
Redesign compare as a 2-theme deep comparison:
- Always exactly 2 themes, full width split
- Each side: full contrast analysis (chips, markers, footnotes, readability)
- New: “Contrast diff” view — highlight issues unique to each theme
- Green markers: “this theme passes where the other fails”
- Red markers: “this theme fails where the other passes”
- Grey: both pass or both fail
- Entry: “Compare with…” button on detail page, or pick 2 from favorites
-
For 3+ themes, use existing “Scene Across Themes” page:
- Already shows all themes for one fixture
- Add readability badges to each theme card there
- This is the right UX for “scanning many themes at once”
-
New sidebar flow:
- ★ Favorites (expandable, unlimited)
- Compare: shows current compare pair (if any), or “Pick 2 to compare”
- Quick-compare: drag two favorites, or “Compare A vs B” from any theme card
Pros: Cleanest conceptual model. Compare is purpose-built for contrast analysis. “Scene Across” handles the “many themes” case. Favorites are intuitive. Cons: Most work. Loses 3-4 theme compare (redirected to scene-across). Bigger UX change for existing users.
Recommendation
Start with Proposal A as the pragmatic path:
- Cap at 3 themes is a small constraint with big UX payoff
- Contrast issues slot in naturally with existing infrastructure
- Favorites is a clean rename with lifted limit
- Can evolve toward Proposal C later if the 2-theme deep comparison proves valuable
Quick win regardless of approach: Pass issue_details to TermOutputView on the compare page + add readability badges. This alone adds huge value with minimal code change.
Features
- Compact scene rendering mode (litmus-1vws) — Done ·
archived - Light/dark and contrast quality filters (litmus-1wbl) — Done ·
archived - Live terminal capture (litmus-2mjz) — Done ·
archived - Provider ecosystem view (litmus-2w3r) — Done ·
archived - Provider-scoped URL routing with fixture anchors (litmus-3svg) — Done ·
archived - Show all themes with availability feedback (litmus-4uyp) — Done ·
archived - Theme-first vs provider-first navigation (litmus-5fj8) — Done ·
archived - Graceful provider switch when theme unavailable (litmus-5www) — Done ·
archived - Compare tray (persistent bottom bar) (litmus-65nt) — Done ·
archived - Tabbed scene navigation on detail page (litmus-73hr) — Done ·
archived - Shortlist UX improvements (litmus-84j7) — Done ·
archived - kitty.conf export (litmus-bpp9) — Done ·
archived - TOML and Nix config export (litmus-ct1q) — Done ·
archived - Theme search by name (litmus-dbel) — Done ·
archived - Compare accumulator with floating bar (litmus-e29y) — Done ·
archived - ANSI color swatches display (litmus-g6c0) — Done ·
archived - Side-by-side theme comparison (litmus-gjmg) — Done ·
archived - Theme listing page with family grouping (litmus-gyq0) — Done ·
archived - GitHub star button + Cloudflare Pages deployment prep (litmus-gzd5) — Done ·
archived - Grid layout for scene-across-themes (litmus-ibhf) — Done ·
archived - Contrast issues feature improvements (litmus-jzjb) — Done ·
archived - Interactive contrast issue navigation with footnotes (litmus-mm3f) — Done ·
archived - TUI navigation (litmus-oonb) — Done ·
archived - Color diff overlay on compare (litmus-p07t) — Done ·
archived - Core HTML renderer (litmus-q2my) — Done ·
archived - Web UI/UX overhaul: sidebar + full-width + app theming (litmus-qm77) — Done ·
archived - App Theme UI Improvements (litmus-r6e1) — Done ·
archived - Mini scene preview on theme cards (litmus-s6e2) — Done ·
archived - Hardcoded app mock-ups (litmus-t78w) — Done ·
archived - Sidebar simplification & shortlist redesign (litmus-t8hr) — Done ·
archived - Single-theme detail page (litmus-v3cn) — Done ·
archived - Unified scene navigation & sticky filters (litmus-x2wd) — Done ·
archived - Sticky toolbar for page-level controls (litmus-ywps) — Done ·
archived - Create terminal scenes (litmus-yx5o) — Done ·
archived - Multi-theme compare route (litmus-yy2r) — Done ·
archived
Compact scene rendering mode (litmus-1vws)
Add compact prop to SceneView: smaller font, tighter padding, max-height with fade-out gradient. Makes 19 themes fit in ~4 screens.
Summary of Changes
The scene-across-themes page now uses compact=true on SceneView, which uses smaller font, tighter padding, and max-height with fade-out gradient. Makes 19 themes scannable in a grid.
Light/dark and contrast quality filters (litmus-1wbl)
Add toggle filters above theme grid: light/dark mode filter and ‘good contrast only’ toggle that hides themes with WCAG AA failures.
Summary of Changes
Added filter controls to theme listing:
- VariantFilter enum with All/Dark/Light modes using relative_luminance check
- Good contrast toggle using validate_theme_readability
- FilterButton component with active/inactive styling
- Filtered theme count indicator
- Empty state when no themes match
- CSS: .filter-bar and .filter-btn styles
Live terminal capture (litmus-2mjz)
Run real commands (git diff, ls) and display output with theme colors applied.
Plan
- Create with that takes real command output and applies theme colors
- Update to export
- Update to capture command output and add (Tab cycles: Swatches → Mockups → Live)
Summary of Changes
Added widgets/live.rs with LiveWidget that captures real git diff and ls -la output at startup and renders it with theme colors applied. Git diff lines are colored by prefix (+/-/@@/headers); ls entries are colored by file type (directory/symlink/executable/hidden). Tab now cycles Swatches → Mockups → Live.
Provider ecosystem view (litmus-2w3r)
Provider ecosystem view (e.g. ‘kitty ecosystem’ showing all consumers together).
Summary of Changes
Added scene-centric ecosystem view:
- /scene/:scene_id route renders one scene across all 19 themes
- Each theme rendering links to its detail page for drill-down
- Nav bar now includes scene links (Shell Prompt, Git Diff, etc.) for quick access
- Enables side-by-side comparison of how themes handle the same terminal context
Provider-scoped URL routing with fixture anchors (litmus-3svg)
Restructure URL routing to include the provider and support fixture-level deep links.
Current: /theme/ayu-light Target: /kitty/theme/ayu-light#python-repl
Requirements
- Change route from /theme/:slug to /:provider/theme/:slug
- Derive ActiveProvider from URL parameter instead of global signal
- Apply same pattern to other routes: /:provider/compare/:slugs, /:provider/scene/:scene_id
- Browse page: /:provider/ (or keep / and redirect based on provider)
- Add anchor IDs to fixture sections in detail page (scene-{fixture.id} may already exist)
- Update URL hash on scroll using existing IntersectionObserver logic
- Navigating to a URL with #fixture-id scrolls to that fixture
- Update all internal links (browse page cards, sidebar, minimap) to include provider prefix
Notes
Provider switching in sidebar should navigate to the new provider-scoped URL rather than toggling a global signal.
Summary of Changes
Implemented provider-scoped URL routing:
- Route enum uses
#[nest("/:provider")]to prefix all routes with the provider - Root
/redirects to/{default_provider}/ - Shell syncs URL provider → ActiveProvider signal
- Provider selector navigates via Link instead of toggling state
- All internal links include provider parameter
- Fixture deep-links:
#fixture-idscrolls on load, URL hash updates on scroll via IntersectionObserver
Key files: main.rs (Route enum + helpers), shell.rs (sync), sidebar.rs (navigation), all page components
Show all themes with availability feedback (litmus-4uyp)
Theme list only shows themes for the active provider — switching providers refreshes the list jarringly. Show all themes instead with dimmed cards + badge for unavailable ones. ## Design: Dimmed cards with unavailable badge
- Replace themes_for_provider() with a function that returns all themes, each annotated with an available: bool flag for the current provider.
- Available themes: Render normally, clickable, navigate to detail page.
- Unavailable themes: Reduced opacity (0.4–0.5), small “unavailable” badge or icon overlay, cursor: not-allowed, click suppressed (no navigation).
- Alphabetical sort preserved — unavailable themes stay in their natural position, not pushed to the bottom.
- Filter counts (variant toggles, total) reflect only available themes for the active provider.
- Search/variant/readability filters still apply to all themes (available and unavailable).
Files: themes.rs (new all_themes_with_availability()), theme_list.rs (card rendering + click gating), state.rs (filter counts), style.css (dimmed card styles)
Tasks
- Add all_themes_with_availability() returning (Theme, bool) tuples
- Update theme_list.rs to render unavailable cards with reduced opacity
- Add ‘unavailable’ badge/icon overlay on dimmed cards
- Suppress click/navigation for unavailable themes (cursor: not-allowed)
- Keep alphabetical sort with unavailable themes in natural position
- Filter counts reflect only available themes
- Add CSS for dimmed card state (.theme-card–unavailable or similar)
- Test that search/variant/readability filters apply to all themes
Plan
themes.rs
Add all_themes_with_availability(provider: &str) -> Vec<(Theme, bool)>:
- For each definition, check if provider colors exist for the requested provider →
available = true - If unavailable, use first available provider’s colors for rendering →
available = false - Every definition has at least one provider (existing test guarantees this), so all are renderable
- Sort alphabetically by name
theme_list.rs
- Replace
themes_for_provider()withall_themes_with_availability() - Pass
available: boolto ThemeCard component - Filter counts (variant badges, total) count only available themes
- Shown count / filter applies to all themes (available + unavailable)
ThemeCard component
- Accept
available: boolprop - If unavailable: wrap in div instead of Link, add
.theme-card--unavailableclass, show badge - If available: keep existing Link behavior
style.css
.theme-card--unavailable: opacity 0.45, cursor not-allowed, pointer-events none on card-link.theme-card-unavailable-badge: small label overlay
Summary of Changes
Added all_themes_with_availability() to themes.rs that returns every theme annotated with a bool for provider availability. Unavailable themes fall back to the first available provider’s colors for rendering.
Updated ThemeList to show all themes — unavailable cards are dimmed (opacity 0.45), have an “unavailable” badge, no hover effect, and no navigation. Extracted ThemeCardBody as a shared component to avoid RSX duplication. Filter counts (variant badges) reflect available themes only; shown/total counter reflects all visible cards.
5 new tests cover the availability function (count, sorting, marking, unavailable presence, nonexistent provider).
Theme-first vs provider-first navigation (litmus-5fj8)
Theme-first vs provider-first navigation (or hybrid) for browsing the library.
Summary of Changes
Added dual navigation on the home page:
- “Browse by Scene” section with clickable scene cards (name + description) linking to scene-across-themes view
- “Browse by Theme” section with family-grouped theme cards
- Both navigation styles were already functional; this makes them discoverable from the landing page
Graceful provider switch when theme unavailable (litmus-5www)
When viewing a theme detail page and switching to a provider that doesn’t have the theme, currently loads a blank “Theme not found” page. Instead, stay on the current page and show an inline alert.
Requirements
- When provider switch is requested but theme is unavailable for target provider, stay on current page
- Show an alert/banner on the current page: “{theme} is not available for {provider}”
- Add provider availability metadata to the detail page (which providers have this theme) so the UI can check before navigating
- Provider selector in sidebar should indicate which providers have the current theme (e.g. dim/disable unavailable ones, or show a badge)
- Dismiss alert on user action or after timeout
Design Notes
Key insight: the user is already on the page viewing the theme. Don’t navigate away and fail — prevent the bad navigation and inform inline.
Summary of Changes
Implemented graceful provider switching:
- Provider buttons dim when theme is unavailable for that provider
- Clicking an unavailable provider shows a dismissible alert banner instead of navigating
- Alert auto-dismisses after 3s or on manual dismiss (x button)
- Added
theme_available_for_provider()helper,AlertMessagestate, andAlertBannercomponent
Key files: sidebar.rs (availability check + button logic), shell.rs (AlertBanner), state.rs (AlertMessage), themes.rs (helper), style.css (styles)
Compare tray (persistent bottom bar) (litmus-65nt)
Global floating bar showing selected themes as chips. Visible on all pages when 1+ theme selected. Max 4 themes.
Summary of Changes
Already implemented as part of M7 (litmus-e29y). The CompareBar component is a global floating bottom bar visible on all pages when 1+ theme selected, showing chips with remove buttons. Max 4 themes.
Tabbed scene navigation on detail page (litmus-73hr)
Replace AllScenesView (stacked) with tab bar. Only one scene renders at a time. Signal-driven active tab.
Summary of Changes
Replaced AllScenesView on the detail page with tabbed scene navigation. Added scene-tab buttons with active state styling. Only one scene renders at a time, driven by a use_signal index. CSS for .scene-tabs, .scene-tab, and .scene-tab-active added.
Shortlist UX improvements (litmus-84j7)
- Limit shortlist to 5 themes (keep most recent)
- Apply pushes current theme to top of shortlist
- Gray out Shortlist checkbox for current theme
- Show ‘Feel Lucky’ when shortlist empty, pick random theme
Summary of Changes
- Reduced MAX_SHORTLIST from 8 to 5
- Apply button now pushes previous app theme to top of shortlist (with dedup and truncate)
- ShortlistCheckbox and ShortlistToggle show ‘Current’ and are grayed out/disabled for the active app theme
- Detail page ‘c’ keyboard shortcut also respects current theme
- Sidebar shows ‘Feel Lucky’ button when shortlist is empty (no app theme set), which picks two random themes and navigates to compare
- Added js-sys dependency for wasm random
- Added CSS for disabled shortlist states and Feel Lucky button styling
kitty.conf export (litmus-bpp9)
Copy kitty.conf button on detail page. Serialize Theme to kitty config format. Copy to clipboard via Clipboard API.
Summary of Changes
Added export::to_kitty_conf() in litmus-model that serializes a Theme to kitty.conf format. ExportButtons component on detail page with clipboard copy via document::eval.
TOML and Nix config export (litmus-ct1q)
Copy TOML button (canonical format) and Nix snippet (programs.kitty.settings attrset). Clipboard API.
Summary of Changes
Added export::to_toml() and export::to_nix() in litmus-model. TOML exports in litmus canonical format, Nix exports as an attrset suitable for programs.kitty.settings. Both available via ExportButtons UI.
Theme search by name (litmus-dbel)
Add text input that filters themes by name or family. Pure client-side string matching.
Summary of Changes
Added theme search input to the filter bar. Case-insensitive matching against theme name and family. Combines with variant and contrast filters.
Compare accumulator with floating bar (litmus-e29y)
Global ‘Add to compare’ button stores theme slug in context signal. Floating bottom bar shows selected themes, navigates to compare when 2+ selected.
Summary of Changes
Added global compare selection using Dioxus context signal (CompareSelection). Added CompareToggle button on theme cards and detail page. Added floating CompareBar at the bottom showing selected themes as chips with remove buttons, a ‘Go to Compare’ link (when 2+ selected), and a Clear button. CSS for .compare-toggle, .compare-bar, .compare-chip, and .compare-bar-btn. Max 4 themes can be selected.
ANSI color swatches display (litmus-g6c0)
Display the 16 ANSI colors + fg/bg/cursor/selection as colored blocks in the terminal.
Summary of Changes
Implemented ANSI color swatches TUI display using ratatui and crossterm. Added ThemeWithExtras wrapper with hardcoded Tokyo Night theme. SwatchesWidget renders 2x8 grid with labels plus fg/bg/cursor/sel row. main.rs bootstraps TUI with clean terminal restore on quit.
Side-by-side theme comparison (litmus-gjmg)
Side-by-side theme comparison: same scene rendered with two different themes.
Summary of Changes
Added side-by-side theme comparison:
- /compare/:left/:right route renders all scenes in two themes side by side
- Theme selector dropdowns for switching comparison targets
- “Compare…” link on each theme detail page
- Responsive CSS: two columns on desktop, stacked on mobile
Theme listing page with family grouping (litmus-gyq0)
Theme listing page organized by theme family.
Summary of Changes
Added family grouping to theme listing page:
family.rs: theme_family() extracts family from name using known prefix list; group_by_family() groups themes- Listing page now shows themes organized under family headings (Catppuccin, Gruvbox, etc.)
- Standalone themes (Dracula, Nord, Kanagawa) appear as their own single-theme groups
GitHub star button + Cloudflare Pages deployment prep (litmus-gzd5)
- Add GitHubStars component to components.rs
- Add star button to sidebar header row
- Add CSS for star button and header row
- Create _redirects file for SPA routing
- Create _headers file for security/cache headers
- Create GitHub Actions deploy workflow
- Create beans for manual Cloudflare/DNS tasks
Summary of Changes
- Added
GitHubStarscomponent usinguse_resource+eval()to fetch stargazer count from GitHub API - Restructured sidebar header with flexbox row for logo + star pill button
- Created
_redirects(SPA fallback) and_headers(security + caching) for Cloudflare Pages - Created GitHub Actions workflow (
deploy.yml) that builds WASM and deploys via wrangler - Created follow-up beans for manual Cloudflare dashboard and DNS setup
Grid layout for scene-across-themes (litmus-ibhf)
Replace vertical scenes-container with 2-column CSS grid for scannable comparison of all themes.
Summary of Changes
Replaced vertical scenes-container with a 2-column CSS grid (.scene-grid) for the scene-across-themes page. Each theme gets a .scene-grid-card with the theme name link and compare toggle.
Contrast issues feature improvements (litmus-jzjb)
Add stable slugs, scene tab badges, visual-first issue display, and scene span markers
Summary of Changes
1. Model: Stable slugs for issue types
- Added
ThemeColor::slug()method returning short identifiers (“fg”, “bg”, “ansi1”, etc.) - Added
slug,fg_color, andbg_colorfields toContrastIssue - Slug format:
"scene-id/fg-slug-on-bg-slug"(e.g."cargo-build/ansi1-on-bg") - Populated from span fg/bg in
validate_scene_contrast
2. Scene tab badges
- Built
HashMap<&str, usize>counting issues per scene - Scene tab buttons show a pill badge with count when > 0
- CSS: small red pill using
--app-errorbackground
3. Visual-first issue display
- Deduplicated issues by slug within each scene group
- Replaced text-heavy layout with colored sample spans showing actual fg/bg
- Ratio shown prominently, hex codes dimmer as secondary info
- Scene group headers are clickable buttons that navigate to that scene tab
4. Scene span markers
- Added
issue_spans: Vec<(usize, usize)>prop to SceneView - Threaded through LineView (line_idx) → SpanView (has_issue)
- CSS class
contrast-issue-spanwith dashed red outline - theme_detail.rs filters issues to current scene and builds the (line, span) vec
- ScenePreview and AllScenesView use defaults (no markers)
Interactive contrast issue navigation with footnotes (litmus-mm3f)
Enhance contrast issue display with navigable issue IDs, merged footnotes, and click-to-cycle behavior.
Issue IDs
- Assign short IDs (C1, C2, …) to each unique contrast rule violation
- Key by (fg_term, bg_term) — the TermColor variants, not resolved hex values
- Same ANSI color role = same ID across fixtures and themes
- Chip legend shows: “C1: bright black on bg — 2.3:1”
Merged Footnotes
- Display small superscript footnote tags (C1, C2) on affected spans in the rendered terminal output
- Merge footnotes for contiguous rectangular regions:
- If consecutive lines have the same issue at the same column range, show one footnote for the block
- e.g. editor-ui line numbers (C1 on lines 1-20, col 0-3) → single C1 tag at edge of block
- Footnotes are for visual connection to the header chips, not interactive themselves
Click-to-Cycle
- Clicking a chip (C1) in the header scrolls to the first fixture containing that issue
- Subsequent clicks cycle to the next fixture with the same issue
- When all occurrences have been visited, cycling wraps around
- Clicking an already-selected chip deselects it
Visual Feedback
- Selected chip: filled/highlighted style (vs default outline)
- Markers matching the focused issue: enhanced border (brighter/thicker) or subtle pulse
- Non-focused issue markers: dim slightly when another issue is focused
State
active_issue: Option<(String, usize)>— selected issue ID + current fixture index in cycle- Derived: list of fixture IDs containing each issue for cycling
Requirements
- Change dedup key from (fg_hex, bg_hex) to (fg_term, bg_term) for issue identity
- Assign stable short IDs (C1, C2, …) to unique rule violations
- Update header chips to show ID + color role name + ratio
- Implement contiguous region merging for footnote placement
- Render merged footnotes as superscript tags on affected spans/blocks
- Add active_issue state and click-to-cycle on header chips
- Scroll to fixture on chip click, cycle on repeat click
- Visual feedback: selected chip style, enhanced focused markers, dimmed others
- CSS for footnote tags, selected chip, focused/dimmed marker states
Notes
- Mobile: not a focus, just ensure it’s not broken. Footnotes are visual-only, interaction is via header chips.
- If footnotes prove too noisy in practice, fall back to color-coded borders per issue ID (option C from brainstorm).
Summary of Changes
Implemented interactive contrast issue navigation with:
- Stable rule IDs (C1, C2, …) keyed by (fg_term, bg_term) via
build_issue_registry - Merged footnotes at end of contiguous issue regions via
compute_footnotes - Clickable issue chips that cycle through affected fixtures with smooth scrolling
- Focused/dimmed visual states for markers and footnotes
- Escape key to clear focus
- 7 unit tests for the pure-logic functions
Key files: term_renderer.rs (registry, footnotes, rendering), theme_detail.rs (state, chips, cycling), style.css (visual states)
TUI navigation (litmus-oonb)
Switch between themes, toggle between swatches/mock-ups/live views. Use ratatui or similar TUI framework.
Summary of Changes
- Added
catppuccin_mocha()andsolarized_dark()theme constructors totheme_data.rs - Added
all_themes()returning all three themes - Introduced
Appstruct with themes, theme_index, view, git_diff, ls_output fields - Added
View::name()returning display name for status bar - Refactored
run()to useApp::new()and split layout into content + 1-line status bar - Status bar shows: theme name [N/M] | view name | key hints
- Key bindings: Tab (next view), BackTab/Shift+Tab (prev view), Left (prev theme), Right (next theme)
Color diff overlay on compare (litmus-p07t)
Toggle that highlights spans where resolved colors differ between compared themes. Dashed underline on differing spans.
Summary of Changes
Added ColorDiffTable component to the compare page. Shows a collapsible table of all 19 colors (bg, fg, cursor + 16 ANSI) with swatches and hex values per theme. Rows where colors differ are highlighted with a subtle pink background. Header shows count of differing colors.
Core HTML renderer (litmus-q2my)
Core renderer: theme data + annotated content → colored HTML spans (monospace).
Summary of Changes
Added web rendering engine:
scene_renderer.rs: Dioxus components that resolve ThemeColor references to inline CSS styles on HTML spans inside monospace pre blocksthemes.rs: compile-time embedding of all 19 themes for WASM buildsmain.rs: full app with theme selector dropdown and all scenes rendered- Added PartialEq derives to Theme/AnsiColors/Scene for Dioxus component props
Web UI/UX overhaul: sidebar + full-width + app theming (litmus-qm77)
Replace top nav + floating compare bar with persistent left sidebar. Full-width layout. App chrome theming via CSS custom properties. Mobile drawer. CRT easter egg.
Summary of Changes
Phase 1: File Split + State Consolidation
- Split
main.rs(1269 lines) into modular files:state.rs— Global signals: CompareSelection, FilterState, ActiveScene, AppThemeSlug, SidebarOpen + helper functionscomponents.rs— Shared components: FilterButton, ColorSwatch, CompareToggle, CvdSelector, ExportButtons, ColorDiffTablepages/theme_list.rs— ThemeList (home grid)pages/theme_detail.rs— ThemeDetailpages/scene_across.rs— SceneAcrossThemespages/compare.rs— CompareThemes + CompareSelectorsidebar.rs— Persistent sidebar with all navigationshell.rs— Shell layout (sidebar + main area)
main.rsreduced to ~47 lines: App, Route, main() only- All filter/compare state lifted to global signals via use_context_provider
Phase 2: Sidebar + Layout
- Replaced top nav + floating CompareBar with persistent 280px left sidebar
- Sidebar sections: logo, search, filters (All/Dark/Light/contrast), CVD selector, scrollable theme list (family-grouped with color dots), scene chips, compare management, app theme dropdown
- Main content area is full-width (fluid)
- Two-column flexbox layout
Phase 3: App Theming
- 12 CSS custom properties (
--app-*) dynamically set from any selected theme theme_to_css_vars_js()generates JS to set all properties on documentElementuse_effectin Shell watches AppThemeSlug and applies theme- Default Tokyo Night-inspired colors as fallback
- Light theme detection via luminance → sets
data-theme="light"for CSS adjustments - All hardcoded colors replaced with
var(--app-*)references throughout style.css
Phase 4: Mobile Polish
- Sidebar becomes slide-in drawer on ≤768px with CSS transform transition
- Mobile header bar with hamburger button
- Overlay backdrop when drawer is open
- 44px minimum touch targets on interactive elements
Phase 5: CRT Easter Egg
- CSS-only scanline effect on
.scene-block prewhendata-crt="true"is set on root - Includes repeating gradient scanlines, inset box-shadow, text-shadow glow, and flicker animation
- Scene transitions: opacity 0.15s ease on scene pre blocks
App Theme UI Improvements (litmus-r6e1)
Restructure ThemeCard, add ShortlistCheckbox, rework sidebar, rename button text, fix tooltip clipping
Summary of Changes
1. ThemeCard restructured (theme_list.rs)
- Link now wraps only the card body (header + preview + swatches), not the footer actions
- Action buttons sit outside the Link in a
.theme-card-actionsdiv with app theme colors
2. ShortlistCheckbox added (components.rs)
- New
ShortlistCheckboxcomponent with<label><input type=checkbox> Shortlist</label> - Used on browse page cards;
ShortlistToggle(button) kept for detail page - Renamed “Use as App Theme” → “Apply”, “✓ App Theme” → “✓ Applied”
3. Sidebar reworked (sidebar.rs)
- Removed compare checkboxes (
checked: HashSet) and all checkbox-based gating - App theme pinned at top of shortlist with “current” badge
- Compare URL = app theme + shortlist slugs (deduped); shown when 2+ items
4. Tooltip clipping fixed (scene_renderer.rs, style.css)
line_idxforwarded toSpanView; lines 0-1 getcontrast-tooltip-belowclass.scene-block pre:overflow-x: clip; overflow-y: visibleinstead ofoverflow-x: auto.contrast-issues-list: increased max-height,overflow-y: visible
5. CSS updates (style.css)
.theme-card-actionsgets explicit app theme bg/fg/border.shortlist-checkboxstyles.sidebar-current-badgeand.sidebar-shortlist-name-linkstyles- Removed old
.sidebar-shortlist-checkstyles
Mini scene preview on theme cards (litmus-s6e2)
Render a compact 4-5 line excerpt of the shell-prompt scene on each ThemeCard so users can feel a theme at a glance.
Summary of Changes
Added mini scene preview to theme cards:
- New ScenePreview component renders first N lines of a scene with no title
- ThemeCard now shows 5-line shell prompt preview between name and swatches
- CSS: .scene-preview with 0.6rem font, max-height, and gradient fade-out mask
- Also added .scene-compact CSS class for future grid views
Hardcoded app mock-ups (litmus-t78w)
Simple app mock-ups: hardcoded terminal output rendered with theme colors (fake git diff, ls output, shell prompt).
Summary of Changes
- Created widgets/util.rs with shared to_ratatui_color() helper
- Created widgets/mockups.rs with MockupsWidget rendering hardcoded git diff and ls -la output using theme colors
- Updated widgets/swatches.rs to use the shared helper from util
- Updated widgets/mod.rs to export both widgets
- Updated main.rs with View enum (Swatches/Mockups) and Tab key cycling between views
Sidebar simplification & shortlist redesign (litmus-t8hr)
Simplify sidebar to shortlist/favorites, move filters to browse page, fix compare workflow, add UseAsAppTheme button
Summary of Changes
State refactoring (state.rs, main.rs)
- Renamed
CompareSelectiontoShortlistwithMAX_SHORTLIST = 8 - Extracted CVD from
FilterStateinto newCvdSimulationglobal signal - Made
FilterStatepage-local (removed from global context providers) - Updated all context providers in
App
Components (components.rs)
- Renamed
CompareToggletoShortlistTogglewith +Shortlist/Shortlisted text - Added
UseAsAppThemeButtoncomponent (toggles app theme, shows checkmark when active)
Browse page (theme_list.rs)
- Added inline filter bar (search, variant buttons, readability dropdown)
- Flat alphabetical grid — removed family group headings
- ThemeCard now shows ShortlistToggle + UseAsAppThemeButton
- FilterState is local
use_signal
Sidebar (sidebar.rs)
- Stripped search, variant filters, readability dropdown, theme list, app theme dropdown
- Added nav links (Browse Themes, Compare)
- Added shortlist panel with checkboxes for compare selection
- Compare button builds URL from checked themes (max 4)
- CVD stays in sidebar as global accessibility tool
Detail page (theme_detail.rs)
- Replaced CompareToggle with ShortlistToggle + UseAsAppThemeButton
- Reads CVD from
CvdSimulationsignal - Updated
ckeyboard shortcut to toggle shortlist
Compare page (compare.rs)
- Removed
CompareSelectordropdowns — URL is source of truth - Reads CVD from
CvdSimulationsignal
Scene across page (scene_across.rs)
- Updated to use ShortlistToggle and CvdSimulation
CSS (style.css)
- Added: .filter-bar, .filter-bar-search, .filter-bar-readability, .shortlist-toggle, .use-as-app-theme-btn, .sidebar-shortlist*, .sidebar-nav*
- Removed: .sidebar-search, .sidebar-filters, .sidebar-readability*, .sidebar-theme-list, .sidebar-family*, .sidebar-theme-item, .sidebar-app-theme*, .family-group, .family-name, .compare-toggle, .compare-selector, .compare-select, .compare-vs, .compare-chip*
Cleanup (family.rs)
- Removed unused
ThemeFamilystruct andgroup_by_familyfunction - Kept
theme_family()(still used for search matching)
Single-theme detail page (litmus-v3cn)
Single-theme detail page showing all ecosystem previews for that theme.
Summary of Changes
Enhanced theme detail page:
- fg/bg contrast ratio display with green/red pass/fail indicator
- WCAG AA contrast validation summary showing issue count across all scenes
- ANSI color grid with color names under each swatch (8-column layout)
- All scene previews rendered below the palette
Unified scene navigation & sticky filters (litmus-x2wd)
Detail page: show all scenes vertically with minimap. Compare page: add minimap. Sticky filter bar on theme list.
Summary of Changes
1. Detail page: all scenes vertically (theme_detail.rs)
- Removed tab bar, ActiveScene signal usage, and left/right arrow key scene navigation
- All scenes now render vertically with `id=“scene-{id}”“ for scroll targeting
- Each scene section has a heading with name and issue count badge
- Contrast issue scene buttons now scroll to the scene instead of switching tabs
2. Scene minimap component (components.rs)
- Added
SceneMinimapcomponent: fixed vertical strip on the right edge (120px wide) - Uses IntersectionObserver to track which scenes are in the viewport
- Polls visible scene state every 200ms via eval-based async loop
- Click scrolls smoothly to the target scene
- Active scenes highlighted with accent border-left + accent text color
- Hidden on mobile (≤768px)
3. Compare page (compare.rs)
- Added
id="scene-{scene.id}"to eachcompare-scene-groupdiv - Added
SceneMinimapcomponent
4. Sticky filter bar (style.css)
- Filter bar is now position: sticky with top: 0, z-index: 30
- Has background and border-bottom for visual separation
- Mobile: top: 45px to sit below mobile header
5. State (state.rs + main.rs)
- Added
VisibleScenes(HashSet<String>)signal - Provided as context in App component
Sticky toolbar for page-level controls (litmus-ywps)
Add a sticky toolbar at the top of the content area so page controls remain accessible when scrolling.
Requirements
- Create a sticky toolbar component (position: sticky; top: 0; z-index above content)
- Detail page toolbar: provider badge, contrast score summary, shortlist toggle
- Compare page toolbar: Simulated / Screenshot toggle, fixture selector
- Browse page toolbar: search input + filter controls (already sticky)
- Style toolbar to match app chrome theme with subtle bottom border/shadow
- Ensure toolbar doesn’t overlap with sidebar on desktop or mobile header
Notes
Each page defines its own toolbar contents. The shell layout provides the sticky container, pages fill it via a slot or context pattern.
Summary of Changes
Made page-level controls sticky across all pages using CSS position: sticky. Detail page header (theme name, contrast ratio, shortlist), compare page toolbar (simulated/screenshot toggle + column headers), and scene-across fixture tabs all now stick at the top when scrolling. Mobile responsive offsets account for the 45px mobile header. Browse page filter bar was already sticky.
Files changed:
crates/litmus-web/assets/style.css— sticky positioning + mobile offsets for .detail-header, .compare-toolbar, .scene-tabs
Create terminal scenes (litmus-yx5o)
Create realistic terminal scenes: shell prompt, ls –color, git diff (with context/additions/deletions/merge conflicts), delta output, tig log view.
Summary of Changes
Added scenes module with 5 built-in terminal scenes:
- Shell prompt: prompts with grep output, error messages, and dirty branch indicator
- Git diff: multi-hunk diff with context/additions/deletions
- Directory listing: colorized ls -la with dirs, executables, symlinks, hidden files
- Cargo build: compiler warnings, errors with code spans, notes, and help suggestions
- Git log: branch graph with commit hashes, HEAD/remote decorations, branch names
Multi-theme compare route (litmus-yy2r)
Change compare from /compare/:left/:right to support 2-4 themes. N-column layout with horizontal scroll on mobile.
Summary of Changes
Changed compare route from /compare/:left/:right to /compare/:slugs with comma-separated slugs supporting 2-4 themes. N-column grid layout with compact scenes for 3+ themes. CompareSelector renders N dropdowns dynamically.
Tasks
- Update litmus-cli to render TermOutput (litmus-0uoe) — Done ·
archived - Build ANSI-to-spans parser using VTE (litmus-28sq) — Done ·
archived - Curate 10-20 high-quality themes (litmus-2ixq) — Done ·
archived - Visual polish and edge case handling (litmus-2xrw) — Done ·
archived - Compact expandable palette display (litmus-3fax) — Done ·
archived - Research existing terminal output datasets and ANSI test suites (litmus-3lcp) — Done ·
archived - Improve compare view layout (litmus-3px1) — Done ·
archived - Set up GitHub Pages for docs.litmus.edger.dev (litmus-3zkh) — Done ·
archived - Cap compare at 3 themes (litmus-4ai1) — Done ·
archived - Define canonical theme file format (litmus-4t4e) — Done ·
archived - Fix sidebar, shortlist, palette, and compare view issues (litmus-4tgw) — Done ·
archived - Generate and curate first batch of candidate fixtures (litmus-52qn) — Done ·
archived - Cache-bust manifest.json fetch to prevent stale CDN cache (litmus-5osv) — Done ·
archived - Theme quality checks (litmus-5tj3) — Done ·
archived - Set up rclone config and upload script (litmus-6gbb) — Done ·
archived - M6: CI Automation for Screenshots (litmus-6y50) — Done ·
archived - Add compact issue chips per theme on compare page (litmus-72xs) — Done ·
archived - Create R2 bucket and API token (litmus-7k5y) — Done ·
archived - Diagnose: identify exact failing spans for light/dark theme pairs (litmus-7qql) — Done ·
archived - Litmus repo skeleton setup (litmus-7rix) — Done ·
archived - Implement kitty.conf and base16 YAML parsers (litmus-8dqh) — Done ·
archived - Migrate from just to mise (litmus-8sd8) — Done ·
archived - Add readability badges and contrast markers to compare page (litmus-962t) — Done ·
archived - Integrate ANSI capture into fixture pipeline (litmus-9eg8) — Done ·
archived - Update README with usage instructions (litmus-9uzr) — Done ·
archived - M1: Screenshot Data Model (litmus-b10b) — Done ·
archived - Update contrast validation for TermColor (litmus-bcel) — Done ·
archived - Share link and decision flow (litmus-bo4e) — Done ·
archived - Build color swatch and color showcase fixtures (litmus-c55s) — Done ·
archived - DNS: CNAME litmus.edger.dev → litmus.pages.dev (litmus-dcjz) — Done ·
archived - Update litmus-cli to load new theme format (litmus-dv2l) — Done ·
archived - Change screenshot capture ratio from 16:9 to 4:3 (litmus-dvjb) — Done ·
archived - Reorder fixtures for better first impression (litmus-e9d6) — Done ·
archived - Configure R2 cache rules (litmus-exy0) — Done ·
archived - Write first Litmus blogpost: What If You Could Test Drive a Terminal Theme? (litmus-f6mk) — Done
- Set up fixtures/candidates/ staging directory and review workflow (litmus-feex) — Done ·
archived - M5: Web App Integration (litmus-fgts) — Done ·
archived - Parse kitty.conf theme files (litmus-fp9r) — Done ·
archived - Define scene format (litmus-h4yq) — Done ·
archived - Design internal theme representation (litmus-i1bi) — Done ·
archived - Remove old Theme struct and hand-curated color sections (litmus-i4kf) — Done ·
archived - Update mdbook documentation to reflect current project state (M6-M13) (litmus-iqfq) — Done ·
archived - M4: Cloudflare R2 Storage Setup (litmus-j0d6) — Done ·
archived - Add ThemeDefinition and ProviderColors types to litmus-model (litmus-jmna) — Done ·
archived - Expand themes collection to ~60 themes (litmus-jr2u) — Done ·
archived - New compare entry flow (litmus-k3fj) — Done ·
archived - Docs update and spec brainstorming (litmus-k6ah) — Done ·
archived - Remove old Scene, ThemeColor, StyledSpan types and hand-written scenes (litmus-kbzo) — Done ·
archived - Fix scene color choices that bias against light themes (litmus-ktsr) — Done ·
archived - M3: Capture Tool (litmus-capture) (litmus-lizo) — Done ·
archived - Update litmus-web to render TermOutput instead of Scene (litmus-lm76) — Done ·
archived - Cloudflare Pages dashboard setup (litmus-lwju) — Done ·
archived - Configure custom domain for R2 bucket (litmus-mclx) — Done ·
archived - Convert themes to canonical format (litmus-mpo6) — Done ·
archived - Rename shortlist to favorites and decouple from compare (litmus-nqee) — Done ·
archived - Simplify nav bar (litmus-o3j2) — Done ·
archived - Convert existing themes to ThemeDefinition format (litmus-o4w9) — Done ·
archived - Create CHANGELOG.md (litmus-otas) — Done ·
archived - Per-theme issue dots in sidebar fixture minimap (litmus-p5xo) — Done ·
archived - Add TermColor, TermSpan, TermLine, TermOutput types to litmus-model (litmus-q9lp) — Done ·
archived - Contrast and readability validation in scenes (litmus-r363) — Done ·
archived - M2: Fixture System (litmus-rz7q) — Done ·
archived - Audit existing fixtures against quality criteria (litmus-sk2k) — Done ·
archived - Responsive layout with monospace font rendering (litmus-smve) — Done ·
archived - Keyboard navigation for detail page (litmus-ti3e) — Done ·
archived - Implement APCA algorithm for light theme readability scoring (litmus-u02e) — Done ·
archived - Web app shell (litmus-u1cy) — Done ·
archived - Scene selector tabs on comparison page (litmus-udmz) — Done ·
archived - Organize themes by family (litmus-vas0) — Done ·
archived - Vendor provider theme data (kitty-themes, wezterm color schemes) (litmus-vkne) — Done ·
archived - Theme validation and error handling (litmus-vvye) — Done ·
archived - Unit tests for theme parsing (litmus-wl4v) — Done ·
archived - Write comprehensive contrast tests for light and dark themes (litmus-wp56) — Done ·
archived - Build production manifest and deploy (litmus-wrol) — Done ·
archived - Screenshot in side-by-side view (litmus-x2vo) — Done ·
archived - Update litmus-web to provider-scoped theme rendering (litmus-y6dc) — Done ·
archived - Set up mdbook docs site (litmus-yesx) — Done ·
archived - Upgrade bacon export for richer Claude Code diagnostics (litmus-yte1) — Done ·
archived - Build litmus extract-colors command (litmus-z20l) — Done ·
archived - Integration test skeleton for essential features (litmus-z7p4) — Draft
- Smoke test: verify live screenshots end-to-end (litmus-zg8b) — Done ·
archived
Update litmus-cli to render TermOutput (litmus-0uoe)
Migrate litmus-cli mockup views from Scene to TermOutput:
- Load TermOutput data (bundled or from fixtures directory)
- Render TermSpan using crossterm colors:
- TermColor::Default → crossterm reset
- TermColor::Ansi(n) → crossterm AnsiValue
- TermColor::Indexed(n) → crossterm AnsiValue
- TermColor::Rgb → crossterm Rgb
- Minimal changes — CLI is simpler than web
Depends on: TermOutput types, fixture pipeline generating output files
Plan
- Add
resolve_with_theme()toTermColorin litmus-model (reuses existingindexed_color()) - Add
serde_jsondep to litmus-cli - Write tests:
resolve_with_themeunit tests, TermOutput-to-ratatui-Lines conversion test - Embed 3 fixture output.json files (git-diff, ls-color, shell-prompt) via
include_str!() - Rewrite MockupsWidget to parse embedded fixtures, map TermSpan → ratatui Span using theme colors
- Add
term_color_to_ratatui()helper in util.rs
Summary of Changes
- Added
TermColor::resolve_with_theme()to litmus-model for resolving terminal colors against a Theme (5 tests) - Rewrote
MockupsWidgetto render real parsed TermOutput from embedded fixture JSON files - Embedded 5 fixtures: git-diff, git-log, ls-color, cargo-build, shell-prompt
- Added Up/Down arrow keys to cycle between fixtures in the TUI
- Used
LazyLockto cache parsed fixture data (avoids re-parsing on every frame) - 8 new tests for color resolution, line conversion, modifier handling, and fixture loading
Commits: d3a0818, d5b902d, 64d5917, f45b930
Build ANSI-to-spans parser using VTE (litmus-28sq)
Add ANSI output parsing to litmus-capture (or a new litmus-parse crate):
- Use vte or termwiz crate to process raw ANSI byte streams
- Intermediate cell grid to handle cursor movement and overwrites
- Collapse adjacent cells with identical attributes into TermSpans
- Input: raw bytes from fixture command output
- Output: TermOutput struct
- Handle: SGR (colors, bold, italic, dim, underline), newlines, basic cursor movement
- Map SGR color codes to TermColor variants:
- \e[30-37m / \e[90-97m → Ansi(0-15)
- \e[38;5;Nm → Ansi/Indexed depending on N
- \e[38;2;R;G;Bm → Rgb(r,g,b)
- \e[39m / no color → Default
Depends on: TermOutput types in litmus-model
Plan
Architecture
- New module:
crates/litmus-capture/src/ansi_parser.rs - Add
vtecrate dependency for ANSI escape sequence parsing - Cell grid approach: track cursor position, attributes per cell, then collapse
Core types
CellAttrs: fg/bg TermColor + bold/italic/dim/underlineCell: character + CellAttrsGrid: rows×cols of Cells, cursor position trackingAnsiParser: wraps Grid, implements VTE Perform trait
SGR mapping
- 30-37 → Ansi(0-7) fg, 40-47 → Ansi(0-7) bg
- 90-97 → Ansi(8-15) fg, 100-107 → Ansi(8-15) bg
- 38;5;N → indexed fg, 48;5;N → indexed bg
- 38;2;R;G;B → RGB fg, 48;2;R;G;B → RGB bg
- 39 → Default fg, 49 → Default bg
- 0 → reset, 1 → bold, 2 → dim, 3 → italic, 4 → underline
Public API
pub fn parse_ansi(input: &[u8], cols: u16, rows: u16) -> TermOutput
Tests (TDD)
- Plain text without escapes
- Basic SGR colors (30-37, 40-47)
- Bright colors (90-97, 100-107)
- 256-color mode (38;5;N, 48;5;N)
- 24-bit truecolor (38;2;R;G;B)
- Bold/italic/dim/underline attributes
- Reset (SGR 0) clears all attributes
- Newline handling
- Span collapsing (adjacent same-attr cells merge)
- Cursor movement (basic)
- Real git diff output parsing
Summary of Changes
Added ansi_parser module to litmus-capture with:
- Cell grid architecture: VTE-backed parser with cols×rows grid, cursor tracking, and SGR attribute state
- Full SGR support: standard colors (30-47), bright (90-107), 256-color (38;5;N), truecolor (38;2;R;G;B), bold/italic/dim/underline, individual resets (22/23/24)
- Colon-form subparams: handles 38:5:N and 38:2::R:G:B forms used by modern terminals
- Cursor operations: CUU/CUD/CUF/CUB movement, CUP absolute positioning, EL (erase in line), ED (erase in display)
- Span collapsing: merges adjacent cells with identical attributes, trims trailing default spaces
parse-ansiCLI subcommand: reads raw ANSI from file/stdin, outputs structured TermOutput JSON- 37 tests covering all SGR mappings, edge cases (empty input, zero dimensions, column overflow), and real-world output patterns
Key design decisions:
- Characters beyond terminal width are clipped (no line wrapping)
- Trailing default-attribute spaces are trimmed; styled spaces are preserved
- Zero dimensions clamped to 1×1 to prevent underflow
Curate 10-20 high-quality themes (litmus-2ixq)
Select themes: Catppuccin Mocha/Latte/Frappe/Macchiato, Tokyo Night, Gruvbox Dark/Light, Dracula, Nord, Rose Pine, Solarized Dark/Light, Kanagawa, Everforest, etc.
Summary of Changes
Curated 19 themes across 8 families:
- Catppuccin (Mocha, Latte, Frappé, Macchiato)
- Tokyo Night (Night, Storm, Day)
- Gruvbox (Dark, Light)
- Dracula
- Nord
- Rosé Pine (Pine, Moon, Dawn)
- Solarized (Dark, Light)
- Kanagawa
- Everforest (Dark, Light)
Visual polish and edge case handling (litmus-2xrw)
Visual polish, edge case handling for very bright/dark themes and low contrast scenarios.
Summary of Changes
Visual polish for edge cases:
- Theme cards show light/dark variant label and fg/bg contrast ratio
- Neutral gray borders (rgba) visible on both light and dark theme cards/scene blocks
- Box shadow for card depth regardless of background
- Relative luminance detection for light vs dark theme classification
Compact expandable palette display (litmus-3fax)
Collapse ANSI palette into horizontal strip that expands on click. Gets palette out of the way so scene is front and center.
Summary of Changes
Replaced the full color palette on the detail page with a compact expandable palette. Compact mode shows a single row of swatches (bg, fg, cursor + ANSI strip). Clicking expands to show full detail with selection colors, labeled ANSI grid (8x2), and hex values. Driven by use_signal toggle.
Research existing terminal output datasets and ANSI test suites (litmus-3lcp)
Research what already exists for realistic terminal output samples:
- Terminal emulator test suites (xterm.js, alacritty, kitty, wezterm test data)
- ANSI art collections and archives
- Terminal recording tools (asciinema, terminalizer) — public recording galleries
- Other theme preview tools — how do they generate sample content?
- Color scheme testing tools (base16, terminal.sexy, gogh)
Goal: find reusable content or inspiration, not reinvent. Document findings in a research note. Stage promising candidates.
Research Findings
Top Recommendations for Litmus
| Priority | Resource | License | Why |
|---|---|---|---|
| 1 | tinted-theming/schemes (GitHub) | MIT | 250+ color schemes in YAML, directly importable |
| 2 | Gogh themes (GitHub) | MIT | 200+ themes, clean 18-color YAML format |
| 3 | shell-color-scripts (GitLab dwt1) | MIT | 50+ color patterns for sample preview content |
| 4 | terminal.sexy (GitHub) | MIT | Template-based preview approach (captures real tmux pane content) |
| 5 | pastel / vivid (sharkdp, Rust) | MIT/Apache-2.0 | Color manipulation + filetype database for realistic ls output |
| 6 | colortest (eliminmax) | Mixed | Canonical 256-color test pattern with Rust impl |
| 7 | asciinema asciicast v2 | Apache-2.0 | Format for capturing/replaying real terminal sessions |
| 8 | alacritty-theme (GitHub) | Apache-2.0 | 100+ TOML themes with screenshot previews |
Key Findings
Theme data sources: tinted-theming/schemes (MIT, 250+ base16/base24 YAML) and Gogh (MIT, 200+ YAML) are the most valuable for importing color schemes. Both are permissively licensed.
Preview content generation: terminal.sexy uses captured tmux panes as templates. shell-color-scripts (MIT) has 50+ bash scripts producing diverse color patterns — output could be captured as sample content. vivid’s filetypes.yml provides a comprehensive filetype database for realistic ls output.
ANSI art: 16colo.rs is the largest archive but per-artist copyright prevents bundling. Not directly usable.
Test patterns: colortest provides a standardized 256-color output (16 base + 216 cube + 24 grayscale) with Rust implementation. pastel has colorcheck for terminal capability detection.
Recording format: asciinema’s asciicast v2 (NDJSON with timestamps and raw ANSI) is the standard for terminal recordings. Public recordings could supplement fixture content.
Improve compare view layout (litmus-3px1)
Fix multiple issues with compare view:
- Remove multiple horizontal scroll bars, use single page-level scroll
- Show theme names as column headers at top only (vertical slices)
- Integrate color diff into the grid layout
- Remove repeated ‘Choose’ buttons
- Hide minimap issue badges on compare page (data is for single theme only)
Summary of Changes
- Single scroll bar: Moved
overflow-x: autofrom individual.compare-gridand.color-diff-bodyto page-level.page-compare - Vertical column headers: Added sticky theme name headers at top of page (with links to detail pages), removed per-scene theme name + Choose button repetition
- Removed Choose buttons: Theme names in sticky header are now clickable links instead
- Hidden minimap badges on compare page: Added
show_badgesprop toSceneMinimap, set to false on compare route (badges only make sense for single-theme detail view)
Set up GitHub Pages for docs.litmus.edger.dev (litmus-3zkh)
Manual steps needed to publish mdbook docs to GitHub Pages:
Tasks
- Enable GitHub Pages in repo settings (Settings → Pages → Source: “GitHub Actions”)
- Add DNS CNAME record:
docs.litmus.edger.dev→edger-dev.github.io - Configure custom domain in repo Settings → Pages → Custom domain:
docs.litmus.edger.dev - Verify HTTPS is enabled after DNS propagates
Cap compare at 3 themes (litmus-4ai1)
Enforce a maximum of 3 themes in side-by-side compare.
- Change MAX_COMPARE from 4 to 3 in state.rs
- Truncate slug list to 3 in URL parsing
- Update sidebar compare button to respect the cap
- N/A — shortlist holds up to 5 but compare URL is capped; decoupling is in litmus-nqee
- Ensure CSS grid adapts well to 2 and 3 columns with contrast markers
Summary of Changes
Changed MAX_COMPARE from 4 to 3. Sidebar compare URL builder stops at 3 slugs. Compare page URL parser uses .take(MAX_COMPARE). Compact mode triggers at 3 columns.
Define canonical theme file format (litmus-4t4e)
Define a canonical theme file format (likely TOML or JSON) based on M0 learnings.
Summary of Changes
Defined canonical TOML format with and sections. Implemented in .
Fix sidebar, shortlist, palette, and compare view issues (litmus-4tgw)
Five fixes:
- Browse Themes sidebar link active on detail pages (should not be)
- Shortlist at max 5: should evict oldest instead of refusing
- Compare page: removing from shortlist should refresh; clearing should go to themes
- Detail page color palette always expanded
- Compare page: replace ColorDiffTable with per-theme color palettes at end
Summary of Changes
- Browse Themes active state: Changed to only highlight on ThemeList, not ThemeDetail
- Shortlist evicts oldest at max: Both ShortlistCheckbox and keyboard handler now remove the oldest entry when adding a 6th theme
- Compare page reacts to shortlist changes: Remove button rebuilds compare URL and navigates; clear button navigates to themes list
- Detail page palette always expanded: Changed initial state from false to true
- Compare page palette: Removed ColorDiffTable component and CSS, added per-theme color palettes at the end of the compare view
- Cleanup: Removed dead ColorDiffTable component, ANSI_NAMES constant in components.rs, and all color-diff CSS
Generate and curate first batch of candidate fixtures (litmus-52qn)
Using findings from research + agent generation, create first batch of candidates:
Priority scenarios:
- ripgrep search results (filename, line number, match highlighting)
- bat/cat syntax-highlighted source code
- journalctl/log viewer with severity colors
- neovim/helix editor UI
For each candidate:
- Write setup.sh + command.sh
- Run capture, inspect output
- Evaluate against quality criteria
- Document assessment in REVIEW.md
- Promote good ones, discard or iterate on the rest
Depends on: staging workflow, research
Plan
- Create setup.sh + command.sh for each candidate: ripgrep, bat, journalctl, neovim-like
- Test each locally (FIXTURE_WORK_DIR + command.sh)
- Parse with litmus-capture parse-ansi
- Fill out REVIEW.md for each
- Promote good ones to fixtures/
- Create ripgrep candidate
- Create bat candidate
- Create journalctl candidate
- Create editor-ui candidate
- Test and review all
- Promote passing candidates
Summary of Changes
Created 4 fixture candidates, all passing quality criteria:
- ripgrep-search — rg search results with heading mode (magenta filenames, green line numbers, bold red match highlighting). 13 lines, uses real rg.
- bat-syntax — Syntax-highlighted Python with bat (extensive color from syntax theme). 24 lines, fills 80x24 exactly.
- log-viewer — Simulated structured app logs (INFO/WARN/ERROR/DEBUG levels). 19 lines, pure ANSI 16-color, excellent color variety.
- editor-ui — Simulated text editor UI (neovim/helix style) with syntax-highlighted Rust, line numbers, and reverse-video status bar. 23 lines, pure ANSI.
All 4 promoted to fixtures/ with output.json, REVIEW.md. Embedded in both litmus-web and litmus-cli. README updated.
Cache-bust manifest.json fetch to prevent stale CDN cache (litmus-5osv)
The browser cached an old CDN manifest.json, causing screenshots for newer fixtures to appear missing. The manifest URL has no cache-busting — switching from CDN to local dev still serves the cached CDN response.
Design
Add a hash or timestamp query parameter to the manifest.json fetch URL to prevent browser caching. Options:
- Use a build-time hash (e.g. from git rev or compile timestamp) appended as
?v=<hash> - Or set
cache: 'no-cache'on the fetch request in the JS code
The simplest fix is adding cache: 'no-cache' to the fetch options in main.rs’s tryFetch function, since the manifest itself is small and should always be fresh.
Tasks
- Add cache-busting to manifest.json fetch in main.rs (tryFetch)
- Verify dev and production both get fresh manifests
- Test that screenshot images still load (they already have
?v=<checksum>via cache_busted_url)
Plan
Add { cache: 'no-cache' } to the fetch call in the tryFetch JS function in main.rs. This tells the browser to revalidate with the server on every fetch (sends conditional request with If-None-Match/If-Modified-Since). The manifest is small (<600KB) so the revalidation overhead is negligible. No Rust code changes needed — just the embedded JS string.
Summary of Changes
Added cache: 'no-cache' option to the fetch() call in the tryFetch JS function (main.rs). This forces the browser to revalidate the manifest with the server on every load, preventing stale CDN responses from being served from disk cache. Screenshot images are unaffected — they already use checksum-based cache busting.
Theme quality checks (litmus-5tj3)
Ensure all required colors are present and no missing values for each curated theme.
Summary of Changes
All 19 theme files validated:
- All parse successfully through the TOML parser
- All required colors present (background, foreground, 16 ANSI colors)
- Optional fields (cursor, selection_background, selection_foreground) explicitly provided for all themes
- All existing litmus-model tests pass (11/11)
Set up rclone config and upload script (litmus-6gbb)
Create rclone-based sync tooling:
- Add rclone to project dev dependencies (added to flake.nix devShell)
- Create rclone config for R2 (inline flags, no config file needed)
- Write mise task
screenshots-syncwith –checksum flag - Ensure correct Content-Type headers are set on upload (rclone auto-detects from extension)
- Tested with dry-run, then full upload (117.9 MiB, ~1000 files)
- Env vars: R2_ACCESS_KEY_ID, R2_SECRET_ACCESS_KEY, R2_ENDPOINTS (set via shadowenv)
Summary of Changes
- Added
rcloneto flake.nix devShell packages - Added
screenshots-syncmise task: rclone sync with checksum-based diffing - Added
screenshots-deploymise task: builds production manifest + syncs - All env vars passed inline (no rclone.conf needed)
- Successfully uploaded all screenshots to R2 bucket
M6: CI Automation for Screenshots (litmus-6y50)
GitHub Actions workflow screenshots.yml: workflow_dispatch + push trigger on themes/** and fixtures/**. Installs Nix, runs litmus-capture capture-all, uploads to R2, updates manifest.
Summary of Changes
Created .github/workflows/screenshots.yml with:
- Triggers: workflow_dispatch (with optional provider/theme/fixture filters) + push on themes/, fixtures/, crates/litmus-capture/**
- Environment: WLR_BACKENDS=headless + WLR_RENDERER=pixman + LIBGL_ALWAYS_SOFTWARE=1 for GPU-free headless Wayland
- Steps: Nix install → cargo build litmus-capture → capture-all → manifest build → coverage check → aws s3 upload to R2
- Uses R2_ACCESS_KEY_ID, R2_SECRET_ACCESS_KEY, R2_ENDPOINT GitHub secrets
- Reports screenshot count and staging size in job summary
Note: Headless Wayland capture requires testing on actual runner; may need to add Mesa software rendering packages or adjust WLR/GL env vars.
Add compact issue chips per theme on compare page (litmus-72xs)
Interactive issue chips below each column header for navigation.
- Render compact chip strip per theme (reuse detail-issue-chip CSS)
- Adapt chip layout for narrower columns (scrollable or wrapping)
- Click chip → scroll to first affected fixture in that column
- Click again → cycle to next affected fixture
- Escape to deactivate
Summary of Changes
Added interactive contrast issue chips to the compare page’s column headers. Each theme column shows its deduplicated contrast rules as clickable chips (reusing the existing detail-issue-chip CSS with compact sizing). Clicking a chip scrolls to the first affected fixture and highlights the contrast issue spans in that column’s terminal output. Clicking again cycles to the next affected fixture. Escape clears the selection. Single-fixture chips toggle off on re-click.
Files changed:
crates/litmus-web/src/pages/compare.rs— extended ThemeContrastData with rules + fixtures_per_rule, added active_issue signal, rendered chip strips, wired focused_rule to TermOutputViewcrates/litmus-web/assets/style.css— added .compare-chips container and compact chip overrides
Create R2 bucket and API token (litmus-7k5y)
Manual setup via Cloudflare dashboard:
- Create R2 bucket (e.g.
litmus-screenshots) - Create API token with R2 read/write permissions (for rclone)
- Store Access Key ID and Secret Access Key securely
- I’ve create a API Token
- already put it in environment: SCREENSHOTS_UPLOADING_API_TOKEN
- Note the account ID and bucket endpoint URL
- app id in env: CLOUDFLARE_APP_ID
- endpoint URL: https://537f79be922377f50fb4ed655f6ab6b7.r2.cloudflarestorage.com/litmus-screenshots
- Document the setup in the epic body
Diagnose: identify exact failing spans for light/dark theme pairs (litmus-7qql)
Write a diagnostic test that outputs contrast ratios and pass/fail status for every scored span across all scenes, for both a light theme (Catppuccin Latte, Solarized Light) and its dark counterpart. Identify patterns in what’s failing.
Litmus repo skeleton setup (litmus-7rix)
Set up build system and code skeleton for the litmus terminal color theme previewer. Includes Cargo workspace, three crates (litmus-model, litmus-web, litmus-cli), flake.nix, and updates to existing files.
Summary of Changes
Created the full repo skeleton:
- Cargo.toml (workspace root): resolver 2, workspace package defaults, shared deps
- rust-toolchain.toml: stable channel, wasm32-unknown-unknown target, rustfmt+clippy
- crates/litmus-model: lib crate with
ColorandThemestructs, serde derive - crates/litmus-web: bin crate with Dioxus 0.7 hello-world app, web feature flag, Dioxus.toml
- crates/litmus-cli: bin crate with stub main
- flake.nix: fenix stable toolchain + wasm target, crane builds litmus-cli, clippy+fmt checks, devShell with dioxus-cli and just
- .gitignore: added
result,result-*,dist/ - justfile: added
dev,build-web,build-cli,check,fmtrecipes
Implement kitty.conf and base16 YAML parsers (litmus-8dqh)
Implement parsers for at least: kitty.conf and base16 YAML into the internal theme format.
Migrate from just to mise (litmus-8sd8)
Replace justfile with .mise.toml, update flake.nix, delete justfile
Summary of Changes
- Created with all 6 tasks from justfile
- Replaced with in
- Deleted
Add readability badges and contrast markers to compare page (litmus-962t)
Wire contrast issues into the compare page — the highest-value change.
- Call validate_fixtures_contrast() per theme in compare.rs
- Add ScoreRing (readability %) to each column header
- Add issue count badge next to readability score
- Pass issue_details to TermOutputView (enables span markers, tooltips, footnotes)
- Add per-fixture issue count badge on fixture headers in compare grid
Summary of Changes
- Added
ThemeContrastDatastruct andcompute_theme_contrast()helper - Column headers now show ScoreRing readability % and issue count badge
TermOutputViewreceivesissue_details, enabling span markers, tooltips, and footnotes- Per-fixture headers show worst-case issue count across compared themes
- Review fix: aligned issue count calculation with detail page (rules.len())
Integrate ANSI capture into fixture pipeline (litmus-9eg8)
Update the capture pipeline to also capture and parse ANSI output:
- When running fixture command.sh through a provider terminal, tee the ANSI byte stream (via PTY or script command)
- Parse the captured stream into TermOutput using the ANSI parser
- Write output.{provider}.json alongside the fixture scripts
- Add CLI flags: –parse-only (skip screenshot, just parse), –fixture (filter)
- Generated files are checked into git so litmus-web can embed them
Depends on: ANSI parser
Plan
- Add ParseFixtures subcommand with –fixture, –force, –cols, –rows flags
- Implement run_fixture_and_parse: runs setup.sh + command.sh, captures stdout, parses ANSI
- Write output.json alongside fixture scripts
- Generate output.json for all 9 fixtures
- Verify zero warnings, all tests pass
Summary of Changes
Added parse-fixtures subcommand to litmus-capture that runs each fixture’s setup.sh + command.sh, captures raw ANSI stdout bytes, parses them into structured TermOutput JSON using the VTE-based parser, and writes output.json alongside the fixture scripts.
The output files are provider-independent (raw terminal output is the same regardless of which terminal renders it) and are checked into git so litmus-web can embed them for theme-independent scene rendering.
Update README with usage instructions (litmus-9uzr)
README update with usage instructions for the final MVP.
Summary of Changes
Rewrote README to reflect the implemented MVP:
- Feature list covering 19 themes, 5 scenes, contrast validation, comparison, dual navigation
- Getting started section with CLI and web app run commands
- Project structure overview with key modules
- Architecture explanation: semantic color refs, provider/consumer model
- Trimmed speculative design sections, kept concise future directions
M1: Screenshot Data Model (litmus-b10b)
Add screenshot.rs to litmus-model with Provider, Fixture, ScreenshotKey, ScreenshotMeta, ScreenshotManifest types. Wire into lib.rs. Add serde round-trip tests.
Summary of Changes
Added crates/litmus-model/src/screenshot.rs with:
Provider— terminal emulator metadata (slug, name, optional version)Fixture— reproducible terminal scenario (id, name, description)ScreenshotKey— composite key (provider, theme, fixture slugs), flattened in JSONImageFormat— Png | Webp with extension/mime helpersScreenshotMeta— full screenshot record with URL, dimensions, format, timestamp, SHA-256 checksumScreenshotManifest— top-level index with find/for_provider/for_theme/build_index query methods
17 tests covering: serde round-trip, find, URL construction, cache busting, index building, JSON shape verification.
Wired into lib.rs as pub mod screenshot;.
Update contrast validation for TermColor (litmus-bcel)
Update contrast validation to work with TermOutput instead of Scene:
- TermColor::Default and Ansi(0-15) pairs: validate against theme palette (same as before)
- TermColor::Indexed/Rgb vs Default/Ansi: validate fixed color against theme-dependent counterpart
- TermColor::Indexed/Rgb vs Indexed/Rgb: skip (both fixed, theme-independent)
- New insight: can flag “this fixture uses hardcoded colors that clash with this theme’s background”
- Update ReadabilityIssue to reference TermSpan positions
Depends on: TermOutput types, web rendering migration
Plan
- Add
TermContrastIssuestruct (references fixture_id and TermColor variants) - Add
validate_term_output_contrast(output: &TermOutput, theme: &Theme)function - Skip pairs where both fg and bg are fixed (Indexed/Rgb vs Indexed/Rgb)
- Validate all other pairs using APCA
- Add
validate_all_fixtures_contrast(fixtures: &[TermOutput], theme: &Theme)convenience - Tests: low-contrast detection, skip-fixed-pairs, dim exclusion
Summary of Changes
- Added
TermContrastIssuestruct with fixture_id and TermColor variant fields - Added
validate_term_output_contrast()— validates TermOutput spans using APCA- Skips fixed-color pairs (Indexed/Rgb vs Indexed/Rgb)
- Skips Default/Default pairs (theme-controlled)
- Skips dim and whitespace-only spans
- Added
validate_fixtures_contrast()— aggregates across multiple fixtures - 7 tests: ANSI detection, fixed-pair skip, fixed-on-theme, dim skip, default/default skip, Ansi-on-Ansi bg, multi-fixture aggregation
Commits: f868232, 53b2a5e
Share link and decision flow (litmus-bo4e)
Copy link button on detail page. ‘Choose this one’ button on compare page that navigates to detail with export options.
Summary of Changes
Added ‘Copy Link’ button in the export section on the detail page. Added ‘Choose’ links on the compare page next to each theme name that navigate to the detail page with export options.
Build color swatch and color showcase fixtures (litmus-c55s)
Create two special-purpose fixtures:
color-swatch/ — raw reference palette:
- Small Rust or shell program
- Prints all 16 ANSI colors (0-15) as labeled foreground text + background blocks
- 256-color palette grid (6x6x6 cube + 24 grayscale)
- A few truecolor gradients (red→green, dark→light, hue wheel)
- Must fit 80x24
color-showcase/ — themed palette in context:
- Looks like a real scenario (e.g. status dashboard, CI pipeline summary)
- Naturally exercises all 16 ANSI colors including brights
- Uses background colors for status bars/highlights
- Deterministic output
Plan
color-swatch fixture
A shell script that prints a reference palette:
- 16 ANSI colors as labeled foreground text and background blocks
- 256-color palette grid (6×6×6 cube sampling + grayscale)
- A few truecolor gradient samples
- setup.sh is empty (no state needed), command.sh does all output
color-showcase fixture
A simulated CI/deploy dashboard that naturally uses all 16 ANSI colors:
- Build status lines (green pass, red fail, yellow warning)
- Deploy pipeline stages
- Service health checks
- Deterministic, self-contained printf/echo output
Todo
- Create color-swatch fixture (setup.sh + command.sh)
- Create color-showcase fixture (setup.sh + command.sh)
- Test both fixtures locally
- Update README.md fixture inventory
- Review
Summary of Changes
Created two new fixtures:
- color-swatch: Reference palette with 16 ANSI colors (fg labels + bg blocks), full 256-color cube, grayscale ramp, and truecolor gradient. 19 lines.
- color-showcase: Simulated CI deploy dashboard exercising all 16 standard and bright ANSI colors through build, deploy, health check, and error sections. 20 lines.
Both are pure printf output (no setup needed), deterministic, fit 80x24, and parse cleanly through the ANSI parser. Updated fixtures/README.md inventory.
DNS: CNAME litmus.edger.dev → litmus.pages.dev (litmus-dcjz)
Manual: Add CNAME record for litmus.edger.dev pointing to litmus.pages.dev in Cloudflare DNS. Verify HTTPS works after propagation.
Update litmus-cli to load new theme format (litmus-dv2l)
Migrate litmus-cli from old Theme struct to ThemeDefinition + ProviderColors:
- Load ThemeDefinition + pick one ProviderColors (first available, or –provider flag)
- Thread ProviderColors through to rendering (swatches, mockups, live views)
- Minimal changes — CLI is simpler than web
Depends on: new model types, converted themes
Plan
-
Update
theme_data.rs:- Add
load_bundled_provider_themes(provider: Option<&str>) -> Vec<Theme> - Uses
load_themes_dir()to load ThemeDefinitions + ProviderColors - For each ThemeDefinition, picks first available ProviderColors (or filtered by provider)
- Converts (ThemeDefinition, ProviderColors) → Theme via
to_theme()helper - Falls back to hardcoded themes if nothing found
- Add
-
Update
main.rs:- Add
--providerCLI flag - Pass to
load_bundled_provider_themes()
- Add
-
Keep all widget code unchanged — still render with
&Theme
Todo
- Add ProviderColors → Theme conversion
- Update load_bundled_themes to use load_themes_dir
- Add –provider CLI flag
- Tests pass, zero warnings
- Review
Summary of Changes
Migrated litmus-cli from directly loading Theme objects to using the new provider-based system:
- ProviderColors::to_theme(): New method converts provider colors + theme name into a renderable Theme
- load_bundled_themes(provider): Rewrote to use load_themes_dir(), picking one ProviderColors per ThemeDefinition (first available alphabetically, or filtered by –provider)
- –provider CLI flag: Simple arg parser for –provider
filtering - All widget rendering unchanged — still receives &Theme
- Hardcoded fallback themes preserved for environments without themes/ directory
- 160 tests pass, zero warnings
Change screenshot capture ratio from 16:9 to 4:3 (litmus-dvjb)
Current screenshots are 1280x720 (16:9), which is too wide for terminal content — leaves dead space on the right. Switch to 4:3 (e.g. 960x720 or 1024x768) for a more natural terminal aspect ratio.
Tasks
- Decide exact capture dimensions: 80 cols × 32 rows → ~1280×960 (4:3 at 2x)
- Update capture config/code with new terminal window size
- Fix display resolution via wlr-randr (WLR_SCREEN_SIZE removed in wlroots 0.19)
- Re-capture all screenshots for both providers (kitty, wezterm)
- Rebuild manifest
- Verify screenshots look good at new ratio
Implementation
The root cause was that grim captures the entire Wayland display managed by cage, not just the terminal window. Changing TermGeometry rows only affected terminal cell config, not the compositor resolution.
Fix: Set WLR_SCREEN_SIZE environment variable on the cage command in capture.rs to {pixel_width}x{pixel_height} from the geometry. Added pixel_width and pixel_height fields to TermGeometry (default 1280x960 = 4:3).
Notes
- wlroots 0.19+ ignores
WLR_SCREEN_SIZEenv var — usedwlr-randr --custom-modeinside the cage wrapper script instead - Added
pixel_width/pixel_heighttoTermGeometry(default 1280x960 = 4:3) - Added
wlr-randrto nix devshell dependencies
Re-capture requires Wayland + GPU environment:
mise run capture-kitty
mise run capture-wezterm
mise run capture-manifest
Then verify the new screenshots have ~4:3 ratio and look good.
Reorder fixtures for better first impression (litmus-e9d6)
Reorder the fixture list so the most visually informative fixtures appear first. Current order is arbitrary (roughly insertion order). New order prioritizes showing theme character at a glance.
New Order
Tier 1 — At a glance (show all colors, reveal theme character):
- color-showcase — exercises all 16 ANSI colors
- editor-ui — rich syntax highlighting, line numbers, status bars
Tier 2 — Real-world developer workflows: 3. bat-syntax — syntax highlighting 4. git-diff — add/remove coloring 5. cargo-build — error/warning colors 6. ripgrep-search — match highlighting
Tier 3 — Shell & TUI: 7. git-log — graph + decorations 8. shell-prompt — minimal but everyday 9. python-repl — prompt + output 10. ls-color — file type colors 11. htop — TUI with bars and gauges 12. log-viewer — structured log levels
Requirements
- Reorder FIXTURE_DATA in fixtures.rs
- Verify default_fixture() returns color-showcase (used for browse page preview cards)
- Run tests
Summary of Changes
Reordered FIXTURE_DATA in fixtures.rs from arbitrary insertion order to a tiered layout. color-showcase is now first (best preview thumbnail for browse page cards).
Files changed:
crates/litmus-web/src/fixtures.rs
Configure R2 cache rules (litmus-exy0)
Set up cache headers via Cloudflare cache rules (or R2 response headers):
- Images (*.webp): 1 year edge + browser TTL via Cloudflare cache rule
- manifest.json: 1 minute edge + browser TTL via Cloudflare cache rule
- CORS: deferred to smoke test — custom domain through Cloudflare proxy should handle it
- Verify headers — CORS working, cache rules showing DYNAMIC (needs follow-up)
Write first Litmus blogpost: What If You Could Test Drive a Terminal Theme? (litmus-f6mk)
First promotional blogpost for Litmus. Publish on personal blog + Reddit (r/commandline, r/unixporn, r/rust). Goal: drive traffic to litmus.edger.dev. Tone: casual dev storytelling + visual showcase. ~550 words, 5 sections with screenshots. Approved structure: 1) Frustration hook + hero screenshot, 2) Core value prop + comparison screenshot, 3) Real output differentiator + fixture screenshots, 4) Accessibility twist + CVD screenshot, 5) CTA.
Summary of Changes
Drafted the full blogpost at ~/blog/content/posts/what-if-you-could-test-drive-a-terminal-theme.md with 6 screenshots placed at ~/blog/static/images/litmus-*.png. ~500 words, 5 sections following the approved outline.
Set up fixtures/candidates/ staging directory and review workflow (litmus-feex)
Set up the staging infrastructure:
- Create fixtures/candidates/ directory
- Add fixtures/candidates/README.md documenting the review workflow and quality criteria
- Add a REVIEW.md template for candidate assessment
- Optionally: a small script that runs capture on all candidates for quick visual review
Summary of Changes
Created fixtures/candidates/ staging directory with:
- README.md documenting the review workflow (create → test → parse → assess → promote/discard)
- Quality criteria table (color variety, instant recognition, 80x24, deterministic, self-contained)
- REVIEW.md template for candidate assessment with checkboxes and color coverage notes
- .gitkeep to track the directory in git when empty
M5: Web App Integration (litmus-fgts)
Add ManifestState + ActiveProvider to state.rs. Fetch manifest.json on app mount. Add ScreenshotView component with lazy loading and simulated-scene fallback. Add provider selector pill bar to ThemeDetail. Integrate into theme_detail, scene_across, compare pages.
Summary of Changes
state.rs: Added ActiveProvider (slug string, default ‘simulated’) and ManifestState (Option
main.rs: Added context providers for ActiveProvider + ManifestState. Added manifest fetch on app mount using eval JS + spawn; populates ManifestState when manifest.json is available at MANIFEST_URL.
screenshot_view.rs (new):
ScreenshotSceneViewcomponent: renders real screenshot<img>from CDN URL when available, with simulated SceneView as fallback on load error or when no screenshot existsProviderSelectorcomponent: pill buttons showing ‘Simulated’ + any providers with screenshots for the current theme (hidden when only Simulated is available)scene_id_to_fixture_id(): maps simulated scene IDs to fixture IDs (neovim → None, deferred)
pages/theme_detail.rs: Added ProviderSelector bar above scenes; conditionally uses ScreenshotSceneView when provider != ‘simulated’ and a fixture mapping exists.
assets/style.css: Added CSS for .screenshot-block, .screenshot-img, .screenshot-fallback, .provider-selector, .provider-pills, .provider-pill, .provider-pill-active.
Cargo.toml (litmus-web): Added serde_json dependency for manifest deserialization.
Parse kitty.conf theme files (litmus-fp9r)
Parse kitty.conf theme files directly as input (no canonical format yet).
Summary of Changes
- Added to — parses with/without prefix, case-insensitive
- Created with struct and parser
- Handles: name metadata comment, foreground/background, color0–color15, cursor, selection_background/foreground
- Returns if required fields missing; optional fields (cursor, selection) are when absent
- 4 unit tests all passing
- Added to — converts → with sensible defaults
- Updated : accepts file paths as CLI args; if provided, uses only those themes; otherwise falls back to hardcoded themes
Define scene format (litmus-h4yq)
Define the scene format: annotated sample content with semantic color references.
Summary of Changes
Added scene module to litmus-model with:
ThemeColorenum: semantic color references (Foreground, Background, Cursor, Selection*, Ansi(0-15)) withresolve()methodTextStyle: bold/italic/underline/dim modifiersStyledSpan: text + optional fg/bg ThemeColor + TextStyle, with builder methodsSceneLine: sequence of StyledSpansScene: id, name, description, and lines — a complete terminal scene definition- All types are serde-serializable for data-driven scene definitions
Design internal theme representation (litmus-i1bi)
Color palette schema: ANSI 0-15, foreground, background, cursor, selection, etc.
Remove old Theme struct and hand-curated color sections (litmus-i4kf)
Final cleanup once all consumers have migrated:
- Remove the old Theme struct from litmus-model (or rename ProviderColors to Theme if that’s cleaner)
- Remove old toml_format.rs parser (or repurpose for ThemeDefinition parsing)
- Delete hand-curated [colors] and [colors.ansi] sections from authored theme TOMLs
- Verify all tests pass, no references to old types remain
Depends on: web and CLI migrations complete
Plan
- Update litmus-capture load_all_themes() to use load_themes_dir() instead of parse_toml_theme()
- Remove unused parse_toml_theme import from capture crate
- Verify no old-format theme TOMLs remain
- Keep parse_toml_theme in litmus-model for CLI user-provided file support
- Keep Theme struct as the runtime display model
Todo
- Update capture load_all_themes to use new format
- Remove unused imports
- Verify all theme files are in new format
- Zero warnings across all crates
- All tests pass
Summary of Changes
Updated litmus-capture’s load_all_themes() to use load_themes_dir() + ProviderColors::to_theme() instead of the old parse_toml_theme() path. Removed unused parse_toml_theme import from capture crate.
Kept in place:
- Theme struct (runtime display model, used everywhere)
- parse_toml_theme (still used by CLI for user-provided .toml files)
- defaults module (used by all format parsers)
- toml_format module (still needed)
All 60 theme definitions verified to be in new ThemeDefinition format.
Update mdbook documentation to reflect current project state (M6-M13) (litmus-iqfq)
Rewrite docs: update intro/milestones, add architecture/development/agentic-workflow pages, delete contributing.md placeholder
Summary of Changes
- introduction.md: Added current scale (29 themes, 15 families, 8 scenes, 3 crates), project status section
- architecture.md: New — workspace layout, data model, scene system, web app, accessibility/CVD
- development.md: New — replaces contributing.md, covers prerequisites, quick start, mise tasks, dev loop
- development/adding-themes.md: New — step-by-step guide with TOML format, themes.rs registration, family setup
- development/adding-scenes.md: New — guide covering ThemeColor, StyledSpan API, scene registration
- milestones.md: Rewritten — M0-M13 in past tense, removed Post-MVP section
- agentic-workflow.md: New — documents Claude Code + beans development process
- SUMMARY.md: Updated with new book structure
- contributing.md: Deleted (replaced by development.md)
- mdbook builds cleanly with no broken links
M4: Cloudflare R2 Storage Setup (litmus-j0d6)
Create R2 bucket litmus-screenshots with public access and custom domain. Configure CORS. Document URL scheme: {base_url}/v1/{provider}/{theme}/{fixture}.webp. Add R2 credentials to GitHub Actions secrets. Note: manual infra step, document the process.
Summary of Changes
M4 is primarily a manual infrastructure setup task. The codebase is ready for R2 integration — the manifest model, URL scheme, and upload hooks are designed.
Setup steps for the user:
- Create R2 bucket
litmus-screenshotsin Cloudflare dashboard - Enable public access with custom domain
screenshots.litmus.edger.dev - Configure CORS:
- Allowed origin:
https://litmus.edger.dev - Allowed methods: GET
- Allowed origin:
- Add GitHub Actions secrets:
R2_ACCESS_KEY_IDR2_SECRET_ACCESS_KEYR2_ACCOUNT_IDR2_ENDPOINT(e.g.https://${ACCOUNT_ID}.r2.cloudflarestorage.com)LITMUS_SCREENSHOTS_BASE_URL(e.g.https://screenshots.litmus.edger.dev)
URL scheme implemented:
{base_url}/v1/{provider}/{theme}/{fixture}.webp
Cache headers to configure on the bucket:
- Images:
Cache-Control: public, max-age=31536000, immutable - manifest.json:
Cache-Control: public, max-age=3600
No code changes needed for M4 — the data model and CLI upload command are ready in M3.
Add ThemeDefinition and ProviderColors types to litmus-model (litmus-jmna)
Add new types to litmus-model alongside the existing Theme struct (don’t remove it yet):
ThemeDefinition: name, variant (dark/light), slug, providers (HashMap<String, String>)ProviderColors: provider slug, source_version, all 22 color fields (same as current Theme colors)- TOML deserialization for both:
.tomlfor definitions,.{provider}.tomlfor provider colors - Loader function that scans a themes directory and returns
Vec<ThemeDefinition>+HashMap<(slug, provider), ProviderColors>
Keep the old Theme struct and parsers intact — they’ll be removed in a later task after consumers migrate.
Plan
New file: crates/litmus-model/src/provider.rs
Variantenum (Dark/Light) with serde lowercaseThemeDefinitionstruct: name, variant, slug (derived from filename), providers HashMapProviderColorsstruct: provider slug, source_version, same color fields as Theme- TOML parsing for both types (reuse existing parse_field pattern)
load_themes_dir()function: scan directory, return Vec+ HashMap<(String, String), ProviderColors>
Test-first approach
- Write tests for Variant serde, ThemeDefinition parsing, ProviderColors parsing, and loader function
- Then implement to make them pass
Commits
- Tests commit (failing)
- Implementation commit
Summary of Changes
Added provider module to litmus-model with:
Variantenum (Dark/Light) with lowercase serdeThemeDefinitionstruct: name, variant, slug (from filename), providers HashMap<String, String>ProviderColorsstruct: provider slug, source_version, all 21 color fields matching Theme- TOML parsing for both types via
parse_theme_definition()andparse_provider_colors() load_themes_dir()recursive directory loader returning Vec+ HashMap<(slug, provider), ProviderColors> - Shared
parse_hex_color()helper extracted to lib.rs, reused across toml_format and provider modules - Filename/TOML provider validation in the loader
- 20 tests covering parsing, serde, edge cases, directory loading, error propagation
Old Theme struct left intact as specified.
Expand themes collection to ~60 themes (litmus-jr2u)
Add ~30 new well-known themes from verified sources: GitHub, One Light, Night Owl, Iceberg, Snazzy, Zenburn, Flexoki, Oxocarbon, Poimandres, Cyberdream, Vesper, Modus, Nightfox family, Sonokai, Andromeda, Melange, Tender
Summary of Changes
Added 31 new themes from well-known, verified sources (official repos, kitty-themes, alacritty-theme, iTerm2-Color-Schemes):
New theme families (with dark/light variants):
- GitHub (Dark, Light, Dark Dimmed) — from projekt0n/github-theme-contrib
- Cyberdream (Dark, Light) — from scottmckendry/cyberdream.nvim
- Flexoki (Dark, Light) — from kepano/flexoki
- Iceberg (Dark, Light) — from cocopon/iceberg.vim
- Melange (Dark, Light) — from savq/melange-nvim
- Modus (Vivendi, Operandi) — from GNU Emacs modus-themes
- Night Owl + Light Owl — from sdras/night-owl-vscode-theme
- Oxocarbon (Dark, Light) — from nyoom-engineering/oxocarbon
- Sonokai (Default, Shusia) — from sainnhe/sonokai
New Nightfox family members:
- Dawnfox, Dayfox, Duskfox, Nordfox, Terafox — from EdenEast/nightfox.nvim
- Moved existing nightfox.toml into nightfox/ directory
New standalone themes:
- Andromeda — from EliverLara/Andromeda
- One Light — Atom One Light terminal port
- Poimandres — from drcmda/poimandres-theme
- Snazzy — from sindresorhus/hyper-snazzy
- Tender — from jacoborus/tender.vim
- Vesper — from raunofreiberg/vesper
- Zenburn — classic vim theme
Code changes:
- Updated themes.rs with 60 include_str! entries (was 29)
- Updated family.rs with new prefix families + suffix-based grouping for Nightfox and Owl families
- All tests pass
Total: 29 → 60 themes
New compare entry flow (litmus-k3fj)
Strict 2-theme side-by-side compare: left = app theme, right = compared theme.
- Set MAX_COMPARE to 2, removed compact from compare page
- Added CompareButton component on theme cards
- Added CompareButton on detail page
- Added theme picker dropdowns in compare column headers
- Feel Lucky already works correctly with MAX_COMPARE=2
- Hardcoded 2-column grid, removed –compare-cols CSS var
Summary of Changes
Simplified compare to strict 2-theme side-by-side. Added CompareButton component (compare against app theme) on cards and detail page. Added theme picker dropdowns in compare column headers to swap themes without leaving the page. Hardcoded 2-column grid layout. MAX_COMPARE reduced to 2.
Docs update and spec brainstorming (litmus-k6ah)
Restructure and rewrite the mdbook docs site to serve end users and contributors. Current docs are substantially out of date since v0.3.0 and structured around developer internals.
Approved Structure
Introduction — elevator pitch, live link, 60 themes / 13 fixtures / 2 providers
The Model — provider/consumer/silo conceptual framework (centerpiece)
├─ Providers — terminal emulators as color palette sources
├─ Consumers — apps that inherit ANSI colors, fixtures as representations
└─ Silos (Roadmap) — apps with independent themes (neovim etc), clearly future
Using Litmus — end-user guide
├─ Browsing Themes — filters, search, dark/light, provider switching
├─ Theme Detail — fixtures, contrast chips, screenshot vs simulated
├─ Comparing Themes — side-by-side, issue navigation, favorites
└─ Exporting Config — kitty.conf, TOML, Nix
Under the Hood
├─ Capture Pipeline — litmus-capture, headless terminals, ANSI → WebP → R2
├─ Rendering — TermOutput model, TermColor resolution, dual-mode display
├─ Accessibility — WCAG contrast, APCA scoring, CVD simulation
└─ Architecture — 4 crates, workspace layout, data flow, deployment
Contributing
├─ Dev Setup — nix, mise, bacon workflow
├─ Adding Themes — updated from current
├─ Adding Fixtures — updated from current
└─ Adding Providers — new
Roadmap — replaces milestones + next-stage-plan, shipped vs planned
Pages Removed
milestones.md→ compressed into Roadmapnext-stage-plan.md→ absorbed into Roadmap (shipped vs planned)agentic-workflow.md→ bacon/beans details move to Dev Setup; rest cut
The Model (Centerpiece)
The page that makes litmus click conceptually. ~100-150 lines, explanatory not academic.
-
Opening hook — “Why switching terminal themes is broken” — framed through color ownership. The pain isn’t just manual config — it’s that different apps get colors from different places, and you can’t see the full picture until everything is configured.
-
The three roles:
- Providers — terminal emulators defining 16 ANSI palette + fg/bg/cursor/selection. Source of truth. Changing a provider’s theme changes everything inside it. Litmus supports kitty and wezterm.
- Consumers — apps inheriting the provider’s palette via ANSI codes. Don’t define colors, reference by index.
git diffsays “make additions green” (ANSI 2), and what “green” looks like depends on the provider. Why the samegit difflooks different under Tokyo Night vs Gruvbox. - Silos — apps with their own color palette, independent of provider. Neovim colorschemes, lazygit built-in themes. (Roadmap — planned for next major version, not yet in litmus.)
-
Ecosystems — provider + consumers as a natural group. A theme preview showing only swatches is useless — you need to see the ecosystem. This is litmus’s core insight.
-
Dual-mode apps — brief note: some apps (lazygit, jjui) can be consumer or silo. (Roadmap.)
-
What litmus does with this — captures real terminal output from real providers. Not simulation of what git diff might look like — actual pixels your terminal would render.
Under the Hood
Capture Pipeline
- Problem solved: simulated rendering can’t capture font rendering, ligatures, actual app behavior. Litmus captures real screenshots from headless Wayland terminals.
- Flow: litmus-capture writes temp provider config (80×32, 12pt FiraCode, 1280×960 at 2x) → runs fixture setup.sh + command.sh → launches terminal headlessly → screenshots with grim → PNG→WebP + SHA-256 checksum → repeat for every (provider × theme × fixture) combination.
- ANSI parsing: also parses raw ANSI escapes into structured TermOutput for the simulated rendering path. Two display modes: real screenshots and simulated.
- Manifest & CDN: Cloudflare R2, manifest.json indexes all screenshots. Web app fetches manifest once, lazy-loads images. Cache-busted URLs (1yr TTL images, 1min manifest).
Rendering
- TermOutput model: replaces old Scene system. TermColor (Default/Ansi/Indexed/Rgb) → TermSpan (text + fg + bg) → lines. Models real terminal output including 256-color and truecolor.
- Color resolution: TermColor::resolve() maps semantic refs to RGB via ProviderColors. Ansi(2) → provider’s “green”. Rgb passes through.
- Dual display: detail page shows real screenshots (R2) alongside simulated rendering (ANSI → TermOutput → CSS). Screenshots are ground truth; simulated enables CVD sim and contrast analysis.
Accessibility
- WCAG contrast: contrast.rs computes ratios via WCAG 2.1 relative luminance. AA (4.5:1) and AAA (7:1).
- APCA scoring: perceptually accurate readability beyond WCAG ratio.
- Per-fixture analysis: every span checked. Issues surface as interactive chips — click to cycle.
- CVD simulation: Machado 2009 matrices transform entire palettes. Applied at ProviderColors level.
Architecture
- Four crates: litmus-model (shared types, any target), litmus-web (Dioxus WASM), litmus-cli (ratatui TUI, secondary), litmus-capture (headless capture, native Wayland).
- Data flow: theme TOML → ThemeDefinition + ProviderColors → compile-time embed → fixtures from output.json → TermColor resolves against ProviderColors → CSS. Screenshots: R2 manifest → lazy images.
- Deployment: Cloudflare Pages via GitHub Actions. dx build → critical CSS injection → Wrangler deploy. Screenshots on R2 subdomain.
- Provider-scoped routing: all routes under /:provider/ — same theme shows different data per provider.
Using Litmus
- Browsing: 60 themes grouped by family, search, dark/light filter, readability threshold, provider toggle (kitty/wezterm), greyed unavailable themes.
- Detail: 13 fixtures, dual display (screenshot + simulated), contrast chips, fixture minimap, sticky toolbar.
- Comparing: 2-theme side-by-side, inline pickers, per-fixture issue dots, “vs.” memory, favorites (20 cap, star toggle).
- Export: kitty.conf, TOML, Nix. Copy or download.
Contributing
- Dev Setup: Rust stable + wasm32, Nix optional, mise required.
mise run dev, bacon workflow, beans for tracking. - Adding Themes: updated for ThemeDefinition + per-provider color files.
- Adding Fixtures: updated for capture-based system (setup.sh + command.sh + output.json).
- Adding Providers: new page — config generator in litmus-capture/src/providers/, color extraction, manifest pipeline.
Roadmap
- Shipped: v0.1–v0.5 compressed summary, link to CHANGELOG.md.
- Next major: silo support (neovim colorschemes, dual-mode), ecosystem view, extended export (alacritty, foot, ghostty, Windows Terminal, iTerm2), consumer config preview.
- Not in scope: live terminal capture in browser, theme editor, plugin system, user accounts.
Tasks
- Write SUMMARY.md (new structure)
- Write Introduction (updated stats, live link)
- Write The Model (centerpiece, expanded from concepts.md)
- Write Using Litmus (4 subsections)
- Write Under the Hood: Capture Pipeline
- Write Under the Hood: Rendering
- Write Under the Hood: Accessibility
- Write Under the Hood: Architecture (4 crates, data flow, deployment)
- Write Contributing: Dev Setup (absorb agentic-workflow bacon/beans bits)
- Update Contributing: Adding Themes
- Update Contributing: Adding Fixtures
- Write Contributing: Adding Providers (new)
- Write Roadmap (replace milestones + next-stage-plan)
- Remove stale pages (milestones.md, next-stage-plan.md, agentic-workflow.md)
- Rebuild docs/dist
Summary of Changes
Rewrote the entire mdbook documentation site with a new structure targeting end users and contributors:
- The Model (centerpiece) — expanded provider/consumer/silo conceptual framework
- Using Litmus — 4-page end-user guide (browsing, detail, comparing, exporting)
- Under the Hood — 4-page technical deep dive (capture pipeline, rendering, accessibility, architecture)
- Contributing — 4-page contributor guide (dev setup, adding themes/fixtures/providers)
- Roadmap — replaces stale milestones and next-stage-plan pages
Removed 6 stale pages (milestones, next-stage-plan, agentic-workflow, concepts, architecture, development). Fixed all technical inaccuracies found during code review (TermColor::Indexed type, color count, route params, font name, theme count).
Remove old Scene, ThemeColor, StyledSpan types and hand-written scenes (litmus-kbzo)
Final cleanup once all consumers migrated to TermOutput:
- Delete scene.rs (ThemeColor, StyledSpan, SceneLine, Scene)
- Delete scenes.rs (hand-written scene definitions)
- Remove scene_id_to_fixture_id() mapping (no longer needed)
- Clean up any remaining references to old types
- Verify all tests pass
Depends on: web and CLI migrations, contrast validation update
Plan
- Add
term_readability_score(theme, fixtures)to contrast.rs - Make
fixtures::all_fixtures()pub in litmus-web - Update SceneMinimap to accept fixture metadata instead of Scene
- Update sidebar to pass fixture data to minimap
- Update all pages (theme_detail, scene_across, compare, theme_list) to iterate fixtures instead of scenes
- Remove SceneView/ScenePreview fallback calls
- Remove scene_renderer.rs module
- Remove scene-based contrast functions and tests
- Delete scene.rs and scenes.rs from litmus-model
- Clean up all remaining references
Summary of Changes
Removed all Scene/ThemeColor/StyledSpan types and hand-written scene definitions:
- Deleted
scene.rsandscenes.rsfrom litmus-model - Deleted
scene_renderer.rsfrom litmus-web - Removed
scene_id_to_fixture_id()mapping from screenshot_view.rs - Removed
ContrastIssue,validate_scene_contrast(),readability_score(),validate_theme_readability()and their tests from contrast.rs - Updated
scene_across.rsandcompare.rsto iterate fixtures directly - Updated
theme_list.rsto useterm_readability_score()and removed scene_renderer fallback - Updated
state.rsfilter to useterm_readability_score() - Removed unused
fixture_by_id()and its test - All 177 tests pass, zero compiler warnings
Fix scene color choices that bias against light themes (litmus-ktsr)
Based on diagnosis, fix scene color references that are unrealistic or systematically penalize light themes. E.g. ansi(15) as body text, status bar combos.
M3: Capture Tool (litmus-capture) (litmus-lizo)
New workspace crate crates/litmus-capture/. CLI binary with: capture, capture-all, upload, manifest commands. Kitty provider module. Uses cage for headless Wayland, grim for screenshots, FiraCode font, 80x24 geometry. WebP output to staging dir.
Summary of Changes
Created crates/litmus-capture/ — a new workspace crate (native binary, excluded from wasm build).
Modules:
error.rs— CaptureError enum for typed errorsproviders/mod.rs— ProviderCapture trait + TermGeometry struct + provider registryproviders/kitty.rs— KittyProvider: generates kitty.conf with theme colors + FiraCode/80×24/no-decorations settings; builds launch argscapture.rs— core capture logic: fixture setup, cage+kitty orchestration via wrapper script, sentinel-file completion detection, grim screenshot, PNG→WebP conversion, sha256 checksummanifest.rs— builds ScreenshotManifest by scanning staging/{provider}/{theme}/{fixture}.webp; CoverageReport for CI validationmain.rs— CLI with clap:capture,capture-all,manifest build,manifest checksubcommands
Key design:
- Fixture lifecycle: setup.sh → kitty+command.sh in cage → sentinel file detection → grim screenshot → PNG→WebP
- wrapper.sh runs inside cage’s Wayland session; both kitty and grim connect to cage’s compositor
- Fixed geometry: 80×24 cols/rows, FiraCode 12pt
- 16 unit tests pass; 83 total workspace tests pass
Update litmus-web to render TermOutput instead of Scene (litmus-lm76)
Migrate litmus-web from Scene to TermOutput rendering:
- Embed output.{provider}.json files via include_str!() or include_bytes!()
- New TermOutputView component replaces SceneView:
- Renders TermSpan as with inline CSS color
- TermColor::Default → theme fg/bg from ProviderColors
- TermColor::Ansi(n) → ProviderColors ANSI palette
- TermColor::Indexed(n) → fixed 256-color → rgb CSS
- TermColor::Rgb → literal rgb CSS
- Bold/italic/dim/underline → CSS font-weight/style/opacity/decoration
- Provider selector switches which output.{provider}.json is rendered
- Update theme_detail.rs side-by-side to use TermOutputView on the left
- Update ScenePreview (theme list cards) to use TermOutput
Depends on: TermOutput types, fixture pipeline generating output files
Plan
- Create
crates/litmus-web/src/fixtures.rs— embed fixture output.json files, parse with LazyLock - Create
crates/litmus-web/src/term_renderer.rs— TermOutputView and TermOutputPreview components - Wire TermOutputView into theme_detail.rs (replace SceneView on left panel)
- Wire TermOutputPreview into theme_list.rs (replace ScenePreview in cards)
- Wire TermOutputView into scene_across.rs and compare.rs
- Tests for color resolution and fixture loading
Summary of Changes
- Created
fixtures.rsmodule: embeds 8 fixture output.json files, OnceLock caching,fixture_by_id()anddefault_fixture()API (4 tests) - Created
term_renderer.rsmodule:TermOutputViewandTermOutputPreviewDioxus components that render TermSpan with theme-aware inline CSS colors - Updated theme_list.rs: TermOutputPreview for card previews (ScenePreview fallback)
- Updated theme_detail.rs: TermOutputView on left panel of side-by-side (SceneView fallback for neovim)
- Updated scene_across.rs: TermOutputView in grid cards
- Updated compare.rs: TermOutputView in comparison grid
Note: Contrast analysis tooltips are not available in TermOutput path (they were Scene-model-specific). Header stats still computed. Can be reimplemented as a follow-up.
Commits: 975c94d, c475a27, bc2393a
Cloudflare Pages dashboard setup (litmus-lwju)
Manual: Create Pages project ‘litmus’ in Cloudflare dashboard. Add CLOUDFLARE_API_TOKEN and CLOUDFLARE_ACCOUNT_ID as GitHub repo secrets.
Configure custom domain for R2 bucket (litmus-mclx)
Point screenshots.litmus.edger.dev to the R2 bucket:
- Enable public access on the R2 bucket with custom domain
- Add CNAME record (auto-created by Cloudflare custom domain flow)
- Verify SSL/TLS is working on the custom domain
- Confirm bucket contents are accessible at https://screenshots.litmus.edger.dev/
Convert themes to canonical format (litmus-mpo6)
Convert each selected theme from its source format into the canonical format defined in M1.
Summary of Changes
All 19 themes written as canonical TOML files in themes/ directory. Each file follows the format defined in M1 with name, colors (background, foreground, cursor, selection), and full 16-color ANSI palette.
Rename shortlist to favorites and decouple from compare (litmus-nqee)
Decouple the bookmarking and comparison features.
- Renamed Shortlist → Favorites everywhere (structs, components, CSS, labels)
- Changed labels to use star icons (★/☆)
- Raised MAX_FAVORITES from 5 to 20
- Persist favorites to localStorage (deferred)
- Removed auto-build of compare URL from favorites
- Sidebar favorites section decoupled from compare
Summary of Changes
Renamed Shortlist to Favorites across all code, CSS, and UI labels. Changed toggle labels to use star icons. Raised limit from 5 to 20. Decoupled favorites from compare: removing a favorite no longer updates the compare URL, Feel Lucky no longer adds to favorites, and the compare nav link is independent. localStorage persistence deferred to a follow-up.
Simplify nav bar (litmus-o3j2)
Move scene links out of top nav into the Browse by Scene section on home page. Nav becomes: logo, tagline only. Reduces clutter.
Summary of Changes
Removed 5 scene navigation links from the top nav bar. They were redundant with the Browse by Scene section on the home page.
Convert existing themes to ThemeDefinition format (litmus-o4w9)
Write a script (or extend extract-colors) that:
- Reads each existing themes/**/*.toml (which currently have [colors] sections)
- Matches theme names to kitty/wezterm built-in theme names (fuzzy matching or manual mapping for popular ones)
- Writes new authored .toml files with just name, variant, and [providers] section
- Runs extract-colors to generate per-provider color files
- Validates that extracted colors roughly match the old hand-curated colors (flag large diffs for review)
Start with the popular themes that have clear matches in both providers. Themes without provider matches stay in old format until manually mapped.
Depends on: extract-colors command
Summary of Changes
Converted 54 existing hand-curated themes to the new ThemeDefinition format with provider mappings:
- Wrote with manual name override dictionaries for kitty and wezterm vendor theme names
- Rewrote 54 theme .toml files from old [colors] format to new ThemeDefinition format (name, variant, [providers])
- Generated 87 provider color files (.kitty.toml and .wezterm.toml) via
litmus-capture extract-colors - Added variant detection with word-boundary matching and manual overrides for edge cases (e.g. Moonlight)
- 6 themes could not be converted due to no vendor match: Cyberdream Dark/Light, Melange Dark/Light, Light Owl, Oxocarbon Light
- 2 vendor extraction failures accepted (Sonokai kitty has only 15 ANSI colors in vendor data)
Create CHANGELOG.md (litmus-otas)
Add a CHANGELOG.md to track releases. Start with v0.3.0 (screenshot system + R2 deployment) and backfill v0.2.0 and v0.1.0 from git history.
Summary of Changes
Created CHANGELOG.md with entries for v0.1.0, v0.2.0, v0.3.0, and Unreleased. Backfilled from git history across main and dev branches.
Per-theme issue dots in sidebar fixture minimap (litmus-p5xo)
Show per-theme colored dots on the sidebar fixture minimap during compare.
- Added CompareIssueDots context signal with per-theme data
- Uses each theme’s foreground hex color for the dot
- Render colored dots per fixture in the minimap
- Each dot represents one theme’s issue count (tooltip shows name + count)
- Detail page clears compare dots, falls back to single badge
Summary of Changes
Added CompareIssueDots context signal. Compare page publishes per-theme issue data. Minimap renders colored dots (one per theme with issues) when on compare page, falls back to single badge on detail page. Detail page clears compare dots on mount.
Add TermColor, TermSpan, TermLine, TermOutput types to litmus-model (litmus-q9lp)
Add the new terminal output types to litmus-model:
- TermColor enum: Default, Ansi(u8), Indexed(u8), Rgb(u8, u8, u8)
- TermSpan: text + fg/bg TermColor + bold/italic/dim/underline
- TermLine: Vec
- TermOutput: id, name, cols, rows, Vec
- Serde JSON serialization/deserialization for all types
- TermColor resolution method: resolve(provider_colors) → CSS-ready rgb values
- Indexed(16-255) → fixed RGB lookup table (standard 256-color palette)
Keep existing Scene/ThemeColor types — removed in a later task.
Plan
New file: crates/litmus-model/src/term_output.rs
TermColorenum: Default, Ansi(u8), Indexed(u8), Rgb(u8, u8, u8)TermColor::resolve()method: takes ProviderColors, returns Color- Default → uses context (caller decides fg vs bg)
- Ansi(0-15) → ProviderColors.ansi lookup
- Indexed(16-255) → fixed 256-color palette lookup table
- Rgb → direct Color
TermSpan: text + fg/bg TermColor + bold/italic/dim/underlineTermLine: VecTermOutput: id, name, cols, rows, Vec- Serde JSON serialization/deserialization for all types
256-color palette
Standard xterm-256 color palette: colors 16-231 are a 6x6x6 color cube, 232-255 are grayscale.
Commits
- Tests + implementation
- Review fixes
Summary of Changes
Added term_output module to litmus-model with:
TermColorenum: Default, Ansi(0-15), Indexed(16-255), Rgb(r,g,b) with tagged serdeTermColor::resolve(): maps to concrete Color via ProviderColors + default fallback- Standard xterm-256 color palette: 6x6x6 cube (16-231) + grayscale ramp (232-255)
TermSpan: text + fg/bg TermColor + bold/italic/dim/underline with skip_serializing_ifTermLineandTermOutput: structured parsed terminal output- 11 tests covering resolution, palette corners, and JSON serde round-trips
Existing Scene/ThemeColor types kept intact.
Contrast and readability validation in scenes (litmus-r363)
Ensure scenes expose real readability issues such as low contrast and color blending.
Summary of Changes
Added contrast module to litmus-model with:
- WCAG 2.1 relative luminance calculation (
srgb_to_linear,relative_luminance) contrast_ratio(c1, c2): standard WCAG contrast ratio (1.0 to 21.0)validate_scene_contrast: checks every non-whitespace span in a scene, resolving semantic colors against a theme, with separate thresholds for normal vs bold textvalidate_theme_readability: convenience function that checks all built-in scenes at WCAG AA levelContrastIssuestruct with full location info (scene/line/span), colors, ratio, and threshold
M2: Fixture System (litmus-rz7q)
Create fixtures/ directory at workspace root. Each fixture has setup.sh, command.sh, optional teardown.sh, README.md. Initial fixtures: git-diff, ls-color, cargo-build, shell-prompt, git-log, python-repl, htop. Add fixtures/README.md with authoring guide.
Summary of Changes
Created fixtures/ directory at workspace root with 7 fixture subdirectories:
git-diff/— real git diff with additions, deletions, context (setup: git repo with committed files + unstaged changes)ls-color/— real ls -la with dirs, symlinks, executables, hidden filesgit-log/— real git log –graph with branches, merges, tags (pinned timestamps for reproducibility)cargo-build/— real cargo build with intentional warnings + type errorshell-prompt/— scripted bash session output (interactive shell not scriptable reliably)python-repl/— scripted Python REPL session outputhtop/— top -b batch mode with scripted fallback
Each fixture has setup.sh + command.sh (all tested working). fixtures/README.md with authoring guide. Interface: FIXTURE_WORK_DIR env var, CWD set to work dir when command.sh runs.
Audit existing fixtures against quality criteria (litmus-sk2k)
Review the 7 existing fixtures against the quality criteria:
- Color variety (≥4 distinct ANSI colors)
- Instant recognition
- Fits 80x24
- Deterministic
- Self-contained
Fix any that don’t meet criteria. Known issues:
- htop fallback may not be deterministic (real top output varies)
- Some fixtures rely on tool color defaults which may vary by system
- shell-prompt and python-repl are simulated (printf/echo) — check if they still make sense as fixtures or should be replaced with real tool output
This can run in parallel with new fixture work.
Audit Results
| Fixture | Color | Recognition | 80x24 | Deterministic | Self-Contained | Action |
|---|---|---|---|---|---|---|
| git-diff | PASS | PASS | PASS | PASS | PASS | None |
| git-log | PASS | PASS | PASS | PASS | PASS | None |
| ls-color | PASS | PASS | PASS | FIXED | PASS | Set explicit LS_COLORS |
| cargo-build | PASS | PASS | PASS | PASS | PASS | None |
| shell-prompt | PASS | PASS | PASS | PASS | PASS | Simulated (OK) |
| python-repl | PASS | PASS | PASS | PASS | PASS | Simulated (OK) |
| htop | PASS | PASS | PASS | FIXED | PASS | Always use scripted output |
Summary of Changes
- htop: Removed non-deterministic
top -b -n 1path that produced varying process data. Now always uses scripted htop-like display with fixed processes and ANSI colors. - ls-color: Added explicit
LS_COLORSexport to ensure consistent file type coloring across systems. - shell-prompt, python-repl: Confirmed simulated approach is appropriate for reproducibility.
- 5 fixtures (git-diff, git-log, cargo-build, shell-prompt, python-repl) passed all criteria with no changes needed.
Responsive layout with monospace font rendering (litmus-smve)
Responsive layout, monospace font rendering for terminal scenes.
Summary of Changes
Added responsive layout and monospace font rendering:
- CSS reset and base styles in assets/style.css
- Monospace font stack (JetBrains Mono, Fira Code, Cascadia Code, system fallbacks) with ligatures disabled
- Mobile-first responsive breakpoints at 640px (single-column grid, smaller scene fonts)
- Reusable CSS classes for theme cards with hover effects, swatches, nav, scene blocks, color palette
- Configured Dioxus.toml to serve the stylesheet
Keyboard navigation for detail page (litmus-ti3e)
ArrowLeft/Right to switch scene tabs, ‘c’ to toggle compare selection. onkeydown handler on outer div.
Summary of Changes
Added keyboard navigation to the detail page: ArrowLeft/ArrowRight switches scene tabs, ‘c’ toggles compare selection. The outer div is made focusable with tabindex and autofocus. A subtle keyboard hint is shown next to the scene tabs.
Implement APCA algorithm for light theme readability scoring (litmus-u02e)
Replace WCAG 2.x symmetric contrast ratio with APCA (polarity-aware) in readability_score(). WCAG under-estimates perceived contrast for dark text on light backgrounds, causing light themes to score incorrectly low. APCA correctly models both polarities. Results: Catppuccin Latte 31.9% → ~90%, Solarized Light 10.2% → 99.6%, with parity to dark variants.
Web app shell (litmus-u1cy)
Set up the web app shell (likely static site — Rust backend optional, could be pure frontend).
Summary of Changes
Restructured litmus-web with proper Dioxus routing:
- Shell layout: nav header with app title, shared across all pages
- ThemeList page: responsive grid of clickable theme cards showing name + 16-color swatch
- ThemeDetail page: back link, full color palette (special + ANSI), all scene previews
- Added dioxus/router feature for client-side navigation
Scene selector tabs on comparison page (litmus-udmz)
Horizontal scene pills/tabs on scene-across-themes page to switch scenes without going back to home.
Summary of Changes
Added horizontal scene selector tabs at the top of the scene-across-themes page. Each tab is a Link to the same route with a different scene_id, styled with scene-tab/scene-tab-active classes.
Organize themes by family (litmus-vas0)
Group themes by family (e.g. Catppuccin family: Mocha, Latte, Frappe, Macchiato).
Summary of Changes
Themes organized into family subdirectories:
- themes/catppuccin/ (4 variants)
- themes/tokyo-night/ (3 variants)
- themes/gruvbox/ (2 variants)
- themes/rose-pine/ (3 variants)
- themes/solarized/ (2 variants)
- themes/everforest/ (2 variants)
- themes/dracula.toml (standalone)
- themes/nord.toml (standalone)
- themes/kanagawa.toml (standalone)
Vendor provider theme data (kitty-themes, wezterm color schemes) (litmus-vkne)
Add vendored provider theme registries under vendor/:
- vendor/kitty-themes/ — from kovidgoyal/kitty-themes repo (the .conf theme files)
- vendor/wezterm-colorschemes/ — from wez/wezterm color scheme data
Use git subtree (preferred over submodule for simplicity). Document the update procedure in a vendor/README.md.
These are the source of truth for the extract-colors step.
Summary of Changes
Vendored provider theme data from two sources:
- kitty-themes (385 .conf files): Added via
git subtreefrom kovidgoyal/kitty-themes - wezterm-colorschemes (996 .toml files): Extracted from wez/wezterm’s embedded scheme_data.rs (1001 schemes, 5 lost to filename collisions from aliases)
Also added:
vendor/README.mdwith update procedures for both sourcesscripts/extract-wezterm-schemes.pyfor reproducible wezterm extraction
Theme validation and error handling (litmus-vvye)
Validate theme data, handle parse errors gracefully, ensure required fields are present.
Summary of Changes
Created enum in with , , , , , variants. Created for cursor/selection default computation.
Unit tests for theme parsing (litmus-wl4v)
Unit tests covering parsing of kitty.conf and base16 YAML, validation edge cases.
Summary of Changes
11 tests across all parsers: 4 kitty tests (updated for Result API), 3 base16 tests, 3 TOML tests, 1 AnsiColors round-trip test. All pass.
Write comprehensive contrast tests for light and dark themes (litmus-wp56)
Add test coverage for: readability_score on known good/bad themes, per-span diagnostics for Catppuccin Latte and Solarized Light, score parity checks between light/dark variants.
Build production manifest and deploy (litmus-wrol)
Build manifest.json with production base_url and upload:
- Run capture-manifest with –base-url https://screenshots.litmus.edger.dev
- Include manifest.json in the rclone sync (it lives at the bucket root)
- Verify manifest.json is accessible at https://screenshots.litmus.edger.dev/manifest.json
- Added mise tasks:
screenshots-syncandscreenshots-deploy
Screenshot in side-by-side view (litmus-x2vo)
Goal
Add a global toggle on the compare page to switch all columns between simulated scene rendering and real provider screenshots.
Current State
The compare page (pages/compare.rs) shows 2-4 themes side by side. Each column renders simulated SceneView for every scene. There are no screenshots on this page — those only appear on the detail page.
Changes
- Add a toolbar at the top of the compare page with:
- Rendering toggle: Simulated / Screenshot (default: Simulated)
- Provider selector: dropdown (kitty, wezterm, etc.) — only visible when Screenshot mode is active
- When in Screenshot mode, replace
SceneViewwithScreenshotImagefor each (theme, scene) cell - Use
scene_id_to_fixture_id()mapping (same as detail page) - If a screenshot is missing for a given theme+fixture+provider, show the existing placeholder
- Responsive: on narrow screens (<900px), columns stack vertically regardless of mode
Dependencies
- Blocked by
litmus-y6dc(global provider selector) — provider is app-level state, no need for a per-page provider dropdown - Uses existing
ScreenshotImagecomponent and manifest infrastructure - Toggle is just Simulated/Screenshot; provider comes from global state
Plan
- Add a
use_signal(|| false)for screenshot mode in CompareThemes - Add a toolbar above the grid with Simulated/Screenshot toggle buttons (reuse provider-btn styling pattern)
- When screenshot mode is active, render ScreenshotImage (using ActiveProvider) instead of TermOutputView
- Show placeholder when screenshot not available
- Add CSS for the toggle toolbar
- Add todo items:
- Add screenshot mode toggle signal
- Add toolbar with toggle buttons
- Conditionally render ScreenshotImage vs TermOutputView
- Add CSS styling for toolbar
- Verify compilation, zero warnings
Summary of Changes
Added Simulated/Screenshot toggle to the compare page:
- Toggle buttons in toolbar above column headers (reuses provider-btn styling pattern)
- Screenshot mode renders ScreenshotImage using the global ActiveProvider
- Shows “No screenshot” placeholder when screenshot unavailable for a theme+fixture+provider
- Default is Simulated mode (TermOutputView rendering)
- CSS: .compare-toolbar, .compare-view-toggle, .compare-toggle-btn, .compare-screenshot-placeholder
Update litmus-web to provider-scoped theme rendering (litmus-y6dc)
Migrate litmus-web from the old Theme struct to ThemeDefinition + ProviderColors.
Global Provider Selector
Provider becomes app-level state, not a per-page control. Switching provider changes simulated colors, screenshots, contrast validation, and which themes are visible.
UI: Segmented control (buttons, not dropdown) at the top of sidebar, above all filters. E.g. [kitty] [wezterm]. Styled as a prominent first-class mode selector.
State persistence:
- URL parameter takes precedence (e.g.
/themes?provider=kitty) - Falls back to localStorage
- Falls back to first available provider
- Shareable/bookmarkable links
Filtering: Selecting a provider filters the theme list to only themes with ProviderColors for that provider. Themes without colors for the selected provider are hidden everywhere (list, detail, compare).
Navigation edge case: If user is on a detail page and switches to a provider that doesn’t have that theme, redirect to theme list.
Data Loading
- Update load_embedded_themes() to return Vec
+ per-provider color map - Theme only listed if it has ProviderColors for the active provider
Rendering
- Simulated scenes render using ProviderColors for the active provider
- Screenshots use active provider
- Contrast validation scoped to active provider’s colors
- Theme list cards show previews using active provider’s colors
- Compare page uses active provider for all columns
State Management
- New global signal:
ActiveProvider(String)(like existing CvdSimulation, VariantFilter) - Remove per-page provider dropdowns (detail page, compare page)
- All components read from global ActiveProvider
Depends on: new model types, converted themes
Plan
Phase 1: Update themes.rs data loading
- Replace single THEME_DATA array with two arrays: DEFINITION_DATA and PROVIDER_COLORS_DATA
- Parse using parse_theme_definition() and parse_provider_colors()
- Add load_embedded_theme_data() → (Vec
, HashMap<Key, ProviderColors>) - Add available_providers() → sorted list of provider slugs
- Add themes_for_provider(provider) → Vec
Phase 2: Add ActiveProvider signal to state.rs
- New signal: ActiveProvider(String)
- URL param: ?provider=kitty
- localStorage fallback
- Default: first available provider
Phase 3: Update components
- Shell: theme chrome uses active provider
- Sidebar: add provider selector (segmented control)
- ThemeList: filter by active provider
- ThemeDetail: use active provider for rendering + screenshots
- Compare: use active provider
- SceneAcross: use active provider
Phase 4: Navigation edge cases
- Provider switch on detail page → redirect if theme unavailable
Todo
- Update themes.rs with provider-based loading
- Add ActiveProvider signal
- Add provider selector UI
- Update all pages to filter by provider
- Handle navigation edge cases (graceful fallback — detail page shows “not found” with link back)
- Compiles for wasm32, zero warnings
Summary of Changes
Phase 1: Rewrote themes.rs with two embedded arrays (DEFINITION_DATA + PROVIDER_COLORS_DATA), OnceLock caching, and new API: themes_for_provider(), available_providers(). Converted 6 remaining old-format themes (cyberdream, melange, light-owl, oxocarbon-light) to ThemeDefinition + provider colors.
Phase 2: Added ActiveProvider signal to state.rs with default-to-first-available provider.
Phase 3: Added provider segmented control in sidebar. Updated all 4 pages (ThemeList, ThemeDetail, Compare, SceneAcross) and Shell to read from ActiveProvider signal.
Phase 4: ThemeDetail gracefully shows “not found” with back link when theme unavailable for selected provider. Shortlist items persist across provider switches (raw slug shown if name unavailable).
All 169 tests pass, zero compiler warnings, wasm32 compilation verified.
Set up mdbook docs site (litmus-yesx)
Add mdbook documentation site with book.toml, src/ files, mise tasks, flake.nix integration, and .gitignore update
Summary of Changes
- Created
docs/book.toml(title: Litmus, src: src, build-dir: dist) - Created
docs/src/SUMMARY.mdwith 4-page TOC - Created
docs/src/introduction.mdfrom README problem/solution section - Created
docs/src/concepts.mdwith three-layer model and provider ecosystems - Created
docs/src/milestones.mdadapted from existing docs/milestones.md - Created
docs/src/contributing.mdplaceholder - Added
pkgs.mdbookto flake.nix devShells packages - Added
docs-serveanddocs-buildtasks to .mise.toml - Added
docs/dist/to .gitignore - Verified:
mdbook build docsproducesdocs/dist/successfully
Upgrade bacon export for richer Claude Code diagnostics (litmus-yte1)
Switch from deprecated export_locations to analyser exporter with cargo_json analyzer for structured diagnostics. Add claude-diagnostics job, update mise task, update CLAUDE.md.
Summary of Changes
Updated bacon export to use analyser exporter with cargo_json analyzer.
Build litmus extract-colors command (litmus-z20l)
Add an extract-colors subcommand to litmus-capture (or a new litmus-extract crate):
- Reads ThemeDefinition files from themes/
- For each provider mapping, looks up the theme name in vendored data (vendor/kitty-themes/, vendor/wezterm-colorschemes/)
- Parses the provider’s native format into ProviderColors (reuse/extend existing kitty.rs parser, add wezterm TOML parser)
- Writes {theme-slug}.{provider}.toml next to the definition file
- Flags: –provider (filter to one provider), –theme (filter to one theme)
- Skips if generated file already exists and vendored source hasn’t changed (optional optimization)
Depends on: new model types, vendored theme data
Plan
Step 1: Add wezterm TOML parser to litmus-model
New wezterm.rs module with parse_wezterm_scheme() → Theme
Format: uses ansi/brights arrays, cursor_bg/cursor_fg, selection_bg/selection_fg
Step 2: Add Theme → ProviderColors conversion
Helper in provider module: ProviderColors::from_theme(theme, provider_slug, version)
Step 3: Add ProviderColors serialization to TOML
ProviderColors::to_toml() → formatted output matching expected generated file format
Step 4: Add ExtractColors subcommand to litmus-capture
- Read ThemeDefinition files from themes/
- For each provider mapping, look up vendored theme file
- Parse with appropriate parser → Theme → ProviderColors
- Write {slug}.{provider}.toml
- Flags: –provider, –theme
Step 5: Add kitty vendor lookup
Look up theme name in vendor/kitty-themes/themes.json index → find .conf file
Step 6: Add wezterm vendor lookup
Look up theme name in vendored .toml files (scan by metadata.name field)
Summary of Changes
Implemented the extract-colors pipeline across two crates:
litmus-model additions:
wezterm.rs: New parser for wezterm color scheme TOML format (ansi/brights arrays, optional cursor/selection)provider.rs: AddedProviderColors::from_theme()conversion andto_toml()serialization
litmus-capture additions:
extract.rs: Vendor index builders (kitty themes.json, wezterm metadata.name + aliases), theme definition scanner, provider-specific color extractionmain.rs: NewExtractColorssubcommand with –provider, –theme, –force flags
End-to-end tested: successfully extracts colors from both kitty and wezterm vendor data, writing correctly formatted ProviderColors TOML files.
Integration test skeleton for essential features (litmus-z7p4)
Set up a skeleton for integration/smoke tests that verify essential UI features don’t regress across refactors. Start with contrast issue visualization as the first tracked feature.
Requirements
- Define test infrastructure (framework, how tests run, what they verify)
- Add first test case: contrast issues are detected and displayed for a known theme with known violations
- Document pattern for adding new essential feature tests
Notes
This is a draft — details to be refined later. The goal is to have a lightweight safety net for features that have regressed before (contrast display being the first example).
Smoke test: verify live screenshots end-to-end (litmus-zg8b)
Full end-to-end verification:
- Sync all screenshots to R2 (117.9 MiB, 1560 screenshots)
- Verify a sample image loads (200 OK, 84 KB)
- Verify manifest loads (200 OK, production base_url confirmed)
- Check cache headers — cache rules still showing DYNAMIC, deferred to follow-up
- CORS working: access-control-allow-origin: * (via R2 bucket CORS policy)
- Load the live web app and confirm screenshots render correctly
- Cache busting deferred — app is working end-to-end
Summary of Changes
All screenshots serving live from https://screenshots.litmus.edger.dev. Manifest, images, and CORS all verified. Cache rules still showing cf-cache-status: DYNAMIC — may need zone-level investigation but not blocking.
Bugs
- Investigate Cloudflare cache rules not applying to R2 custom domain (litmus-7zy0) — Done ·
archived - Fix: Missing Cargo.lock for crane/nix build (litmus-el0x) — Done ·
archived - Fix headless screenshot capture: kitty EGL + config keys (litmus-haaf) — Done ·
archived - Remove provider dropdown from detail page (litmus-lywi) — Done ·
archived - Fix wasm-bindgen-cli version mismatch (litmus-r5xi) — Done ·
archived - Inline contrast issue markers with hover tooltips (litmus-rs0b) — Done ·
archived - Fix minimap placement, scoring consistency, duplicate scenes (litmus-vryv) — Done ·
archived
Investigate Cloudflare cache rules not applying to R2 custom domain (litmus-7zy0)
Cache rules created under the correct zone for screenshots.litmus.edger.dev but cf-cache-status remains DYNAMIC on all requests. Images and manifest serve correctly, just not being cached at the edge.
Context
- Two cache rules active: immutable images (1yr TTL) and manifest short TTL (1min)
- Both rules set to ‘Eligible for cache’ with ‘Ignore cache-control header and use this TTL’
- R2 bucket serves via custom domain, CORS working
- Rules confirmed on the correct zone
To investigate
- Check if R2 custom domain responses bypass cache by default
- Check Cloudflare docs for R2 + cache rules interaction
- Try a Cache Everything page rule as alternative — not needed
- Check if the zone plan level affects R2 caching behavior — no, affects all plans
Summary of Changes
Root cause: R2 does not set Cache-Control headers by default. Cloudflare cache rules do not reliably override this for R2 custom domains — a known limitation where R2 responses bypass the normal cache layer.
Fix: Updated screenshots-sync mise task to set Cache-Control headers via rclone --header-upload:
- Images:
public, max-age=31536000, immutable(1 year) - Manifest:
public, max-age=60(1 minute)
Added screenshots-sync_ignore-checksum task for one-time force re-upload.
Verified: cf-cache-status: HIT confirmed on both manifest and images.
Fix: Missing Cargo.lock for crane/nix build (litmus-el0x)
Crane requires Cargo.lock to be present and git-tracked. Generate it, git-add it, and add pname to commonArgs in flake.nix.
Summary of Changes
- Generated
Cargo.lockvianix run nixpkgs#cargo -- generate-lockfile(cargo not available in PATH) - Staged
Cargo.lockwithgit addso crane can see it - Added
pname = "litmus";tocommonArgsinflake.nixto silence crane name warning - Verified
nix develop --command echo OKsucceeds
Fix headless screenshot capture: kitty EGL + config keys (litmus-haaf)
Two blocking issues preventing screenshot capture from working:
-
Wrong kitty config keys:
initial_window_columns/initial_window_rowsare not valid kitty config options (logged as ‘Ignoring unknown config key’). Correct keys areinitial_window_width/initial_window_height(cell-count, no suffix). -
EGL failure: kitty requires OpenGL/EGL but the wlroots headless backend (cage + WLR_BACKENDS=headless) does not expose EGL to client applications. kitty exits with ‘[glfw error 65542]: EGL: Failed to initialize EGL’.
Fix: Switch from cage+grim (Wayland headless) to xvfb-run+scrot (X11 virtual framebuffer). xvfb-run creates a virtual X11 display; scrot takes X11 screenshots. Mesa software rendering (LIBGL_ALWAYS_SOFTWARE=1) works on X11 Xvfb with no GPU.
Tasks
- Fix kitty config: initial_window_columns → initial_window_width, initial_window_rows → initial_window_height
- Switched approach: cage+grim with foot terminal (no OpenGL required) instead of xvfb+kitty
- Update flake.nix: added foot, grim, cage; foot config format fixed for v1.26.1
- Update screenshots.yml: uses nix devshell (cage+grim+foot), removed xvfb/mesa deps
Summary of Changes
-
Root cause: Two separate issues blocked headless capture:
- Kitty config used
initial_window_columns/initial_window_rows(invalid) → fixed toinitial_window_width/initial_window_height - Kitty requires OpenGL/EGL which wlroots headless backend doesn’t expose → switched to foot terminal (renders via Wayland SHM, no OpenGL)
- Kitty config used
-
Architecture: cage (headless Wayland) + foot (SHM terminal) + grim (Wayland screenshot)
- foot is pure Wayland, renders via shared-memory buffers — works perfectly with WLR_RENDERER=pixman
- FootProvider generates foot 1.26.1 config with [colors-dark] section
- cursor color uses double-value format:
cursor = <bg> <text>
-
Additional fixes:
- Fixture command paths now canonicalized to absolute (was failing after
cdto work dir) - FIXTURE_WORK_DIR exported to terminal environment
- git-diff and git-log fixtures now use
git --no-pagerto prevent pager from blocking
- Fixture command paths now canonicalized to absolute (was failing after
-
Verified: End-to-end capture of tokyo-night + git-diff and ls-color produces valid WebP screenshots
Remove provider dropdown from detail page (litmus-lywi)
The detail page has its own local selected_provider signal and dropdown, leftover from before the UI rework. Remove it and read from global ActiveProvider instead. ## Design
Remove the local signal and dropdown from theme_detail.rs. Read the provider from the global ActiveProvider context signal instead. Single source of truth.
Files: theme_detail.rs, style.css (remove .detail-provider-select / .detail-provider-label)
Tasks
- Remove local selected_provider signal from theme_detail.rs
- Remove provider dropdown UI from theme_detail.rs
- Use global ActiveProvider context signal for screenshot lookups
- Remove .detail-provider-select / .detail-provider-label CSS
- Verify detail page works with sidebar provider switching
Summary of Changes
Removed the local selected_provider signal and provider dropdown from the detail page. Screenshot lookups now use the global ActiveProvider context signal (single source of truth). Hoisted loop-invariant cur_provider and reused existing this_slug instead of recomputing per iteration. Removed the now-unused manifest_provider_slugs function and associated CSS.
Fix wasm-bindgen-cli version mismatch (litmus-r5xi)
dx build fails because pkgs.dioxus-cli bundles wasm-bindgen-cli 0.2.108 but Cargo.lock requires 0.2.114. Fix by pinning wasm-bindgen-cli in flake.nix and CI.
Summary of Changes
- Added to devShell packages so it takes precedence over the version bundled by
- Pinned in and added explicit install step for CI
Inline contrast issue markers with hover tooltips (litmus-rs0b)
The contrast validation logic exists (validate_fixtures_contrast in theme_detail.rs) and shows count badges, but inline marking on the actual rendered text spans is missing/regressed. Need to highlight the specific TermSpan elements that fail contrast rules.
Requirements
- Identify which TermSpan elements fail contrast rules and mark them inline in the rendered output
- Visual marker: multi-layer colored border (2-3 colors with increasing sizes) to ensure visibility on any theme background — use app theme colors, not the previewed theme’s colors
- Hover popup on marked spans: show rule name (e.g. “WCAG AA normal text”), computed contrast ratio, required threshold, and the fg/bg colors involved
- Preserve existing count badges in header and per-fixture sections
- Verify contrast validation works end-to-end after recent refactors (TermOutput migration etc.)
Design Notes
The border trick: use a multi-layer border (e.g. 1px inner + 2px outer in contrasting colors from the app chrome theme) so the marker is visible regardless of the previewed theme’s background color. The app theme is always accessible via CSS custom properties.
Summary of Changes
Restored inline contrast issue markers that were lost in the Scene→TermOutput migration. Added SpanIssueDetail type and issue_details prop through the TermOutputView→TermLineView→TermSpanView pipeline. Marked spans display a multi-layer box-shadow border (app-error + app-bg halo) visible on any theme background, with hover tooltips showing APCA threshold, WCAG ratio, and fg/bg color chips. Wired issue details from theme_detail.rs into the renderer.
Files changed:
crates/litmus-web/src/term_renderer.rs— SpanIssueDetail, issue-aware rendering pipelinecrates/litmus-web/src/pages/theme_detail.rs— build issue detail tuples, pass to renderercrates/litmus-web/assets/style.css— multi-layer box-shadow border, tooltip max-width
Fix minimap placement, scoring consistency, duplicate scenes (litmus-vryv)
Move minimap to sidebar, unify APCA for score+issues, remove expanded issues section
Drafts
Integration test skeleton for essential features (litmus-z7p4)
Set up a skeleton for integration/smoke tests that verify essential UI features don’t regress across refactors. Start with contrast issue visualization as the first tracked feature.
Requirements
- Define test infrastructure (framework, how tests run, what they verify)
- Add first test case: contrast issues are detected and displayed for a known theme with known violations
- Document pattern for adding new essential feature tests
Notes
This is a draft — details to be refined later. The goal is to have a lightweight safety net for features that have regressed before (contrast display being the first example).