main.rs
1 #![feature(let_chains)] 2 3 mod add; 4 mod config; 5 mod debug; 6 mod import; 7 mod list; 8 mod remove; 9 mod util; 10 11 use anyhow::Result; 12 use clap::{Parser, Subcommand}; 13 use std::{ 14 io::{self, ErrorKind}, 15 path::PathBuf, 16 sync::OnceLock, 17 }; 18 use util::rerun_with_root; 19 20 #[derive(Parser, Debug)] 21 #[command(name = "dots")] 22 struct Cli { 23 #[arg(short, long)] 24 /// Only output the found items 25 silent: bool, 26 #[command(subcommand)] 27 command: Commands, 28 } 29 30 #[derive(Subcommand, Debug)] 31 enum Commands { 32 /// Add the given path to the system 33 #[command(arg_required_else_help = true)] 34 Add { 35 /// Format: (sub-dir of ~/.config/rebos/files)/(path to symlink). 36 /// If the path is absolute, it is automatically prepended with <`DEFAULT_SUBDIR`>. 37 /// "{hostname}" can be used as a placeholder for the actual hostname of the system. 38 /// "{home}" can be used as a placeholder for the home dir of the user. 39 path: PathBuf, 40 41 #[arg(long)] 42 /// Copy instead of symlink the path 43 copy: bool, 44 45 #[arg(short, long)] 46 /// Overwrite the destination without asking 47 force: bool, 48 }, 49 /// Remove the given path from the system (does not remove the files the path points to, only the symlink) 50 #[command(arg_required_else_help = true)] 51 Remove { 52 /// Format: (sub-dir of ~/.config/rebos/files}/{path to symlink) 53 /// If the path is absolute, it is assumed to already be the path to remove. 54 /// "{hostname}" can be used as a placeholder for the actual hostname of the system. 55 /// "{home}" can be used as a placeholder for the home dir of the user. 56 path: PathBuf, 57 }, 58 /// Import the given path from the system 59 #[command(arg_required_else_help = true)] 60 Import { 61 /// Format: (sub-dir of ~/.config/rebos/files)/(path to symlink). 62 /// If the path is absolute, it is automatically prepended with <`DEFAULT_SUBDIR`>. 63 /// "{hostname}" can be used as a placeholder for the actual hostname of the system. 64 /// "{home}" can be used as a placeholder for the home dir of the user. 65 path: PathBuf, 66 67 #[arg(long)] 68 /// Copy instead of symlink the path 69 copy: bool, 70 }, 71 /// Outputs a list of all symlinks on the system that are probably made by dots 72 List { 73 #[arg(short, long)] 74 /// Assume that the current user is root 75 rooted: bool, 76 77 #[arg(long, trailing_var_arg = true, num_args(1..))] 78 copy: Option<Vec<String>>, 79 }, 80 /// Debugging commands 81 #[command(subcommand)] 82 Debug(DebugCommands), 83 /// Interactively creates the config file 84 Config, 85 } 86 87 #[derive(Subcommand, Debug)] 88 enum DebugCommands { 89 /// Print the config path of the given path 90 ConfigPath { 91 /// Format: (sub-dir of ~/.config/rebos/files)/(path to symlink). 92 /// If the path is absolute, it is automatically prepended with <`DEFAULT_SUBDIR`>. 93 /// "{hostname}" can be used as a placeholder for the actual hostname of the system. 94 /// "{home}" can be used as a placeholder for the home dir of the user. 95 path: PathBuf, 96 }, 97 /// Print the system path of the given path 98 SystemPath { 99 /// Format: (sub-dir of ~/.config/rebos/files)/(path to symlink). 100 /// If the path is absolute, it is automatically prepended with <`DEFAULT_SUBDIR`>. 101 /// "{hostname}" can be used as a placeholder for the actual hostname of the system. 102 /// "{home}" can be used as a placeholder for the home dir of the user. 103 path: PathBuf, 104 }, 105 } 106 107 static SILENT: OnceLock<bool> = OnceLock::new(); 108 109 #[expect(clippy::expect_used)] 110 fn main() -> Result<()> { 111 let args = Cli::parse(); 112 113 SILENT 114 .set(args.silent) 115 .expect("SILENT shouldnt be already initialized"); 116 117 match args.command { 118 Commands::Add { path, force, copy } => add::add(&path, force, copy), 119 Commands::Remove { path } => remove::remove(&path), 120 Commands::Import { path, copy } => import::import(&path, copy), 121 Commands::List { rooted, copy } => list::list(rooted, copy), 122 Commands::Debug(debug_command) => debug::debug(debug_command), 123 Commands::Config => config::Config::setup(), 124 } 125 .inspect_err(|error| { 126 if let Some(io_error) = error.root_cause().downcast_ref::<io::Error>() { 127 if io_error.kind() == ErrorKind::PermissionDenied { 128 rerun_with_root(&format!("{error}")); // just formatting the error should be fine for handling context 129 } 130 } 131 }) 132 }