Keyboard shortcuts

Press or to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

Introduction

Litmus test your terminal themes — preview them across all your apps before you commit.

Litmus Try it live at litmus.edger.dev

The Problem

Switching terminal themes is a frustrating loop:

  1. You find a theme that looks nice in a preview
  2. You edit configs for kitty, wezterm, neovim, zellij, delta…
  3. You discover git diff is unreadable, or cargo warnings blend into the background
  4. You revert everything and try the next theme
  5. 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

ProviderStatusNotes
kittySupportedFull capture and color extraction
weztermSupportedFull capture and color extraction
alacrittyPlannedNext major version
footPlannedNext major version
ghosttyPlannedNext 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.

FixtureConsumerWhat it demonstrates
git-diffgit diffDiff colors: additions, deletions, context
cargo-buildcargo buildCompiler output: warnings, errors, notes
bat-syntaxbatSyntax-highlighted source with line numbers
ripgrep-searchrgMatch highlighting, filenames, line numbers
ls-colorls --colorFile type colors: dirs, executables, symlinks
git-loggit log --graphGraph colors and branch decorations
shell-promptbash sessionPrompt colors and command output
python-replpython3REPL output, tracebacks
htoptopCPU, memory, process table
log-viewerapp logsStructured logs: INFO/WARN/ERROR/DEBUG
color-swatchANSI paletteReference palette: 16 ANSI + 256-color
color-showcaseCI dashboardSimulated deploy pipeline using all 16 ANSI colors
editor-uitext editorSyntax 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 diff additions are distinguishable from context lines
  • Whether cargo warnings are readable against the background
  • Whether bat line numbers have enough contrast
  • Whether ls directory 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

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.

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:

  1. 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)
  2. Setup — run the fixture’s setup.sh to create any required state (git repos, source files, etc.) in a temp directory
  3. Launch — start the terminal emulator headlessly with the fixture’s command.sh
  4. Capture — wait for the command to finish, take a screenshot with grim
  5. Convert — PNG → WebP for smaller file sizes
  6. 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 color
  • Ansi(n) → the nth color in the provider’s palette
  • Indexed(n) → looked up from the fixed xterm-256 color table
  • Rgb(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. Each TermSpan becomes a <span> with inline color and background-color CSS 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:

  1. Resolve fg and bg via TermColor::resolve() with the active ProviderColors
  2. Apply bold, italic, dim as CSS font-weight, font-style, opacity
  3. 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:

  1. Convert sRGB to linear luminance using the standard gamma transfer function
  2. Compute relative luminance for each color
  3. Calculate the contrast ratio between foreground and background
  4. 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:

  1. Converts sRGB to linear RGB
  2. Applies a 3×3 transformation matrix specific to the condition
  3. 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

CrateRoleTarget
litmus-modelShared types and logicany
litmus-webWeb frontendwasm32
litmus-cliTUI prototypenative
litmus-captureScreenshot capture pipelinenative (Wayland)

litmus-model

The foundation crate — no UI dependencies, compiles for any target. Contains:

  • Theme typesThemeDefinition, ProviderColors, Color, AnsiColors
  • Screenshot modelProvider, Fixture, ScreenshotKey, ScreenshotManifest
  • TermOutputTermColor, 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:

RoutePagePurpose
/:provider/ThemeListHome — filterable theme grid
/:provider/theme/:slugThemeDetailSingle theme, all fixtures
/:provider/scene/:scene_idSceneAcrossThemesOne fixture across all themes
/:provider/compare/:slugsCompareThemesTwo 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:

  1. Build with dx build --release (Dioxus CLI)
  2. Inject critical CSS to prevent flash of unstyled content during WASM load
  3. 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 themes
  • LastComparedSlug — previous compare partner for the “vs.” button
  • FilterState — search query, variant filter, readability threshold
  • CvdSimulation — active CVD mode (none/protanopia/deuteranopia/tritanopia)
  • ManifestState — cached screenshot manifest from CDN
  • AppThemeSlug — 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

  • Rust toolchain — stable channel with the wasm32-unknown-unknown target, as defined in rust-toolchain.toml
  • mise — task runner for all dev commands
  • Nix (optional) — flake.nix provides a reproducible dev shell with all dependencies

For screenshot capture only (not needed for web development):

  • Wayland compositor with GPU access
  • grim for 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:

  1. Start bacon in a terminal pane: mise run bacon-claude-diagnostics
  2. Edit code
  3. Bacon watches for changes and writes diagnostics to .bacon-claude-diagnostics
  4. 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

TaskDescription
devStart Dioxus dev server (port 8883)
dev-screenshotsLocal screenshot server + web app
build-webBuild WASM release
build-cliBuild CLI release
checkcargo check across workspace
fmtFormat with cargo fmt
docs-serveServe mdbook with live reload (port 8882)
docs-buildBuild static docs
capture-kittyCapture all kitty screenshots
capture-weztermCapture all wezterm screenshots
screenshots-deployBuild 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

  1. Check .bacon-claude-diagnostics for compilation errors
  2. Start the web app with mise run dev
  3. Confirm the theme appears in the theme list
  4. Check that all fixtures render on the detail page
  5. 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:

  1. Parse the ANSI output: litmus-capture parse-fixtures
  2. Register the fixture in crates/litmus-web/src/fixtures.rs
  3. Capture screenshots: mise run capture-kitty && mise run capture-wezterm
  4. 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:

  1. Vendored theme data — the provider’s theme definitions, checked into the repo
  2. Color extractor — code that parses the vendored data into litmus’s provider color format
  3. Capture config generator — code that writes the provider’s config file for headless screenshot capture
  4. 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)

