diff --git a/Cargo.lock b/Cargo.lock index c4e8976f..d758e9a1 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -709,7 +709,7 @@ dependencies = [ "bitflags 2.9.1", "cexpr", "clang-sys", - "itertools 0.11.0", + "itertools 0.12.1", "lazy_static", "lazycell", "log", @@ -732,7 +732,7 @@ dependencies = [ "bitflags 2.9.1", "cexpr", "clang-sys", - "itertools 0.11.0", + "itertools 0.13.0", "log", "prettyplease", "proc-macro2", @@ -2574,6 +2574,15 @@ version = "1.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "42703706b716c37f96a77aea830392ad231f44c9e9a67872fa5548707e11b11c" +[[package]] +name = "fsevent-sys" +version = "4.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "76ee7a02da4d231650c7cea31349b889be2f45ddb3ef3032d2ec8185f6313fd2" +dependencies = [ + "libc", +] + [[package]] name = "ftree" version = "1.2.0" @@ -3156,6 +3165,7 @@ dependencies = [ "itertools 0.13.0", "markdown", "markup5ever_rcdom", + "notify", "num-traits", "once_cell", "paste", @@ -3625,7 +3635,7 @@ dependencies = [ "js-sys", "log", "wasm-bindgen", - "windows-core 0.58.0", + "windows-core 0.61.2", ] [[package]] @@ -3870,6 +3880,26 @@ version = "2.0.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f4c7245a08504955605670dbf141fceab975f15ca21570696aebe9d2e71576bd" +[[package]] +name = "inotify" +version = "0.10.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fdd168d97690d0b8c412d6b6c10360277f4d7ee495c5d0d5d5fe0854923255cc" +dependencies = [ + "bitflags 1.3.2", + "inotify-sys", + "libc", +] + +[[package]] +name = "inotify-sys" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e05c02b5e89bff3b946cedeca278abc628fe811e604f027c45a8aa3cf793d0eb" +dependencies = [ + "libc", +] + [[package]] name = "inout" version = "0.1.4" @@ -4113,6 +4143,26 @@ dependencies = [ "libloading", ] +[[package]] +name = "kqueue" +version = "1.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "eac30106d7dce88daf4a3fcb4879ea939476d5074a9b7ddd0fb97fa4bed5596a" +dependencies = [ + "kqueue-sys", + "libc", +] + +[[package]] +name = "kqueue-sys" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ed9625ffda8729b85e45cf04090035ac368927b8cebc34898e7c120f52e4838b" +dependencies = [ + "bitflags 1.3.2", + "libc", +] + [[package]] name = "kuchikiki" version = "0.8.2" @@ -4541,6 +4591,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "78bed444cc8a2160f01cbcf811ef18cac863ad68ae8ca62092e8db51d51c761c" dependencies = [ "libc", + "log", "wasi 0.11.1+wasi-snapshot-preview1", "windows-sys 0.59.0", ] @@ -4688,6 +4739,34 @@ dependencies = [ "windows-sys 0.59.0", ] +[[package]] +name = "notify" +version = "7.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c533b4c39709f9ba5005d8002048266593c1cfaf3c5f0739d5b8ab0c6c504009" +dependencies = [ + "bitflags 2.9.1", + "filetime", + "fsevent-sys", + "inotify", + "kqueue", + "libc", + "log", + "mio", + "notify-types", + "walkdir", + "windows-sys 0.52.0", +] + +[[package]] +name = "notify-types" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "585d3cb5e12e01aed9e8a1f70d5c6b5e86fe2a6e48fc8cd0b3e0b8df6f6eb174" +dependencies = [ + "instant", +] + [[package]] name = "ntapi" version = "0.4.1" @@ -4842,7 +4921,7 @@ version = "0.7.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "77e878c846a8abae00dd069496dbe8751b16ac1c3d6bd2a7283a938e8228f90d" dependencies = [ - "proc-macro-crate 1.3.1", + "proc-macro-crate 3.3.0", "proc-macro2", "quote", "syn 2.0.105", diff --git a/Cargo.toml b/Cargo.toml index 8119ccbc..84219274 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -33,6 +33,7 @@ rust-i18n = "3" raw-window-handle = "0.6.2" smol = "1" tracing = "0.1.41" +notify = "7.0.0" [workspace.dependencies.windows] features = ["Wdk", "Wdk_System", "Wdk_System_SystemServices"] diff --git a/crates/story/examples/markdown.rs b/crates/story/examples/markdown.rs index 4e5f96f1..1ae2006b 100644 --- a/crates/story/examples/markdown.rs +++ b/crates/story/examples/markdown.rs @@ -1,5 +1,3 @@ -use std::rc::Rc; - use gpui::*; use gpui_component::{ highlighter::{HighlightTheme, Language}, @@ -83,7 +81,7 @@ impl Render for Example { .child( TextView::markdown("preview", self.input_state.read(cx).value()).style( TextViewStyle { - highlight_theme: Rc::new(theme.clone()), + highlight_theme: theme.clone(), is_dark, ..Default::default() }, diff --git a/crates/story/src/themes.rs b/crates/story/src/themes.rs index 7d1e7874..239d7a10 100644 --- a/crates/story/src/themes.rs +++ b/crates/story/src/themes.rs @@ -1,13 +1,12 @@ -use std::{collections::HashMap, sync::LazyLock}; +use std::path::PathBuf; -use anyhow::Context; use gpui::{ div, px, Action, App, InteractiveElement as _, ParentElement as _, Render, SharedString, }; use gpui_component::{ button::{Button, ButtonVariants}, popup_menu::PopupMenuExt, - IconName, Sizable, Theme, ThemeConfig, ThemeSet, + IconName, Sizable, Theme, ThemeRegistry, }; use serde::{Deserialize, Serialize}; @@ -23,54 +22,25 @@ struct State { pub fn init(cx: &mut App) { // Load last theme state let json = std::fs::read_to_string(STATE_FILE).unwrap_or(String::default()); - if let Ok(state) = serde_json::from_str::(&json) { - tracing::info!("apply theme: {:?}", state.theme); - AppState::global_mut(cx).theme_name = Some(state.theme.clone()); - if let Some(theme) = THEMES.get(&state.theme) { - Theme::global_mut(cx).apply_config(theme); + tracing::info!("Load themes..."); + if let Err(err) = ThemeRegistry::watch_dir(PathBuf::from("./themes"), cx, move |cx| { + if let Ok(state) = serde_json::from_str::(&json) { + tracing::info!("apply theme: {:?}", state.theme); + AppState::global_mut(cx).theme_name = Some(state.theme.clone()); + + if let Some(theme) = ThemeRegistry::global(cx) + .themes() + .get(&state.theme) + .cloned() + { + Theme::global_mut(cx).apply_config(&theme); + } } + }) { + tracing::error!("Failed to watch themes directory: {}", err); } } -static THEMES: LazyLock> = LazyLock::new(|| { - fn parse_themes(source: &str) -> ThemeSet { - serde_json::from_str(source) - .context(format!("source: '{}'", source)) - .unwrap() - } - - let mut themes = HashMap::new(); - for source in [ - include_str!("../../../themes/adventure.json"), - include_str!("../../../themes/alduin.json"), - include_str!("../../../themes/ayu.json"), - include_str!("../../../themes/catppuccin.json"), - include_str!("../../../themes/everforest.json"), - include_str!("../../../themes/fahrenheit.json"), - include_str!("../../../themes/gruvbox.json"), - include_str!("../../../themes/harper.json"), - include_str!("../../../themes/hybrid.json"), - include_str!("../../../themes/jellybeans.json"), - include_str!("../../../themes/kibble.json"), - include_str!("../../../themes/macos-classic.json"), - include_str!("../../../themes/mandarin-square.json"), - include_str!("../../../themes/matrix.json"), - include_str!("../../../themes/mellifluous.json"), - include_str!("../../../themes/molokai.json"), - include_str!("../../../themes/solarized.json"), - include_str!("../../../themes/spaceduck.json"), - include_str!("../../../themes/tokyonight.json"), - include_str!("../../../themes/twilight.json"), - ] { - let theme_set = parse_themes(source); - for theme in theme_set.themes { - themes.insert(theme.name.clone(), theme); - } - } - - themes -}); - #[derive(Action, Clone, PartialEq)] #[action(namespace = themes, no_json)] struct SwitchTheme(SharedString); @@ -103,13 +73,10 @@ impl Render for ThemeSwitcher { .on_action(cx.listener(|this, switch: &SwitchTheme, _, cx| { this.current_theme_name = switch.0.clone(); let theme_name = this.current_theme_name.clone(); - - if let Some(theme_config) = THEMES.get(&theme_name) { - Theme::global_mut(cx).apply_config(theme_config); - } else if theme_name == "default-light" { - Theme::global_mut(cx).set_default_light(); - } else if theme_name == "default-dark" { - Theme::global_mut(cx).set_default_dark(); + if let Some(theme_config) = + ThemeRegistry::global(cx).themes().get(&theme_name).cloned() + { + Theme::global_mut(cx).apply_config(&theme_config); } // Save AppState @@ -129,26 +96,17 @@ impl Render for ThemeSwitcher { .small() .popup_menu({ let current_theme_id = self.current_theme_name.clone(); - move |menu, _, _| { - let mut menu = menu - .scrollable() - .max_h(px(600.)) - .menu_with_check( - "Default Light", - current_theme_id == "default-light", - Box::new(SwitchTheme("default-light".into())), - ) - .menu_with_check( - "Default Dark", - current_theme_id == "default-dark", - Box::new(SwitchTheme("default-dark".into())), - ); + move |menu, _, cx| { + let mut menu = menu.scrollable().max_h(px(600.)); - let mut names = THEMES.keys().collect::>(); - names.sort(); + let names = ThemeRegistry::global(cx) + .sorted_themes() + .iter() + .map(|theme| theme.name.clone()) + .collect::>(); for theme_name in names { - let is_selected = *theme_name == current_theme_id; + let is_selected = theme_name == current_theme_id; menu = menu.menu_with_check( theme_name.clone(), is_selected, diff --git a/crates/ui/Cargo.toml b/crates/ui/Cargo.toml index 1c029bf8..681e7e7c 100644 --- a/crates/ui/Cargo.toml +++ b/crates/ui/Cargo.toml @@ -59,6 +59,7 @@ smallvec.workspace = true smol.workspace = true tracing.workspace = true anyhow.workspace = true +notify.workspace = true enum-iterator = "2.1.0" futures-util = "0.3.31" diff --git a/crates/ui/src/highlighter/registry.rs b/crates/ui/src/highlighter/registry.rs index 08501f8c..e73b6514 100644 --- a/crates/ui/src/highlighter/registry.rs +++ b/crates/ui/src/highlighter/registry.rs @@ -2,11 +2,11 @@ use gpui::{App, FontWeight, HighlightStyle, Hsla, SharedString}; use schemars::JsonSchema; use serde::{Deserialize, Serialize}; use serde_repr::{Deserialize_repr, Serialize_repr}; -use std::{collections::HashMap, ops::Deref, sync::LazyLock}; +use std::{collections::HashMap, ops::Deref, sync::Arc}; use crate::{ highlighter::{languages, Language}, - ActiveTheme, Colorize, ThemeMode, + ActiveTheme, Colorize, ThemeMode, DEFAULT_THEME_COLORS, }; pub(super) fn init(cx: &mut App) { @@ -58,15 +58,6 @@ pub(super) const HIGHLIGHT_NAMES: [&str; 40] = [ "variant", ]; -const DEFAULT_DARK: LazyLock = LazyLock::new(|| { - let json = include_str!("./themes/dark.json"); - serde_json::from_str(json).unwrap() -}); -const DEFAULT_LIGHT: LazyLock = LazyLock::new(|| { - let json = include_str!("./themes/light.json"); - serde_json::from_str(json).unwrap() -}); - #[derive(Debug, Clone, PartialEq, Eq)] pub struct LanguageConfig { pub name: SharedString, @@ -460,12 +451,12 @@ impl Deref for HighlightTheme { } impl HighlightTheme { - pub fn default_dark() -> Self { - DEFAULT_DARK.clone() + pub fn default_dark() -> Arc { + DEFAULT_THEME_COLORS[&ThemeMode::Dark].1.clone() } - pub fn default_light() -> Self { - DEFAULT_LIGHT.clone() + pub fn default_light() -> Arc { + DEFAULT_THEME_COLORS[&ThemeMode::Light].1.clone() } } @@ -523,23 +514,8 @@ impl LanguageRegistry { #[cfg(test)] mod tests { - use gpui::rgb; - use crate::highlighter::LanguageConfig; - #[test] - fn test_syntax_colors() { - use super::{HighlightTheme, SyntaxColors}; - - let theme: HighlightTheme = - serde_json::from_str(include_str!("./themes/light.json")).unwrap(); - let syntax: &SyntaxColors = &theme.style.syntax; - - assert_eq!(syntax.style("keyword"), Some(rgb(0x0433ff).into())); - assert_eq!(syntax.style("keyword.repeat"), Some(rgb(0x0433ff).into())); - assert_eq!(syntax.style("foo"), None); - } - #[test] fn test_registry() { use super::LanguageRegistry; diff --git a/crates/ui/src/highlighter/themes/README.md b/crates/ui/src/highlighter/themes/README.md deleted file mode 100644 index 94820287..00000000 --- a/crates/ui/src/highlighter/themes/README.md +++ /dev/null @@ -1 +0,0 @@ -The default theme based on [zed-theme-macos-classic](https://github.com/huacnlee/zed-theme-macos-classic). \ No newline at end of file diff --git a/crates/ui/src/highlighter/themes/dark.json b/crates/ui/src/highlighter/themes/dark.json deleted file mode 100644 index f561779d..00000000 --- a/crates/ui/src/highlighter/themes/dark.json +++ /dev/null @@ -1,170 +0,0 @@ -{ - "name": "macOS Classic Dark", - "appearance": "dark", - "style": { - "editor.foreground": "#DDDDDD", - "editor.background": "#000000", - "editor.active_line.background": "#131313", - "editor.line_number": "#8F8F8F", - "editor.active_line_number": "#DDDDDD", - "conflict": "#D2602D", - "conflict.background": null, - "conflict.border": null, - "created": "#3f72e2", - "created.background": "#0C4619", - "created.border": null, - "deleted": null, - "deleted.background": "#46190C", - "deleted.border": null, - "error": null, - "error.background": "#46190C", - "error.border": "#802207", - "hidden": "#9E9E9E", - "hidden.background": null, - "hidden.border": null, - "hint": "#b283f8", - "hint.background": "#250c4b", - "hint.border": "#3f0891", - "ignored": null, - "ignored.background": null, - "ignored.border": null, - "info": null, - "info.background": "#0C194D", - "info.border": "#082190", - "modified": "#B0A878", - "modified.background": "#3A310E", - "modified.border": null, - "predictive": "#5D5945", - "predictive.background": null, - "predictive.border": null, - "renamed": null, - "renamed.background": null, - "renamed.border": null, - "success": null, - "success.background": "#0C4619", - "success.border": null, - "unreachable": null, - "unreachable.background": null, - "unreachable.border": null, - "warning": null, - "warning.background": "#3A310E", - "warning.border": "#7B6508", - "syntax": { - "attribute": { - "color": "#be9a52", - "font_style": null, - "font_weight": null - }, - "boolean": { - "color": "#E1D797", - "font_style": null, - "font_weight": null - }, - "comment": { - "color": "#9E9E9E", - "font_style": null, - "font_weight": null - }, - "comment.doc": { - "color": "#9E9E9E", - "font_style": null, - "font_weight": null - }, - "constant": { - "color": "#E1D797", - "font_style": null, - "font_weight": null - }, - "constructor": { - "color": "#b5af9a", - "font_style": null, - "font_weight": null - }, - "embedded": { - "color": "#CACCCA", - "font_style": null, - "font_weight": null - }, - "function": { - "color": "#E1D797", - "font_style": null, - "font_weight": null - }, - "keyword": { - "color": "#E19773", - "font_style": null, - "font_weight": null - }, - "link_text": { - "color": "#A86D3B", - "font_style": "normal", - "font_weight": null - }, - "link_uri": { - "color": "#6F6D66", - "font_style": "italic", - "font_weight": null - }, - "number": { - "color": "#E19773", - "font_style": null, - "font_weight": null - }, - "string": { - "color": "#76BA53", - "font_style": null, - "font_weight": null - }, - "string.escape": { - "color": "#76BA53", - "font_style": null, - "font_weight": null - }, - "string.regex": { - "color": "#76BA53", - "font_style": null, - "font_weight": null - }, - "string.special": { - "color": "#E1D797", - "font_style": null, - "font_weight": null - }, - "string.special.symbol": { - "color": "#E1D797", - "font_style": null, - "font_weight": null - }, - "tag": { - "color": "#b5af9a", - "font_style": null, - "font_weight": null - }, - "text.literal": { - "color": "#E1D797", - "font_style": null, - "font_weight": null - }, - "title": { - "color": "#A76D3B", - "font_style": null, - "font_weight": 600 - }, - "type": { - "color": "#A86D3B", - "font_style": null, - "font_weight": null - }, - "property": { - "color": "#CACCCA", - "font_style": null, - "font_weight": null - }, - "variable.special": { - "color": "#E19773", - "font_style": null, - "font_weight": null - } - } - } -} diff --git a/crates/ui/src/highlighter/themes/light.json b/crates/ui/src/highlighter/themes/light.json deleted file mode 100644 index f2ecd9fc..00000000 --- a/crates/ui/src/highlighter/themes/light.json +++ /dev/null @@ -1,175 +0,0 @@ -{ - "name": "macOS Classic Light", - "appearance": "light", - "style": { - "editor.foreground": "#000000", - "editor.background": "#ffffff", - "editor.active_line.background": "#F5F5F5", - "editor.line_number": "#929292", - "editor.active_line_number": "#000000", - "conflict": "#C5060B", - "conflict.background": null, - "conflict.border": null, - "created": "#1642FF", - "created.background": "#e5ffe9", - "created.border": null, - "deleted": null, - "deleted.background": "#FBEAE5", - "deleted.border": null, - "error": null, - "error.background": "#FBEAE5", - "error.border": "#EC9F89", - "hidden": "#6D6D6D", - "hidden.background": null, - "hidden.border": null, - "hint": "#9e5dff", - "hint.background": "#ece6ff", - "hint.border": "#bc8fff", - "ignored": null, - "ignored.background": null, - "ignored.border": null, - "info": null, - "info.background": "#E5EAFF", - "info.border": "#8DA1FF", - "modified": "#9e7008", - "modified.background": "#fff2e5", - "modified.border": null, - "predictive": "#A4ABB6", - "predictive.background": null, - "predictive.border": null, - "renamed": null, - "renamed.background": null, - "renamed.border": null, - "success": null, - "success.background": "#E5FFE5", - "success.border": null, - "unreachable": null, - "unreachable.background": null, - "unreachable.border": null, - "warning": "#C99401", - "warning.background": "#FFFBE5", - "warning.border": "#D9CC89", - "syntax": { - "attribute": { - "color": "#957931", - "font_style": null, - "font_weight": null - }, - "boolean": { - "color": "#C5060B", - "font_style": null, - "font_weight": null - }, - "comment": { - "color": "#007fff", - "font_style": null, - "font_weight": null - }, - "comment.doc": { - "color": "#007fff", - "font_style": null, - "font_weight": null - }, - "constant": { - "color": "#C5060B", - "font_style": null, - "font_weight": null - }, - "constructor": { - "color": "#0433ff", - "font_style": null, - "font_weight": null - }, - "embedded": { - "color": "#333333", - "font_style": null, - "font_weight": null - }, - "function": { - "color": "#0000A2", - "font_style": null, - "font_weight": null - }, - "keyword": { - "color": "#0433ff", - "font_style": null, - "font_weight": null - }, - "link_text": { - "color": "#0000A2", - "font_style": "normal", - "font_weight": null - }, - "link_uri": { - "color": "#6A7293", - "font_style": "italic", - "font_weight": null - }, - "number": { - "color": "#0433ff", - "font_style": null, - "font_weight": null - }, - "string": { - "color": "#036A07", - "font_style": null, - "font_weight": null - }, - "string.escape": { - "color": "#036A07", - "font_style": null, - "font_weight": null - }, - "string.regex": { - "color": "#036A07", - "font_style": null, - "font_weight": null - }, - "string.special": { - "color": "#d21f07", - "font_style": null, - "font_weight": null - }, - "string.special.symbol": { - "color": "#d21f07", - "font_style": null, - "font_weight": null - }, - "tag": { - "color": "#0433ff", - "font_style": null, - "font_weight": null - }, - "text.literal": { - "color": "#6F42C1", - "font_style": null, - "font_weight": null - }, - "title": { - "color": "#0433FF", - "font_style": null, - "font_weight": null - }, - "type": { - "color": "#6f42c1", - "font_style": null, - "font_weight": null - }, - "property": { - "color": "#333333", - "font_style": null, - "font_weight": null - }, - "variable": { - "color": "#333333", - "font_style": null, - "font_weight": null - }, - "variable.special": { - "color": "#C5060B", - "font_style": null, - "font_weight": null - } - } - } -} diff --git a/crates/ui/src/text/text_view.rs b/crates/ui/src/text/text_view.rs index 24f7e346..95faa4ed 100644 --- a/crates/ui/src/text/text_view.rs +++ b/crates/ui/src/text/text_view.rs @@ -1,4 +1,4 @@ -use std::rc::Rc; +use std::sync::Arc; use gpui::{px, rems, App, ElementId, IntoElement, Pixels, Rems, RenderOnce, SharedString, Window}; @@ -88,7 +88,7 @@ pub struct TextViewStyle { /// Base font size for headings, default is 14px. pub heading_base_font_size: Pixels, /// Highlight theme for code blocks. Default: [`HighlightTheme::default_light()`] - pub highlight_theme: Rc, + pub highlight_theme: Arc, pub is_dark: bool, } @@ -105,7 +105,7 @@ impl Default for TextViewStyle { Self { paragraph_gap: rems(1.), heading_base_font_size: px(14.), - highlight_theme: Rc::new(HighlightTheme::default_light().clone()), + highlight_theme: HighlightTheme::default_light().clone(), is_dark: false, } } diff --git a/crates/ui/src/theme/mod.rs b/crates/ui/src/theme/mod.rs index d6085dec..6e576c65 100644 --- a/crates/ui/src/theme/mod.rs +++ b/crates/ui/src/theme/mod.rs @@ -4,17 +4,23 @@ use schemars::JsonSchema; use serde::{Deserialize, Serialize}; use std::{ ops::{Deref, DerefMut}, + rc::Rc, sync::Arc, }; mod color; +mod registry; mod schema; mod theme_color; + pub use color::*; +pub use registry::*; pub use schema::*; pub use theme_color::*; pub fn init(cx: &mut App) { + registry::init(cx); + Theme::sync_system_appearance(None, cx); Theme::sync_scrollbar_appearance(cx); } @@ -33,11 +39,9 @@ impl ActiveTheme for App { #[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)] pub struct Theme { pub colors: ThemeColor, - pub light_theme: ThemeColor, - pub dark_theme: ThemeColor, pub highlight_theme: Arc, - pub light_highlight_theme: Arc, - pub dark_highlight_theme: Arc, + pub light_theme: Rc, + pub dark_theme: Rc, pub mode: ThemeMode, pub font_family: SharedString, @@ -58,7 +62,7 @@ pub struct Theme { impl Default for Theme { fn default() -> Self { - Self::from(ThemeColor::light()) + Self::from(ThemeColor::default()) } } @@ -97,21 +101,21 @@ impl Theme { self.mode.is_dark() } - /// Sets the theme to default light. - pub fn set_default_light(&mut self) { - self.light_theme = ThemeColor::light(); - self.colors = ThemeColor::light(); - self.light_highlight_theme = Arc::new(HighlightTheme::default_light()); - self.highlight_theme = self.light_highlight_theme.clone(); - } + // /// Sets the theme to default light. + // pub fn set_default_light(&mut self) { + // self.light_theme = ThemeColor::light(); + // self.colors = ThemeColor::light(); + // self.light_highlight_theme = Arc::new(HighlightTheme::default_light()); + // self.highlight_theme = self.light_highlight_theme.clone(); + // } - /// Sets the theme to default dark. - pub fn set_default_dark(&mut self) { - self.dark_theme = ThemeColor::dark(); - self.colors = ThemeColor::dark(); - self.dark_highlight_theme = Arc::new(HighlightTheme::default_dark()); - self.highlight_theme = self.dark_highlight_theme.clone(); - } + // /// Sets the theme to default dark. + // pub fn set_default_dark(&mut self) { + // self.dark_theme = ThemeColor::dark(); + // self.colors = ThemeColor::dark(); + // self.dark_highlight_theme = Arc::new(HighlightTheme::default_dark()); + // self.highlight_theme = self.dark_highlight_theme.clone(); + // } /// Sync the theme with the system appearance pub fn sync_system_appearance(window: Option<&mut Window>, cx: &mut App) { @@ -137,23 +141,19 @@ impl Theme { pub fn change(mode: impl Into, window: Option<&mut Window>, cx: &mut App) { let mode = mode.into(); if !cx.has_global::() { - let colors = match mode { - ThemeMode::Light => ThemeColor::light(), - ThemeMode::Dark => ThemeColor::dark(), - }; - let mut theme = Theme::from(colors); - theme.light_theme = ThemeColor::light(); - theme.dark_theme = ThemeColor::dark(); + let mut theme = Theme::default(); + theme.light_theme = ThemeRegistry::global(cx).default_light_theme().clone(); + theme.dark_theme = ThemeRegistry::global(cx).default_dark_theme().clone(); cx.set_global(theme); } let theme = cx.global_mut::(); theme.mode = mode; - theme.colors = if mode.is_dark() { - theme.dark_theme + if mode.is_dark() { + theme.apply_config(&theme.dark_theme.clone()); } else { - theme.light_theme - }; + theme.apply_config(&theme.light_theme.clone()); + } if let Some(window) = window { window.refresh(); @@ -163,12 +163,8 @@ impl Theme { impl From for Theme { fn from(colors: ThemeColor) -> Self { - let mode = ThemeMode::default(); - let light_highlight_theme = Arc::new(HighlightTheme::default_light()); - let dark_highlight_theme = Arc::new(HighlightTheme::default_dark()); - Theme { - mode, + mode: ThemeMode::default(), transparent: Hsla::transparent_black(), font_size: px(16.), font_family: if cfg!(target_os = "macos") { @@ -185,11 +181,9 @@ impl From for Theme { tile_grid_size: px(8.), tile_shadow: true, colors, - light_theme: ThemeColor::light(), - dark_theme: ThemeColor::dark(), - highlight_theme: light_highlight_theme.clone(), - light_highlight_theme: light_highlight_theme, - dark_highlight_theme: dark_highlight_theme, + light_theme: Rc::new(ThemeConfig::default()), + dark_theme: Rc::new(ThemeConfig::default()), + highlight_theme: HighlightTheme::default_light(), } } } diff --git a/crates/ui/src/theme/registry.rs b/crates/ui/src/theme/registry.rs new file mode 100644 index 00000000..c327492f --- /dev/null +++ b/crates/ui/src/theme/registry.rs @@ -0,0 +1,256 @@ +use crate::{highlighter::HighlightTheme, Theme, ThemeColor, ThemeConfig, ThemeMode, ThemeSet}; +use anyhow::Result; +use gpui::{App, Global, SharedString}; +use notify::Watcher as _; +use std::{ + collections::HashMap, + fs, + path::PathBuf, + rc::Rc, + sync::{Arc, LazyLock}, +}; + +const DEFAULT_THEME: &str = include_str!("../../../../themes/default.json"); +pub(crate) const DEFAULT_THEME_COLORS: LazyLock< + HashMap, Arc)>, +> = LazyLock::new(|| { + let mut colors = HashMap::new(); + + let themes: Vec = serde_json::from_str::(DEFAULT_THEME) + .expect("Failed to parse themes/default.json") + .themes; + + for theme in themes { + let mut theme_color = ThemeColor::default(); + theme_color.apply_config(&theme, &ThemeColor::default()); + + let highlight_theme = HighlightTheme { + name: theme.name.to_string(), + appearance: theme.mode, + style: theme.highlight.unwrap_or_default(), + }; + + colors.insert( + theme.mode, + (Arc::new(theme_color), Arc::new(highlight_theme)), + ); + } + + colors +}); + +pub(super) fn init(cx: &mut App) { + cx.set_global(ThemeRegistry::default()); + ThemeRegistry::global_mut(cx).init_default_themes(); + + // Observe changes to the theme registry to apply changes to the active theme + cx.observe_global::(|cx| { + tracing::info!("Reload active theme..."); + let light_theme = Theme::global(cx).light_theme.name.clone(); + let dark_theme = Theme::global(cx).dark_theme.name.clone(); + if let Some(theme) = ThemeRegistry::global(cx) + .themes() + .get(&light_theme) + .cloned() + { + Theme::global_mut(cx).light_theme = theme; + } + if let Some(theme) = ThemeRegistry::global(cx).themes().get(&dark_theme).cloned() { + Theme::global_mut(cx).dark_theme = theme; + } + let mode = Theme::global(cx).mode; + + Theme::change(mode, None, cx); + cx.refresh_windows(); + }) + .detach(); +} + +#[derive(Default, Debug)] +pub struct ThemeRegistry { + themes_dir: PathBuf, + default_themes: HashMap>, + themes: HashMap>, + has_custom_themes: bool, +} + +impl Global for ThemeRegistry {} + +impl ThemeRegistry { + pub fn global(cx: &App) -> &Self { + cx.global::() + } + + pub fn global_mut(cx: &mut App) -> &mut Self { + cx.global_mut::() + } + + /// Watch themes directory. + /// + /// And reload themes to trigger the `on_load` callback. + pub fn watch_dir(themes_dir: PathBuf, cx: &mut App, on_load: F) -> Result<()> + where + F: Fn(&mut App) + 'static, + { + Self::global_mut(cx).themes_dir = themes_dir.clone(); + + // Load theme in the background. + cx.spawn(async move |cx| { + _ = cx.update(|cx| { + if let Err(err) = Self::_watch_themes_dir(themes_dir, cx) { + tracing::error!("Failed to watch themes directory: {}", err); + } + + Self::reload_themes(cx); + on_load(cx); + }); + }) + .detach(); + + Ok(()) + } + + /// Returns a reference to the map of themes (including default themes). + pub fn themes(&self) -> &HashMap> { + &self.themes + } + + /// Returns a sorted list of themes. + pub fn sorted_themes(&self) -> Vec<&Rc> { + let mut themes = self.themes.values().collect::>(); + // sort by is_default true first, then light first dark later, then by name case-insensitive + themes.sort_by(|a, b| { + b.is_default + .cmp(&a.is_default) + .then(a.name.to_lowercase().cmp(&b.name.to_lowercase())) + }); + themes + } + + /// Returns a reference to the map of default themes. + pub fn default_themes(&self) -> &HashMap> { + &self.default_themes + } + + pub fn default_light_theme(&self) -> &Rc { + &self.default_themes[&ThemeMode::Light] + } + + pub fn default_dark_theme(&self) -> &Rc { + &self.default_themes[&ThemeMode::Dark] + } + + fn init_default_themes(&mut self) { + let default_themes: Vec = serde_json::from_str::(DEFAULT_THEME) + .expect("failed to parse default theme.") + .themes; + for theme in default_themes.into_iter() { + if theme.mode.is_dark() { + self.default_themes.insert(ThemeMode::Dark, Rc::new(theme)); + } else { + self.default_themes.insert(ThemeMode::Light, Rc::new(theme)); + } + } + self.themes = self + .default_themes + .values() + .map(|theme| { + let name = theme.name.clone(); + (name, Rc::clone(theme)) + }) + .collect(); + } + + fn _watch_themes_dir(themes_dir: PathBuf, cx: &mut App) -> anyhow::Result<()> { + if !themes_dir.exists() { + fs::create_dir_all(&themes_dir)?; + } + + let (tx, rx) = smol::channel::bounded(100); + let mut watcher = + notify::recommended_watcher(move |res: notify::Result| { + if let Ok(event) = &res { + match event.kind { + notify::EventKind::Create(_) + | notify::EventKind::Modify(_) + | notify::EventKind::Remove(_) => { + if let Err(err) = tx.send_blocking(res) { + tracing::error!("Failed to send theme event: {:?}", err); + } + } + _ => {} + } + } + })?; + + cx.spawn(async move |cx| { + if let Err(err) = watcher.watch(&themes_dir, notify::RecursiveMode::Recursive) { + tracing::error!("Failed to watch themes directory: {:?}", err); + } + + while (rx.recv().await).is_ok() { + tracing::info!("Reloading themes..."); + _ = cx.update(Self::reload_themes); + } + }) + .detach(); + + Ok(()) + } + + fn reload_themes(cx: &mut App) { + let registry = Self::global_mut(cx); + match registry.reload() { + Ok(_) => { + tracing::info!("Themes reloaded successfully."); + } + Err(e) => tracing::error!("Failed to reload themes: {:?}", e), + } + } + + /// Reload themes from the `themes_dir`. + fn reload(&mut self) -> Result<()> { + let mut themes = vec![]; + + if self.themes_dir.exists() { + for entry in fs::read_dir(&self.themes_dir)? { + let entry = entry?; + let path = entry.path(); + if path.is_file() && path.extension().and_then(|s| s.to_str()) == Some("json") { + let file_content = fs::read_to_string(path.clone())?; + + match serde_json::from_str::(&file_content) { + Ok(theme_set) => { + themes.extend(theme_set.themes); + } + Err(e) => { + tracing::error!( + "ignored invalid theme file: {}, {}", + path.display(), + e + ); + } + } + } + } + } + + self.themes.clear(); + for theme in themes.iter() { + if self.themes.contains_key(&theme.name) { + continue; + } + + if theme.is_default { + self.default_themes + .insert(theme.mode, Rc::new(theme.clone())); + } + + self.has_custom_themes = true; + self.themes + .insert(theme.name.clone(), Rc::new(theme.clone())); + } + + Ok(()) + } +} diff --git a/crates/ui/src/theme/schema.rs b/crates/ui/src/theme/schema.rs index d55f7b04..96febf83 100644 --- a/crates/ui/src/theme/schema.rs +++ b/crates/ui/src/theme/schema.rs @@ -1,4 +1,4 @@ -use std::sync::Arc; +use std::{rc::Rc, sync::Arc}; use anyhow::Result; use gpui::{Hsla, SharedString}; @@ -32,6 +32,8 @@ pub struct ThemeSet { #[derive(Debug, Clone, Default, Serialize, Deserialize, JsonSchema)] #[serde(default)] pub struct ThemeConfig { + /// Whether this theme is the default theme. + pub is_default: bool, /// The name of the theme. pub name: SharedString, /// The mode of the theme, default is light. @@ -379,15 +381,10 @@ fn try_parse_color(color: &str) -> Result { Ok(rgba.into()) } -impl Theme { - /// Apply the given theme configuration to the current theme. - pub fn apply_config(&mut self, config: &ThemeConfig) { +impl ThemeColor { + /// Create a new `ThemeColor` from a `ThemeConfig`. + pub(crate) fn apply_config(&mut self, config: &ThemeConfig, default_theme: &ThemeColor) { let colors = config.colors.clone(); - let default_theme = if config.mode.is_dark() { - ThemeColor::dark() - } else { - ThemeColor::light() - }; macro_rules! apply_color { ($config_field:ident) => { @@ -413,8 +410,6 @@ impl Theme { }; } - self.mode = config.mode; - // Base colors for fallback apply_color!(red); apply_color!(red_light, fallback = self.red.opacity(0.8)); @@ -535,15 +530,19 @@ impl Theme { // TODO: Apply default fallback colors to highlight. // Ensure opacity for list_active, table_active - self.colors.list_active = self.colors.list_active.alpha(0.2); - self.colors.table_active = self.colors.table_active.alpha(0.2); + self.list_active = self.list_active.alpha(0.2); + self.table_active = self.table_active.alpha(0.2); + } +} +impl Theme { + /// Apply the given theme configuration to the current theme. + pub fn apply_config(&mut self, config: &Rc) { if config.mode.is_dark() { - self.dark_theme = self.colors; + self.dark_theme = config.clone(); } else { - self.light_theme = self.colors; + self.light_theme = config.clone(); } - if let Some(style) = &config.highlight { let highlight_theme = Arc::new(HighlightTheme { name: config.name.to_string(), @@ -551,12 +550,15 @@ impl Theme { style: style.clone(), }); self.highlight_theme = highlight_theme.clone(); - if config.mode.is_dark() { - self.dark_highlight_theme = highlight_theme; - } else { - self.light_highlight_theme = highlight_theme; - } } + + let default_theme = if config.mode.is_dark() { + ThemeColor::dark() + } else { + ThemeColor::light() + }; + + self.colors.apply_config(&config, &default_theme); } } diff --git a/crates/ui/src/theme/theme_color.rs b/crates/ui/src/theme/theme_color.rs index 09e43c57..50dae591 100644 --- a/crates/ui/src/theme/theme_color.rs +++ b/crates/ui/src/theme/theme_color.rs @@ -1,5 +1,8 @@ -use super::color::*; -use gpui::{transparent_black, Hsla}; +use std::sync::Arc; + +use crate::{theme::DEFAULT_THEME_COLORS, ThemeMode}; + +use gpui::Hsla; use schemars::JsonSchema; use serde::{Deserialize, Serialize}; @@ -213,225 +216,11 @@ pub struct ThemeColor { } impl ThemeColor { - pub fn light() -> Self { - Self { - accent: neutral_100(), - accent_foreground: neutral_900(), - accordion: white(), - accordion_active: neutral_200(), - accordion_hover: neutral_100(), - background: white(), - border: neutral_200(), - group_box: neutral_100(), - group_box_foreground: neutral_900(), - caret: neutral_950(), - chart_1: blue_300(), - chart_2: blue_500(), - chart_3: blue_600(), - chart_4: blue_700(), - chart_5: blue_800(), - danger: red_500(), - danger_active: red_600(), - danger_foreground: red_50(), - danger_hover: red_500().opacity(0.9), - description_list_label: neutral_100(), - description_list_label_foreground: neutral_900(), - drag_border: blue_500(), - drop_target: blue_500().opacity(0.25), - foreground: neutral_950(), - info: sky_500(), - info_active: sky_600(), - info_hover: sky_500().opacity(0.9), - info_foreground: sky_50(), - input: neutral_200(), - link: neutral_950(), - link_active: neutral_950(), - link_hover: neutral_800(), - list: white(), - list_active: blue_200().opacity(0.2), - list_active_border: blue_400(), - list_even: neutral_50(), - list_head: neutral_50(), - list_hover: neutral_100(), - muted: neutral_100(), - muted_foreground: neutral_500(), - popover: white(), - popover_foreground: neutral_950(), - primary: neutral_900(), - primary_active: neutral_950(), - primary_foreground: neutral_50(), - primary_hover: neutral_800(), - progress_bar: neutral_900(), - ring: neutral_950(), - scrollbar: neutral_50().opacity(0.5), - scrollbar_thumb: neutral_400().opacity(0.9), - scrollbar_thumb_hover: neutral_400(), - secondary: neutral_100(), - secondary_active: neutral_200(), - secondary_foreground: neutral_900(), - secondary_hover: neutral_100().opacity(0.5), - selection: blue_200(), - sidebar: neutral_50(), - sidebar_accent: neutral_200(), - sidebar_accent_foreground: neutral_900(), - sidebar_border: neutral_200(), - sidebar_foreground: neutral_900(), - sidebar_primary: neutral_900(), - sidebar_primary_foreground: neutral_50(), - skeleton: neutral_100(), - slider_bar: neutral_900(), - slider_thumb: white(), - success: green_500(), - success_active: green_600(), - success_hover: green_500().opacity(0.9), - success_foreground: gray_50(), - switch: neutral_300(), - tab: gpui::transparent_black(), - tab_active: white(), - tab_active_foreground: neutral_900(), - tab_bar: neutral_100(), - tab_bar_segmented: neutral_100(), - tab_foreground: neutral_700(), - table: white(), - table_active: blue_200().opacity(0.2), - table_active_border: blue_400(), - table_even: neutral_50(), - table_head: neutral_50(), - table_head_foreground: neutral_500(), - table_hover: neutral_100(), - table_row_border: neutral_200().opacity(0.7), - tiles: neutral_50(), - title_bar: white(), - title_bar_border: neutral_200(), - warning: yellow_500(), - warning_active: yellow_600(), - warning_hover: yellow_500().opacity(0.9), - warning_foreground: gray_50(), - overlay: black().opacity(0.05), - window_border: neutral_200(), - red: red_500(), - red_light: red_200(), - green: green_500(), - green_light: green_200(), - blue: blue_500(), - blue_light: blue_200(), - yellow: yellow_500(), - yellow_light: yellow_200(), - magenta: purple_500(), - magenta_light: purple_200(), - cyan: cyan_500(), - cyan_light: cyan_200(), - } + pub fn light() -> Arc { + DEFAULT_THEME_COLORS[&ThemeMode::Light].0.clone() } - pub fn dark() -> Self { - Self { - accent: neutral_900(), - accent_foreground: neutral_50(), - accordion: neutral_950(), - accordion_active: neutral_800(), - accordion_hover: neutral_800().opacity(0.9), - background: neutral_950(), - border: neutral_800(), - group_box: neutral_900(), - group_box_foreground: neutral_50(), - caret: neutral_50(), - chart_1: blue_300(), - chart_2: blue_500(), - chart_3: blue_600(), - chart_4: blue_700(), - chart_5: blue_800(), - danger: red_900(), - danger_active: red_900().darken(0.2), - danger_foreground: red_50(), - danger_hover: red_900().lighten(0.1), - description_list_label: neutral_900(), - description_list_label_foreground: neutral_100(), - drag_border: blue_500(), - drop_target: blue_500().opacity(0.1), - foreground: neutral_50(), - info: sky_900(), - info_active: sky_900().darken(0.2), - info_foreground: sky_50(), - info_hover: sky_900().lighten(0.1), - input: neutral_800(), - link: neutral_50(), - link_active: neutral_50().darken(0.2), - link_hover: neutral_50().lighten(0.2), - list: neutral_950(), - list_active: blue_800().opacity(0.2), - list_active_border: blue_700(), - list_even: neutral_900().opacity(0.8), - list_head: neutral_900().opacity(0.8), - list_hover: neutral_800(), - muted: neutral_800(), - muted_foreground: neutral_500(), - popover: neutral_950(), - popover_foreground: neutral_50(), - primary: neutral_50(), - primary_active: neutral_300(), - primary_foreground: neutral_900(), - primary_hover: neutral_200(), - progress_bar: neutral_100(), - ring: neutral_300(), - scrollbar: neutral_900().opacity(0.5), - scrollbar_thumb: neutral_700().opacity(0.9), - scrollbar_thumb_hover: neutral_700(), - secondary: neutral_900(), - secondary_active: neutral_800(), - secondary_foreground: neutral_50(), - secondary_hover: neutral_900().lighten(0.1), - selection: blue_700(), - sidebar: neutral_950(), - sidebar_accent: neutral_800(), - sidebar_accent_foreground: neutral_100(), - sidebar_border: neutral_800(), - sidebar_foreground: neutral_100(), - sidebar_primary: neutral_100(), - sidebar_primary_foreground: neutral_950(), - skeleton: neutral_900(), - slider_bar: neutral_50(), - slider_thumb: neutral_950(), - success: green_900(), - success_active: green_900().darken(0.2), - success_foreground: green_50(), - success_hover: green_900().lighten(0.1), - switch: neutral_700(), - tab: transparent_black(), - tab_active: neutral_950(), - tab_active_foreground: neutral_50(), - tab_bar: neutral_900(), - tab_bar_segmented: neutral_900(), - tab_foreground: neutral_300(), - table: neutral_950(), - table_active: blue_800().opacity(0.2), - table_active_border: blue_700(), - table_even: neutral_900().opacity(0.8), - table_head: neutral_900().opacity(0.8), - table_head_foreground: neutral_600(), - table_hover: neutral_800(), - table_row_border: neutral_800().opacity(0.7), - tiles: neutral_900(), - title_bar: neutral_950(), - title_bar_border: neutral_800(), - warning: yellow_900(), - warning_active: yellow_900().darken(0.2), - warning_foreground: yellow_50(), - warning_hover: yellow_900().lighten(0.1), - overlay: white().opacity(0.03), - window_border: neutral_800(), - red: red_500(), - red_light: red_200(), - green: green_500(), - green_light: green_200(), - blue: blue_500(), - blue_light: blue_200(), - yellow: yellow_500(), - yellow_light: yellow_200(), - magenta: purple_500(), - magenta_light: purple_200(), - cyan: cyan_500(), - cyan_light: cyan_200(), - } + pub fn dark() -> Arc { + DEFAULT_THEME_COLORS[&ThemeMode::Dark].0.clone() } } diff --git a/themes/default.json b/themes/default.json new file mode 100644 index 00000000..03bd73ac --- /dev/null +++ b/themes/default.json @@ -0,0 +1,423 @@ +{ + "$schema": "https://github.com/longbridge/gpui-component/raw/refs/heads/main/.theme-schema.json", + "name": "Default", + "author": "shadcn", + "url": "https://ui.shadcn.com", + "themes": [ + { + "is_default": true, + "name": "Default Light", + "mode": "light", + "colors": { + "accent.background": "#f5f5f5", + "accent.foreground": "#171717", + "accordion.background": "#ffffff", + "accordion.active.background": "#e5e5e5", + "accordion.hover.background": "#f5f5f5", + "background": "#ffffff", + "border": "#e5e5e5", + "group_box.background": "#f5f5f5", + "group_box.foreground": "#171717", + "caret": "#0a0a0a", + "chart_1": "#93c5fd", + "chart_2": "#3b82f6", + "chart_3": "#2563eb", + "chart_4": "#1d4ed8", + "chart_5": "#1e40af", + "danger.background": "#ef4444", + "danger.active.background": "#dc2626", + "danger.foreground": "#fef2f2", + "danger.hover.background": "#ef444499", + "description_list_label.background": "#f5f5f5", + "description_list_label.foreground": "#171717", + "drag_border": "#3b82f6", + "drop_target.background": "#3b82f640", + "foreground": "#0a0a0a", + "info.background": "#0ea5e9", + "info.active.background": "#0284c7", + "info.foreground": "#f0f9ff", + "info.hover.background": "#0ea5e999", + "input.border": "#e5e5e5", + "link.foreground": "#0a0a0a", + "link.active.foreground": "#0a0a0a", + "link.hover.foreground": "#404040", + "list.background": "#ffffff", + "list.active.background": "#bfdbfe33", + "list.active.border": "#60a5fa", + "list.even.background": "#fafafa", + "list.head.background": "#fafafa", + "list.hover.background": "#f5f5f5", + "muted.background": "#f5f5f5", + "muted.foreground": "#737373", + "popover.background": "#ffffff", + "popover.foreground": "#0a0a0a", + "primary.background": "#171717", + "primary.active.background": "#0a0a0a", + "primary.foreground": "#fafafa", + "primary.hover.background": "#404040", + "progress_bar.background": "#171717", + "ring": "#0a0a0a", + "scrollbar.background": "#fafafa80", + "scrollbar.thumb.background": "#a3a3a3e6", + "scrollbar.thumb.hover.background": "#a3a3a3", + "secondary.background": "#f5f5f5", + "secondary.active.background": "#e5e5e5", + "secondary.foreground": "#171717", + "secondary.hover.background": "#f5f5f580", + "selection.background": "#bfdbfe", + "sidebar.background": "#fafafa", + "sidebar.accent.background": "#e5e5e5", + "sidebar.accent.foreground": "#171717", + "sidebar.border": "#e5e5e5", + "sidebar.foreground": "#171717", + "sidebar.primary.background": "#171717", + "sidebar.primary.foreground": "#fafafa", + "skeleton.background": "#f5f5f5", + "slider.bar.background": "#171717", + "slider.thumb.background": "#ffffff", + "success.background": "#22c55e", + "success.active.background": "#16a34a", + "success.foreground": "#f9fafb", + "success.hover.background": "#22c55e99", + "switch.background": "#d4d4d4", + "tab.background": "#00000000", + "tab.active.background": "#ffffff", + "tab.active.foreground": "#171717", + "tab_bar.background": "#f5f5f5", + "tab_bar.segmented.background": "#f5f5f5", + "tab.foreground": "#404040", + "table.background": "#ffffff", + "table.active.background": "#bfdbfe33", + "table.active.border": "#60a5fa", + "table.even.background": "#fafafa", + "table.head.background": "#fafafa", + "table.head.foreground": "#737373", + "table.hover.background": "#f5f5f5", + "table.row.border": "#e5e5e5b3", + "tiles.background": "#fafafa", + "title_bar.background": "#ffffff", + "title_bar.border": "#e5e5e5", + "warning.background": "#eab308", + "warning.active.background": "#ca8a04", + "warning.hover.background": "#eab30899", + "warning.foreground": "#f9fafb", + "overlay.background": "#0000000d", + "window.border": "#e5e5e5", + "base.red": "#ef4444", + "base.red.light": "#fecaca", + "base.green": "#22c55e", + "base.green.light": "#bbf7d0", + "base.blue": "#3b82f6", + "base.blue.light": "#bfdbfe", + "base.yellow": "#eab308", + "base.yellow.light": "#fef08a", + "base.magenta": "#9333ea", + "base.magenta.light": "#e9d5ff", + "base.cyan": "#06b6d4", + "base.cyan.light": "#cffafe" + }, + "highlight": { + "editor.foreground": "#000000", + "editor.background": "#ffffff", + "editor.active_line.background": "#F5F5F5", + "editor.line_number": "#929292", + "editor.active_line_number": "#000000", + "conflict": "#C5060B", + "created": "#1642FF", + "hidden": "#6D6D6D", + "hint": "#9e5dff", + "modified": "#9e7008", + "predictive": "#A4ABB6", + "warning": "#C99401", + "syntax": { + "attribute": { + "color": "#957931" + }, + "boolean": { + "color": "#C5060B" + }, + "comment": { + "color": "#007fff" + }, + "comment.doc": { + "color": "#007fff" + }, + "constant": { + "color": "#C5060B" + }, + "constructor": { + "color": "#0433ff" + }, + "embedded": { + "color": "#333333" + }, + "function": { + "color": "#0000A2" + }, + "keyword": { + "color": "#0433ff" + }, + "link_text": { + "color": "#0000A2", + "font_style": "normal" + }, + "link_uri": { + "color": "#6A7293", + "font_style": "italic" + }, + "number": { + "color": "#0433ff" + }, + "string": { + "color": "#036A07" + }, + "string.escape": { + "color": "#036A07" + }, + "string.regex": { + "color": "#036A07" + }, + "string.special": { + "color": "#d21f07" + }, + "string.special.symbol": { + "color": "#d21f07" + }, + "tag": { + "color": "#0433ff" + }, + "text.literal": { + "color": "#6F42C1" + }, + "title": { + "color": "#0433FF" + }, + "type": { + "color": "#6f42c1" + }, + "property": { + "color": "#333333" + }, + "variable": { + "color": "#333333" + }, + "variable.special": { + "color": "#C5060B" + } + } + } + }, + { + "is_default": true, + "name": "Default Dark", + "mode": "dark", + "colors": { + "accent.background": "#171717", + "accent.foreground": "#fafafa", + "accordion.background": "#0a0a0a", + "accordion.active.background": "#262626", + "accordion.hover.background": "#262626e6", + "background": "#0a0a0a", + "border": "#262626", + "group_box.background": "#171717", + "group_box.foreground": "#fafafa", + "caret": "#fafafa", + "chart_1": "#93c5fd", + "chart_2": "#3b82f6", + "chart_3": "#2563eb", + "chart_4": "#1d4ed8", + "chart_5": "#1e40af", + "danger.background": "#7f1d1d", + "danger.active.background": "#991b1b", + "danger.foreground": "#fef2f2", + "danger.hover.background": "#b91c1c", + "description_list_label.background": "#171717", + "description_list_label.foreground": "#f5f5f5", + "drag_border": "#3b82f6", + "drop_target.background": "#3b82f619", + "foreground": "#fafafa", + "info.background": "#0c4a6e", + "info.active.background": "#075985", + "info.foreground": "#f0f9ff", + "info.hover.background": "#0369a1", + "input.border": "#262626", + "link.foreground": "#fafafa", + "link.active.foreground": "#d4d4d4", + "link.hover.foreground": "#ffffff", + "list.background": "#0a0a0a", + "list.active.background": "#1e40af33", + "list.active.border": "#1d4ed8", + "list.even.background": "#171717cc", + "list.head.background": "#171717cc", + "list.hover.background": "#262626", + "muted.background": "#262626", + "muted.foreground": "#737373", + "popover.background": "#0a0a0a", + "popover.foreground": "#fafafa", + "primary.background": "#fafafa", + "primary.active.background": "#d4d4d4", + "primary.foreground": "#171717", + "primary.hover.background": "#e5e5e5", + "progress_bar.background": "#f5f5f5", + "ring": "#d4d4d4", + "scrollbar.background": "#17171780", + "scrollbar.thumb.background": "#525252e6", + "scrollbar.thumb.hover.background": "#525252", + "secondary.background": "#171717", + "secondary.active.background": "#262626", + "secondary.foreground": "#fafafa", + "secondary.hover.background": "#1a1a1a", + "selection.background": "#1d4ed8", + "sidebar.background": "#0a0a0a", + "sidebar.accent.background": "#262626", + "sidebar.accent.foreground": "#f5f5f5", + "sidebar.border": "#262626", + "sidebar.foreground": "#f5f5f5", + "sidebar.primary.background": "#f5f5f5", + "sidebar.primary.foreground": "#0a0a0a", + "skeleton.background": "#171717", + "slider.bar.background": "#fafafa", + "slider.thumb.background": "#0a0a0a", + "success.background": "#14532d", + "success.active.background": "#166534", + "success.foreground": "#f0fdf4", + "success.hover.background": "#15803d", + "switch.background": "#404040", + "tab.background": "#00000000", + "tab.active.background": "#0a0a0a", + "tab.active.foreground": "#fafafa", + "tab_bar.background": "#171717", + "tab_bar.segmented.background": "#171717", + "tab.foreground": "#d4d4d4", + "table.background": "#0a0a0a", + "table.active.background": "#1e40af33", + "table.active.border": "#1d4ed8", + "table.even.background": "#171717cc", + "table.head.background": "#171717cc", + "table.head.foreground": "#525252", + "table.hover.background": "#262626", + "table.row.border": "#262626b3", + "tiles.background": "#171717", + "title_bar.background": "#0a0a0a", + "title_bar.border": "#262626", + "warning.background": "#713f12", + "warning.active.background": "#854d0e", + "warning.foreground": "#fefce8", + "warning.hover.background": "#a16207", + "overlay.background": "#ffffff08", + "window.border": "#262626", + "base.red": "#ef4444", + "base.red.light": "#fecaca", + "base.green": "#22c55e", + "base.green.light": "#bbf7d0", + "base.blue": "#3b82f6", + "base.blue.light": "#bfdbfe", + "base.yellow": "#eab308", + "base.yellow.light": "#fef08a", + "base.magenta": "#9333ea", + "base.magenta.light": "#e9d5ff", + "base.cyan": "#06b6d4", + "base.cyan.light": "#cffafe" + }, + "highlight": { + "editor.foreground": "#CACCCA", + "editor.background": "#131313", + "editor.active_line.background": "#353436", + "editor.line_number": "#8F8F8F", + "editor.active_line_number": "#DDDDDD", + "conflict": "#D2602D", + "created": "#3f72e2", + "created.background": "#0C4619", + "deleted.background": "#46190C", + "error.background": "#46190C", + "error.border": "#802207", + "hidden": "#9E9E9E", + "hint": "#b283f8", + "hint.background": "#250c4b", + "hint.border": "#3f0891", + "info.background": "#0C194D", + "info.border": "#082190", + "modified": "#B0A878", + "modified.background": "#3A310E", + "predictive": "#5D5945", + "success.background": "#0C4619", + "warning.background": "#3A310E", + "warning.border": "#7B6508", + "syntax": { + "attribute": { + "color": "#be9a52" + }, + "boolean": { + "color": "#E1D797" + }, + "comment": { + "color": "#9E9E9E" + }, + "comment.doc": { + "color": "#9E9E9E" + }, + "constant": { + "color": "#E1D797" + }, + "constructor": { + "color": "#b5af9a" + }, + "embedded": { + "color": "#CACCCA" + }, + "function": { + "color": "#E1D797" + }, + "keyword": { + "color": "#E19773" + }, + "link_text": { + "color": "#A86D3B", + "font_style": "normal" + }, + "link_uri": { + "color": "#6F6D66", + "font_style": "italic" + }, + "number": { + "color": "#E19773" + }, + "string": { + "color": "#76BA53" + }, + "string.escape": { + "color": "#76BA53" + }, + "string.regex": { + "color": "#76BA53" + }, + "string.special": { + "color": "#E1D797" + }, + "string.special.symbol": { + "color": "#E1D797" + }, + "tag": { + "color": "#b5af9a" + }, + "text.literal": { + "color": "#E1D797" + }, + "title": { + "color": "#A76D3B", + "font_weight": 600 + }, + "type": { + "color": "#A86D3B" + }, + "property": { + "color": "#CACCCA" + }, + "variable.special": { + "color": "#E19773" + } + } + } + } + ] +} \ No newline at end of file