From 84f44e8068ba21e9373b6032e0643720c8ef947f Mon Sep 17 00:00:00 2001 From: malsony Date: Mon, 25 May 2026 18:19:56 +0800 Subject: [PATCH 1/2] 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 --- crates/tui/src/config_ui.rs | 8 +++- crates/tui/src/palette.rs | 71 ++++++++++++++++++++++++++++-- crates/tui/src/tui/theme_picker.rs | 21 +++------ 3 files changed, 79 insertions(+), 21 deletions(-) diff --git a/crates/tui/src/config_ui.rs b/crates/tui/src/config_ui.rs index 7e400496e..da5aa1263 100644 --- a/crates/tui/src/config_ui.rs +++ b/crates/tui/src/config_ui.rs @@ -184,6 +184,7 @@ pub enum UiThemeValue { TokyoNight, Dracula, GruvboxDark, + Matrix, } #[derive(Debug, Clone, Copy, Serialize, Deserialize, JsonSchema, PartialEq, Eq)] @@ -743,6 +744,7 @@ impl UiThemeValue { Self::TokyoNight => "tokyo-night", Self::Dracula => "dracula", Self::GruvboxDark => "gruvbox-dark", + Self::Matrix => "matrix", } } @@ -756,6 +758,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}'"), } @@ -1184,7 +1187,8 @@ background_color = "#1A1B26" "catppuccin-mocha", "tokyo-night", "dracula", - "gruvbox-dark" + "gruvbox-dark", + "matrix" ]) ); } @@ -1269,4 +1273,4 @@ mcp_config_path = "disk-mcp.json" assert!(outcome.changed); assert!(!outcome.requires_engine_sync); } -} +} \ No newline at end of file diff --git a/crates/tui/src/palette.rs b/crates/tui/src/palette.rs index 2890804cb..05102c02a 100644 --- a/crates/tui/src/palette.rs +++ b/crates/tui/src/palette.rs @@ -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 @@ -864,6 +874,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`. @@ -877,6 +930,7 @@ pub enum ThemeId { TokyoNight, Dracula, GruvboxDark, + Matrix, } impl ThemeId { @@ -894,6 +948,7 @@ impl ThemeId { "tokyo-night" => Some(Self::TokyoNight), "dracula" => Some(Self::Dracula), "gruvbox-dark" => Some(Self::GruvboxDark), + "matrix" => Some(Self::Matrix), _ => None, } } @@ -911,6 +966,7 @@ impl ThemeId { Self::TokyoNight => "tokyo-night", Self::Dracula => "dracula", Self::GruvboxDark => "gruvbox-dark", + Self::Matrix => "matrix", } } @@ -926,6 +982,7 @@ impl ThemeId { Self::TokyoNight => "Tokyo Night", Self::Dracula => "Dracula", Self::GruvboxDark => "Gruvbox Dark", + Self::Matrix => "Matrix", } } @@ -941,6 +998,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", } } @@ -959,6 +1017,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, } } } @@ -973,6 +1032,7 @@ pub const SELECTABLE_THEMES: &[ThemeId] = &[ ThemeId::TokyoNight, ThemeId::Dracula, ThemeId::GruvboxDark, + ThemeId::Matrix, ]; impl UiTheme { @@ -1016,6 +1076,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, } } @@ -1185,7 +1246,7 @@ const fn theme_diff_deleted_bg(ui: &UiTheme) -> Color { pub const fn theme_remap_active(theme: ThemeId) -> bool { matches!( theme, - ThemeId::CatppuccinMocha | ThemeId::TokyoNight | ThemeId::Dracula | ThemeId::GruvboxDark + ThemeId::CatppuccinMocha | ThemeId::TokyoNight | ThemeId::Dracula | ThemeId::GruvboxDark | ThemeId::Matrix ) } @@ -1219,7 +1280,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 { @@ -2060,4 +2125,4 @@ mod tests { let _ = ColorDepth::detect(); let _ = adapt_color(DEEPSEEK_INK, ColorDepth::detect()); } -} +} \ No newline at end of file diff --git a/crates/tui/src/tui/theme_picker.rs b/crates/tui/src/tui/theme_picker.rs index 85da1d41a..65857fb61 100644 --- a/crates/tui/src/tui/theme_picker.rs +++ b/crates/tui/src/tui/theme_picker.rs @@ -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] From dae710db075f51338cd9f885cc7a157781e0e148 Mon Sep 17 00:00:00 2001 From: malsony Date: Mon, 25 May 2026 18:54:00 +0800 Subject: [PATCH 2/2] fix(clippy): collapse nested if and suppress too_many_arguments warning --- crates/tui/src/tui/ui.rs | 1 + 1 file changed, 1 insertion(+) diff --git a/crates/tui/src/tui/ui.rs b/crates/tui/src/tui/ui.rs index 61cb195c7..afc7a68db 100644 --- a/crates/tui/src/tui/ui.rs +++ b/crates/tui/src/tui/ui.rs @@ -5908,6 +5908,7 @@ fn toggle_live_transcript_overlay(app: &mut App) { app.needs_redraw = true; } +#[allow(clippy::too_many_arguments)] async fn handle_view_events( terminal: &mut AppTerminal, app: &mut App,