Theme System Integration Guide
This document explains how to integrate the theme system with Scarab’s daemon and client.
Architecture Overview
┌─────────────────┐
│ scarab-daemon │
│ │
│ ThemePlugin │ ◄─── Handles theme commands
│ ThemeManager │ Manages theme state
└─────────────────┘
│
│ IPC (RemoteCommand)
▼
┌─────────────────┐
│ scarab-client │
│ │
│ ThemeUI │ ◄─── Bevy UI components
│ Theme Preview │ Live preview overlay
└─────────────────┘
Daemon Integration
1. Load Plugin
In scarab-daemon/src/main.rs:
use scarab_themes::ThemePlugin;
// In plugin initialization
let theme_plugin = Box::new(ThemePlugin::new());
plugin_manager.register_plugin(theme_plugin)?;2. Handle Theme Changes
The plugin automatically handles theme commands via on_remote_command. When a theme is applied, it should:
- Update the config
- Notify all connected clients
- Persist the change
TODO: Extend RemoteCommand enum to support theme updates:
// In scarab-protocol/src/lib.rs
pub enum RemoteCommand {
// ... existing variants ...
ThemeChanged {
theme_id: String,
colors: ColorConfig,
},
}Client Integration (Bevy UI)
1. Theme Selector UI
Create a Bevy system to display theme selection:
// In scarab-client/src/ui/theme_selector.rs
use bevy::prelude::*;
use scarab_themes::ThemeManager;
#[derive(Component)]
pub struct ThemeSelectorUI;
pub fn spawn_theme_selector(
mut commands: Commands,
manager: Res<ThemeManager>,
asset_server: Res<AssetServer>,
) {
commands
.spawn(NodeBundle {
style: Style {
width: Val::Percent(100.0),
height: Val::Percent(100.0),
position_type: PositionType::Absolute,
justify_content: JustifyContent::Center,
align_items: AlignItems::Center,
..default()
},
background_color: Color::rgba(0.0, 0.0, 0.0, 0.8).into(),
..default()
})
.with_children(|parent| {
// Modal container
parent
.spawn(NodeBundle {
style: Style {
width: Val::Px(600.0),
height: Val::Px(400.0),
flex_direction: FlexDirection::Column,
padding: UiRect::all(Val::Px(20.0)),
..default()
},
background_color: Color::rgb(0.15, 0.15, 0.15).into(),
..default()
})
.with_children(|modal| {
// Title
modal.spawn(TextBundle::from_section(
"Select Theme",
TextStyle {
font: asset_server.load("fonts/FiraSans-Bold.ttf"),
font_size: 24.0,
color: Color::WHITE,
},
));
// Theme list (scrollable)
for theme in manager.all_themes() {
spawn_theme_item(modal, theme, &asset_server);
}
});
})
.insert(ThemeSelectorUI);
}
fn spawn_theme_item(
parent: &mut ChildBuilder,
theme: &Theme,
asset_server: &AssetServer,
) {
parent
.spawn(ButtonBundle {
style: Style {
width: Val::Percent(100.0),
height: Val::Px(60.0),
margin: UiRect::all(Val::Px(5.0)),
padding: UiRect::all(Val::Px(10.0)),
justify_content: JustifyContent::SpaceBetween,
align_items: AlignItems::Center,
..default()
},
background_color: Color::rgb(0.2, 0.2, 0.2).into(),
..default()
})
.with_children(|button| {
// Theme name and description
button.spawn(TextBundle::from_section(
theme.name(),
TextStyle {
font: asset_server.load("fonts/FiraSans-Regular.ttf"),
font_size: 18.0,
color: Color::WHITE,
},
));
// Color preview swatches
spawn_color_preview(button, theme);
});
}
fn spawn_color_preview(parent: &mut ChildBuilder, theme: &Theme) {
parent
.spawn(NodeBundle {
style: Style {
width: Val::Px(120.0),
height: Val::Px(40.0),
flex_direction: FlexDirection::Row,
gap: Val::Px(2.0),
..default()
},
..default()
})
.with_children(|container| {
// Show 8 color swatches
let colors = [
&theme.colors.palette.red,
&theme.colors.palette.green,
&theme.colors.palette.yellow,
&theme.colors.palette.blue,
&theme.colors.palette.magenta,
&theme.colors.palette.cyan,
&theme.colors.foreground,
&theme.colors.background,
];
for color_hex in colors {
let color = hex_to_color(color_hex);
container.spawn(NodeBundle {
style: Style {
width: Val::Px(15.0),
height: Val::Px(40.0),
..default()
},
background_color: color.into(),
..default()
});
}
});
}
// Helper to convert hex to Bevy Color
fn hex_to_color(hex: &str) -> Color {
let hex = hex.trim_start_matches('#');
let r = u8::from_str_radix(&hex[0..2], 16).unwrap_or(0) as f32 / 255.0;
let g = u8::from_str_radix(&hex[2..4], 16).unwrap_or(0) as f32 / 255.0;
let b = u8::from_str_radix(&hex[4..6], 16).unwrap_or(0) as f32 / 255.0;
Color::rgb(r, g, b)
}2. Live Preview System
Create a system to handle live preview:
// In scarab-client/src/ui/theme_preview.rs
use bevy::prelude::*;
#[derive(Resource)]
pub struct ThemePreview {
pub active: bool,
pub overlay_alpha: f32,
}
pub fn update_theme_preview(
mut preview: ResMut<ThemePreview>,
time: Res<Time>,
keyboard: Res<Input<KeyCode>>,
) {
// Fade in/out preview overlay
if preview.active {
preview.overlay_alpha = (preview.overlay_alpha + time.delta_seconds() * 2.0).min(1.0);
} else {
preview.overlay_alpha = (preview.overlay_alpha - time.delta_seconds() * 2.0).max(0.0);
}
// Cancel preview on Escape
if keyboard.just_pressed(KeyCode::Escape) {
preview.active = false;
}
}
pub fn render_preview_overlay(
mut commands: Commands,
preview: Res<ThemePreview>,
query: Query<Entity, With<ThemePreviewOverlay>>,
) {
if preview.overlay_alpha > 0.0 {
// Show preview banner
if query.is_empty() {
commands.spawn((
NodeBundle {
style: Style {
width: Val::Percent(100.0),
height: Val::Px(40.0),
position_type: PositionType::Absolute,
top: Val::Px(0.0),
justify_content: JustifyContent::Center,
align_items: AlignItems::Center,
..default()
},
background_color: Color::rgba(0.0, 0.5, 1.0, preview.overlay_alpha * 0.8).into(),
..default()
},
ThemePreviewOverlay,
));
}
} else {
// Remove preview banner
for entity in query.iter() {
commands.entity(entity).despawn_recursive();
}
}
}
#[derive(Component)]
struct ThemePreviewOverlay;3. Add to Client Main
In scarab-client/src/main.rs:
mod ui {
pub mod theme_selector;
pub mod theme_preview;
}
use ui::{theme_selector, theme_preview};
use scarab_themes::ThemeManager;
fn main() {
let mut app = App::new();
// Initialize theme manager
let mut theme_manager = ThemeManager::new();
theme_manager.initialize().expect("Failed to init themes");
app
.add_plugins(DefaultPlugins)
.insert_resource(theme_manager)
.insert_resource(theme_preview::ThemePreview {
active: false,
overlay_alpha: 0.0,
})
.add_systems(Update, theme_preview::update_theme_preview)
.add_systems(Update, theme_preview::render_preview_overlay)
.run();
}TODO List
High Priority
- Extend
RemoteCommandenum to includeThemeChangedvariant - Implement IPC message passing for theme updates
- Create Bevy UI components for theme selector
- Add live preview system with smooth transitions
- Persist theme selection to config file
Medium Priority
- Add keyboard shortcuts for quick theme switching (e.g., Ctrl+T)
- Implement theme preview with sample terminal output
- Add “favorite themes” feature
- Create theme carousel/slider UI
- Add theme search/filter by tags
Low Priority
- Add theme auto-switch based on time of day
- Implement theme randomizer
- Add theme color extraction from wallpaper
- Create theme sharing/marketplace integration
- Add theme animation transitions
Testing
Test the integration:
# Build the workspace
cargo build -p scarab-themes
# Run tests
cargo test -p scarab-themes
# Run integration tests
cargo test -p scarab-themes --test integration_test
# Check formatting
cargo fmt -p scarab-themes -- --check
# Run clippy
cargo clippy -p scarab-themesExample Theme Files
See examples/custom-theme.toml for a complete example of creating custom themes.
Example Base16 import:
# Download Base16 theme
curl -o ~/.config/scarab/themes/ocean.yaml \
https://raw.githubusercontent.com/chriskempson/base16-schemes/master/ocean.yaml
# Import via command palette
# Open: Ctrl+Shift+P
# Type: "Theme: Import"
# Select: ocean.yamlPerformance Considerations
-
Theme Loading: All themes are loaded at startup. For 13 themes, this adds ~10ms to startup time.
-
Live Preview: Preview updates trigger full terminal re-render. Use debouncing to limit update frequency.
-
IPC: Theme changes are sent via IPC. Consider batching if multiple theme operations occur rapidly.
-
Memory: Each theme uses ~2KB. Total memory overhead is minimal (<50KB for all themes).