@@ -1,5 +1,5 @@
11use std::{
2− collections::HashMap,
2+ collections::{HashMap, HashSet},
33 path::{Path, PathBuf},
44 str,
55};
@@ -37,19 +37,21 @@ pub static BASE16_DEFAULT_THEME: Lazy<Theme> = Lazy::new(|| Theme {
3737
3838#[derive(Clone, Debug)]
3939pub struct Loader {
40− user_dir: PathBuf,
41− default_dir: PathBuf,
40+ /// Theme directories to search from highest to lowest priority
41+ theme_dirs: Vec<PathBuf>,
4242}
4343impl Loader {
44− /// Creates a new loader that can load themes from two directories.
45− pub fn new<P: AsRef<Path>>(user_dir: P, default_dir: P) -> Self {
44+ /// Creates a new loader that can load themes from multiple directories.
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 {
4649 Self {
47− user_dir: user_dir.as_ref().join("themes"),
48− default_dir: default_dir.as_ref().join("themes"),
50+ theme_dirs: dirs.iter().map(|p| p.join("themes")).collect(),
4951 }
5052 }
5153
52− /// Loads a theme first looking in the `user_dir` then in `default_dir`
54+ /// Loads a theme searching directories in priority order.
5355 pub fn load(&self, name: &str) -> Result<Theme> {
5456 if name == "default" {
5557 return Ok(self.default());
@@ -58,7 +60,8 @@ impl Loader {
5860 return Ok(self.base16_default());
5961 }
6062
61− let theme = self.load_theme(name, name, false).map(Theme::from)?;
63+ let mut visited_paths = HashSet::new();
64+ let theme = self.load_theme(name, &mut visited_paths).map(Theme::from)?;
6265
6366 Ok(Theme {
6467 name: name.into(),
@@ -66,16 +69,18 @@ impl Loader {
6669 })
6770 }
6871
69− // load the theme and its parent recursively and merge them
70− // `base_theme_name` is the theme from the config.toml,
71− // used to prevent some circular loading scenarios
72− fn load_theme(
73− &self,
74− name: &str,
75− base_theme_name: &str,
76− only_default_dir: bool,
77− ) -> Result<Value> {
78− let path = self.path(name, only_default_dir);
72+ /// Recursively load a theme, merging with any inherited parent themes.
73+ ///
74+ /// The paths that have been visited in the inheritance hierarchy are tracked
75+ /// to detect and avoid cycling.
76+ ///
77+ /// It is possible for one file to inherit from another file with the same name
78+ /// so long as the second file is in a themes directory with lower priority.
79+ /// However, it is not recommended that users do this as it will make tracing
80+ /// errors more difficult.
81+ fn load_theme(&self, name: &str, visited_paths: &mut HashSet<PathBuf>) -> Result<Value> {
82+ let path = self.path(name, visited_paths)?;
83+
7984 let theme_toml = self.load_toml(path)?;
8085
8186 let inherits = theme_toml.get("inherits");
@@ -92,11 +97,7 @@ impl Loader {
9297 // load default themes's toml from const.
9398 "default" => DEFAULT_THEME_DATA.clone(),
9499 "base16_default" => BASE16_DEFAULT_THEME_DATA.clone(),
95− _ => self.load_theme(
96− parent_theme_name,
97− base_theme_name,
98− base_theme_name == parent_theme_name,
99− )?,
100+ _ => self.load_theme(parent_theme_name, visited_paths)?,
100101 };
101102
102103 self.merge_themes(parent_theme_toml, theme_toml)
@@ -148,7 +149,7 @@ impl Loader {
148149 merge_toml_values(theme, palette.into(), 1)
149150 }
150151
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`
152153 fn load_toml(&self, path: PathBuf) -> Result<Value> {
153154 let data = std::fs::read_to_string(path)?;
154155 let value = toml::from_str(&data)?;
@@ -156,25 +157,35 @@ impl Loader {
156157 Ok(value)
157158 }
158159
159− // Returns the path to the theme with the name
160− // With `only_default_dir` as false the path will first search for the user path
161− // disabled it ignores the user path and returns only the default path
162− fn path(&self, name: &str, only_default_dir: bool) -> PathBuf {
160+ /// Returns the path to the theme with the given name
161+ ///
162+ /// Ignores paths already visited and follows directory priority order.
163+ fn path(&self, name: &str, visited_paths: &mut HashSet<PathBuf>) -> Result<PathBuf> {
163164 let filename = format!("{}.toml", name);
164165
165− let user_path = self.user_dir.join(&filename);
166− if !only_default_dir && user_path.exists() {
167− user_path
168− } else {
169− self.default_dir.join(filename)
170− }
171− }
172−
173− /// Lists all theme names available in default and user directory
174− pub fn names(&self) -> Vec<String> {
175− let mut names = Self::read_names(&self.user_dir);
176− names.extend(Self::read_names(&self.default_dir));
177− names
166+ let mut cycle_found = false; // track if there was a path, but it was in a cycle
167+ self.theme_dirs
168+ .iter()
169+ .find_map(|dir| {
170+ let path = dir.join(&filename);
171+ if !path.exists() {
172+ None
173+ } else if visited_paths.contains(&path) {
174+ // Avoiding cycle, continuing to look in lower priority directories
175+ cycle_found = true;
176+ None
177+ } else {
178+ visited_paths.insert(path.clone());
179+ Some(path)
180+ }
181+ })
182+ .ok_or_else(|| {
183+ if cycle_found {
184+ anyhow!("Theme: cycle found in inheriting: {}", name)
185+ } else {
186+ anyhow!("Theme: file not found for: {}", name)
187+ }
188+ })
178189 }
179190
180191 pub fn default_theme(&self, true_color: bool) -> Theme {