diff --git a/AGENTS.md b/AGENTS.md index 7ad6261d..d4b7c988 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -46,7 +46,9 @@ sdk/src/ ← npm package (@basiclines/rampa-sdk) cli/src/ ← npm package (@basiclines/rampa) ├── index.ts Main CLI entry (citty framework) ├── colorspace.ts `rampa colorspace` subcommand - └── palette.ts `rampa palette` subcommand + ├── palette.ts `rampa palette` subcommand + ├── theme.ts `rampa theme` subcommand — list/search/install/show/preview + └── theme-generators/ Per-app theme generators (ghostty, iterm2, alacritty, kitty, warp…) src/ ← React web app (rampa.studio) ├── components/ shadcn/ui + @react-three/fiber for 3D viewer @@ -107,12 +109,23 @@ All color space constructors validate that input colors use the same format (hex When adding a new color space type, update ALL export functions in `src/usecases/GenerateExports.ts`: `getSpaceColors`, `generateSpaceJsonExport`, `generateSpaceSdkExport`, `generateSpaceCliExport`, `generateSpaceCssExport`. ### CLI Argument Patterns -CLI uses manual argument parsing (not a framework for `colorspace` subcommand). When adding flags: -- Add parsing in `parseColorspaceArgs()` -- Add config file support in `loadConfig()` +CLI uses manual argument parsing (not a framework for `colorspace` and `theme` subcommands). When adding flags: +- Add parsing in `parseColorspaceArgs()` or `parseThemeArgs()` +- Add config file support in `loadConfig()` (colorspace) - Preserve new fields in the config merge block (lines ~339-348) - Guard `interpolation === false` for modes that need it +### Theme Subcommand +`rampa theme` installs/lists color themes from the VS Code marketplace. Key files: +- `cli/src/theme.ts` — `parseThemeArgs()`, `listThemes()`, `showTheme()`, `installTheme()` +- `cli/src/theme-generators/` — one file per app (ghostty, iterm2, alacritty, kitty, windows-terminal, warp, hyper, vscode, xcode, android-studio) +- `themes/` — 7,800+ YAML files, each with `colors`, `meta` (mode/pair/contrast), `source` +- `scripts/pair-themes.ts` — Pairs dark/light variants; run after scraping + +Theme pairing uses 4 passes: (1) same-publisher exact token match, (2) cross-publisher token match, (3) implicit counterpart (no token on one side), (4) full OKLCH color signature + name similarity for flavor-named families (Catppuccin etc.). + +List filters: `--paired`, `--sort`, `--min-installs`, `--min-contrast` (avg APCA), `--min-distinct` (full OKLCH signature dedup, same-publisher scoped). + ### Testing - Runtime: `bun:test` with `describe`/`it`/`expect` - SDK tests verify palette sizes, index clamping, format conversion, and interpolation modes diff --git a/cli/README.md b/cli/README.md index 0a0ea54f..ca4396db 100644 --- a/cli/README.md +++ b/cli/README.md @@ -567,6 +567,63 @@ rampa palette photo.jpg --average rampa palette photo.jpg --temperature ``` +### Color Themes + +Install and manage color themes across multiple terminal and editor apps. Themes are sourced from the VS Code marketplace and converted to native formats. + +```bash +rampa theme list +rampa theme list "Dracula" +rampa theme list --paired --sort installs +rampa theme "Tokyo Night" --show +rampa theme "Dracula" --install ghostty +rampa theme "Catppuccin Mocha" --install ghostty,kitty,alacritty --dry-run +``` + +**Supported apps:** `ghostty`, `iterm2`, `alacritty`, `kitty`, `windows-terminal`, `warp`, `hyper`, `vscode`, `xcode`, `android-studio` + +#### List flags + +| Flag | Description | Default | +|------|-------------|---------| +| `[query]` | Fuzzy search by name (positional) | — | +| `--paired` | Show only dark/light pairs (one row per pair) | off | +| `--sort ` | Sort by: `name`, `installs`, `rating`, `ratings`, `mode` (chainable) | `name` | +| `--min-installs ` | Only show themes with at least n installs | 0 | +| `--min-contrast ` | Only show themes where avg APCA contrast of fg + tonal colors ≥ n (0–108) | 0 | +| `--min-distinct ` | Deduplicate similar themes within the same publisher using full OKLCH signature (0–100) | 0 | +| `--all` | Show all results (default cap: 100) | off | +| `--local` | Use local `themes/` directory instead of fetching from GitHub | off | + +#### Install / inspect flags + +| Flag | Description | +|------|-------------| +| `--install ` | Generate and write theme file for the given app | +| `--show` | Print theme colors and metadata | +| `--preview` | Open interactive browser preview | +| `--dry-run` | Print generated output without writing | + +#### Examples + +```bash +# Curated paired list — popular, good contrast, deduplicated +rampa theme list --local --sort installs --paired \ + --min-installs 1000 --min-contrast 50 --min-distinct 20 --all + +# Fuzzy search + paired filter +rampa theme list --paired "Solarized" + +# Install to multiple apps +rampa theme "Gruvbox Dark Hard" --install alacritty +rampa theme "Dracula" --install ghostty,kitty,iterm2 +rampa theme "Catppuccin Mocha" --install warp --dry-run + +# Preview before installing +rampa theme "Tokyo Night" --show +rampa theme "Aura Dark" --preview +``` + ## Contextual Help Run any flag without a value to see detailed help: diff --git a/cli/src/index.ts b/cli/src/index.ts index 842197db..f0acbc3a 100644 --- a/cli/src/index.ts +++ b/cli/src/index.ts @@ -64,6 +64,13 @@ if (args[0] === 'palette') { process.exit(0); } +// Intercept theme subcommand +if (args[0] === 'theme') { + const { runTheme } = await import('./theme'); + await runTheme(args.slice(1)); + process.exit(0); +} + if (args.includes('--help') || args.includes('-h') || args.includes('help') || args.length === 0) { showHelp(); } @@ -157,6 +164,15 @@ IMAGE PALETTE ${cyan}rampa palette${reset} ${dim}Extract color palettes from images${reset} ${dim}Use rampa palette --help for details${reset} +COLOR THEMES + ${cyan}rampa theme list${reset} ${dim}List all available themes${reset} + ${cyan}rampa theme list "Aura"${reset} ${dim}Search themes by name (fuzzy)${reset} + ${cyan}rampa theme list --paired${reset} ${dim}Show only dark/light pairs${reset} + ${cyan}rampa theme list --paired --sort installs --min-installs 1000 --min-contrast 50 --min-distinct 20${reset} + ${cyan}rampa theme "Dracula" --show${reset} ${dim}Inspect theme colors${reset} + ${cyan}rampa theme "Dracula" --install ghostty${reset} ${dim}Install theme for an app${reset} + ${dim}Use rampa theme --help for details${reset} + OTHER ${cyan}-h, --help${reset} ${dim}Show this help${reset} ${cyan}-v, --version${reset} ${dim}Show version${reset} @@ -178,6 +194,10 @@ EXAMPLES ${cyan}rampa inspect -c '#ff6600'${reset} ${cyan}rampa palette photo.jpg${reset} ${cyan}rampa palette photo.jpg --ansi --count 3${reset} + ${cyan}rampa theme list "Dracula"${reset} + ${cyan}rampa theme list --paired --sort rating${reset} + ${cyan}rampa theme "Tokyo Night" --install ghostty,kitty,vscode${reset} + ${cyan}rampa theme "Aura Dark" --install alacritty --dry-run${reset} `; console.log(help.trim()); process.exit(0); diff --git a/cli/src/theme-color-engine.ts b/cli/src/theme-color-engine.ts new file mode 100644 index 00000000..9c7cf769 --- /dev/null +++ b/cli/src/theme-color-engine.ts @@ -0,0 +1,106 @@ +import type { ThemeYAML } from './theme-schema'; +import { PlaneColorSpace } from '../../sdk/src/plane-color-space'; +import { hexToOklch, oklchToHex } from '../../src/engine/OklchMathEngine'; + +export interface EditorPalette { + // Backgrounds + bg: string; + bgAlt: string; + lineHighlight: string; + selection: string; + selectionBorder: string; + + // Foregrounds + fg: string; + fgMuted: string; + comment: string; + + // Cursor + cursor: string; + + // Syntax tokens + keyword: string; + string: string; + number: string; + fn: string; + type: string; + variable: string; + operator: string; + + // Diff backgrounds + added: string; + removed: string; + modified: string; + + // Status colors + error: string; + warning: string; + info: string; + + // Pass-through ANSI + ansi: ThemeYAML['colors']; + mode: 'dark' | 'light'; +} + +const PLANE_SIZE = 10; + +const h = (c: any): string => `${c}`; + +export function deriveEditorPalette(theme: ThemeYAML): EditorPalette { + const c = theme.colors; + const bg = c.bg; + const fg = c.fg; + const blue = c.blue; + const green = c.green; + const yellow = c.yellow; + const red = c.red; + const magenta = c.magenta; + const cyan = c.cyan; + + // type: blue rotated -90° toward teal + const [bl, bc, bh] = hexToOklch(blue); + const teal = oklchToHex(bl, bc, ((bh - 90) + 360) % 360); + + const plane = (hue: string) => new PlaneColorSpace(bg, fg, hue).interpolation('lab').size(PLANE_SIZE); + + const bluePlane = plane(blue); + const greenPlane = plane(green); + const yellowPlane = plane(yellow); + const redPlane = plane(red); + const magPlane = plane(magenta); + const typePlane = plane(teal); + const neutralPlane = plane(blue); + + return { + bg, + bgAlt: h(neutralPlane.at(2, 1)), + lineHighlight: h(neutralPlane.at(2, 1)), + selection: h(bluePlane.at(8, 3)), + selectionBorder: h(bluePlane.at(5, 4)), + + fg, + fgMuted: h(neutralPlane.at(0, 7)), + comment: h(neutralPlane.at(0, 4)), + + cursor: h(bluePlane.at(9, 7)), + + keyword: h(bluePlane.at(9, 7)), + string: h(greenPlane.at(9, 7)), + number: h(yellowPlane.at(9, 6)), + fn: h(yellowPlane.at(9, 7)), + type: h(typePlane.at(6, 8)), + variable: fg, + operator: h(magPlane.at(9, 7)), + + added: h(greenPlane.at(5, 3)), + removed: h(redPlane.at(5, 3)), + modified: h(yellowPlane.at(5, 3)), + + error: c.red, + warning: c.yellow, + info: c.cyan, + + ansi: c, + mode: theme.meta.mode, + }; +} diff --git a/cli/src/theme-generators/alacritty.ts b/cli/src/theme-generators/alacritty.ts new file mode 100644 index 00000000..6895273e --- /dev/null +++ b/cli/src/theme-generators/alacritty.ts @@ -0,0 +1,57 @@ +import type { ThemeYAML } from '../theme-schema'; +import type { ThemeGenerator } from './base'; +import { deriveEditorPalette } from '../theme-color-engine'; + +export const alacrittyGenerator: ThemeGenerator = { + name: 'alacritty', + + generate(theme: ThemeYAML): string { + const p = deriveEditorPalette(theme); + const c = theme.colors; + const lines: string[] = []; + + lines.push('[colors.primary]'); + lines.push(`background = "${c.bg}"`); + lines.push(`foreground = "${c.fg}"`); + lines.push(''); + lines.push('[colors.cursor]'); + lines.push(`cursor = "${p.cursor}"`); + lines.push(`text = "${c.bg}"`); + lines.push(''); + lines.push('[colors.selection]'); + lines.push(`background = "${p.selection}"`); + lines.push(`text = "${c.fg}"`); + lines.push(''); + lines.push('[colors.normal]'); + lines.push(`black = "${c.black}"`); + lines.push(`red = "${c.red}"`); + lines.push(`green = "${c.green}"`); + lines.push(`yellow = "${c.yellow}"`); + lines.push(`blue = "${c.blue}"`); + lines.push(`magenta = "${c.magenta}"`); + lines.push(`cyan = "${c.cyan}"`); + lines.push(`white = "${c.white}"`); + lines.push(''); + lines.push('[colors.bright]'); + lines.push(`black = "${c.brightBlack}"`); + lines.push(`red = "${c.brightRed}"`); + lines.push(`green = "${c.brightGreen}"`); + lines.push(`yellow = "${c.brightYellow}"`); + lines.push(`blue = "${c.brightBlue}"`); + lines.push(`magenta = "${c.brightMagenta}"`); + lines.push(`cyan = "${c.brightCyan}"`); + lines.push(`white = "${c.brightWhite}"`); + + return lines.join('\n') + '\n'; + }, + + installPath(os) { + if (os === 'darwin' || os === 'linux') return '~/.config/alacritty/themes'; + if (os === 'win32') return '%APPDATA%\\alacritty\\themes'; + return null; + }, + + fileExtension() { + return '.toml'; + }, +}; diff --git a/cli/src/theme-generators/android-studio.ts b/cli/src/theme-generators/android-studio.ts new file mode 100644 index 00000000..542313ae --- /dev/null +++ b/cli/src/theme-generators/android-studio.ts @@ -0,0 +1,141 @@ +import type { ThemeYAML } from '../theme-schema'; +import type { ThemeGenerator } from './base'; +import { deriveEditorPalette } from '../theme-color-engine'; + +function hex6(hex: string): string { + return hex.replace('#', '').toUpperCase(); +} + +function colorAttr(name: string, hex: string): string { + return ` `; +} + +function fgAttr(name: string, hex: string): string { + return syntaxAttr(name, [`