initial commit

This commit is contained in:
Josh Thomas 2024-12-13 20:02:28 -06:00
commit 540bb97629
12 changed files with 1601 additions and 0 deletions

20
.gitignore vendored Normal file
View file

@ -0,0 +1,20 @@
# Created by https://www.toptal.com/developers/gitignore/api/rust
# Edit at https://www.toptal.com/developers/gitignore?templates=rust
### Rust ###
# Generated by Cargo
# will have compiled files and executables
debug/
target/
# Remove Cargo.lock from gitignore if creating an executable, leave it for libraries
# More information here https://doc.rust-lang.org/cargo/guide/cargo-toml-vs-cargo-lock.html
# Cargo.lock
# These are backup files generated by rustfmt
**/*.rs.bk
# MSVC Windows builds of rustc generate these, which store debugging information
*.pdb
# End of https://www.toptal.com/developers/gitignore/api/rust

748
Cargo.lock generated Normal file
View file

@ -0,0 +1,748 @@
# This file is automatically @generated by Cargo.
# It is not intended for manual editing.
version = 4
[[package]]
name = "anstream"
version = "0.6.18"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8acc5369981196006228e28809f761875c0327210a891e941f4c683b3a99529b"
dependencies = [
"anstyle",
"anstyle-parse",
"anstyle-query",
"anstyle-wincon",
"colorchoice",
"is_terminal_polyfill",
"utf8parse",
]
[[package]]
name = "anstyle"
version = "1.0.10"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "55cc3b69f167a1ef2e161439aa98aed94e6028e5f9a59be9a6ffb47aef1651f9"
[[package]]
name = "anstyle-parse"
version = "0.2.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3b2d16507662817a6a20a9ea92df6652ee4f94f914589377d69f3b21bc5798a9"
dependencies = [
"utf8parse",
]
[[package]]
name = "anstyle-query"
version = "1.1.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "79947af37f4177cfead1110013d678905c37501914fba0efea834c3fe9a8d60c"
dependencies = [
"windows-sys 0.59.0",
]
[[package]]
name = "anstyle-wincon"
version = "3.0.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2109dbce0e72be3ec00bed26e6a7479ca384ad226efdd66db8fa2e3a38c83125"
dependencies = [
"anstyle",
"windows-sys 0.59.0",
]
[[package]]
name = "atomic"
version = "0.6.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8d818003e740b63afc82337e3160717f4f63078720a810b7b903e70a5d1d2994"
dependencies = [
"bytemuck",
]
[[package]]
name = "autocfg"
version = "1.4.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ace50bade8e6234aa140d9a2f552bbee1db4d353f69b8217bc503490fc1a9f26"
[[package]]
name = "bitflags"
version = "2.6.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b048fb63fd8b5923fc5aa7b340d8e156aec7ec02f0c78fa8a6ddc2613f6f71de"
[[package]]
name = "bytemuck"
version = "1.20.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8b37c88a63ffd85d15b406896cc343916d7cf57838a847b3a6f2ca5d39a5695a"
[[package]]
name = "cfg-if"
version = "1.0.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd"
[[package]]
name = "clap"
version = "4.5.23"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3135e7ec2ef7b10c6ed8950f0f792ed96ee093fa088608f1c76e569722700c84"
dependencies = [
"clap_builder",
"clap_derive",
]
[[package]]
name = "clap_builder"
version = "4.5.23"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "30582fc632330df2bd26877bde0c1f4470d57c582bbc070376afcd04d8cb4838"
dependencies = [
"anstream",
"anstyle",
"clap_lex",
"strsim",
]
[[package]]
name = "clap_derive"
version = "4.5.18"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "4ac6a0c7b1a9e9a5186361f67dfa1b88213572f427fb9ab038efb2bd8c582dab"
dependencies = [
"heck",
"proc-macro2",
"quote",
"syn",
]
[[package]]
name = "clap_lex"
version = "0.7.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f46ad14479a25103f283c0f10005961cf086d8dc42205bb44c46ac563475dca6"
[[package]]
name = "colorchoice"
version = "1.0.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5b63caa9aa9397e2d9480a9b13673856c78d8ac123288526c37d7839f2a86990"
[[package]]
name = "dirs"
version = "5.0.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "44c45a9d03d6676652bcb5e724c7e988de1acad23a711b5217ab9cbecbec2225"
dependencies = [
"dirs-sys",
]
[[package]]
name = "dirs-sys"
version = "0.4.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "520f05a5cbd335fae5a99ff7a6ab8627577660ee5cfd6a94a6a929b52ff0321c"
dependencies = [
"libc",
"option-ext",
"redox_users",
"windows-sys 0.48.0",
]
[[package]]
name = "equivalent"
version = "1.0.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5443807d6dff69373d433ab9ef5378ad8df50ca6298caf15de6e52e24aaf54d5"
[[package]]
name = "errno"
version = "0.3.10"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "33d852cb9b869c2a9b3df2f71a3074817f01e1844f839a144f5fcef059a4eb5d"
dependencies = [
"libc",
"windows-sys 0.59.0",
]
[[package]]
name = "fastrand"
version = "2.3.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "37909eebbb50d72f9059c3b6d82c0463f2ff062c9e95845c43a6c9c0355411be"
[[package]]
name = "figment"
version = "0.10.19"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8cb01cd46b0cf372153850f4c6c272d9cbea2da513e07538405148f95bd789f3"
dependencies = [
"atomic",
"pear",
"serde",
"toml",
"uncased",
"version_check",
]
[[package]]
name = "getrandom"
version = "0.2.15"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c4567c8db10ae91089c99af84c68c38da3ec2f087c3f82960bcdbf3656b6f4d7"
dependencies = [
"cfg-if",
"libc",
"wasi",
]
[[package]]
name = "hashbrown"
version = "0.15.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "bf151400ff0baff5465007dd2f3e717f3fe502074ca563069ce3a6629d07b289"
[[package]]
name = "heck"
version = "0.5.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea"
[[package]]
name = "indexmap"
version = "2.7.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "62f822373a4fe84d4bb149bf54e584a7f4abec90e072ed49cda0edea5b95471f"
dependencies = [
"equivalent",
"hashbrown",
]
[[package]]
name = "inlinable_string"
version = "0.1.15"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c8fae54786f62fb2918dcfae3d568594e50eb9b5c25bf04371af6fe7516452fb"
[[package]]
name = "is_terminal_polyfill"
version = "1.70.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7943c866cc5cd64cbc25b2e01621d07fa8eb2a1a23160ee81ce38704e97b8ecf"
[[package]]
name = "libc"
version = "0.2.168"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5aaeb2981e0606ca11d79718f8bb01164f1d6ed75080182d3abf017e6d244b6d"
[[package]]
name = "libredox"
version = "0.1.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c0ff37bd590ca25063e35af745c343cb7a0271906fb7b37e4813e8f79f00268d"
dependencies = [
"bitflags",
"libc",
]
[[package]]
name = "linux-raw-sys"
version = "0.4.14"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "78b3ae25bc7c8c38cec158d1f2757ee79e9b3740fbc7ccf0e59e4b08d793fa89"
[[package]]
name = "lock_api"
version = "0.4.12"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "07af8b9cdd281b7915f413fa73f29ebd5d55d0d3f0155584dade1ff18cea1b17"
dependencies = [
"autocfg",
"scopeguard",
]
[[package]]
name = "memchr"
version = "2.7.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "78ca9ab1a0babb1e7d5695e3530886289c18cf2f87ec19a575a0abdce112e3a3"
[[package]]
name = "once_cell"
version = "1.20.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1261fe7e33c73b354eab43b1273a57c8f967d0391e80353e51f764ac02cf6775"
[[package]]
name = "option-ext"
version = "0.2.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "04744f49eae99ab78e0d5c0b603ab218f515ea8cfe5a456d7629ad883a3b6e7d"
[[package]]
name = "parking_lot"
version = "0.12.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f1bf18183cf54e8d6059647fc3063646a1801cf30896933ec2311622cc4b9a27"
dependencies = [
"lock_api",
"parking_lot_core",
]
[[package]]
name = "parking_lot_core"
version = "0.9.10"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1e401f977ab385c9e4e3ab30627d6f26d00e2c73eef317493c4ec6d468726cf8"
dependencies = [
"cfg-if",
"libc",
"redox_syscall",
"smallvec",
"windows-targets 0.52.6",
]
[[package]]
name = "pear"
version = "0.2.9"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "bdeeaa00ce488657faba8ebf44ab9361f9365a97bd39ffb8a60663f57ff4b467"
dependencies = [
"inlinable_string",
"pear_codegen",
"yansi",
]
[[package]]
name = "pear_codegen"
version = "0.2.9"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "4bab5b985dc082b345f812b7df84e1bef27e7207b39e448439ba8bd69c93f147"
dependencies = [
"proc-macro2",
"proc-macro2-diagnostics",
"quote",
"syn",
]
[[package]]
name = "proc-macro2"
version = "1.0.92"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "37d3544b3f2748c54e147655edb5025752e2303145b5aefb3c3ea2c78b973bb0"
dependencies = [
"unicode-ident",
]
[[package]]
name = "proc-macro2-diagnostics"
version = "0.10.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "af066a9c399a26e020ada66a034357a868728e72cd426f3adcd35f80d88d88c8"
dependencies = [
"proc-macro2",
"quote",
"syn",
"version_check",
"yansi",
]
[[package]]
name = "quote"
version = "1.0.37"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b5b9d34b8991d19d98081b46eacdd8eb58c6f2b201139f7c5f643cc155a633af"
dependencies = [
"proc-macro2",
]
[[package]]
name = "redox_syscall"
version = "0.5.8"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "03a862b389f93e68874fbf580b9de08dd02facb9a788ebadaf4a3fd33cf58834"
dependencies = [
"bitflags",
]
[[package]]
name = "redox_users"
version = "0.4.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ba009ff324d1fc1b900bd1fdb31564febe58a8ccc8a6fdbb93b543d33b13ca43"
dependencies = [
"getrandom",
"libredox",
"thiserror 1.0.69",
]
[[package]]
name = "rustix"
version = "0.38.42"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f93dc38ecbab2eb790ff964bb77fa94faf256fd3e73285fd7ba0903b76bedb85"
dependencies = [
"bitflags",
"errno",
"libc",
"linux-raw-sys",
"windows-sys 0.59.0",
]
[[package]]
name = "scopeguard"
version = "1.2.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "94143f37725109f92c262ed2cf5e59bce7498c01bcc1502d7b9afe439a4e9f49"
[[package]]
name = "serde"
version = "1.0.216"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0b9781016e935a97e8beecf0c933758c97a5520d32930e460142b4cd80c6338e"
dependencies = [
"serde_derive",
]
[[package]]
name = "serde_derive"
version = "1.0.216"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "46f859dbbf73865c6627ed570e78961cd3ac92407a2d117204c49232485da55e"
dependencies = [
"proc-macro2",
"quote",
"syn",
]
[[package]]
name = "serde_spanned"
version = "0.6.8"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "87607cb1398ed59d48732e575a4c28a7a8ebf2454b964fe3f224f2afc07909e1"
dependencies = [
"serde",
]
[[package]]
name = "shadow-rs"
version = "0.1.0"
dependencies = [
"clap",
"dirs",
"figment",
"serde",
"temp-env",
"tempfile",
"thiserror 2.0.6",
"toml",
]
[[package]]
name = "smallvec"
version = "1.13.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3c5e1a9a646d36c3599cd173a41282daf47c44583ad367b8e6837255952e5c67"
[[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.90"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "919d3b74a5dd0ccd15aeb8f93e7006bd9e14c295087c9896a110f490752bcf31"
dependencies = [
"proc-macro2",
"quote",
"unicode-ident",
]
[[package]]
name = "temp-env"
version = "0.3.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "96374855068f47402c3121c6eed88d29cb1de8f3ab27090e273e420bdabcf050"
dependencies = [
"parking_lot",
]
[[package]]
name = "tempfile"
version = "3.14.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "28cce251fcbc87fac86a866eeb0d6c2d536fc16d06f184bb61aeae11aa4cee0c"
dependencies = [
"cfg-if",
"fastrand",
"once_cell",
"rustix",
"windows-sys 0.59.0",
]
[[package]]
name = "thiserror"
version = "1.0.69"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b6aaf5339b578ea85b50e080feb250a3e8ae8cfcdff9a461c9ec2904bc923f52"
dependencies = [
"thiserror-impl 1.0.69",
]
[[package]]
name = "thiserror"
version = "2.0.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8fec2a1820ebd077e2b90c4df007bebf344cd394098a13c563957d0afc83ea47"
dependencies = [
"thiserror-impl 2.0.6",
]
[[package]]
name = "thiserror-impl"
version = "1.0.69"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "4fee6c4efc90059e10f81e6d42c60a18f76588c3d74cb83a0b242a2b6c7504c1"
dependencies = [
"proc-macro2",
"quote",
"syn",
]
[[package]]
name = "thiserror-impl"
version = "2.0.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d65750cab40f4ff1929fb1ba509e9914eb756131cef4210da8d5d700d26f6312"
dependencies = [
"proc-macro2",
"quote",
"syn",
]
[[package]]
name = "toml"
version = "0.8.19"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a1ed1f98e3fdc28d6d910e6737ae6ab1a93bf1985935a1193e68f93eeb68d24e"
dependencies = [
"serde",
"serde_spanned",
"toml_datetime",
"toml_edit",
]
[[package]]
name = "toml_datetime"
version = "0.6.8"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0dd7358ecb8fc2f8d014bf86f6f638ce72ba252a2c3a2572f2a795f1d23efb41"
dependencies = [
"serde",
]
[[package]]
name = "toml_edit"
version = "0.22.22"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "4ae48d6208a266e853d946088ed816055e556cc6028c5e8e2b84d9fa5dd7c7f5"
dependencies = [
"indexmap",
"serde",
"serde_spanned",
"toml_datetime",
"winnow",
]
[[package]]
name = "uncased"
version = "0.9.10"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e1b88fcfe09e89d3866a5c11019378088af2d24c3fbd4f0543f96b479ec90697"
dependencies = [
"version_check",
]
[[package]]
name = "unicode-ident"
version = "1.0.14"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "adb9e6ca4f869e1180728b7950e35922a7fc6397f7b641499e8f3ef06e50dc83"
[[package]]
name = "utf8parse"
version = "0.2.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "06abde3611657adf66d383f00b093d7faecc7fa57071cce2578660c9f1010821"
[[package]]
name = "version_check"
version = "0.9.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0b928f33d975fc6ad9f86c8f283853ad26bdd5b10b7f1542aa2fa15e2289105a"
[[package]]
name = "wasi"
version = "0.11.0+wasi-snapshot-preview1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9c8d87e72b64a3b4db28d11ce29237c246188f4f51057d65a7eab63b7987e423"
[[package]]
name = "windows-sys"
version = "0.48.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "677d2418bec65e3338edb076e806bc1ec15693c5d0104683f2efe857f61056a9"
dependencies = [
"windows-targets 0.48.5",
]
[[package]]
name = "windows-sys"
version = "0.59.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1e38bc4d79ed67fd075bcc251a1c39b32a1776bbe92e5bef1f0bf1f8c531853b"
dependencies = [
"windows-targets 0.52.6",
]
[[package]]
name = "windows-targets"
version = "0.48.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9a2fa6e2155d7247be68c096456083145c183cbbbc2764150dda45a87197940c"
dependencies = [
"windows_aarch64_gnullvm 0.48.5",
"windows_aarch64_msvc 0.48.5",
"windows_i686_gnu 0.48.5",
"windows_i686_msvc 0.48.5",
"windows_x86_64_gnu 0.48.5",
"windows_x86_64_gnullvm 0.48.5",
"windows_x86_64_msvc 0.48.5",
]
[[package]]
name = "windows-targets"
version = "0.52.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9b724f72796e036ab90c1021d4780d4d3d648aca59e491e6b98e725b84e99973"
dependencies = [
"windows_aarch64_gnullvm 0.52.6",
"windows_aarch64_msvc 0.52.6",
"windows_i686_gnu 0.52.6",
"windows_i686_gnullvm",
"windows_i686_msvc 0.52.6",
"windows_x86_64_gnu 0.52.6",
"windows_x86_64_gnullvm 0.52.6",
"windows_x86_64_msvc 0.52.6",
]
[[package]]
name = "windows_aarch64_gnullvm"
version = "0.48.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2b38e32f0abccf9987a4e3079dfb67dcd799fb61361e53e2882c3cbaf0d905d8"
[[package]]
name = "windows_aarch64_gnullvm"
version = "0.52.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "32a4622180e7a0ec044bb555404c800bc9fd9ec262ec147edd5989ccd0c02cd3"
[[package]]
name = "windows_aarch64_msvc"
version = "0.48.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "dc35310971f3b2dbbf3f0690a219f40e2d9afcf64f9ab7cc1be722937c26b4bc"
[[package]]
name = "windows_aarch64_msvc"
version = "0.52.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "09ec2a7bb152e2252b53fa7803150007879548bc709c039df7627cabbd05d469"
[[package]]
name = "windows_i686_gnu"
version = "0.48.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a75915e7def60c94dcef72200b9a8e58e5091744960da64ec734a6c6e9b3743e"
[[package]]
name = "windows_i686_gnu"
version = "0.52.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8e9b5ad5ab802e97eb8e295ac6720e509ee4c243f69d781394014ebfe8bbfa0b"
[[package]]
name = "windows_i686_gnullvm"
version = "0.52.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0eee52d38c090b3caa76c563b86c3a4bd71ef1a819287c19d586d7334ae8ed66"
[[package]]
name = "windows_i686_msvc"
version = "0.48.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8f55c233f70c4b27f66c523580f78f1004e8b5a8b659e05a4eb49d4166cca406"
[[package]]
name = "windows_i686_msvc"
version = "0.52.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "240948bc05c5e7c6dabba28bf89d89ffce3e303022809e73deaefe4f6ec56c66"
[[package]]
name = "windows_x86_64_gnu"
version = "0.48.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "53d40abd2583d23e4718fddf1ebec84dbff8381c07cae67ff7768bbf19c6718e"
[[package]]
name = "windows_x86_64_gnu"
version = "0.52.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "147a5c80aabfbf0c7d901cb5895d1de30ef2907eb21fbbab29ca94c5b08b1a78"
[[package]]
name = "windows_x86_64_gnullvm"
version = "0.48.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0b7b52767868a23d5bab768e390dc5f5c55825b6d30b86c844ff2dc7414044cc"
[[package]]
name = "windows_x86_64_gnullvm"
version = "0.52.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "24d5b23dc417412679681396f2b49f3de8c1473deb516bd34410872eff51ed0d"
[[package]]
name = "windows_x86_64_msvc"
version = "0.48.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ed94fce61571a4006852b7389a063ab983c02eb1bb37b47f8272ce92d06d9538"
[[package]]
name = "windows_x86_64_msvc"
version = "0.52.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "589f6da84c646204747d1270a2a5661ea66ed1cced2631d546fdfb155959f9ec"
[[package]]
name = "winnow"
version = "0.6.20"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "36c1fec1a2bb5866f07c25f68c26e565c4c200aebb96d7e55710c19d3e8ac49b"
dependencies = [
"memchr",
]
[[package]]
name = "yansi"
version = "1.0.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "cfe53a6657fd280eaa890a3bc59152892ffa3e30101319d168b781ed6529b049"

20
Cargo.toml Normal file
View file

@ -0,0 +1,20 @@
[package]
name = "shadow-rs"
version = "0.1.0"
edition = "2021"
[dependencies]
clap = { version = "4.5", features = ["derive", "env"] }
dirs = "5.0"
figment = { version = "0.10", features = ["env", "toml"] }
serde = { version = "1.0", features = ["derive"] }
thiserror = "2.0"
toml = "0.8"
[dev-dependencies]
temp-env = "0.3"
tempfile = "3.14"
[[bin]]
name = "shadow"
path = "src/main.rs"

102
Justfile Normal file
View file

@ -0,0 +1,102 @@
set dotenv-load := true
set unstable := true
# List all available commands
[private]
default:
@just --list
[private]
test-cleanup:
#!/usr/bin/env bash
set -euo pipefail
rm -rf test_bin
echo "Test cleanup complete"
[private]
test-setup:
#!/usr/bin/env bash
set -euo pipefail
cargo build
TEST_DIR="test_bin"
mkdir -p $TEST_DIR
cd $TEST_DIR
ln -sf ../target/debug/shadow .
ln -sf shadow ls
ln -sf shadow tree
ln -sf shadow cat
mkdir -p other_bin
cd ..
echo "Test setup complete"
clean:
rm -rf target/
lint:
@just --fmt
cargo fmt
cargo clippy
test:
#!/usr/bin/env bash
set -euo pipefail
just test-setup
cd test_bin
export PATH="$PWD:$PATH"
echo -e "\nTesting shadow add commands:"
./shadow add ls eza
./shadow add tree "eza --tree"
./shadow add cat bat --bin-path ./other_bin
echo -e "\nTesting shadow list command:"
./shadow list
echo -e "\nTesting shadowed commands:"
./ls --version
./tree --version
./cat --version
echo -e "\nTesting shadow remove command:"
./shadow remove ls
echo "After removing ls:"
./shadow list
echo -e "\nTesting remove with specific bin path:"
./shadow remove cat --bin-path ./other_bin
echo "After removing cat:"
./shadow list
echo -e "\nTesting command with --raw flag:"
if ! ./tree --raw --version; then
echo "Raw command should execute original binary"
fi
echo -e "\nTesting non-existent shadow:"
if ./shadow remove nonexistent; then
echo "Error: remove should fail for non-existent shadow"
exit 1
else
echo "Successfully detected non-existent shadow"
fi
echo -e "\nTesting invalid commands:"
if ./shadow add; then
echo "Error: add should fail without arguments"
exit 1
else
echo "Successfully detected invalid add command"
fi
cd ..
just test-cleanup

10
LICENSE Normal file
View file

@ -0,0 +1,10 @@
MIT License
Copyright (c) 2024 Josh Thomas
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.

3
README.md Normal file
View file

@ -0,0 +1,3 @@
# shadow
A CLI tool to manage your shell aliases.

170
src/cli.rs Normal file
View file

@ -0,0 +1,170 @@
use crate::commands::Commands;
use crate::config::Config;
use crate::error::ExitCode;
use clap::Parser;
use std::env;
#[derive(Parser, Debug)]
#[command(author, version, about, long_about = None)]
pub struct Cli {
#[command(subcommand)]
command: Option<Commands>,
}
#[derive(Debug)]
pub struct ShadowedArgs {
args: Vec<String>,
is_raw: bool,
}
impl ShadowedArgs {
pub fn from_env() -> Self {
let args: Vec<String> = env::args().skip(1).collect();
let is_raw = args.contains(&"--raw".to_string()) || args.contains(&"-R".to_string());
let args = args
.into_iter()
.filter(|arg| arg != "--raw" && arg != "-R")
.collect();
Self { args, is_raw }
}
}
impl Cli {
pub fn execute(config: Config) -> ExitCode {
let cli = Self::parse();
match &cli.command {
Some(cmd) => cmd.execute(config),
None => {
println!("Use --help for usage information");
ExitCode::InvalidArguments
}
}
}
pub fn execute_shadowed(config: Config, command: &str) -> ExitCode {
let args = ShadowedArgs::from_env();
match config
.shadows()
.find(command)
.map(|shadow| shadow.execute(&args.args, args.is_raw))
{
Ok(code) => code,
Err(e) => {
eprintln!("{}", e);
e.into()
}
}
}
}
#[cfg(test)]
mod tests {
use super::*;
use tempfile::TempDir;
impl ShadowedArgs {
pub fn new(args: Vec<String>) -> Self {
let is_raw = args.contains(&"--raw".to_string()) || args.contains(&"-R".to_string());
let args = args
.into_iter()
.filter(|arg| arg != "--raw" && arg != "-R")
.collect();
Self { args, is_raw }
}
}
impl Cli {
pub fn execute_shadowed_with_args(
config: Config,
_temp_dir: TempDir, // Add this to keep it alive
command: &str,
args: ShadowedArgs,
) -> ExitCode {
match config
.shadows()
.find(command)
.map(|shadow| shadow.execute(&args.args, args.is_raw))
{
Ok(code) => code,
Err(e) => {
eprintln!("{}", e);
e.into()
}
}
}
pub fn execute_with_args(config: Config, _temp_dir: TempDir, args: Vec<&str>) -> ExitCode {
let cli = Self::try_parse_from(args).unwrap();
match &cli.command {
Some(cmd) => cmd.execute(config),
None => {
println!("Use --help for usage information");
ExitCode::InvalidArguments
}
}
}
}
mod shadowed_args {
use super::*;
#[test]
fn test_normal_args() {
let args = ShadowedArgs::new(vec!["arg1".to_string(), "arg2".to_string()]);
assert_eq!(args.args, vec!["arg1", "arg2"]);
assert!(!args.is_raw);
}
#[test]
fn test_raw_long_flag() {
let args = ShadowedArgs::new(vec!["--raw".to_string(), "arg1".to_string()]);
assert_eq!(args.args, vec!["arg1"]);
assert!(args.is_raw);
}
#[test]
fn test_raw_short_flag() {
let args = ShadowedArgs::new(vec!["-R".to_string(), "arg1".to_string()]);
assert_eq!(args.args, vec!["arg1"]);
assert!(args.is_raw);
}
#[test]
fn test_raw_flag_middle() {
let args = ShadowedArgs::new(vec![
"arg1".to_string(),
"--raw".to_string(),
"arg2".to_string(),
]);
assert_eq!(args.args, vec!["arg1", "arg2"]);
assert!(args.is_raw);
}
#[test]
fn test_multiple_raw_flags() {
let args = ShadowedArgs::new(vec![
"--raw".to_string(),
"-R".to_string(),
"arg1".to_string(),
]);
assert_eq!(args.args, vec!["arg1"]);
assert!(args.is_raw);
}
#[test]
fn test_empty_args() {
let args = ShadowedArgs::new(vec![]);
assert!(args.args.is_empty());
assert!(!args.is_raw);
}
#[test]
fn test_only_raw_flag() {
let args = ShadowedArgs::new(vec!["--raw".to_string()]);
assert!(args.args.is_empty());
assert!(args.is_raw);
}
}
}

129
src/commands.rs Normal file
View file

@ -0,0 +1,129 @@
use crate::config::Config;
use crate::error::ExitCode;
use crate::shadows::Shadow;
use clap::Parser;
use clap::Subcommand;
use std::path::PathBuf;
use std::process::Command;
#[derive(Subcommand, Debug)]
pub enum Commands {
/// Add a new shadow
Add(Add),
/// Remove a shadow
Remove(Remove),
/// List all shadows
List(List),
}
impl Commands {
pub fn execute(&self, config: Config) -> ExitCode {
match self {
Commands::Add(cmd) => cmd.execute(config),
Commands::Remove(cmd) => cmd.execute(config),
Commands::List(cmd) => cmd.execute(config),
}
}
}
#[derive(Clone, Debug, Parser)]
pub struct Add {
/// Original command to shadow
original: String,
/// Replacement command
replacement: String,
/// Directory to create symlink in
#[arg(long)]
bin_path: Option<PathBuf>,
}
impl Add {
pub fn execute(&self, mut config: Config) -> ExitCode {
if config.shadows().contains(&self.original) {
eprintln!("Command already shadowed: {}", self.original);
return ExitCode::DuplicateCommand;
}
if Command::new(&self.original).output().is_err() {
eprintln!("Command not found: {}", self.original);
return ExitCode::CommandFailed;
}
let bin_path = match &self.bin_path {
Some(p) if p == config.settings().bin_path() => None,
Some(p) => Some(p.clone()),
None => None,
};
let shadow = Shadow::new(self.original.clone(), self.replacement.clone(), bin_path);
if let Err(e) = shadow.create_symlink(config.settings()) {
eprintln!("{}", e);
return e.into();
}
match config.add(shadow) {
Ok(()) => {
println!("Added shadow: {}", self.original);
ExitCode::Success
}
Err(e) => {
eprintln!("{}", e);
e.into()
}
}
}
}
#[derive(Clone, Debug, Parser)]
pub struct Remove {
/// Command to un-shadow
original: String,
/// Directory containing the symlink
#[arg(long)]
bin_path: Option<PathBuf>,
}
impl Remove {
pub fn execute(&self, mut config: Config) -> ExitCode {
let shadow = match config.shadows().find(&self.original) {
Ok(shadow) => shadow,
Err(e) => {
eprintln!("{}", e);
return e.into();
}
};
if let Err(e) = shadow.remove_symlink(config.settings()) {
eprintln!("{}", e);
return e.into();
}
match config.remove(&self.original) {
Ok(()) => {
println!("Removed shadow: {}", self.original);
ExitCode::Success
}
Err(e) => {
eprintln!("{}", e);
e.into()
}
}
}
}
#[derive(Clone, Debug, Parser)]
pub struct List;
impl List {
pub fn execute(&self, config: Config) -> ExitCode {
match config.shadows().is_empty() {
true => println!("No shadows configured"),
false => config
.shadows()
.iter()
.for_each(|shadow| println!("{}", shadow)),
}
ExitCode::Success
}
}

110
src/config.rs Normal file
View file

@ -0,0 +1,110 @@
use crate::error::{Result, ShadowError};
use crate::shadows::{Shadow, Shadows};
use serde::{Deserialize, Serialize};
use std::path::PathBuf;
#[derive(Debug, Serialize, Deserialize, Clone)]
pub struct Config {
#[serde(default)]
settings: Settings,
#[serde(default)]
#[serde(skip_serializing_if = "Shadows::is_empty")]
shadows: Shadows,
}
impl Config {
pub fn new() -> Result<Self> {
let config = Config {
settings: Settings::default(),
shadows: Shadows::default(),
};
config.save()?;
Ok(config)
}
pub fn load() -> Result<Self> {
if Self::config_path().exists() {
let contents = std::fs::read_to_string(Self::config_path())?;
toml::from_str(&contents).map_err(|e| ShadowError::ConfigError(e.to_string()))
} else {
Self::new()
}
}
pub fn save(&self) -> Result<()> {
if let Some(parent) = Self::config_path().parent() {
std::fs::create_dir_all(parent)?;
}
let contents =
toml::to_string_pretty(self).map_err(|e| ShadowError::ConfigError(e.to_string()))?;
std::fs::write(Self::config_path(), contents)
.map_err(|e| ShadowError::ConfigError(e.to_string()))?;
Ok(())
}
pub fn settings(&self) -> &Settings {
&self.settings
}
pub fn shadows(&self) -> &Shadows {
&self.shadows
}
pub fn add(&mut self, shadow: Shadow) -> Result<()> {
self.shadows.push(shadow);
self.save()?;
Ok(())
}
pub fn remove(&mut self, original: &str) -> Result<()> {
let position = self
.shadows
.iter()
.position(|shadow| shadow.original() == original)
.ok_or_else(|| ShadowError::ShadowNotFound(original.to_string()))?;
self.shadows.remove(position);
self.save()?;
Ok(())
}
fn config_path() -> PathBuf {
dirs::config_dir()
.expect("Could not find config directory")
.join("shadow/config.toml")
}
}
#[derive(Debug, Serialize, Deserialize, Default, Clone)]
pub struct Settings {
#[serde(default = "Settings::default_bin_path")]
bin_path: PathBuf,
#[serde(default)]
always_use_raw: bool,
}
impl Settings {
pub fn new(bin_path: PathBuf, always_use_raw: bool) -> Self {
Self {
bin_path,
always_use_raw,
}
}
fn default() -> Self {
Self::new(Self::default_bin_path(), false)
}
pub fn bin_path(&self) -> &PathBuf {
&self.bin_path
}
fn default_bin_path() -> PathBuf {
dirs::executable_dir()
.or_else(|| dirs::home_dir().map(|h| h.join(".local/bin")))
.expect("Could not determine binary directory")
}
}

46
src/error.rs Normal file
View file

@ -0,0 +1,46 @@
use thiserror::Error;
#[derive(Debug, Clone, Copy, PartialEq)]
pub enum ExitCode {
Success = 0,
GeneralError = 1,
ConfigError = 2,
InvalidArguments = 64,
CommandNotFound = 127,
CommandFailed = 128,
DuplicateCommand = 129,
}
impl From<ExitCode> for i32 {
fn from(code: ExitCode) -> i32 {
code as i32
}
}
impl From<ShadowError> for ExitCode {
fn from(error: ShadowError) -> Self {
match error {
ShadowError::ShadowNotFound(_) => ExitCode::CommandNotFound,
ShadowError::CommandExecutionError(_) => ExitCode::CommandFailed,
ShadowError::ConfigError(_) => ExitCode::ConfigError,
ShadowError::InvalidReplacement(_) => ExitCode::InvalidArguments,
ShadowError::DuplicateCommand(_) => ExitCode::DuplicateCommand,
}
}
}
#[derive(Error, Debug)]
pub enum ShadowError {
#[error("No shadow found for command: {0}")]
ShadowNotFound(String),
#[error("Command already shadowed: {0}")]
DuplicateCommand(String),
#[error("Failed to execute command: {0}")]
CommandExecutionError(#[from] std::io::Error),
#[error("Failed to load config: {0}")]
ConfigError(String),
#[error("Invalid replacement command: {0}")]
InvalidReplacement(String),
}
pub type Result<T> = std::result::Result<T, ShadowError>;

38
src/main.rs Normal file
View file

@ -0,0 +1,38 @@
mod cli;
mod commands;
mod config;
mod error;
mod shadows;
use crate::cli::Cli;
use crate::config::Config;
use crate::error::ExitCode;
use std::env;
use std::path::Path;
use std::process::exit;
fn main() {
// For debugging
println!("Args: {:?}", std::env::args().collect::<Vec<_>>());
println!("Program: {:?}", std::env::current_exe().unwrap());
let args = env::args().next().unwrap();
let program_name = Path::new(&args)
.file_name()
.and_then(|name| name.to_str())
.unwrap_or("shadow");
let config = match Config::load() {
Ok(config) => config,
Err(e) => {
eprintln!("Failed to load config: {}", e);
exit(ExitCode::ConfigError.into());
}
};
let exit_code = match program_name {
"shadow" => Cli::execute(config),
command => Cli::execute_shadowed(config, command),
};
exit(exit_code.into())
}

205
src/shadows.rs Normal file
View file

@ -0,0 +1,205 @@
use crate::config::Settings;
use crate::error::{ExitCode, Result, ShadowError};
use serde::{Deserialize, Serialize};
use std::env;
use std::fmt;
use std::fs;
use std::ops::{Deref, DerefMut};
use std::path::Path;
use std::path::PathBuf;
use std::process::Command;
#[derive(Clone, Debug, Default, Serialize, Deserialize)]
pub struct Shadows(Vec<Shadow>);
impl Shadows {
pub fn find<S: AsRef<str>>(&self, original: S) -> Result<&Shadow> {
self.0
.iter()
.find(|s| s.original == original.as_ref())
.ok_or_else(|| ShadowError::ShadowNotFound(original.as_ref().to_string()))
}
pub fn contains<S: AsRef<str>>(&self, original: S) -> bool {
self.find(original).is_ok()
}
pub fn is_empty(&self) -> bool {
self.0.is_empty()
}
}
impl Deref for Shadows {
type Target = Vec<Shadow>;
fn deref(&self) -> &Self::Target {
&self.0
}
}
impl DerefMut for Shadows {
fn deref_mut(&mut self) -> &mut Self::Target {
&mut self.0
}
}
impl IntoIterator for Shadows {
type Item = Shadow;
type IntoIter = std::vec::IntoIter<Self::Item>;
fn into_iter(self) -> Self::IntoIter {
self.0.into_iter()
}
}
impl<'a> IntoIterator for &'a Shadows {
type Item = &'a Shadow;
type IntoIter = std::slice::Iter<'a, Shadow>;
fn into_iter(self) -> Self::IntoIter {
self.0.iter()
}
}
#[derive(Clone, Debug, Serialize, Deserialize, PartialEq)]
pub struct Shadow {
original: String,
replacement: String,
#[serde(skip_serializing_if = "Option::is_none")]
bin_path: Option<PathBuf>,
}
impl Shadow {
pub fn new(original: String, replacement: String, bin_path: Option<PathBuf>) -> Self {
Self {
original,
replacement,
bin_path,
}
}
pub fn original(&self) -> &String {
&self.original
}
pub fn replacement(&self) -> &String {
&self.replacement
}
pub fn bin_path(&self) -> &Option<PathBuf> {
&self.bin_path
}
pub fn create_symlink(&self, settings: &Settings) -> Result<()> {
let bin_path = self
.bin_path
.as_deref()
.unwrap_or_else(|| settings.bin_path());
fs::create_dir_all(bin_path).map_err(|e| {
ShadowError::ConfigError(format!("Failed to create bin directory: {}", e))
})?;
let target = env::current_exe().map_err(|e| {
ShadowError::ConfigError(format!("Failed to get executable path: {}", e))
})?;
let link_path = self.link_path(bin_path);
if link_path.exists() {
fs::remove_file(&link_path)?;
}
#[cfg(unix)]
std::os::unix::fs::symlink(&target, &link_path)?;
#[cfg(windows)]
std::os::windows::fs::symlink_file(&target, &link_path)?;
Ok(())
}
pub fn remove_symlink(&self, settings: &Settings) -> Result<()> {
let bin_path = self
.bin_path
.as_deref()
.unwrap_or_else(|| settings.bin_path());
let link_path = self.link_path(bin_path);
if link_path.exists() {
fs::remove_file(&link_path)?;
}
Ok(())
}
fn link_path(&self, bin_path: &Path) -> PathBuf {
let link_name = if cfg!(windows) {
format!("{}.exe", self.original)
} else {
self.original.clone()
};
bin_path.join(link_name)
}
pub fn execute(&self, args: &[String], raw: bool) -> ExitCode {
if raw {
self.execute_original(args)
} else {
self.execute_shadow(args)
}
}
fn execute_original(&self, args: &[String]) -> ExitCode {
match Command::new(&self.original).args(args).status() {
Ok(status) => match status.code() {
Some(0) => ExitCode::Success,
Some(_) => ExitCode::CommandFailed,
None => ExitCode::CommandFailed,
},
Err(e) => {
eprintln!("Failed to execute {}: {}", self.original, e);
ExitCode::CommandFailed
}
}
}
fn execute_shadow(&self, args: &[String]) -> ExitCode {
let parts: Vec<&str> = self.replacement.split_whitespace().collect();
let (cmd, base_args) = match parts.split_first() {
Some(parts) => parts,
None => {
eprintln!("Invalid replacement command: {}", self.replacement);
return ExitCode::InvalidArguments;
}
};
let all_args: Vec<String> = base_args
.iter()
.map(|&s| s.to_string())
.chain(args.iter().cloned())
.collect();
match Command::new(cmd).args(all_args).status() {
Ok(status) => match status.code() {
Some(0) => ExitCode::Success,
Some(_) => ExitCode::CommandFailed,
None => ExitCode::CommandFailed,
},
Err(e) => {
eprintln!("Failed to execute {}: {}", cmd, e);
ExitCode::CommandFailed
}
}
}
}
impl fmt::Display for Shadow {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "{} → {}", self.original, self.replacement)?;
if let Some(path) = &self.bin_path {
write!(f, " (in {})", path.display())?;
}
Ok(())
}
}