aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorMax Bossing <info@maxbossing.de>2025-07-15 14:41:26 +0200
committerMax Bossing <info@maxbossing.de>2025-07-15 14:41:26 +0200
commit75ed52cb761d90b5e5a96717a4d00ef57003994d (patch)
treed4464a446d3324105274101fe7a72d81b658960e
init
-rw-r--r--.gitignore2
-rw-r--r--Cargo.lock319
-rw-r--r--Cargo.toml9
-rw-r--r--LICENSE13
-rw-r--r--README.md41
-rw-r--r--src/config.rs44
-rw-r--r--src/main.rs57
7 files changed, 485 insertions, 0 deletions
diff --git a/.gitignore b/.gitignore
new file mode 100644
index 0000000..e673575
--- /dev/null
+++ b/.gitignore
@@ -0,0 +1,2 @@
+.idea/
+target/ \ No newline at end of file
diff --git a/Cargo.lock b/Cargo.lock
new file mode 100644
index 0000000..3093ab3
--- /dev/null
+++ b/Cargo.lock
@@ -0,0 +1,319 @@
+# This file is automatically @generated by Cargo.
+# It is not intended for manual editing.
+version = 4
+
+[[package]]
+name = "bitflags"
+version = "2.9.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "1b8e56985ec62d17e9c1001dc89c88ecd7dc08e47eba5ec7c29c7b5eeecde967"
+
+[[package]]
+name = "cfg-if"
+version = "1.0.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "9555578bc9e57714c812a1f84e4fc5b4d21fcb063490c624de019f7464c91268"
+
+[[package]]
+name = "dirs"
+version = "6.0.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "c3e8aa94d75141228480295a7d0e7feb620b1a5ad9f12bc40be62411e38cce4e"
+dependencies = [
+ "dirs-sys",
+]
+
+[[package]]
+name = "dirs-sys"
+version = "0.5.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "e01a3366d27ee9890022452ee61b2b63a67e6f13f58900b651ff5665f0bb1fab"
+dependencies = [
+ "libc",
+ "option-ext",
+ "redox_users",
+ "windows-sys",
+]
+
+[[package]]
+name = "dots"
+version = "1.0.0"
+dependencies = [
+ "dirs",
+ "serde",
+ "toml",
+]
+
+[[package]]
+name = "equivalent"
+version = "1.0.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "877a4ace8713b0bcf2a4e7eec82529c029f1d0619886d18145fea96c3ffe5c0f"
+
+[[package]]
+name = "getrandom"
+version = "0.2.16"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "335ff9f135e4384c8150d6f27c6daed433577f86b4750418338c01a1a2528592"
+dependencies = [
+ "cfg-if",
+ "libc",
+ "wasi",
+]
+
+[[package]]
+name = "hashbrown"
+version = "0.15.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "5971ac85611da7067dbfcabef3c70ebb5606018acd9e2a3903a0da507521e0d5"
+
+[[package]]
+name = "indexmap"
+version = "2.10.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "fe4cd85333e22411419a0bcae1297d25e58c9443848b11dc6a86fefe8c78a661"
+dependencies = [
+ "equivalent",
+ "hashbrown",
+]
+
+[[package]]
+name = "libc"
+version = "0.2.174"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "1171693293099992e19cddea4e8b849964e9846f4acee11b3948bcc337be8776"
+
+[[package]]
+name = "libredox"
+version = "0.1.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "1580801010e535496706ba011c15f8532df6b42297d2e471fec38ceadd8c0638"
+dependencies = [
+ "bitflags",
+ "libc",
+]
+
+[[package]]
+name = "option-ext"
+version = "0.2.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "04744f49eae99ab78e0d5c0b603ab218f515ea8cfe5a456d7629ad883a3b6e7d"
+
+[[package]]
+name = "proc-macro2"
+version = "1.0.95"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "02b3e5e68a3a1a02aad3ec490a98007cbc13c37cbe84a3cd7b8e406d76e7f778"
+dependencies = [
+ "unicode-ident",
+]
+
+[[package]]
+name = "quote"
+version = "1.0.40"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "1885c039570dc00dcb4ff087a89e185fd56bae234ddc7f056a945bf36467248d"
+dependencies = [
+ "proc-macro2",
+]
+
+[[package]]
+name = "redox_users"
+version = "0.5.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "dd6f9d3d47bdd2ad6945c5015a226ec6155d0bcdfd8f7cd29f86b71f8de99d2b"
+dependencies = [
+ "getrandom",
+ "libredox",
+ "thiserror",
+]
+
+[[package]]
+name = "serde"
+version = "1.0.219"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "5f0e2c6ed6606019b4e29e69dbaba95b11854410e5347d525002456dbbb786b6"
+dependencies = [
+ "serde_derive",
+]
+
+[[package]]
+name = "serde_derive"
+version = "1.0.219"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "5b0276cf7f2c73365f7157c8123c21cd9a50fbbd844757af28ca1f5925fc2a00"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "syn",
+]
+
+[[package]]
+name = "serde_spanned"
+version = "1.0.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "40734c41988f7306bb04f0ecf60ec0f3f1caa34290e4e8ea471dcd3346483b83"
+dependencies = [
+ "serde",
+]
+
+[[package]]
+name = "syn"
+version = "2.0.104"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "17b6f705963418cdb9927482fa304bc562ece2fdd4f616084c50b7023b435a40"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "unicode-ident",
+]
+
+[[package]]
+name = "thiserror"
+version = "2.0.12"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "567b8a2dae586314f7be2a752ec7474332959c6460e02bde30d702a66d488708"
+dependencies = [
+ "thiserror-impl",
+]
+
+[[package]]
+name = "thiserror-impl"
+version = "2.0.12"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "7f7cf42b4507d8ea322120659672cf1b9dbb93f8f2d4ecfd6e51350ff5b17a1d"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "syn",
+]
+
+[[package]]
+name = "toml"
+version = "0.9.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "ed0aee96c12fa71097902e0bb061a5e1ebd766a6636bb605ba401c45c1650eac"
+dependencies = [
+ "indexmap",
+ "serde",
+ "serde_spanned",
+ "toml_datetime",
+ "toml_parser",
+ "toml_writer",
+ "winnow",
+]
+
+[[package]]
+name = "toml_datetime"
+version = "0.7.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "bade1c3e902f58d73d3f294cd7f20391c1cb2fbcb643b73566bc773971df91e3"
+dependencies = [
+ "serde",
+]
+
+[[package]]
+name = "toml_parser"
+version = "1.0.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "97200572db069e74c512a14117b296ba0a80a30123fbbb5aa1f4a348f639ca30"
+dependencies = [
+ "winnow",
+]
+
+[[package]]
+name = "toml_writer"
+version = "1.0.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "fcc842091f2def52017664b53082ecbbeb5c7731092bad69d2c63050401dfd64"
+
+[[package]]
+name = "unicode-ident"
+version = "1.0.18"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "5a5f39404a5da50712a4c1eecf25e90dd62b613502b7e925fd4e4d19b5c96512"
+
+[[package]]
+name = "wasi"
+version = "0.11.1+wasi-snapshot-preview1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "ccf3ec651a847eb01de73ccad15eb7d99f80485de043efb2f370cd654f4ea44b"
+
+[[package]]
+name = "windows-sys"
+version = "0.60.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "f2f500e4d28234f72040990ec9d39e3a6b950f9f22d3dba18416c35882612bcb"
+dependencies = [
+ "windows-targets",
+]
+
+[[package]]
+name = "windows-targets"
+version = "0.53.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "c66f69fcc9ce11da9966ddb31a40968cad001c5bedeb5c2b82ede4253ab48aef"
+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.53.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "86b8d5f90ddd19cb4a147a5fa63ca848db3df085e25fee3cc10b39b6eebae764"
+
+[[package]]
+name = "windows_aarch64_msvc"
+version = "0.53.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "c7651a1f62a11b8cbd5e0d42526e55f2c99886c77e007179efff86c2b137e66c"
+
+[[package]]
+name = "windows_i686_gnu"
+version = "0.53.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "c1dc67659d35f387f5f6c479dc4e28f1d4bb90ddd1a5d3da2e5d97b42d6272c3"
+
+[[package]]
+name = "windows_i686_gnullvm"
+version = "0.53.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "9ce6ccbdedbf6d6354471319e781c0dfef054c81fbc7cf83f338a4296c0cae11"
+
+[[package]]
+name = "windows_i686_msvc"
+version = "0.53.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "581fee95406bb13382d2f65cd4a908ca7b1e4c2f1917f143ba16efe98a589b5d"
+
+[[package]]
+name = "windows_x86_64_gnu"
+version = "0.53.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "2e55b5ac9ea33f2fc1716d1742db15574fd6fc8dadc51caab1c16a3d3b4190ba"
+
+[[package]]
+name = "windows_x86_64_gnullvm"
+version = "0.53.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "0a6e035dd0599267ce1ee132e51c27dd29437f63325753051e71dd9e42406c57"
+
+[[package]]
+name = "windows_x86_64_msvc"
+version = "0.53.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "271414315aff87387382ec3d271b52d7ae78726f5d44ac98b4f4030c91880486"
+
+[[package]]
+name = "winnow"
+version = "0.7.12"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "f3edebf492c8125044983378ecb5766203ad3b4c2f7a922bd7dd207f6d443e95"
diff --git a/Cargo.toml b/Cargo.toml
new file mode 100644
index 0000000..8f93c25
--- /dev/null
+++ b/Cargo.toml
@@ -0,0 +1,9 @@
+[package]
+name = "dots"
+version = "1.0.0"
+edition = "2024"
+
+[dependencies]
+dirs = "6.0.0"
+serde = { version = "1.0.219", features = ["derive"] }
+toml = "0.9.2"
diff --git a/LICENSE b/LICENSE
new file mode 100644
index 0000000..ef61788
--- /dev/null
+++ b/LICENSE
@@ -0,0 +1,13 @@
+Copyright © 2025 Max Bossing
+
+Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated
+documentation files (the “Software”), to deal in the Software without restriction, including without limitation
+the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and
+to permit persons to whom the Software is furnished to do so, subject to the following conditions:
+
+The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED “AS IS”, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE
+WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
+COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR
+OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. \ No newline at end of file
diff --git a/README.md b/README.md
new file mode 100644
index 0000000..8cadb89
--- /dev/null
+++ b/README.md
@@ -0,0 +1,41 @@
+If you are like me, you probably store your dotfiles in a git repo and symlink them into the right places. However, this
+always takes a considerable amount of time to do on a new system. In the past, I resorted to shell scripts to do my bidding,
+but that was often machine-specific, wouldn't allow me to configure multiple machines without significant hassle, and was
+generally a bit _clunky_.
+
+# The solution: `dots`
+
+`dots` is a machine-agnostic, fs-agnostic and structure-agnostic deployer for dotfiles stored in an arbitrary directory.
+Written in rust, it's :sparkles: _blazingly fast_ :sparkles: and easy to use.
+
+## dots.toml
+
+At the core, `dots` just symlinks arbitrary files and directories as specified by a configuration file. By default, `dots`
+will look for a `dots.toml` file in the current directory.
+
+Example `dots.toml`:
+```toml
+dots_dir = "/home/anon/dots" # If omitted, defaults to working directory
+
+[[dot]]
+src = "nvim" # Resolved against dots_dir
+dest = ".config/nvim" # Resolved against $HOME
+
+[[dot]]
+src = "fish"
+dest = ".config/fish"
+
+[[dot]]
+src = "tmux.conf"
+dest = ".tmux.conf"
+
+[[...]]
+```
+
+If you want to use a different file (for example a different machine but based on the same files), pass a path to the executable
+```bash
+dots some_file.dots.toml
+```
+
+## License
+This is licensed under the [MIT](LICENSE) license. \ No newline at end of file
diff --git a/src/config.rs b/src/config.rs
new file mode 100644
index 0000000..6fd790a
--- /dev/null
+++ b/src/config.rs
@@ -0,0 +1,44 @@
+use std::env;
+use std::path::PathBuf;
+use serde::Deserialize;
+
+#[derive(Deserialize, Debug)]
+pub struct Config {
+ #[serde(default = "default_dots_dir")]
+ pub dots_dir: PathBuf,
+ #[serde(rename = "dot")]
+ pub dots: Vec<Dot>,
+}
+
+#[derive(Deserialize, Debug)]
+pub struct Dot {
+ #[serde(rename = "src")]
+ pub source: PathBuf,
+ #[serde(rename = "dest")]
+ pub destination: PathBuf,
+}
+
+pub enum ConfigLoadError {
+ IOError(std::io::Error),
+ DeserializationError(toml::de::Error),
+}
+
+impl Config {
+ pub fn load(from: Option<PathBuf>) -> Result<Config, ConfigLoadError> {
+ let path = from.unwrap_or(PathBuf::from("dots.toml"));
+ match std::fs::read_to_string(path).map_err(ConfigLoadError::IOError) {
+ Ok(string) => match toml::from_str(&string) {
+ Ok(config) => Ok(config),
+ Err(err) => Err(ConfigLoadError::DeserializationError(err)),
+ }
+ Err(e) => {
+ Err(e)
+ }
+ }
+ }
+}
+
+fn default_dots_dir() -> PathBuf {
+ env::current_dir().expect("failed to get current dir")
+}
+
diff --git a/src/main.rs b/src/main.rs
new file mode 100644
index 0000000..151ee8a
--- /dev/null
+++ b/src/main.rs
@@ -0,0 +1,57 @@
+use std::env;
+use std::os::unix::fs::symlink;
+use std::path::PathBuf;
+use std::process::exit;
+use dirs::{home_dir};
+use crate::config::{Config, ConfigLoadError, Dot};
+
+mod config;
+
+fn main() {
+ let args = env::args().skip(1).collect::<Vec<String>>();
+
+ if args.len() > 1 {
+ eprintln!("usage: {} [config]", args[0]);
+ exit(1);
+ }
+
+ if args.len() == 1 && (args[0] == "-h" || args[0] == "--help") {
+ println!("Usage: {} [config]", args[0]);
+ println!("options:");
+ println!(" -h, --help Print help information");
+ println!("By default, dots will look for a bummsdots.toml file in the current directory");
+ println!("This can be changed by passing the filename");
+ exit(0);
+ }
+
+ let config = Config::load(args.get(0).map(|o| PathBuf::from(o))).map_err(|err|
+ match err {
+ ConfigLoadError::IOError(err) => {
+ eprintln!("failed to load config file: {}", err);
+ exit(1)
+ }
+ ConfigLoadError::DeserializationError(err) => {
+ eprintln!("failed to deserialize config file: {}", err);
+ exit(1)
+ }
+ }
+ ).unwrap();
+
+ let prepended_mappings = config.dots.iter().map(|m|
+ Dot {
+ source: env::current_dir().expect("failed to get current dir").join(&m.source),
+ destination: prepend_user_dir(&m.destination)
+ }
+ );
+
+ for dot in prepended_mappings {
+ println!("linking from {} to {}", dot.source.display(), dot.destination.display());
+ let _ = symlink(&dot.source, &dot.destination).map_err(|err|
+ eprintln!("failed to symlink: {}", err.to_string())
+ );
+ }
+}
+
+fn prepend_user_dir(path: &PathBuf) -> PathBuf {
+ home_dir().unwrap().join(path)
+} \ No newline at end of file