use std::ffi::OsStr; use std::fmt; use std::fs::{metadata, symlink_metadata}; use std::path::PathBuf; use std::process::Command; use dialoguer::{theme::SimpleTheme, Confirm, MultiSelect}; macro_rules! unwrap_or_false { ($expr: expr) => { match $expr { Ok(v) => v, Err(_) => return false, } }; } /// Returns the home directory. pub fn home() -> PathBuf { PathBuf::from(std::env::var("HOME").unwrap()) } /// A path from the user's home. pub fn from_home(path: &str) -> PathBuf { home().join(path) } /// Tests that a command exists. pub fn test_command(cmd: &str) -> bool { unwrap_or_false!(command("sh", &["-c", &format!("command -v {}", cmd)]).output()) .status .success() } /// Helper to create commands in one line. pub fn command(program: &str, args: I) -> Command where I: IntoIterator, S: AsRef, { let mut command = Command::new(program); command.args(args); command } /// Helper to create commands for cloning a github repository. pub fn github_clone>(user: &str, repo: &str, dest: T) -> Command { command( "git", &[ &"clone".as_ref(), &format!("https://github.com/{}/{}", user, repo).as_ref(), dest.as_ref(), ], ) } /// Helper to create commands for cloning a gitea repository. pub fn gitea_clone>(user: &str, repo: &str, dest: T) -> Command { command( "git", &[ &"clone".as_ref(), &format!("https://gitea.tforgione.fr/{}/{}", user, repo).as_ref(), dest.as_ref(), ], ) } /// The supported package manager. #[derive(Copy, Clone)] pub enum PackageManager { /// The apt manager, availble on Debian, Ubuntu and others. Apt, /// The pacman manager, available on ArchLinux, Manjaro and others. Pacman, } impl PackageManager { /// Returns the command to install a page using the right package manager. pub fn install<'a>(&self, package: &'a str) -> Command { match self { PackageManager::Apt => { command("sh", &["-c", &format!("sudo apt install -y {}", package)]) } PackageManager::Pacman => command("sudo", &["pacman", "-S", package]), } } } /// All the installable components. #[derive(Copy, Clone, PartialEq, Eq)] pub enum Component { /// Whether the user is allowed to run sudo commands. Sudo, /// Git enables to clone repository, which is necessary for many components. Git, /// The configuration uses the zsh shell. Zsh, /// The configuration files. Dotfiles, /// The zshrc config file Zshrc, /// The oh my zsh repository. OhMyZsh, } impl Component { /// List all components. pub fn all() -> Vec { vec![ Component::Git, Component::Zsh, Component::Dotfiles, Component::OhMyZsh, Component::Zshrc, ] } /// Returns the command that installs a component. pub fn install_command(self, package_manager: PackageManager) -> Command { match self { Component::Sudo => panic!("you can't install sudo"), Component::Git => package_manager.install("git"), Component::Zsh => package_manager.install("zsh"), Component::Dotfiles => { gitea_clone("tforgione", "dotfiles", from_home(".config/dotfiles")) } Component::OhMyZsh => github_clone("ohmyzsh", "ohmyzsh", from_home(".config/ohmyzsh")), Component::Zshrc => command( "ln", &[ AsRef::::as_ref("-s"), &from_home(".config/dotfiles/zshrc").as_ref(), &from_home(".zshrc").as_ref(), ], ), } } /// Installs a component. pub fn install(self) -> Result<()> { self.install_command(PackageManager::Apt).output().unwrap(); Ok(()) } /// Returns a command that tests if a component is installed. pub fn is_installed(self) -> bool { match self { Component::Sudo => true, Component::Git => test_command("git"), Component::Zsh => test_command("zsh"), Component::Dotfiles => { unwrap_or_false!(metadata(from_home(".config/dotfiles"))).is_dir() } Component::OhMyZsh => unwrap_or_false!(metadata(from_home(".config/ohmyzsh"))).is_dir(), Component::Zshrc => unwrap_or_false!(symlink_metadata(from_home(".zshrc"))) .file_type() .is_symlink(), } } /// Returns the dependencies of each component. pub fn dependencies(self) -> &'static [Component] { match self { Component::Sudo => &[], Component::Git => &[Component::Sudo], Component::Zsh => &[Component::Sudo], Component::Dotfiles => &[Component::Git], Component::Zshrc => &[Component::Zsh, Component::Dotfiles, Component::OhMyZsh], Component::OhMyZsh => &[Component::Git], } } /// Returns the str representation of the command. pub fn to_str(self) -> &'static str { match self { Component::Sudo => "sudo", Component::Git => "git", Component::Zsh => "zsh", Component::Dotfiles => "dotfiles", Component::Zshrc => "zshrc", Component::OhMyZsh => "ohmyzsh", } } } impl fmt::Display for Component { fn fmt(&self, fmt: &mut fmt::Formatter) -> fmt::Result { write!( fmt, "{} (requires {}{})", self.to_str(), self.dependencies() .into_iter() .map(|x| x.to_str()) .collect::>() .join(", "), if self.is_installed() { ", already installed" } else { "" } ) } } /// The struct that holds the information about the install. pub struct Installer { /// All the components to install, and whether the users want to install them. components: Vec<(Component, bool)>, } impl Installer { /// Creates a new installer. pub fn new() -> Installer { Installer { components: Component::all().into_iter().map(|x| (x, false)).collect(), } } /// Asks interactively to the user which components to install. pub fn interact(&mut self) -> bool { let multiselected = self .components .iter() .map(|(component, _)| format!("{}", component)) .collect::>(); let defaults = self .components .iter() .map(|(_, active)| *active) .collect::>(); let selections = MultiSelect::with_theme(&SimpleTheme) .items(&multiselected[..]) .defaults(&defaults[..]) .interact() .unwrap(); for i in selections { self.components[i].1 = true; } println!("The following components will be installed:"); for (comp, to_install) in &self.components { if *to_install { println!(" - {}", comp.to_str()); } } let message = "Is this correct?"; Confirm::with_theme(&SimpleTheme) .with_prompt(message) .interact() .unwrap() } /// Runs the installer. pub fn start(&self) -> Result<()> { for (component, to_install) in &self.components { if *to_install { self.install_component(*component)?; } } Ok(()) } /// Installs a component. pub fn install_component(&self, component: Component) -> Result<()> { if component.is_installed() || component == Component::Sudo { return Ok(()); } println!("Install {}", component.to_str()); for dep in component.dependencies() { self.install_component(*dep)?; } if !component.is_installed() { component.install()?; } Ok(()) } } /// The error type of this library. pub enum Error {} impl fmt::Display for Error { fn fmt(&self, _: &mut fmt::Formatter) -> fmt::Result { Ok(()) } } /// The result type of this library. pub type Result = std::result::Result; /// Runs the installer pub fn main() -> Result<()> { let mut installer = Installer::new(); if installer.interact() { installer.start()?; } Ok(()) }