All Tasks

Epics

Features

Tasks

Bugs

Drafts

Epics

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:

  1. Color variety — uses ≥4 distinct ANSI colors naturally (not forced)
  2. Instant recognition — a developer recognizes the scenario within 2 seconds
  3. Fits 80x24 — no truncation, no scrolling needed
  4. Deterministic — no timestamps, PIDs, paths that vary between runs (use fixed dates, fake PIDs, $FIXTURE_WORK_DIR)
  5. 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)

  1. Existing datasets/collections — terminal output samples, ANSI test suites, terminal emulator test fixtures
  2. Agent-generated — Claude writes fixture scripts for specific scenarios, human reviews
  3. 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:

  1. Generate or discover candidate → write to fixtures/candidates/{name}/
  2. Run capture pipeline → inspect screenshot + parsed output
  3. Evaluate against quality criteria (document in REVIEW.md)
  4. Accepted → move to fixtures/{name}/, delete REVIEW.md
  5. 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):

  1. litmus-c55s — Build color swatch and color showcase fixtures
  2. litmus-feex — Set up fixtures/candidates/ staging directory and review workflow
  3. litmus-3lcp — Research existing terminal output datasets and ANSI test suites
  4. litmus-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:

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

StatusDone · archived
TypeEpic
Prioritynormal

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:

  1. Fix app theme switcher (shell.rs): Moved app_theme.read() inside use_effect so Dioxus tracks the dependency
  2. Filter navigation (sidebar.rs): Added use_navigator() — search, variant, and readability filter changes push Route::ThemeList
  3. Remove scene tabs from sidebar (sidebar.rs): Removed Scenes section entirely
  4. Separate variant/contrast filters (sidebar.rs): All/Dark/Light now show count badges; readability is a separate dropdown below
  5. Readability score (contrast.rs, state.rs, sidebar.rs, theme_list.rs, theme_detail.rs): Added readability_score(), replaced good_contrast: bool with min_readability: Option<u8>, shown on sidebar items, theme cards, and detail header
  6. 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
  7. Move CVD (sidebar.rs): CVD section moved below Compare, just above App Theme
  8. 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):

  1. litmus-jmna — Add ThemeDefinition + ProviderColors types to litmus-model
  2. litmus-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-screenshots created with custom domain + CORS policy
  • rclone added to devShell for checksum-based sync
  • mise tasks: screenshots-sync (sync only) and screenshots-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

  1. Missing contrast issues in side-by-side view — the most valuable analysis feature is absent from the comparison page
  2. Compare page scales poorly — works well for 2 themes, OK for 3, unusable for more without a very wide monitor
  3. Shortlist/compare coupling is confusing — shortlist limited to 5 because it’s tied to compare; users may not understand the relationship
  4. 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 shownTermOutputView receives empty issue_details
  • URL: /:provider/compare/slug1,slug2,...

Contrast issue system (fully built on detail page):

  • validate_fixtures_contrast()Vec<TermContrastIssue> per theme
  • build_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 as Vec<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:

  1. 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
  2. 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_details to TermOutputView)
    • Footnotes at line ends
    • Per-fixture issue count badge on fixture headers
  3. 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
  4. 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:

  1. Keep 2-4 theme support as-is

  2. 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
  3. 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)
  4. 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”
  5. 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:

  1. 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
  2. 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
  3. 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”
  4. 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)

StatusDone · archived
TypeFeature
Prioritynormal
Parent(litmus-6pas)

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)

StatusDone · archived
TypeFeature
Prioritynormal
Parent(litmus-m8gs)

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)

StatusDone · archived
TypeFeature
Prioritynormal
Parent(litmus-f1b3)

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)

StatusDone · archived
TypeFeature
Prioritynormal
Parent(litmus-m8ze)

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)

StatusDone · archived
TypeFeature
Prioritynormal

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

StatusDone · archived
TypeFeature
Prioritynormal

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() with all_themes_with_availability()
  • Pass available: bool to ThemeCard component
  • Filter counts (variant badges, total) count only available themes
  • Shown count / filter applies to all themes (available + unavailable)

