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) -> Result<PathBuf, Error> {
305 if let Some(path) = &self.unix_socket_path {
306 Ok(expand_home(path))
307 } else {
308 let username = whoami::username().map_err(|_| {
309 Error::NoUsername(
310 "Specify an explicit unix socket path in your config to fix this error."
311 .to_string(),
312 )
313 })?;
314 Ok(self
315 .runtime_directory()
316 .join(format!("pueue_{username}.socket")))
317 }
318 }
319
320 pub fn alias_file(&self) -> PathBuf {
323 if let Some(path) = &self.alias_file {
324 expand_home(path)
325 } else if let Some(config_dir) = default_configuration_directory() {
326 config_dir.join("pueue_aliases.yml")
327 } else {
328 PathBuf::from("pueue_aliases.yml")
329 }
330 }
331
332 pub fn pid_path(&self) -> PathBuf {
335 if let Some(path) = &self.pid_path {
336 expand_home(path)
337 } else {
338 self.runtime_directory().join("pueue.pid")
339 }
340 }
341
342 pub fn daemon_cert(&self) -> PathBuf {
343 if let Some(path) = &self.daemon_cert {
344 expand_home(path)
345 } else {
346 self.pueue_directory().join("certs").join("daemon.cert")
347 }
348 }
349
350 pub fn daemon_key(&self) -> PathBuf {
351 if let Some(path) = &self.daemon_key {
352 expand_home(path)
353 } else {
354 self.pueue_directory().join("certs").join("daemon.key")
355 }
356 }
357
358 pub fn shared_secret_path(&self) -> PathBuf {
359 if let Some(path) = &self.shared_secret_path {
360 expand_home(path)
361 } else {
362 self.pueue_directory().join("shared_secret")
363 }
364 }
365}
366
367impl Settings {
368 pub fn read(from_file: &Option<PathBuf>) -> Result<(Settings, bool), Error> {
374 let from_file = from_file
376 .clone()
377 .or_else(|| std::env::var(PUEUE_CONFIG_PATH_ENV).map(PathBuf::from).ok());
378
379 if let Some(path) = &from_file {
381 let file = File::open(path)
383 .map_err(|err| Error::IoPathError(path.clone(), "opening config file", err))?;
384 let reader = BufReader::new(file);
385
386 let settings = serde_yaml::from_reader(reader)
387 .map_err(|err| Error::ConfigDeserialization(err.to_string()))?;
388 return Ok((settings, true));
389 };
390
391 info!("Parsing config files");
392
393 let config_dirs = configuration_directories();
394 for directory in config_dirs.into_iter() {
395 let path = directory.join("pueue.yml");
396 info!("Checking path: {path:?}");
397
398 if path.exists() && path.is_file() {
400 info!("Found config file at: {path:?}");
401
402 let file = File::open(&path)
404 .map_err(|err| Error::IoPathError(path, "opening config file.", err))?;
405 let reader = BufReader::new(file);
406
407 let settings = serde_yaml::from_reader(reader)
408 .map_err(|err| Error::ConfigDeserialization(err.to_string()))?;
409 return Ok((settings, true));
410 }
411 }
412
413 info!("No config file found. Use default config.");
414 Ok((Settings::default(), false))
416 }
417
418 pub fn save(&self, path: &Option<PathBuf>) -> Result<(), Error> {
422 let config_path = if let Some(path) = path {
423 path.clone()
424 } else if let Ok(path) = std::env::var(PUEUE_CONFIG_PATH_ENV) {
425 PathBuf::from(path)
426 } else if let Some(path) = dirs::config_dir() {
427 let path = path.join("pueue");
428 path.join("pueue.yml")
429 } else {
430 return Err(Error::Generic(
431 "Failed to resolve default config directory. User home cannot be determined."
432 .into(),
433 ));
434 };
435 let config_dir = config_path
436 .parent()
437 .ok_or_else(|| Error::InvalidPath("Couldn't resolve config directory".into()))?;
438
439 if !config_dir.exists() {
441 create_dir_all(config_dir).map_err(|err| {
442 Error::IoPathError(config_dir.to_path_buf(), "creating config dir", err)
443 })?;
444 }
445
446 let content = match serde_yaml::to_string(self) {
447 Ok(content) => content,
448 Err(error) => {
449 return Err(Error::Generic(format!(
450 "Configuration file serialization failed:\n{error}"
451 )));
452 }
453 };
454 let mut file = File::create(&config_path).map_err(|err| {
455 Error::IoPathError(config_dir.to_path_buf(), "creating settings file", err)
456 })?;
457 file.write_all(content.as_bytes()).map_err(|err| {
458 Error::IoPathError(config_dir.to_path_buf(), "writing settings file", err)
459 })?;
460
461 Ok(())
462 }
463
464 pub fn load_profile(&mut self, profile: &str) -> Result<(), Error> {
466 let profile = self.profiles.remove(profile).ok_or_else(|| {
467 Error::ConfigDeserialization(format!("Couldn't find profile with name \"{profile}\""))
468 })?;
469
470 self.client = profile.client;
471 self.daemon = profile.daemon;
472 self.shared = profile.shared;
473
474 Ok(())
475 }
476}
477
478#[cfg(test)]
479mod test {
480 use super::*;
481
482 #[test]
484 fn test_load_profile() {
485 let mut settings = Settings::default();
487 assert_eq!(
488 settings.client.status_time_format,
489 default_status_time_format()
490 );
491 assert_eq!(
492 settings.daemon.callback_log_lines,
493 default_callback_log_lines()
494 );
495 assert_eq!(settings.shared.host, default_host());
496
497 let mut profile = Settings::default();
499 profile.client.status_time_format = "test".to_string();
500 profile.daemon.callback_log_lines = 100_000;
501 profile.shared.host = "quatschhost".to_string();
502 let profile = NestedSettings {
503 client: profile.client,
504 daemon: profile.daemon,
505 shared: profile.shared,
506 };
507
508 settings.profiles.insert("testprofile".to_string(), profile);
509
510 settings
512 .load_profile("testprofile")
513 .expect("We just added the profile");
514
515 assert_eq!(settings.client.status_time_format, "test");
516 assert_eq!(settings.daemon.callback_log_lines, 100_000);
517 assert_eq!(settings.shared.host, "quatschhost");
518 }
519
520 #[test]
522 fn test_error_on_missing_profile() {
523 let mut settings = Settings::default();
524
525 let result = settings.load_profile("doesn't exist");
526 let expected_error_message = "Couldn't find profile with name \"doesn't exist\"";
527 if let Err(Error::ConfigDeserialization(error_message)) = result {
528 assert_eq!(error_message, expected_error_message);
529 return;
530 }
531
532 panic!("Got unexpected result when expecting missing profile error: {result:?}");
533 }
534}