on master help sign in
Generalised to multiple runtime directories with priorities (#5411)
ce1fb9epaul-scott
book/src/install.md+27−11
@@ -137,8 +137,8 @@ cargo install --path helix-term --locked
137```
137```
138
138
139This command will create the `hx` executable and construct the tree-sitter
139This command will create the `hx` executable and construct the tree-sitter
140−grammars either in the `runtime` folder, or in the folder specified in `HELIX_RUNTIME`
140+grammars in the local `runtime` folder. To build the tree-sitter grammars requires
141−(as described below). To build the tree-sitter grammars requires a c++ compiler to be installed, for example `gcc-c++`.
141+a c++ compiler to be installed, for example `gcc-c++`.
142
142
143> 💡 If you are using the musl-libc instead of glibc the following environment variable must be set during the build
143> 💡 If you are using the musl-libc instead of glibc the following environment variable must be set during the build
144> to ensure tree-sitter grammars can be loaded correctly:
144> to ensure tree-sitter grammars can be loaded correctly:
@@ -149,11 +149,13 @@ grammars either in the `runtime` folder, or in the folder specified in `HELIX_RU
149
149
150> 💡 Tree-sitter grammars can be fetched and compiled if not pre-packaged. Fetch
150> 💡 Tree-sitter grammars can be fetched and compiled if not pre-packaged. Fetch
151> grammars with `hx --grammar fetch` (requires `git`) and compile them with
151> grammars with `hx --grammar fetch` (requires `git`) and compile them with
152−> `hx --grammar build` (requires a C++ compiler).
152+> `hx --grammar build` (requires a C++ compiler). This will install them in
153+> the `runtime` directory within the user's helix config directory (more
154+> [details below](#multiple-runtime-directories)).
153
155
154### Configuring Helix's runtime files
156### Configuring Helix's runtime files
155
157
156−- **Linux and macOS**
158+#### Linux and macOS
157
159
158Either set the `HELIX_RUNTIME` environment variable to point to the runtime files and add it to your `~/.bashrc` or equivalent:
160Either set the `HELIX_RUNTIME` environment variable to point to the runtime files and add it to your `~/.bashrc` or equivalent:
159
161
@@ -167,7 +169,7 @@ Or, create a symlink in `~/.config/helix` that links to the source code director
167ln -s $PWD/runtime ~/.config/helix/runtime
169ln -s $PWD/runtime ~/.config/helix/runtime
168```
170```
169
171
170−- **Windows**
172+#### Windows
171
173
172Either set the `HELIX_RUNTIME` environment variable to point to the runtime files using the Windows setting (search for
174Either set the `HELIX_RUNTIME` environment variable to point to the runtime files using the Windows setting (search for
173`Edit environment variables for your account`) or use the `setx` command in
175`Edit environment variables for your account`) or use the `setx` command in
@@ -182,13 +184,27 @@ setx HELIX_RUNTIME "%userprofile%\source\repos\helix\runtime"
182
184
183Or, create a symlink in `%appdata%\helix\` that links to the source code directory:
185Or, create a symlink in `%appdata%\helix\` that links to the source code directory:
184
186
185− | Method | Command |
187+| Method | Command |
186− | ---------- | -------------------------------------------------------------------------------------- |
188+| ---------- | -------------------------------------------------------------------------------------- |
187− | PowerShell | `New-Item -ItemType Junction -Target "runtime" -Path "$Env:AppData\helix\runtime"` |
189+| PowerShell | `New-Item -ItemType Junction -Target "runtime" -Path "$Env:AppData\helix\runtime"` |
188− | Cmd | `cd %appdata%\helix` <br/> `mklink /D runtime "%userprofile%\src\helix\runtime"` |
190+| Cmd | `cd %appdata%\helix` <br/> `mklink /D runtime "%userprofile%\src\helix\runtime"` |
189
191
190− > 💡 On Windows, creating a symbolic link may require running PowerShell or
192+> 💡 On Windows, creating a symbolic link may require running PowerShell or
191− > Cmd as an administrator.
193+> Cmd as an administrator.
194+
195+#### Multiple runtime directories
196+
197+When Helix finds multiple runtime directories it will search through them for files in the
198+following order:
199+
200+1. `runtime/` sibling directory to `$CARGO_MANIFEST_DIR` directory (this is intended for
201+ developing and testing helix only).
202+2. `runtime/` subdirectory of OS-dependent helix user config directory.
203+3. `$HELIX_RUNTIME`.
204+4. `runtime/` subdirectory of path to Helix executable.
205+
206+This order also sets the priority for selecting which file will be used if multiple runtime
207+directories have files with the same name.
192
208
193### Validating the installation
209### Validating the installation
194
210
helix-loader/src/grammar.rs+14−9
@@ -67,8 +67,9 @@ pub fn get_language(name: &str) -> Result<Language> {
67#[cfg(not(target_arch = "wasm32"))]
67#[cfg(not(target_arch = "wasm32"))]
68pub fn get_language(name: &str) -> Result<Language> {
68pub fn get_language(name: &str) -> Result<Language> {
69 use libloading::{Library, Symbol};
69 use libloading::{Library, Symbol};
70− let mut library_path = crate::runtime_dir().join("grammars").join(name);
70+ let mut rel_library_path = PathBuf::new().join("grammars").join(name);
71− library_path.set_extension(DYLIB_EXTENSION);
71+ rel_library_path.set_extension(DYLIB_EXTENSION);
72+ let library_path = crate::runtime_file(&rel_library_path);
72
73
73 let library = unsafe { Library::new(&library_path) }
74 let library = unsafe { Library::new(&library_path) }
74 .with_context(|| format!("Error opening dynamic library {:?}", library_path))?;
75 .with_context(|| format!("Error opening dynamic library {:?}", library_path))?;
@@ -252,7 +253,9 @@ fn fetch_grammar(grammar: GrammarConfiguration) -> Result<FetchStatus> {
252 remote, revision, ..
253 remote, revision, ..
253 } = grammar.source
254 } = grammar.source
254 {
255 {
255− let grammar_dir = crate::runtime_dir()
256+ let grammar_dir = crate::runtime_dirs()
257+ .first()
258+ .expect("No runtime directories provided") // guaranteed by post-condition
256 .join("grammars")
259 .join("grammars")
257 .join("sources")
260 .join("sources")
258 .join(&grammar.grammar_id);
261 .join(&grammar.grammar_id);
@@ -350,7 +353,9 @@ fn build_grammar(grammar: GrammarConfiguration, target: Option<&str>) -> Result<
350 let grammar_dir = if let GrammarSource::Local { path } = &grammar.source {
353 let grammar_dir = if let GrammarSource::Local { path } = &grammar.source {
351 PathBuf::from(&path)
354 PathBuf::from(&path)
352 } else {
355 } else {
353− crate::runtime_dir()
356+ crate::runtime_dirs()
357+ .first()
358+ .expect("No runtime directories provided") // guaranteed by post-condition
354 .join("grammars")
359 .join("grammars")
355 .join("sources")
360 .join("sources")
356 .join(&grammar.grammar_id)
361 .join(&grammar.grammar_id)
@@ -401,7 +406,10 @@ fn build_tree_sitter_library(
401 None
406 None
402 }
407 }
403 };
408 };
404− let parser_lib_path = crate::runtime_dir().join("grammars");
409+ let parser_lib_path = crate::runtime_dirs()
410+ .first()
411+ .expect("No runtime directories provided") // guaranteed by post-condition
412+ .join("grammars");
405 let mut library_path = parser_lib_path.join(&grammar.grammar_id);
413 let mut library_path = parser_lib_path.join(&grammar.grammar_id);
406 library_path.set_extension(DYLIB_EXTENSION);
414 library_path.set_extension(DYLIB_EXTENSION);
407
415
@@ -511,9 +519,6 @@ fn mtime(path: &Path) -> Result<SystemTime> {
511/// Gives the contents of a file from a language's `runtime/queries/<lang>`
519/// Gives the contents of a file from a language's `runtime/queries/<lang>`
512/// directory
520/// directory
513pub fn load_runtime_file(language: &str, filename: &str) -> Result<String, std::io::Error> {
521pub fn load_runtime_file(language: &str, filename: &str) -> Result<String, std::io::Error> {
514− let path = crate::RUNTIME_DIR
522+ let path = crate::runtime_file(&PathBuf::new().join("queries").join(language).join(filename));
515− .join("queries")
516− .join(language)
517− .join(filename);
518 std::fs::read_to_string(path)
523 std::fs::read_to_string(path)
519}
524}
helix-loader/src/lib.rs+67−14
@@ -2,11 +2,12 @@ pub mod config;
2pub mod grammar;
2pub mod grammar;
3
3
4use etcetera::base_strategy::{choose_base_strategy, BaseStrategy};
4use etcetera::base_strategy::{choose_base_strategy, BaseStrategy};
5−use std::path::PathBuf;
5+use std::path::{Path, PathBuf};
6
6
7pub const VERSION_AND_GIT_HASH: &str = env!("VERSION_AND_GIT_HASH");
7pub const VERSION_AND_GIT_HASH: &str = env!("VERSION_AND_GIT_HASH");
8
8
9−pub static RUNTIME_DIR: once_cell::sync::Lazy<PathBuf> = once_cell::sync::Lazy::new(runtime_dir);
9+static RUNTIME_DIRS: once_cell::sync::Lazy<Vec<PathBuf>> =
10+ once_cell::sync::Lazy::new(prioritize_runtime_dirs);
10
11
11static CONFIG_FILE: once_cell::sync::OnceCell<PathBuf> = once_cell::sync::OnceCell::new();
12static CONFIG_FILE: once_cell::sync::OnceCell<PathBuf> = once_cell::sync::OnceCell::new();
12
13
@@ -25,31 +26,83 @@ pub fn initialize_config_file(specified_file: Option<PathBuf>) {
25 CONFIG_FILE.set(config_file).ok();
26 CONFIG_FILE.set(config_file).ok();
26}
27}
27
28
28−pub fn runtime_dir() -> PathBuf {
29+/// A list of runtime directories from highest to lowest priority
29− if let Ok(dir) = std::env::var("HELIX_RUNTIME") {
30+///
30− return dir.into();
31+/// The priority is:
31− }
32+///
32−
33+/// 1. sibling directory to `CARGO_MANIFEST_DIR` (if environment variable is set)
34+/// 2. subdirectory of user config directory (always included)
35+/// 3. `HELIX_RUNTIME` (if environment variable is set)
36+/// 4. subdirectory of path to helix executable (always included)
37+///
38+/// Postcondition: returns at least two paths (they might not exist).
39+fn prioritize_runtime_dirs() -> Vec<PathBuf> {
40+ const RT_DIR: &str = "runtime";
41+ // Adding higher priority first
42+ let mut rt_dirs = Vec::new();
33 if let Ok(dir) = std::env::var("CARGO_MANIFEST_DIR") {
43 if let Ok(dir) = std::env::var("CARGO_MANIFEST_DIR") {
34 // this is the directory of the crate being run by cargo, we need the workspace path so we take the parent
44 // this is the directory of the crate being run by cargo, we need the workspace path so we take the parent
35 let path = std::path::PathBuf::from(dir).parent().unwrap().join(RT_DIR);
45 let path = std::path::PathBuf::from(dir).parent().unwrap().join(RT_DIR);
36 log::debug!("runtime dir: {}", path.to_string_lossy());
46 log::debug!("runtime dir: {}", path.to_string_lossy());
37− return path;
47+ rt_dirs.push(path);
38 }
48 }
39
49
40− const RT_DIR: &str = "runtime";
41− let conf_dir = config_dir().join(RT_DIR);
50+ let conf_rt_dir = config_dir().join(RT_DIR);
42− if conf_dir.exists() {
51+ rt_dirs.push(conf_rt_dir);
43− return conf_dir;
52+
53+ if let Ok(dir) = std::env::var("HELIX_RUNTIME") {
54+ rt_dirs.push(dir.into());
44 }
55 }
45
56
46 // fallback to location of the executable being run
57 // fallback to location of the executable being run
47 // canonicalize the path in case the executable is symlinked
58 // canonicalize the path in case the executable is symlinked
48− std::env::current_exe()
59+ let exe_rt_dir = std::env::current_exe()
49 .ok()
60 .ok()
50 .and_then(|path| std::fs::canonicalize(path).ok())
61 .and_then(|path| std::fs::canonicalize(path).ok())
51 .and_then(|path| path.parent().map(|path| path.to_path_buf().join(RT_DIR)))
62 .and_then(|path| path.parent().map(|path| path.to_path_buf().join(RT_DIR)))
52− .unwrap()
63+ .unwrap();
64+ rt_dirs.push(exe_rt_dir);
65+ rt_dirs
66+}
67+
68+/// Runtime directories ordered from highest to lowest priority
69+///
70+/// All directories should be checked when looking for files.
71+///
72+/// Postcondition: returns at least one path (it might not exist).
73+pub fn runtime_dirs() -> &'static [PathBuf] {
74+ &RUNTIME_DIRS
75+}
76+
77+/// Find file with path relative to runtime directory
78+///
79+/// `rel_path` should be the relative path from within the `runtime/` directory.
80+/// The valid runtime directories are searched in priority order and the first
81+/// file found to exist is returned, otherwise None.
82+fn find_runtime_file(rel_path: &Path) -> Option<PathBuf> {
83+ RUNTIME_DIRS.iter().find_map(|rt_dir| {
84+ let path = rt_dir.join(rel_path);
85+ if path.exists() {
86+ Some(path)
87+ } else {
88+ None
89+ }
90+ })
91+}
92+
93+/// Find file with path relative to runtime directory
94+///
95+/// `rel_path` should be the relative path from within the `runtime/` directory.
96+/// The valid runtime directories are searched in priority order and the first
97+/// file found to exist is returned, otherwise the path to the final attempt
98+/// that failed.
99+pub fn runtime_file(rel_path: &Path) -> PathBuf {
100+ find_runtime_file(rel_path).unwrap_or_else(|| {
101+ RUNTIME_DIRS
102+ .last()
103+ .map(|dir| dir.join(rel_path))
104+ .unwrap_or_default()
105+ })
53}
106}
54
107
55pub fn config_dir() -> PathBuf {
108pub fn config_dir() -> PathBuf {
helix-term/src/application.rs+5−5
@@ -31,6 +31,7 @@ use crate::{
31use log::{debug, error, warn};
31use log::{debug, error, warn};
32use std::{
32use std::{
33 io::{stdin, stdout},
33 io::{stdin, stdout},
34+ path::Path,
34 sync::Arc,
35 sync::Arc,
35 time::{Duration, Instant},
36 time::{Duration, Instant},
36};
37};
@@ -113,10 +114,9 @@ impl Application {
113
114
114 use helix_view::editor::Action;
115 use helix_view::editor::Action;
115
116
117+ let mut theme_parent_dirs = vec![helix_loader::config_dir()];
118+ theme_parent_dirs.extend(helix_loader::runtime_dirs().iter().cloned());
116− let theme_loader = std::sync::Arc::new(theme::Loader::new(
119+ let theme_loader = std::sync::Arc::new(theme::Loader::new(&theme_parent_dirs));
117− &helix_loader::config_dir(),
118− &helix_loader::runtime_dir(),
119− ));
120
120
121 let true_color = config.editor.true_color || crate::true_color();
121 let true_color = config.editor.true_color || crate::true_color();
122 let theme = config
122 let theme = config
@@ -162,7 +162,7 @@ impl Application {
162 compositor.push(editor_view);
162 compositor.push(editor_view);
163
163
164 if args.load_tutor {
164 if args.load_tutor {
165− let path = helix_loader::runtime_dir().join("tutor");
165+ let path = helix_loader::runtime_file(Path::new("tutor"));
166 editor.open(&path, Action::VerticalSplit)?;
166 editor.open(&path, Action::VerticalSplit)?;
167 // Unset path to prevent accidentally saving to the original tutor file.
167 // Unset path to prevent accidentally saving to the original tutor file.
168 doc_mut!(editor).set_path(None)?;
168 doc_mut!(editor).set_path(None)?;
helix-term/src/commands/typed.rs+1−1
@@ -1565,7 +1565,7 @@ fn tutor(
1565 return Ok(());
1565 return Ok(());
1566 }
1566 }
1567
1567
1568− let path = helix_loader::runtime_dir().join("tutor");
1568+ let path = helix_loader::runtime_file(Path::new("tutor"));
1569 cx.editor.open(&path, Action::Replace)?;
1569 cx.editor.open(&path, Action::Replace)?;
1570 // Unset path to prevent accidentally saving to the original tutor file.
1570 // Unset path to prevent accidentally saving to the original tutor file.
1571 doc_mut!(cx.editor).set_path(None)?;
1571 doc_mut!(cx.editor).set_path(None)?;
helix-term/src/health.rs+26−12
@@ -52,7 +52,7 @@ pub fn general() -> std::io::Result<()> {
52 let config_file = helix_loader::config_file();
52 let config_file = helix_loader::config_file();
53 let lang_file = helix_loader::lang_config_file();
53 let lang_file = helix_loader::lang_config_file();
54 let log_file = helix_loader::log_file();
54 let log_file = helix_loader::log_file();
55− let rt_dir = helix_loader::runtime_dir();
55+ let rt_dirs = helix_loader::runtime_dirs();
56 let clipboard_provider = get_clipboard_provider();
56 let clipboard_provider = get_clipboard_provider();
57
57
58 if config_file.exists() {
58 if config_file.exists() {
@@ -66,17 +66,31 @@ pub fn general() -> std::io::Result<()> {
66 writeln!(stdout, "Language file: default")?;
66 writeln!(stdout, "Language file: default")?;
67 }
67 }
68 writeln!(stdout, "Log file: {}", log_file.display())?;
68 writeln!(stdout, "Log file: {}", log_file.display())?;
69− writeln!(stdout, "Runtime directory: {}", rt_dir.display())?;
69+ writeln!(
70−
70+ stdout,
71+ "Runtime directories: {}",
72+ rt_dirs
73+ .iter()
74+ .map(|d| d.to_string_lossy())
75+ .collect::<Vec<_>>()
76+ .join(";")
77+ )?;
78+ for rt_dir in rt_dirs.iter() {
71− if let Ok(path) = std::fs::read_link(&rt_dir) {
79+ if let Ok(path) = std::fs::read_link(rt_dir) {
80+ let msg = format!(
72− let msg = format!("Runtime directory is symlinked to {}", path.display());
81+ "Runtime directory {} is symlinked to: {}",
82+ rt_dir.display(),
83+ path.display()
84+ );
73− writeln!(stdout, "{}", msg.yellow())?;
85+ writeln!(stdout, "{}", msg.yellow())?;
74− }
86+ }
75− if !rt_dir.exists() {
87+ if !rt_dir.exists() {
76− writeln!(stdout, "{}", "Runtime directory does not exist.".red())?;
88+ let msg = format!("Runtime directory does not exist: {}", rt_dir.display());
77− }
89+ writeln!(stdout, "{}", msg.yellow())?;
78− if rt_dir.read_dir().ok().map(|it| it.count()) == Some(0) {
90+ } else if rt_dir.read_dir().ok().map(|it| it.count()) == Some(0) {
79− writeln!(stdout, "{}", "Runtime directory is empty.".red())?;
91+ let msg = format!("Runtime directory is empty: {}", rt_dir.display());
92+ writeln!(stdout, "{}", msg.yellow())?;
93+ }
80 }
94 }
81 writeln!(stdout, "Clipboard provider: {}", clipboard_provider.name())?;
95 writeln!(stdout, "Clipboard provider: {}", clipboard_provider.name())?;
82
96
helix-term/src/ui/mod.rs+4−4
@@ -280,10 +280,10 @@ pub mod completers {
280 }
280 }
281
281
282 pub fn theme(_editor: &Editor, input: &str) -> Vec<Completion> {
282 pub fn theme(_editor: &Editor, input: &str) -> Vec<Completion> {
283− let mut names = theme::Loader::read_names(&helix_loader::runtime_dir().join("themes"));
283+ let mut names = theme::Loader::read_names(&helix_loader::config_dir().join("themes"));
284− names.extend(theme::Loader::read_names(
284+ for rt_dir in helix_loader::runtime_dirs() {
285− &helix_loader::config_dir().join("themes"),
285+ names.extend(theme::Loader::read_names(&rt_dir.join("themes")));
286− ));
286+ }
287 names.push("default".into());
287 names.push("default".into());
288 names.push("base16_default".into());
288 names.push("base16_default".into());
289 names.sort();
289 names.sort();
helix-view/src/theme.rs+53−42
@@ -1,5 +1,5 @@
1use std::{
1use std::{
2− collections::HashMap,
2+ collections::{HashMap, HashSet},
3 path::{Path, PathBuf},
3 path::{Path, PathBuf},
4 str,
4 str,
5};
5};
@@ -37,19 +37,21 @@ pub static BASE16_DEFAULT_THEME: Lazy<Theme> = Lazy::new(|| Theme {
37
37
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>,
42}
42}
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 {
46 Self {
49 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"),
49 }
51 }
50 }
52 }
51
53
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());
59 }
61 }
60
62
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)?;
62
65
63 Ok(Theme {
66 Ok(Theme {
64 name: name.into(),
67 name: name.into(),
@@ -66,16 +69,18 @@ impl Loader {
66 })
69 })
67 }
70 }
68
71
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.
73− &self,
76+ ///
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)?;
83+
79 let theme_toml = self.load_toml(path)?;
84 let theme_toml = self.load_toml(path)?;
80
85
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)?,
97− base_theme_name,
98− base_theme_name == parent_theme_name,
99− )?,
100 };
101 };
101
102
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)
149 }
150 }
150
151
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)
157 }
158 }
158
159
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);
164
165
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| {
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() {
167− user_path
172+ None
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;
171− }
176+ None
172−
177+ } else {
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(|| {
183+ if cycle_found {
184+ anyhow!("Theme: cycle found in inheriting: {}", name)
185+ } else {
186+ anyhow!("Theme: file not found for: {}", name)
187+ }
188+ })
178 }
189 }
179
190
180 pub fn default_theme(&self, true_color: bool) -> Theme {
191 pub fn default_theme(&self, true_color: bool) -> Theme {