ThemeCard component

  • Accept available: bool prop
  • If unavailable: wrap in div instead of Link, add .theme-card--unavailable class, 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)

StatusDone · archived
TypeFeature
Prioritynormal
Parent(litmus-iiek)

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)

StatusDone · archived
TypeFeature
Prioritynormal
Blocked byProvider-scoped URL routing with fixture anchors (litmus-3svg)

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, AlertMessage state, and AlertBanner component

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)

StatusDone · archived
TypeFeature
Prioritynormal
Parent(litmus-gspc)

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)

StatusDone · archived
TypeFeature
Prioritynormal
Parent(litmus-74j8)

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)

StatusDone · archived
TypeFeature
Prioritynormal
  1. Limit shortlist to 5 themes (keep most recent)
  2. Apply pushes current theme to top of shortlist
  3. Gray out Shortlist checkbox for current theme
  4. 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)

StatusDone · archived
TypeFeature
Prioritynormal
Parent(litmus-lio0)

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)

StatusDone · archived
TypeFeature
Prioritynormal
Parent(litmus-lio0)

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)

StatusDone · archived
TypeFeature
Prioritynormal
Parent(litmus-m8gs)

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)

StatusDone · archived
TypeFeature
Prioritynormal
Parent(litmus-74j8)

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)

StatusDone · archived
TypeFeature
Prioritynormal
Parent(litmus-f1b3)

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)

StatusDone · archived
TypeFeature
Prioritynormal
Parent(litmus-iiek)

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)

StatusDone · archived
TypeFeature
Prioritynormal
Parent(litmus-m8ze)

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)

StatusDone · archived
TypeFeature
Prioritynormal
  • 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 GitHubStars component using use_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)

StatusDone · archived
TypeFeature
Prioritynormal
Parent(litmus-6pas)

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)

StatusDone · archived
TypeFeature
Prioritynormal

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, and bg_color fields to ContrastIssue
  • 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-error background

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

StatusDone · archived
TypeFeature
Prioritynormal

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)

StatusDone · archived
TypeFeature
Prioritynormal
Parent(litmus-f1b3)

Switch between themes, toggle between swatches/mock-ups/live views. Use ratatui or similar TUI framework.

Summary of Changes

  • Added catppuccin_mocha() and solarized_dark() theme constructors to theme_data.rs
  • Added all_themes() returning all three themes
  • Introduced App struct with themes, theme_index, view, git_diff, ls_output fields
  • Added View::name() returning display name for status bar
  • Refactored run() to use App::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)

StatusDone · archived
TypeFeature
Prioritynormal
Parent(litmus-gspc)

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)

StatusDone · archived
TypeFeature
Prioritynormal
Parent(litmus-2wte)

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 blocks
  • themes.rs: compile-time embedding of all 19 themes for WASM builds
  • main.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)

StatusDone · archived
TypeFeature
Prioritynormal

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 functions
    • components.rs — Shared components: FilterButton, ColorSwatch, CompareToggle, CvdSelector, ExportButtons, ColorDiffTable
    • pages/theme_list.rs — ThemeList (home grid)
    • pages/theme_detail.rs — ThemeDetail
    • pages/scene_across.rs — SceneAcrossThemes
    • pages/compare.rs — CompareThemes + CompareSelector
    • sidebar.rs — Persistent sidebar with all navigation
    • shell.rs — Shell layout (sidebar + main area)
  • main.rs reduced 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 documentElement
  • use_effect in 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 pre when data-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)

StatusDone · archived
TypeFeature
Prioritynormal

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-actions div with app theme colors

2. ShortlistCheckbox added (components.rs)

  • New ShortlistCheckbox component 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_idx forwarded to SpanView; lines 0-1 get contrast-tooltip-below class
  • .scene-block pre: overflow-x: clip; overflow-y: visible instead of overflow-x: auto
  • .contrast-issues-list: increased max-height, overflow-y: visible

5. CSS updates (style.css)

  • .theme-card-actions gets explicit app theme bg/fg/border
  • .shortlist-checkbox styles
  • .sidebar-current-badge and .sidebar-shortlist-name-link styles
  • Removed old .sidebar-shortlist-check styles

Mini scene preview on theme cards (litmus-s6e2)

StatusDone · archived
TypeFeature
Prioritynormal
Parent(litmus-m8gs)

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)

StatusDone · archived
TypeFeature
Prioritynormal
Parent(litmus-f1b3)

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)

StatusDone · archived
TypeFeature
Prioritynormal

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 CompareSelection to Shortlist with MAX_SHORTLIST = 8
  • Extracted CVD from FilterState into new CvdSimulation global signal
  • Made FilterState page-local (removed from global context providers)
  • Updated all context providers in App

