trail/config/
last_used.rs1use std::io;
19use std::path::{Path, PathBuf};
20
21use serde::{Deserialize, Serialize};
22
23pub const STATE_FILE_NAME: &str = "last_config.toml";
25
26#[derive(Debug, Serialize, Deserialize)]
28struct StoredConfigPath {
29 path: PathBuf,
31}
32
33#[derive(Debug, Clone, PartialEq, Eq)]
38pub enum ConfigSource {
39 Defaults,
41
42 Explicit(PathBuf),
46
47 Remembered(PathBuf),
52}
53
54pub fn resolve(
62 explicit: Option<PathBuf>,
63 remembered: Option<PathBuf>,
64 ignore_remembered: bool,
65) -> ConfigSource {
66 if let Some(path) = explicit {
67 return ConfigSource::Explicit(path);
68 }
69 if ignore_remembered {
70 return ConfigSource::Defaults;
71 }
72 match remembered {
73 Some(path) => ConfigSource::Remembered(path),
74 None => ConfigSource::Defaults,
75 }
76}
77
78pub fn remembered(state_file: &Path) -> Option<PathBuf> {
84 let content = match std::fs::read_to_string(state_file) {
85 Ok(c) => c,
86 Err(e) => {
87 tracing::debug!(?state_file, "no remembered config: {e}");
89 return None;
90 }
91 };
92
93 match toml::from_str::<StoredConfigPath>(&content) {
94 Ok(stored) => Some(stored.path),
95 Err(e) => {
96 tracing::debug!(?state_file, "ignoring malformed remembered config: {e}");
97 None
98 }
99 }
100}
101
102pub fn remember(state_file: &Path, config_path: &Path) -> io::Result<()> {
117 let absolute = std::path::absolute(config_path).unwrap_or_else(|e| {
121 tracing::debug!(?config_path, "could not absolutize config path: {e}");
122 config_path.to_owned()
123 });
124
125 let stored = StoredConfigPath { path: absolute };
126 let content = toml::to_string_pretty(&stored)
127 .map_err(|e| io::Error::new(io::ErrorKind::InvalidData, e))?;
128 std::fs::write(state_file, content)
129}
130
131#[cfg(test)]
132mod tests {
133 use super::*;
134
135 #[test]
136 fn resolve_prefers_explicit_over_remembered() {
137 let source = resolve(
138 Some(PathBuf::from("/explicit.toml")),
139 Some(PathBuf::from("/remembered.toml")),
140 false,
141 );
142 assert_eq!(
143 source,
144 ConfigSource::Explicit(PathBuf::from("/explicit.toml"))
145 );
146 }
147
148 #[test]
149 fn resolve_uses_remembered_when_no_explicit() {
150 let source = resolve(None, Some(PathBuf::from("/remembered.toml")), false);
151 assert_eq!(
152 source,
153 ConfigSource::Remembered(PathBuf::from("/remembered.toml"))
154 );
155 }
156
157 #[test]
158 fn resolve_ignores_remembered_when_bypassed() {
159 let source = resolve(None, Some(PathBuf::from("/remembered.toml")), true);
160 assert_eq!(source, ConfigSource::Defaults);
161 }
162
163 #[test]
164 fn resolve_falls_back_to_defaults() {
165 assert_eq!(resolve(None, None, false), ConfigSource::Defaults);
166 }
167
168 #[test]
169 fn explicit_still_wins_when_remembered_is_bypassed() {
170 let source = resolve(Some(PathBuf::from("/explicit.toml")), None, true);
173 assert_eq!(
174 source,
175 ConfigSource::Explicit(PathBuf::from("/explicit.toml"))
176 );
177 }
178
179 #[test]
180 fn remember_then_recall_round_trips() {
181 let dir = tempfile::tempdir().unwrap();
182 let state = dir.path().join(STATE_FILE_NAME);
183 let config = dir.path().join("trail.toml");
184 std::fs::write(&config, "").unwrap();
185
186 remember(&state, &config).unwrap();
187 assert_eq!(remembered(&state), Some(config));
188 }
189
190 #[test]
191 fn remember_stores_an_absolute_path() {
192 let dir = tempfile::tempdir().unwrap();
193 let state = dir.path().join(STATE_FILE_NAME);
194
195 remember(&state, Path::new("trail.toml")).unwrap();
196
197 let recalled = remembered(&state).expect("a path should be remembered");
198 assert!(
199 recalled.is_absolute(),
200 "a relative --config path must be stored absolute so it still \
201 resolves when Trail is next launched from another directory; got {recalled:?}"
202 );
203 assert!(recalled.ends_with("trail.toml"));
204 }
205
206 #[test]
207 fn recall_returns_none_when_state_file_is_absent() {
208 let dir = tempfile::tempdir().unwrap();
209 assert_eq!(remembered(&dir.path().join("does-not-exist.toml")), None);
210 }
211
212 #[test]
213 fn recall_returns_none_for_a_malformed_state_file() {
214 let dir = tempfile::tempdir().unwrap();
215 let state = dir.path().join(STATE_FILE_NAME);
216 std::fs::write(&state, "this is not valid toml {{{").unwrap();
217
218 assert_eq!(remembered(&state), None);
221 }
222
223 #[test]
224 fn recall_returns_none_when_key_is_missing() {
225 let dir = tempfile::tempdir().unwrap();
226 let state = dir.path().join(STATE_FILE_NAME);
227 std::fs::write(&state, "unrelated = \"value\"\n").unwrap();
228
229 assert_eq!(remembered(&state), None);
230 }
231
232 #[test]
233 fn remember_overwrites_a_previous_path() {
234 let dir = tempfile::tempdir().unwrap();
235 let state = dir.path().join(STATE_FILE_NAME);
236 let first = dir.path().join("first.toml");
237 let second = dir.path().join("second.toml");
238
239 remember(&state, &first).unwrap();
240 remember(&state, &second).unwrap();
241
242 assert_eq!(remembered(&state), Some(second));
243 }
244}