mirror of
https://github.com/zed-industries/zed.git
synced 2024-09-19 18:41:56 +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",
|
"color",
|
||||||
"derive_more",
|
"derive_more",
|
||||||
"fs",
|
"fs",
|
||||||
|
"futures 0.3.28",
|
||||||
"gpui",
|
"gpui",
|
||||||
"indexmap 1.9.3",
|
"indexmap 1.9.3",
|
||||||
"itertools 0.11.0",
|
"itertools 0.11.0",
|
||||||
|
@ -23,6 +23,7 @@ doctest = false
|
|||||||
anyhow.workspace = true
|
anyhow.workspace = true
|
||||||
derive_more.workspace = true
|
derive_more.workspace = true
|
||||||
fs = { path = "../fs" }
|
fs = { path = "../fs" }
|
||||||
|
futures.workspace = true
|
||||||
gpui = { path = "../gpui" }
|
gpui = { path = "../gpui" }
|
||||||
indexmap = { version = "1.6.2", features = ["serde"] }
|
indexmap = { version = "1.6.2", features = ["serde"] }
|
||||||
palette = { version = "0.7.3", default-features = false, features = ["std"] }
|
palette = { version = "0.7.3", default-features = false, features = ["std"] }
|
||||||
|
@ -1,9 +1,13 @@
|
|||||||
use std::collections::HashMap;
|
use std::collections::HashMap;
|
||||||
|
use std::path::Path;
|
||||||
use std::sync::Arc;
|
use std::sync::Arc;
|
||||||
|
|
||||||
use anyhow::{anyhow, Context, Result};
|
use anyhow::{anyhow, Context, Result};
|
||||||
use derive_more::{Deref, DerefMut};
|
use derive_more::{Deref, DerefMut};
|
||||||
|
use fs::Fs;
|
||||||
|
use futures::StreamExt;
|
||||||
use gpui::{AppContext, AssetSource, HighlightStyle, SharedString};
|
use gpui::{AppContext, AssetSource, HighlightStyle, SharedString};
|
||||||
|
use parking_lot::RwLock;
|
||||||
use refineable::Refineable;
|
use refineable::Refineable;
|
||||||
use util::ResultExt;
|
use util::ResultExt;
|
||||||
|
|
||||||
@ -26,40 +30,41 @@ pub struct ThemeMeta {
|
|||||||
///
|
///
|
||||||
/// This should not be exposed outside of this module.
|
/// This should not be exposed outside of this module.
|
||||||
#[derive(Default, Deref, DerefMut)]
|
#[derive(Default, Deref, DerefMut)]
|
||||||
struct GlobalThemeRegistry(ThemeRegistry);
|
struct GlobalThemeRegistry(Arc<ThemeRegistry>);
|
||||||
|
|
||||||
/// Initializes the theme registry.
|
/// Initializes the theme registry.
|
||||||
pub fn init(assets: Box<dyn AssetSource>, cx: &mut AppContext) {
|
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 {
|
pub struct ThemeRegistry {
|
||||||
|
state: RwLock<ThemeRegistryState>,
|
||||||
assets: Box<dyn AssetSource>,
|
assets: Box<dyn AssetSource>,
|
||||||
themes: HashMap<SharedString, Arc<Theme>>,
|
|
||||||
}
|
}
|
||||||
|
|
||||||
impl ThemeRegistry {
|
impl ThemeRegistry {
|
||||||
/// Returns the global [`ThemeRegistry`].
|
/// Returns the global [`ThemeRegistry`].
|
||||||
pub fn global(cx: &AppContext) -> &Self {
|
pub fn global(cx: &AppContext) -> Arc<Self> {
|
||||||
cx.global::<GlobalThemeRegistry>()
|
cx.global::<GlobalThemeRegistry>().0.clone()
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Returns a mutable reference to the global [`ThemeRegistry`].
|
/// Returns the global [`ThemeRegistry`].
|
||||||
pub fn global_mut(cx: &mut AppContext) -> &mut Self {
|
|
||||||
cx.global_mut::<GlobalThemeRegistry>()
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Returns a mutable reference to the global [`ThemeRegistry`].
|
|
||||||
///
|
///
|
||||||
/// Inserts a default [`ThemeRegistry`] if one does not yet exist.
|
/// Inserts a default [`ThemeRegistry`] if one does not yet exist.
|
||||||
pub fn default_global(cx: &mut AppContext) -> &mut Self {
|
pub fn default_global(cx: &mut AppContext) -> Arc<Self> {
|
||||||
cx.default_global::<GlobalThemeRegistry>()
|
cx.default_global::<GlobalThemeRegistry>().0.clone()
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn new(assets: Box<dyn AssetSource>) -> Self {
|
pub fn new(assets: Box<dyn AssetSource>) -> Self {
|
||||||
let mut registry = Self {
|
let registry = Self {
|
||||||
assets,
|
state: RwLock::new(ThemeRegistryState {
|
||||||
themes: HashMap::new(),
|
themes: HashMap::new(),
|
||||||
|
}),
|
||||||
|
assets,
|
||||||
};
|
};
|
||||||
|
|
||||||
// We're loading our new versions of the One themes by default, as
|
// We're loading our new versions of the One themes by default, as
|
||||||
@ -72,30 +77,27 @@ impl ThemeRegistry {
|
|||||||
registry
|
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() {
|
for family in families.into_iter() {
|
||||||
self.insert_themes(family.themes);
|
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() {
|
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)]
|
#[allow(unused)]
|
||||||
fn insert_user_theme_families(
|
fn insert_user_theme_families(&self, families: impl IntoIterator<Item = ThemeFamilyContent>) {
|
||||||
&mut self,
|
|
||||||
families: impl IntoIterator<Item = ThemeFamilyContent>,
|
|
||||||
) {
|
|
||||||
for family in families.into_iter() {
|
for family in families.into_iter() {
|
||||||
self.insert_user_themes(family.themes);
|
self.insert_user_themes(family.themes);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[allow(unused)]
|
pub fn insert_user_themes(&self, themes: impl IntoIterator<Item = ThemeContent>) {
|
||||||
fn insert_user_themes(&mut self, themes: impl IntoIterator<Item = ThemeContent>) {
|
|
||||||
self.insert_themes(themes.into_iter().map(|user_theme| {
|
self.insert_themes(themes.into_iter().map(|user_theme| {
|
||||||
let mut theme_colors = match user_theme.appearance {
|
let mut theme_colors = match user_theme.appearance {
|
||||||
AppearanceContent::Light => ThemeColors::light(),
|
AppearanceContent::Light => ThemeColors::light(),
|
||||||
@ -186,28 +188,36 @@ impl ThemeRegistry {
|
|||||||
}
|
}
|
||||||
|
|
||||||
pub fn clear(&mut self) {
|
pub fn clear(&mut self) {
|
||||||
self.themes.clear();
|
self.state.write().themes.clear();
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn list_names(&self, _staff: bool) -> impl Iterator<Item = SharedString> + '_ {
|
pub fn list_names(&self, _staff: bool) -> Vec<SharedString> {
|
||||||
self.themes.keys().cloned()
|
self.state.read().themes.keys().cloned().collect()
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn list(&self, _staff: bool) -> impl Iterator<Item = ThemeMeta> + '_ {
|
pub fn list(&self, _staff: bool) -> Vec<ThemeMeta> {
|
||||||
self.themes.values().map(|theme| ThemeMeta {
|
self.state
|
||||||
|
.read()
|
||||||
|
.themes
|
||||||
|
.values()
|
||||||
|
.map(|theme| ThemeMeta {
|
||||||
name: theme.name.clone(),
|
name: theme.name.clone(),
|
||||||
appearance: theme.appearance(),
|
appearance: theme.appearance(),
|
||||||
})
|
})
|
||||||
|
.collect()
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn get(&self, name: &str) -> Result<Arc<Theme>> {
|
pub fn get(&self, name: &str) -> Result<Arc<Theme>> {
|
||||||
self.themes
|
self.state
|
||||||
|
.read()
|
||||||
|
.themes
|
||||||
.get(name)
|
.get(name)
|
||||||
.ok_or_else(|| anyhow!("theme not found: {}", name))
|
.ok_or_else(|| anyhow!("theme not found: {}", name))
|
||||||
.cloned()
|
.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
|
let theme_paths = self
|
||||||
.assets
|
.assets
|
||||||
.list("themes/")
|
.list("themes/")
|
||||||
@ -230,6 +240,32 @@ impl ThemeRegistry {
|
|||||||
self.insert_user_theme_families([theme_family]);
|
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 {
|
impl Default for ThemeRegistry {
|
||||||
|
@ -26,6 +26,7 @@ pub struct ThemeSettings {
|
|||||||
pub buffer_font: Font,
|
pub buffer_font: Font,
|
||||||
pub buffer_font_size: Pixels,
|
pub buffer_font_size: Pixels,
|
||||||
pub buffer_line_height: BufferLineHeight,
|
pub buffer_line_height: BufferLineHeight,
|
||||||
|
pub requested_theme: Option<String>,
|
||||||
pub active_theme: Arc<Theme>,
|
pub active_theme: Arc<Theme>,
|
||||||
pub theme_overrides: Option<ThemeStyleContent>,
|
pub theme_overrides: Option<ThemeStyleContent>,
|
||||||
}
|
}
|
||||||
@ -89,6 +90,25 @@ impl ThemeSettings {
|
|||||||
f32::max(self.buffer_line_height.value(), MIN_LINE_HEIGHT)
|
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.
|
/// Applies the theme overrides, if there are any, to the current theme.
|
||||||
pub fn apply_theme_overrides(&mut self) {
|
pub fn apply_theme_overrides(&mut self) {
|
||||||
if let Some(theme_overrides) = &self.theme_overrides {
|
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_font_size: defaults.buffer_font_size.unwrap().into(),
|
||||||
buffer_line_height: defaults.buffer_line_height.unwrap(),
|
buffer_line_height: defaults.buffer_line_height.unwrap(),
|
||||||
|
requested_theme: defaults.theme.clone(),
|
||||||
active_theme: themes
|
active_theme: themes
|
||||||
.get(defaults.theme.as_ref().unwrap())
|
.get(defaults.theme.as_ref().unwrap())
|
||||||
.or(themes.get(&one_dark().name))
|
.or(themes.get(&one_dark().name))
|
||||||
@ -205,6 +226,8 @@ impl settings::Settings for ThemeSettings {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if let Some(value) = &value.theme {
|
if let Some(value) = &value.theme {
|
||||||
|
this.requested_theme = Some(value.clone());
|
||||||
|
|
||||||
if let Some(theme) = themes.get(value).log_err() {
|
if let Some(theme) = themes.get(value).log_err() {
|
||||||
this.active_theme = theme;
|
this.active_theme = theme;
|
||||||
}
|
}
|
||||||
@ -232,6 +255,7 @@ impl settings::Settings for ThemeSettings {
|
|||||||
let mut root_schema = generator.root_schema_for::<ThemeSettingsContent>();
|
let mut root_schema = generator.root_schema_for::<ThemeSettingsContent>();
|
||||||
let theme_names = ThemeRegistry::global(cx)
|
let theme_names = ThemeRegistry::global(cx)
|
||||||
.list_names(params.staff_mode)
|
.list_names(params.staff_mode)
|
||||||
|
.into_iter()
|
||||||
.map(|theme_name| Value::String(theme_name.to_string()))
|
.map(|theme_name| Value::String(theme_name.to_string()))
|
||||||
.collect();
|
.collect();
|
||||||
|
|
||||||
|
@ -63,7 +63,7 @@ pub fn init(themes_to_load: LoadThemes, cx: &mut AppContext) {
|
|||||||
registry::init(assets, cx);
|
registry::init(assets, cx);
|
||||||
|
|
||||||
if load_user_themes {
|
if load_user_themes {
|
||||||
ThemeRegistry::global_mut(cx).load_user_themes();
|
ThemeRegistry::global(cx).load_bundled_themes();
|
||||||
}
|
}
|
||||||
|
|
||||||
ThemeSettings::register(cx);
|
ThemeSettings::register(cx);
|
||||||
|
@ -103,7 +103,7 @@ impl ThemeSelectorDelegate {
|
|||||||
|
|
||||||
let staff_mode = cx.is_staff();
|
let staff_mode = cx.is_staff();
|
||||||
let registry = ThemeRegistry::global(cx);
|
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| {
|
themes.sort_unstable_by(|a, b| {
|
||||||
a.appearance
|
a.appearance
|
||||||
.is_light()
|
.is_light()
|
||||||
|
@ -8,6 +8,7 @@ lazy_static::lazy_static! {
|
|||||||
pub static ref CONFIG_DIR: PathBuf = HOME.join(".config").join("zed");
|
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 CONVERSATIONS_DIR: PathBuf = HOME.join(".config/zed/conversations");
|
||||||
pub static ref EMBEDDINGS_DIR: PathBuf = HOME.join(".config/zed/embeddings");
|
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 LOGS_DIR: PathBuf = HOME.join("Library/Logs/Zed");
|
||||||
pub static ref SUPPORT_DIR: PathBuf = HOME.join("Library/Application Support/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");
|
pub static ref PLUGINS_DIR: PathBuf = HOME.join("Library/Application Support/Zed/plugins");
|
||||||
|
@ -38,7 +38,7 @@ use std::{
|
|||||||
},
|
},
|
||||||
thread,
|
thread,
|
||||||
};
|
};
|
||||||
use theme::ActiveTheme;
|
use theme::{ActiveTheme, ThemeRegistry, ThemeSettings};
|
||||||
use util::{
|
use util::{
|
||||||
async_maybe,
|
async_maybe,
|
||||||
channel::{parse_zed_link, AppCommitSha, ReleaseChannel, RELEASE_CHANNEL},
|
channel::{parse_zed_link, AppCommitSha, ReleaseChannel, RELEASE_CHANNEL},
|
||||||
@ -164,6 +164,36 @@ fn main() {
|
|||||||
);
|
);
|
||||||
assistant::init(cx);
|
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()))
|
cx.spawn(|_| watch_languages(fs.clone(), languages.clone()))
|
||||||
.detach();
|
.detach();
|
||||||
watch_file_types(fs.clone(), cx);
|
watch_file_types(fs.clone(), cx);
|
||||||
|
@ -2690,7 +2690,7 @@ mod tests {
|
|||||||
theme::init(theme::LoadThemes::JustBase, cx);
|
theme::init(theme::LoadThemes::JustBase, cx);
|
||||||
|
|
||||||
let mut has_default_theme = false;
|
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();
|
let theme = themes.get(&theme_name).unwrap();
|
||||||
assert_eq!(theme.name, theme_name);
|
assert_eq!(theme.name, theme_name);
|
||||||
if theme.name == ThemeSettings::get(None, cx).active_theme.name {
|
if theme.name == ThemeSettings::get(None, cx).active_theme.name {
|
||||||
|
Loading…
Reference in New Issue
Block a user