aboutsummaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
-rw-r--r--.gitignore3
-rw-r--r--Cargo.lock399
-rw-r--r--Cargo.toml12
-rw-r--r--PKGBUILD15
-rw-r--r--README.md15
-rwxr-xr-xinstall.sh4
-rw-r--r--src/cli.rs8
-rw-r--r--src/config.rs93
-rw-r--r--src/install.rs49
-rw-r--r--src/main.rs45
-rw-r--r--src/util.rs34
11 files changed, 677 insertions, 0 deletions
diff --git a/.gitignore b/.gitignore
new file mode 100644
index 0000000..34fb28a
--- /dev/null
+++ b/.gitignore
@@ -0,0 +1,3 @@
+/target
+*.pkg.tar.zst
+/pkg
diff --git a/Cargo.lock b/Cargo.lock
new file mode 100644
index 0000000..99a163a
--- /dev/null
+++ b/Cargo.lock
@@ -0,0 +1,399 @@
+# This file is automatically @generated by Cargo.
+# It is not intended for manual editing.
+version = 3
+
+[[package]]
+name = "anstream"
+version = "0.3.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "0ca84f3628370c59db74ee214b3263d58f9aadd9b4fe7e711fd87dc452b7f163"
+dependencies = [
+ "anstyle",
+ "anstyle-parse",
+ "anstyle-query",
+ "anstyle-wincon",
+ "colorchoice",
+ "is-terminal",
+ "utf8parse",
+]
+
+[[package]]
+name = "anstyle"
+version = "1.0.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "3a30da5c5f2d5e72842e00bcb57657162cdabef0931f40e2deb9b4140440cecd"
+
+[[package]]
+name = "anstyle-parse"
+version = "0.2.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "938874ff5980b03a87c5524b3ae5b59cf99b1d6bc836848df7bc5ada9643c333"
+dependencies = [
+ "utf8parse",
+]
+
+[[package]]
+name = "anstyle-query"
+version = "1.0.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "5ca11d4be1bab0c8bc8734a9aa7bf4ee8316d462a08c6ac5052f888fef5b494b"
+dependencies = [
+ "windows-sys",
+]
+
+[[package]]
+name = "anstyle-wincon"
+version = "1.0.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "180abfa45703aebe0093f79badacc01b8fd4ea2e35118747e5811127f926e188"
+dependencies = [
+ "anstyle",
+ "windows-sys",
+]
+
+[[package]]
+name = "bitflags"
+version = "2.3.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "630be753d4e58660abd17930c71b647fe46c27ea6b63cc59e1e3851406972e42"
+
+[[package]]
+name = "cc"
+version = "1.0.79"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "50d30906286121d95be3d479533b458f87493b30a4b5f79a607db8f5d11aa91f"
+
+[[package]]
+name = "clap"
+version = "4.3.17"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "5b0827b011f6f8ab38590295339817b0d26f344aa4932c3ced71b45b0c54b4a9"
+dependencies = [
+ "clap_builder",
+ "clap_derive",
+ "once_cell",
+]
+
+[[package]]
+name = "clap_builder"
+version = "4.3.17"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "9441b403be87be858db6a23edb493e7f694761acdc3343d5a0fcaafd304cbc9e"
+dependencies = [
+ "anstream",
+ "anstyle",
+ "clap_lex",
+ "strsim",
+]
+
+[[package]]
+name = "clap_derive"
+version = "4.3.12"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "54a9bb5758fc5dfe728d1019941681eccaf0cf8a4189b692a0ee2f2ecf90a050"
+dependencies = [
+ "heck",
+ "proc-macro2",
+ "quote",
+ "syn",
+]
+
+[[package]]
+name = "clap_lex"
+version = "0.5.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "2da6da31387c7e4ef160ffab6d5e7f00c42626fe39aea70a7b0f1773f7dd6c1b"
+
+[[package]]
+name = "colorchoice"
+version = "1.0.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "acbf1af155f9b9ef647e42cdc158db4b64a1b61f743629225fde6f3e0be2a7c7"
+
+[[package]]
+name = "colored"
+version = "2.0.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "2674ec482fbc38012cf31e6c42ba0177b431a0cb6f15fe40efa5aab1bda516f6"
+dependencies = [
+ "is-terminal",
+ "lazy_static",
+ "windows-sys",
+]
+
+[[package]]
+name = "dotfiles-installer"
+version = "0.1.0"
+dependencies = [
+ "clap",
+ "colored",
+ "serde",
+ "serde_yaml",
+]
+
+[[package]]
+name = "equivalent"
+version = "1.0.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "5443807d6dff69373d433ab9ef5378ad8df50ca6298caf15de6e52e24aaf54d5"
+
+[[package]]
+name = "errno"
+version = "0.3.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "4bcfec3a70f97c962c307b2d2c56e358cf1d00b558d74262b5f929ee8cc7e73a"
+dependencies = [
+ "errno-dragonfly",
+ "libc",
+ "windows-sys",
+]
+
+[[package]]
+name = "errno-dragonfly"
+version = "0.1.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "aa68f1b12764fab894d2755d2518754e71b4fd80ecfb822714a1206c2aab39bf"
+dependencies = [
+ "cc",
+ "libc",
+]
+
+[[package]]
+name = "hashbrown"
+version = "0.14.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "2c6201b9ff9fd90a5a3bac2e56a830d0caa509576f0e503818ee82c181b3437a"
+
+[[package]]
+name = "heck"
+version = "0.4.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "95505c38b4572b2d910cecb0281560f54b440a19336cbbcb27bf6ce6adc6f5a8"
+
+[[package]]
+name = "hermit-abi"
+version = "0.3.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "443144c8cdadd93ebf52ddb4056d257f5b52c04d3c804e657d19eb73fc33668b"
+
+[[package]]
+name = "indexmap"
+version = "2.0.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "d5477fe2230a79769d8dc68e0eabf5437907c0457a5614a9e8dddb67f65eb65d"
+dependencies = [
+ "equivalent",
+ "hashbrown",
+]
+
+[[package]]
+name = "is-terminal"
+version = "0.4.9"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "cb0889898416213fab133e1d33a0e5858a48177452750691bde3666d0fdbaf8b"
+dependencies = [
+ "hermit-abi",
+ "rustix",
+ "windows-sys",
+]
+
+[[package]]
+name = "itoa"
+version = "1.0.9"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "af150ab688ff2122fcef229be89cb50dd66af9e01a4ff320cc137eecc9bacc38"
+
+[[package]]
+name = "lazy_static"
+version = "1.4.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "e2abad23fbc42b3700f2f279844dc832adb2b2eb069b2df918f455c4e18cc646"
+
+[[package]]
+name = "libc"
+version = "0.2.147"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "b4668fb0ea861c1df094127ac5f1da3409a82116a4ba74fca2e58ef927159bb3"
+
+[[package]]
+name = "linux-raw-sys"
+version = "0.4.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "09fc20d2ca12cb9f044c93e3bd6d32d523e6e2ec3db4f7b2939cd99026ecd3f0"
+
+[[package]]
+name = "once_cell"
+version = "1.18.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "dd8b5dd2ae5ed71462c540258bedcb51965123ad7e7ccf4b9a8cafaa4a63576d"
+
+[[package]]
+name = "proc-macro2"
+version = "1.0.66"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "18fb31db3f9bddb2ea821cde30a9f70117e3f119938b5ee630b7403aa6e2ead9"
+dependencies = [
+ "unicode-ident",
+]
+
+[[package]]
+name = "quote"
+version = "1.0.31"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "5fe8a65d69dd0808184ebb5f836ab526bb259db23c657efa38711b1072ee47f0"
+dependencies = [
+ "proc-macro2",
+]
+
+[[package]]
+name = "rustix"
+version = "0.38.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "0a962918ea88d644592894bc6dc55acc6c0956488adcebbfb6e273506b7fd6e5"
+dependencies = [
+ "bitflags",
+ "errno",
+ "libc",
+ "linux-raw-sys",
+ "windows-sys",
+]
+
+[[package]]
+name = "ryu"
+version = "1.0.15"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "1ad4cc8da4ef723ed60bced201181d83791ad433213d8c24efffda1eec85d741"
+
+[[package]]
+name = "serde"
+version = "1.0.171"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "30e27d1e4fd7659406c492fd6cfaf2066ba8773de45ca75e855590f856dc34a9"
+dependencies = [
+ "serde_derive",
+]
+
+[[package]]
+name = "serde_derive"
+version = "1.0.171"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "389894603bd18c46fa56231694f8d827779c0951a667087194cf9de94ed24682"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "syn",
+]
+
+[[package]]
+name = "serde_yaml"
+version = "0.9.24"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "bd5f51e3fdb5b9cdd1577e1cb7a733474191b1aca6a72c2e50913241632c1180"
+dependencies = [
+ "indexmap",
+ "itoa",
+ "ryu",
+ "serde",
+ "unsafe-libyaml",
+]
+
+[[package]]
+name = "strsim"
+version = "0.10.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "73473c0e59e6d5812c5dfe2a064a6444949f089e20eec9a2e5506596494e4623"
+
+[[package]]
+name = "syn"
+version = "2.0.26"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "45c3457aacde3c65315de5031ec191ce46604304d2446e803d71ade03308d970"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "unicode-ident",
+]
+
+[[package]]
+name = "unicode-ident"
+version = "1.0.11"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "301abaae475aa91687eb82514b328ab47a211a533026cb25fc3e519b86adfc3c"
+
+[[package]]
+name = "unsafe-libyaml"
+version = "0.2.9"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "f28467d3e1d3c6586d8f25fa243f544f5800fec42d97032474e17222c2b75cfa"
+
+[[package]]
+name = "utf8parse"
+version = "0.2.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "711b9620af191e0cdc7468a8d14e709c3dcdb115b36f838e601583af800a370a"
+
+[[package]]
+name = "windows-sys"
+version = "0.48.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "677d2418bec65e3338edb076e806bc1ec15693c5d0104683f2efe857f61056a9"
+dependencies = [
+ "windows-targets",
+]
+
+[[package]]
+name = "windows-targets"
+version = "0.48.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "05d4b17490f70499f20b9e791dcf6a299785ce8af4d709018206dc5b4953e95f"
+dependencies = [
+ "windows_aarch64_gnullvm",
+ "windows_aarch64_msvc",
+ "windows_i686_gnu",
+ "windows_i686_msvc",
+ "windows_x86_64_gnu",
+ "windows_x86_64_gnullvm",
+ "windows_x86_64_msvc",
+]
+
+[[package]]
+name = "windows_aarch64_gnullvm"
+version = "0.48.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "91ae572e1b79dba883e0d315474df7305d12f569b400fcf90581b06062f7e1bc"
+
+[[package]]
+name = "windows_aarch64_msvc"
+version = "0.48.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "b2ef27e0d7bdfcfc7b868b317c1d32c641a6fe4629c171b8928c7b08d98d7cf3"
+
+[[package]]
+name = "windows_i686_gnu"
+version = "0.48.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "622a1962a7db830d6fd0a69683c80a18fda201879f0f447f065a3b7467daa241"
+
+[[package]]
+name = "windows_i686_msvc"
+version = "0.48.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "4542c6e364ce21bf45d69fdd2a8e455fa38d316158cfd43b3ac1c5b1b19f8e00"
+
+[[package]]
+name = "windows_x86_64_gnu"
+version = "0.48.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "ca2b8a661f7628cbd23440e50b05d705db3686f894fc9580820623656af974b1"
+
+[[package]]
+name = "windows_x86_64_gnullvm"
+version = "0.48.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "7896dbc1f41e08872e9d5e8f8baa8fdd2677f29468c4e156210174edc7f7b953"
+
+[[package]]
+name = "windows_x86_64_msvc"
+version = "0.48.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "1a515f5799fe4961cb532f983ce2b23082366b898e52ffbce459c86f67c8378a"
diff --git a/Cargo.toml b/Cargo.toml
new file mode 100644
index 0000000..a5478e4
--- /dev/null
+++ b/Cargo.toml
@@ -0,0 +1,12 @@
+[package]
+name = "dotfiles-installer"
+version = "0.1.0"
+edition = "2021"
+
+# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
+
+[dependencies]
+serde = { version = "1.0", features = ["derive"] }
+serde_yaml = "0.9.24"
+colored = "2"
+clap = { version = "4.3.17", features = ["derive"] }
diff --git a/PKGBUILD b/PKGBUILD
new file mode 100644
index 0000000..addd2f9
--- /dev/null
+++ b/PKGBUILD
@@ -0,0 +1,15 @@
+pkgname=dotfiles-installer
+pkgver=0.1.0
+pkgrel=1
+makedepends=('rust' 'cargo')
+arch=('any')
+
+build() {
+ cargo build --frozen --release
+}
+
+package() {
+ cd $srcdir/..
+ install -Dm755 "target/release/$pkgname" \
+ -t "$pkgdir/usr/bin"
+}
diff --git a/README.md b/README.md
new file mode 100644
index 0000000..798aa26
--- /dev/null
+++ b/README.md
@@ -0,0 +1,15 @@
+# dotfiles-installer
+
+Small program to automatically install my [dotfiles](https://github.com/LMBishop/dotfiles). Unless you happen to be me, I wouldn't recommend you use this.
+
+## Install
+
+Arch Linux only:
+```
+$ ./install.sh
+```
+
+## Usage
+```
+$ dotfiles-installer --file <FILE>
+``` \ No newline at end of file
diff --git a/install.sh b/install.sh
new file mode 100755
index 0000000..836b464
--- /dev/null
+++ b/install.sh
@@ -0,0 +1,4 @@
+#!/bin/sh
+
+makepkg
+sudo pacman -U dotfiles-installer*.pkg.tar* \ No newline at end of file
diff --git a/src/cli.rs b/src/cli.rs
new file mode 100644
index 0000000..dc53357
--- /dev/null
+++ b/src/cli.rs
@@ -0,0 +1,8 @@
+use clap::Parser;
+
+#[derive(Parser, Debug)]
+#[command(author, version, about, long_about = None)]
+pub struct Args {
+ #[arg(short, long)]
+ pub file: String,
+} \ No newline at end of file
diff --git a/src/config.rs b/src/config.rs
new file mode 100644
index 0000000..2a0e38b
--- /dev/null
+++ b/src/config.rs
@@ -0,0 +1,93 @@
+use std::{fs::File, path::PathBuf};
+use std::fmt;
+use std::error::Error;
+
+use serde::{Serialize, Deserialize};
+
+#[derive(Debug, Serialize, Deserialize)]
+pub struct CopyPath {
+ pub from: String,
+ pub to: String,
+ pub recursive: bool,
+}
+
+#[derive(Debug, Serialize, Deserialize)]
+pub enum Step {
+ Link(CopyPath),
+ Copy(CopyPath),
+ Shell(String),
+}
+
+#[derive(Debug, Serialize, Deserialize)]
+pub struct Stage {
+ pub name: Option<String>,
+ pub steps: Option<Vec<Step>>,
+ pub from_file: Option<String>,
+
+ #[serde(skip_deserializing)]
+ pub base_path: PathBuf,
+}
+
+#[derive(Debug, Serialize, Deserialize)]
+pub struct Config {
+ pub stages: Vec<Stage>,
+
+ #[serde(skip_deserializing)]
+ pub base_path: PathBuf,
+}
+
+#[derive(Debug)]
+struct StageFileError {
+ stage_file: String,
+ cause: Box<dyn std::error::Error>,
+}
+
+impl Error for StageFileError {}
+
+impl fmt::Display for StageFileError {
+ fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
+ let stage_file = &self.stage_file;
+ let cause = &self.cause.to_string();
+
+ write!(f, "Failed to load stage from file '{stage_file}': {cause}")
+ }
+}
+
+impl Config {
+ fn resolve_stage_file(base_path: PathBuf, file_name: &str) -> Result<Stage, Box<dyn std::error::Error>> {
+ let mut file_path = base_path.clone();
+ file_path.push(file_name);
+ let file = File::open(&file_path)?;
+ let stage: Stage = serde_yaml::from_reader(file)?;
+ let parent_path = file_path.parent().unwrap();
+
+ Ok(Stage { base_path: parent_path.to_path_buf(), ..stage })
+ }
+
+ pub fn from_file(file_name: &str) -> Result<Self, Box<dyn std::error::Error>> {
+ let base_path = std::env::current_dir()?;
+ let mut file_path = base_path.clone();
+ file_path.push(file_name);
+
+ let file = File::open(&file_path)?;
+ let loaded_config: Config = serde_yaml::from_reader(file)?;
+
+ let mut stages: Vec<Stage> = Vec::new();
+
+ for stage in loaded_config.stages {
+ if let Some(file_name) = stage.from_file {
+ match Self::resolve_stage_file(base_path.clone(), &file_name) {
+ Ok(loaded_stage) => stages.push(loaded_stage),
+ Err(err) => return Err(Box::new(StageFileError {
+ stage_file: file_name,
+ cause: err
+ }))
+ }
+ } else {
+ stages.push(Stage { base_path: base_path.clone(), ..stage });
+ }
+ }
+
+ Ok(Config { stages, base_path: base_path })
+ }
+} \ No newline at end of file
diff --git a/src/install.rs b/src/install.rs
new file mode 100644
index 0000000..57da431
--- /dev/null
+++ b/src/install.rs
@@ -0,0 +1,49 @@
+use crate::config::{Step, CopyPath};
+use std::{fs, path::PathBuf};
+use crate::util::expand_home;
+
+fn resolve_paths_and_mkdir(paths: &CopyPath, base_path: &PathBuf) -> Result<(PathBuf, PathBuf), Box<dyn std::error::Error>> {
+ let expanded_home = &expand_home(&paths.to);
+ let destination = PathBuf::from(expanded_home);
+ let mut source = base_path.clone();
+ source.push(&paths.from);
+
+ let dest_parent = destination.parent().unwrap();
+ fs::create_dir_all(dest_parent)?;
+ Ok((source, destination))
+}
+
+fn ln(paths: &CopyPath, base_path: &PathBuf) -> Result<bool, Box<dyn std::error::Error>> {
+ let (source, destination) = resolve_paths_and_mkdir(paths, base_path)?;
+ let source = source.as_path();
+ let destination = destination.as_path();
+
+ let _ = fs::remove_file(destination);
+ fs::hard_link(source, destination)?;
+ Ok(true)
+}
+
+fn cp(paths: &CopyPath, base_path: &PathBuf) -> Result<bool, Box<dyn std::error::Error>> {
+ let (source, destination) = resolve_paths_and_mkdir(paths, base_path)?;
+ let source = source.as_path();
+ let destination = destination.as_path();
+
+ if fs::metadata(destination).is_ok() {
+ return Ok(false);
+ }
+ fs::copy(source, destination)?;
+ Ok(true)
+}
+
+fn run_shell(command: &String) -> Result<bool, Box<dyn std::error::Error>> {
+ let exit = std::process::Command::new("/bin/sh").arg("-c").arg(command).output()?;
+ return Ok(exit.status.success());
+}
+
+pub fn run_step(step: &Step, base_path: &PathBuf) -> Result<bool, Box<dyn std::error::Error>> {
+ match step {
+ Step::Link(path) => { ln(path, base_path) },
+ Step::Copy(path) => { cp(path, base_path) },
+ Step::Shell(command) => run_shell(command),
+ }
+} \ No newline at end of file
diff --git a/src/main.rs b/src/main.rs
new file mode 100644
index 0000000..08a4f51
--- /dev/null
+++ b/src/main.rs
@@ -0,0 +1,45 @@
+mod config;
+mod install;
+mod util;
+mod cli;
+
+use clap::Parser;
+use colored::*;
+use std::process;
+use config::Config;
+use util::*;
+
+fn main() {
+ let args = cli::Args::parse();
+ let install_profile = &args.file;
+
+ let loaded_config: Config = Config::from_file(install_profile).unwrap_or_else(|err| {
+ eprintln!("Cannot load '{install_profile}': {}", err.to_string());
+ process::exit(EXIT_IO_ERROR);
+ });
+
+ let num_stages = loaded_config.stages.len();
+ for i in 1..=num_stages {
+ let stage = loaded_config.stages.get(i-1).unwrap();
+ let name = stage.name.clone().unwrap();
+ let count = format!("[{i}/{num_stages}]").yellow();
+
+ println!("{} {}", count.bold(), name.bold());
+ for step in stage.steps.as_ref().unwrap() {
+ let step_result = install::run_step(&step, &stage.base_path);
+
+ println!("{}", fmt_step(step, &step_result));
+ if step_result.is_err() {
+ eprintln!("Step failed: {}", step_result.unwrap_err().to_string());
+ println!();
+ println!("{} {} {} {}{}", "Install stopped at stage".red(), i.to_string().red(), "of".red(), num_stages.to_string().red(), ".".red());
+ process::exit(EXIT_INSTALL_FAILED);
+ }
+ }
+
+ println!()
+ }
+
+ println!("{} {} {}", "Install of".bright_green(), install_profile.bright_green(), "completed.".bright_green())
+}
+
diff --git a/src/util.rs b/src/util.rs
new file mode 100644
index 0000000..9c0ed1c
--- /dev/null
+++ b/src/util.rs
@@ -0,0 +1,34 @@
+use crate::config::Step;
+use colored::*;
+
+pub const EXIT_IO_ERROR: i32 = 1;
+pub const EXIT_INSTALL_FAILED: i32 = 2;
+
+// todo: figure out a better way of doing this
+pub fn str_step(step: &Step) -> String {
+ match step {
+ Step::Link(path) => format!("Link {} to {}", &path.from, &path.to),
+ Step::Copy(path) => format!("Copy {} to {}", &path.from, &path.to),
+ Step::Shell(command) => format!("Run {}", command),
+ }
+}
+
+pub fn fmt_step(step: &Step, result: &Result<bool, Box<dyn std::error::Error>>) -> String {
+ let action = str_step(step);
+
+ match result {
+ Ok(true) => { format!("{}{} {} {}", "[".bright_black(), "✔".green(), "]".bright_black(), action) }
+ Ok(false) => { format!("{}{} {} {}", "[".bright_black(), "▼".bright_black(), "]".bright_black(), action) }
+ Err(_) => { format!("{}{} {} {}", "[".bright_black(), "✘".red(), "]".bright_black(), action) }
+ }
+}
+
+pub fn expand_home(path: &str) -> String {
+ let home = std::env::var("HOME").expect("no $HOME");
+ if path.starts_with("~") {
+ let rest_of_path = &path[1..];
+ format!("{}{}", home, rest_of_path)
+ } else {
+ path.to_owned()
+ }
+} \ No newline at end of file