/ src / config.rs
config.rs
  1  use std::{fs, io::stdin, path::PathBuf, sync::LazyLock};
  2  
  3  use anyhow::{Context as _, Result, bail, ensure};
  4  
  5  use crate::{add::bool_question, util::home};
  6  
  7  #[expect(clippy::unwrap_used)]
  8  pub static CONFIG: LazyLock<Config> = LazyLock::new(|| Config::load().unwrap());
  9  
 10  #[derive(Default)]
 11  pub struct Config {
 12      /// The default subdir of files/
 13      pub default_subdir: String,
 14      /// The path to the files/ directory
 15      pub files_path: String,
 16      /// The paths that should be searched by `list()`
 17      pub list_paths: Vec<String>,
 18      /// The paths that shouldn't be searched by `list()`
 19      pub ignore_paths: Vec<PathBuf>,
 20      /// Whether to run 'list' with root privileges
 21      pub root: bool,
 22  }
 23  impl Config {
 24      pub fn setup() -> Result<()> {
 25          let stdin = stdin();
 26  
 27          let mut file_string = String::new();
 28  
 29          let mut prompt_field = |field: &str, prompt: &str| -> Result<()> {
 30              file_string.push_str(field);
 31              file_string.push_str(" = ");
 32              println!("{prompt}:");
 33              stdin.read_line(&mut file_string)?;
 34              Ok(())
 35          };
 36  
 37          prompt_field(
 38              "files_path",
 39              "Please enter the path of the files that should be managed by dots",
 40          )?;
 41  
 42          prompt_field("default_subdir", "Please enter the default subdir")?;
 43  
 44          prompt_field(
 45              "list_paths",
 46              "Please enter the paths that should be searched by `dots list`, separated by commas",
 47          )?;
 48  
 49          prompt_field(
 50              "ignore_paths",
 51              "Please enter the paths that should be ignored by `dots list`, separated by commas",
 52          )?;
 53  
 54          if bool_question("Should `dots list` be run with root privileges?")? {
 55              file_string.push_str("root");
 56          }
 57  
 58          let path = format!("{}/.config/dots", home()?);
 59  
 60          fs::write(path, file_string)?;
 61  
 62          Ok(())
 63      }
 64      fn load() -> Result<Self> {
 65          let path = format!("{}/.config/dots", home()?);
 66  
 67          let string = fs::read_to_string(&path)
 68              .with_context(|| format!("\nFailed to read config at {path}. Run `dots config` to create it interactively or do so manually."))?;
 69  
 70          let mut config = Self::default();
 71  
 72          for line in string.lines() {
 73              match line.split_once('=') {
 74                  Some((key, value)) => match key.trim() {
 75                      "default_subdir" => value.trim().clone_into(&mut config.default_subdir),
 76                      "files_path" => value.trim().clone_into(&mut config.files_path),
 77                      "list_paths" => config
 78                          .list_paths
 79                          .extend(value.split(',').map(|value| value.trim().to_owned())),
 80                      "ignore_paths" => config
 81                          .ignore_paths
 82                          .extend(value.split(',').map(|value| value.trim().into())),
 83                      "root" => config.root = true,
 84                      other => bail!("Unknown config entry: {other}"),
 85                  },
 86                  None => match line.trim() {
 87                      "root" => config.root = true,
 88                      other => bail!("Unknown config key: {other}"),
 89                  },
 90              }
 91          }
 92  
 93          ensure!(
 94              !config.default_subdir.is_empty(),
 95              "default_subdir is empty or not in the config. Maybe try adding something like `default_subdir = common` to your dots config file"
 96          );
 97  
 98          Ok(config)
 99      }
100  }