├── .config └── flakebox │ ├── .gitignore │ ├── bin │ └── flakebox-in-each-cargo-workspace │ ├── id │ └── shellHook.sh ├── .envrc ├── .github └── workflows │ ├── flakebox-ci.yml │ └── flakebox-flakehub-publish.yml ├── .gitignore ├── .rustfmt.toml ├── .travis.yml ├── Cargo.lock ├── Cargo.toml ├── Makefile ├── README.md ├── README.tpl ├── flake.lock ├── flake.nix ├── justfile ├── misc └── git-hooks │ ├── commit-msg │ ├── commit-template.txt │ └── pre-commit ├── rustfmt.toml └── src ├── lib.rs ├── main.rs ├── opts.rs └── tests.rs /.config/flakebox/.gitignore: -------------------------------------------------------------------------------- 1 | tmp/ 2 | -------------------------------------------------------------------------------- /.config/flakebox/bin/flakebox-in-each-cargo-workspace: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | # Run a given command in every directory that contains cargo workspace 3 | # Right now it just scans for `Cargo.lock` 4 | 5 | set -euo pipefail 6 | 7 | find . -name Cargo.lock | while read -r path ; do 8 | ( 9 | cd "$(dirname "$path")" 10 | "$@" 11 | ) 12 | done 13 | -------------------------------------------------------------------------------- /.config/flakebox/id: -------------------------------------------------------------------------------- 1 | 8a7d4cdb57ddb6ed90a5e3c149df05e9f7160a1219d1c2196b19699054d12e9240807e8d4c5517b15583cf5ee1474598506bc4aeab32f24ce799564f32ce915d 2 | -------------------------------------------------------------------------------- /.config/flakebox/shellHook.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | root="$(git rev-parse --show-toplevel)" 3 | dot_git="$(git rev-parse --git-common-dir)" 4 | if [[ ! -d "${dot_git}/hooks" ]]; then mkdir -p "${dot_git}/hooks"; fi 5 | # fix old bug 6 | rm -f "${dot_git}/hooks/comit-msg" 7 | rm -f "${dot_git}/hooks/commit-msg" 8 | ln -sf "${root}/misc/git-hooks/commit-msg" "${dot_git}/hooks/commit-msg" 9 | 10 | root="$(git rev-parse --show-toplevel)" 11 | dot_git="$(git rev-parse --git-common-dir)" 12 | if [[ ! -d "${dot_git}/hooks" ]]; then mkdir -p "${dot_git}/hooks"; fi 13 | # fix old bug 14 | rm -f "${dot_git}/hooks/pre-comit" 15 | rm -f "${dot_git}/hooks/pre-commit" 16 | ln -sf "${root}/misc/git-hooks/pre-commit" "${dot_git}/hooks/pre-commit" 17 | 18 | # set template 19 | git config commit.template misc/git-hooks/commit-template.txt 20 | 21 | if ! flakebox lint --silent; then 22 | >&2 echo "ℹ️ Project recommendations detected. Run 'flakebox lint' for more info." 23 | fi 24 | 25 | if [ -n "${DIRENV_IN_ENVRC:-}" ]; then 26 | # and not set DIRENV_LOG_FORMAT 27 | if [ -n "${DIRENV_LOG_FORMAT:-}" ]; then 28 | >&2 echo "💡 Set 'DIRENV_LOG_FORMAT=\"\"' in your shell environment variables for a cleaner output of direnv" 29 | fi 30 | fi 31 | 32 | >&2 echo "💡 Run 'just' for a list of available 'just ...' helper recipes" 33 | -------------------------------------------------------------------------------- /.envrc: -------------------------------------------------------------------------------- 1 | use flake 2 | -------------------------------------------------------------------------------- /.github/workflows/flakebox-ci.yml: -------------------------------------------------------------------------------- 1 | # THIS FILE IS AUTOGENERATED FROM FLAKEBOX CONFIGURATION 2 | 3 | jobs: 4 | build: 5 | name: Build 6 | runs-on: ${{ matrix.runs-on }} 7 | steps: 8 | - uses: actions/checkout@v4 9 | - name: Install Nix 10 | uses: DeterminateSystems/nix-installer-action@v4 11 | - name: Magic Nix Cache 12 | uses: DeterminateSystems/magic-nix-cache-action@v2 13 | - name: Build on ${{ matrix.host }} 14 | run: nix build .#ci.dotr 15 | strategy: 16 | matrix: 17 | host: 18 | - macos-x86_64 19 | - macos-aarch64 20 | - linux 21 | include: 22 | - host: linux 23 | runs-on: ubuntu-latest 24 | timeout: 60 25 | - host: macos-x86_64 26 | runs-on: macos-12 27 | timeout: 60 28 | - host: macos-aarch64 29 | runs-on: macos-14 30 | timeout: 60 31 | timeout-minutes: ${{ matrix.timeout }} 32 | flake: 33 | name: Flake self-check 34 | runs-on: ubuntu-latest 35 | steps: 36 | - uses: actions/checkout@v4 37 | - name: Check Nix flake inputs 38 | uses: DeterminateSystems/flake-checker-action@v5 39 | with: 40 | fail-mode: true 41 | lint: 42 | name: Lint 43 | runs-on: ubuntu-latest 44 | steps: 45 | - uses: actions/checkout@v4 46 | - name: Install Nix 47 | uses: DeterminateSystems/nix-installer-action@v4 48 | - name: Magic Nix Cache 49 | uses: DeterminateSystems/magic-nix-cache-action@v2 50 | - name: Cargo Cache 51 | uses: actions/cache@v3 52 | with: 53 | key: ${{ runner.os }}-${{ hashFiles('Cargo.lock') }} 54 | path: ~/.cargo 55 | - name: Commit Check 56 | run: '# run the same check that git `pre-commit` hook does 57 | 58 | nix develop --ignore-environment .#lint --command ./misc/git-hooks/pre-commit 59 | 60 | ' 61 | name: CI 62 | 'on': 63 | merge_group: 64 | branches: 65 | - master 66 | - main 67 | pull_request: 68 | branches: 69 | - master 70 | - main 71 | push: 72 | branches: 73 | - master 74 | - main 75 | tags: 76 | - v* 77 | workflow_dispatch: {} 78 | 79 | 80 | # THIS FILE IS AUTOGENERATED FROM FLAKEBOX CONFIGURATION 81 | -------------------------------------------------------------------------------- /.github/workflows/flakebox-flakehub-publish.yml: -------------------------------------------------------------------------------- 1 | # THIS FILE IS AUTOGENERATED FROM FLAKEBOX CONFIGURATION 2 | 3 | jobs: 4 | flakehub-publish: 5 | permissions: 6 | contents: read 7 | id-token: write 8 | runs-on: ubuntu-latest 9 | steps: 10 | - uses: actions/checkout@v4 11 | with: 12 | ref: ${{ (inputs.tag != null) && format('refs/tags/{0}', inputs.tag) || '' 13 | }} 14 | - name: Install Nix 15 | uses: DeterminateSystems/nix-installer-action@v4 16 | - name: Flakehub Push 17 | uses: DeterminateSystems/flakehub-push@main 18 | with: 19 | name: ${{ github.repository }} 20 | tag: ${{ inputs.tag }} 21 | visibility: public 22 | name: Publish to Flakehub 23 | 'on': 24 | push: 25 | tags: 26 | - v?[0-9]+.[0-9]+.[0-9]+* 27 | workflow_dispatch: 28 | inputs: 29 | tags: 30 | description: The existing tag to publish to FlakeHub 31 | required: true 32 | type: string 33 | 34 | 35 | # THIS FILE IS AUTOGENERATED FROM FLAKEBOX CONFIGURATION 36 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | /target/ 2 | **/*.rs.bk 3 | /rusty-tags.vi 4 | /test.src 5 | /test.dst 6 | /.direnv 7 | /result 8 | -------------------------------------------------------------------------------- /.rustfmt.toml: -------------------------------------------------------------------------------- 1 | group_imports = "StdExternalCrate" 2 | wrap_comments = true 3 | format_code_in_doc_comments = true 4 | imports_granularity = "Module" 5 | edition = "2021" 6 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: rust 2 | cache: cargo 3 | rust: 4 | - stable 5 | - 1.20.0 6 | - beta 7 | - nightly 8 | 9 | script: 10 | - export PATH=$PATH:~/.cargo/bin 11 | - make all 12 | 13 | env: 14 | global: 15 | - RUST_BACKTRACE=1 16 | - RUST_TEST_THREADS=1 17 | matrix: 18 | - 19 | - RELEASE=true 20 | 21 | notifications: 22 | webhooks: 23 | on_success: change # options: [always|never|change] default: always 24 | on_failure: always # options: [always|never|change] default: always 25 | on_start: false # default: false 26 | -------------------------------------------------------------------------------- /Cargo.lock: -------------------------------------------------------------------------------- 1 | # This file is automatically @generated by Cargo. 2 | # It is not intended for manual editing. 3 | version = 3 4 | 5 | [[package]] 6 | name = "aho-corasick" 7 | version = "1.1.2" 8 | source = "registry+https://github.com/rust-lang/crates.io-index" 9 | checksum = "b2969dcb958b36655471fc61f7e416fa76033bdd4bfed0678d8fee1e2d07a1f0" 10 | dependencies = [ 11 | "memchr", 12 | ] 13 | 14 | [[package]] 15 | name = "anstream" 16 | version = "0.6.12" 17 | source = "registry+https://github.com/rust-lang/crates.io-index" 18 | checksum = "96b09b5178381e0874812a9b157f7fe84982617e48f71f4e3235482775e5b540" 19 | dependencies = [ 20 | "anstyle", 21 | "anstyle-parse", 22 | "anstyle-query", 23 | "anstyle-wincon", 24 | "colorchoice", 25 | "utf8parse", 26 | ] 27 | 28 | [[package]] 29 | name = "anstyle" 30 | version = "1.0.6" 31 | source = "registry+https://github.com/rust-lang/crates.io-index" 32 | checksum = "8901269c6307e8d93993578286ac0edf7f195079ffff5ebdeea6a59ffb7e36bc" 33 | 34 | [[package]] 35 | name = "anstyle-parse" 36 | version = "0.2.3" 37 | source = "registry+https://github.com/rust-lang/crates.io-index" 38 | checksum = "c75ac65da39e5fe5ab759307499ddad880d724eed2f6ce5b5e8a26f4f387928c" 39 | dependencies = [ 40 | "utf8parse", 41 | ] 42 | 43 | [[package]] 44 | name = "anstyle-query" 45 | version = "1.0.2" 46 | source = "registry+https://github.com/rust-lang/crates.io-index" 47 | checksum = "e28923312444cdd728e4738b3f9c9cac739500909bb3d3c94b43551b16517648" 48 | dependencies = [ 49 | "windows-sys", 50 | ] 51 | 52 | [[package]] 53 | name = "anstyle-wincon" 54 | version = "3.0.2" 55 | source = "registry+https://github.com/rust-lang/crates.io-index" 56 | checksum = "1cd54b81ec8d6180e24654d0b371ad22fc3dd083b6ff8ba325b72e00c87660a7" 57 | dependencies = [ 58 | "anstyle", 59 | "windows-sys", 60 | ] 61 | 62 | [[package]] 63 | name = "anyhow" 64 | version = "1.0.80" 65 | source = "registry+https://github.com/rust-lang/crates.io-index" 66 | checksum = "5ad32ce52e4161730f7098c077cd2ed6229b5804ccf99e5366be1ab72a98b4e1" 67 | 68 | [[package]] 69 | name = "cfg-if" 70 | version = "1.0.0" 71 | source = "registry+https://github.com/rust-lang/crates.io-index" 72 | checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd" 73 | 74 | [[package]] 75 | name = "clap" 76 | version = "4.5.1" 77 | source = "registry+https://github.com/rust-lang/crates.io-index" 78 | checksum = "c918d541ef2913577a0f9566e9ce27cb35b6df072075769e0b26cb5a554520da" 79 | dependencies = [ 80 | "clap_builder", 81 | "clap_derive", 82 | ] 83 | 84 | [[package]] 85 | name = "clap-verbosity-flag" 86 | version = "2.2.0" 87 | source = "registry+https://github.com/rust-lang/crates.io-index" 88 | checksum = "bb9b20c0dd58e4c2e991c8d203bbeb76c11304d1011659686b5b644bc29aa478" 89 | dependencies = [ 90 | "clap", 91 | "log", 92 | ] 93 | 94 | [[package]] 95 | name = "clap_builder" 96 | version = "4.5.1" 97 | source = "registry+https://github.com/rust-lang/crates.io-index" 98 | checksum = "9f3e7391dad68afb0c2ede1bf619f579a3dc9c2ec67f089baa397123a2f3d1eb" 99 | dependencies = [ 100 | "anstream", 101 | "anstyle", 102 | "clap_lex", 103 | "strsim", 104 | ] 105 | 106 | [[package]] 107 | name = "clap_derive" 108 | version = "4.5.0" 109 | source = "registry+https://github.com/rust-lang/crates.io-index" 110 | checksum = "307bc0538d5f0f83b8248db3087aa92fe504e4691294d0c96c0eabc33f47ba47" 111 | dependencies = [ 112 | "heck", 113 | "proc-macro2", 114 | "quote", 115 | "syn", 116 | ] 117 | 118 | [[package]] 119 | name = "clap_lex" 120 | version = "0.7.0" 121 | source = "registry+https://github.com/rust-lang/crates.io-index" 122 | checksum = "98cc8fbded0c607b7ba9dd60cd98df59af97e84d24e49c8557331cfc26d301ce" 123 | 124 | [[package]] 125 | name = "colorchoice" 126 | version = "1.0.0" 127 | source = "registry+https://github.com/rust-lang/crates.io-index" 128 | checksum = "acbf1af155f9b9ef647e42cdc158db4b64a1b61f743629225fde6f3e0be2a7c7" 129 | 130 | [[package]] 131 | name = "dotr" 132 | version = "0.4.0" 133 | dependencies = [ 134 | "anyhow", 135 | "clap", 136 | "clap-verbosity-flag", 137 | "serde", 138 | "serde_derive", 139 | "tempdir", 140 | "toml", 141 | "tracing", 142 | "tracing-subscriber", 143 | "walkdir", 144 | ] 145 | 146 | [[package]] 147 | name = "fuchsia-cprng" 148 | version = "0.1.1" 149 | source = "registry+https://github.com/rust-lang/crates.io-index" 150 | checksum = "a06f77d526c1a601b7c4cdd98f54b5eaabffc14d5f2f0296febdc7f357c6d3ba" 151 | 152 | [[package]] 153 | name = "heck" 154 | version = "0.4.1" 155 | source = "registry+https://github.com/rust-lang/crates.io-index" 156 | checksum = "95505c38b4572b2d910cecb0281560f54b440a19336cbbcb27bf6ce6adc6f5a8" 157 | 158 | [[package]] 159 | name = "lazy_static" 160 | version = "1.4.0" 161 | source = "registry+https://github.com/rust-lang/crates.io-index" 162 | checksum = "e2abad23fbc42b3700f2f279844dc832adb2b2eb069b2df918f455c4e18cc646" 163 | 164 | [[package]] 165 | name = "libc" 166 | version = "0.2.153" 167 | source = "registry+https://github.com/rust-lang/crates.io-index" 168 | checksum = "9c198f91728a82281a64e1f4f9eeb25d82cb32a5de251c6bd1b5154d63a8e7bd" 169 | 170 | [[package]] 171 | name = "log" 172 | version = "0.4.20" 173 | source = "registry+https://github.com/rust-lang/crates.io-index" 174 | checksum = "b5e6163cb8c49088c2c36f57875e58ccd8c87c7427f7fbd50ea6710b2f3f2e8f" 175 | 176 | [[package]] 177 | name = "matchers" 178 | version = "0.1.0" 179 | source = "registry+https://github.com/rust-lang/crates.io-index" 180 | checksum = "8263075bb86c5a1b1427b5ae862e8889656f126e9f77c484496e8b47cf5c5558" 181 | dependencies = [ 182 | "regex-automata 0.1.10", 183 | ] 184 | 185 | [[package]] 186 | name = "memchr" 187 | version = "2.7.1" 188 | source = "registry+https://github.com/rust-lang/crates.io-index" 189 | checksum = "523dc4f511e55ab87b694dc30d0f820d60906ef06413f93d4d7a1385599cc149" 190 | 191 | [[package]] 192 | name = "nu-ansi-term" 193 | version = "0.46.0" 194 | source = "registry+https://github.com/rust-lang/crates.io-index" 195 | checksum = "77a8165726e8236064dbb45459242600304b42a5ea24ee2948e18e023bf7ba84" 196 | dependencies = [ 197 | "overload", 198 | "winapi", 199 | ] 200 | 201 | [[package]] 202 | name = "once_cell" 203 | version = "1.19.0" 204 | source = "registry+https://github.com/rust-lang/crates.io-index" 205 | checksum = "3fdb12b2476b595f9358c5161aa467c2438859caa136dec86c26fdd2efe17b92" 206 | 207 | [[package]] 208 | name = "overload" 209 | version = "0.1.1" 210 | source = "registry+https://github.com/rust-lang/crates.io-index" 211 | checksum = "b15813163c1d831bf4a13c3610c05c0d03b39feb07f7e09fa234dac9b15aaf39" 212 | 213 | [[package]] 214 | name = "pin-project-lite" 215 | version = "0.2.13" 216 | source = "registry+https://github.com/rust-lang/crates.io-index" 217 | checksum = "8afb450f006bf6385ca15ef45d71d2288452bc3683ce2e2cacc0d18e4be60b58" 218 | 219 | [[package]] 220 | name = "proc-macro2" 221 | version = "1.0.78" 222 | source = "registry+https://github.com/rust-lang/crates.io-index" 223 | checksum = "e2422ad645d89c99f8f3e6b88a9fdeca7fabeac836b1002371c4367c8f984aae" 224 | dependencies = [ 225 | "unicode-ident", 226 | ] 227 | 228 | [[package]] 229 | name = "quote" 230 | version = "1.0.35" 231 | source = "registry+https://github.com/rust-lang/crates.io-index" 232 | checksum = "291ec9ab5efd934aaf503a6466c5d5251535d108ee747472c3977cc5acc868ef" 233 | dependencies = [ 234 | "proc-macro2", 235 | ] 236 | 237 | [[package]] 238 | name = "rand" 239 | version = "0.4.6" 240 | source = "registry+https://github.com/rust-lang/crates.io-index" 241 | checksum = "552840b97013b1a26992c11eac34bdd778e464601a4c2054b5f0bff7c6761293" 242 | dependencies = [ 243 | "fuchsia-cprng", 244 | "libc", 245 | "rand_core 0.3.1", 246 | "rdrand", 247 | "winapi", 248 | ] 249 | 250 | [[package]] 251 | name = "rand_core" 252 | version = "0.3.1" 253 | source = "registry+https://github.com/rust-lang/crates.io-index" 254 | checksum = "7a6fdeb83b075e8266dcc8762c22776f6877a63111121f5f8c7411e5be7eed4b" 255 | dependencies = [ 256 | "rand_core 0.4.2", 257 | ] 258 | 259 | [[package]] 260 | name = "rand_core" 261 | version = "0.4.2" 262 | source = "registry+https://github.com/rust-lang/crates.io-index" 263 | checksum = "9c33a3c44ca05fa6f1807d8e6743f3824e8509beca625669633be0acbdf509dc" 264 | 265 | [[package]] 266 | name = "rdrand" 267 | version = "0.4.0" 268 | source = "registry+https://github.com/rust-lang/crates.io-index" 269 | checksum = "678054eb77286b51581ba43620cc911abf02758c91f93f479767aed0f90458b2" 270 | dependencies = [ 271 | "rand_core 0.3.1", 272 | ] 273 | 274 | [[package]] 275 | name = "regex" 276 | version = "1.10.3" 277 | source = "registry+https://github.com/rust-lang/crates.io-index" 278 | checksum = "b62dbe01f0b06f9d8dc7d49e05a0785f153b00b2c227856282f671e0318c9b15" 279 | dependencies = [ 280 | "aho-corasick", 281 | "memchr", 282 | "regex-automata 0.4.5", 283 | "regex-syntax 0.8.2", 284 | ] 285 | 286 | [[package]] 287 | name = "regex-automata" 288 | version = "0.1.10" 289 | source = "registry+https://github.com/rust-lang/crates.io-index" 290 | checksum = "6c230d73fb8d8c1b9c0b3135c5142a8acee3a0558fb8db5cf1cb65f8d7862132" 291 | dependencies = [ 292 | "regex-syntax 0.6.29", 293 | ] 294 | 295 | [[package]] 296 | name = "regex-automata" 297 | version = "0.4.5" 298 | source = "registry+https://github.com/rust-lang/crates.io-index" 299 | checksum = "5bb987efffd3c6d0d8f5f89510bb458559eab11e4f869acb20bf845e016259cd" 300 | dependencies = [ 301 | "aho-corasick", 302 | "memchr", 303 | "regex-syntax 0.8.2", 304 | ] 305 | 306 | [[package]] 307 | name = "regex-syntax" 308 | version = "0.6.29" 309 | source = "registry+https://github.com/rust-lang/crates.io-index" 310 | checksum = "f162c6dd7b008981e4d40210aca20b4bd0f9b60ca9271061b07f78537722f2e1" 311 | 312 | [[package]] 313 | name = "regex-syntax" 314 | version = "0.8.2" 315 | source = "registry+https://github.com/rust-lang/crates.io-index" 316 | checksum = "c08c74e62047bb2de4ff487b251e4a92e24f48745648451635cec7d591162d9f" 317 | 318 | [[package]] 319 | name = "remove_dir_all" 320 | version = "0.5.3" 321 | source = "registry+https://github.com/rust-lang/crates.io-index" 322 | checksum = "3acd125665422973a33ac9d3dd2df85edad0f4ae9b00dafb1a05e43a9f5ef8e7" 323 | dependencies = [ 324 | "winapi", 325 | ] 326 | 327 | [[package]] 328 | name = "same-file" 329 | version = "1.0.6" 330 | source = "registry+https://github.com/rust-lang/crates.io-index" 331 | checksum = "93fc1dc3aaa9bfed95e02e6eadabb4baf7e3078b0bd1b4d7b6b0b68378900502" 332 | dependencies = [ 333 | "winapi-util", 334 | ] 335 | 336 | [[package]] 337 | name = "serde" 338 | version = "1.0.196" 339 | source = "registry+https://github.com/rust-lang/crates.io-index" 340 | checksum = "870026e60fa08c69f064aa766c10f10b1d62db9ccd4d0abb206472bee0ce3b32" 341 | dependencies = [ 342 | "serde_derive", 343 | ] 344 | 345 | [[package]] 346 | name = "serde_derive" 347 | version = "1.0.196" 348 | source = "registry+https://github.com/rust-lang/crates.io-index" 349 | checksum = "33c85360c95e7d137454dc81d9a4ed2b8efd8fbe19cee57357b32b9771fccb67" 350 | dependencies = [ 351 | "proc-macro2", 352 | "quote", 353 | "syn", 354 | ] 355 | 356 | [[package]] 357 | name = "sharded-slab" 358 | version = "0.1.7" 359 | source = "registry+https://github.com/rust-lang/crates.io-index" 360 | checksum = "f40ca3c46823713e0d4209592e8d6e826aa57e928f09752619fc696c499637f6" 361 | dependencies = [ 362 | "lazy_static", 363 | ] 364 | 365 | [[package]] 366 | name = "smallvec" 367 | version = "1.13.1" 368 | source = "registry+https://github.com/rust-lang/crates.io-index" 369 | checksum = "e6ecd384b10a64542d77071bd64bd7b231f4ed5940fba55e98c3de13824cf3d7" 370 | 371 | [[package]] 372 | name = "strsim" 373 | version = "0.11.0" 374 | source = "registry+https://github.com/rust-lang/crates.io-index" 375 | checksum = "5ee073c9e4cd00e28217186dbe12796d692868f432bf2e97ee73bed0c56dfa01" 376 | 377 | [[package]] 378 | name = "syn" 379 | version = "2.0.49" 380 | source = "registry+https://github.com/rust-lang/crates.io-index" 381 | checksum = "915aea9e586f80826ee59f8453c1101f9d1c4b3964cd2460185ee8e299ada496" 382 | dependencies = [ 383 | "proc-macro2", 384 | "quote", 385 | "unicode-ident", 386 | ] 387 | 388 | [[package]] 389 | name = "tempdir" 390 | version = "0.3.7" 391 | source = "registry+https://github.com/rust-lang/crates.io-index" 392 | checksum = "15f2b5fb00ccdf689e0149d1b1b3c03fead81c2b37735d812fa8bddbbf41b6d8" 393 | dependencies = [ 394 | "rand", 395 | "remove_dir_all", 396 | ] 397 | 398 | [[package]] 399 | name = "thread_local" 400 | version = "1.1.7" 401 | source = "registry+https://github.com/rust-lang/crates.io-index" 402 | checksum = "3fdd6f064ccff2d6567adcb3873ca630700f00b5ad3f060c25b5dcfd9a4ce152" 403 | dependencies = [ 404 | "cfg-if", 405 | "once_cell", 406 | ] 407 | 408 | [[package]] 409 | name = "toml" 410 | version = "0.4.10" 411 | source = "registry+https://github.com/rust-lang/crates.io-index" 412 | checksum = "758664fc71a3a69038656bee8b6be6477d2a6c315a6b81f7081f591bffa4111f" 413 | dependencies = [ 414 | "serde", 415 | ] 416 | 417 | [[package]] 418 | name = "tracing" 419 | version = "0.1.40" 420 | source = "registry+https://github.com/rust-lang/crates.io-index" 421 | checksum = "c3523ab5a71916ccf420eebdf5521fcef02141234bbc0b8a49f2fdc4544364ef" 422 | dependencies = [ 423 | "pin-project-lite", 424 | "tracing-attributes", 425 | "tracing-core", 426 | ] 427 | 428 | [[package]] 429 | name = "tracing-attributes" 430 | version = "0.1.27" 431 | source = "registry+https://github.com/rust-lang/crates.io-index" 432 | checksum = "34704c8d6ebcbc939824180af020566b01a7c01f80641264eba0999f6c2b6be7" 433 | dependencies = [ 434 | "proc-macro2", 435 | "quote", 436 | "syn", 437 | ] 438 | 439 | [[package]] 440 | name = "tracing-core" 441 | version = "0.1.32" 442 | source = "registry+https://github.com/rust-lang/crates.io-index" 443 | checksum = "c06d3da6113f116aaee68e4d601191614c9053067f9ab7f6edbcb161237daa54" 444 | dependencies = [ 445 | "once_cell", 446 | "valuable", 447 | ] 448 | 449 | [[package]] 450 | name = "tracing-log" 451 | version = "0.2.0" 452 | source = "registry+https://github.com/rust-lang/crates.io-index" 453 | checksum = "ee855f1f400bd0e5c02d150ae5de3840039a3f54b025156404e34c23c03f47c3" 454 | dependencies = [ 455 | "log", 456 | "once_cell", 457 | "tracing-core", 458 | ] 459 | 460 | [[package]] 461 | name = "tracing-subscriber" 462 | version = "0.3.18" 463 | source = "registry+https://github.com/rust-lang/crates.io-index" 464 | checksum = "ad0f048c97dbd9faa9b7df56362b8ebcaa52adb06b498c050d2f4e32f90a7a8b" 465 | dependencies = [ 466 | "matchers", 467 | "nu-ansi-term", 468 | "once_cell", 469 | "regex", 470 | "sharded-slab", 471 | "smallvec", 472 | "thread_local", 473 | "tracing", 474 | "tracing-core", 475 | "tracing-log", 476 | ] 477 | 478 | [[package]] 479 | name = "unicode-ident" 480 | version = "1.0.12" 481 | source = "registry+https://github.com/rust-lang/crates.io-index" 482 | checksum = "3354b9ac3fae1ff6755cb6db53683adb661634f67557942dea4facebec0fee4b" 483 | 484 | [[package]] 485 | name = "utf8parse" 486 | version = "0.2.1" 487 | source = "registry+https://github.com/rust-lang/crates.io-index" 488 | checksum = "711b9620af191e0cdc7468a8d14e709c3dcdb115b36f838e601583af800a370a" 489 | 490 | [[package]] 491 | name = "valuable" 492 | version = "0.1.0" 493 | source = "registry+https://github.com/rust-lang/crates.io-index" 494 | checksum = "830b7e5d4d90034032940e4ace0d9a9a057e7a45cd94e6c007832e39edb82f6d" 495 | 496 | [[package]] 497 | name = "walkdir" 498 | version = "2.4.0" 499 | source = "registry+https://github.com/rust-lang/crates.io-index" 500 | checksum = "d71d857dc86794ca4c280d616f7da00d2dbfd8cd788846559a6813e6aa4b54ee" 501 | dependencies = [ 502 | "same-file", 503 | "winapi-util", 504 | ] 505 | 506 | [[package]] 507 | name = "winapi" 508 | version = "0.3.9" 509 | source = "registry+https://github.com/rust-lang/crates.io-index" 510 | checksum = "5c839a674fcd7a98952e593242ea400abe93992746761e38641405d28b00f419" 511 | dependencies = [ 512 | "winapi-i686-pc-windows-gnu", 513 | "winapi-x86_64-pc-windows-gnu", 514 | ] 515 | 516 | [[package]] 517 | name = "winapi-i686-pc-windows-gnu" 518 | version = "0.4.0" 519 | source = "registry+https://github.com/rust-lang/crates.io-index" 520 | checksum = "ac3b87c63620426dd9b991e5ce0329eff545bccbbb34f3be09ff6fb6ab51b7b6" 521 | 522 | [[package]] 523 | name = "winapi-util" 524 | version = "0.1.6" 525 | source = "registry+https://github.com/rust-lang/crates.io-index" 526 | checksum = "f29e6f9198ba0d26b4c9f07dbe6f9ed633e1f3d5b8b414090084349e46a52596" 527 | dependencies = [ 528 | "winapi", 529 | ] 530 | 531 | [[package]] 532 | name = "winapi-x86_64-pc-windows-gnu" 533 | version = "0.4.0" 534 | source = "registry+https://github.com/rust-lang/crates.io-index" 535 | checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f" 536 | 537 | [[package]] 538 | name = "windows-sys" 539 | version = "0.52.0" 540 | source = "registry+https://github.com/rust-lang/crates.io-index" 541 | checksum = "282be5f36a8ce781fad8c8ae18fa3f9beff57ec1b52cb3de0789201425d9a33d" 542 | dependencies = [ 543 | "windows-targets", 544 | ] 545 | 546 | [[package]] 547 | name = "windows-targets" 548 | version = "0.52.0" 549 | source = "registry+https://github.com/rust-lang/crates.io-index" 550 | checksum = "8a18201040b24831fbb9e4eb208f8892e1f50a37feb53cc7ff887feb8f50e7cd" 551 | dependencies = [ 552 | "windows_aarch64_gnullvm", 553 | "windows_aarch64_msvc", 554 | "windows_i686_gnu", 555 | "windows_i686_msvc", 556 | "windows_x86_64_gnu", 557 | "windows_x86_64_gnullvm", 558 | "windows_x86_64_msvc", 559 | ] 560 | 561 | [[package]] 562 | name = "windows_aarch64_gnullvm" 563 | version = "0.52.0" 564 | source = "registry+https://github.com/rust-lang/crates.io-index" 565 | checksum = "cb7764e35d4db8a7921e09562a0304bf2f93e0a51bfccee0bd0bb0b666b015ea" 566 | 567 | [[package]] 568 | name = "windows_aarch64_msvc" 569 | version = "0.52.0" 570 | source = "registry+https://github.com/rust-lang/crates.io-index" 571 | checksum = "bbaa0368d4f1d2aaefc55b6fcfee13f41544ddf36801e793edbbfd7d7df075ef" 572 | 573 | [[package]] 574 | name = "windows_i686_gnu" 575 | version = "0.52.0" 576 | source = "registry+https://github.com/rust-lang/crates.io-index" 577 | checksum = "a28637cb1fa3560a16915793afb20081aba2c92ee8af57b4d5f28e4b3e7df313" 578 | 579 | [[package]] 580 | name = "windows_i686_msvc" 581 | version = "0.52.0" 582 | source = "registry+https://github.com/rust-lang/crates.io-index" 583 | checksum = "ffe5e8e31046ce6230cc7215707b816e339ff4d4d67c65dffa206fd0f7aa7b9a" 584 | 585 | [[package]] 586 | name = "windows_x86_64_gnu" 587 | version = "0.52.0" 588 | source = "registry+https://github.com/rust-lang/crates.io-index" 589 | checksum = "3d6fa32db2bc4a2f5abeacf2b69f7992cd09dca97498da74a151a3132c26befd" 590 | 591 | [[package]] 592 | name = "windows_x86_64_gnullvm" 593 | version = "0.52.0" 594 | source = "registry+https://github.com/rust-lang/crates.io-index" 595 | checksum = "1a657e1e9d3f514745a572a6846d3c7aa7dbe1658c056ed9c3344c4109a6949e" 596 | 597 | [[package]] 598 | name = "windows_x86_64_msvc" 599 | version = "0.52.0" 600 | source = "registry+https://github.com/rust-lang/crates.io-index" 601 | checksum = "dff9641d1cd4be8d1a070daf9e3773c5f67e78b4d9d42263020c057706765c04" 602 | -------------------------------------------------------------------------------- /Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | authors = ["Dawid Ciężarkiewicz "] 3 | description = "Very simple dotfile manager" 4 | documentation = "https://docs.rs/dotr/" 5 | homepage = "https://github.com/dpc/dotr" 6 | keywords = ["rc", "dot", "dotfile"] 7 | license = "MPL-2.0" 8 | name = "dotr" 9 | readme = "README.md" 10 | repository = "https://github.com/dpc/dotr" 11 | version = "0.4.0" 12 | edition = '2018' 13 | 14 | 15 | [dependencies] 16 | clap = { version = "4", features = ["derive", "env"] } 17 | serde = { version = "1", features = ["derive"] } 18 | serde_derive = "1" 19 | tempdir = "*" 20 | toml = "0.4" 21 | walkdir = "2" 22 | tracing = "*" 23 | clap-verbosity-flag = "2.2.0" 24 | tracing-subscriber = { version = "0.3.18", features = ["env-filter", "fmt"] } 25 | anyhow = "1.0.80" 26 | 27 | [profile] 28 | 29 | [profile.ci] 30 | inherits = "dev" 31 | incremental = false 32 | debug = "line-tables-only" 33 | lto = "off" 34 | 35 | [profile.release] 36 | strip = true 37 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | PKG_NAME=$(shell grep name Cargo.toml | head -n 1 | awk -F \" '{print $$2}') 2 | DOCS_DEFAULT_MODULE=$(PKG_NAME) 3 | ifeq (, $(shell which cargo-check 2> /dev/null)) 4 | DEFAULT_TARGET=build 5 | else 6 | DEFAULT_TARGET=build 7 | endif 8 | 9 | default: $(DEFAULT_TARGET) 10 | 11 | ALL_TARGETS += build $(EXAMPLES) test doc 12 | ifneq ($(RELEASE),) 13 | $(info RELEASE BUILD: $(PKG_NAME)) 14 | CARGO_FLAGS += --release 15 | ALL_TARGETS += bench 16 | else 17 | $(info DEBUG BUILD: $(PKG_NAME); use `RELEASE=true make [args]` for release build) 18 | endif 19 | 20 | EXAMPLES = $(shell cd examples 2>/dev/null && ls *.rs 2>/dev/null | sed -e 's/.rs$$//g' ) 21 | 22 | all: $(ALL_TARGETS) 23 | 24 | .PHONY: run test build doc clean clippy 25 | run test build clean: 26 | cargo $@ $(CARGO_FLAGS) 27 | 28 | check: 29 | $(info Running check; use `make build` to actually build) 30 | cargo $@ $(CARGO_FLAGS) 31 | 32 | clippy: 33 | cargo build --features clippy 34 | 35 | .PHONY: bench 36 | bench: 37 | cargo $@ $(filter-out --release,$(CARGO_FLAGS)) 38 | 39 | .PHONY: travistest 40 | travistest: 41 | for i in `seq 1`; do make test || exit 1 ; done 42 | 43 | .PHONY: readme 44 | readme: README.md 45 | 46 | README.md: README.tpl src/bin.rs 47 | cargo readme > README.md 48 | 49 | .PHONY: longtest 50 | longtest: 51 | @echo "Running longtest. Press Ctrl+C to stop at any time" 52 | @sleep 2 53 | @i=0; while i=$$((i + 1)) && echo "Iteration $$i" && make test ; do :; done 54 | 55 | .PHONY: $(EXAMPLES) 56 | $(EXAMPLES): 57 | cargo build --example $@ $(CARGO_FLAGS) 58 | 59 | .PHONY: doc 60 | doc: FORCE 61 | rm -rf target/doc 62 | cargo doc 63 | 64 | .PHONY: publishdoc 65 | publishdoc: doc 66 | echo '' > target/doc/index.html 67 | ghp-import -n target/doc 68 | git push -f origin gh-pages 69 | 70 | .PHONY: docview 71 | docview: doc 72 | xdg-open target/doc/$(PKG_NAME)/index.html 73 | 74 | .PHONY: FORCE 75 | FORCE: 76 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | 2 | 3 |