Components (components.rs)

  • Renamed CompareToggle to ShortlistToggle with +Shortlist/Shortlisted text
  • Added UseAsAppThemeButton component (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
  • 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 CvdSimulation signal
  • Updated c keyboard shortcut to toggle shortlist

Compare page (compare.rs)

  • Removed CompareSelector dropdowns — URL is source of truth
  • Reads CVD from CvdSimulation signal

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 ThemeFamily struct and group_by_family function
  • Kept theme_family() (still used for search matching)

Single-theme detail page (litmus-v3cn)

StatusDone · archived
TypeFeature
Prioritynormal
Parent(litmus-m8ze)

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)

StatusDone · archived
TypeFeature
Prioritynormal

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 SceneMinimap component: 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 each compare-scene-group div
  • Added SceneMinimap component

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)

StatusDone · archived
TypeFeature
Prioritynormal

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)

StatusDone · archived
TypeFeature
Prioritynormal
Parent(litmus-2wte)

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)

StatusDone · archived
TypeFeature
Prioritynormal
Parent(litmus-gspc)

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)

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

  1. Add resolve_with_theme() to TermColor in litmus-model (reuses existing indexed_color())
  2. Add serde_json dep to litmus-cli
  3. Write tests: resolve_with_theme unit tests, TermOutput-to-ratatui-Lines conversion test
  4. Embed 3 fixture output.json files (git-diff, ls-color, shell-prompt) via include_str!()
  5. Rewrite MockupsWidget to parse embedded fixtures, map TermSpan → ratatui Span using theme colors
  6. 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 MockupsWidget to 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 LazyLock to 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 vte crate 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/underline
  • Cell: character + CellAttrs
  • Grid: rows×cols of Cells, cursor position tracking
  • AnsiParser: 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-ansi CLI 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)

StatusDone · archived
TypeTask
Prioritynormal
Parent(litmus-irro)

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)

StatusDone · archived
TypeTask
Prioritynormal
Parent(litmus-iiek)

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)

StatusDone · archived
TypeTask
Prioritynormal
Parent(litmus-74j8)

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)

StatusDone · archived
TypeTask
Prioritynormal
ParentFixtures iteration — more realistic, better color showcase (litmus-49jz)

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

PriorityResourceLicenseWhy
1tinted-theming/schemes (GitHub)MIT250+ color schemes in YAML, directly importable
2Gogh themes (GitHub)MIT200+ themes, clean 18-color YAML format
3shell-color-scripts (GitLab dwt1)MIT50+ color patterns for sample preview content
4terminal.sexy (GitHub)MITTemplate-based preview approach (captures real tmux pane content)
5pastel / vivid (sharkdp, Rust)MIT/Apache-2.0Color manipulation + filetype database for realistic ls output
6colortest (eliminmax)MixedCanonical 256-color test pattern with Rust impl
7asciinema asciicast v2Apache-2.0Format for capturing/replaying real terminal sessions
8alacritty-theme (GitHub)Apache-2.0100+ 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)

StatusDone · archived
TypeTask
Prioritynormal

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: auto from individual .compare-grid and .color-diff-body to 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_badges prop to SceneMinimap, 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)

StatusDone · archived
TypeTask
Prioritynormal

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

StatusDone · archived
TypeTask
Prioritynormal
ParentUI/UX improvement: shortlist, side-by-side, and contrast issues (litmus-ysy5)

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)

StatusDone · archived
TypeTask
Prioritynormal
Parent(litmus-haxl)

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)

StatusDone · archived
TypeTask
Prioritynormal

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

  1. Browse Themes active state: Changed to only highlight on ThemeList, not ThemeDetail
  2. Shortlist evicts oldest at max: Both ShortlistCheckbox and keyboard handler now remove the oldest entry when adding a 6th theme
  3. Compare page reacts to shortlist changes: Remove button rebuilds compare URL and navigates; clear button navigates to themes list
  4. Detail page palette always expanded: Changed initial state from false to true
  5. Compare page palette: Removed ColorDiffTable component and CSS, added per-theme color palettes at the end of the compare view
  6. 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

  1. Create setup.sh + command.sh for each candidate: ripgrep, bat, journalctl, neovim-like
  2. Test each locally (FIXTURE_WORK_DIR + command.sh)
  3. Parse with litmus-capture parse-ansi
  4. Fill out REVIEW.md for each
  5. 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:

  1. ripgrep-search — rg search results with heading mode (magenta filenames, green line numbers, bold red match highlighting). 13 lines, uses real rg.
  2. bat-syntax — Syntax-highlighted Python with bat (extensive color from syntax theme). 24 lines, fills 80x24 exactly.
  3. log-viewer — Simulated structured app logs (INFO/WARN/ERROR/DEBUG levels). 19 lines, pure ANSI 16-color, excellent color variety.
  4. 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)

