Agent skill
script-kit-theme
Theme and color system for script-kit-gpui
Install this agent skill to your Project
npx add-skill https://github.com/johnlindquist/script-kit-next/tree/main/.opencode/skill/script-kit-theme
SKILL.md
script-kit-theme
A comprehensive theming system for Script Kit's GPUI interface. The theme supports:
- Dark/light mode with automatic system appearance detection
- Focus-aware color variations (dimmed colors when window loses focus)
- macOS vibrancy/translucency effects
- ANSI terminal color palette (16 colors)
- Integration with gpui-component's ThemeColor system
- Hot-reloading via file watcher
Theme configuration lives at ~/.scriptkit/kit/theme.json.
Important: See AGENTS.md §17b for the GPUI Vibrancy Gotcha - GPUI hides the CAChameleonLayer so you must provide your own dark tint via theme colors at 70-85% opacity.
Key Types
Core Theme Types (types.rs)
/// Complete theme definition - the root struct
pub struct Theme {
pub colors: ColorScheme, // Required: base colors
pub focus_aware: Option<FocusAwareColorScheme>, // Optional: focus-specific overrides
pub opacity: Option<BackgroundOpacity>, // Window transparency settings
pub drop_shadow: Option<DropShadow>, // Window shadow config
pub vibrancy: Option<VibrancySettings>, // macOS blur effect
pub fonts: Option<FontConfig>, // Font families and sizes
}
/// Color scheme with all color categories
pub struct ColorScheme {
pub background: BackgroundColors, // main, title_bar, search_box, log_panel
pub text: TextColors, // primary, secondary, tertiary, muted, dimmed
pub accent: AccentColors, // selected (#fbbf24 gold), selected_subtle
pub ui: UIColors, // border, success, error, warning, info
pub terminal: TerminalColors, // 16 ANSI colors for embedded terminal
}
HexColor (hex_color.rs)
/// Colors stored as u32 (0xRRGGBB)
pub type HexColor = u32;
// Supports multiple input formats in JSON:
// - Numbers: 1973790 (decimal)
// - Hex strings: "#1E1E1E", "1E1E1E", "0x1E1E1E"
// - RGB/RGBA: "rgb(30, 30, 30)", "rgba(30, 30, 30, 1.0)"
Opacity Settings (types.rs)
pub struct BackgroundOpacity {
pub main: f32, // Main window (0.30)
pub title_bar: f32, // Title bar (0.30)
pub search_box: f32, // Input fields (0.40)
pub log_panel: f32, // Log/terminal (0.40)
pub selected: f32, // Selected item (0.15)
pub hover: f32, // Hovered item (0.08)
pub dialog: f32, // Dialogs/popups (0.15)
pub input: f32, // Input backgrounds (0.30)
pub panel: f32, // Panels/containers (0.20)
// ... and more state-specific opacities
}
Vibrancy (types.rs)
pub struct VibrancySettings {
pub enabled: bool, // Default: true
pub material: VibrancyMaterial, // Default: Popover
}
pub enum VibrancyMaterial {
Hud, // Dark, high contrast
Popover, // Light blur (default) - matches Electron's vibrancy:'popover'
Menu, // System menu style
Sidebar, // Sidebar blur
Content, // Content background
}
Semantic Colors (semantic.rs)
Higher-level abstraction for component styling:
/// Focus-aware wrapper for any type
pub struct FocusAware<T> {
pub focused: T,
pub unfocused: T,
}
/// Semantic tokens (bg_*, text_*, border_*, status_*, overlay_*)
pub struct SemanticColors {
// Backgrounds
pub bg_primary: Hsla,
pub bg_secondary: Hsla,
pub bg_selected: Hsla,
pub bg_hover: Hsla,
// Text
pub text_primary: Hsla,
pub text_secondary: Hsla,
pub text_muted: Hsla,
pub text_accent: Hsla,
// Status
pub status_success: Hsla,
pub status_error: Hsla,
// ... and more
}
/// UI surface types for consistent styling
pub enum Surface {
App, Sidebar, Panel, Input, Elevated, ListItem, Header
}
Usage Patterns
Loading the Theme
use crate::theme::{load_theme, Theme};
// Loads from ~/.scriptkit/kit/theme.json, falls back to defaults
let theme = load_theme();
// Get colors based on window focus state
let colors = theme.get_colors(is_focused);
// Get opacity settings (auto-reduced by 10% when unfocused)
let opacity = theme.get_opacity_for_focus(is_focused);
Using Theme in Components
// Get background with proper opacity for vibrancy
let (r, g, b, a) = theme.background_rgba(BackgroundRole::Main, is_focused);
div().bg(rgba(r, g, b, a))
// Use semantic colors
let semantic = SemanticColors::dark();
div()
.bg(semantic.bg_primary)
.text_color(semantic.text_primary)
.border_color(semantic.border_default)
Focus-Aware Styling
use crate::theme::semantic::FocusAware;
let colors = FocusAware::new(
SemanticColors::dark(),
SemanticColors::dark().dimmed(),
);
// In render, pick based on window focus
let current = colors.for_focus(window.is_focused());
div().bg(current.bg_primary)
Lightweight Helpers for Render Closures
use crate::theme::helpers::{ListItemColors, InputFieldColors};
// Pre-compute colors (Copy type, no heap allocation)
let list_colors = theme.colors.list_item_colors();
// Use in render closure without cloning full theme
.child(list_item.map(move |item| {
div()
.bg(if selected { list_colors.background_selected } else { list_colors.background })
.text_color(list_colors.text)
}))
Integration
GPUI-Component Theme Sync (gpui_integration.rs)
The theme syncs with gpui-component's global ThemeColor:
use crate::theme::sync_gpui_component_theme;
// Call after gpui_component::init(cx) in main.rs
// Also called automatically when theme.json changes
sync_gpui_component_theme(cx);
This maps Script Kit colors to gpui-component tokens: background, foreground, accent, primary, secondary, muted, list_*, sidebar_*, etc.
Theme Service (service.rs)
Global file watcher that hot-reloads theme changes:
use crate::theme::service::{ensure_theme_service, theme_revision};
// Call once at app startup
ensure_theme_service(cx);
// Check theme revision for cache invalidation
let rev = theme_revision();
if self.cached_rev != rev {
self.cached_rev = rev;
self.recompute_styles();
}
Key functions:
ensure_theme_service(cx)- Start the global theme watcher (idempotent)theme_revision()- Get current revision number for cache invalidationis_theme_service_running()- Check if service is active (debug/testing)
Theme Validation (validation.rs)
Validate theme JSON before loading:
use crate::theme::validation::validate_theme_json;
let json: Value = serde_json::from_str(&contents)?;
let diagnostics = validate_theme_json(&json);
if diagnostics.has_errors() {
eprintln!("{}", diagnostics.format_for_log());
}
Example theme.json
{
"colors": {
"background": {
"main": "#1E1E1E",
"title_bar": "#2D2D30",
"search_box": "#3C3C3C",
"log_panel": "#0D0D0D"
},
"text": {
"primary": "#FFFFFF",
"secondary": "#CCCCCC",
"tertiary": "#999999",
"muted": "#808080",
"dimmed": "#666666"
},
"accent": {
"selected": "#FBBF24",
"selected_subtle": "#2A2A2A"
},
"ui": {
"border": "#464647",
"success": "#00FF00",
"error": "#EF4444",
"warning": "#F59E0B",
"info": "#3B82F6"
}
},
"opacity": {
"main": 0.30,
"title_bar": 0.30,
"selected": 0.15,
"hover": 0.08
},
"vibrancy": {
"enabled": true,
"material": "popover"
},
"fonts": {
"mono_family": "JetBrains Mono",
"mono_size": 16.0,
"ui_family": ".SystemUIFont",
"ui_size": 16.0
}
}
Anti-patterns
DON'T: Clone full theme into render closures
// Bad - heap allocation on every render
let theme_clone = theme.clone();
.child(items.map(move |item| {
let colors = theme_clone.colors.clone(); // Expensive!
...
}))
DO: Use lightweight helper structs:
// Good - Copy type, stack allocated
let list_colors = theme.colors.list_item_colors();
.child(items.map(move |item| {
div().bg(list_colors.background_selected) // Zero-cost
}))
DON'T: Apply opacity to UI element colors
// Bad - makes text unreadable
let bg = hex_to_hsla(colors.background.main);
let bg_with_opacity = hsla(bg.h, bg.s, bg.l, 0.3); // Wrong!
DO: Only use opacity for window-level vibrancy:
// Good - opacity is for window background, not UI elements
let main_bg = if vibrancy_enabled {
with_vibrancy(colors.background.main, 0.37) // Tuned for vibrancy
} else {
hex_to_hsla(colors.background.main) // Opaque when no vibrancy
};
DON'T: Hardcode colors
// Bad
div().bg(rgb(0x1e1e1e))
DO: Use theme tokens:
// Good
let colors = theme.get_colors(is_focused);
div().bg(hex_to_hsla(colors.background.main))
DON'T: Ignore focus state
// Bad - always same appearance
let colors = theme.colors.clone();
DO: Respect window focus:
// Good - dims when unfocused
let colors = theme.get_colors(cx.is_window_focused());
DON'T: Start multiple theme watchers
// Bad - creates duplicate watchers per window
fn setup_window() {
start_theme_watcher(cx); // Each window starts its own!
}
DO: Use the global theme service:
// Good - single watcher, broadcasts to all windows
fn main() {
ensure_theme_service(cx); // Once at startup
}
File Structure
src/theme/
mod.rs - Module exports and re-exports
types.rs - Theme, ColorScheme, BackgroundOpacity, VibrancySettings, FontConfig, load_theme()
hex_color.rs - HexColor type and serde support
semantic.rs - FocusAware<T>, SemanticColors, Surface, SurfaceStyle
helpers.rs - ListItemColors, InputFieldColors (Copy types for render closures)
gpui_integration.rs - sync_gpui_component_theme()
service.rs - Global theme watcher service (ensure_theme_service, theme_revision)
validation.rs - Theme JSON validation with diagnostics
theme_tests.rs - Unit tests
validation_tests.rs - Validation-specific tests
Recommended Agent Skills
Expand your agent's capabilities with these related and highly-rated skills.
Generate Component Documentation
Based on existing docs styles and specific API implementations, and referencing same name stories, generate comprehensive documentation for the new component.
Generate Component Story
Generate a comprehensive story for a new component for as example.
new-component
How to write a new component of GPUI Component.
troubleshooting
Diagnose and fix common Script Kit issues. Use when the user reports bugs, crashes, missing features, or unexpected behavior in Script Kit GPUI.
script-authoring
Create and manage TypeScript scripts for Script Kit. Use when the user wants to write a new script, edit an existing script, or understand Script Kit's SDK and metadata system.
agents
Create mdflow-backed agent files for Script Kit. Use when the user wants to create AI agents, configure agent backends (Claude, Gemini, Codex), or manage agent metadata.
Didn't find tool you were looking for?