1use std::{
4 env::current_dir,
5 ffi::OsString,
6 fmt,
7 io::Cursor,
8 path::{Path, PathBuf},
9};
10
11use crate::error::{TopiaryConfigError, TopiaryConfigResult};
12
13#[derive(Debug, Clone)]
15pub enum Source {
16 Builtin,
17 Directory(PathBuf),
18 File(PathBuf),
19}
20
21impl From<Source> for nickel_lang_core::program::Input<Cursor<String>, OsString> {
22 fn from(source: Source) -> Self {
23 match source {
24 Source::Builtin => {
25 Self::Source(Cursor::new(source.builtin_nickel()), "built-in".into())
26 }
27 Source::Directory(path) => Self::Path(path.into()),
28 Source::File(path) => Self::Path(path.into()),
29 }
30 }
31}
32
33impl Source {
34 pub fn config_sources(path: &Option<PathBuf>) -> impl Iterator<Item = (&'static str, Self)> {
42 let mut sources = Vec::new();
43
44 if let Some(path) = path {
45 let source = if path.is_dir() {
46 Self::Directory(path.clone())
47 } else {
48 Self::File(path.clone())
49 };
50
51 sources.push(("CLI", source));
52 }
53
54 sources.append(&mut vec![
55 ("workspace", workspace_config_dir()),
56 #[cfg(target_os = "macos")]
57 ("unix-home", unix_home_config_dir()),
58 ("OS", os_config_dir()),
59 ("built-in", Self::Builtin),
61 ]);
62
63 sources.into_iter()
64 }
65
66 pub fn queries_dir(&self) -> Option<PathBuf> {
68 match self {
69 Source::Builtin => None,
70 Source::Directory(dir) => Some(dir.join("queries")),
71 Source::File(file) => file.parent().map(|d| d.join("queries")),
72 }
73 }
74
75 pub fn languages_file(&self) -> Option<PathBuf> {
77 match self {
78 Source::Builtin => None,
79 Source::File(file) => Some(file.clone()),
80 Source::Directory(dir) => Some(dir.join("languages.ncl")),
81 }
82 }
83
84 fn valid_config_sources(file: &Option<PathBuf>) -> impl Iterator<Item = (&'static str, Self)> {
86 Self::config_sources(file).filter_map(|(hint, candidate)| {
87 if matches!(candidate, Self::Builtin) {
88 return Some((hint, candidate));
89 }
90 let languages_file = candidate.languages_file().unwrap();
91 if !languages_file.exists() {
92 log::debug!("configuration file not found: {}.", candidate);
93 return None;
94 }
95
96 Some((hint, Self::File(languages_file)))
97 })
98 }
99 pub fn fetch_all(file: &Option<PathBuf>) -> Vec<Self> {
102 log::info!("Adding built-in configuration to merge");
104 Self::valid_config_sources(file)
105 .inspect(|(hint, candidate)| {
106 let Self::File(path) = candidate else { return };
107 log::info!(
108 "Adding {hint}-specified configuration to merge: {}",
109 path.display()
110 );
111 })
112 .map(|(_, s)| s)
113 .collect()
114 }
115
116 pub fn languages_exists(&self) -> bool {
118 match self {
119 Source::Builtin => true,
120 Source::File(file) => file.exists(),
121 Source::Directory(dir) => dir.join("languages.ncl").exists(),
122 }
123 }
124
125 pub fn fetch_one(file: &Option<PathBuf>) -> Self {
128 let (hint, source) = Self::valid_config_sources(file)
129 .next()
130 .expect("built-in should always be present");
131 log::info!("Using {hint}-specified configuration: {source}");
132 source
133 }
134
135 #[allow(clippy::result_large_err)]
136 pub fn read(&self) -> TopiaryConfigResult<Vec<u8>> {
137 match self {
138 Self::Builtin => Ok(self.builtin_nickel().into_bytes()),
139
140 Self::Directory(dir) => read_to_string(&dir.join("languages.ncl")),
141 Self::File(path) => read_to_string(path),
142 }
143 }
144
145 fn builtin_nickel(&self) -> String {
146 include_str!("../languages.ncl").to_string()
147 }
148}
149
150fn read_to_string(path: &Path) -> TopiaryConfigResult<Vec<u8>> {
151 std::fs::read_to_string(path)
152 .map_err(TopiaryConfigError::Io)
153 .map(|s| s.into_bytes())
154}
155
156impl fmt::Display for Source {
157 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
158 match self {
159 Self::Builtin => write!(f, "<built-in>"),
160
161 Self::File(path) | Self::Directory(path) => {
162 let config = path.canonicalize().unwrap_or(path.clone());
168 write!(f, "{}", config.display())
169 }
170 }
171 }
172}
173
174fn os_config_dir() -> Source {
177 Source::Directory(crate::project_dirs().config_dir().to_path_buf())
178}
179
180fn workspace_config_dir() -> Source {
184 let pwd = current_dir().expect("Could not get current working directory");
185 let dir = pwd
186 .ancestors()
187 .map(|path| path.join(".topiary"))
188 .find(|path| path.exists())
189 .unwrap_or_else(|| pwd.join(".topiary"));
190
191 Source::Directory(dir)
192}
193
194#[cfg(target_os = "macos")]
199fn unix_home_config_dir() -> Source {
200 let dir = std::env::home_dir()
201 .unwrap_or_default()
202 .join(".config/topiary");
203
204 Source::Directory(dir)
205}