parent
94473ca8da
commit
34ff8f54ab
55
README.md
55
README.md
|
@ -1,11 +1,17 @@
|
||||||
#### kot
|
#### kot
|
||||||
|
|
||||||
Learning to program in Rust by making myself a Linux CLI tool to help manage dotfiles and configurations.
|
Learning to program in Rust by making myself a Linux CLI tool to help manage dotfiles and configurations.
|
||||||
|
|
||||||
There are many other tools to manage dotfiles that work just fine. For now, this is intended to be just for my own learning / use and not a general dotfiles management utility.
|
There are many other tools to manage dotfiles that work just fine. For now, this is intended to be just for my own learning / use and not a general dotfiles management utility.
|
||||||
|
|
||||||
|
Follow [Rustup instructions](https://rustup.rs/) to setup the Rust toolchain
|
||||||
|
|
||||||
|
Then to build and run `kot`, run the following commands
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
[kapper@kubuntu ~]$./kot --help
|
git clone https://gitlab.com/shaunrd0/kot && cd kot
|
||||||
|
cargo build
|
||||||
|
./target/debug/kot --help
|
||||||
|
|
||||||
kot 0.1.0
|
kot 0.1.0
|
||||||
CLI utility for managing Linux user configurations
|
CLI utility for managing Linux user configurations
|
||||||
|
|
||||||
|
@ -27,22 +33,41 @@ ARGS:
|
||||||
|
|
||||||
To store dotfiles, this repository uses submodules. To update surface-level submodules, we can run the following commands
|
To store dotfiles, this repository uses submodules. To update surface-level submodules, we can run the following commands
|
||||||
```bash
|
```bash
|
||||||
git submodule init
|
git submodule update --init
|
||||||
git submodule update
|
|
||||||
Submodule path 'dot': checked out '7877117d5bd413ecf35c86efb4514742d8136843'
|
Submodule path 'dot': checked out '7877117d5bd413ecf35c86efb4514742d8136843'
|
||||||
```
|
```
|
||||||
|
|
||||||
But in the case of my dotfiles repository, [shaunrd0/dot](https://gitlab.com/shaunrd0/dot), I use submodules to clone repositories for vim plugins. To update all submodules *and their nested submodules*, we can run the following commands
|
But in the case of my dotfiles repository, [shaunrd0/dot](https://gitlab.com/shaunrd0/dot), I use submodules to clone repositories for vim plugins. To update all submodules *and their nested submodules*, we can run the following commands
|
||||||
```bash
|
```bash
|
||||||
git submodule init
|
git submodule update --init --recursive
|
||||||
git submodule update --recursive
|
|
||||||
Submodule path 'dot': checked out '7877117d5bd413ecf35c86efb4514742d8136843'
|
Submodule 'dotfiles/dot' (https://gitlab.com/shaunrd0/dot) registered for path 'dotfiles/dot'
|
||||||
Submodule path 'dot/.vim/bundle/Colorizer': checked out '826d5691ac7d36589591314621047b1b9d89ed34'
|
Cloning into '/home/kapper/Code/kotd/dotfiles/dot'...
|
||||||
Submodule path 'dot/.vim/bundle/ale': checked out '3ea887d2f4d43dd55d81213517344226f6399ed6'
|
warning: redirecting to https://gitlab.com/shaunrd0/dot.git/
|
||||||
Submodule path 'dot/.vim/bundle/clang_complete': checked out '293a1062274a06be61797612034bd8d87851406e'
|
Submodule path 'dotfiles/dot': checked out '7877117d5bd413ecf35c86efb4514742d8136843'
|
||||||
Submodule path 'dot/.vim/bundle/supertab': checked out 'd80e8e2c1fa08607fa34c0ca5f1b66d8a906c5ef'
|
Submodule '.vim/bundle/Colorizer' (https://github.com/chrisbra/Colorizer) registered for path 'dotfiles/dot/.vim/bundle/Colorizer'
|
||||||
Submodule path 'dot/.vim/bundle/unicode.vim': checked out 'afb8db4f81580771c39967e89bc5772e72b9018e'
|
Submodule '.vim/bundle/ale' (https://github.com/dense-analysis/ale) registered for path 'dotfiles/dot/.vim/bundle/ale'
|
||||||
Submodule path 'dot/.vim/bundle/vim-airline': checked out 'cb1bc19064d3762e4e08103afb37a246b797d902'
|
Submodule '.vim/bundle/clang_complete' (https://github.com/xavierd/clang_complete) registered for path 'dotfiles/dot/.vim/bundle/clang_complete'
|
||||||
Submodule path 'dot/.vim/bundle/vim-airline-themes': checked out 'd148d42d9caf331ff08b6cae683d5b210003cde7'
|
Submodule '.vim/bundle/supertab' (https://github.com/ervandew/supertab) registered for path 'dotfiles/dot/.vim/bundle/supertab'
|
||||||
Submodule path 'dot/.vim/bundle/vim-signify': checked out 'b2a0450e23c63b75bbeabf4f0c28f9b4b2480689'
|
Submodule '.vim/bundle/unicode.vim' (https://github.com/chrisbra/unicode.vim) registered for path 'dotfiles/dot/.vim/bundle/unicode.vim'
|
||||||
|
Submodule '.vim/bundle/vim-airline' (https://github.com/vim-airline/vim-airline) registered for path 'dotfiles/dot/.vim/bundle/vim-airline'
|
||||||
|
Submodule '.vim/bundle/vim-airline-themes' (https://github.com/vim-airline/vim-airline-themes) registered for path 'dotfiles/dot/.vim/bundle/vim-airline-themes'
|
||||||
|
Submodule '.vim/bundle/vim-signify' (https://github.com/mhinz/vim-signify) registered for path 'dotfiles/dot/.vim/bundle/vim-signify'
|
||||||
|
Cloning into '/home/kapper/Code/kotd/dotfiles/dot/.vim/bundle/Colorizer'...
|
||||||
|
Cloning into '/home/kapper/Code/kotd/dotfiles/dot/.vim/bundle/ale'...
|
||||||
|
Cloning into '/home/kapper/Code/kotd/dotfiles/dot/.vim/bundle/clang_complete'...
|
||||||
|
Cloning into '/home/kapper/Code/kotd/dotfiles/dot/.vim/bundle/supertab'...
|
||||||
|
Cloning into '/home/kapper/Code/kotd/dotfiles/dot/.vim/bundle/unicode.vim'...
|
||||||
|
Cloning into '/home/kapper/Code/kotd/dotfiles/dot/.vim/bundle/vim-airline'...
|
||||||
|
Cloning into '/home/kapper/Code/kotd/dotfiles/dot/.vim/bundle/vim-airline-themes'...
|
||||||
|
Cloning into '/home/kapper/Code/kotd/dotfiles/dot/.vim/bundle/vim-signify'...
|
||||||
|
Submodule path 'dotfiles/dot/.vim/bundle/Colorizer': checked out '826d5691ac7d36589591314621047b1b9d89ed34'
|
||||||
|
Submodule path 'dotfiles/dot/.vim/bundle/ale': checked out '3ea887d2f4d43dd55d81213517344226f6399ed6'
|
||||||
|
Submodule path 'dotfiles/dot/.vim/bundle/clang_complete': checked out '293a1062274a06be61797612034bd8d87851406e'
|
||||||
|
Submodule path 'dotfiles/dot/.vim/bundle/supertab': checked out 'd80e8e2c1fa08607fa34c0ca5f1b66d8a906c5ef'
|
||||||
|
Submodule path 'dotfiles/dot/.vim/bundle/unicode.vim': checked out 'afb8db4f81580771c39967e89bc5772e72b9018e'
|
||||||
|
Submodule path 'dotfiles/dot/.vim/bundle/vim-airline': checked out 'cb1bc19064d3762e4e08103afb37a246b797d902'
|
||||||
|
Submodule path 'dotfiles/dot/.vim/bundle/vim-airline-themes': checked out 'd148d42d9caf331ff08b6cae683d5b210003cde7'
|
||||||
|
Submodule path 'dotfiles/dot/.vim/bundle/vim-signify': checked out 'b2a0450e23c63b75bbeabf4f0c28f9b4b2480689'
|
||||||
```
|
```
|
||||||
|
|
||||||
|
|
|
@ -1 +0,0 @@
|
||||||
This directory is for testing the backup process of user configurations that conflict with configurations we're attempting to install.
|
|
12
src/kot.rs
12
src/kot.rs
|
@ -16,6 +16,8 @@ pub mod io;
|
||||||
|
|
||||||
// -----------------------------------------------------------------------------
|
// -----------------------------------------------------------------------------
|
||||||
|
|
||||||
|
// Creates symbolic links to the configurations we're installing
|
||||||
|
// TODO: On error, revert to last good state
|
||||||
pub fn install_configs(args: & cli::Cli) -> std::io::Result<()> {
|
pub fn install_configs(args: & cli::Cli) -> std::io::Result<()> {
|
||||||
// Get the configurations and their target installation paths
|
// Get the configurations and their target installation paths
|
||||||
// + Checks for conflicts and prompts user to abort or continue
|
// + Checks for conflicts and prompts user to abort or continue
|
||||||
|
@ -26,12 +28,16 @@ pub fn install_configs(args: & cli::Cli) -> std::io::Result<()> {
|
||||||
println!("Installing config: {:?}\n+ At location: {:?}\n", config_path, target_path);
|
println!("Installing config: {:?}\n+ At location: {:?}\n", config_path, target_path);
|
||||||
|
|
||||||
match std::os::unix::fs::symlink(config_path, target_path) {
|
match std::os::unix::fs::symlink(config_path, target_path) {
|
||||||
Ok(()) => (),
|
Ok(()) => (), // Configuration installed successfully
|
||||||
Err(_e) => {
|
Err(_e) => {
|
||||||
|
// Attempt to remove the file or directory, and then symlink the new config
|
||||||
match target_path.is_dir() {
|
match target_path.is_dir() {
|
||||||
true => fs_extra::dir::remove(target_path),
|
true => fs_extra::dir::remove(target_path)
|
||||||
false => fs_extra::file::remove(target_path),
|
.expect(&format!("Error: Unable to remove directory: {:?}", target_path)),
|
||||||
|
false => fs_extra::file::remove(target_path)
|
||||||
|
.expect(&format!("Error: Unable to remove file: {:?}", target_path)),
|
||||||
};
|
};
|
||||||
|
// Try to symlink the config again, if failure exit with error
|
||||||
std::os::unix::fs::symlink(config_path, target_path)
|
std::os::unix::fs::symlink(config_path, target_path)
|
||||||
.expect(&format!("Unable to symlink config: {:?}", config_path));
|
.expect(&format!("Unable to symlink config: {:?}", config_path));
|
||||||
},
|
},
|
||||||
|
|
|
@ -24,13 +24,14 @@ pub struct Cli {
|
||||||
help="Local or full path to user configurations to install",
|
help="Local or full path to user configurations to install",
|
||||||
parse(from_os_str)
|
parse(from_os_str)
|
||||||
)]
|
)]
|
||||||
pub configs_dir: std::path::PathBuf,
|
pub dotfiles_dir: std::path::PathBuf,
|
||||||
|
|
||||||
#[structopt(
|
#[structopt(
|
||||||
help="The location to attempt installation of user configurations",
|
help="The location to attempt installation of user configurations",
|
||||||
default_value="dry-runs/kapper", // TODO: Remove temp default value after tests
|
default_value="dry-runs/kapper", // TODO: Remove temp default value after tests
|
||||||
// env = "HOME", // Default value to env variable $HOME
|
// env = "HOME", // Default value to env variable $HOME
|
||||||
long="home-dir",
|
name="install-dir",
|
||||||
|
short, long,
|
||||||
parse(from_os_str)
|
parse(from_os_str)
|
||||||
)]
|
)]
|
||||||
pub install_dir: std::path::PathBuf,
|
pub install_dir: std::path::PathBuf,
|
||||||
|
@ -38,7 +39,8 @@ pub struct Cli {
|
||||||
#[structopt(
|
#[structopt(
|
||||||
help="The location to store backups for this user",
|
help="The location to store backups for this user",
|
||||||
default_value="backups/kapper",
|
default_value="backups/kapper",
|
||||||
long="backup-dir",
|
name="backup-dir",
|
||||||
|
short, long,
|
||||||
parse(from_os_str)
|
parse(from_os_str)
|
||||||
)]
|
)]
|
||||||
pub backup_dir: std::path::PathBuf,
|
pub backup_dir: std::path::PathBuf,
|
||||||
|
@ -61,8 +63,16 @@ pub fn from_args() -> Cli {
|
||||||
impl Cli {
|
impl Cli {
|
||||||
// Helper function to normalize arguments passed to program
|
// Helper function to normalize arguments passed to program
|
||||||
pub fn normalize(mut self) -> Self {
|
pub fn normalize(mut self) -> Self {
|
||||||
self.configs_dir = self.configs_dir.canonicalize().unwrap();
|
// If the path to the dotfiles doesn't exist, exit with error
|
||||||
|
if !&self.dotfiles_dir.exists() {
|
||||||
|
panic!("Error: Dotfiles configuration at {:?} does not exist", self.dotfiles_dir);
|
||||||
|
}
|
||||||
|
self.dotfiles_dir = self.dotfiles_dir.canonicalize().unwrap();
|
||||||
|
|
||||||
|
// If either the install or backup dir don't exist, create them
|
||||||
|
std::fs::create_dir_all(&self.install_dir).ok();
|
||||||
self.install_dir = self.install_dir.canonicalize().unwrap();
|
self.install_dir = self.install_dir.canonicalize().unwrap();
|
||||||
|
std::fs::create_dir_all(&self.backup_dir).ok();
|
||||||
self.backup_dir = self.backup_dir.canonicalize().unwrap();
|
self.backup_dir = self.backup_dir.canonicalize().unwrap();
|
||||||
self
|
self
|
||||||
}
|
}
|
||||||
|
|
|
@ -19,20 +19,27 @@ use fs_extra::dir;
|
||||||
|
|
||||||
// -----------------------------------------------------------------------------
|
// -----------------------------------------------------------------------------
|
||||||
|
|
||||||
|
// Creates a backup of configurations that conflict
|
||||||
|
// + Backup directory location is specified by CLI --backup-dir
|
||||||
|
// TODO: Automatically create backup directory
|
||||||
|
// TODO: .kotignore in dotfiles repo to specify files to not install / backup
|
||||||
|
// TODO: .kotrc in dotfiles repo or home dir to set backup-dir and install-dir?
|
||||||
fn backup_config(config_path: & PathBuf, backup_dir: & PathBuf) -> super::io::Result<()> {
|
fn backup_config(config_path: & PathBuf, backup_dir: & PathBuf) -> super::io::Result<()> {
|
||||||
let mut backup_path = backup_dir.to_owned();
|
let mut backup_path = backup_dir.to_owned();
|
||||||
backup_path.push(config_path.file_name().unwrap());
|
backup_path.push(config_path.file_name().unwrap());
|
||||||
match config_path.is_dir() {
|
match config_path.is_dir() {
|
||||||
true => {
|
true => {
|
||||||
|
// Copy directory with recursion using fs_extra::dir::move_dir
|
||||||
let mut options = dir::CopyOptions::new();
|
let mut options = dir::CopyOptions::new();
|
||||||
options.copy_inside = true;
|
options.copy_inside = true;
|
||||||
dir::move_dir(config_path, backup_path, &options)
|
dir::move_dir(config_path, backup_path, &options)
|
||||||
},
|
},
|
||||||
false => {
|
false => {
|
||||||
|
// Copy single configuration file
|
||||||
let options = fs_extra::file::CopyOptions::new();
|
let options = fs_extra::file::CopyOptions::new();
|
||||||
fs_extra::file::move_file(config_path, backup_path, &options)
|
fs_extra::file::move_file(config_path, backup_path, &options)
|
||||||
},
|
},
|
||||||
};
|
}.expect(&format!("Error: Unable to backup config: {:?}", config_path));
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -41,27 +48,41 @@ fn backup_config(config_path: & PathBuf, backup_dir: & PathBuf) -> super::io::Re
|
||||||
pub fn get_target_paths(args: & super::cli::Cli) -> super::io::Result<HashMap<PathBuf, PathBuf>> {
|
pub fn get_target_paths(args: & super::cli::Cli) -> super::io::Result<HashMap<PathBuf, PathBuf>> {
|
||||||
let mut config_map = HashMap::new();
|
let mut config_map = HashMap::new();
|
||||||
|
|
||||||
|
// Local variable for the installation directory as an absolute path
|
||||||
let mut config_target = args.install_dir.to_owned();
|
let mut config_target = args.install_dir.to_owned();
|
||||||
for config_entry in fs::read_dir(&args.configs_dir)? {
|
// For each file or directory within the dotfiles we're installing
|
||||||
|
for config_entry in fs::read_dir(&args.dotfiles_dir)? {
|
||||||
|
// Match result from reading each item in dotfiles, return error if any
|
||||||
match config_entry {
|
match config_entry {
|
||||||
Err(err) => return Err(err),
|
Err(err) => return Err(err),
|
||||||
Ok(entry) => {
|
Ok(entry) => {
|
||||||
|
// Create full path to target config file (or directory) by push onto install path
|
||||||
config_target.push(entry.file_name());
|
config_target.push(entry.file_name());
|
||||||
|
|
||||||
|
// If the target configuration file or directory already exists
|
||||||
if config_target.exists() {
|
if config_target.exists() {
|
||||||
match super::io::prompt(format!("Configuration already exists: {:?}\nAbort? Enter y/n or Y/N: ", config_target)) {
|
// Ask client if they would like to abort given the config collision
|
||||||
true => return Err(std::io::Error::from(std::io::ErrorKind::AlreadyExists)),//panic!("User abort"),
|
let msg = format!("Configuration already exists: {:?}\
|
||||||
false => backup_config(&config_target, &args.backup_dir).ok(), // TODO: Backup colliding configs
|
\nAbort? Enter y/n or Y/N: ", config_target);
|
||||||
|
|
||||||
|
// If we abort, exit; If we continue, back up the configs
|
||||||
|
match super::io::prompt(msg) {
|
||||||
|
true => return Err(std::io::Error::from(std::io::ErrorKind::AlreadyExists)),
|
||||||
|
false => backup_config(&config_target, &args.backup_dir).ok(),
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// If the entry doesn't already exist, insert it into the config_map
|
||||||
|
// TODO: If the entry does exist, should there be an exception?
|
||||||
config_map.entry(entry.path().to_owned())
|
config_map.entry(entry.path().to_owned())
|
||||||
.or_insert(config_target.to_owned());
|
.or_insert(config_target.to_owned());
|
||||||
|
|
||||||
|
// Reset config_target to be equal to requested install_dir
|
||||||
config_target.pop();
|
config_target.pop();
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
Ok(config_map)
|
Ok(config_map)
|
||||||
}
|
}
|
||||||
|
|
|
@ -17,6 +17,8 @@ use std::io;
|
||||||
|
|
||||||
// -----------------------------------------------------------------------------
|
// -----------------------------------------------------------------------------
|
||||||
|
|
||||||
|
// Asks user for y/n Y/N input, returns true/false respectively
|
||||||
|
// + Prompt output defined by msg parameter String
|
||||||
pub fn prompt(msg: String) -> bool {
|
pub fn prompt(msg: String) -> bool {
|
||||||
println!("{}", msg);
|
println!("{}", msg);
|
||||||
let mut reply = String::new();
|
let mut reply = String::new();
|
||||||
|
@ -25,6 +27,7 @@ pub fn prompt(msg: String) -> bool {
|
||||||
match reply.trim() {
|
match reply.trim() {
|
||||||
"y" | "Y" => true,
|
"y" | "Y" => true,
|
||||||
"n" | "N" => false,
|
"n" | "N" => false,
|
||||||
|
// Handle garbage input
|
||||||
_ => prompt("Please enter y/n or Y/N\n".to_owned()),
|
_ => prompt("Please enter y/n or Y/N\n".to_owned()),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
13
src/main.rs
13
src/main.rs
|
@ -15,12 +15,21 @@ mod kot;
|
||||||
// -----------------------------------------------------------------------------
|
// -----------------------------------------------------------------------------
|
||||||
|
|
||||||
fn main() {
|
fn main() {
|
||||||
|
// Call augmented kot::cli::from_args() to parse CLI arguments
|
||||||
let args = kot::cli::from_args();
|
let args = kot::cli::from_args();
|
||||||
|
// At this point all paths exist and have been converted to absolute paths
|
||||||
println!("args: {:?}\n", args);
|
println!("args: {:?}\n", args);
|
||||||
|
|
||||||
|
// Attempt to install the configurations, checking for collisions
|
||||||
match kot::install_configs(&args) {
|
match kot::install_configs(&args) {
|
||||||
Err(e) => println!("Error: {:?}\n+ Configs used: {:?}\n+ Install directory: {:?}\n",
|
Err(e) => {
|
||||||
e.kind(), args.configs_dir, args.install_dir),
|
// If there was an error, show the error type and run settings
|
||||||
|
println!(
|
||||||
|
"Error: {:?}\n+ Configs used: {:?}\n+ Install directory: {:?}\n",
|
||||||
|
e.kind(), args.dotfiles_dir, args.install_dir
|
||||||
|
)
|
||||||
|
},
|
||||||
|
// Configurations installed successfully
|
||||||
Ok(()) => (),
|
Ok(()) => (),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
Loading…
Reference in New Issue