mirror of
https://github.com/zed-industries/zed.git
synced 2024-09-19 02:17:35 +03:00
Add support for loading user themes (#7027)
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`.
This commit is contained in:
parent
5f4dd36a1a
commit
9cb5a84b8d
1
Cargo.lock
generated
1
Cargo.lock
generated
@ -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>()
|
||||
pub fn global(cx: &AppContext) -> Arc<Self> {
|
||||
cx.global::<GlobalThemeRegistry>().0.clone()
|
||||
}
|
||||
|
||||
/// Returns a mutable reference to the global [`ThemeRegistry`].
|
||||
pub fn global_mut(cx: &mut AppContext) -> &mut Self {
|
||||
cx.global_mut::<GlobalThemeRegistry>()
|
||||
}
|
||||
|
||||
/// 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 {
|
||||
|
Loading…
Reference in New Issue
Block a user