diff options
author | Freya Murphy <freya@freyacat.org> | 2025-01-16 14:45:14 -0500 |
---|---|---|
committer | Freya Murphy <freya@freyacat.org> | 2025-01-16 14:45:14 -0500 |
commit | 60985236c7070425c28aa0c5ce675306d06ab123 (patch) | |
tree | 7448aef5dadf19d4504151ebc370f9e20757ef10 | |
download | iris-60985236c7070425c28aa0c5ce675306d06ab123.tar.gz iris-60985236c7070425c28aa0c5ce675306d06ab123.tar.bz2 iris-60985236c7070425c28aa0c5ce675306d06ab123.zip |
-rw-r--r-- | .gitignore | 1 | ||||
-rw-r--r-- | Cargo.lock | 971 | ||||
-rw-r--r-- | Cargo.toml | 24 | ||||
-rw-r--r-- | LICENSE | 165 | ||||
-rw-r--r-- | rustfmt.toml | 7 | ||||
-rw-r--r-- | src/error.rs | 49 | ||||
-rw-r--r-- | src/lib.rs | 52 | ||||
-rw-r--r-- | src/main.rs | 193 | ||||
-rw-r--r-- | src/model/config.rs | 110 | ||||
-rw-r--r-- | src/model/mod.rs | 3 | ||||
-rw-r--r-- | src/model/plugin.rs | 283 | ||||
-rw-r--r-- | src/model/repo.rs | 123 | ||||
-rw-r--r-- | src/parse/de.rs | 291 | ||||
-rw-r--r-- | src/parse/mod.rs | 2 | ||||
-rw-r--r-- | src/parse/ser.rs | 62 | ||||
-rw-r--r-- | src/path.rs | 69 |
16 files changed, 2405 insertions, 0 deletions
diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..eb5a316 --- /dev/null +++ b/.gitignore @@ -0,0 +1 @@ +target diff --git a/Cargo.lock b/Cargo.lock new file mode 100644 index 0000000..eb42e4f --- /dev/null +++ b/Cargo.lock @@ -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", +] diff --git a/Cargo.toml b/Cargo.toml new file mode 100644 index 0000000..c1c37e2 --- /dev/null +++ b/Cargo.toml @@ -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" @@ -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. diff --git a/rustfmt.toml b/rustfmt.toml new file mode 100644 index 0000000..52e6f5f --- /dev/null +++ b/rustfmt.toml @@ -0,0 +1,7 @@ +hard_tabs = true +tab_spaces = 4 +max_width = 80 + +use_field_init_shorthand = true + + diff --git a/src/error.rs b/src/error.rs new file mode 100644 index 0000000..65e7aff --- /dev/null +++ b/src/error.rs @@ -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>; diff --git a/src/lib.rs b/src/lib.rs new file mode 100644 index 0000000..27d4e6c --- /dev/null +++ b/src/lib.rs @@ -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) +} diff --git a/src/main.rs b/src/main.rs new file mode 100644 index 0000000..86cd716 --- /dev/null +++ b/src/main.rs @@ -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); + } +} diff --git a/src/model/config.rs b/src/model/config.rs new file mode 100644 index 0000000..844609d --- /dev/null +++ b/src/model/config.rs @@ -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(()) + } +} diff --git a/src/model/mod.rs b/src/model/mod.rs new file mode 100644 index 0000000..41088af --- /dev/null +++ b/src/model/mod.rs @@ -0,0 +1,3 @@ +pub mod config; +pub mod plugin; +pub mod repo; diff --git a/src/model/plugin.rs b/src/model/plugin.rs new file mode 100644 index 0000000..bb097c6 --- /dev/null +++ b/src/model/plugin.rs @@ -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) + } +} diff --git a/src/model/repo.rs b/src/model/repo.rs new file mode 100644 index 0000000..557efb4 --- /dev/null +++ b/src/model/repo.rs @@ -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 {} diff --git a/src/parse/de.rs b/src/parse/de.rs new file mode 100644 index 0000000..4b312a4 --- /dev/null +++ b/src/parse/de.rs @@ -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) + } +} diff --git a/src/parse/mod.rs b/src/parse/mod.rs new file mode 100644 index 0000000..2e6ab62 --- /dev/null +++ b/src/parse/mod.rs @@ -0,0 +1,2 @@ +pub mod de; +pub mod ser; diff --git a/src/parse/ser.rs b/src/parse/ser.rs new file mode 100644 index 0000000..3a29fde --- /dev/null +++ b/src/parse/ser.rs @@ -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) + } +} diff --git a/src/path.rs b/src/path.rs new file mode 100644 index 0000000..b7fdf91 --- /dev/null +++ b/src/path.rs @@ -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") +} |