/ src / add.rs
add.rs
  1  use std::{
  2      fs::{self, create_dir_all, symlink_metadata},
  3      io::{ErrorKind, Write as _, stdin, stdout},
  4      os::unix::fs::symlink,
  5      path::Path,
  6      process::exit,
  7  };
  8  
  9  use anyhow::{Context as _, Result, ensure};
 10  
 11  use crate::util::{config_path, paths_equal, rerun_with_root, system_path};
 12  
 13  /// Symlink a the given path to its location in the actual system
 14  pub fn add(path: &Path, force: bool, copy: bool) -> Result<()> {
 15      if copy {
 16          return add_copy(path, force);
 17      }
 18  
 19      let config_path = config_path(path)?;
 20      let system_path = system_path(path)?;
 21  
 22      // If the system path already exists
 23      if symlink_metadata(&system_path).is_ok() {
 24          // Check if it is a symlink that points to the correct location
 25          if let Ok(destination) = fs::read_link(&system_path)
 26              && destination == config_path
 27          {
 28              return Ok(());
 29          }
 30  
 31          // -> It isnt
 32          ask_for_overwrite(force, &config_path, &system_path)?;
 33      }
 34  
 35      // At this point the path either doesn't exist yet, or the user has decided to overwrite it
 36      println!(
 37          "Symlinking {} to {}",
 38          config_path.display(),
 39          system_path.display(),
 40      );
 41      create_symlink(&config_path, &system_path)
 42  }
 43  
 44  /// Symlink a the given path to its location in the actual system
 45  pub fn add_copy(path: &Path, force: bool) -> Result<()> {
 46      let config_path = config_path(path)?;
 47      let system_path = system_path(path)?;
 48  
 49      ensure!(
 50          !config_path.is_dir(),
 51          "Only files and symlinks are currently supported with --copy"
 52      );
 53  
 54      // If path exists on the system
 55      if fs::exists(path).with_context(
 56          || format!("checking if the path {} already exists", path.display()),
 57          // And is not equal to the one in the config
 58      )? && let Err(e) = paths_equal(&config_path, &system_path)
 59      {
 60          eprintln!("{e}");
 61          ask_for_overwrite(force, &config_path, &system_path)?;
 62      }
 63  
 64      // At this point the path either doesn't exist yet, or the user has decided to overwrite it
 65      println!(
 66          "Copying {} to {}",
 67          config_path.display(),
 68          system_path.display(),
 69      );
 70  
 71      fs::copy(config_path, system_path).map(|_| {}).context(
 72          // Ignore number of bytes copied
 73          "copying config path to system path",
 74      )
 75  }
 76  
 77  /// Asks for overwrite and removes the path from the system if requested, exits if not
 78  fn ask_for_overwrite(force: bool, config_path: &Path, system_path: &Path) -> Result<()> {
 79      if force
 80          || match paths_equal(config_path, system_path) {
 81              Ok(()) => true,
 82              Err(e) => {
 83                  bool_question(&format!("{e}, overwrite system path?")).unwrap_or_default()
 84                      && bool_question("Are you sure?").unwrap_or_default()
 85              }
 86          }
 87      {
 88          if system_path.is_dir() {
 89              fs::remove_dir_all(system_path)
 90          } else {
 91              fs::remove_file(system_path)
 92          }
 93          .with_context(|| format!("removing path {}", system_path.display()))
 94      } else {
 95          exit(1)
 96      }
 97  }
 98  
 99  /// Creates a symlink from `config_path` to `system_path`
100  #[expect(clippy::wildcard_enum_match_arm)]
101  fn create_symlink(config_path: &Path, system_path: &Path) -> Result<()> {
102      // Try creating the symlink
103      if let Err(e) = symlink(config_path, system_path) {
104          match e.kind() {
105              ErrorKind::PermissionDenied => {
106                  rerun_with_root("Creating symlink");
107              }
108              ErrorKind::NotFound => {
109                  create_dir_all(
110                      system_path
111                          .parent()
112                          .context("Failed to get parent of system path")?,
113                  )
114                  .context("creating parent directories")?;
115  
116                  create_symlink(config_path, system_path)?;
117              }
118              _ => {
119                  return Err(e.into());
120              }
121          }
122      }
123      Ok(())
124  }
125  
126  /// Asks the user the given question and returns the users answer.
127  pub fn bool_question(question: &str) -> Result<bool> {
128      print!("{question} ");
129  
130      stdout().flush()?;
131  
132      let mut buffer = String::with_capacity(3); // The longest accepted answer is 3 characters long
133  
134      loop {
135          buffer.clear();
136  
137          stdin().read_line(&mut buffer)?;
138  
139          match buffer.trim() {
140              "y" | "Y" | "yes" | "Yes" => return Ok(true),
141              "n" | "N" | "no" | "No" => return Ok(false),
142              _other => {}
143          }
144      }
145  }