theme: Add ThemeRegistry to load and watch themes dir. (#1148)

This commit is contained in:
Jason Lee 2025-08-18 19:29:27 +08:00 committed by GitHub
parent dcca730d65
commit 6ae7558c44
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
15 changed files with 869 additions and 738 deletions

87
Cargo.lock generated
View file

@ -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",

View file

@ -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"]

View file

@ -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()
},

View file

@ -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::<State>(&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::<State>(&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<HashMap<SharedString, ThemeConfig>> = 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::<Vec<&SharedString>>();
names.sort();
let names = ThemeRegistry::global(cx)
.sorted_themes()
.iter()
.map(|theme| theme.name.clone())
.collect::<Vec<SharedString>>();
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,

View file

@ -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"

View file

@ -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<HighlightTheme> = LazyLock::new(|| {
let json = include_str!("./themes/dark.json");
serde_json::from_str(json).unwrap()
});
const DEFAULT_LIGHT: LazyLock<HighlightTheme> = 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<Self> {
DEFAULT_THEME_COLORS[&ThemeMode::Dark].1.clone()
}
pub fn default_light() -> Self {
DEFAULT_LIGHT.clone()
pub fn default_light() -> Arc<Self> {
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;

View file

@ -1 +0,0 @@
The default theme based on [zed-theme-macos-classic](https://github.com/huacnlee/zed-theme-macos-classic).

View file

@ -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
}
}
}
}

View file

@ -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
}
}
}
}

View file

@ -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<HighlightTheme>,
pub highlight_theme: Arc<HighlightTheme>,
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,
}
}

View file

@ -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<HighlightTheme>,
pub light_highlight_theme: Arc<HighlightTheme>,
pub dark_highlight_theme: Arc<HighlightTheme>,
pub light_theme: Rc<ThemeConfig>,
pub dark_theme: Rc<ThemeConfig>,
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<ThemeMode>, window: Option<&mut Window>, cx: &mut App) {
let mode = mode.into();
if !cx.has_global::<Theme>() {
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>();
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<ThemeColor> 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<ThemeColor> 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(),
}
}
}

View file

@ -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<ThemeMode, (Arc<ThemeColor>, Arc<HighlightTheme>)>,
> = LazyLock::new(|| {
let mut colors = HashMap::new();
let themes: Vec<ThemeConfig> = serde_json::from_str::<ThemeSet>(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::<ThemeRegistry>(|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<ThemeMode, Rc<ThemeConfig>>,
themes: HashMap<SharedString, Rc<ThemeConfig>>,
has_custom_themes: bool,
}
impl Global for ThemeRegistry {}
impl ThemeRegistry {
pub fn global(cx: &App) -> &Self {
cx.global::<Self>()
}
pub fn global_mut(cx: &mut App) -> &mut Self {
cx.global_mut::<Self>()
}
/// Watch themes directory.
///
/// And reload themes to trigger the `on_load` callback.
pub fn watch_dir<F>(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<SharedString, Rc<ThemeConfig>> {
&self.themes
}
/// Returns a sorted list of themes.
pub fn sorted_themes(&self) -> Vec<&Rc<ThemeConfig>> {
let mut themes = self.themes.values().collect::<Vec<_>>();
// 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<ThemeMode, Rc<ThemeConfig>> {
&self.default_themes
}
pub fn default_light_theme(&self) -> &Rc<ThemeConfig> {
&self.default_themes[&ThemeMode::Light]
}
pub fn default_dark_theme(&self) -> &Rc<ThemeConfig> {
&self.default_themes[&ThemeMode::Dark]
}
fn init_default_themes(&mut self) {
let default_themes: Vec<ThemeConfig> = serde_json::from_str::<ThemeSet>(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<notify::Event>| {
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::<ThemeSet>(&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(())
}
}

View file

@ -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<Hsla> {
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<ThemeConfig>) {
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);
}
}

View file

@ -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<Self> {
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<Self> {
DEFAULT_THEME_COLORS[&ThemeMode::Dark].0.clone()
}
}

423
themes/default.json Normal file
View file

@ -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"
}
}
}
}
]
}