initial commit

This commit is contained in:
Youwen Wu 2024-06-26 01:46:51 -07:00
commit ca1b36218f
Signed by: youwen5
GPG key ID: 865658ED1FE61EC3
9 changed files with 762 additions and 0 deletions

2
.gitignore vendored Normal file
View file

@ -0,0 +1,2 @@
/target
dartgun-test/

2
.prettierrc.toml Normal file
View file

@ -0,0 +1,2 @@
lineWidth = 80
proseWrap = "always"

338
Cargo.lock generated Normal file
View file

@ -0,0 +1,338 @@
# This file is automatically @generated by Cargo.
# It is not intended for manual editing.
version = 3
[[package]]
name = "anstream"
version = "0.6.14"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "418c75fa768af9c03be99d17643f93f79bbba589895012a80e3452a19ddda15b"
dependencies = [
"anstyle",
"anstyle-parse",
"anstyle-query",
"anstyle-wincon",
"colorchoice",
"is_terminal_polyfill",
"utf8parse",
]
[[package]]
name = "anstyle"
version = "1.0.7"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "038dfcf04a5feb68e9c60b21c9625a54c2c0616e79b72b0fd87075a056ae1d1b"
[[package]]
name = "anstyle-parse"
version = "0.2.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c03a11a9034d92058ceb6ee011ce58af4a9bf61491aa7e1e59ecd24bd40d22d4"
dependencies = [
"utf8parse",
]
[[package]]
name = "anstyle-query"
version = "1.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ad186efb764318d35165f1758e7dcef3b10628e26d41a44bc5550652e6804391"
dependencies = [
"windows-sys",
]
[[package]]
name = "anstyle-wincon"
version = "3.0.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "61a38449feb7068f52bb06c12759005cf459ee52bb4adc1d5a7c4322d716fb19"
dependencies = [
"anstyle",
"windows-sys",
]
[[package]]
name = "clap"
version = "4.5.7"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5db83dced34638ad474f39f250d7fea9598bdd239eaced1bdf45d597da0f433f"
dependencies = [
"clap_builder",
"clap_derive",
]
[[package]]
name = "clap_builder"
version = "4.5.7"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f7e204572485eb3fbf28f871612191521df159bc3e15a9f5064c66dba3a8c05f"
dependencies = [
"anstream",
"anstyle",
"clap_lex",
"strsim",
]
[[package]]
name = "clap_derive"
version = "4.5.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c780290ccf4fb26629baa7a1081e68ced113f1d3ec302fa5948f1c381ebf06c6"
dependencies = [
"heck",
"proc-macro2",
"quote",
"syn",
]
[[package]]
name = "clap_lex"
version = "0.7.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "4b82cf0babdbd58558212896d1a4272303a57bdb245c2bf1147185fb45640e70"
[[package]]
name = "colorchoice"
version = "1.0.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0b6a852b24ab71dffc585bcb46eaf7959d175cb865a7152e35b348d1b2960422"
[[package]]
name = "dartgun-rs"
version = "0.1.0"
dependencies = [
"clap",
"toml",
]
[[package]]
name = "equivalent"
version = "1.0.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5443807d6dff69373d433ab9ef5378ad8df50ca6298caf15de6e52e24aaf54d5"
[[package]]
name = "hashbrown"
version = "0.14.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e5274423e17b7c9fc20b6e7e208532f9b19825d82dfd615708b70edd83df41f1"
[[package]]
name = "heck"
version = "0.5.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea"
[[package]]
name = "indexmap"
version = "2.2.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "168fb715dda47215e360912c096649d23d58bf392ac62f73919e831745e40f26"
dependencies = [
"equivalent",
"hashbrown",
]
[[package]]
name = "is_terminal_polyfill"
version = "1.70.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f8478577c03552c21db0e2724ffb8986a5ce7af88107e6be5d2ee6e158c12800"
[[package]]
name = "memchr"
version = "2.7.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "78ca9ab1a0babb1e7d5695e3530886289c18cf2f87ec19a575a0abdce112e3a3"
[[package]]
name = "proc-macro2"
version = "1.0.86"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5e719e8df665df0d1c8fbfd238015744736151d4445ec0836b8e628aae103b77"
dependencies = [
"unicode-ident",
]
[[package]]
name = "quote"
version = "1.0.36"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0fa76aaf39101c457836aec0ce2316dbdc3ab723cdda1c6bd4e6ad4208acaca7"
dependencies = [
"proc-macro2",
]
[[package]]
name = "serde"
version = "1.0.203"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7253ab4de971e72fb7be983802300c30b5a7f0c2e56fab8abfc6a214307c0094"
dependencies = [
"serde_derive",
]
[[package]]
name = "serde_derive"
version = "1.0.203"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "500cbc0ebeb6f46627f50f3f5811ccf6bf00643be300b4c3eabc0ef55dc5b5ba"
dependencies = [
"proc-macro2",
"quote",
"syn",
]
[[package]]
name = "serde_spanned"
version = "0.6.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "79e674e01f999af37c49f70a6ede167a8a60b2503e56c5599532a65baa5969a0"
dependencies = [
"serde",
]
[[package]]
name = "strsim"
version = "0.11.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7da8b5736845d9f2fcb837ea5d9e2628564b3b043a70948a3f0b778838c5fb4f"
[[package]]
name = "syn"
version = "2.0.68"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "901fa70d88b9d6c98022e23b4136f9f3e54e4662c3bc1bd1d84a42a9a0f0c1e9"
dependencies = [
"proc-macro2",
"quote",
"unicode-ident",
]
[[package]]
name = "toml"
version = "0.8.14"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6f49eb2ab21d2f26bd6db7bf383edc527a7ebaee412d17af4d40fdccd442f335"
dependencies = [
"serde",
"serde_spanned",
"toml_datetime",
"toml_edit",
]
[[package]]
name = "toml_datetime"
version = "0.6.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "4badfd56924ae69bcc9039335b2e017639ce3f9b001c393c1b2d1ef846ce2cbf"
dependencies = [
"serde",
]
[[package]]
name = "toml_edit"
version = "0.22.14"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f21c7aaf97f1bd9ca9d4f9e73b0a6c74bd5afef56f2bc931943a6e1c37e04e38"
dependencies = [
"indexmap",
"serde",
"serde_spanned",
"toml_datetime",
"winnow",
]
[[package]]
name = "unicode-ident"
version = "1.0.12"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3354b9ac3fae1ff6755cb6db53683adb661634f67557942dea4facebec0fee4b"
[[package]]
name = "utf8parse"
version = "0.2.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "06abde3611657adf66d383f00b093d7faecc7fa57071cce2578660c9f1010821"
[[package]]
name = "windows-sys"
version = "0.52.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "282be5f36a8ce781fad8c8ae18fa3f9beff57ec1b52cb3de0789201425d9a33d"
dependencies = [
"windows-targets",
]
[[package]]
name = "windows-targets"
version = "0.52.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6f0713a46559409d202e70e28227288446bf7841d3211583a4b53e3f6d96e7eb"
dependencies = [
"windows_aarch64_gnullvm",
"windows_aarch64_msvc",
"windows_i686_gnu",
"windows_i686_gnullvm",
"windows_i686_msvc",
"windows_x86_64_gnu",
"windows_x86_64_gnullvm",
"windows_x86_64_msvc",
]
[[package]]
name = "windows_aarch64_gnullvm"
version = "0.52.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7088eed71e8b8dda258ecc8bac5fb1153c5cffaf2578fc8ff5d61e23578d3263"
[[package]]
name = "windows_aarch64_msvc"
version = "0.52.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9985fd1504e250c615ca5f281c3f7a6da76213ebd5ccc9561496568a2752afb6"
[[package]]
name = "windows_i686_gnu"
version = "0.52.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "88ba073cf16d5372720ec942a8ccbf61626074c6d4dd2e745299726ce8b89670"
[[package]]
name = "windows_i686_gnullvm"
version = "0.52.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "87f4261229030a858f36b459e748ae97545d6f1ec60e5e0d6a3d32e0dc232ee9"
[[package]]
name = "windows_i686_msvc"
version = "0.52.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "db3c2bf3d13d5b658be73463284eaf12830ac9a26a90c717b7f771dfe97487bf"
[[package]]
name = "windows_x86_64_gnu"
version = "0.52.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "4e4246f76bdeff09eb48875a0fd3e2af6aada79d409d33011886d3e1581517d9"
[[package]]
name = "windows_x86_64_gnullvm"
version = "0.52.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "852298e482cd67c356ddd9570386e2862b5673c85bd5f88df9ab6802b334c596"
[[package]]
name = "windows_x86_64_msvc"
version = "0.52.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "bec47e5bfd1bff0eeaf6d8b485cc1074891a197ab4225d504cb7a1ab88b02bf0"
[[package]]
name = "winnow"
version = "0.6.13"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "59b5e5f6c299a3c7890b876a2a587f3115162487e704907d9b6cd29473052ba1"
dependencies = [
"memchr",
]

