315 lines
		
	
	
		
			8.6 KiB
		
	
	
	
		
			Rust
		
	
	
	
	
	
			
		
		
	
	
			315 lines
		
	
	
		
			8.6 KiB
		
	
	
	
		
			Rust
		
	
	
	
	
	
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<I, S>(program: &str, args: I) -> Command
 | 
						|
where
 | 
						|
    I: IntoIterator<Item = S>,
 | 
						|
    S: AsRef<OsStr>,
 | 
						|
{
 | 
						|
    let mut command = Command::new(program);
 | 
						|
    command.args(args);
 | 
						|
    command
 | 
						|
}
 | 
						|
 | 
						|
/// Helper to create commands for cloning a github repository.
 | 
						|
pub fn github_clone<T: AsRef<OsStr>>(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<T: AsRef<OsStr>>(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<Component> {
 | 
						|
        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::<OsStr>::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::<Vec<_>>()
 | 
						|
                .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::<Vec<_>>();
 | 
						|
 | 
						|
        let defaults = self
 | 
						|
            .components
 | 
						|
            .iter()
 | 
						|
            .map(|(_, active)| *active)
 | 
						|
            .collect::<Vec<_>>();
 | 
						|
 | 
						|
        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<T> = std::result::Result<T, Error>;
 | 
						|
 | 
						|
/// Runs the installer
 | 
						|
pub fn main() -> Result<()> {
 | 
						|
    let mut installer = Installer::new();
 | 
						|
    if installer.interact() {
 | 
						|
        installer.start()?;
 | 
						|
    }
 | 
						|
    Ok(())
 | 
						|
}
 |