StatusDone · archived
TypeTask
Prioritynormal

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)

StatusDone · archived
TypeTask
Prioritynormal
Parent(litmus-irro)

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)

StatusDone · archived
TypeTask
Prioritynormal
ParentDeploy screenshots to Cloudflare R2 (litmus-v2g1)
Blocked byCreate R2 bucket and API token (litmus-7k5y)

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-sync with –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 rclone to flake.nix devShell packages
  • Added screenshots-sync mise task: rclone sync with checksum-based diffing
  • Added screenshots-deploy mise 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 TermOutputView
  • crates/litmus-web/assets/style.css — added .compare-chips container and compact chip overrides

Create R2 bucket and API token (litmus-7k5y)

StatusDone · archived
TypeTask
Prioritynormal
ParentDeploy screenshots to Cloudflare R2 (litmus-v2g1)

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)

StatusDone · archived
TypeTask
Priorityhigh
Parent(litmus-sh4g)

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)

StatusDone · archived
TypeTask
Prioritynormal

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 Color and Theme structs, 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, fmt recipes

Implement kitty.conf and base16 YAML parsers (litmus-8dqh)

StatusDone · archived
TypeTask
Prioritynormal
Parent(litmus-haxl)

Implement parsers for at least: kitty.conf and base16 YAML into the internal theme format.

Migrate from just to mise (litmus-8sd8)

StatusDone · archived
TypeTask
Prioritynormal

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)

StatusDone · archived
TypeTask
Prioritynormal
ParentUI/UX improvement: shortlist, side-by-side, and contrast issues (litmus-ysy5)

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 ThemeContrastData struct and compute_theme_contrast() helper
  • Column headers now show ScoreRing readability % and issue count badge
  • TermOutputView receives issue_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)

StatusDone · archived
TypeTask
Prioritynormal
ParentUnify scenes and fixtures (litmus-coma)
Blocked byBuild ANSI-to-spans parser using VTE (litmus-28sq)

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)

StatusDone · archived
TypeTask
Prioritynormal
Parent(litmus-iiek)

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)

StatusDone · archived
TypeTask
Prioritynormal
Parent(litmus-k2id)

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 JSON
  • ImageFormat — Png | Webp with extension/mime helpers
  • ScreenshotMeta — full screenshot record with URL, dimensions, format, timestamp, SHA-256 checksum
  • ScreenshotManifest — 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

  1. Add TermContrastIssue struct (references fixture_id and TermColor variants)
  2. Add validate_term_output_contrast(output: &TermOutput, theme: &Theme) function
  3. Skip pairs where both fg and bg are fixed (Indexed/Rgb vs Indexed/Rgb)
  4. Validate all other pairs using APCA
  5. Add validate_all_fixtures_contrast(fixtures: &[TermOutput], theme: &Theme) convenience
  6. Tests: low-contrast detection, skip-fixed-pairs, dim exclusion

Summary of Changes

  • Added TermContrastIssue struct 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)

StatusDone · archived
TypeTask
Prioritynormal
Parent(litmus-lio0)

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)

StatusDone · archived
TypeTask
Prioritynormal
ParentFixtures iteration — more realistic, better color showcase (litmus-49jz)

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)

StatusDone · archived
TypeTask
Prioritynormal

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

  1. 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
  2. Update main.rs:

    • Add --provider CLI flag
    • Pass to load_bundled_provider_themes()
  3. 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)

StatusDone · archived
TypeTask
Prioritynormal

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_SIZE env var — used wlr-randr --custom-mode inside the cage wrapper script instead
  • Added pixel_width/pixel_height to TermGeometry (default 1280x960 = 4:3)
  • Added wlr-randr to 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)

StatusDone · archived
TypeTask
Prioritynormal

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

  1. color-showcase — exercises all 16 ANSI colors
  2. 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)

StatusDone
TypeTask
Prioritynormal

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)

StatusDone · archived
TypeTask
Prioritynormal
ParentFixtures iteration — more realistic, better color showcase (litmus-49jz)

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)

StatusDone · archived
TypeTask
Prioritynormal
Parent(litmus-k2id)
Blocked byM1: Screenshot Data Model (litmus-b10b), M4: Cloudflare R2 Storage Setup (litmus-j0d6)

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) global state types.

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

  • ScreenshotSceneView component: renders real screenshot <img> from CDN URL when available, with simulated SceneView as fallback on load error or when no screenshot exists
  • ProviderSelector component: 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)

StatusDone · archived
TypeTask
Prioritynormal
Parent(litmus-f1b3)

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)

StatusDone · archived
TypeTask
Prioritynormal
Parent(litmus-2wte)

Define the scene format: annotated sample content with semantic color references.

Summary of Changes