4 | 5 | Travis CI Build Status 6 | 7 | 8 | crates.io 9 | 10 | 11 | Gitter Chat 12 | 13 |
14 |

15 | 16 | # dotr 17 | 18 | See [wiki](https://github.com/dpc/dotr/wiki) for current project status. 19 | 20 | `dotr` is a very simple dotfile manager 21 | 22 | It supports `link` and `unlink` operations and couple 23 | of basic flags like `force`. 24 | 25 | I wrote it for myself, so it's in Rust and does exactly what I want, so I 26 | can fix/customize if I need something. But hey, maybe it also does 27 | exactly what you want too! 28 | 29 | #### Installation: 30 | 31 | * [Install Rust](https://www.rustup.rs/) 32 | 33 | ```norust 34 | cargo install dotr 35 | ``` 36 | 37 | #### Usage: 38 | 39 | ```norust 40 | dotr help 41 | ``` 42 | 43 | #### Ignoring files: 44 | 45 | `dotr` can skip some of the files in the source directory. To configure that, 46 | create a file called `dotr.toml` with an `ignore` key set to an array of 47 | files to be excluded: 48 | 49 | ```toml 50 | ignore = ["LICENSE", "user.js"] 51 | ``` 52 | 53 | The `dotr.toml` file will be loaded, if present, from the source directory. 54 | 55 | #### TODO: 56 | 57 | * Make it a separate library + binary 58 | 59 | # License 60 | 61 | dotr is licensed under: MPL-2.0 62 | -------------------------------------------------------------------------------- /README.tpl: -------------------------------------------------------------------------------- 1 | 2 | 3 |

