This commit is contained in:
Murphy 2025-01-16 14:45:14 -05:00
commit 60985236c7
Signed by: freya
GPG key ID: 9FBC6FFD6D2DBF17
16 changed files with 2405 additions and 0 deletions

1
.gitignore vendored Normal file
View file

@ -0,0 +1 @@
target

971
Cargo.lock generated Normal file
View file

@ -0,0 +1,971 @@
# This file is automatically @generated by Cargo.
# It is not intended for manual editing.
version = 4
[[package]]
name = "aho-corasick"
version = "1.1.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8e60d3430d3a69478ad0993f19238d2df97c507009a52b3c10addcd7f6bcb916"
dependencies = [
"memchr",
]
[[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 = "bitflags"
version = "2.7.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1be3f42a67d6d345ecd59f675f3f012d6974981560836e938c22b424b85ce1be"
[[package]]
name = "cc"
version = "1.2.7"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a012a0df96dd6d06ba9a1b29d6402d1a5d77c6befd2566afdc26e10603dc93d7"
dependencies = [
"jobserver",
"libc",
"shlex",
]
[[package]]
name = "cfg-if"
version = "1.0.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd"
[[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 = "displaydoc"
version = "0.2.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "97369cbbc041bc366949bc74d34658d6cda5621039731c6310521892a3a20ae0"
dependencies = [
"proc-macro2",
"quote",
"syn",
]
[[package]]
name = "env_filter"
version = "0.1.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "186e05a59d4c50738528153b83b0b0194d3a29507dfec16eccd4b342903397d0"
dependencies = [
"log",
"regex",
]
[[package]]
name = "env_logger"
version = "0.11.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "dcaee3d8e3cfc3fd92428d477bc97fc29ec8716d180c0d74c643bb26166660e0"
dependencies = [
"anstream",
"anstyle",
"env_filter",
"humantime",
"log",
]
[[package]]
name = "equivalent"
version = "1.0.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5443807d6dff69373d433ab9ef5378ad8df50ca6298caf15de6e52e24aaf54d5"
[[package]]
name = "form_urlencoded"
version = "1.2.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e13624c2627564efccf4934284bdd98cbaa14e79b0b5a141218e507b3a823456"
dependencies = [
"percent-encoding",
]
[[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 = "git2"
version = "0.20.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3fda788993cc341f69012feba8bf45c0ba4f3291fcc08e214b4d5a7332d88aff"
dependencies = [
"bitflags",
"libc",
"libgit2-sys",
"log",
"openssl-probe",
"openssl-sys",
"url",
]
[[package]]
name = "hashbrown"
version = "0.15.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "bf151400ff0baff5465007dd2f3e717f3fe502074ca563069ce3a6629d07b289"
[[package]]
name = "humantime"
version = "2.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9a3a5bfb195931eeb336b2a7b4d761daec841b97f947d34394601737a7bba5e4"
[[package]]
name = "icu_collections"
version = "1.5.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "db2fa452206ebee18c4b5c2274dbf1de17008e874b4dc4f0aea9d01ca79e4526"
dependencies = [
"displaydoc",
"yoke",
"zerofrom",
"zerovec",
]
[[package]]
name = "icu_locid"
version = "1.5.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "13acbb8371917fc971be86fc8057c41a64b521c184808a698c02acc242dbf637"
dependencies = [
"displaydoc",
"litemap",
"tinystr",
"writeable",
"zerovec",
]
[[package]]
name = "icu_locid_transform"
version = "1.5.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "01d11ac35de8e40fdeda00d9e1e9d92525f3f9d887cdd7aa81d727596788b54e"
dependencies = [
"displaydoc",
"icu_locid",
"icu_locid_transform_data",
"icu_provider",
"tinystr",
"zerovec",
]
[[package]]
name = "icu_locid_transform_data"
version = "1.5.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "fdc8ff3388f852bede6b579ad4e978ab004f139284d7b28715f773507b946f6e"
[[package]]
name = "icu_normalizer"
version = "1.5.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "19ce3e0da2ec68599d193c93d088142efd7f9c5d6fc9b803774855747dc6a84f"
dependencies = [
"displaydoc",
"icu_collections",
"icu_normalizer_data",
"icu_properties",
"icu_provider",
"smallvec",
"utf16_iter",
"utf8_iter",
"write16",
"zerovec",
]
[[package]]
name = "icu_normalizer_data"
version = "1.5.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f8cafbf7aa791e9b22bec55a167906f9e1215fd475cd22adfcf660e03e989516"
[[package]]
name = "icu_properties"
version = "1.5.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "93d6020766cfc6302c15dbbc9c8778c37e62c14427cb7f6e601d849e092aeef5"
dependencies = [
"displaydoc",
"icu_collections",
"icu_locid_transform",
"icu_properties_data",
"icu_provider",
"tinystr",
"zerovec",
]
[[package]]
name = "icu_properties_data"
version = "1.5.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "67a8effbc3dd3e4ba1afa8ad918d5684b8868b3b26500753effea8d2eed19569"
[[package]]
name = "icu_provider"
version = "1.5.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6ed421c8a8ef78d3e2dbc98a973be2f3770cb42b606e3ab18d6237c4dfde68d9"
dependencies = [
"displaydoc",
"icu_locid",
"icu_provider_macros",
"stable_deref_trait",
"tinystr",
"writeable",
"yoke",
"zerofrom",
"zerovec",
]
[[package]]
name = "icu_provider_macros"
version = "1.5.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1ec89e9337638ecdc08744df490b221a7399bf8d164eb52a665454e60e075ad6"
dependencies = [
"proc-macro2",
"quote",
"syn",
]
[[package]]
name = "idna"
version = "1.0.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "686f825264d630750a544639377bae737628043f20d38bbc029e8f29ea968a7e"
dependencies = [
"idna_adapter",
"smallvec",
"utf8_iter",
]
[[package]]
name = "idna_adapter"
version = "1.2.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "daca1df1c957320b2cf139ac61e7bd64fed304c5040df000a745aa1de3b4ef71"
dependencies = [
"icu_normalizer",
"icu_properties",
]
[[package]]
name = "indexmap"
version = "2.7.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "62f822373a4fe84d4bb149bf54e584a7f4abec90e072ed49cda0edea5b95471f"
dependencies = [
"equivalent",
"hashbrown",
]
[[package]]
name = "iris"
version = "0.1.0"
dependencies = [
"dirs",
"env_logger",
"git2",
"log",
"serde",
"thiserror 2.0.10",
"toml",
"url",
]
[[package]]
name = "is_terminal_polyfill"
version = "1.70.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7943c866cc5cd64cbc25b2e01621d07fa8eb2a1a23160ee81ce38704e97b8ecf"
[[package]]
name = "jobserver"
version = "0.1.32"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "48d1dbcbbeb6a7fec7e059840aa538bd62aaccf972c7346c4d9d2059312853d0"
dependencies = [
"libc",
]
[[package]]
name = "libc"
version = "0.2.169"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b5aba8db14291edd000dfcc4d620c7ebfb122c613afb886ca8803fa4e128a20a"
[[package]]
name = "libgit2-sys"
version = "0.18.0+1.9.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e1a117465e7e1597e8febea8bb0c410f1c7fb93b1e1cddf34363f8390367ffec"
dependencies = [
"cc",
"libc",
"libssh2-sys",
"libz-sys",
"openssl-sys",
"pkg-config",
]
[[package]]
name = "libredox"
version = "0.1.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c0ff37bd590ca25063e35af745c343cb7a0271906fb7b37e4813e8f79f00268d"
dependencies = [
"bitflags",
"libc",
]
[[package]]
name = "libssh2-sys"
version = "0.3.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2dc8a030b787e2119a731f1951d6a773e2280c660f8ec4b0f5e1505a386e71ee"
dependencies = [
"cc",
"libc",
"libz-sys",
"openssl-sys",
"pkg-config",
"vcpkg",
]
[[package]]
name = "libz-sys"
version = "1.1.21"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "df9b68e50e6e0b26f672573834882eb57759f6db9b3be2ea3c35c91188bb4eaa"
dependencies = [
"cc",
"libc",
"pkg-config",
"vcpkg",
]
[[package]]
name = "litemap"
version = "0.7.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "4ee93343901ab17bd981295f2cf0026d4ad018c7c31ba84549a4ddbb47a45104"
[[package]]
name = "log"
version = "0.4.22"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a7a70ba024b9dc04c27ea2f0c0548feb474ec5c54bba33a7f72f873a39d07b24"
[[package]]
name = "memchr"
version = "2.7.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "78ca9ab1a0babb1e7d5695e3530886289c18cf2f87ec19a575a0abdce112e3a3"
[[package]]
name = "openssl-probe"
version = "0.1.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ff011a302c396a5197692431fc1948019154afc178baf7d8e37367442a4601cf"
[[package]]
name = "openssl-sys"
version = "0.9.104"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "45abf306cbf99debc8195b66b7346498d7b10c210de50418b5ccd7ceba08c741"
dependencies = [
"cc",
"libc",
"pkg-config",
"vcpkg",
]
[[package]]
name = "option-ext"
version = "0.2.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "04744f49eae99ab78e0d5c0b603ab218f515ea8cfe5a456d7629ad883a3b6e7d"
[[package]]
name = "percent-encoding"
version = "2.3.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e3148f5046208a5d56bcfc03053e3ca6334e51da8dfb19b6cdc8b306fae3283e"
[[package]]
name = "pkg-config"
version = "0.3.31"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "953ec861398dccce10c670dfeaf3ec4911ca479e9c02154b3a215178c5f566f2"
[[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 = "quote"
version = "1.0.38"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0e4dccaaaf89514f546c693ddc140f729f958c247918a13380cccc6078391acc"
dependencies = [
"proc-macro2",
]
[[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 = "regex"
version = "1.11.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b544ef1b4eac5dc2db33ea63606ae9ffcfac26c1416a2806ae0bf5f56b201191"
dependencies = [
"aho-corasick",
"memchr",
"regex-automata",
"regex-syntax",
]
[[package]]
name = "regex-automata"
version = "0.4.9"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "809e8dc61f6de73b46c85f4c96486310fe304c434cfa43669d7b40f711150908"
dependencies = [
"aho-corasick",
"memchr",
"regex-syntax",
]
[[package]]
name = "regex-syntax"
version = "0.8.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2b15c43186be67a4fd63bee50d0303afffcef381492ebe2c5d87f324e1b8815c"
[[package]]
name = "serde"
version = "1.0.217"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "02fc4265df13d6fa1d00ecff087228cc0a2b5f3c0e87e258d8b94a156e984c70"
dependencies = [
"serde_derive",
]
[[package]]
name = "serde_derive"
version = "1.0.217"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5a9bf7cf98d04a2b28aead066b7496853d4779c9cc183c440dbac457641e19a0"
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 = "shlex"
version = "1.3.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0fda2ff0d084019ba4d7c6f371c95d8fd75ce3524c3cb8fb653a3023f6323e64"
[[package]]
name = "smallvec"
version = "1.13.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3c5e1a9a646d36c3599cd173a41282daf47c44583ad367b8e6837255952e5c67"
[[package]]
name = "stable_deref_trait"
version = "1.2.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a8f112729512f8e442d81f95a8a7ddf2b7c6b8a1a6f509a95864142b30cab2d3"
[[package]]
name = "syn"
version = "2.0.96"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d5d0adab1ae378d7f53bdebc67a39f1f151407ef230f0ce2883572f5d8985c80"
dependencies = [
"proc-macro2",
"quote",
"unicode-ident",
]
[[package]]
name = "synstructure"
version = "0.13.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c8af7666ab7b6390ab78131fb5b0fce11d6b7a6951602017c35fa82800708971"
dependencies = [
"proc-macro2",
"quote",
"syn",
]
[[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.10"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a3ac7f54ca534db81081ef1c1e7f6ea8a3ef428d2fc069097c079443d24124d3"
dependencies = [
"thiserror-impl 2.0.10",
]
[[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.10"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9e9465d30713b56a37ede7185763c3492a91be2f5fa68d958c44e41ab9248beb"
dependencies = [
"proc-macro2",
"quote",
"syn",
]
[[package]]
name = "tinystr"
version = "0.7.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9117f5d4db391c1cf6927e7bea3db74b9a1c1add8f7eda9ffd5364f40f57b82f"
dependencies = [
"displaydoc",
"zerovec",
]
[[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 = "unicode-ident"
version = "1.0.14"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "adb9e6ca4f869e1180728b7950e35922a7fc6397f7b641499e8f3ef06e50dc83"
[[package]]
name = "url"
version = "2.5.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "32f8b686cadd1473f4bd0117a5d28d36b1ade384ea9b5069a1c40aefed7fda60"
dependencies = [
"form_urlencoded",
"idna",
"percent-encoding",
]
[[package]]
name = "utf16_iter"
version = "1.0.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c8232dd3cdaed5356e0f716d285e4b40b932ac434100fe9b7e0e8e935b9e6246"
[[package]]
name = "utf8_iter"
version = "1.0.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b6c140620e7ffbb22c2dee59cafe6084a59b5ffc27a8859a5f0d494b5d52b6be"
[[package]]
name = "utf8parse"
version = "0.2.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "06abde3611657adf66d383f00b093d7faecc7fa57071cce2578660c9f1010821"
[[package]]
name = "vcpkg"
version = "0.2.15"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "accd4ea62f7bb7a82fe23066fb0957d48ef677f6eeb8215f372f52e48bb32426"
[[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.22"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "39281189af81c07ec09db316b302a3e67bf9bd7cbf6c820b50e35fee9c2fa980"
dependencies = [
"memchr",
]
[[package]]
name = "write16"
version = "1.0.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d1890f4022759daae28ed4fe62859b1236caebfc61ede2f63ed4e695f3f6d936"
[[package]]
name = "writeable"
version = "0.5.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1e9df38ee2d2c3c5948ea468a8406ff0db0b29ae1ffde1bcf20ef305bcc95c51"
[[package]]
name = "yoke"
version = "0.7.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "120e6aef9aa629e3d4f52dc8cc43a015c7724194c97dfaf45180d2daf2b77f40"
dependencies = [
"serde",
"stable_deref_trait",
"yoke-derive",
"zerofrom",
]
[[package]]
name = "yoke-derive"
version = "0.7.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2380878cad4ac9aac1e2435f3eb4020e8374b5f13c296cb75b4620ff8e229154"
dependencies = [
"proc-macro2",
"quote",
"syn",
"synstructure",
]
[[package]]
name = "zerofrom"
version = "0.1.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "cff3ee08c995dee1859d998dea82f7374f2826091dd9cd47def953cae446cd2e"
dependencies = [
"zerofrom-derive",
]
[[package]]
name = "zerofrom-derive"
version = "0.1.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "595eed982f7d355beb85837f651fa22e90b3c044842dc7f2c2842c086f295808"
dependencies = [
"proc-macro2",
"quote",
"syn",
"synstructure",
]
[[package]]
name = "zerovec"
version = "0.10.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "aa2b893d79df23bfb12d5461018d408ea19dfafe76c2c7ef6d4eba614f8ff079"
dependencies = [
"yoke",
"zerofrom",
"zerovec-derive",
]
[[package]]
name = "zerovec-derive"
version = "0.10.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6eafa6dfb17584ea3e2bd6e76e0cc15ad7af12b09abdd1ca55961bed9b1063c6"
dependencies = [
"proc-macro2",
"quote",
"syn",
]

24
Cargo.toml Normal file
View file

@ -0,0 +1,24 @@
[package]
name = "iris"
version = "0.1.0"
authors = ["Freya Murphy <contact@freyacat.org>"]
edition = "2021"
description = "A locking plugin manager for vim"
license-file = "LICENSE"
[lints.rust]
unsafe_code = "forbid"
[lints.clippy]
all = "warn"
nursery = "warn"
[dependencies]
dirs = "5"
env_logger = "0.11.6"
git2 = "0.20"
log = "0.4"
serde = "1"
thiserror = "2"
toml = "0.8"
url = "2"

165
LICENSE Normal file
View file

@ -0,0 +1,165 @@
GNU LESSER GENERAL PUBLIC LICENSE
Version 3, 29 June 2007
Copyright (C) 2007 Free Software Foundation, Inc. <https://fsf.org/>
Everyone is permitted to copy and distribute verbatim copies
of this license document, but changing it is not allowed.
This version of the GNU Lesser General Public License incorporates
the terms and conditions of version 3 of the GNU General Public
License, supplemented by the additional permissions listed below.
0. Additional Definitions.
As used herein, "this License" refers to version 3 of the GNU Lesser
General Public License, and the "GNU GPL" refers to version 3 of the GNU
General Public License.
"The Library" refers to a covered work governed by this License,
other than an Application or a Combined Work as defined below.
An "Application" is any work that makes use of an interface provided
by the Library, but which is not otherwise based on the Library.
Defining a subclass of a class defined by the Library is deemed a mode
of using an interface provided by the Library.
A "Combined Work" is a work produced by combining or linking an
Application with the Library. The particular version of the Library
with which the Combined Work was made is also called the "Linked
Version".
The "Minimal Corresponding Source" for a Combined Work means the
Corresponding Source for the Combined Work, excluding any source code
for portions of the Combined Work that, considered in isolation, are
based on the Application, and not on the Linked Version.
The "Corresponding Application Code" for a Combined Work means the
object code and/or source code for the Application, including any data
and utility programs needed for reproducing the Combined Work from the
Application, but excluding the System Libraries of the Combined Work.
1. Exception to Section 3 of the GNU GPL.
You may convey a covered work under sections 3 and 4 of this License
without being bound by section 3 of the GNU GPL.
2. Conveying Modified Versions.
If you modify a copy of the Library, and, in your modifications, a
facility refers to a function or data to be supplied by an Application
that uses the facility (other than as an argument passed when the
facility is invoked), then you may convey a copy of the modified
version:
a) under this License, provided that you make a good faith effort to
ensure that, in the event an Application does not supply the
function or data, the facility still operates, and performs
whatever part of its purpose remains meaningful, or
b) under the GNU GPL, with none of the additional permissions of
this License applicable to that copy.
3. Object Code Incorporating Material from Library Header Files.
The object code form of an Application may incorporate material from
a header file that is part of the Library. You may convey such object
code under terms of your choice, provided that, if the incorporated
material is not limited to numerical parameters, data structure
layouts and accessors, or small macros, inline functions and templates
(ten or fewer lines in length), you do both of the following:
a) Give prominent notice with each copy of the object code that the
Library is used in it and that the Library and its use are
covered by this License.
b) Accompany the object code with a copy of the GNU GPL and this license
document.
4. Combined Works.
You may convey a Combined Work under terms of your choice that,
taken together, effectively do not restrict modification of the
portions of the Library contained in the Combined Work and reverse
engineering for debugging such modifications, if you also do each of
the following:
a) Give prominent notice with each copy of the Combined Work that
the Library is used in it and that the Library and its use are
covered by this License.
b) Accompany the Combined Work with a copy of the GNU GPL and this license
document.
c) For a Combined Work that displays copyright notices during
execution, include the copyright notice for the Library among
these notices, as well as a reference directing the user to the
copies of the GNU GPL and this license document.
d) Do one of the following:
0) Convey the Minimal Corresponding Source under the terms of this
License, and the Corresponding Application Code in a form
suitable for, and under terms that permit, the user to
recombine or relink the Application with a modified version of
the Linked Version to produce a modified Combined Work, in the
manner specified by section 6 of the GNU GPL for conveying
Corresponding Source.
1) Use a suitable shared library mechanism for linking with the
Library. A suitable mechanism is one that (a) uses at run time
a copy of the Library already present on the user's computer
system, and (b) will operate properly with a modified version
of the Library that is interface-compatible with the Linked
Version.
e) Provide Installation Information, but only if you would otherwise
be required to provide such information under section 6 of the
GNU GPL, and only to the extent that such information is
necessary to install and execute a modified version of the
Combined Work produced by recombining or relinking the
Application with a modified version of the Linked Version. (If
you use option 4d0, the Installation Information must accompany
the Minimal Corresponding Source and Corresponding Application
Code. If you use option 4d1, you must provide the Installation
Information in the manner specified by section 6 of the GNU GPL
for conveying Corresponding Source.)
5. Combined Libraries.
You may place library facilities that are a work based on the
Library side by side in a single library together with other library
facilities that are not Applications and are not covered by this
License, and convey such a combined library under terms of your
choice, if you do both of the following:
a) Accompany the combined library with a copy of the same work based
on the Library, uncombined with any other library facilities,
conveyed under the terms of this License.
b) Give prominent notice with the combined library that part of it
is a work based on the Library, and explaining where to find the
accompanying uncombined form of the same work.
6. Revised Versions of the GNU Lesser General Public License.
The Free Software Foundation may publish revised and/or new versions
of the GNU Lesser General Public License from time to time. Such new
versions will be similar in spirit to the present version, but may
differ in detail to address new problems or concerns.
Each version is given a distinguishing version number. If the
Library as you received it specifies that a certain numbered version
of the GNU Lesser General Public License "or any later version"
applies to it, you have the option of following the terms and
conditions either of that published version or of any later version
published by the Free Software Foundation. If the Library as you
received it does not specify a version number of the GNU Lesser
General Public License, you may choose any version of the GNU Lesser
General Public License ever published by the Free Software Foundation.
If the Library as you received it specifies that a proxy can decide
whether future versions of the GNU Lesser General Public License shall
apply, that proxy's public statement of acceptance of any version is
permanent authorization for you to choose that version for the
Library.

7
rustfmt.toml Normal file
View file

@ -0,0 +1,7 @@
hard_tabs = true
tab_spaces = 4
max_width = 80
use_field_init_shorthand = true

49
src/error.rs Normal file
View file

@ -0,0 +1,49 @@
//! Definition of an iris [error][Error].
use std::fmt;
use std::io;
macro_rules! error {
($($arg:tt)*) => {
$crate::error::Error::new(format!($($arg)*))
};
}
pub(crate) use error;
/// Errors that can occur in iris
#[derive(thiserror::Error, Debug)]
pub enum Error {
/// Occurs when a file could not be read, or could not be written to.
#[error(transparent)]
Io(#[from] io::Error),
/// Occurs when an error was raised from the local git repository
#[error(transparent)]
Git(#[from] git2::Error),
/// Occurs when iris failed to convert its internal structures into the
/// config string representation.
#[error(transparent)]
De(#[from] crate::parse::de::Error),
/// Occurs when a provided string representation of an iris structure is
/// invalid and could not be converted/parsed.
#[error(transparent)]
Ser(#[from] crate::parse::ser::Error),
/// Generic error message, occurs anywhere
#[error("{0}")]
Custom(String),
}
impl Error {
pub fn new(fmt: impl fmt::Display) -> Self {
Self::Custom(fmt.to_string())
}
}
impl From<&str> for Error {
fn from(msg: &str) -> Self {
Self::new(msg)
}
}
/// iris result type
pub type Result<T> = std::result::Result<T, Error>;

52
src/lib.rs Normal file
View file

@ -0,0 +1,52 @@
//! # Iris
//! A locking plugin manager for vim
mod model;
pub use model::{config::Config, plugin::Plugin, repo::Repository};
mod parse;
mod error;
pub use error::{Error, Result};
pub mod path;
use std::{
fs::File,
io::{BufWriter, Write},
path::Path,
};
/// Iris copyright header prepended to all generated files
const COPYRIGHT_HEADER: &str = "\
IRIS - A locking plugin manager for vim
Copyright © 2025 Freya Murphy <contact@freyacat.org>
This file is part of IRIS
IRIS is free software; you can redistribute it and/or modify it
under the terms of the GNU General Public License as published by
the Free Software Foundation; either version 3 of the License, or (at
your option) any later version.
IRIS is distributed in the hope that it will be useful, but
WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with IRIS. If not, see <http://www.gnu.org/licenses/>.";
/// Opens file writer at `path` with copyright header already written
pub(crate) fn get_writer<P>(path: P, c: char) -> Result<BufWriter<File>>
where
P: AsRef<Path>,
{
let file = File::create(path)?;
let mut w = BufWriter::new(file);
for line in COPYRIGHT_HEADER.lines() {
writeln!(w, "{c}{c}{c} {line}")?;
}
writeln!(w)?;
Ok(w)
}

193
src/main.rs Normal file
View file

@ -0,0 +1,193 @@
//! # Iris
//! A locking plugin manager for vim
use iris::{Config, Result};
use log::info;
use std::{env, process::exit};
const USAGE: &str = "\
Usage: iris [OPTION]... COMMAND [FILE}";
const HELP: &str = "\
A locking plugin manager for vim
Commands:
switch checkout plugins using their current refs and generate autoload
file
lock update plugin refs to point to the latest commit
Options:
-v, --verbose enable verbose output
-q, --quiet disable output
-h, --help print the help message
-V, --version print the program version
";
/// print the program version
fn version() -> ! {
println!("iris {}", env!("CARGO_PKG_VERSION"));
exit(0);
}
/// print the program's usage
fn usage() -> ! {
eprintln!("{USAGE}");
exit(1);
}
/// print the program's help message
fn help() -> ! {
println!("{USAGE}\n{HELP}");
exit(0);
}
struct Options {
/// the command to run
command: String,
/// the file to load instead of defaults
file: Option<String>,
/// print verbose output
verbose: bool,
/// silence output
quiet: bool,
}
// parse options
fn opts() -> Options {
let mut args = env::args().into_iter().peekable();
let mut quiet = false;
let mut verbose = false;
// skip program name
_ = args.next();
// options
while let Some(arg) = args.peek() {
// arg is not an option
if !arg.starts_with("-") {
break;
};
// grab next
let arg = args.next().expect("no arg?!");
match arg.as_str() {
// stop parsing options
"--" => break,
// quiet
"-q" | "--quiet" => {
quiet = true;
}
// verbose
"-v" | "--verbose" => {
verbose = true;
}
// version
"-V" | "--version" => version(),
// help
"-h" | "--help" => help(),
// invalid
_ => {
eprintln!("invalid options: '{arg}'");
exit(1);
}
};
}
// command
let Some(command) = args.next() else {
usage();
};
// file
let file = args.next();
// force end of arguments
if args.next().is_some() {
usage();
};
Options {
command,
file,
quiet,
verbose,
}
}
fn log(opts: &Options) {
if !opts.quiet {
let level = if opts.verbose {
log::LevelFilter::Trace
} else {
log::LevelFilter::Info
};
env_logger::Builder::from_default_env()
.filter_level(level)
.format_timestamp(None)
.format_module_path(false)
.format_source_path(false)
.init();
}
}
fn switch(opts: &Options) -> Result<()> {
let cfg = match &opts.file {
Some(path) => Config::read_from_file(path),
None => Config::read_from_lock_file()
.or_else(|_| Config::read_from_plugin_file()),
}?;
for plugin in &cfg.plugins {
plugin.switch()?;
info!("switched {}", plugin.id);
}
cfg.write_autoload_file()?;
Ok(())
}
fn lock(opts: &Options) -> Result<()> {
let mut cfg = match &opts.file {
Some(path) => Config::read_from_file(path),
None => Config::read_from_plugin_file(),
}?;
for plugin in &mut cfg.plugins {
plugin.lock()?;
info!("locked {}", plugin.id);
}
// save lock file
cfg.write_to_lock_file()?;
Ok(())
}
fn inner() -> Result<()> {
// parse opts
let opts = opts();
// initalize logger (if not quiet)
log(&opts);
// handle command
match opts.command.as_str() {
"switch" => switch(&opts)?,
"lock" => lock(&opts)?,
cmd => {
eprintln!("unknown command: '{cmd}'");
exit(1);
}
};
Ok(())
}
fn main() {
if let Err(err) = inner() {
log::error!("{err}");
exit(1);
}
}

110
src/model/config.rs Normal file
View file

@ -0,0 +1,110 @@
//! Definition of an iris [config][Config].
use crate::{get_writer, path, Plugin, Result};
use log::trace;
use std::{fs, io::Write, path::Path};
/// Iris config
#[derive(Debug)]
pub struct Config {
/// List of iris plugin specifications
pub plugins: Vec<Plugin>,
}
impl Config {
/// Read an iris config from the file system
pub fn read_from_file<P>(path: P) -> Result<Self>
where
P: AsRef<Path>,
{
trace!("reading: {}", path.as_ref().display());
let contents = fs::read_to_string(&path)?;
let iris = Self::parse(&contents)?;
Ok(iris)
}
/// Read an iris config from the plugin file path
pub fn read_from_plugin_file() -> Result<Self> {
trace!("read from plugin file");
Self::read_from_file(path::plugin_file())
}
/// Read an iris config from the lock file path
pub fn read_from_lock_file() -> Result<Self> {
trace!("read from lock file");
Self::read_from_file(path::lock_file())
}
/// Write the iris config to the file system.
pub fn write_to_file<P>(&self, path: P) -> Result<()>
where
P: AsRef<Path>,
{
trace!("writing: {}", path.as_ref().display());
let contents = self.to_string()?;
let mut write = get_writer(&path, '#')?;
write.write_all(contents.as_bytes())?;
Ok(())
}
/// Write the iris config to the plugins file.
pub fn write_to_plugin_file(&self) -> Result<()> {
self.write_to_file(path::plugin_file())
}
/// Write the iris config to the lock file.
pub fn write_to_lock_file(&self) -> Result<()> {
self.write_to_file(path::lock_file())
}
/// Write the vim autoload file formed from each plugin.
pub fn write_autoload_file(&self) -> Result<()> {
let path = path::autoload_file();
trace!("writing: {}", path.display());
let mut f = get_writer(&path, '"')?;
// BEGIN iris#load
writeln!(f, "function! iris#load()")?;
// add each plugin to load path
for plugin in &self.plugins {
// runtime path
{
let path = plugin.runtime_path().display();
writeln!(f, "set runtimepath+={path}")?;
}
// lua package path
if let Some(path) = plugin.lua_package_path() {
let path = path.display();
writeln!(f, "lua package.path = package.path .. \";{path}\"")?;
}
// vim source file
if let Some(path) = plugin.vim_source_file() {
let path = path.display();
writeln!(f, "source {path}")?;
}
// lua source file
if let Some(path) = plugin.lua_source_file() {
let path = path.display();
writeln!(f, "lua dofile('{path}')")?;
}
}
for plugin in &self.plugins {
// vim source after file
if let Some(path) = plugin.vim_source_after_file() {
let path = path.display();
writeln!(f, "source {path}")?;
}
// lua source after file
if let Some(path) = plugin.lua_source_after_file() {
let path = path.display();
writeln!(f, "lua dofile('{path}')")?;
}
}
writeln!(f, "endfunction")?;
// END iris#load
Ok(())
}
}

3
src/model/mod.rs Normal file
View file

@ -0,0 +1,3 @@
pub mod config;
pub mod plugin;
pub mod repo;

283
src/model/plugin.rs Normal file
View file

@ -0,0 +1,283 @@
//! Defines of an iris [plugin][Plugin].
use crate::{error::error, path, Repository, Result};
use log::{debug, trace, warn};
use std::{borrow::Cow, cmp::Ordering, collections::HashSet, path::PathBuf};
use url::Url;
/// Returns the url to the fallback git server. If a repository url is provided
/// and it does not contain a git server (just author and repo), this url will
/// be used in its absence.
fn fallback_git_server() -> &'static Url {
use std::sync::OnceLock;
static FALLBACK: OnceLock<Url> = OnceLock::new();
FALLBACK.get_or_init(|| {
Url::parse("https://github.com")
.expect("!! IRIS BUG !! invalid fallback git server url")
})
}
/// # Plugin
///
/// A structure representing an iris plugin specification.
///
/// An iris plugin has two parts.
///
/// - [id], must be a unique identifier for this plugin and the plugins
/// module name
/// - [args], a set of arguments to configure this plugin
///
/// [id]: Self::id
/// [args]: Self::args
#[derive(Debug, Eq)]
pub struct Plugin {
/// The plugins unique identifier. Used for the vim module name and
/// repository checkout foler.
pub id: String,
/// The commit oid (if set) that the local repository will be forced to
/// track. If not set then the repository will track the latest commit in
/// the current tracking branch.
pub commit: Option<String>,
/// The branch name (if set) that the local repository will be forced to
/// track. If not set then the repository will track the repositories
/// default branch.
pub branch: Option<String>,
/// Set of plugin ids this plugin should load before
pub run: Option<String>,
/// Handle to the local plugin repository.
pub before: HashSet<String>,
/// Set of plugin ids this plugin should load after
pub after: HashSet<String>,
/// The command to run when vim starts up
repo: Repository,
}
impl Plugin {
/// Create a new iris plugin.
///
/// Creates a plugin from the git repo at `url` with the unique id `id`.
/// The plugin will be unlocked meaning it will always track the latest
/// commit until locked. A handle to a local repository will be created but
/// not loaded until called upon.
pub fn new(id: String, url: &str) -> Result<Self> {
// resolve and parse url
let url = {
// try url as is
Url::parse(url)
// try url with fallback
.or_else(|_| fallback_git_server().join(url))
// invalid git url!
.map_err(|_| error!("invalid git url {url}"))
}?;
// get the path to the local repository checkout
let repo_path = path::plugin_dir().join(&id);
// create repository handle
let repo = Repository::new(url, repo_path);
debug!("found plugin '{id}'");
Ok(Self {
id,
commit: None,
branch: None,
run: None,
before: HashSet::new(),
after: HashSet::new(),
repo,
})
}
/// The url to the remote git repository.
pub const fn url(&self) -> &Url {
&self.repo.url
}
/// The path to the local repository
pub const fn repo_path(&self) -> &PathBuf {
&self.repo.path
}
/// The commit oid that the plugin repository is currently tracking
fn commit(&self) -> Result<Cow<'_, str>> {
// returned locked commit if set
if let Some(commit) = &self.commit {
return Ok(Cow::Borrowed(commit));
}
// get commit from repo
let branch = self.branch()?;
let commit = self.repo.latest_commit(&branch)?;
Ok(Cow::Owned(commit))
}
/// The branch name that the plugin repository is currently tracking
fn branch(&self) -> Result<Cow<'_, str>> {
// return locked branch if set
if let Some(branch) = &self.branch {
return Ok(Cow::Borrowed(branch));
}
// get branch from repo
let branch = self.repo.default_branch()?;
Ok(Cow::Owned(branch))
}
/// Fetches latest changes in the local repository but does not update
/// checkout.
pub fn fetch(&self) -> Result<()> {
trace!("fetching {}", self.id);
let branch = self.branch()?;
self.repo.fetch(&branch)?;
Ok(())
}
/// Checkout plugin repository using their current refs
pub fn switch(&self) -> Result<()> {
trace!("switching {}", self.id);
let commit = self.commit()?;
self.repo.checkout(&commit)?;
Ok(())
}
/// Update plugin ref to point to the latest commit
pub fn lock(&mut self) -> Result<()> {
trace!("locking {}", self.id);
let branch = self.branch()?;
self.repo.fetch(&branch)?;
let commit = self.repo.latest_commit(&branch)?;
self.commit = Some(commit);
Ok(())
}
/// Runtime path
///
/// This this the plugins root directory, and is used by vim/neovim to
/// find the vim/lua runtime scripts.
///
/// vim: set runtimepath+=<path>
pub fn runtime_path(&self) -> &PathBuf {
self.repo_path()
}
/// Lua package path
///
/// This is the path to the lua module if the plugin has one. If so
/// it needs to be added to the lua package path. The path is located
/// in the repos lua directory at /lua/<id>/init.lua or /lua/<id>.lua. If
/// both exist the former takes priority.
///
/// lua: package.path = package.path .. ";<path>"
pub fn lua_package_path(&self) -> Option<PathBuf> {
let mut lua = self.repo_path().to_owned();
lua.push("lua");
lua.push(&self.id);
// /lua/<id>/init.lua
let module = lua.join("init.lua");
if module.exists() {
return Some(module.with_file_name("?.lua"));
}
// /lua/<id>/.lua
let module = lua.with_extension("lua");
if module.exists() {
return Some(module.with_file_name("?.lua"));
}
// no lua package path found
None
}
/// Vim source file
///
/// Path to a vim file in the /plugin directory that needs to be
/// sourced. This is located at /plugin/<id>.vim
///
/// vim: source <path>
pub fn vim_source_file(&self) -> Option<PathBuf> {
let mut path = self.repo_path().to_owned();
path.push("plugin");
path.push(&self.id);
path.set_extension("vim");
Some(path).filter(|path| path.exists())
}
/// Lua source file
///
/// Path to a lua file in the /plugin directory that needs to be
/// sourced. This is located at /plugin/<id>.lua
///
/// lua: dofile('<path>')
pub fn lua_source_file(&self) -> Option<PathBuf> {
let mut path = self.repo_path().to_owned();
path.push("plugin");
path.push(&self.id);
path.set_extension("lua");
Some(path).filter(|path| path.exists())
}
/// Vim source after file
///
/// Path to a vim file in the /after/plugin directory that needs to be
/// sourced last. This is located at /after/plugin/<id>.vim
///
/// vim: source <path>
pub fn vim_source_after_file(&self) -> Option<PathBuf> {
let mut path = self.repo_path().to_owned();
path.push("after/plugin");
path.push(&self.id);
path.set_extension("vim");
Some(path).filter(|path| path.exists())
}
/// Lua source after file
///
/// Path to a lua file in the /after/plugin directory that needs to be
/// sourced last. This is located at /after/plugin/<id>.lua
///
/// lua: dofile('<path>')
pub fn lua_source_after_file(&self) -> Option<PathBuf> {
let mut path = self.repo_path().to_owned();
path.push("after/plugin");
path.push(&self.id);
path.set_extension("lua");
Some(path).filter(|path| path.exists())
}
}
impl PartialEq for Plugin {
fn eq(&self, other: &Self) -> bool {
self.id.eq(&other.id)
}
}
impl PartialOrd for Plugin {
fn partial_cmp(&self, other: &Self) -> Option<std::cmp::Ordering> {
let is_after =
other.before.contains(&self.id) || self.after.contains(&other.id);
let is_before =
other.after.contains(&self.id) || self.before.contains(&other.id);
match (is_before, is_after) {
// equal, no ordering
(false, false) => Some(Ordering::Equal),
// before
(true, false) => Some(Ordering::Less),
// after
(false, true) => Some(Ordering::Greater),
// cycle detected
(true, true) => {
warn!(
"plugin dependency cycle detected between '{}' and '{}'",
&self.id, &other.id
);
None
}
}
}
}
impl Ord for Plugin {
fn cmp(&self, other: &Self) -> Ordering {
self.partial_cmp(other).unwrap_or(Ordering::Equal)
}
}

123
src/model/repo.rs Normal file
View file

@ -0,0 +1,123 @@
//! Definition of an iris [repo][Repository].
use crate::{error, Result};
use log::{debug, trace};
use std::{path::PathBuf, sync::OnceLock};
use url::Url;
/// A handle to a locally stored git repository.
pub struct Repository {
/// The remote url that the git repository is located at.
pub url: Url,
/// The path on disk that the this repository is/will be located at.
pub path: PathBuf,
/// The git2 lib handle to the repo
inner: OnceLock<git2::Repository>,
}
impl Repository {
/// Creates a new handle to a local git repository.
pub const fn new(url: Url, path: PathBuf) -> Self {
Self {
url,
path,
inner: OnceLock::new(),
}
}
/// Gets the handle to the local repository
fn repo(&self) -> Result<&git2::Repository> {
// we only need to initalize once
if let Some(repo) = self.inner.get() {
return Ok(repo);
}
trace!("loading repo");
// repository not loaded, try loading it
let repo = if self.path.exists() {
// repo exists on file system, just open it
git2::Repository::open(&self.path)?
} else {
// repo needs to be cloned
git2::Repository::clone(self.url.as_str(), &self.path)?
};
// save and return repo
Ok(self.inner.get_or_init(|| repo))
}
/// Gets the current remote in the git repository
fn remote(&self) -> Result<git2::Remote> {
let repo = self.repo()?;
let remote_name = "origin";
let mut remote = repo.find_remote(remote_name)?;
// make sure that out remote is connected before
// we do anything with it. otherwise things will
// fail!
if !remote.connected() {
trace!("connecting to remote");
remote.connect(git2::Direction::Fetch)?;
}
Ok(remote)
}
/// Fetches the latest commits in the provided branch.
pub fn fetch(&self, branch: &str) -> Result<()> {
let mut remote = self.remote()?;
trace!("fetching '{branch}");
remote.fetch(&[branch], None, None)?;
Ok(())
}
/// Returns the default branch of the git repositroy.
pub fn default_branch(&self) -> Result<String> {
let remote = self.remote()?;
let buf = remote.default_branch()?;
let name = buf
.as_str()
.and_then(|s| s.strip_prefix("refs/heads/"))
.map(String::from)
.ok_or_else(|| error::error!("cannot get default branch"))?;
debug!("default branch is '{name}'");
Ok(name)
}
/// Returns the latest fetched commit in `branch`.
pub fn latest_commit(&self, branch: &str) -> Result<String> {
let repo = self.repo()?;
let refr_name = format!("refs/remotes/origin/{branch}");
let refr = repo.find_reference(&refr_name)?;
let commit = refr.peel_to_commit()?;
let oid = commit.id().to_string();
Ok(oid)
}
/// Checks out the local repository to the given commit and detaches
/// HEAD to that commit.
pub fn checkout(&self, commit: &str) -> Result<()> {
let repo = self.repo()?;
trace!("checkout to '{commit}'");
let oid = git2::Oid::from_str(commit)?;
let commit = repo.find_commit(oid)?;
repo.reset(commit.as_object(), git2::ResetType::Hard, None)?;
Ok(())
}
}
impl std::fmt::Debug for Repository {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(f, "_")
}
}
impl PartialEq for Repository {
fn eq(&self, other: &Self) -> bool {
self.url.eq(&other.url) && self.path.eq(&other.path)
}
}
impl Eq for Repository {}

291
src/parse/de.rs Normal file
View file

@ -0,0 +1,291 @@
//! Deserialize an iris structure from a string.
use crate::{Config, Plugin};
use serde::de;
use std::collections::HashSet;
use std::fmt;
use std::str::FromStr;
/// Errors that can occur when deserializing a type
#[derive(thiserror::Error, Debug)]
pub enum Error {
/// Occurs when toml could not be deserialized
#[error(transparent)]
Toml(#[from] toml::de::Error),
}
macro_rules! error {
($($arg:tt)*) => {
de::Error::custom(format!($($arg)*))
};
}
struct PluginIDsVisitor;
impl<'de> de::Visitor<'de> for PluginIDsVisitor {
type Value = Vec<String>;
fn expecting(&self, f: &mut fmt::Formatter) -> fmt::Result {
write!(f, "single or list of plugin ids")
}
fn visit_str<E>(self, v: &str) -> Result<Self::Value, E>
where
E: de::Error,
{
self.visit_string(v.to_string())
}
fn visit_string<E>(self, v: String) -> Result<Self::Value, E>
where
E: de::Error,
{
Ok(vec![v])
}
fn visit_seq<A>(self, mut seq: A) -> Result<Self::Value, A::Error>
where
A: de::SeqAccess<'de>,
{
let mut values =
seq.size_hint().map_or_else(Vec::new, Vec::with_capacity);
while let Some(value) = seq.next_element::<String>()? {
values.push(value);
}
Ok(values)
}
}
struct PluginIDsSeed;
impl<'de> de::DeserializeSeed<'de> for PluginIDsSeed {
type Value = HashSet<String>;
fn deserialize<D>(self, d: D) -> Result<Self::Value, D::Error>
where
D: de::Deserializer<'de>,
{
let values = d.deserialize_any(PluginIDsVisitor)?;
Ok(HashSet::from_iter(values))
}
}
// plugin visitor with seeded plugin id
struct PluginVisitor(Option<String>);
impl<'de> de::Visitor<'de> for PluginVisitor {
type Value = Plugin;
fn expecting(&self, f: &mut fmt::Formatter) -> fmt::Result {
match &self.0 {
Some(id) => write!(f, "arguments for plugin '{id}'"),
None => write!(f, "plugin id and arguments"),
}
}
fn visit_str<E>(self, v: &str) -> Result<Self::Value, E>
where
E: de::Error,
{
self.visit_string(v.to_string())
}
fn visit_borrowed_str<E>(self, v: &'_ str) -> Result<Self::Value, E>
where
E: de::Error,
{
self.visit_string(v.to_string())
}
fn visit_string<E>(self, url: String) -> Result<Self::Value, E>
where
E: de::Error,
{
let Some(id) = self.0 else {
return Err(de::Error::missing_field("id"));
};
Self::Value::new(id, &url).map_err(E::custom)
}
fn visit_map<A>(self, mut map: A) -> Result<Self::Value, A::Error>
where
A: de::MapAccess<'de>,
{
// parse each map value as a possbile field for the plugin
let mut id = self.0; // plugin id (required)
let mut url = None; // repository url (required)
let mut commit = None; // commit to lock to
let mut branch = None; // branch to lock to
let mut run = None; // command to run on launch
let mut before = HashSet::new(); // plugins to load before
let mut after = HashSet::new(); // plugins to load after
while let Some(key) = map.next_key::<String>()? {
match key.as_str() {
// plugin id
"id" => {
id = Some(map.next_value::<String>()?);
}
// repo url
"url" => {
url = Some(map.next_value::<String>()?);
}
// locked commit
"commit" => {
commit = Some(map.next_value::<String>()?);
}
// locked branch
"branch" => {
branch = Some(map.next_value::<String>()?);
}
// vim command to run on launch
"run" => {
run = Some(map.next_value::<String>()?);
}
// plugins to load before
"before" => {
before = map.next_value_seed(PluginIDsSeed)?;
}
// plugins to load after
"after" => {
after = map.next_value_seed(PluginIDsSeed)?;
}
// invalid key!
key => return Err(error!("unknown plugin field '{key}'")),
};
}
// id is a required field
let Some(id) = id else {
return Err(de::Error::missing_field("id"));
};
// url is a required field
let Some(url) = url else {
return Err(de::Error::missing_field("url"));
};
let mut plugin =
Self::Value::new(id, &url).map_err(de::Error::custom)?;
plugin.commit = commit;
plugin.branch = branch;
plugin.run = run;
plugin.before = before;
plugin.after = after;
Ok(plugin)
}
}
// deserialize plugin with possible id
struct PluginSeed(Option<String>);
impl<'de> de::DeserializeSeed<'de> for PluginSeed {
type Value = Plugin;
fn deserialize<D>(self, d: D) -> Result<Self::Value, D::Error>
where
D: de::Deserializer<'de>,
{
d.deserialize_any(PluginVisitor(self.0))
}
}
// plugins visitor
struct PluginsVisitor;
impl<'de> de::Visitor<'de> for PluginsVisitor {
type Value = Vec<Plugin>;
fn expecting(&self, f: &mut fmt::Formatter) -> fmt::Result {
f.write_str("map of plugin id's to their arguments")
}
fn visit_map<A>(self, mut map: A) -> Result<Self::Value, A::Error>
where
A: de::MapAccess<'de>,
{
let mut plugins =
map.size_hint().map_or_else(Vec::new, Vec::with_capacity);
while let Some(id) = map.next_key()? {
let plugin = map.next_value_seed(PluginSeed(Some(id)))?;
plugins.push(plugin);
}
Ok(plugins)
}
fn visit_seq<A>(self, mut seq: A) -> Result<Self::Value, A::Error>
where
A: de::SeqAccess<'de>,
{
let mut plugins =
seq.size_hint().map_or_else(Vec::new, Vec::with_capacity);
while let Some(plugin) = seq.next_element_seed(PluginSeed(None))? {
plugins.push(plugin);
}
Ok(plugins)
}
}
// plugins seed
struct PluginsSeed;
impl<'de> de::DeserializeSeed<'de> for PluginsSeed {
type Value = Vec<Plugin>;
fn deserialize<D>(self, d: D) -> Result<Self::Value, D::Error>
where
D: de::Deserializer<'de>,
{
d.deserialize_any(PluginsVisitor)
}
}
// config visitor
struct ConfigVisitor;
impl<'de> de::Visitor<'de> for ConfigVisitor {
type Value = Config;
fn expecting(&self, f: &mut fmt::Formatter) -> fmt::Result {
f.write_str("iris config value")
}
fn visit_map<A>(self, mut map: A) -> Result<Self::Value, A::Error>
where
A: de::MapAccess<'de>,
{
let mut plugins = None; // list of plugins (required)
while let Some(key) = map.next_key::<String>()? {
match key.as_str() {
"plugins" => {
plugins = Some(map.next_value_seed(PluginsSeed)?);
}
key => return Err(error!("unknown config field '{key}'")),
};
}
// plugins is a required field
let Some(mut plugins) = plugins else {
return Err(de::Error::missing_field("plugins"));
};
plugins.sort();
Ok(Config { plugins })
}
}
impl<'de> de::Deserialize<'de> for Config {
fn deserialize<D>(d: D) -> Result<Self, D::Error>
where
D: de::Deserializer<'de>,
{
d.deserialize_map(ConfigVisitor)
}
}
impl Config {
pub fn parse(s: &str) -> crate::Result<Self> {
toml::from_str(s)
.map_err(Error::from)
.map_err(crate::Error::from)
}
}
impl FromStr for Config {
type Err = crate::Error;
fn from_str(s: &str) -> Result<Self, Self::Err> {
Self::parse(s)
}
}

2
src/parse/mod.rs Normal file
View file

@ -0,0 +1,2 @@
pub mod de;
pub mod ser;

62
src/parse/ser.rs Normal file
View file

@ -0,0 +1,62 @@
//! Serialize an iris structure into a string.
use crate::{Config, Plugin};
use serde::ser;
/// Errors that can occur when serializing a type
#[derive(thiserror::Error, Debug)]
pub enum Error {
/// Occurs when toml could not be eserialized
#[error(transparent)]
Toml(#[from] toml::ser::Error),
}
impl ser::Serialize for Plugin {
fn serialize<S>(&self, s: S) -> Result<S::Ok, S::Error>
where
S: ser::Serializer,
{
use ser::SerializeMap;
let mut s = s.serialize_map(None)?;
s.serialize_entry("id", &self.id)?;
s.serialize_entry("url", self.url().as_str())?;
if let Some(commit) = &self.commit {
s.serialize_entry("commit", commit)?;
}
if let Some(branch) = &self.branch {
s.serialize_entry("branch", branch)?;
}
if let Some(run) = &self.run {
s.serialize_entry("run", run)?;
}
s.serialize_entry("before", &self.before)?;
s.serialize_entry("after", &self.after)?;
s.end()
}
}
impl ser::Serialize for Config {
fn serialize<S>(&self, s: S) -> Result<S::Ok, S::Error>
where
S: ser::Serializer,
{
use ser::SerializeMap;
let mut s = s.serialize_map(Some(1))?;
// plugins
s.serialize_key("plugins")?;
s.serialize_value(&self.plugins)?;
s.end()
}
}
impl Config {
/// Serializes a string from the iris config
pub fn to_string(&self) -> crate::Result<String> {
toml::to_string(self)
.map_err(Error::from)
.map_err(crate::Error::from)
}
}

69
src/path.rs Normal file
View file

@ -0,0 +1,69 @@
//! Directory and file paths used by iris.
use std::path::PathBuf;
/// Root of the data directory
fn base_config_dir() -> PathBuf {
dirs::config_local_dir().expect("could not locate config directory")
}
/// Root of the config directory
fn base_data_dir() -> PathBuf {
dirs::data_local_dir().expect("could not locate data directory")
}
/// Path to the current diectort
pub fn current_dir() -> PathBuf {
std::env::current_dir().expect("could not locate current directory")
}
/// Path to iris's config directory.
///
/// This is the directory where iris will look and save it's config files.
pub fn config_dir() -> PathBuf {
base_config_dir().join("iris")
}
/// Default path to iris's plugin file
pub fn default_plugin_file() -> PathBuf {
config_dir().join("iris.toml")
}
/// Path to iris's plugin file
pub fn plugin_file() -> PathBuf {
[current_dir(), config_dir()]
.into_iter()
.map(|dir| dir.join("iris.toml"))
.find(|path| path.exists())
.unwrap_or_else(default_plugin_file)
}
/// Path to iris's lock file
pub fn lock_file() -> PathBuf {
plugin_file().with_file_name("iris.lock")
}
/// Path to iris's data directory.
pub fn data_dir() -> PathBuf {
base_data_dir().join("iris")
}
/// Path to iris's plugin directory.
///
/// This is where each checkout for each plugin repository will be saved.
pub fn plugin_dir() -> PathBuf {
data_dir().join("plugins")
}
/// Path to iris's transaction lock file.
///
/// The transaction lock ensures that only one process is operating on iris's
/// data directory at a time.
pub fn transaction_lock_file() -> PathBuf {
data_dir().join("lock")
}
/// Destination path for iris's vim autoload file.
pub fn autoload_file() -> PathBuf {
base_data_dir().join("nvim/site/autoload/iris.vim")
}