use crate::{
output::{ActivityKind, ActivityStatus},
rendering::DisplayRole,
};
use ratatui::style::{Color, Modifier, Style};
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub(crate) struct MissionControlTheme {
pub(crate) background: Color,
pub(crate) panel: Color,
pub(crate) panel_alt: Color,
pub(crate) selection: Color,
pub(crate) border_dark: Color,
pub(crate) green_dim: Color,
pub(crate) green_muted: Color,
pub(crate) green: Color,
pub(crate) green_bright: Color,
pub(crate) green_neon: Color,
pub(crate) bash_yellow: Color,
pub(crate) bash_yellow_dim: Color,
pub(crate) mint: Color,
pub(crate) text: Color,
pub(crate) success_bg: Color,
pub(crate) pending_bg: Color,
pub(crate) error_bg: Color,
pub(crate) cyan_mint: Color,
pub(crate) lavender_cool: Color,
}
pub(crate) const MATRIX_GREEN: MissionControlTheme = MissionControlTheme {
background: Color::Reset,
panel: Color::Rgb(0x00, 0x1a, 0x0a),
panel_alt: Color::Rgb(0x00, 0x24, 0x0d),
selection: Color::Rgb(0x00, 0x3d, 0x18),
border_dark: Color::Rgb(0x2b, 0x7a, 0x3e),
green_dim: Color::Rgb(0x60, 0xb8, 0x75),
green_muted: Color::Rgb(0x77, 0xc8, 0x8a),
green: Color::Rgb(0x00, 0xd1, 0x5f),
green_bright: Color::Rgb(0x00, 0xff, 0x66),
green_neon: Color::Rgb(0x66, 0xff, 0x99),
bash_yellow: Color::Rgb(0xff, 0xd7, 0x5f),
bash_yellow_dim: Color::Rgb(0xb8, 0x94, 0x3a),
mint: Color::Rgb(0xb8, 0xff, 0xd0),
text: Color::Rgb(0xd9, 0xff, 0xe5),
success_bg: Color::Rgb(0x00, 0x26, 0x0f),
pending_bg: Color::Rgb(0x00, 0x1f, 0x0c),
error_bg: Color::Rgb(0x06, 0x1d, 0x22),
cyan_mint: Color::Rgb(0x8f, 0xff, 0xd2),
lavender_cool: Color::Rgb(0xd6, 0xcc, 0xff),
};
impl MissionControlTheme {
pub(crate) const fn app(self) -> Style {
Style::new().fg(self.text).bg(self.background)
}
pub(crate) const fn pane(self) -> Style {
Style::new().fg(self.text).bg(self.background)
}
pub(crate) const fn panel_alt(self) -> Style {
Style::new().fg(self.text).bg(self.panel_alt)
}
pub(crate) const fn overlay_panel(self) -> Style {
Style::new().fg(self.text).bg(self.panel_alt)
}
pub(crate) const fn border(self, active: bool) -> Style {
Style::new()
.fg(if active {
self.green_bright
} else {
self.border_dark
})
.bg(self.background)
}
pub(crate) const fn bash_border(self, active: bool) -> Style {
Style::new()
.fg(if active {
self.bash_yellow
} else {
self.bash_yellow_dim
})
.bg(self.background)
}
pub(crate) const fn bash_title(self, active: bool) -> Style {
Style::new()
.fg(if active {
self.bash_yellow
} else {
self.bash_yellow_dim
})
.bg(self.background)
.add_modifier(Modifier::BOLD)
}
pub(crate) const fn bash_accent(self, _active: bool) -> Style {
Style::new().fg(self.bash_yellow).bg(self.background)
}
pub(crate) const fn border_alt(self) -> Style {
Style::new().fg(self.border_dark).bg(self.panel_alt)
}
pub(crate) const fn title(self, active: bool) -> Style {
Style::new()
.fg(if active {
self.green_bright
} else {
self.green_muted
})
.bg(self.background)
.add_modifier(Modifier::BOLD)
}
pub(crate) const fn title_alt(self) -> Style {
Style::new()
.fg(self.green_neon)
.bg(self.panel_alt)
.add_modifier(Modifier::BOLD)
}
pub(crate) const fn muted(self, _active: bool) -> Style {
Style::new().fg(self.green_muted).bg(self.background)
}
pub(crate) const fn dim(self, _active: bool) -> Style {
Style::new().fg(self.green_dim).bg(self.background)
}
pub(crate) const fn accent(self, _active: bool) -> Style {
Style::new().fg(self.green_bright).bg(self.background)
}
pub(crate) const fn transcript_user_body(self) -> Style {
Style::new().fg(self.mint).bg(self.background)
}
pub(crate) const fn transcript_tool_body(self) -> Style {
Style::new()
.fg(self.cyan_mint)
.bg(self.background)
.add_modifier(Modifier::DIM)
}
pub(crate) const fn transcript_session_body(self) -> Style {
Style::new()
.fg(self.green_muted)
.bg(self.background)
.add_modifier(Modifier::DIM)
}
pub(crate) const fn transcript_error_label(self) -> Style {
Style::new()
.fg(self.lavender_cool)
.bg(self.error_bg)
.add_modifier(Modifier::BOLD)
}
pub(crate) const fn transcript_error_body(self) -> Style {
Style::new()
.fg(self.lavender_cool)
.bg(self.error_bg)
.add_modifier(Modifier::DIM)
}
pub(crate) const fn selection(self) -> Style {
Style::new().fg(self.text).bg(self.selection)
}
pub(crate) const fn cursor(self, _active: bool) -> Style {
Style::new()
.fg(self.green_neon)
.bg(self.background)
.add_modifier(Modifier::BOLD)
}
pub(crate) const fn status_line(self) -> Style {
Style::new().fg(self.green_muted).bg(self.background)
}
pub(crate) fn status(self, status: ActivityStatus, selected: bool, active: bool) -> Style {
let bg = if selected && active {
self.selection
} else {
self.background
};
match status {
ActivityStatus::Queued => Style::new().fg(self.green_dim).bg(bg),
ActivityStatus::Running | ActivityStatus::Writing => Style::new()
.fg(self.green_bright)
.bg(bg)
.add_modifier(Modifier::BOLD),
ActivityStatus::Success => Style::new().fg(self.green_neon).bg(bg),
ActivityStatus::Failed => Style::new()
.fg(self.lavender_cool)
.bg(if active && !selected {
self.error_bg
} else {
bg
})
.add_modifier(Modifier::BOLD),
ActivityStatus::Canceled => Style::new()
.fg(self.green_dim)
.bg(bg)
.add_modifier(Modifier::DIM),
}
}
pub(crate) fn kind(self, kind: &ActivityKind, selected: bool, active: bool) -> Style {
let bg = if selected && active {
self.selection
} else {
self.background
};
let fg = match kind {
ActivityKind::Tool => self.green_bright,
ActivityKind::Hook => self.green_muted,
ActivityKind::ProviderContextInjection => self.green_dim,
ActivityKind::SubagentBatch => self.cyan_mint,
ActivityKind::SubagentTask => self.mint,
ActivityKind::Assistant => self.green_neon,
ActivityKind::Diagnostic => self.lavender_cool,
ActivityKind::Compaction => self.cyan_mint,
};
Style::new().fg(fg).bg(bg)
}
pub(crate) fn label(self, selected: bool, active: bool) -> Style {
if selected && active {
self.selection()
} else {
self.pane()
}
}
pub(crate) const fn display_role(self, role: DisplayRole) -> Style {
match role {
DisplayRole::Plain => self.pane(),
DisplayRole::Heading => Style::new()
.fg(self.green_neon)
.bg(self.background)
.add_modifier(Modifier::BOLD),
DisplayRole::ListMarker => Style::new()
.fg(self.green_bright)
.bg(self.background)
.add_modifier(Modifier::BOLD),
DisplayRole::BlockQuote => Style::new().fg(self.green_muted).bg(self.background),
DisplayRole::InlineCode => Style::new().fg(self.cyan_mint).bg(self.panel),
DisplayRole::CodeFence | DisplayRole::CodeLanguageLabel => Style::new()
.fg(self.green_dim)
.bg(self.panel)
.add_modifier(Modifier::DIM),
DisplayRole::CodeBlockGutter => Style::new()
.fg(self.green_dim)
.bg(self.panel)
.add_modifier(Modifier::DIM),
DisplayRole::Keyword => Style::new()
.fg(self.green_bright)
.bg(self.panel)
.add_modifier(Modifier::BOLD),
DisplayRole::String => Style::new().fg(self.mint).bg(self.panel),
DisplayRole::Comment => Style::new()
.fg(self.green_dim)
.bg(self.panel)
.add_modifier(Modifier::ITALIC),
DisplayRole::Number => Style::new().fg(self.cyan_mint).bg(self.panel),
DisplayRole::Function => Style::new().fg(self.green_neon).bg(self.panel),
DisplayRole::Type => Style::new().fg(self.lavender_cool).bg(self.panel),
DisplayRole::Operator | DisplayRole::Punctuation => {
Style::new().fg(self.green_muted).bg(self.panel)
}
DisplayRole::FallbackCode => Style::new().fg(self.text).bg(self.panel),
DisplayRole::DiffInserted => Style::new().fg(self.green_neon).bg(self.success_bg),
DisplayRole::DiffRemoved => Style::new().fg(self.lavender_cool).bg(self.error_bg),
DisplayRole::DiffChanged => Style::new().fg(self.cyan_mint).bg(self.pending_bg),
DisplayRole::DiffContext => Style::new().fg(self.text).bg(self.background),
DisplayRole::DiffHunkHeader => Style::new()
.fg(self.cyan_mint)
.bg(self.background)
.add_modifier(Modifier::BOLD),
DisplayRole::DiffFileHeader => Style::new()
.fg(self.green_bright)
.bg(self.background)
.add_modifier(Modifier::BOLD),
DisplayRole::DiffMetadata => Style::new().fg(self.green_dim).bg(self.background),
}
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn matrix_green_contains_all_prd_palette_tokens() {
let theme = MATRIX_GREEN;
assert_eq!(theme.background, Color::Reset);
assert_eq!(theme.panel, Color::Rgb(0x00, 0x1a, 0x0a));
assert_eq!(theme.panel_alt, Color::Rgb(0x00, 0x24, 0x0d));
assert_eq!(theme.selection, Color::Rgb(0x00, 0x3d, 0x18));
assert_eq!(theme.border_dark, Color::Rgb(0x2b, 0x7a, 0x3e));
assert_eq!(theme.green_dim, Color::Rgb(0x60, 0xb8, 0x75));
assert_eq!(theme.green_muted, Color::Rgb(0x77, 0xc8, 0x8a));
assert_eq!(theme.green, Color::Rgb(0x00, 0xd1, 0x5f));
assert_eq!(theme.green_bright, Color::Rgb(0x00, 0xff, 0x66));
assert_eq!(theme.green_neon, Color::Rgb(0x66, 0xff, 0x99));
assert_eq!(theme.mint, Color::Rgb(0xb8, 0xff, 0xd0));
assert_eq!(theme.text, Color::Rgb(0xd9, 0xff, 0xe5));
assert_eq!(theme.success_bg, Color::Rgb(0x00, 0x26, 0x0f));
assert_eq!(theme.pending_bg, Color::Rgb(0x00, 0x1f, 0x0c));
assert_eq!(theme.error_bg, Color::Rgb(0x06, 0x1d, 0x22));
assert_eq!(theme.cyan_mint, Color::Rgb(0x8f, 0xff, 0xd2));
assert_eq!(theme.lavender_cool, Color::Rgb(0xd6, 0xcc, 0xff));
}
}