1use std::{
3 collections::HashMap,
4 fs::{File, create_dir_all},
5 io::{BufReader, prelude::*},
6 path::{Path, PathBuf},
7};
8
9use serde::{Deserialize, Serialize};
10use shellexpand::tilde;
11
12use crate::{error::Error, internal_prelude::*, setting_defaults::*};
13
14pub const PUEUE_CONFIG_PATH_ENV: &str = "PUEUE_CONFIG_PATH";
16
17#[derive(PartialEq, Eq, Clone, Debug, Deserialize, Serialize)]
19pub struct Shared {
20 pub pueue_directory: Option<PathBuf>,
26 pub runtime_directory: Option<PathBuf>,
32 pub alias_file: Option<PathBuf>,
38
39 #[cfg(not(target_os = "windows"))]
42 #[serde(default = "default_true")]
43 pub use_unix_socket: bool,
44 #[cfg(not(target_os = "windows"))]
49 pub unix_socket_path: Option<PathBuf>,
50 #[cfg(not(target_os = "windows"))]
55 pub unix_socket_permissions: Option<u32>,
56
57 #[serde(default = "default_host")]
59 pub host: String,
60 #[serde(default = "default_port")]
62 pub port: String,
63
64 pub pid_path: Option<PathBuf>,
67
68 pub daemon_cert: Option<PathBuf>,
74 pub daemon_key: Option<PathBuf>,
79 pub shared_secret_path: Option<PathBuf>,
84}
85
86#[derive(PartialEq, Eq, Clone, Debug, Deserialize, Serialize, Default)]
88#[serde(rename_all = "lowercase")]
89pub enum EditMode {
90 #[default]
92 Toml,
93 Files,
95}
96
97#[derive(PartialEq, Eq, Clone, Debug, Deserialize, Serialize)]
99pub struct Client {
100 #[serde(default = "Default::default")]
104 pub restart_in_place: bool,
105 #[serde(default = "default_true")]
108 pub read_local_logs: bool,
109 #[serde(default = "Default::default")]
111 pub show_confirmation_questions: bool,
112 #[serde(default = "Default::default")]
114 pub edit_mode: EditMode,
115 #[serde(default = "Default::default")]
118 pub show_expanded_aliases: bool,
119 #[serde(default = "Default::default")]
121 pub dark_mode: bool,
122 pub max_status_lines: Option<usize>,
124 #[serde(default = "default_status_time_format")]
126 pub status_time_format: String,
127 #[serde(default = "default_status_datetime_format")]
129 pub status_datetime_format: String,
130}
131
132#[derive(PartialEq, Eq, Clone, Debug, Deserialize, Serialize)]
134pub struct Daemon {
135 #[serde(default = "Default::default")]
137 pub pause_group_on_failure: bool,
138 #[serde(default = "Default::default")]
140 pub pause_all_on_failure: bool,
141 #[serde(default = "Default::default")]
148 pub compress_state_file: bool,
149 pub callback: Option<String>,
151 #[serde(default = "Default::default")]
153 pub env_vars: HashMap<String, String>,
154 #[serde(default = "default_callback_log_lines")]
156 pub callback_log_lines: usize,
157 pub shell_command: Option<Vec<String>>,
174}
175
176impl Default for Shared {
177 fn default() -> Self {
178 Shared {
179 pueue_directory: None,
180 runtime_directory: None,
181 alias_file: None,
182
183 #[cfg(not(target_os = "windows"))]
184 unix_socket_path: None,
185 #[cfg(not(target_os = "windows"))]
186 use_unix_socket: true,
187 #[cfg(not(target_os = "windows"))]
188 unix_socket_permissions: Some(0o700),
189 host: default_host(),
190 port: default_port(),
191
192 pid_path: None,
193 daemon_cert: None,
194 daemon_key: None,
195 shared_secret_path: None,
196 }
197 }
198}
199
200impl Default for Client {
201 fn default() -> Self {
202 Client {
203 restart_in_place: false,
204 read_local_logs: true,
205 show_confirmation_questions: false,
206 show_expanded_aliases: false,
207 edit_mode: Default::default(),
208 dark_mode: false,
209 max_status_lines: None,
210 status_time_format: default_status_time_format(),
211 status_datetime_format: default_status_datetime_format(),
212 }
213 }
214}
215
216impl Default for Daemon {
217 fn default() -> Self {
218 Daemon {
219 pause_group_on_failure: false,
220 pause_all_on_failure: false,
221 callback: None,
222 callback_log_lines: default_callback_log_lines(),
223 compress_state_file: false,
224 shell_command: None,
225 env_vars: HashMap::new(),
226 }
227 }
228}
229
230#[derive(PartialEq, Eq, Clone, Default, Debug, Deserialize, Serialize)]
233pub struct Settings {
234 #[serde(default = "Default::default")]
235 pub client: Client,
236 #[serde(default = "Default::default")]
237 pub daemon: Daemon,
238 #[serde(default = "Default::default")]
239 pub shared: Shared,
240 #[serde(default = "HashMap::new")]
241 pub profiles: HashMap<String, NestedSettings>,
242}
243
244#[derive(PartialEq, Eq, Clone, Debug, Deserialize, Serialize)]
248pub struct NestedSettings {
249 #[serde(default = "Default::default")]
250 pub client: Client,
251 #[serde(default = "Default::default")]
252 pub daemon: Daemon,
253 #[serde(default = "Default::default")]
254 pub shared: Shared,
255}
256
257pub fn default_configuration_directory() -> Option<PathBuf> {
258 dirs::config_dir().map(|dir| dir.join("pueue"))
259}
260
261pub fn configuration_directories() -> Vec<PathBuf> {
264 if let Some(config_dir) = default_configuration_directory() {
265 vec![config_dir, PathBuf::from(".")]
266 } else {
267 vec![PathBuf::from(".")]
268 }
269}
270
271pub fn expand_home(old_path: &Path) -> PathBuf {
273 PathBuf::from(tilde(&old_path.to_string_lossy()).into_owned())
274}
275
276impl Shared {
277 pub fn pueue_directory(&self) -> PathBuf {
278 if let Some(path) = &self.pueue_directory {
279 expand_home(path)
280 } else if let Some(path) = dirs::data_local_dir() {
281 path.join("pueue")
282 } else {
283 PathBuf::from("./pueue")
284 }
285 }
286
287 pub fn runtime_directory(&self) -> PathBuf {
292 if let Some(path) = &self.runtime_directory {
293 expand_home(path)
294 } else if let Some(path) = dirs::runtime_dir() {
295 path
296 } else {
297 self.pueue_directory()
298 }
299 }
300
301 #[cfg(not(target_os = "windows"))]
304 pub fn unix_socket_path(&self) -> PathBuf {
305 if let Some(path) = &self.unix_socket_path {
306 expand_home(path)
307 } else {
308 self.runtime_directory()
309 .join(format!("pueue_{}.socket", whoami::username()))
310 }
311 }
312
313 pub fn alias_file(&self) -> PathBuf {
316 if let Some(path) = &self.alias_file {
317 expand_home(path)
318 } else if let Some(config_dir) = default_configuration_directory() {
319 config_dir.join("pueue_aliases.yml")
320 } else {
321 PathBuf::from("pueue_aliases.yml")
322 }
323 }
324
325 pub fn pid_path(&self) -> PathBuf {
328 if let Some(path) = &self.pid_path {
329 expand_home(path)
330 } else {
331 self.runtime_directory().join("pueue.pid")
332 }
333 }
334
335 pub fn daemon_cert(&self) -> PathBuf {
336 if let Some(path) = &self.daemon_cert {
337 expand_home(path)
338 } else {
339 self.pueue_directory().join("certs").join("daemon.cert")
340 }
341 }
342
343 pub fn daemon_key(&self) -> PathBuf {
344 if let Some(path) = &self.daemon_key {
345 expand_home(path)
346 } else {
347 self.pueue_directory().join("certs").join("daemon.key")
348 }
349 }
350
351 pub fn shared_secret_path(&self) -> PathBuf {
352 if let Some(path) = &self.shared_secret_path {
353 expand_home(path)
354 } else {
355 self.pueue_directory().join("shared_secret")
356 }
357 }
358}
359
360impl Settings {
361 pub fn read(from_file: &Option<PathBuf>) -> Result<(Settings, bool), Error> {
367 let from_file = from_file
369 .clone()
370 .or_else(|| std::env::var(PUEUE_CONFIG_PATH_ENV).map(PathBuf::from).ok());
371
372 if let Some(path) = &from_file {
374 let file = File::open(path)
376 .map_err(|err| Error::IoPathError(path.clone(), "opening config file", err))?;
377 let reader = BufReader::new(file);
378
379 let settings = serde_yaml::from_reader(reader)
380 .map_err(|err| Error::ConfigDeserialization(err.to_string()))?;
381 return Ok((settings, true));
382 };
383
384 info!("Parsing config files");
385
386 let config_dirs = configuration_directories();
387 for directory in config_dirs.into_iter() {
388 let path = directory.join("pueue.yml");
389 info!("Checking path: {path:?}");
390
391 if path.exists() && path.is_file() {
393 info!("Found config file at: {path:?}");
394
395 let file = File::open(&path)
397 .map_err(|err| Error::IoPathError(path, "opening config file.", err))?;
398 let reader = BufReader::new(file);
399
400 let settings = serde_yaml::from_reader(reader)
401 .map_err(|err| Error::ConfigDeserialization(err.to_string()))?;
402 return Ok((settings, true));
403 }
404 }
405
406 info!("No config file found. Use default config.");
407 Ok((Settings::default(), false))
409 }
410
411 pub fn save(&self, path: &Option<PathBuf>) -> Result<(), Error> {
415 let config_path = if let Some(path) = path {
416 path.clone()
417 } else if let Ok(path) = std::env::var(PUEUE_CONFIG_PATH_ENV) {
418 PathBuf::from(path)
419 } else if let Some(path) = dirs::config_dir() {
420 let path = path.join("pueue");
421 path.join("pueue.yml")
422 } else {
423 return Err(Error::Generic(
424 "Failed to resolve default config directory. User home cannot be determined."
425 .into(),
426 ));
427 };
428 let config_dir = config_path
429 .parent()
430 .ok_or_else(|| Error::InvalidPath("Couldn't resolve config directory".into()))?;
431
432 if !config_dir.exists() {
434 create_dir_all(config_dir).map_err(|err| {
435 Error::IoPathError(config_dir.to_path_buf(), "creating config dir", err)
436 })?;
437 }
438
439 let content = match serde_yaml::to_string(self) {
440 Ok(content) => content,
441 Err(error) => {
442 return Err(Error::Generic(format!(
443 "Configuration file serialization failed:\n{error}"
444 )));
445 }
446 };
447 let mut file = File::create(&config_path).map_err(|err| {
448 Error::IoPathError(config_dir.to_path_buf(), "creating settings file", err)
449 })?;
450 file.write_all(content.as_bytes()).map_err(|err| {
451 Error::IoPathError(config_dir.to_path_buf(), "writing settings file", err)
452 })?;
453
454 Ok(())
455 }
456
457 pub fn load_profile(&mut self, profile: &str) -> Result<(), Error> {
459 let profile = self.profiles.remove(profile).ok_or_else(|| {
460 Error::ConfigDeserialization(format!("Couldn't find profile with name \"{profile}\""))
461 })?;
462
463 self.client = profile.client;
464 self.daemon = profile.daemon;
465 self.shared = profile.shared;
466
467 Ok(())
468 }
469}
470
471#[cfg(test)]
472mod test {
473 use super::*;
474
475 #[test]
477 fn test_load_profile() {
478 let mut settings = Settings::default();
480 assert_eq!(
481 settings.client.status_time_format,
482 default_status_time_format()
483 );
484 assert_eq!(
485 settings.daemon.callback_log_lines,
486 default_callback_log_lines()
487 );
488 assert_eq!(settings.shared.host, default_host());
489
490 let mut profile = Settings::default();
492 profile.client.status_time_format = "test".to_string();
493 profile.daemon.callback_log_lines = 100_000;
494 profile.shared.host = "quatschhost".to_string();
495 let profile = NestedSettings {
496 client: profile.client,
497 daemon: profile.daemon,
498 shared: profile.shared,
499 };
500
501 settings.profiles.insert("testprofile".to_string(), profile);
502
503 settings
505 .load_profile("testprofile")
506 .expect("We just added the profile");
507
508 assert_eq!(settings.client.status_time_format, "test");
509 assert_eq!(settings.daemon.callback_log_lines, 100_000);
510 assert_eq!(settings.shared.host, "quatschhost");
511 }
512
513 #[test]
515 fn test_error_on_missing_profile() {
516 let mut settings = Settings::default();
517
518 let result = settings.load_profile("doesn't exist");
519 let expected_error_message = "Couldn't find profile with name \"doesn't exist\"";
520 if let Err(Error::ConfigDeserialization(error_message)) = result {
521 assert_eq!(error_message, expected_error_message);
522 return;
523 }
524
525 panic!("Got unexpected result when expecting missing profile error: {result:?}");
526 }
527}