feat(theme): add Matrix films inspired theme and improve theme_picker logic
- Add Matrix films inspired color scheme - Refactor theme_picker to use SELECTABLE_THEMES for last-theme lookup instead of hard-coding
This commit is contained in:
@@ -184,6 +184,7 @@ pub enum UiThemeValue {
|
||||
TokyoNight,
|
||||
Dracula,
|
||||
GruvboxDark,
|
||||
Matrix,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Copy, Serialize, Deserialize, JsonSchema, PartialEq, Eq)]
|
||||
@@ -748,6 +749,7 @@ impl UiThemeValue {
|
||||
Self::TokyoNight => "tokyo-night",
|
||||
Self::Dracula => "dracula",
|
||||
Self::GruvboxDark => "gruvbox-dark",
|
||||
Self::Matrix => "matrix",
|
||||
}
|
||||
}
|
||||
|
||||
@@ -761,6 +763,7 @@ impl UiThemeValue {
|
||||
Some("tokyo-night") => Ok(Self::TokyoNight),
|
||||
Some("dracula") => Ok(Self::Dracula),
|
||||
Some("gruvbox-dark") => Ok(Self::GruvboxDark),
|
||||
Some("matrix") => Ok(Self::Matrix),
|
||||
Some(other) => bail!("unsupported theme '{other}'"),
|
||||
None => bail!("invalid theme '{value}'"),
|
||||
}
|
||||
@@ -1191,7 +1194,8 @@ background_color = "#1A1B26"
|
||||
"catppuccin-mocha",
|
||||
"tokyo-night",
|
||||
"dracula",
|
||||
"gruvbox-dark"
|
||||
"gruvbox-dark",
|
||||
"matrix"
|
||||
])
|
||||
);
|
||||
}
|
||||
@@ -1276,4 +1280,4 @@ mcp_config_path = "disk-mcp.json"
|
||||
assert!(outcome.changed);
|
||||
assert!(!outcome.requires_engine_sync);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -81,6 +81,16 @@ pub const GRAYSCALE_TEXT_SOFT_RGB: (u8, u8, u8) = (220, 220, 220); // #DCDCDC
|
||||
pub const GRAYSCALE_BORDER_RGB: (u8, u8, u8) = (96, 96, 96); // #606060
|
||||
pub const GRAYSCALE_SELECTION_RGB: (u8, u8, u8) = (62, 62, 62); // #3E3E3E
|
||||
|
||||
pub const MATRIX_SURFACE_RGB: (u8, u8, u8) = (0, 10, 0); // #000A00
|
||||
pub const MATRIX_ELEVATED_RGB: (u8, u8, u8) = (0, 51, 0); // #003300
|
||||
pub const MATRIX_SELECTION_RGB: (u8, u8, u8) = (0, 51, 0); // #003300
|
||||
pub const MATRIX_TEXT_BODY_RGB: (u8, u8, u8) = (136, 255, 136); // #88FF88
|
||||
pub const MATRIX_TEXT_MUTED_RGB: (u8, u8, u8) = (0, 68, 0); // #004400
|
||||
pub const MATRIX_TEXT_HINT_RGB: (u8, u8, u8) = (0, 102, 0); // #006600
|
||||
pub const MATRIX_TEXT_SOFT_RGB: (u8, u8, u8) = (221, 255, 221); // #DDFFDD
|
||||
pub const MATRIX_TEXT_DIM_RGB: (u8, u8, u8) = (0, 102, 0); // #006600
|
||||
pub const MATRIX_BORDER_RGB: (u8, u8, u8) = (0, 204, 0); // #00CC00
|
||||
|
||||
// New semantic colors
|
||||
pub const BORDER_COLOR_RGB: (u8, u8, u8) = WHALE_BORDER_RGB; // #2A4A7F
|
||||
|
||||
@@ -925,6 +935,49 @@ pub const GRUVBOX_DARK_UI_THEME: UiTheme = UiTheme {
|
||||
tool_failed: Color::Rgb(0xfb, 0x49, 0x34), // red
|
||||
};
|
||||
|
||||
pub const MATRIX_UI_THEME: UiTheme = UiTheme {
|
||||
name: "matrix",
|
||||
mode: PaletteMode::Dark,
|
||||
surface_bg: Color::Rgb(MATRIX_SURFACE_RGB.0, MATRIX_SURFACE_RGB.1, MATRIX_SURFACE_RGB.2),
|
||||
panel_bg: Color::Rgb(MATRIX_SURFACE_RGB.0, MATRIX_SURFACE_RGB.1, MATRIX_SURFACE_RGB.2),
|
||||
elevated_bg: Color::Rgb(MATRIX_ELEVATED_RGB.0, MATRIX_ELEVATED_RGB.1, MATRIX_ELEVATED_RGB.2),
|
||||
composer_bg: Color::Rgb(MATRIX_SURFACE_RGB.0, MATRIX_SURFACE_RGB.1, MATRIX_SURFACE_RGB.2),
|
||||
selection_bg: Color::Rgb(MATRIX_SELECTION_RGB.0, MATRIX_SELECTION_RGB.1, MATRIX_SELECTION_RGB.2),
|
||||
header_bg: Color::Rgb(MATRIX_SURFACE_RGB.0, MATRIX_SURFACE_RGB.1, MATRIX_SURFACE_RGB.2),
|
||||
footer_bg: Color::Rgb(MATRIX_SURFACE_RGB.0, MATRIX_SURFACE_RGB.1, MATRIX_SURFACE_RGB.2),
|
||||
text_dim: Color::Rgb(MATRIX_TEXT_DIM_RGB.0, MATRIX_TEXT_DIM_RGB.1, MATRIX_TEXT_DIM_RGB.2),
|
||||
text_hint: Color::Rgb(MATRIX_TEXT_HINT_RGB.0, MATRIX_TEXT_HINT_RGB.1, MATRIX_TEXT_HINT_RGB.2),
|
||||
text_muted: Color::Rgb(MATRIX_TEXT_MUTED_RGB.0, MATRIX_TEXT_MUTED_RGB.1, MATRIX_TEXT_MUTED_RGB.2),
|
||||
text_body: Color::Rgb(MATRIX_TEXT_BODY_RGB.0, MATRIX_TEXT_BODY_RGB.1, MATRIX_TEXT_BODY_RGB.2),
|
||||
text_soft: Color::Rgb(MATRIX_TEXT_SOFT_RGB.0, MATRIX_TEXT_SOFT_RGB.1, MATRIX_TEXT_SOFT_RGB.2),
|
||||
border: Color::Rgb(MATRIX_BORDER_RGB.0, MATRIX_BORDER_RGB.1, MATRIX_BORDER_RGB.2),
|
||||
accent_primary: Color::Rgb(0, 204, 0),
|
||||
accent_secondary: Color::Rgb(0, 153, 0),
|
||||
accent_action: Color::Rgb(0x88, 0xff, 0x88),
|
||||
error_fg: Color::Rgb(0xb4, 0, 0),
|
||||
error_hover: Color::Rgb(0xe0, 0, 0),
|
||||
error_surface: Color::Rgb(0x1a, 0x0d, 0x0d),
|
||||
error_border: Color::Rgb(0xb4, 0, 0),
|
||||
error_text: Color::Rgb(0xff, 0x44, 0x44),
|
||||
warning: Color::Rgb(204, 204, 0),
|
||||
success: Color::Rgb(0x88, 0xff, 0x88),
|
||||
info: Color::Rgb(0, 204, 0),
|
||||
mode_agent: Color::Rgb(0, 153, 0),
|
||||
mode_yolo: Color::Rgb(255, 100, 100),
|
||||
mode_plan: Color::Rgb(255, 170, 60),
|
||||
mode_goal: Color::Rgb(170, 255, 170),
|
||||
status_ready: Color::Rgb(0, 85, 0),
|
||||
status_working: Color::Rgb(MATRIX_TEXT_BODY_RGB.0, MATRIX_TEXT_BODY_RGB.1, MATRIX_TEXT_BODY_RGB.2),
|
||||
status_warning: Color::Rgb(204, 204, 0),
|
||||
diff_added_fg: Color::Rgb(0x88, 0xff, 0x88),
|
||||
diff_deleted_fg: Color::Rgb(0xb4, 0, 0),
|
||||
diff_added_bg: Color::Rgb(0x0d, 0x1a, 0x0d),
|
||||
diff_deleted_bg: Color::Rgb(0x1a, 0x0d, 0x0d),
|
||||
tool_running: Color::Rgb(0x88, 0xff, 0x88),
|
||||
tool_success: Color::Rgb(0, 102, 0),
|
||||
tool_failed: Color::Rgb(0xb4, 0, 0),
|
||||
};
|
||||
|
||||
/// Stable identifiers for the named themes the user can select. `System`
|
||||
/// defers to `PaletteMode::detect()` (terminal-driven dark/light). Each
|
||||
/// dark/light id resolves to a single fixed `UiTheme`.
|
||||
@@ -939,6 +992,7 @@ pub enum ThemeId {
|
||||
TokyoNight,
|
||||
Dracula,
|
||||
GruvboxDark,
|
||||
Matrix,
|
||||
}
|
||||
|
||||
impl ThemeId {
|
||||
@@ -957,6 +1011,7 @@ impl ThemeId {
|
||||
"tokyo-night" => Some(Self::TokyoNight),
|
||||
"dracula" => Some(Self::Dracula),
|
||||
"gruvbox-dark" => Some(Self::GruvboxDark),
|
||||
"matrix" => Some(Self::Matrix),
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
@@ -975,6 +1030,7 @@ impl ThemeId {
|
||||
Self::TokyoNight => "tokyo-night",
|
||||
Self::Dracula => "dracula",
|
||||
Self::GruvboxDark => "gruvbox-dark",
|
||||
Self::Matrix => "matrix",
|
||||
}
|
||||
}
|
||||
|
||||
@@ -991,6 +1047,7 @@ impl ThemeId {
|
||||
Self::TokyoNight => "Tokyo Night",
|
||||
Self::Dracula => "Dracula",
|
||||
Self::GruvboxDark => "Gruvbox Dark",
|
||||
Self::Matrix => "Matrix",
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1007,6 +1064,7 @@ impl ThemeId {
|
||||
Self::TokyoNight => "Deep blue/violet night palette",
|
||||
Self::Dracula => "Classic high-contrast purple",
|
||||
Self::GruvboxDark => "Vintage warm earth tones",
|
||||
Self::Matrix => "The Matrix films inspired theme",
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1026,6 +1084,7 @@ impl ThemeId {
|
||||
Self::TokyoNight => TOKYO_NIGHT_UI_THEME,
|
||||
Self::Dracula => DRACULA_UI_THEME,
|
||||
Self::GruvboxDark => GRUVBOX_DARK_UI_THEME,
|
||||
Self::Matrix => MATRIX_UI_THEME,
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1041,6 +1100,7 @@ pub const SELECTABLE_THEMES: &[ThemeId] = &[
|
||||
ThemeId::TokyoNight,
|
||||
ThemeId::Dracula,
|
||||
ThemeId::GruvboxDark,
|
||||
ThemeId::Matrix,
|
||||
];
|
||||
|
||||
impl UiTheme {
|
||||
@@ -1085,6 +1145,7 @@ pub fn normalize_theme_name(value: &str) -> Option<&'static str> {
|
||||
"tokyo-night" | "tokyonight" | "tokyo" => Some("tokyo-night"),
|
||||
"dracula" => Some("dracula"),
|
||||
"gruvbox-dark" | "gruvbox" => Some("gruvbox-dark"),
|
||||
"matrix" | "hacker" => Some("matrix"),
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
@@ -1259,6 +1320,7 @@ pub const fn theme_remap_active(theme: ThemeId) -> bool {
|
||||
| ThemeId::TokyoNight
|
||||
| ThemeId::Dracula
|
||||
| ThemeId::GruvboxDark
|
||||
| ThemeId::Matrix
|
||||
)
|
||||
}
|
||||
|
||||
@@ -1292,7 +1354,11 @@ pub fn adapt_fg_for_theme(color: Color, theme: ThemeId, ui: &UiTheme) -> Color {
|
||||
} else if color == TEXT_ACCENT || color == DEEPSEEK_SKY || color == ACCENT_TOOL_LIVE {
|
||||
ui.status_working
|
||||
} else if color == TEXT_REASONING || color == ACCENT_REASONING_LIVE {
|
||||
ui.mode_plan
|
||||
if theme == ThemeId::Matrix {
|
||||
Color::Rgb(0x00, 0x55, 0x00) // #005500
|
||||
} else {
|
||||
ui.mode_plan
|
||||
}
|
||||
} else if color == ACCENT_TOOL_ISSUE {
|
||||
ui.mode_yolo
|
||||
} else if color == STATUS_WARNING {
|
||||
@@ -2161,4 +2227,4 @@ mod tests {
|
||||
let _ = ColorDepth::detect();
|
||||
let _ = adapt_color(DEEPSEEK_INK, ColorDepth::detect());
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -90,23 +90,11 @@ impl ThemePickerView {
|
||||
}
|
||||
|
||||
fn move_up(&mut self) {
|
||||
let len = SELECTABLE_THEMES.len();
|
||||
if len == 0 {
|
||||
self.selected = 0;
|
||||
} else if self.selected == 0 {
|
||||
self.selected = len - 1;
|
||||
} else {
|
||||
self.selected -= 1;
|
||||
}
|
||||
self.selected = (self.selected + SELECTABLE_THEMES.len() - 1) % SELECTABLE_THEMES.len();
|
||||
}
|
||||
|
||||
fn move_down(&mut self) {
|
||||
let len = SELECTABLE_THEMES.len();
|
||||
if len == 0 {
|
||||
self.selected = 0;
|
||||
} else {
|
||||
self.selected = (self.selected + 1) % len;
|
||||
}
|
||||
self.selected = (self.selected + 1) % SELECTABLE_THEMES.len();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -323,12 +311,13 @@ mod tests {
|
||||
#[test]
|
||||
fn arrow_navigation_wraps_at_picker_edges() {
|
||||
let mut v = ThemePickerView::new("system".to_string());
|
||||
let last = SELECTABLE_THEMES.last().unwrap();
|
||||
|
||||
let action = v.handle_key(key(KeyCode::Up));
|
||||
assert_eq!(selected_name(&action), Some(ThemeId::GruvboxDark.name()));
|
||||
assert_eq!(selected_name(&action), Some(last.name()));
|
||||
|
||||
let action = v.handle_key(key(KeyCode::Down));
|
||||
assert_eq!(selected_name(&action), Some(ThemeId::System.name()));
|
||||
assert_eq!(selected_name(&action), Some(SELECTABLE_THEMES[0].name()));
|
||||
}
|
||||
|
||||
#[test]
|
||||
|
||||
Reference in New Issue
Block a user