From 85d99cec5f43b5074695a82f756b9e3b0d4d3836 Mon Sep 17 00:00:00 2001 From: sxyazi Date: Thu, 29 Feb 2024 11:54:05 +0800 Subject: [PATCH] feat: flavor (#753) --- yazi-boot/src/boot.rs | 14 +++--- yazi-config/preset/theme.toml | 7 +++ yazi-config/src/lib.rs | 16 +++---- yazi-config/src/preset.rs | 56 ++++++++++++++++------- yazi-config/src/preview/preview.rs | 4 +- yazi-config/src/theme/flavor.rs | 23 ++++++++++ yazi-config/src/theme/mod.rs | 2 + yazi-config/src/theme/theme.rs | 72 ++++++++++++++++-------------- yazi-core/src/lib.rs | 6 +-- yazi-fm/src/logs.rs | 5 ++- yazi-shared/src/ro_cell.rs | 18 +++++++- yazi-shared/src/xdg.rs | 16 ++++--- 12 files changed, 156 insertions(+), 83 deletions(-) create mode 100644 yazi-config/src/theme/flavor.rs diff --git a/yazi-boot/src/boot.rs b/yazi-boot/src/boot.rs index a23830481..65fee0f04 100644 --- a/yazi-boot/src/boot.rs +++ b/yazi-boot/src/boot.rs @@ -1,4 +1,4 @@ -use std::{ffi::OsString, fs, path::{Path, PathBuf}, process}; +use std::{ffi::OsString, path::{Path, PathBuf}, process}; use clap::Parser; use serde::Serialize; @@ -16,7 +16,6 @@ pub struct Boot { pub config_dir: PathBuf, pub flavor_dir: PathBuf, pub plugin_dir: PathBuf, - pub state_dir: PathBuf, } impl Boot { @@ -37,7 +36,7 @@ impl Boot { impl Default for Boot { fn default() -> Self { - let config_dir = Xdg::config_dir().unwrap(); + let config_dir = Xdg::config_dir(); let (cwd, file) = Self::parse_entry(ARGS.entry.as_deref()); let boot = Self { @@ -47,13 +46,10 @@ impl Default for Boot { flavor_dir: config_dir.join("flavors"), plugin_dir: config_dir.join("plugins"), config_dir, - state_dir: Xdg::state_dir().unwrap(), }; - fs::create_dir_all(&boot.flavor_dir).expect("Failed to create flavor directory"); - fs::create_dir_all(&boot.plugin_dir).expect("Failed to create plugin directory"); - fs::create_dir_all(&boot.state_dir).expect("Failed to create state directory"); - + std::fs::create_dir_all(&boot.flavor_dir).expect("Failed to create flavor directory"); + std::fs::create_dir_all(&boot.plugin_dir).expect("Failed to create plugin directory"); boot } } @@ -75,7 +71,7 @@ impl Default for Args { if args.clear_cache { if PREVIEW.cache_dir == Xdg::cache_dir() { println!("Clearing cache directory: \n{:?}", PREVIEW.cache_dir); - fs::remove_dir_all(&PREVIEW.cache_dir).unwrap(); + std::fs::remove_dir_all(&PREVIEW.cache_dir).unwrap(); } else { println!( "You've changed the default cache directory, for your data's safety, please clear it manually: \n{:?}", diff --git a/yazi-config/preset/theme.toml b/yazi-config/preset/theme.toml index 40fc269cb..c1965186c 100644 --- a/yazi-config/preset/theme.toml +++ b/yazi-config/preset/theme.toml @@ -4,6 +4,13 @@ # vim:fileencoding=utf-8:foldmethod=marker +# : Flavor {{{ + +[flavor] +use = "" + +# : }}} + # : Manager {{{ [manager] diff --git a/yazi-config/src/lib.rs b/yazi-config/src/lib.rs index ed53e2544..117de23be 100644 --- a/yazi-config/src/lib.rs +++ b/yazi-config/src/lib.rs @@ -1,6 +1,6 @@ #![allow(clippy::module_inception)] -use yazi_shared::RoCell; +use yazi_shared::{RoCell, Xdg}; pub mod keymap; mod layout; @@ -23,11 +23,11 @@ pub(crate) use pattern::*; pub(crate) use preset::*; pub use priority::*; -pub static LAYOUT: RoCell> = RoCell::new(); - +static MERGED_YAZI: RoCell = RoCell::new(); static MERGED_KEYMAP: RoCell = RoCell::new(); static MERGED_THEME: RoCell = RoCell::new(); -static MERGED_YAZI: RoCell = RoCell::new(); + +pub static LAYOUT: RoCell> = RoCell::new(); pub static KEYMAP: RoCell = RoCell::new(); pub static LOG: RoCell = RoCell::new(); @@ -42,12 +42,12 @@ pub static SELECT: RoCell = RoCell::new(); pub static WHICH: RoCell = RoCell::new(); pub fn init() { - LAYOUT.with(Default::default); - - let config_dir = yazi_shared::Xdg::config_dir().unwrap(); + let config_dir = Xdg::config_dir(); + MERGED_YAZI.init(Preset::yazi(&config_dir)); MERGED_KEYMAP.init(Preset::keymap(&config_dir)); MERGED_THEME.init(Preset::theme(&config_dir)); - MERGED_YAZI.init(Preset::yazi(&config_dir)); + + LAYOUT.with(Default::default); KEYMAP.with(Default::default); LOG.with(Default::default); diff --git a/yazi-config/src/preset.rs b/yazi-config/src/preset.rs index e8b916a6d..0560c4a13 100644 --- a/yazi-config/src/preset.rs +++ b/yazi-config/src/preset.rs @@ -1,23 +1,35 @@ use std::{mem, path::{Path, PathBuf}}; +use anyhow::Context; use toml::{Table, Value}; +use crate::theme::Flavor; + pub(crate) struct Preset; impl Preset { - #[inline] - pub(crate) fn keymap(dir: &Path) -> String { - Self::merge_str(dir.join("keymap.toml"), include_str!("../preset/keymap.toml")) + pub(crate) fn yazi(p: &Path) -> String { + Self::merge_path(p.join("yazi.toml"), include_str!("../preset/yazi.toml")) } - #[inline] - pub(crate) fn theme(dir: &Path) -> String { - Self::merge_str(dir.join("theme.toml"), include_str!("../preset/theme.toml")) + pub(crate) fn keymap(p: &Path) -> String { + Self::merge_path(p.join("keymap.toml"), include_str!("../preset/keymap.toml")) } - #[inline] - pub(crate) fn yazi(dir: &Path) -> String { - Self::merge_str(dir.join("yazi.toml"), include_str!("../preset/yazi.toml")) + pub(crate) fn theme(p: &Path) -> String { + let Ok(user) = std::fs::read_to_string(p.join("theme.toml")) else { + return include_str!("../preset/theme.toml").to_owned(); + }; + let Some(use_) = Flavor::parse_use(&user) else { + return Self::merge_str(&user, include_str!("../preset/theme.toml")); + }; + + let p = p.join(format!("flavors/{}.yazi/flavor.toml", use_)); + let flavor = std::fs::read_to_string(&p) + .with_context(|| format!("Failed to load flavor {:?}", p)) + .unwrap(); + + Self::merge_str(&user, &Self::merge_str(&flavor, include_str!("../preset/theme.toml"))) } #[inline] @@ -25,6 +37,24 @@ impl Preset { *a = b.into_iter().chain(mem::take(a)).chain(c).collect(); } + #[inline] + pub(crate) fn merge_str(user: &str, base: &str) -> String { + let mut t = user.parse().unwrap(); + Self::merge(&mut t, base.parse().unwrap(), 2); + + t.to_string() + } + + #[inline] + fn merge_path(user: PathBuf, base: &str) -> String { + let s = std::fs::read_to_string(user).unwrap_or_default(); + if s.is_empty() { + return base.to_string(); + } + + Self::merge_str(&s, base) + } + fn merge(a: &mut Table, b: Table, max: u8) { for (k, v) in b { let Some(a) = a.get_mut(&k) else { @@ -45,12 +75,4 @@ impl Preset { *a = v; } } - - fn merge_str(user: PathBuf, base: &str) -> String { - let mut user = std::fs::read_to_string(user).unwrap_or_default().parse::().unwrap(); - let base = base.parse::
().unwrap(); - - Self::merge(&mut user, base, 2); - user.to_string() - } } diff --git a/yazi-config/src/preview/preview.rs b/yazi-config/src/preview/preview.rs index 9e3d334c1..b70138ce4 100644 --- a/yazi-config/src/preview/preview.rs +++ b/yazi-config/src/preview/preview.rs @@ -2,9 +2,9 @@ use std::{fs, path::PathBuf, time::{self, SystemTime}}; use serde::{Deserialize, Serialize}; use validator::Validate; -use yazi_shared::{fs::expand_path, Xdg}; +use yazi_shared::fs::expand_path; -use crate::{validation::check_validation, MERGED_YAZI}; +use crate::{validation::check_validation, Xdg, MERGED_YAZI}; #[derive(Debug, Serialize)] pub struct Preview { diff --git a/yazi-config/src/theme/flavor.rs b/yazi-config/src/theme/flavor.rs new file mode 100644 index 000000000..7827c2039 --- /dev/null +++ b/yazi-config/src/theme/flavor.rs @@ -0,0 +1,23 @@ +use serde::{Deserialize, Serialize}; + +#[derive(Deserialize, Serialize)] +pub struct Flavor { + #[serde(rename = "use")] + pub use_: String, +} + +impl Flavor { + pub fn parse_use(s: &str) -> Option { + #[derive(Deserialize)] + struct Outer { + flavor: Inner, + } + #[derive(Deserialize)] + struct Inner { + #[serde(rename = "use")] + pub use_: String, + } + + toml::from_str::(s).ok().map(|o| o.flavor.use_).filter(|s| !s.is_empty()) + } +} diff --git a/yazi-config/src/theme/mod.rs b/yazi-config/src/theme/mod.rs index c3f0b0044..f4fd0102a 100644 --- a/yazi-config/src/theme/mod.rs +++ b/yazi-config/src/theme/mod.rs @@ -1,5 +1,6 @@ mod color; mod filetype; +mod flavor; mod icon; mod is; mod style; @@ -7,6 +8,7 @@ mod theme; pub use color::*; pub use filetype::*; +pub use flavor::*; pub use icon::*; pub use is::*; pub use style::*; diff --git a/yazi-config/src/theme/theme.rs b/yazi-config/src/theme/theme.rs index 8534c90fe..2bca90984 100644 --- a/yazi-config/src/theme/theme.rs +++ b/yazi-config/src/theme/theme.rs @@ -2,11 +2,48 @@ use std::path::PathBuf; use serde::{Deserialize, Serialize}; use validator::Validate; -use yazi_shared::fs::expand_path; +use yazi_shared::{fs::expand_path, Xdg}; -use super::{Filetype, Icon, Style}; +use super::{Filetype, Flavor, Icon, Style}; use crate::{validation::check_validation, MERGED_THEME}; +#[derive(Deserialize, Serialize)] +pub struct Theme { + pub flavor: Flavor, + pub manager: Manager, + status: Status, + pub input: Input, + pub select: Select, + pub completion: Completion, + pub tasks: Tasks, + pub which: Which, + pub help: Help, + + // File-specific styles + #[serde(rename = "filetype", deserialize_with = "Filetype::deserialize", skip_serializing)] + pub filetypes: Vec, + #[serde(rename = "icon", deserialize_with = "Icon::deserialize", skip_serializing)] + pub icons: Vec, +} + +impl Default for Theme { + fn default() -> Self { + let mut theme: Self = toml::from_str(&MERGED_THEME).unwrap(); + + check_validation(theme.manager.validate()); + check_validation(theme.which.validate()); + + if theme.flavor.use_.is_empty() { + theme.manager.syntect_theme = expand_path(&theme.manager.syntect_theme); + } else { + theme.manager.syntect_theme = + Xdg::config_dir().join(format!("flavors/{}.yazi/tmtheme.xml", theme.flavor.use_)); + } + + theme + } +} + #[derive(Deserialize, Serialize, Validate)] pub struct Manager { cwd: Style, @@ -123,34 +160,3 @@ pub struct Help { pub hovered: Style, pub footer: Style, } - -#[derive(Deserialize, Serialize)] -pub struct Theme { - pub manager: Manager, - status: Status, - pub input: Input, - pub select: Select, - pub completion: Completion, - pub tasks: Tasks, - pub which: Which, - pub help: Help, - - // File-specific styles - #[serde(rename = "filetype", deserialize_with = "Filetype::deserialize", skip_serializing)] - pub filetypes: Vec, - #[serde(rename = "icon", deserialize_with = "Icon::deserialize", skip_serializing)] - pub icons: Vec, -} - -impl Default for Theme { - fn default() -> Self { - let mut theme: Self = toml::from_str(&MERGED_THEME).unwrap(); - - check_validation(theme.manager.validate()); - check_validation(theme.which.validate()); - - theme.manager.syntect_theme = expand_path(&theme.manager.syntect_theme); - - theme - } -} diff --git a/yazi-core/src/lib.rs b/yazi-core/src/lib.rs index 7a096e5f9..e7ff1a7ef 100644 --- a/yazi-core/src/lib.rs +++ b/yazi-core/src/lib.rs @@ -22,8 +22,4 @@ pub mod which; pub use clipboard::*; pub use step::*; -pub fn init() { - CLIPBOARD.with(Default::default); - - yazi_scheduler::init(); -} +pub fn init() { CLIPBOARD.with(Default::default); } diff --git a/yazi-fm/src/logs.rs b/yazi-fm/src/logs.rs index 4ea8162bd..5548a1b93 100644 --- a/yazi-fm/src/logs.rs +++ b/yazi-fm/src/logs.rs @@ -8,7 +8,10 @@ pub(super) struct Logs; impl Logs { pub(super) fn start() { - let appender = tracing_appender::rolling::never(Xdg::state_dir().unwrap(), "yazi.log"); + let state_dir = Xdg::state_dir(); + std::fs::create_dir_all(&state_dir).expect("Failed to create state directory"); + + let appender = tracing_appender::rolling::never(state_dir, "yazi.log"); let (handle, guard) = tracing_appender::non_blocking(appender); // let filter = EnvFilter::from_default_env(); diff --git a/yazi-shared/src/ro_cell.rs b/yazi-shared/src/ro_cell.rs index 7a378d489..2f0ac56d0 100644 --- a/yazi-shared/src/ro_cell.rs +++ b/yazi-shared/src/ro_cell.rs @@ -1,4 +1,4 @@ -use std::{cell::UnsafeCell, fmt::{self, Display}, ops::Deref}; +use std::{cell::UnsafeCell, fmt::{self, Display}, mem, ops::Deref}; // Read-only cell. It's safe to use this in a static variable, but it's not safe // to mutate it. This is useful for storing static data that is expensive to @@ -13,6 +13,7 @@ impl RoCell { #[inline] pub fn init(&self, value: T) { + debug_assert!(!self.is_initialized()); unsafe { *self.0.get() = Some(value); } @@ -26,18 +27,31 @@ impl RoCell { self.init(f()); } + #[inline] + pub fn replace(&self, value: T) -> T { + debug_assert!(self.is_initialized()); + unsafe { mem::replace(&mut *self.0.get(), Some(value)).unwrap_unchecked() } + } + #[inline] pub fn drop(&self) { unsafe { *self.0.get() = None; } } + + #[cfg(debug_assertions)] + #[inline] + fn is_initialized(&self) -> bool { unsafe { (*self.0.get()).is_some() } } } impl Deref for RoCell { type Target = T; - fn deref(&self) -> &Self::Target { unsafe { (*self.0.get()).as_ref().unwrap() } } + fn deref(&self) -> &Self::Target { + debug_assert!(self.is_initialized()); + unsafe { (*self.0.get()).as_ref().unwrap_unchecked() } + } } impl Display for RoCell diff --git a/yazi-shared/src/xdg.rs b/yazi-shared/src/xdg.rs index 2e785f98d..820b2e195 100644 --- a/yazi-shared/src/xdg.rs +++ b/yazi-shared/src/xdg.rs @@ -5,14 +5,16 @@ use crate::fs::expand_path; pub struct Xdg; impl Xdg { - pub fn config_dir() -> Option { - if let Some(s) = env::var_os("YAZI_CONFIG_HOME").filter(|s| !s.is_empty()) { - return Some(expand_path(s)); + pub fn config_dir() -> PathBuf { + if let Some(p) = env::var_os("YAZI_CONFIG_HOME").map(expand_path).filter(|p| p.is_absolute()) { + return p; } #[cfg(windows)] { - dirs::config_dir().map(|p| p.join("yazi").join("config")) + dirs::config_dir() + .map(|p| p.join("yazi").join("config")) + .expect("Failed to get config directory") } #[cfg(unix)] { @@ -21,13 +23,14 @@ impl Xdg { .filter(|p| p.is_absolute()) .or_else(|| dirs::home_dir().map(|h| h.join(".config"))) .map(|p| p.join("yazi")) + .expect("Failed to get config directory") } } - pub fn state_dir() -> Option { + pub fn state_dir() -> PathBuf { #[cfg(windows)] { - dirs::data_dir().map(|p| p.join("yazi").join("state")) + dirs::data_dir().map(|p| p.join("yazi").join("state")).expect("Failed to get state directory") } #[cfg(unix)] { @@ -36,6 +39,7 @@ impl Xdg { .filter(|p| p.is_absolute()) .or_else(|| dirs::home_dir().map(|h| h.join(".local/state"))) .map(|p| p.join("yazi")) + .expect("Failed to get state directory") } }