Cargo.lock 🔗
@@ -7922,6 +7922,7 @@ dependencies = [
"color",
"derive_more",
"fs",
+ "futures 0.3.28",
"gpui",
"indexmap 1.9.3",
"itertools 0.11.0",
Marshall Bowers created
This PR adds support for loading user themes in Zed.
Themes are loaded from the `themes` directory under the Zed config:
`~/.config/zed/themes`. This directory should contain JSON files
containing a `ThemeFamilyContent`.
Here's an example of the general structure of a theme family file:
```jsonc
{
"name": "Vitesse",
"author": "Anthony Fu",
"themes": [
{
"name": "Vitesse Dark Soft",
"appearance": "dark",
"style": {
"border": "#252525",
// ...
}
}
]
}
```
Themes placed in this directory will be loaded and available in the
theme selector.
Release Notes:
- Added support for loading user themes from `~/.config/zed/themes`.
Cargo.lock | 1
crates/theme/Cargo.toml | 1
crates/theme/src/registry.rs | 104 +++++++++++++++-------
crates/theme/src/settings.rs | 24 +++++
crates/theme/src/theme.rs | 2
crates/theme_selector/src/theme_selector.rs | 2
crates/util/src/paths.rs | 1
crates/zed/src/main.rs | 32 ++++++
crates/zed/src/zed.rs | 2
9 files changed, 131 insertions(+), 38 deletions(-)
@@ -7922,6 +7922,7 @@ dependencies = [
"color",
"derive_more",
"fs",
+ "futures 0.3.28",
"gpui",
"indexmap 1.9.3",
"itertools 0.11.0",
@@ -23,6 +23,7 @@ doctest = false
anyhow.workspace = true
derive_more.workspace = true
fs = { path = "../fs" }
+futures.workspace = true
gpui = { path = "../gpui" }
indexmap = { version = "1.6.2", features = ["serde"] }
palette = { version = "0.7.3", default-features = false, features = ["std"] }
@@ -1,9 +1,13 @@
use std::collections::HashMap;
+use std::path::Path;
use std::sync::Arc;
use anyhow::{anyhow, Context, Result};
use derive_more::{Deref, DerefMut};
+use fs::Fs;
+use futures::StreamExt;
use gpui::{AppContext, AssetSource, HighlightStyle, SharedString};
+use parking_lot::RwLock;
use refineable::Refineable;
use util::ResultExt;
@@ -26,40 +30,41 @@ pub struct ThemeMeta {
///
/// This should not be exposed outside of this module.
#[derive(Default, Deref, DerefMut)]
-struct GlobalThemeRegistry(ThemeRegistry);
+struct GlobalThemeRegistry(Arc<ThemeRegistry>);
/// Initializes the theme registry.
pub fn init(assets: Box<dyn AssetSource>, cx: &mut AppContext) {
- cx.set_global(GlobalThemeRegistry(ThemeRegistry::new(assets)));
+ cx.set_global(GlobalThemeRegistry(Arc::new(ThemeRegistry::new(assets))));
+}
+
+struct ThemeRegistryState {
+ themes: HashMap<SharedString, Arc<Theme>>,
}
pub struct ThemeRegistry {
+ state: RwLock<ThemeRegistryState>,
assets: Box<dyn AssetSource>,
- themes: HashMap<SharedString, Arc<Theme>>,
}
impl ThemeRegistry {
/// Returns the global [`ThemeRegistry`].
- pub fn global(cx: &AppContext) -> &Self {
- cx.global::<GlobalThemeRegistry>()
- }
-
- /// Returns a mutable reference to the global [`ThemeRegistry`].
- pub fn global_mut(cx: &mut AppContext) -> &mut Self {
- cx.global_mut::<GlobalThemeRegistry>()
+ pub fn global(cx: &AppContext) -> Arc<Self> {
+ cx.global::<GlobalThemeRegistry>().0.clone()
}
- /// Returns a mutable reference to the global [`ThemeRegistry`].
+ /// Returns the global [`ThemeRegistry`].
///
/// Inserts a default [`ThemeRegistry`] if one does not yet exist.
- pub fn default_global(cx: &mut AppContext) -> &mut Self {
- cx.default_global::<GlobalThemeRegistry>()
+ pub fn default_global(cx: &mut AppContext) -> Arc<Self> {
+ cx.default_global::<GlobalThemeRegistry>().0.clone()
}
pub fn new(assets: Box<dyn AssetSource>) -> Self {
- let mut registry = Self {
+ let registry = Self {
+ state: RwLock::new(ThemeRegistryState {
+ themes: HashMap::new(),
+ }),
assets,
- themes: HashMap::new(),
};
// We're loading our new versions of the One themes by default, as
@@ -72,30 +77,27 @@ impl ThemeRegistry {
registry
}
- fn insert_theme_families(&mut self, families: impl IntoIterator<Item = ThemeFamily>) {
+ fn insert_theme_families(&self, families: impl IntoIterator<Item = ThemeFamily>) {
for family in families.into_iter() {
self.insert_themes(family.themes);
}
}
- fn insert_themes(&mut self, themes: impl IntoIterator<Item = Theme>) {
+ fn insert_themes(&self, themes: impl IntoIterator<Item = Theme>) {
+ let mut state = self.state.write();
for theme in themes.into_iter() {
- self.themes.insert(theme.name.clone(), Arc::new(theme));
+ state.themes.insert(theme.name.clone(), Arc::new(theme));
}
}
#[allow(unused)]
- fn insert_user_theme_families(
- &mut self,
- families: impl IntoIterator<Item = ThemeFamilyContent>,
- ) {
+ fn insert_user_theme_families(&self, families: impl IntoIterator<Item = ThemeFamilyContent>) {
for family in families.into_iter() {
self.insert_user_themes(family.themes);
}
}
- #[allow(unused)]
- fn insert_user_themes(&mut self, themes: impl IntoIterator<Item = ThemeContent>) {
+ pub fn insert_user_themes(&self, themes: impl IntoIterator<Item = ThemeContent>) {
self.insert_themes(themes.into_iter().map(|user_theme| {
let mut theme_colors = match user_theme.appearance {
AppearanceContent::Light => ThemeColors::light(),
@@ -186,28 +188,36 @@ impl ThemeRegistry {
}
pub fn clear(&mut self) {
- self.themes.clear();
+ self.state.write().themes.clear();
}
- pub fn list_names(&self, _staff: bool) -> impl Iterator<Item = SharedString> + '_ {
- self.themes.keys().cloned()
+ pub fn list_names(&self, _staff: bool) -> Vec<SharedString> {
+ self.state.read().themes.keys().cloned().collect()
}
- pub fn list(&self, _staff: bool) -> impl Iterator<Item = ThemeMeta> + '_ {
- self.themes.values().map(|theme| ThemeMeta {
- name: theme.name.clone(),
- appearance: theme.appearance(),
- })
+ pub fn list(&self, _staff: bool) -> Vec<ThemeMeta> {
+ self.state
+ .read()
+ .themes
+ .values()
+ .map(|theme| ThemeMeta {
+ name: theme.name.clone(),
+ appearance: theme.appearance(),
+ })
+ .collect()
}
pub fn get(&self, name: &str) -> Result<Arc<Theme>> {
- self.themes
+ self.state
+ .read()
+ .themes
.get(name)
.ok_or_else(|| anyhow!("theme not found: {}", name))
.cloned()
}
- pub fn load_user_themes(&mut self) {
+ /// Loads the themes bundled with the Zed binary and adds them to the registry.
+ pub fn load_bundled_themes(&self) {
let theme_paths = self
.assets
.list("themes/")
@@ -230,6 +240,32 @@ impl ThemeRegistry {
self.insert_user_theme_families([theme_family]);
}
}
+
+ /// Loads the user themes from the specified directory and adds them to the registry.
+ pub async fn load_user_themes(&self, themes_path: &Path, fs: Arc<dyn Fs>) -> Result<()> {
+ let mut theme_paths = fs
+ .read_dir(themes_path)
+ .await
+ .with_context(|| format!("reading themes from {themes_path:?}"))?;
+
+ while let Some(theme_path) = theme_paths.next().await {
+ let Some(theme_path) = theme_path.log_err() else {
+ continue;
+ };
+
+ let Some(reader) = fs.open_sync(&theme_path).await.log_err() else {
+ continue;
+ };
+
+ let Some(theme) = serde_json::from_reader(reader).log_err() else {
+ continue;
+ };
+
+ self.insert_user_theme_families([theme]);
+ }
+
+ Ok(())
+ }
}
impl Default for ThemeRegistry {
@@ -26,6 +26,7 @@ pub struct ThemeSettings {
pub buffer_font: Font,
pub buffer_font_size: Pixels,
pub buffer_line_height: BufferLineHeight,
+ pub requested_theme: Option<String>,
pub active_theme: Arc<Theme>,
pub theme_overrides: Option<ThemeStyleContent>,
}
@@ -89,6 +90,25 @@ impl ThemeSettings {
f32::max(self.buffer_line_height.value(), MIN_LINE_HEIGHT)
}
+ /// Switches to the theme with the given name, if it exists.
+ ///
+ /// Returns a `Some` containing the new theme if it was successful.
+ /// Returns `None` otherwise.
+ pub fn switch_theme(&mut self, theme: &str, cx: &mut AppContext) -> Option<Arc<Theme>> {
+ let themes = ThemeRegistry::default_global(cx);
+
+ let mut new_theme = None;
+
+ if let Some(theme) = themes.get(&theme).log_err() {
+ self.active_theme = theme.clone();
+ new_theme = Some(theme);
+ }
+
+ self.apply_theme_overrides();
+
+ new_theme
+ }
+
/// Applies the theme overrides, if there are any, to the current theme.
pub fn apply_theme_overrides(&mut self) {
if let Some(theme_overrides) = &self.theme_overrides {
@@ -182,6 +202,7 @@ impl settings::Settings for ThemeSettings {
},
buffer_font_size: defaults.buffer_font_size.unwrap().into(),
buffer_line_height: defaults.buffer_line_height.unwrap(),
+ requested_theme: defaults.theme.clone(),
active_theme: themes
.get(defaults.theme.as_ref().unwrap())
.or(themes.get(&one_dark().name))
@@ -205,6 +226,8 @@ impl settings::Settings for ThemeSettings {
}
if let Some(value) = &value.theme {
+ this.requested_theme = Some(value.clone());
+
if let Some(theme) = themes.get(value).log_err() {
this.active_theme = theme;
}
@@ -232,6 +255,7 @@ impl settings::Settings for ThemeSettings {
let mut root_schema = generator.root_schema_for::<ThemeSettingsContent>();
let theme_names = ThemeRegistry::global(cx)
.list_names(params.staff_mode)
+ .into_iter()
.map(|theme_name| Value::String(theme_name.to_string()))
.collect();
@@ -63,7 +63,7 @@ pub fn init(themes_to_load: LoadThemes, cx: &mut AppContext) {
registry::init(assets, cx);
if load_user_themes {
- ThemeRegistry::global_mut(cx).load_user_themes();
+ ThemeRegistry::global(cx).load_bundled_themes();
}
ThemeSettings::register(cx);
@@ -103,7 +103,7 @@ impl ThemeSelectorDelegate {
let staff_mode = cx.is_staff();
let registry = ThemeRegistry::global(cx);
- let mut themes = registry.list(staff_mode).collect::<Vec<_>>();
+ let mut themes = registry.list(staff_mode);
themes.sort_unstable_by(|a, b| {
a.appearance
.is_light()
@@ -8,6 +8,7 @@ lazy_static::lazy_static! {
pub static ref CONFIG_DIR: PathBuf = HOME.join(".config").join("zed");
pub static ref CONVERSATIONS_DIR: PathBuf = HOME.join(".config/zed/conversations");
pub static ref EMBEDDINGS_DIR: PathBuf = HOME.join(".config/zed/embeddings");
+ pub static ref THEMES_DIR: PathBuf = HOME.join(".config/zed/themes");
pub static ref LOGS_DIR: PathBuf = HOME.join("Library/Logs/Zed");
pub static ref SUPPORT_DIR: PathBuf = HOME.join("Library/Application Support/Zed");
pub static ref PLUGINS_DIR: PathBuf = HOME.join("Library/Application Support/Zed/plugins");
@@ -38,7 +38,7 @@ use std::{
},
thread,
};
-use theme::ActiveTheme;
+use theme::{ActiveTheme, ThemeRegistry, ThemeSettings};
use util::{
async_maybe,
channel::{parse_zed_link, AppCommitSha, ReleaseChannel, RELEASE_CHANNEL},
@@ -164,6 +164,36 @@ fn main() {
);
assistant::init(cx);
+ // TODO: Should we be loading the themes in a different spot?
+ cx.spawn({
+ let fs = fs.clone();
+ |cx| async move {
+ if let Some(theme_registry) =
+ cx.update(|cx| ThemeRegistry::global(cx).clone()).log_err()
+ {
+ if let Some(()) = theme_registry
+ .load_user_themes(&paths::THEMES_DIR.clone(), fs)
+ .await
+ .log_err()
+ {
+ cx.update(|cx| {
+ let mut theme_settings = ThemeSettings::get_global(cx).clone();
+
+ if let Some(requested_theme) = theme_settings.requested_theme.clone() {
+ if let Some(_theme) =
+ theme_settings.switch_theme(&requested_theme, cx)
+ {
+ ThemeSettings::override_global(theme_settings, cx);
+ }
+ }
+ })
+ .log_err();
+ }
+ }
+ }
+ })
+ .detach();
+
cx.spawn(|_| watch_languages(fs.clone(), languages.clone()))
.detach();
watch_file_types(fs.clone(), cx);
@@ -2690,7 +2690,7 @@ mod tests {
theme::init(theme::LoadThemes::JustBase, cx);
let mut has_default_theme = false;
- for theme_name in themes.list(false).map(|meta| meta.name) {
+ for theme_name in themes.list(false).into_iter().map(|meta| meta.name) {
let theme = themes.get(&theme_name).unwrap();
assert_eq!(theme.name, theme_name);
if theme.name == ThemeSettings::get(None, cx).active_theme.name {