use crate::{
appearance::{self, ColorDepth, RuntimeAppearance},
output::ActivityStatus,
rendering::DisplayRole,
};
use opaline::{OpalineColor, OpalineStyle};
use ratatui::style::{Color, Modifier, Style};
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
struct SurfaceTheme {
background: Color,
panel: Color,
panel_alt: Color,
overlay: Color,
code: Color,
selection: Color,
success: Color,
pending: Color,
error: Color,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
struct TextTheme {
primary: Color,
muted: Color,
dim: Color,
accent: Color,
title: Color,
prompt: Color,
status: Color,
help: Color,
metadata: Color,
command: Color,
command_active: Color,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
struct BorderTheme {
default: Color,
active: Color,
alternate: Color,
command: Color,
command_active: Color,
error: Color,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
struct ActivityTheme {
queued: Color,
running: Color,
writing: Color,
success: Color,
failed: Color,
canceled: Color,
tool: Color,
subagent: Color,
assistant: Color,
diagnostic: Color,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
struct TranscriptTheme {
user: Color,
assistant: Color,
thinking: Color,
tool: Color,
session: Color,
diagnostic: Color,
heading: Color,
section: Color,
error_label: Color,
error_body: Color,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
struct SyntaxTheme {
plain: Color,
keyword: Color,
string: Color,
comment: Color,
number: Color,
function: Color,
type_: Color,
operator: Color,
punctuation: Color,
code_fence: Color,
language_label: Color,
gutter: Color,
fallback: Color,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
struct DiffTheme {
inserted: Color,
removed: Color,
changed: Color,
context: Color,
hunk_header: Color,
file_header: Color,
metadata: Color,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
struct SemanticStyles {
keyword: Style,
line_number: Style,
selected: Style,
active_selected: Style,
focused_border: Style,
unfocused_border: Style,
success: Style,
error: Style,
warning: Style,
info: Style,
dimmed: Style,
muted: Style,
inline_code: Style,
heading: Style,
user_heading: Style,
assistant_heading: Style,
thinking_heading: Style,
tool_heading: Style,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
struct FastTheme {
colors: [Color; 7],
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub(crate) struct MissionControlTheme {
surfaces: SurfaceTheme,
text: TextTheme,
borders: BorderTheme,
activity: ActivityTheme,
transcript: TranscriptTheme,
syntax: SyntaxTheme,
diff: DiffTheme,
styles: SemanticStyles,
fast: FastTheme,
revision: u64,
}
impl Default for MissionControlTheme {
fn default() -> Self {
Self::from_runtime_appearance(&RuntimeAppearance::default(), 0)
}
}
impl MissionControlTheme {
pub(crate) fn from_runtime_appearance(appearance: &RuntimeAppearance, revision: u64) -> Self {
let roles = &appearance.theme.roles;
let depth = appearance.policy.depth;
let lower = |color: OpalineColor| lower_color(color, depth);
let fallback_background = roles.surface_background;
let fallback_code = roles.surface_code;
let styles = &roles.standard_styles;
let styles = SemanticStyles {
keyword: project_opaline_style(
&styles.keyword,
roles.syntax_keyword,
fallback_code,
depth,
),
line_number: project_opaline_style(
&styles.line_number,
roles.syntax_gutter,
fallback_code,
depth,
),
selected: project_opaline_style(
&styles.selected,
roles.text_primary,
roles.surface_selection,
depth,
),
active_selected: project_opaline_style(
&styles.active_selected,
roles.text_primary,
roles.surface_selection,
depth,
),
focused_border: project_opaline_style(
&styles.focused_border,
roles.border_active,
fallback_background,
depth,
),
unfocused_border: project_opaline_style(
&styles.unfocused_border,
roles.border_default,
fallback_background,
depth,
),
success: project_opaline_style(
&styles.success,
roles.status_success,
fallback_background,
depth,
),
error: project_opaline_style(
&styles.error,
roles.status_failed,
roles.surface_error,
depth,
),
warning: project_opaline_style(
&styles.warning,
roles.status_running,
fallback_background,
depth,
),
info: project_opaline_style(
&styles.info,
roles.status_diagnostic,
fallback_background,
depth,
),
dimmed: project_opaline_style(
&styles.dimmed,
roles.text_dim,
fallback_background,
depth,
),
muted: project_opaline_style(
&styles.muted,
roles.text_muted,
fallback_background,
depth,
),
inline_code: project_opaline_style(
&styles.inline_code,
roles.syntax_plain,
fallback_code,
depth,
),
heading: project_opaline_style(
&roles.heading_style,
roles.transcript_heading,
fallback_background,
depth,
),
user_heading: project_opaline_style(
&roles.user_heading_style,
roles.transcript_user,
fallback_background,
depth,
),
assistant_heading: project_opaline_style(
&roles.assistant_heading_style,
roles.transcript_assistant,
fallback_background,
depth,
),
thinking_heading: project_opaline_style(
&roles.thinking_heading_style,
roles.transcript_thinking,
fallback_background,
depth,
),
tool_heading: project_opaline_style(
&roles.tool_heading_style,
roles.transcript_tool,
fallback_background,
depth,
),
};
let fast_colors = roles.gradients.fast.generate(7);
let mut fast = [Color::Reset; 7];
for (index, color) in fast_colors.into_iter().enumerate() {
fast[index] = lower(color);
}
Self {
surfaces: SurfaceTheme {
background: lower(roles.surface_background),
panel: lower(roles.surface_panel),
panel_alt: lower(roles.surface_panel_alt),
overlay: lower(roles.surface_overlay),
code: lower(roles.surface_code),
selection: lower(roles.surface_selection),
success: lower(roles.surface_success),
pending: lower(roles.surface_pending),
error: lower(roles.surface_error),
},
text: TextTheme {
primary: lower(roles.text_primary),
muted: lower(roles.text_muted),
dim: lower(roles.text_dim),
accent: lower(roles.text_accent),
title: lower(roles.text_title),
prompt: lower(roles.text_prompt),
status: lower(roles.text_status),
help: lower(roles.text_help),
metadata: lower(roles.text_metadata),
command: lower(roles.text_command),
command_active: lower(roles.text_command_active),
},
borders: BorderTheme {
default: lower(roles.border_default),
active: lower(roles.border_active),
alternate: lower(roles.border_alternate),
command: lower(roles.border_command),
command_active: lower(roles.border_command_active),
error: lower(roles.border_error),
},
activity: ActivityTheme {
queued: lower(roles.status_queued),
running: lower(roles.status_running),
writing: lower(roles.status_writing),
success: lower(roles.status_success),
failed: lower(roles.status_failed),
canceled: lower(roles.status_canceled),
tool: lower(roles.status_tool),
subagent: lower(roles.status_subagent),
assistant: lower(roles.status_assistant),
diagnostic: lower(roles.status_diagnostic),
},
transcript: TranscriptTheme {
user: lower(roles.transcript_user),
assistant: lower(roles.transcript_assistant),
thinking: lower(roles.transcript_thinking),
tool: lower(roles.transcript_tool),
session: lower(roles.transcript_session),
diagnostic: lower(roles.transcript_diagnostic),
heading: lower(roles.transcript_heading),
section: lower(roles.transcript_section),
error_label: lower(roles.transcript_error_label),
error_body: lower(roles.transcript_error_body),
},
syntax: SyntaxTheme {
plain: lower(roles.syntax_plain),
keyword: lower(roles.syntax_keyword),
string: lower(roles.syntax_string),
comment: lower(roles.syntax_comment),
number: lower(roles.syntax_number),
function: lower(roles.syntax_function),
type_: lower(roles.syntax_type),
operator: lower(roles.syntax_operator),
punctuation: lower(roles.syntax_punctuation),
code_fence: lower(roles.syntax_code_fence),
language_label: lower(roles.syntax_language_label),
gutter: lower(roles.syntax_gutter),
fallback: lower(roles.syntax_fallback),
},
diff: DiffTheme {
inserted: lower(roles.diff_inserted),
removed: lower(roles.diff_removed),
changed: lower(roles.diff_changed),
context: lower(roles.diff_context),
hunk_header: lower(roles.diff_hunk_header),
file_header: lower(roles.diff_file_header),
metadata: lower(roles.diff_metadata),
},
styles,
fast: FastTheme { colors: fast },
revision,
}
}
pub(crate) fn revision(self) -> u64 {
self.revision
}
pub(crate) fn presentation_eq(self, other: Self) -> bool {
let mut left = self;
let mut right = other;
left.revision = 0;
right.revision = 0;
left == right
}
pub(crate) fn fast_mode_rainbow_len(self) -> usize {
self.fast.colors.len()
}
pub(crate) fn fast_mode_rainbow_color(self, index: usize) -> Color {
self.fast.colors[index % self.fast.colors.len()]
}
pub(crate) const fn surface_background(self) -> Color {
self.surfaces.background
}
pub(crate) const fn surface_panel(self) -> Color {
self.surfaces.panel
}
pub(crate) const fn surface_panel_alt(self) -> Color {
self.surfaces.panel_alt
}
pub(crate) const fn surface_overlay(self) -> Color {
self.surfaces.overlay
}
pub(crate) const fn surface_code(self) -> Color {
self.surfaces.code
}
pub(crate) const fn surface_selection(self) -> Color {
self.surfaces.selection
}
pub(crate) const fn surface_success(self) -> Color {
self.surfaces.success
}
pub(crate) const fn surface_pending(self) -> Color {
self.surfaces.pending
}
pub(crate) const fn surface_error(self) -> Color {
self.surfaces.error
}
pub(crate) const fn text_primary(self) -> Color {
self.text.primary
}
pub(crate) const fn text_muted(self) -> Color {
self.text.muted
}
pub(crate) const fn text_dim(self) -> Color {
self.text.dim
}
pub(crate) const fn text_accent(self) -> Color {
self.text.accent
}
pub(crate) const fn text_title(self) -> Color {
self.text.title
}
pub(crate) const fn text_status(self) -> Color {
self.text.status
}
pub(crate) const fn text_command(self) -> Color {
self.text.command
}
pub(crate) const fn text_command_active(self) -> Color {
self.text.command_active
}
pub(crate) const fn border_active(self) -> Color {
self.borders.active
}
pub(crate) const fn border_alternate(self) -> Color {
self.borders.alternate
}
pub(crate) const fn border_command(self) -> Color {
self.borders.command
}
pub(crate) const fn border_command_active(self) -> Color {
self.borders.command_active
}
pub(crate) const fn activity_status_color(self, status: ActivityStatus) -> Color {
match status {
ActivityStatus::Queued => self.activity.queued,
ActivityStatus::Running => self.activity.running,
ActivityStatus::Writing => self.activity.writing,
ActivityStatus::Success => self.activity.success,
ActivityStatus::Failed => self.activity.failed,
ActivityStatus::Canceled => self.activity.canceled,
}
}
pub(crate) const fn transcript_user_color(self) -> Color {
self.transcript.user
}
pub(crate) const fn transcript_tool_color(self) -> Color {
self.transcript.tool
}
pub(crate) const fn transcript_session_color(self) -> Color {
self.transcript.session
}
pub(crate) const fn transcript_error_label_color(self) -> Color {
self.transcript.error_label
}
pub(crate) const fn transcript_error_body_color(self) -> Color {
self.transcript.error_body
}
pub(crate) const fn syntax_string_color(self) -> Color {
self.syntax.string
}
pub(crate) const fn syntax_comment_color(self) -> Color {
self.syntax.comment
}
pub(crate) const fn syntax_number_color(self) -> Color {
self.syntax.number
}
pub(crate) const fn syntax_function_color(self) -> Color {
self.syntax.function
}
pub(crate) const fn syntax_type_color(self) -> Color {
self.syntax.type_
}
pub(crate) const fn syntax_operator_color(self) -> Color {
self.syntax.operator
}
pub(crate) const fn syntax_punctuation_color(self) -> Color {
self.syntax.punctuation
}
pub(crate) const fn syntax_code_fence_color(self) -> Color {
self.syntax.code_fence
}
pub(crate) const fn syntax_language_label_color(self) -> Color {
self.syntax.language_label
}
pub(crate) const fn syntax_fallback_color(self) -> Color {
self.syntax.fallback
}
pub(crate) const fn diff_inserted_color(self) -> Color {
self.diff.inserted
}
pub(crate) const fn diff_removed_color(self) -> Color {
self.diff.removed
}
pub(crate) const fn diff_changed_color(self) -> Color {
self.diff.changed
}
pub(crate) const fn diff_context_color(self) -> Color {
self.diff.context
}
pub(crate) const fn diff_hunk_header_color(self) -> Color {
self.diff.hunk_header
}
pub(crate) const fn diff_file_header_color(self) -> Color {
self.diff.file_header
}
pub(crate) const fn diff_metadata_color(self) -> Color {
self.diff.metadata
}
pub(crate) fn app(self) -> Style {
Style::new()
.fg(self.text_primary())
.bg(self.surface_background())
}
pub(crate) fn pane(self) -> Style {
self.app()
}
pub(crate) fn panel_alt(self) -> Style {
Style::new()
.fg(self.text_primary())
.bg(self.surface_panel_alt())
}
pub(crate) fn overlay_panel(self) -> Style {
Style::new()
.fg(self.text_primary())
.bg(self.surface_overlay())
}
pub(crate) fn border(self, active: bool) -> Style {
if active {
self.styles.focused_border
} else {
self.styles.unfocused_border
}
}
pub(crate) fn bash_border(self, active: bool) -> Style {
Style::new()
.fg(if active {
self.border_command_active()
} else {
self.border_command()
})
.bg(self.surface_background())
}
pub(crate) fn bash_title(self, active: bool) -> Style {
Style::new()
.fg(if active {
self.text_command_active()
} else {
self.text_command()
})
.bg(self.surface_background())
.add_modifier(Modifier::BOLD)
}
pub(crate) fn border_alt(self) -> Style {
Style::new()
.fg(self.border_alternate())
.bg(self.surface_panel_alt())
}
pub(crate) fn title(self, active: bool) -> Style {
Style::new()
.fg(if active {
self.text_title()
} else {
self.text_muted()
})
.bg(self.surface_background())
.add_modifier(Modifier::BOLD)
}
pub(crate) fn title_alt(self) -> Style {
Style::new()
.fg(self.text_title())
.bg(self.surface_overlay())
.add_modifier(Modifier::BOLD)
}
pub(crate) fn muted(self, _active: bool) -> Style {
self.styles.muted
}
pub(crate) fn dim(self, _active: bool) -> Style {
self.styles.dimmed
}
pub(crate) fn accent(self, _active: bool) -> Style {
Style::new()
.fg(self.text_accent())
.bg(self.surface_background())
}
pub(crate) fn transcript_user_body(self) -> Style {
Style::new()
.fg(self.transcript_user_color())
.bg(self.surface_panel())
}
pub(crate) fn transcript_tool_body(self) -> Style {
Style::new()
.fg(self.transcript_tool_color())
.bg(self.surface_panel())
.add_modifier(Modifier::DIM)
}
pub(crate) fn transcript_session_body(self) -> Style {
Style::new()
.fg(self.transcript_session_color())
.bg(self.surface_panel())
.add_modifier(Modifier::DIM)
}
pub(crate) fn transcript_error_label(self) -> Style {
Style::new()
.fg(self.transcript_error_label_color())
.bg(self.surface_error())
.add_modifier(Modifier::BOLD)
}
pub(crate) fn transcript_error_body(self) -> Style {
Style::new()
.fg(self.transcript_error_body_color())
.bg(self.surface_error())
.add_modifier(Modifier::DIM)
}
pub(crate) fn selection(self) -> Style {
self.styles.selected
}
pub(crate) fn right_column_rail_background(self) -> Style {
self.app()
}
pub(crate) fn right_column_rail_active_tab(self) -> Style {
self.selection().add_modifier(Modifier::BOLD)
}
pub(crate) fn right_column_rail_inactive_tab(self) -> Style {
self.dim(false)
}
pub(crate) fn right_column_rail_separator(self) -> Style {
self.dim(false)
}
pub(crate) fn status(self, status: ActivityStatus, selected: bool, active: bool) -> Style {
let background = if selected && active {
self.surface_selection()
} else if status == ActivityStatus::Failed && active {
self.surface_error()
} else {
self.surface_background()
};
let mut style = Style::new()
.fg(self.activity_status_color(status))
.bg(background);
match status {
ActivityStatus::Running | ActivityStatus::Writing | ActivityStatus::Failed => {
style = style.add_modifier(Modifier::BOLD)
}
ActivityStatus::Canceled => style = style.add_modifier(Modifier::DIM),
ActivityStatus::Queued | ActivityStatus::Success => {}
}
style
}
pub(crate) fn label(self, selected: bool, active: bool) -> Style {
if selected && active {
self.styles.active_selected
} else {
self.pane()
}
}
pub(crate) fn fast_mode_label(self, color_enabled: bool) -> Style {
let mut style = Style::new()
.fg(self.text_accent())
.bg(self.surface_background())
.add_modifier(Modifier::BOLD);
if !color_enabled {
style = style.add_modifier(Modifier::UNDERLINED);
}
style
}
pub(crate) fn heading_style(self) -> Style {
self.styles.heading
}
pub(crate) fn display_role(self, role: DisplayRole) -> Style {
match role {
DisplayRole::Plain => self.pane(),
DisplayRole::Heading => self.heading_style(),
DisplayRole::ListMarker => Style::new()
.fg(self.text_accent())
.bg(self.surface_background())
.add_modifier(Modifier::BOLD),
DisplayRole::BlockQuote => Style::new()
.fg(self.text_muted())
.bg(self.surface_background()),
DisplayRole::InlineCode => self.styles.inline_code,
DisplayRole::CodeFence => Style::new()
.fg(self.syntax_code_fence_color())
.bg(self.surface_code())
.add_modifier(Modifier::DIM),
DisplayRole::CodeLanguageLabel => Style::new()
.fg(self.syntax_language_label_color())
.bg(self.surface_code())
.add_modifier(Modifier::DIM),
DisplayRole::CodeBlockGutter => self.styles.line_number,
DisplayRole::Keyword => self.styles.keyword,
DisplayRole::String => Style::new()
.fg(self.syntax_string_color())
.bg(self.surface_code()),
DisplayRole::Comment => Style::new()
.fg(self.syntax_comment_color())
.bg(self.surface_code())
.add_modifier(Modifier::ITALIC),
DisplayRole::Number => Style::new()
.fg(self.syntax_number_color())
.bg(self.surface_code()),
DisplayRole::Function => Style::new()
.fg(self.syntax_function_color())
.bg(self.surface_code()),
DisplayRole::Type => Style::new()
.fg(self.syntax_type_color())
.bg(self.surface_code()),
DisplayRole::Operator => Style::new()
.fg(self.syntax_operator_color())
.bg(self.surface_code()),
DisplayRole::Punctuation => Style::new()
.fg(self.syntax_punctuation_color())
.bg(self.surface_code()),
DisplayRole::FallbackCode => Style::new()
.fg(self.syntax_fallback_color())
.bg(self.surface_code()),
DisplayRole::DiffInserted => Style::new()
.fg(self.diff_inserted_color())
.bg(self.surface_success()),
DisplayRole::DiffRemoved => Style::new()
.fg(self.diff_removed_color())
.bg(self.surface_error()),
DisplayRole::DiffChanged => Style::new()
.fg(self.diff_changed_color())
.bg(self.surface_pending()),
DisplayRole::DiffContext => Style::new()
.fg(self.diff_context_color())
.bg(self.surface_background()),
DisplayRole::DiffHunkHeader => Style::new()
.fg(self.diff_hunk_header_color())
.bg(self.surface_background())
.add_modifier(Modifier::BOLD),
DisplayRole::DiffFileHeader => Style::new()
.fg(self.diff_file_header_color())
.bg(self.surface_background())
.add_modifier(Modifier::BOLD),
DisplayRole::DiffMetadata => Style::new()
.fg(self.diff_metadata_color())
.bg(self.surface_background()),
}
}
}
pub(crate) fn lower_color(color: OpalineColor, depth: ColorDepth) -> Color {
match depth {
ColorDepth::TrueColor => Color::Rgb(color.r, color.g, color.b),
ColorDepth::Ansi256 => Color::Indexed(appearance::ansi256_index(color)),
ColorDepth::Ansi16 => ansi16_color(appearance::ansi16_index(color)),
ColorDepth::None => Color::Reset,
}
}
fn ansi16_color(index: u8) -> Color {
match index {
0 => Color::Black,
1 => Color::Red,
2 => Color::Green,
3 => Color::Yellow,
4 => Color::Blue,
5 => Color::Magenta,
6 => Color::Cyan,
7 => Color::Gray,
8 => Color::DarkGray,
9 => Color::LightRed,
10 => Color::LightGreen,
11 => Color::LightYellow,
12 => Color::LightBlue,
13 => Color::LightMagenta,
14 => Color::LightCyan,
_ => Color::White,
}
}
fn project_opaline_style(
style: &OpalineStyle,
fallback_fg: OpalineColor,
fallback_bg: OpalineColor,
depth: ColorDepth,
) -> Style {
let fg = style
.fg
.map(|color| lower_color(color, depth))
.unwrap_or_else(|| lower_color(fallback_fg, depth));
let bg = style
.bg
.map(|color| lower_color(color, depth))
.unwrap_or_else(|| lower_color(fallback_bg, depth));
let mut modifiers = Modifier::empty();
if style.bold {
modifiers |= Modifier::BOLD;
}
if style.dim {
modifiers |= Modifier::DIM;
}
if style.italic {
modifiers |= Modifier::ITALIC;
}
if style.underline {
modifiers |= Modifier::UNDERLINED;
}
if style.slow_blink {
modifiers |= Modifier::SLOW_BLINK;
}
if style.rapid_blink {
modifiers |= Modifier::RAPID_BLINK;
}
if style.reversed {
modifiers |= Modifier::REVERSED;
}
if style.hidden {
modifiers |= Modifier::HIDDEN;
}
if style.crossed_out {
modifiers |= Modifier::CROSSED_OUT;
}
Style::new().fg(fg).bg(bg).add_modifier(modifiers)
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn matrix_green_comes_from_runtime_appearance() {
let theme = MissionControlTheme::default();
assert_eq!(theme.surface_background(), Color::Rgb(0x00, 0x1a, 0x12));
assert_eq!(theme.surface_panel(), Color::Rgb(0x00, 0x27, 0x1b));
assert_eq!(theme.surface_code(), Color::Rgb(0x00, 0x15, 0x0f));
assert_eq!(theme.text_accent(), Color::Rgb(0x39, 0xff, 0x88));
assert_eq!(theme.text_primary(), Color::Rgb(0xd8, 0xff, 0xe9));
assert_eq!(theme.surface_error(), Color::Rgb(0x06, 0x3b, 0x28));
}
#[test]
fn color_depth_lowering_uses_shared_ansi_indexes() {
let color = OpalineColor::new(255, 0, 0);
assert_eq!(
lower_color(color, ColorDepth::TrueColor),
Color::Rgb(255, 0, 0)
);
assert_eq!(
lower_color(color, ColorDepth::Ansi256),
Color::Indexed(appearance::ansi256_index(color))
);
assert_eq!(
lower_color(color, ColorDepth::Ansi16),
ansi16_color(appearance::ansi16_index(color))
);
assert_eq!(lower_color(color, ColorDepth::None), Color::Reset);
}
#[test]
fn ansi16_named_colors_match_the_shared_index_for_all_entries() {
let palette = [
(0, 0, 0),
(128, 0, 0),
(0, 128, 0),
(128, 128, 0),
(0, 0, 128),
(128, 0, 128),
(0, 128, 128),
(192, 192, 192),
(128, 128, 128),
(255, 0, 0),
(0, 255, 0),
(255, 255, 0),
(0, 0, 255),
(255, 0, 255),
(0, 255, 255),
(255, 255, 255),
];
for (index, (r, g, b)) in palette.into_iter().enumerate() {
let color = OpalineColor::new(r, g, b);
let index = index as u8;
assert_eq!(appearance::ansi16_index(color), index);
assert_eq!(lower_color(color, ColorDepth::Ansi16), ansi16_color(index));
}
}
#[test]
fn runtime_appearance_depth_projection_matches_rich_color_lowering() {
let source = RuntimeAppearance::default();
for depth in [
ColorDepth::TrueColor,
ColorDepth::Ansi256,
ColorDepth::Ansi16,
ColorDepth::None,
] {
let mut appearance = source.clone();
appearance.policy.depth = depth;
appearance.policy.color_enabled = depth != ColorDepth::None;
let theme = MissionControlTheme::from_runtime_appearance(&appearance, 1);
assert_eq!(
theme.text_accent(),
lower_color(appearance.theme.roles.text_accent, depth)
);
let fast = appearance.theme.roles.gradients.fast.generate(7);
assert_eq!(
theme.fast_mode_rainbow_color(0),
lower_color(fast[0], depth)
);
}
}
#[test]
fn no_color_keeps_modifiers_and_resets_colors() {
let mut appearance = RuntimeAppearance::default();
appearance.policy.depth = ColorDepth::None;
appearance.policy.color_enabled = false;
let theme = MissionControlTheme::from_runtime_appearance(&appearance, 1);
let style = theme.display_role(DisplayRole::Keyword);
assert_eq!(style.fg, Some(Color::Reset));
assert_eq!(style.bg, Some(Color::Reset));
assert!(style.add_modifier.contains(Modifier::BOLD));
}
#[test]
fn explicit_opaline_background_beats_contextual_background() {
let appearance = RuntimeAppearance::default();
let theme = MissionControlTheme::from_runtime_appearance(&appearance, 1);
let style = theme.display_role(DisplayRole::InlineCode);
assert_eq!(style.bg, Some(theme.surface_code()));
}
#[test]
fn right_column_rail_styles_preserve_custom_semantic_modifiers_and_backgrounds() {
let appearance = RuntimeAppearance::default();
let mut resolved = (*appearance.theme).clone();
let selected_fg = OpalineColor::new(1, 2, 3);
let selected_bg = OpalineColor::new(4, 5, 6);
let dimmed_fg = OpalineColor::new(7, 8, 9);
let dimmed_bg = OpalineColor::new(10, 11, 12);
let mut selected = OpalineStyle::new()
.with_fg(selected_fg)
.with_bg(selected_bg);
selected.underline = true;
resolved.roles.standard_styles.selected = selected;
let mut dimmed = OpalineStyle::new().with_fg(dimmed_fg).with_bg(dimmed_bg);
dimmed.italic = true;
dimmed.reversed = true;
resolved.roles.standard_styles.dimmed = dimmed;
let appearance = appearance.with_resolved_theme(resolved);
let theme = MissionControlTheme::from_runtime_appearance(&appearance, 1);
let active = theme.right_column_rail_active_tab();
let inactive = theme.right_column_rail_inactive_tab();
assert_eq!(active.fg, Some(Color::Rgb(1, 2, 3)));
assert_eq!(active.bg, Some(Color::Rgb(4, 5, 6)));
assert!(active.add_modifier.contains(Modifier::BOLD));
assert!(active.add_modifier.contains(Modifier::UNDERLINED));
assert_eq!(inactive.fg, Some(Color::Rgb(7, 8, 9)));
assert_eq!(inactive.bg, Some(Color::Rgb(10, 11, 12)));
assert!(inactive.add_modifier.contains(Modifier::ITALIC));
assert!(inactive.add_modifier.contains(Modifier::REVERSED));
assert_eq!(theme.right_column_rail_separator(), inactive);
}
#[test]
fn all_opaline_modifiers_project_to_ratatui() {
let mut style = OpalineStyle::new()
.with_fg(OpalineColor::new(1, 2, 3))
.with_bg(OpalineColor::new(4, 5, 6));
style.bold = true;
style.dim = true;
style.italic = true;
style.underline = true;
style.slow_blink = true;
style.rapid_blink = true;
style.reversed = true;
style.hidden = true;
style.crossed_out = true;
let projected = project_opaline_style(
&style,
OpalineColor::FALLBACK,
OpalineColor::FALLBACK,
ColorDepth::TrueColor,
);
for modifier in [
Modifier::BOLD,
Modifier::DIM,
Modifier::ITALIC,
Modifier::UNDERLINED,
Modifier::SLOW_BLINK,
Modifier::RAPID_BLINK,
Modifier::REVERSED,
Modifier::HIDDEN,
Modifier::CROSSED_OUT,
] {
assert!(
projected.add_modifier.contains(modifier),
"missing {modifier:?}"
);
}
}
}