Added scene module to litmus-model with:

  • ThemeColor enum: semantic color references (Foreground, Background, Cursor, Selection*, Ansi(0-15)) with resolve() method
  • TextStyle: bold/italic/underline/dim modifiers
  • StyledSpan: text + optional fg/bg ThemeColor + TextStyle, with builder methods
  • SceneLine: sequence of StyledSpans
  • Scene: 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)

StatusDone · archived
TypeTask
Prioritynormal
Parent(litmus-haxl)

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

  1. Update litmus-capture load_all_themes() to use load_themes_dir() instead of parse_toml_theme()
  2. Remove unused parse_toml_theme import from capture crate
  3. Verify no old-format theme TOMLs remain
  4. Keep parse_toml_theme in litmus-model for CLI user-provided file support
  5. 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)

StatusDone · archived
TypeTask
Prioritynormal

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)

StatusDone · archived
TypeTask
Prioritynormal
Parent(litmus-k2id)
Blocked byM1: Screenshot Data Model (litmus-b10b)

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:

  1. Create R2 bucket litmus-screenshots in Cloudflare dashboard
  2. Enable public access with custom domain screenshots.litmus.edger.dev
  3. Configure CORS:
    • Allowed origin: https://litmus.edger.dev
    • Allowed methods: GET
  4. Add GitHub Actions secrets:
    • R2_ACCESS_KEY_ID
    • R2_SECRET_ACCESS_KEY
    • R2_ACCOUNT_ID
    • R2_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)

StatusDone · archived
TypeTask
Prioritynormal
ParentProvider-based theme definition (litmus-knrz)

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: .toml for definitions, .{provider}.toml for 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

  1. Variant enum (Dark/Light) with serde lowercase
  2. ThemeDefinition struct: name, variant, slug (derived from filename), providers HashMap
  3. ProviderColors struct: provider slug, source_version, same color fields as Theme
  4. TOML parsing for both types (reuse existing parse_field pattern)
  5. 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

  1. Tests commit (failing)
  2. Implementation commit

Summary of Changes

Added provider module to litmus-model with:

  • Variant enum (Dark/Light) with lowercase serde
  • ThemeDefinition struct: name, variant, slug (from filename), providers HashMap<String, String>
  • ProviderColors struct: provider slug, source_version, all 21 color fields matching Theme
  • TOML parsing for both types via parse_theme_definition() and parse_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)

StatusDone · archived
TypeTask
Prioritynormal

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)

StatusDone · archived
TypeTask
Prioritynormal

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 Roadmap
  • next-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.

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

  2. 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 diff says “make additions green” (ANSI 2), and what “green” looks like depends on the provider. Why the same git diff looks 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.)
  3. 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.

  4. Dual-mode apps — brief note: some apps (lazygit, jjui) can be consumer or silo. (Roadmap.)

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

  1. Add term_readability_score(theme, fixtures) to contrast.rs
  2. Make fixtures::all_fixtures() pub in litmus-web
  3. Update SceneMinimap to accept fixture metadata instead of Scene
  4. Update sidebar to pass fixture data to minimap
  5. Update all pages (theme_detail, scene_across, compare, theme_list) to iterate fixtures instead of scenes
  6. Remove SceneView/ScenePreview fallback calls
  7. Remove scene_renderer.rs module
  8. Remove scene-based contrast functions and tests
  9. Delete scene.rs and scenes.rs from litmus-model
  10. Clean up all remaining references

Summary of Changes

Removed all Scene/ThemeColor/StyledSpan types and hand-written scene definitions:

  • Deleted scene.rs and scenes.rs from litmus-model
  • Deleted scene_renderer.rs from 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.rs and compare.rs to iterate fixtures directly
  • Updated theme_list.rs to use term_readability_score() and removed scene_renderer fallback
  • Updated state.rs filter to use term_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)

StatusDone · archived
TypeTask
Priorityhigh
Parent(litmus-sh4g)

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)

StatusDone · archived
TypeTask
Prioritynormal
Parent(litmus-k2id)
Blocked byM1: Screenshot Data Model (litmus-b10b), M2: Fixture System (litmus-rz7q)

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 errors
  • providers/mod.rs — ProviderCapture trait + TermGeometry struct + provider registry
  • providers/kitty.rs — KittyProvider: generates kitty.conf with theme colors + FiraCode/80×24/no-decorations settings; builds launch args
  • capture.rs — core capture logic: fixture setup, cage+kitty orchestration via wrapper script, sentinel-file completion detection, grim screenshot, PNG→WebP conversion, sha256 checksum
  • manifest.rs — builds ScreenshotManifest by scanning staging/{provider}/{theme}/{fixture}.webp; CoverageReport for CI validation
  • main.rs — CLI with clap: capture, capture-all, manifest build, manifest check subcommands

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

  1. Create crates/litmus-web/src/fixtures.rs — embed fixture output.json files, parse with LazyLock
  2. Create crates/litmus-web/src/term_renderer.rs — TermOutputView and TermOutputPreview components
  3. Wire TermOutputView into theme_detail.rs (replace SceneView on left panel)
  4. Wire TermOutputPreview into theme_list.rs (replace ScenePreview in cards)
  5. Wire TermOutputView into scene_across.rs and compare.rs
  6. Tests for color resolution and fixture loading