4 | 5 | Travis CI Build Status 6 | 7 | 8 | crates.io 9 | 10 | 11 | Gitter Chat 12 | 13 |
14 |

15 | 16 | # {{crate}} 17 | 18 | See [wiki](https://github.com/dpc/dotr/wiki) for current project status. 19 | 20 | {{readme}} 21 | 22 | # License 23 | 24 | {{crate}} is licensed under: {{license}} 25 | -------------------------------------------------------------------------------- /flake.lock: -------------------------------------------------------------------------------- 1 | { 2 | "nodes": { 3 | "android-nixpkgs": { 4 | "inputs": { 5 | "devshell": "devshell", 6 | "flake-utils": "flake-utils_2", 7 | "nixpkgs": [ 8 | "flakebox", 9 | "nixpkgs" 10 | ] 11 | }, 12 | "locked": { 13 | "lastModified": 1695500413, 14 | "narHash": "sha256-yinrAWIc4XZbWQoXOYkUO0lCNQ5z/vMyl+QCYuIwdPc=", 15 | "owner": "dpc", 16 | "repo": "android-nixpkgs", 17 | "rev": "2e42268a196375ce9b010a10ec5250d2f91a09b4", 18 | "type": "github" 19 | }, 20 | "original": { 21 | "owner": "dpc", 22 | "repo": "android-nixpkgs", 23 | "rev": "2e42268a196375ce9b010a10ec5250d2f91a09b4", 24 | "type": "github" 25 | } 26 | }, 27 | "crane": { 28 | "inputs": { 29 | "nixpkgs": [ 30 | "flakebox", 31 | "nixpkgs" 32 | ] 33 | }, 34 | "locked": { 35 | "lastModified": 1699217310, 36 | "narHash": "sha256-xpW3VFUG7yE6UE6Wl0dhqencuENSkV7qpnpe9I8VbPw=", 37 | "owner": "ipetkov", 38 | "repo": "crane", 39 | "rev": "d535642bbe6f377077f7c23f0febb78b1463f449", 40 | "type": "github" 41 | }, 42 | "original": { 43 | "owner": "ipetkov", 44 | "repo": "crane", 45 | "rev": "d535642bbe6f377077f7c23f0febb78b1463f449", 46 | "type": "github" 47 | } 48 | }, 49 | "devshell": { 50 | "inputs": { 51 | "nixpkgs": [ 52 | "flakebox", 53 | "android-nixpkgs", 54 | "nixpkgs" 55 | ], 56 | "systems": "systems" 57 | }, 58 | "locked": { 59 | "lastModified": 1695195896, 60 | "narHash": "sha256-pq9q7YsGXnQzJFkR5284TmxrLNFc0wo4NQ/a5E93CQU=", 61 | "owner": "numtide", 62 | "repo": "devshell", 63 | "rev": "05d40d17bf3459606316e3e9ec683b784ff28f16", 64 | "type": "github" 65 | }, 66 | "original": { 67 | "owner": "numtide", 68 | "repo": "devshell", 69 | "type": "github" 70 | } 71 | }, 72 | "fenix": { 73 | "inputs": { 74 | "nixpkgs": [ 75 | "flakebox", 76 | "nixpkgs" 77 | ], 78 | "rust-analyzer-src": "rust-analyzer-src" 79 | }, 80 | "locked": { 81 | "lastModified": 1706941198, 82 | "narHash": "sha256-t6/qloMYdknVJ9a3QzjylQIZnQfgefJ5kMim50B7dwA=", 83 | "owner": "nix-community", 84 | "repo": "fenix", 85 | "rev": "28dbd8b43ea328ee708f7da538c63e03d5ed93c8", 86 | "type": "github" 87 | }, 88 | "original": { 89 | "owner": "nix-community", 90 | "repo": "fenix", 91 | "type": "github" 92 | } 93 | }, 94 | "flake-utils": { 95 | "locked": { 96 | "lastModified": 1623875721, 97 | "narHash": "sha256-A8BU7bjS5GirpAUv4QA+QnJ4CceLHkcXdRp4xITDB0s=", 98 | "owner": "numtide", 99 | "repo": "flake-utils", 100 | "rev": "f7e004a55b120c02ecb6219596820fcd32ca8772", 101 | "type": "github" 102 | }, 103 | "original": { 104 | "owner": "numtide", 105 | "repo": "flake-utils", 106 | "type": "github" 107 | } 108 | }, 109 | "flake-utils_2": { 110 | "inputs": { 111 | "systems": "systems_2" 112 | }, 113 | "locked": { 114 | "lastModified": 1694529238, 115 | "narHash": "sha256-zsNZZGTGnMOf9YpHKJqMSsa0dXbfmxeoJ7xHlrt+xmY=", 116 | "owner": "numtide", 117 | "repo": "flake-utils", 118 | "rev": "ff7b65b44d01cf9ba6a71320833626af21126384", 119 | "type": "github" 120 | }, 121 | "original": { 122 | "owner": "numtide", 123 | "repo": "flake-utils", 124 | "type": "github" 125 | } 126 | }, 127 | "flake-utils_3": { 128 | "inputs": { 129 | "systems": [ 130 | "flakebox", 131 | "systems" 132 | ] 133 | }, 134 | "locked": { 135 | "lastModified": 1705309234, 136 | "narHash": "sha256-uNRRNRKmJyCRC/8y1RqBkqWBLM034y4qN7EprSdmgyA=", 137 | "owner": "numtide", 138 | "repo": "flake-utils", 139 | "rev": "1ef2e671c3b0c19053962c07dbda38332dcebf26", 140 | "type": "github" 141 | }, 142 | "original": { 143 | "owner": "numtide", 144 | "repo": "flake-utils", 145 | "type": "github" 146 | } 147 | }, 148 | "flakebox": { 149 | "inputs": { 150 | "android-nixpkgs": "android-nixpkgs", 151 | "crane": "crane", 152 | "fenix": "fenix", 153 | "flake-utils": "flake-utils_3", 154 | "nixpkgs": "nixpkgs", 155 | "systems": "systems_3" 156 | }, 157 | "locked": { 158 | "lastModified": 1707423208, 159 | "narHash": "sha256-iyivXUcdOel8QQVqCLiZ34in86uJRYrGKVRFB0HK2S4=", 160 | "owner": "rustshop", 161 | "repo": "flakebox", 162 | "rev": "cd7cc8aabdb555b0b01233c727d76257cbd9c130", 163 | "type": "github" 164 | }, 165 | "original": { 166 | "owner": "rustshop", 167 | "repo": "flakebox", 168 | "type": "github" 169 | } 170 | }, 171 | "nixpkgs": { 172 | "locked": { 173 | "lastModified": 1706826059, 174 | "narHash": "sha256-N69Oab+cbt3flLvYv8fYnEHlBsWwdKciNZHUbynVEOA=", 175 | "owner": "nixos", 176 | "repo": "nixpkgs", 177 | "rev": "25e3d4c0d3591c99929b1ec07883177f6ea70c9d", 178 | "type": "github" 179 | }, 180 | "original": { 181 | "owner": "nixos", 182 | "ref": "nixos-23.11", 183 | "repo": "nixpkgs", 184 | "type": "github" 185 | } 186 | }, 187 | "root": { 188 | "inputs": { 189 | "flake-utils": "flake-utils", 190 | "flakebox": "flakebox" 191 | } 192 | }, 193 | "rust-analyzer-src": { 194 | "flake": false, 195 | "locked": { 196 | "lastModified": 1706875368, 197 | "narHash": "sha256-KOBXxNurIU2lEmO6lR2A5El32X9x8ITt25McxKZ/Ew0=", 198 | "owner": "rust-lang", 199 | "repo": "rust-analyzer", 200 | "rev": "8f6a72871ec87ed53cfe43a09fb284168a284e7e", 201 | "type": "github" 202 | }, 203 | "original": { 204 | "owner": "rust-lang", 205 | "ref": "nightly", 206 | "repo": "rust-analyzer", 207 | "type": "github" 208 | } 209 | }, 210 | "systems": { 211 | "locked": { 212 | "lastModified": 1681028828, 213 | "narHash": "sha256-Vy1rq5AaRuLzOxct8nz4T6wlgyUR7zLU309k9mBC768=", 214 | "owner": "nix-systems", 215 | "repo": "default", 216 | "rev": "da67096a3b9bf56a91d16901293e51ba5b49a27e", 217 | "type": "github" 218 | }, 219 | "original": { 220 | "owner": "nix-systems", 221 | "repo": "default", 222 | "type": "github" 223 | } 224 | }, 225 | "systems_2": { 226 | "locked": { 227 | "lastModified": 1681028828, 228 | "narHash": "sha256-Vy1rq5AaRuLzOxct8nz4T6wlgyUR7zLU309k9mBC768=", 229 | "owner": "nix-systems", 230 | "repo": "default", 231 | "rev": "da67096a3b9bf56a91d16901293e51ba5b49a27e", 232 | "type": "github" 233 | }, 234 | "original": { 235 | "owner": "nix-systems", 236 | "repo": "default", 237 | "type": "github" 238 | } 239 | }, 240 | "systems_3": { 241 | "locked": { 242 | "lastModified": 1681028828, 243 | "narHash": "sha256-Vy1rq5AaRuLzOxct8nz4T6wlgyUR7zLU309k9mBC768=", 244 | "owner": "nix-systems", 245 | "repo": "default", 246 | "rev": "da67096a3b9bf56a91d16901293e51ba5b49a27e", 247 | "type": "github" 248 | }, 249 | "original": { 250 | "owner": "nix-systems", 251 | "repo": "default", 252 | "type": "github" 253 | } 254 | } 255 | }, 256 | "root": "root", 257 | "version": 7 258 | } 259 | -------------------------------------------------------------------------------- /flake.nix: -------------------------------------------------------------------------------- 1 | { 2 | description = "A very basic flake"; 3 | 4 | inputs = { 5 | flake-utils.url = "github:numtide/flake-utils"; 6 | flakebox.url = "github:rustshop/flakebox"; 7 | }; 8 | 9 | outputs = { self, flakebox, flake-utils }: 10 | flake-utils.lib.eachDefaultSystem (system: 11 | let 12 | pkgs = flakebox.inputs.nixpkgs.legacyPackages.${system}; 13 | 14 | lib = pkgs.lib; 15 | fs = lib.fileset; 16 | 17 | projectName = "dotr"; 18 | 19 | flakeboxLib = flakebox.lib.${system} { 20 | config = { 21 | github.ci.buildOutputs = [ ".#ci.${projectName}" ]; 22 | }; 23 | }; 24 | 25 | srcFileset = fs.unions [ 26 | ./Cargo.toml 27 | ./Cargo.lock 28 | ./src 29 | ]; 30 | 31 | 32 | multiBuild = 33 | (flakeboxLib.craneMultiBuild { }) (craneLib': 34 | let 35 | craneLib = (craneLib'.overrideArgs { 36 | pname = projectName; 37 | src = fs.toSource { 38 | root = ./.; 39 | fileset = srcFileset; 40 | }; 41 | }); 42 | in 43 | { 44 | "${projectName}" = craneLib.buildPackage { }; 45 | }); 46 | in 47 | { 48 | packages = { 49 | default = multiBuild.dotr; 50 | }; 51 | legacyPackages = multiBuild; 52 | 53 | devShells = flakeboxLib.mkShells { }; 54 | }); 55 | } 56 | -------------------------------------------------------------------------------- /justfile: -------------------------------------------------------------------------------- 1 | # THIS FILE IS AUTOGENERATED FROM FLAKEBOX CONFIGURATION 2 | 3 | alias b := build 4 | alias c := check 5 | alias t := test 6 | 7 | 8 | [private] 9 | default: 10 | @just --list 11 | 12 | 13 | # run `cargo build` on everything 14 | build *ARGS="--workspace --all-targets": 15 | #!/usr/bin/env bash 16 | set -euo pipefail 17 | if [ ! -f Cargo.toml ]; then 18 | cd {{invocation_directory()}} 19 | fi 20 | cargo build {{ARGS}} 21 | 22 | 23 | # run `cargo check` on everything 24 | check *ARGS="--workspace --all-targets": 25 | #!/usr/bin/env bash 26 | set -euo pipefail 27 | if [ ! -f Cargo.toml ]; then 28 | cd {{invocation_directory()}} 29 | fi 30 | cargo check {{ARGS}} 31 | 32 | 33 | # run all checks recommended before opening a PR 34 | final-check: lint clippy 35 | #!/usr/bin/env bash 36 | set -euo pipefail 37 | if [ ! -f Cargo.toml ]; then 38 | cd {{invocation_directory()}} 39 | fi 40 | cargo test --doc 41 | just test 42 | 43 | 44 | # run code formatters 45 | format: 46 | #!/usr/bin/env bash 47 | set -euo pipefail 48 | if [ ! -f Cargo.toml ]; then 49 | cd {{invocation_directory()}} 50 | fi 51 | cargo fmt --all 52 | nixpkgs-fmt $(echo **.nix) 53 | 54 | 55 | # run lints (git pre-commit hook) 56 | lint: 57 | #!/usr/bin/env bash 58 | set -euo pipefail 59 | env NO_STASH=true $(git rev-parse --git-common-dir)/hooks/pre-commit 60 | 61 | 62 | # run tests 63 | test: build 64 | #!/usr/bin/env bash 65 | set -euo pipefail 66 | if [ ! -f Cargo.toml ]; then 67 | cd {{invocation_directory()}} 68 | fi 69 | cargo test 70 | 71 | 72 | # run and restart on changes 73 | watch *ARGS="-x run": 74 | #!/usr/bin/env bash 75 | set -euo pipefail 76 | if [ ! -f Cargo.toml ]; then 77 | cd {{invocation_directory()}} 78 | fi 79 | env RUST_LOG=${RUST_LOG:-debug} cargo watch {{ARGS}} 80 | 81 | 82 | # run `cargo clippy` on everything 83 | clippy *ARGS="--locked --offline --workspace --all-targets": 84 | cargo clippy {{ARGS}} -- --deny warnings --allow deprecated 85 | 86 | # run `cargo clippy --fix` on everything 87 | clippy-fix *ARGS="--locked --offline --workspace --all-targets": 88 | cargo clippy {{ARGS}} --fix 89 | 90 | 91 | # run `semgrep` 92 | semgrep: 93 | env SEMGREP_ENABLE_VERSION_CHECK=0 \ 94 | semgrep --error --no-rewrite-rule-ids --config .config/semgrep.yaml 95 | 96 | 97 | # check typos 98 | [no-exit-message] 99 | typos *PARAMS: 100 | #!/usr/bin/env bash 101 | set -eo pipefail 102 | 103 | export FLAKEBOX_GIT_LS 104 | FLAKEBOX_GIT_LS="$(git ls-files)" 105 | export FLAKEBOX_GIT_LS_TEXT 106 | FLAKEBOX_GIT_LS_TEXT="$(echo "$FLAKEBOX_GIT_LS" | grep -v -E "^db/|\.(png|ods|jpg|jpeg|woff2|keystore|wasm|ttf|jar|ico)\$")" 107 | 108 | 109 | if ! echo "$FLAKEBOX_GIT_LS_TEXT" | typos {{PARAMS}} --file-list - --force-exclude ; then 110 | >&2 echo "Typos found: Valid new words can be added to '.typos.toml'" 111 | return 1 112 | fi 113 | 114 | # fix all typos 115 | [no-exit-message] 116 | typos-fix-all: 117 | just typos -w 118 | 119 | # THIS FILE IS AUTOGENERATED FROM FLAKEBOX CONFIGURATION 120 | -------------------------------------------------------------------------------- /misc/git-hooks/commit-msg: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | # Sanitize file first, by removing leading lines that are empty or start with a hash, 3 | # as `convco` currently does not do it automatically (but git will) 4 | # TODO: next release of convco should be able to do it automatically 5 | MESSAGE="$( 6 | while read -r line ; do 7 | # skip any initial comments (possibly from previous run) 8 | if [ -z "${body_detected:-}" ] && { [[ "$line" =~ ^#.*$ ]] || [ "$line" == "" ]; }; then 9 | continue 10 | fi 11 | body_detected="true" 12 | 13 | echo "$line" 14 | done < "$1" 15 | )" 16 | 17 | # convco fails on fixup!, so remove fixup! prefix 18 | MESSAGE="${MESSAGE#fixup! }" 19 | if ! convco check --from-stdin <<<"$MESSAGE" ; then 20 | >&2 echo "Please follow conventional commits(https://www.conventionalcommits.org)" 21 | >&2 echo "Use git recommit to fix your commit" 22 | exit 1 23 | fi 24 | -------------------------------------------------------------------------------- /misc/git-hooks/commit-template.txt: -------------------------------------------------------------------------------- 1 | 2 | # Explain *why* this change is being made width limit ->| 3 | -------------------------------------------------------------------------------- /misc/git-hooks/pre-commit: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | set -euo pipefail 4 | 5 | set +e 6 | git diff-files --quiet 7 | is_unclean=$? 8 | set -e 9 | 10 | # Revert `git stash` on exit 11 | function revert_git_stash { 12 | >&2 echo "Unstashing uncommitted changes..." 13 | git stash pop -q 14 | } 15 | 16 | # Stash pending changes and revert them when script ends 17 | if [ -z "${NO_STASH:-}" ] && [ $is_unclean -ne 0 ]; then 18 | >&2 echo "Stashing uncommitted changes..." 19 | GIT_LITERAL_PATHSPECS=0 git stash -q --keep-index 20 | trap revert_git_stash EXIT 21 | fi 22 | 23 | export FLAKEBOX_GIT_LS 24 | FLAKEBOX_GIT_LS="$(git ls-files)" 25 | export FLAKEBOX_GIT_LS_TEXT 26 | FLAKEBOX_GIT_LS_TEXT="$(echo "$FLAKEBOX_GIT_LS" | grep -v -E "\.(png|ods|jpg|jpeg|woff2|keystore|wasm|ttf|jar|ico|gif)\$")" 27 | 28 | 29 | function check_nothing() { 30 | true 31 | } 32 | export -f check_nothing 33 | 34 | function check_cargo_fmt() { 35 | set -euo pipefail 36 | 37 | flakebox-in-each-cargo-workspace cargo fmt --all --check 38 | 39 | } 40 | export -f check_cargo_fmt 41 | 42 | function check_cargo_lock() { 43 | set -euo pipefail 44 | 45 | # https://users.rust-lang.org/t/check-if-the-cargo-lock-is-up-to-date-without-building-anything/91048/5 46 | flakebox-in-each-cargo-workspace cargo update --workspace --locked 47 | 48 | } 49 | export -f check_cargo_lock 50 | 51 | function check_leftover_dbg() { 52 | set -euo pipefail 53 | 54 | errors="" 55 | for path in $(echo "$FLAKEBOX_GIT_LS_TEXT" | grep '.*\.rs'); do 56 | if grep 'dbg!(' "$path" > /dev/null; then 57 | >&2 echo "$path contains dbg! macro" 58 | errors="true" 59 | fi 60 | done 61 | 62 | if [ -n "$errors" ]; then 63 | >&2 echo "Fix the problems above or use --no-verify" 1>&2 64 | return 1 65 | fi 66 | 67 | } 68 | export -f check_leftover_dbg 69 | 70 | function check_semgrep() { 71 | set -euo pipefail 72 | 73 | # semgrep is not available on MacOS 74 | if ! command -v semgrep > /dev/null ; then 75 | >&2 echo "Skipping semgrep check: not available" 76 | return 0 77 | fi 78 | 79 | if [ ! -f .config/semgrep.yaml ] ; then 80 | >&2 echo "Skipping semgrep check: .config/semgrep.yaml doesn't exist" 81 | return 0 82 | fi 83 | 84 | if [ ! -s .config/semgrep.yaml ] ; then 85 | >&2 echo "Skipping semgrep check: .config/semgrep.yaml empty" 86 | return 0 87 | fi 88 | 89 | env SEMGREP_ENABLE_VERSION_CHECK=0 \ 90 | semgrep -q --error --no-rewrite-rule-ids --config .config/semgrep.yaml 91 | 92 | } 93 | export -f check_semgrep 94 | 95 | function check_shellcheck() { 96 | set -euo pipefail 97 | 98 | for path in $(echo "$FLAKEBOX_GIT_LS_TEXT" | grep -E '.*\.sh$'); do 99 | shellcheck --severity=warning "$path" 100 | done 101 | 102 | } 103 | export -f check_shellcheck 104 | 105 | function check_trailing_newline() { 106 | set -euo pipefail 107 | 108 | errors="" 109 | for path in $(echo "$FLAKEBOX_GIT_LS_TEXT"); do 110 | 111 | # extra branches for clarity 112 | if [ ! -s "$path" ]; then 113 | # echo "$path is empty" 114 | true 115 | elif [ -z "$(tail -c 1 < "$path")" ]; then 116 | # echo "$path ends with a newline or with a null byte" 117 | true 118 | else 119 | >&2 echo "$path doesn't end with a newline" 1>&2 120 | errors="true" 121 | fi 122 | done 123 | 124 | if [ -n "$errors" ]; then 125 | >&2 echo "Fix the problems above or use --no-verify" 1>&2 126 | return 1 127 | fi 128 | 129 | } 130 | export -f check_trailing_newline 131 | 132 | function check_trailing_whitespace() { 133 | set -euo pipefail 134 | 135 | rev="HEAD" 136 | if ! git rev-parse -q 1>/dev/null HEAD 2>/dev/null ; then 137 | >&2 echo "Warning: no commits yet, checking against --root" 138 | rev="--root" 139 | fi 140 | if ! git diff --check $rev ; then 141 | >&2 echo "Trailing whitespace detected. Please remove them before committing." 142 | return 1 143 | fi 144 | 145 | } 146 | export -f check_trailing_whitespace 147 | 148 | function check_typos() { 149 | set -euo pipefail 150 | 151 | if ! echo "$FLAKEBOX_GIT_LS_TEXT" | typos --file-list - --force-exclude ; then 152 | >&2 echo "Typos found: Valid new words can be added to '.typos.toml'" 153 | return 1 154 | fi 155 | 156 | } 157 | export -f check_typos 158 | 159 | parallel \ 160 | --nonotice \ 161 | ::: \ 162 | check_cargo_fmt \ 163 | check_cargo_lock \ 164 | check_leftover_dbg \ 165 | check_semgrep \ 166 | check_shellcheck \ 167 | check_trailing_newline \ 168 | check_trailing_whitespace \ 169 | check_typos \ 170 | check_nothing 171 | -------------------------------------------------------------------------------- /rustfmt.toml: -------------------------------------------------------------------------------- 1 | merge_imports = true 2 | -------------------------------------------------------------------------------- /src/lib.rs: -------------------------------------------------------------------------------- 1 | use std::collections::HashSet; 2 | use std::ffi::OsStr; 3 | use std::fs::{self}; 4 | use std::io::{self}; 5 | use std::path::{Path, PathBuf}; 6 | 7 | use tracing::{debug, info, trace, warn}; 8 | use walkdir::WalkDir; 9 | 10 | pub struct Dotr { 11 | ignore: HashSet, 12 | 13 | dry_run: bool, 14 | force: bool, 15 | } 16 | 17 | impl Dotr { 18 | pub fn new() -> Self { 19 | Dotr { 20 | ignore: HashSet::new(), 21 | dry_run: false, 22 | force: false, 23 | } 24 | } 25 | 26 | pub fn set_force(self) -> Self { 27 | Self { 28 | force: true, 29 | ..self 30 | } 31 | } 32 | 33 | pub fn set_dry_run(self) -> Self { 34 | Self { 35 | dry_run: true, 36 | ..self 37 | } 38 | } 39 | 40 | pub fn link_entry( 41 | &self, 42 | src: &walkdir::DirEntry, 43 | src_base: &Path, 44 | dst_base: &Path, 45 | ) -> io::Result<()> { 46 | trace!(path = %src.path().display(), "Walking path"); 47 | 48 | let src = src.path(); 49 | let src_rel = src.strip_prefix(src_base).unwrap(); 50 | 51 | if self.ignore.contains(src_rel) { 52 | debug!(path = %src.display(), "Ignoring file"); 53 | return Ok(()); 54 | } 55 | 56 | let dst = dst_base.join(src_rel); 57 | let dst_metadata = dst.symlink_metadata().ok(); 58 | let dst_type = dst_metadata.map(|m| m.file_type()); 59 | 60 | let src_metadata = src.symlink_metadata()?; 61 | let src_type = src_metadata.file_type(); 62 | 63 | if src_type.is_dir() { 64 | return Ok(()); 65 | } else if src_type.is_file() { 66 | trace!(src = %src.display(), dst=%dst.display(), "Source is a file"); 67 | if dst.exists() || dst.symlink_metadata().is_ok() { 68 | if self.force { 69 | if dst_type.is_some_and(|t| t.is_dir()) { 70 | io::Error::new( 71 | io::ErrorKind::Other, 72 | format!("Can't safely remove {} as it's a directory", dst.display()), 73 | ); 74 | } 75 | if !self.dry_run { 76 | debug!(src = %src.display(), dst=%dst.display(), "Force removing destination"); 77 | fs::remove_file(&dst)?; 78 | } else { 79 | debug!(src = %src.display(), dst=%dst.display(), "Force removing destination (dry-run)"); 80 | } 81 | } else { 82 | if dst_type.map(|t| t.is_symlink()).unwrap_or(false) { 83 | let dst_link_dst = dst.read_link()?; 84 | if *dst_link_dst == *src { 85 | debug!(src = %src.display(), dst=%dst.display(), "Destination already points to the source"); 86 | return Ok(()); 87 | } else { 88 | warn!(src = %src.display(), dst = %dst.display(), dst_dst = %dst_link_dst.display(), "Destination already exists and points elsewhere"); 89 | } 90 | } else { 91 | warn!(src = %src.display(), dst=%dst.display(), "Destination already exists and is not a symlink"); 92 | } 93 | return Ok(()); 94 | } 95 | } else if !self.dry_run { 96 | trace!(src = %src.display(), dst=%dst.display(), "Creating a base directory (if doesn't exist)"); 97 | fs::create_dir_all(dst.parent().unwrap())?; 98 | } 99 | 100 | if !self.dry_run { 101 | trace!(src = %src.display(), dst=%dst.display(), "Creating symlink to a src file"); 102 | std::os::unix::fs::symlink(src, &dst)?; 103 | } 104 | } else if src_type.is_symlink() { 105 | let src_link = src.read_link()?; 106 | trace!(src = %src.display(), dst=%dst.display(), "src-link" = %src_link.display(), "Source is a symlink"); 107 | if dst.exists() || dst.symlink_metadata().is_ok() { 108 | if self.force { 109 | if !self.dry_run { 110 | debug!(src = %src.display(), dst = %dst.display(), "Force removing destination"); 111 | fs::remove_file(&dst)?; 112 | } else { 113 | debug!(src = %src.display(), dst = %dst.display(), "Force removing destination (dry-run)"); 114 | } 115 | } else if Some(src_link.clone()) == dst.read_link().ok() { 116 | debug!( 117 | src = %src.display(), dst = %dst.display(), 118 | "Destination already points to the source (symlink source)" 119 | ); 120 | return Ok(()); 121 | } else { 122 | warn!(src = %src.display(), dst = %dst.display(), "Destination already exists"); 123 | return Ok(()); 124 | } 125 | } else if !self.dry_run { 126 | trace!(src = %src.display(), dst = %dst.display(), "Creating a base directory (if doesn't exist)"); 127 | fs::create_dir_all(dst.parent().unwrap())?; 128 | } 129 | if !self.dry_run { 130 | trace!(src = %src.display(), dst = %dst.display(), "src-link" = %src_link.display(), "Duplicating symlink"); 131 | std::os::unix::fs::symlink(&src_link, &dst)?; 132 | } 133 | } else { 134 | warn!(src = %src.display(), dst = %dst.display(), "Skipping unknown source file type"); 135 | } 136 | Ok(()) 137 | } 138 | 139 | pub fn link(&self, src_base: &Path, dst_base: &Path) -> io::Result<()> { 140 | info!(src = %src_base.display(), dst = %dst_base.display(), "Starting link operation"); 141 | 142 | if !dst_base.exists() { 143 | return Err(io::Error::new( 144 | io::ErrorKind::NotFound, 145 | "Destination doesn't exist", 146 | )); 147 | } 148 | 149 | if !dst_base.is_dir() { 150 | return Err(io::Error::new( 151 | io::ErrorKind::AlreadyExists, 152 | "Destination is not a directory", 153 | )); 154 | } 155 | 156 | let dst_base = dst_base.canonicalize()?; 157 | let src_base = src_base.canonicalize()?; 158 | 159 | assert!(dst_base.is_absolute()); 160 | assert!(src_base.is_absolute()); 161 | 162 | for src in WalkDir::new(&src_base) 163 | .into_iter() 164 | .filter_entry(should_traverse) 165 | .filter_map(|e| e.ok()) 166 | { 167 | self.link_entry(&src, &src_base, &dst_base)?; 168 | } 169 | 170 | Ok(()) 171 | } 172 | 173 | pub fn unlink(&self, src_base: &Path, dst_base: &Path) -> io::Result<()> { 174 | info!(src = %src_base.display(), dst = %dst_base.display(), "Starting unlink operation"); 175 | 176 | let dst_base = dst_base.canonicalize()?; 177 | let src_base = src_base.canonicalize()?; 178 | 179 | assert!(dst_base.is_absolute()); 180 | assert!(src_base.is_absolute()); 181 | 182 | for src in WalkDir::new(&src_base) 183 | .into_iter() 184 | .filter_entry(should_traverse) 185 | .filter_map(|e| e.ok()) 186 | { 187 | self.unlink_entry(&src, &src_base, &dst_base)?; 188 | } 189 | 190 | Ok(()) 191 | } 192 | 193 | pub fn unlink_entry( 194 | &self, 195 | src: &walkdir::DirEntry, 196 | src_base: &Path, 197 | dst_base: &Path, 198 | ) -> io::Result<()> { 199 | trace!(path = %src.path().display(), "Walking path"); 200 | 201 | let src = src.path(); 202 | let src_rel = src.strip_prefix(src_base).unwrap(); 203 | 204 | if self.ignore.contains(src_rel) { 205 | debug!(path = %src.display(), "Ignoring file"); 206 | return Ok(()); 207 | } 208 | 209 | let dst = dst_base.join(src_rel); 210 | 211 | let src_metadata = src.symlink_metadata()?; 212 | let src_type = src_metadata.file_type(); 213 | 214 | if src_type.is_dir() { 215 | return Ok(()); 216 | } else if src_type.is_file() { 217 | trace!(src = %src.display(), dst = %dst.display(), "Unlink a file"); 218 | let dst_metadata = dst.symlink_metadata(); 219 | // exists follows symlinks :/ 220 | if dst.exists() || dst_metadata.is_ok() { 221 | let dst_metadata = dst_metadata?; 222 | if self.force { 223 | if !self.dry_run { 224 | debug!(src = %src.display(), dst = %dst.display(), "Force removing"); 225 | fs::remove_file(&dst)?; 226 | return Ok(()); 227 | } else { 228 | debug!(src = %src.display(), dst = %dst.display(), "Force removing (dry run)"); 229 | } 230 | } else if dst_metadata.file_type().is_file() { 231 | warn!(src = %src.display(), dst = %dst.display(), "Destination already exists and is a file"); 232 | return Ok(()); 233 | } else if dst_metadata.file_type().is_dir() { 234 | warn!(src = %src.display(), dst = %dst.display(), "Destination already exists and is a directory"); 235 | return Ok(()); 236 | } else if dst_metadata.file_type().is_symlink() { 237 | let dst_link = dst.read_link()?; 238 | if dst_link != src { 239 | warn!(src = %src.display(), dst = %dst.display(), "Destination already exists and is a symlink pointing to something else"); 240 | return Ok(()); 241 | } else if !self.dry_run { 242 | fs::remove_file(&dst)?; 243 | } 244 | } else { 245 | warn!(src = %src.display(), dst = %dst.display(), "Destination exists and is of unknown file type"); 246 | } 247 | } else { 248 | debug!(src = %src.display(), dst = %dst.display(), "Destination doesn't exist - nothing to unlink"); 249 | return Ok(()); 250 | } 251 | } else if src_type.is_symlink() { 252 | let src_link = src.read_link()?; 253 | trace!(src = %src.display(), dst = %dst.display(), "Unlink a symlink"); 254 | let dst_metadata = dst.symlink_metadata(); 255 | // exists follows symlinks :/ 256 | if dst.exists() || dst_metadata.is_ok() { 257 | let dst_metadata = dst_metadata?; 258 | if self.force { 259 | if !self.dry_run { 260 | fs::remove_file(&dst)?; 261 | return Ok(()); 262 | } 263 | } else if dst_metadata.file_type().is_file() { 264 | warn!(src = %src.display(), dst = %dst.display(), "Destination already exists and is a file"); 265 | return Ok(()); 266 | } else if dst_metadata.file_type().is_dir() { 267 | warn!(src = %src.display(), dst = %dst.display(), "Destination already exists and is a directory"); 268 | return Ok(()); 269 | } else if dst_metadata.file_type().is_symlink() { 270 | let dst_link = dst.read_link()?; 271 | if dst_link != src_link { 272 | warn!( 273 | src = %src.display(), 274 | dst = %dst.display(), 275 | "dst-link" = %dst_link.display(), 276 | "src-link" = %src_link.display(), 277 | "Destination already exists and is a symlink pointing to something else", 278 | ); 279 | return Ok(()); 280 | } else if !self.dry_run { 281 | fs::remove_file(&dst)?; 282 | } 283 | } else { 284 | warn!(src = %src.display(), dst = %dst.display(), "Destination exists and is of unknown file type"); 285 | } 286 | } else { 287 | debug!(src = %src.display(), dst = %dst.display(), "Destination doesn't exist - nothing to unlink"); 288 | return Ok(()); 289 | } 290 | } else { 291 | warn!(src = %src.display(), dst = %dst.display(), "Skipping unknown source file type"); 292 | } 293 | Ok(()) 294 | } 295 | } 296 | 297 | impl Default for Dotr { 298 | fn default() -> Self { 299 | Self::new() 300 | } 301 | } 302 | 303 | fn should_traverse(de: &walkdir::DirEntry) -> bool { 304 | if !de.path().is_dir() { 305 | return true; 306 | } 307 | 308 | if de.path().file_name() == Some(OsStr::new(".git")) { 309 | return false; 310 | } 311 | 312 | true 313 | } 314 | -------------------------------------------------------------------------------- /src/main.rs: -------------------------------------------------------------------------------- 1 | //! `dotr` is a very simple dotfile manager 2 | //! 3 | //! It supports `link` and `unlink` operations and couple 4 | //! of basic flags like `force`. 5 | //! 6 | //! I wrote it for myself, so it's in Rust and does exactly what I want, so I 7 | //! can fix/customize if I need something. But hey, maybe it also does 8 | //! exactly what you want too! 9 | //! 10 | //! ### Installation: 11 | //! 12 | //! * [Install Rust](https://www.rustup.rs/) 13 | //! 14 | //! ```norust 15 | //! cargo install dotr 16 | //! ``` 17 | //! 18 | //! ### Usage: 19 | //! 20 | //! ```norust 21 | //! dotr help 22 | //! ``` 23 | //! 24 | //! ### Ignoring files: 25 | //! 26 | //! `dotr` can skip some of the files in the source directory. To configure 27 | //! that, create a file called `dotr.toml` with an `ignore` key set to an array 28 | //! of files to be excluded: 29 | //! 30 | //! ```toml 31 | //! ignore = ["LICENSE", "user.js"] 32 | //! ``` 33 | //! 34 | //! The `dotr.toml` file will be loaded, if present, from the source directory. 35 | //! 36 | //! ### TODO: 37 | //! 38 | //! * Make it a separate library + binary 39 | 40 | mod opts; 41 | 42 | use std::process; 43 | 44 | use clap::Parser; 45 | use dotr::Dotr; 46 | use opts::Options; 47 | use tracing_subscriber::{EnvFilter, FmtSubscriber}; 48 | 49 | trait DotrExt { 50 | fn from_opts(opts: Options) -> Self; 51 | } 52 | 53 | impl DotrExt for Dotr { 54 | fn from_opts(opts: Options) -> Self { 55 | let mut dotr = Dotr::new(); 56 | 57 | if opts.force { 58 | dotr = dotr.set_force(); 59 | } 60 | 61 | if opts.dry_run { 62 | dotr = dotr.set_dry_run() 63 | } 64 | 65 | dotr 66 | } 67 | } 68 | 69 | fn init_tracing(verbosity: u8) -> anyhow::Result<()> { 70 | let level = match verbosity { 71 | 0 => "error", 72 | 1 => "warn", 73 | 2 => "info", 74 | 3 => "debug", 75 | _ => "trace", 76 | }; 77 | 78 | let subscriber = FmtSubscriber::builder() 79 | // Use the environment variable, if set, falling back to the specified level if not 80 | .with_env_filter(EnvFilter::new( 81 | std::env::var(tracing_subscriber::EnvFilter::DEFAULT_ENV) 82 | .unwrap_or_else(|_| level.to_string()), 83 | )) 84 | .finish(); 85 | 86 | tracing::subscriber::set_global_default(subscriber)?; 87 | 88 | Ok(()) 89 | } 90 | 91 | fn run() -> anyhow::Result<()> { 92 | let opts = opts::Options::parse(); 93 | 94 | init_tracing(opts.verbose)?; 95 | 96 | let dotr = Dotr::from_opts(opts.clone()); 97 | 98 | match opts.command { 99 | opts::Command::Link => dotr.link(&opts.src_dir, &opts.dst_dir)?, 100 | opts::Command::Unlink => dotr.unlink(&opts.src_dir, &opts.dst_dir)?, 101 | } 102 | 103 | Ok(()) 104 | } 105 | 106 | fn main() { 107 | if let Err(e) = run() { 108 | eprintln!("Error: {}", e); 109 | process::exit(-1); 110 | } 111 | } 112 | 113 | #[cfg(test)] 114 | mod tests; 115 | -------------------------------------------------------------------------------- /src/opts.rs: -------------------------------------------------------------------------------- 1 | use std::path::PathBuf; 2 | 3 | use clap::{Parser, Subcommand}; 4 | 5 | #[derive(Debug, Copy, Clone, Subcommand)] 6 | pub enum Command { 7 | Link, 8 | Unlink, 9 | } 10 | 11 | #[derive(Parser, Debug, Clone)] 12 | #[command(version, about)] 13 | pub struct Options { 14 | #[arg(long)] 15 | pub dst_dir: PathBuf, 16 | #[arg(long, default_value = ".")] 17 | pub src_dir: PathBuf, 18 | #[command(subcommand)] 19 | pub command: Command, 20 | /// Dry Run 21 | #[arg(long)] 22 | pub dry_run: bool, 23 | /// Force file deletion/overwritting 24 | #[arg(long)] 25 | pub force: bool, 26 | 27 | /// Paths to ignore 28 | #[arg(long)] 29 | pub ignore: Vec, 30 | 31 | #[clap(short, long, action = clap::ArgAction::Count)] 32 | pub verbose: u8, 33 | } 34 | -------------------------------------------------------------------------------- /src/tests.rs: -------------------------------------------------------------------------------- 1 | use std::path::Path; 2 | use std::{fs, io}; 3 | 4 | use tempdir::TempDir; 5 | 6 | fn create_file(path: &Path) -> io::Result<()> { 7 | std::fs::File::create(path)?; 8 | Ok(()) 9 | } 10 | 11 | fn assert_is_link(path: &Path, links_to: &Path) { 12 | let dst_path = fs::read_link(path).unwrap(); 13 | 14 | assert_eq!(dst_path, links_to); 15 | } 16 | 17 | #[test] 18 | fn simple_file() -> io::Result<()> { 19 | let dotr = super::Dotr::new(); 20 | 21 | let src = TempDir::new("src").unwrap(); 22 | let dst = TempDir::new("dst").unwrap(); 23 | let src = src.path(); 24 | let dst = dst.path(); 25 | 26 | let src_path = src.join("a"); 27 | let dst_path = dst.join("a"); 28 | create_file(&src_path)?; 29 | 30 | dotr.link(src, dst)?; 31 | assert_is_link(&dst_path, &src_path); 32 | 33 | dotr.unlink(src, dst)?; 34 | assert!(!dst_path.exists()); 35 | 36 | Ok(()) 37 | } 38 | 39 | #[test] 40 | fn simple_nested_file() -> io::Result<()> { 41 | let dotr = super::Dotr::new(); 42 | 43 | let src = TempDir::new("src").unwrap(); 44 | let dst = TempDir::new("dst").unwrap(); 45 | let src = src.path(); 46 | let dst = dst.path(); 47 | 48 | let src_path = src.join("foo").join("a"); 49 | let dst_path = dst.join("foo").join("a"); 50 | fs::create_dir_all(src.join("foo"))?; 51 | create_file(&src_path)?; 52 | 53 | dotr.link(src, dst)?; 54 | assert_is_link(&dst_path, &src_path); 55 | 56 | dotr.unlink(src, dst)?; 57 | assert!(!dst_path.exists()); 58 | 59 | Ok(()) 60 | } 61 | 62 | #[test] 63 | fn simple_symlink() -> io::Result<()> { 64 | let dotr = super::Dotr::new(); 65 | 66 | let src = TempDir::new("src").unwrap(); 67 | let dst = TempDir::new("dst").unwrap(); 68 | let src = src.path(); 69 | let dst = dst.path(); 70 | 71 | let src_path = src.join("a"); 72 | let src_link_path = src.join("a.lnk"); 73 | let dst_path = dst.join("a"); 74 | let dst_link_path = dst.join("a.lnk"); 75 | create_file(&src_path)?; 76 | 77 | std::os::unix::fs::symlink(&src_path, src_link_path)?; 78 | 79 | dotr.link(src, dst)?; 80 | assert_is_link(&dst_link_path, &src_path); 81 | 82 | dotr.unlink(src, dst)?; 83 | assert!(!dst_path.exists()); 84 | assert!(!dst_link_path.exists()); 85 | 86 | Ok(()) 87 | } 88 | --------------------------------------------------------------------------------