@@ -1,5 +1,5 @@
2− collections::HashMap,
2+ collections::{HashMap, HashSet},
3 path::{Path, PathBuf},
3 path::{Path, PathBuf},
@@ -37,19 +37,21 @@ pub static BASE16_DEFAULT_THEME: Lazy<Theme> = Lazy::new(|| Theme {
38#[derive(Clone, Debug)]
38#[derive(Clone, Debug)]
39pub struct Loader {
39pub struct Loader {
40− user_dir: PathBuf,
40+ /// Theme directories to search from highest to lowest priority
41− default_dir: PathBuf,
41+ theme_dirs: Vec<PathBuf>,
43impl Loader {
43impl Loader {
44− /// Creates a new loader that can load themes from two directories.
44+ /// Creates a new loader that can load themes from multiple directories.
45− pub fn new<P: AsRef<Path>>(user_dir: P, default_dir: P) -> Self {
45+ ///
46+ /// The provided directories should be ordered from highest to lowest priority.
47+ /// The directories will have their "themes" subdirectory searched.
48+ pub fn new(dirs: &[PathBuf]) -> Self {
47− user_dir: user_dir.as_ref().join("themes"),
50+ theme_dirs: dirs.iter().map(|p| p.join("themes")).collect(),
48− default_dir: default_dir.as_ref().join("themes"),
52− /// Loads a theme first looking in the `user_dir` then in `default_dir`
54+ /// Loads a theme searching directories in priority order.
53 pub fn load(&self, name: &str) -> Result<Theme> {
55 pub fn load(&self, name: &str) -> Result<Theme> {
54 if name == "default" {
56 if name == "default" {
55 return Ok(self.default());
57 return Ok(self.default());
@@ -58,7 +60,8 @@ impl Loader {
58 return Ok(self.base16_default());
60 return Ok(self.base16_default());
63+ let mut visited_paths = HashSet::new();
61− let theme = self.load_theme(name, name, false).map(Theme::from)?;
64+ let theme = self.load_theme(name, &mut visited_paths).map(Theme::from)?;
63 Ok(Theme {
66 Ok(Theme {
64 name: name.into(),
67 name: name.into(),
@@ -66,16 +69,18 @@ impl Loader {
69− // load the theme and its parent recursively and merge them
72+ /// Recursively load a theme, merging with any inherited parent themes.
70− // `base_theme_name` is the theme from the config.toml,
73+ ///
71− // used to prevent some circular loading scenarios
74+ /// The paths that have been visited in the inheritance hierarchy are tracked
72− fn load_theme(
75+ /// to detect and avoid cycling.
74− name: &str,
77+ /// It is possible for one file to inherit from another file with the same name
75− base_theme_name: &str,
78+ /// so long as the second file is in a themes directory with lower priority.
76− only_default_dir: bool,
79+ /// However, it is not recommended that users do this as it will make tracing
77− ) -> Result<Value> {
80+ /// errors more difficult.
81+ fn load_theme(&self, name: &str, visited_paths: &mut HashSet<PathBuf>) -> Result<Value> {
78− let path = self.path(name, only_default_dir);
82+ let path = self.path(name, visited_paths)?;
79 let theme_toml = self.load_toml(path)?;
84 let theme_toml = self.load_toml(path)?;
81 let inherits = theme_toml.get("inherits");
86 let inherits = theme_toml.get("inherits");
@@ -92,11 +97,7 @@ impl Loader {
92 // load default themes's toml from const.
97 // load default themes's toml from const.
93 "default" => DEFAULT_THEME_DATA.clone(),
98 "default" => DEFAULT_THEME_DATA.clone(),
94 "base16_default" => BASE16_DEFAULT_THEME_DATA.clone(),
99 "base16_default" => BASE16_DEFAULT_THEME_DATA.clone(),
95− _ => self.load_theme(
96− parent_theme_name,
100+ _ => self.load_theme(parent_theme_name, visited_paths)?,
98− base_theme_name == parent_theme_name,
102 self.merge_themes(parent_theme_toml, theme_toml)
103 self.merge_themes(parent_theme_toml, theme_toml)
@@ -148,7 +149,7 @@ impl Loader {
148 merge_toml_values(theme, palette.into(), 1)
149 merge_toml_values(theme, palette.into(), 1)
151− // Loads the theme data as `toml::Value` first from the user_dir then in default_dir
152+ // Loads the theme data as `toml::Value`
152 fn load_toml(&self, path: PathBuf) -> Result<Value> {
153 fn load_toml(&self, path: PathBuf) -> Result<Value> {
153 let data = std::fs::read_to_string(path)?;
154 let data = std::fs::read_to_string(path)?;
154 let value = toml::from_str(&data)?;
155 let value = toml::from_str(&data)?;
@@ -156,25 +157,35 @@ impl Loader {
156 Ok(value)
157 Ok(value)
159− // Returns the path to the theme with the name
160+ /// Returns the path to the theme with the given name
160− // With `only_default_dir` as false the path will first search for the user path
161+ ///
161− // disabled it ignores the user path and returns only the default path
162+ /// Ignores paths already visited and follows directory priority order.
162− fn path(&self, name: &str, only_default_dir: bool) -> PathBuf {
163+ fn path(&self, name: &str, visited_paths: &mut HashSet<PathBuf>) -> Result<PathBuf> {
163 let filename = format!("{}.toml", name);
164 let filename = format!("{}.toml", name);
166+ let mut cycle_found = false; // track if there was a path, but it was in a cycle
165− let user_path = self.user_dir.join(&filename);
170+ let path = dir.join(&filename);
166− if !only_default_dir && user_path.exists() {
171+ if !path.exists() {
168− } else {
173+ } else if visited_paths.contains(&path) {
169− self.default_dir.join(filename)
174+ // Avoiding cycle, continuing to look in lower priority directories
170− }
175+ cycle_found = true;
173− /// Lists all theme names available in default and user directory
178+ visited_paths.insert(path.clone());
174− pub fn names(&self) -> Vec<String> {
179+ Some(path)
175− let mut names = Self::read_names(&self.user_dir);
180+ }
176− names.extend(Self::read_names(&self.default_dir));
181+ })
177− names
182+ .ok_or_else(|| {
184+ anyhow!("Theme: cycle found in inheriting: {}", name)
186+ anyhow!("Theme: file not found for: {}", name)
180 pub fn default_theme(&self, true_color: bool) -> Theme {
191 pub fn default_theme(&self, true_color: bool) -> Theme {