10
Cargo.toml Normal file
View file

@ -0,0 +1,10 @@
[package]
name = "dartgun-rs"
version = "0.1.0"
edition = "2021"
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
[dependencies]
clap = { version = "4.5.7", features = ["derive"] }
toml = "0.8.14"

148
README.md Normal file
View file

@ -0,0 +1,148 @@
# dartgun
the stupid dotfile manager.
## Impetus
Managing dot files are annoying. They're some of the most essential parts of a
developer's system, yet most operating systems (distros) don't provide a way to
easily manage them. There's the `~/.config` directory, which in theory holds all
of your configuration files. If this were true, you could simply `git init`
inside `~/.config` and have a versioned dotfile repo that you could backup and
deploy to new machines. However, configuration files often end up all over a
system. There's [NixOS](https://nixos.org/), but not everyone can dedicate 40
hours a week to configuring their OS.
Advanced solutions dotfile helpers, but most users don't really need much more
than a set of bash scripts which copies their dotfiles around their system.
Dartgun essentially does this in a more systematic manner, and seeks to be just
as still simple to set up and manage. Everything lives in one folder which can
be versioned by git. Dartgun will put your dotfiles in the correct places by
symlinking from the central dotfile directory to the specified locations.
The primary goal is to provide an easy way to manage your dotfiles in a
centralized area and sync them between different systems. A secondary goal is to
help automatically set up a new system with the configuration files in the
correct places. However, automatically installing additional software and
dependencies is outside of the scope of the project.
## Non-goals
Dartgun is specifically designed to have a minimum amount of features to make it
as easy to adopt as possible. If you're reading this, I'll assume that you are
already looking for a dotfile manager. Therefore, it might be easier to list
things that Dartgun is _not_ designed to do. If you do not need any of these
features, then Dartgun might the right dotfile manager for you.
Dartgun is not for:
- Managing a fleet of computers
- Automatically deploying servers
- Deterministically setting up an entire OS from configuration files (see NixOS
for that)
- Automatically installing packages or software alongside dotfiles; it only
manages the files, the user is still responsible for ensuring software is
available
- Power users who want deep customizability and feature sets to help fully
automate system configuration
## Goals
- Easily version dotfiles with git by keeping everything in one central
directory
- Copy dotfiles to a new machine quickly and transparently
- Sync dotfiles between different machines
- Keep configuration effort to a minimum
## How it works
Dartgun allows you to centralize all of your dotfiles in a single directory.
Dartgun is configured through the `dartgun.toml` file. You should set your
machine name in this file like so:
```toml
machine = "youwens-mac"
```
Also, it's common to have programs that are not installed on every computer.
Therefore, each dotfile will specify which application it is for, and whether or
not it should be applied by default. See the dotfile configuration below for how
to configure this.
You can specify which applications are in which machines with the `apps.toml`
file.
```toml
[youwens-mac]
available = ["neovim", "hyprland", "zsh"]
```
### Why symlinking?
Any configuration updates made will always sync back to the dartgun directory so
they can be tracked by git. Likewise, any remote updates pulled in will also be
automatically reflected in the system for free.
### Why no Windows support?
Windows symlinks work a little differently from Unix symlinks. The difference is
not massive and I plan to look into supporting Windows at a later date.
## Usage guide
Begin by creating a folder to place your dotfiles in. I'll refer to it as the
"dartgun folder" for the rest of these instructions. Mine is located at
`~/.dartgun`, but it can be called whatever you want and located wherever you
want it.
Place your dotfiles in the dartgun folder. You can organize them however you
want. The primary configuration is done in the `dartgun.json` file, located in
root of the dartgun folder. In here, you specify where each file or folder in
the directory goes.
For example, I can tell the `.dartgun/nvim` folder to go to `~/.config/nvim`
with
```json
{
darts = [
{
location: "./nvim",
destination: "~/.config/nvim",
strategy: "hardlink",
machines: [youwens-mac, youwens-archlinux],
for: "neovim"
}
]
}
```
```
location: the relative path, from the dartgun folder, where either the directory or file to be copied is located
destination: the destination path to which the file should be copied to. must be an absolute path
strategy: which strategy to use to copy the file. accepts 'hardlink', 'symlink', or 'copy'
machines: which machine names this file will be copied on
for: the application which the dotfile is for
```
Note that you can choose how you want to approach the machine configuration. You
can either have a specific machine key for each computer you own, or specify
machines by platform (eg. "arch", "mac", "fedora"). You can get as fine-grained
or generic as you'd like.
Then, run `dartgun blast` to apply your dotfiles to the locations. For nested
destination directories, it will create all of the directories in the chain if
they do not exist already. You may have to run `sudo dartgun blast` if you are
trying to copy files to restricted locations.
For symlinked and hardlinked directories, you can simply sync your dotfiles by
updating them in the dartgun folder, for example by running `git pull` after
you've set up git versioning.
If you use the `copy` strategy, then you need to re-run `dartgun blast` each
time you update the files in your dartgun folder. generally, we do not recommend
the copy method unless you have to for some reason, since it may lead to desyncs
between the dartgun folder and your actual system, while this is impossible with
hardlinking or symlinking.

