diff --git a/crates/tui/src/config_ui.rs b/crates/tui/src/config_ui.rs index 9cf8ecd20..9537536b3 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)] @@ -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); } -} +} \ No newline at end of file diff --git a/crates/tui/src/palette.rs b/crates/tui/src/palette.rs index b3a5a367f..6df428885 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 @@ -868,6 +878,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`. @@ -881,6 +934,7 @@ pub enum ThemeId { TokyoNight, Dracula, GruvboxDark, + Matrix, } impl ThemeId { @@ -898,6 +952,7 @@ impl ThemeId { "tokyo-night" => Some(Self::TokyoNight), "dracula" => Some(Self::Dracula), "gruvbox-dark" => Some(Self::GruvboxDark), + "matrix" => Some(Self::Matrix), _ => None, } } @@ -915,6 +970,7 @@ impl ThemeId { Self::TokyoNight => "tokyo-night", Self::Dracula => "dracula", Self::GruvboxDark => "gruvbox-dark", + Self::Matrix => "matrix", } } @@ -930,6 +986,7 @@ impl ThemeId { Self::TokyoNight => "Tokyo Night", Self::Dracula => "Dracula", Self::GruvboxDark => "Gruvbox Dark", + Self::Matrix => "Matrix", } } @@ -945,6 +1002,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", } } @@ -963,6 +1021,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, } } } @@ -977,6 +1036,7 @@ pub const SELECTABLE_THEMES: &[ThemeId] = &[ ThemeId::TokyoNight, ThemeId::Dracula, ThemeId::GruvboxDark, + ThemeId::Matrix, ]; impl UiTheme { @@ -1020,6 +1080,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, } } @@ -1189,7 +1250,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 ) } @@ -1223,7 +1284,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 { @@ -2064,4 +2129,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] diff --git a/crates/tui/src/tui/ui.rs b/crates/tui/src/tui/ui.rs index fb89de619..81dcba344 100644 --- a/crates/tui/src/tui/ui.rs +++ b/crates/tui/src/tui/ui.rs @@ -6060,6 +6060,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,