Summary of Changes

  • Created fixtures.rs module: embeds 8 fixture output.json files, OnceLock caching, fixture_by_id() and default_fixture() API (4 tests)
  • Created term_renderer.rs module: TermOutputView and TermOutputPreview Dioxus 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)

StatusDone · archived
TypeTask
Prioritynormal

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)

StatusDone · archived
TypeTask
Prioritynormal
ParentDeploy screenshots to Cloudflare R2 (litmus-v2g1)
Blocked byCreate R2 bucket and API token (litmus-7k5y)

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)

StatusDone · archived
TypeTask
Prioritynormal
Parent(litmus-irro)

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)

StatusDone · archived
TypeTask
Prioritynormal
ParentUI/UX improvement: shortlist, side-by-side, and contrast issues (litmus-ysy5)

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)

StatusDone · archived
TypeTask
Prioritynormal
Parent(litmus-m8gs)

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)

StatusDone · archived
TypeTask
Prioritynormal
ParentProvider-based theme definition (litmus-knrz)
Blocked byBuild litmus extract-colors command (litmus-z20l)

Write a script (or extend extract-colors) that:

  1. Reads each existing themes/**/*.toml (which currently have [colors] sections)
  2. Matches theme names to kitty/wezterm built-in theme names (fuzzy matching or manual mapping for popular ones)
  3. Writes new authored .toml files with just name, variant, and [providers] section
  4. Runs extract-colors to generate per-provider color files
  5. 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)

StatusDone · archived
TypeTask
Prioritynormal

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)

StatusDone · archived
TypeTask
Prioritynormal
ParentUnify scenes and fixtures (litmus-coma)

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

  1. TermColor enum: Default, Ansi(u8), Indexed(u8), Rgb(u8, u8, u8)
  2. 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
  3. TermSpan: text + fg/bg TermColor + bold/italic/dim/underline
  4. TermLine: Vec
  5. TermOutput: id, name, cols, rows, Vec
  6. 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

  1. Tests + implementation
  2. Review fixes

Summary of Changes

Added term_output module to litmus-model with:

  • TermColor enum: Default, Ansi(0-15), Indexed(16-255), Rgb(r,g,b) with tagged serde
  • TermColor::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_if
  • TermLine and TermOutput: 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)

StatusDone · archived
TypeTask
Prioritynormal
Parent(litmus-2wte)

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 text
  • validate_theme_readability: convenience function that checks all built-in scenes at WCAG AA level
  • ContrastIssue struct with full location info (scene/line/span), colors, ratio, and threshold

M2: Fixture System (litmus-rz7q)

StatusDone · archived
TypeTask
Prioritynormal
Parent(litmus-k2id)

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 files
  • git-log/ — real git log –graph with branches, merges, tags (pinned timestamps for reproducibility)
  • cargo-build/ — real cargo build with intentional warnings + type error
  • shell-prompt/ — scripted bash session output (interactive shell not scriptable reliably)
  • python-repl/ — scripted Python REPL session output
  • htop/ — 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)

StatusDone · archived
TypeTask
Prioritynormal
ParentFixtures iteration — more realistic, better color showcase (litmus-49jz)

Review the 7 existing fixtures against the quality criteria:

  1. Color variety (≥4 distinct ANSI colors)
  2. Instant recognition
  3. Fits 80x24
  4. Deterministic
  5. 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

FixtureColorRecognition80x24DeterministicSelf-ContainedAction
git-diffPASSPASSPASSPASSPASSNone
git-logPASSPASSPASSPASSPASSNone
ls-colorPASSPASSPASSFIXEDPASSSet explicit LS_COLORS
cargo-buildPASSPASSPASSPASSPASSNone
shell-promptPASSPASSPASSPASSPASSSimulated (OK)
python-replPASSPASSPASSPASSPASSSimulated (OK)
htopPASSPASSPASSFIXEDPASSAlways use scripted output

Summary of Changes

  • htop: Removed non-deterministic top -b -n 1 path that produced varying process data. Now always uses scripted htop-like display with fixed processes and ANSI colors.
  • ls-color: Added explicit LS_COLORS export 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)

StatusDone · archived
TypeTask
Prioritynormal
Parent(litmus-m8ze)

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)

StatusDone · archived
TypeTask
Prioritynormal
Parent(litmus-74j8)

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)

StatusDone · archived
TypeTask
Prioritynormal
Parent(litmus-sh4g)

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)