30
src/cli.rs Normal file
View file

@ -0,0 +1,30 @@
use std::path::Path;
use crate::dartfile;
use clap::Parser;
/// dartgun's command line interface
#[derive(Parser, Debug)]
#[command(version, about, long_about = None)]
struct Cli {
action: Option<String>,
}
fn fire() {
println!("Attempting to find and read `dartfile.toml`.");
let gun = dartfile::parse(Path::new("./dartgun.toml"));
println!("Writing symlinks...");
match gun.create_symlinks() {
Ok(_) => println!("Symlinks created successfully!"),
Err(err) => println!("Something went wrong while creating symlinks: {}", err),
}
}
pub fn run_cli() {
let cli = Cli::parse();
match cli.action.as_deref() {
Some("fire") => fire(),
_ => println!("No action specified. Run `dartgun -h` for options."),
};
}

196
src/dartfile.rs Normal file
View file

@ -0,0 +1,196 @@
use std::fs;
use std::path::{Path, PathBuf};
use toml::Table;
/// The dartfile module handles parsing the `dotfile.toml` into
/// a well-typed and self-validating data structure.
/// The raw form of a dotfile entry. Paths are stored as strings.
///
/// Should not be used outside of module internals. Its primary purpose
/// is to provide an intermediary between the raw parsed TOML and
/// the strongly typed Dotfile struct
#[derive(Debug)]
struct DotfileRaw {
location: String,
destination: String,
strategy: String,
machines: Vec<String>,
applies_to: String,
}
impl DotfileRaw {
fn determine_strategy(&self) -> Result<Strategy, String> {
match self.strategy.as_str() {
"hardlink" => Ok(Strategy::Hardlink),
"symlink" => Ok(Strategy::Symlink),
_ => Err(String::from(
"Strategy must be either 'symlink' or 'hardlink'",
)),
}
}
fn to_dotfile(&self) -> Result<Dotfile, String> {
let location_path = PathBuf::from(&self.location);
let destination_path = PathBuf::from(&self.destination);
Ok(Dotfile {
location: location_path,
destination: destination_path,
strategy: self.determine_strategy()?,
machines: self.machines.clone(),
applies_to: self.applies_to.clone(),
})
}
// TODO: improve error handling for parsing from raw TOML
/// Create a new `DotfileRaw` from a `toml::Table`, which is a `Vec<Value>`.
/// Will panic if the table does not contain the fields specified.
fn from_table(table: Table) -> DotfileRaw {
let location = table
.get("location")
.ok_or("Missing 'location' field.")
.unwrap()
.as_str()
.unwrap()
.to_string();
let destination = table
.get("destination")
.ok_or("Missing 'destination' field.")
.unwrap()
.as_str()
.unwrap()
.to_string();
let strategy = table
.get("strategy")
.ok_or("Missing 'strategy' field.")
.unwrap()
.as_str()
.unwrap()
.to_string();
let machines = table
.get("machines")
.ok_or("Missing 'machines' field.")
.unwrap()
.as_array()
.unwrap()
.iter()
.map(|x| x.as_str().unwrap().to_string())
.collect();
let applies_to = table
.get("applies_to")
.ok_or("Missing 'applies_to' field.")
.unwrap()
.as_str()
.unwrap()
.to_string();
DotfileRaw {
location,
destination,
strategy,
machines,
applies_to,
}
}
}
/// The configuration object parsed from the `config` field in `dartgun.toml`
#[derive(Debug)]
pub struct Config {
machine: String,
available: Vec<String>,
}
impl Config {
// TODO: improve error handling for parsing config
/// Generate a `Config` from a raw `dartfile.toml`.
/// Will panic! if the format is invalid.
fn from_table(table: Table) -> Config {
let machine = table.get("machine").unwrap().as_str().unwrap().to_string();
let available = table
.get("available")
.unwrap()
.as_array()
.unwrap()
.iter()
.map(|x| x.as_str().unwrap().to_string())
.collect();
Config { available, machine }
}
}
/// A strongly typed Dartfile (aka `dartgun.toml`). Users of this
/// struct can assume that the dartfile is at least semantically valid
#[derive(Debug)]
pub struct Dartfile {
pub config: Config,
pub dots: Vec<Dotfile>,
}
impl Dartfile {
/// Validates the Dartfile by checking each Dotfile entry to ensure
/// the paths specified by `location` are accessible.
pub fn validate(&self) -> Result<(), String> {
for dotfile in self.dots.iter() {
if dotfile.validate().is_err() {
return Err("Invalid dotfile.".to_string());
}
}
Ok(())
}
}
/// Represents which strategy to use when deploying dotfiles across system.
#[derive(Debug)]
pub enum Strategy {
Hardlink,
Symlink,
}
/// A strongly-typed Dotfile entry
#[derive(Debug)]
pub struct Dotfile {
pub location: PathBuf,
pub destination: PathBuf,
pub strategy: Strategy,
pub machines: Vec<String>,
pub applies_to: String,
}
impl Dotfile {
/// Validates the entry by checking whether the `location` paths
/// specified are accessible.
pub fn validate(&self) -> Result<(), String> {
let path_exists = self.location.try_exists();
match path_exists {
Ok(true) => Ok(()),
Ok(false) => Err("Could not follow broken symlink.".to_string()),
Err(_) => Err("An error occurred. Does the path exist?".to_string()),
}
}
}
/// Takes a path to a `dartgun.toml` and produces a well-typed Dartfile object.
/// Currently crashes on any parse errors, but this behavior will likely change in the future.
pub fn parse(path: &Path) -> Dartfile {
let raw_data = fs::read_to_string(path).expect("Couldn't read the file.");
let value: Table = raw_data.parse::<Table>().expect("Couldn't parse the TOML.");
let config_raw = value.get("config").unwrap().as_table().unwrap();
let dots_raw = value.get("dots").unwrap().as_array().unwrap();
let config = Config::from_table(config_raw.clone());
let dots = dots_raw
.iter()
.map(|x| {
match DotfileRaw::from_table(x.as_table().unwrap().clone()).to_dotfile() {
Ok(dotfile) => dotfile,
Err(_) => panic!("An error has occurred parsing the `dartgun.toml` file. Please make sure it is in the correct format.")
}
})
.collect::<Vec<Dotfile>>();
Dartfile { config, dots }
}

18
src/dartgun.rs Normal file
View file

@ -0,0 +1,18 @@
/// Utilities for translating Dartfiles into actual actions on the system.
use crate::dartfile::{Dartfile, Dotfile};
use std::os::unix::fs::symlink;
impl Dotfile {
pub fn create_symlink(&self) -> Result<(), std::io::Error> {
symlink(self.location.canonicalize()?, &self.destination)
}
}
impl Dartfile {
pub fn create_symlinks(&self) -> Result<(), std::io::Error> {
for dot in self.dots.iter() {
dot.create_symlink()?
}
Ok(())
}
}

18
src/main.rs Normal file
View file

@ -0,0 +1,18 @@
use std::path::Path;
use dartfile::parse;
mod cli;
mod dartfile;
mod dartgun;
fn main() {
let test_path = Path::new("./dartgun.toml");
let test_dotfile = parse(test_path);
println!("{:?}", parse(test_path));
match test_dotfile.validate() {
Ok(_) => println!("Dotfile seems valid!"),
Err(_) => println!("Dotfile is invalid!"),
}
cli::run_cli();
}