|
|
|
@ -0,0 +1,314 @@
|
|
|
|
|
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(())
|
|
|
|
|
}
|