StatusDone · archived
TypeTask
Prioritynormal
Parent(litmus-m8ze)

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)

StatusDone · archived
TypeTask
Prioritynormal
Parent(litmus-6pas)

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)

StatusDone · archived
TypeTask
Prioritynormal
Parent(litmus-irro)

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)

StatusDone · archived
TypeTask
Prioritynormal
ParentProvider-based theme definition (litmus-knrz)

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 subtree from 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.md with update procedures for both sources
  • scripts/extract-wezterm-schemes.py for reproducible wezterm extraction

Theme validation and error handling (litmus-vvye)

StatusDone · archived
TypeTask
Prioritynormal
Parent(litmus-haxl)

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)

StatusDone · archived
TypeTask
Prioritynormal
Parent(litmus-haxl)

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)

StatusDone · archived
TypeTask
Priorityhigh
Parent(litmus-sh4g)

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-sync and screenshots-deploy

Screenshot in side-by-side view (litmus-x2vo)

StatusDone · archived
TypeTask
Prioritynormal
Blocked byUpdate litmus-web to provider-scoped theme rendering (litmus-y6dc)

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 SceneView with ScreenshotImage for 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 ScreenshotImage component and manifest infrastructure
  • Toggle is just Simulated/Screenshot; provider comes from global state

Plan

  1. Add a use_signal(|| false) for screenshot mode in CompareThemes
  2. Add a toolbar above the grid with Simulated/Screenshot toggle buttons (reuse provider-btn styling pattern)
  3. When screenshot mode is active, render ScreenshotImage (using ActiveProvider) instead of TermOutputView
  4. Show placeholder when screenshot not available
  5. Add CSS for the toggle toolbar
  6. 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)

StatusDone · archived
TypeTask
Prioritynormal

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.md with 4-page TOC
  • Created docs/src/introduction.md from README problem/solution section
  • Created docs/src/concepts.md with three-layer model and provider ecosystems
  • Created docs/src/milestones.md adapted from existing docs/milestones.md
  • Created docs/src/contributing.md placeholder
  • Added pkgs.mdbook to flake.nix devShells packages
  • Added docs-serve and docs-build tasks to .mise.toml
  • Added docs/dist/ to .gitignore
  • Verified: mdbook build docs produces docs/dist/ successfully

Upgrade bacon export for richer Claude Code diagnostics (litmus-yte1)

StatusDone · archived
TypeTask
Prioritynormal

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: Added ProviderColors::from_theme() conversion and to_toml() serialization

litmus-capture additions:

  • extract.rs: Vendor index builders (kitty themes.json, wezterm metadata.name + aliases), theme definition scanner, provider-specific color extraction
  • main.rs: New ExtractColors subcommand 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)

StatusDraft
TypeTask
Prioritynormal

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)

StatusDone · archived
TypeBug
Prioritynormal

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)

StatusDone · archived
TypeBug
Prioritynormal

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.lock via nix run nixpkgs#cargo -- generate-lockfile (cargo not available in PATH)
  • Staged Cargo.lock with git add so crane can see it
  • Added pname = "litmus"; to commonArgs in flake.nix to silence crane name warning
  • Verified nix develop --command echo OK succeeds

Fix headless screenshot capture: kitty EGL + config keys (litmus-haaf)

StatusDone · archived
TypeBug
Prioritynormal

Two blocking issues preventing screenshot capture from working:

  1. Wrong kitty config keys: initial_window_columns/initial_window_rows are not valid kitty config options (logged as ‘Ignoring unknown config key’). Correct keys are initial_window_width/initial_window_height (cell-count, no suffix).

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

    1. Kitty config used initial_window_columns/initial_window_rows (invalid) → fixed to initial_window_width/initial_window_height
    2. Kitty requires OpenGL/EGL which wlroots headless backend doesn’t expose → switched to foot terminal (renders via Wayland SHM, no OpenGL)
  • 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 cd to work dir)
    • FIXTURE_WORK_DIR exported to terminal environment
    • git-diff and git-log fixtures now use git --no-pager to prevent pager from blocking
  • 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)

StatusDone · archived
TypeBug
Prioritynormal

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)

StatusDone · archived
TypeBug
Prioritynormal

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)

StatusDone · archived
TypeBug
Priorityhigh

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 pipeline
  • crates/litmus-web/src/pages/theme_detail.rs — build issue detail tuples, pass to renderer
  • crates/litmus-web/assets/style.css — multi-layer box-shadow border, tooltip max-width

Fix minimap placement, scoring consistency, duplicate scenes (litmus-vryv)

StatusDone · archived
TypeBug
Prioritynormal

Move minimap to sidebar, unify APCA for score+issues, remove expanded issues section

Drafts

Integration test skeleton for essential features (litmus-z7p4)

StatusDraft
TypeTask
Prioritynormal

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