├── .cargo └── config_fast_builds.toml ├── .envrc ├── .gitignore ├── CHANGELOG.md ├── Cargo.lock ├── Cargo.toml ├── LICENSE ├── README.md ├── cliff.toml ├── flake.lock ├── flake.nix ├── manix.png ├── release.toml ├── rust-toolchain.toml ├── rustfmt.toml └── src ├── bin └── manix.rs ├── comments_docsource.rs ├── lib.rs ├── nix ├── darwin-options.nix ├── hm-options.nix └── nixos-options.nix ├── nixpkgs_tree_docsource.rs ├── options_docsource.rs └── xml_docsource.rs /.cargo/config_fast_builds.toml: -------------------------------------------------------------------------------- 1 | # Add the contents of this file to `config.toml` to enable "fast build" configuration. Please read the notes below. 2 | 3 | # NOTE: For maximum performance, build using a nightly compiler 4 | # If you are using rust stable, remove the "-Zshare-generics=y" below. 5 | 6 | [target.x86_64-unknown-linux-gnu] 7 | linker = "clang" 8 | rustflags = [ 9 | "-Clink-arg=-fuse-ld=lld", # Use LLD Linker 10 | "-Zshare-generics=y", # (Nightly) Make the current crate share its generic instantiations 11 | ] 12 | 13 | # NOTE: you must install [Mach-O LLD Port](https://lld.llvm.org/MachO/index.html) on mac. you can easily do this by installing llvm which includes lld with the "brew" package manager: 14 | # `brew install llvm` 15 | [target.x86_64-apple-darwin] 16 | rustflags = [ 17 | "-Clink-arg=-fuse-ld=/usr/local/opt/llvm/bin/ld64.lld", # Use LLD Linker 18 | "-Zshare-generics=y", # (Nightly) Make the current crate share its generic instantiations 19 | ] 20 | 21 | [target.aarch64-apple-darwin] 22 | rustflags = [ 23 | "-Clink-arg=-fuse-ld=/opt/homebrew/opt/llvm/bin/ld64.lld", # Use LLD Linker 24 | "-Zshare-generics=y", # (Nightly) Make the current crate share its generic instantiations 25 | ] 26 | 27 | [target.x86_64-pc-windows-msvc] 28 | linker = "rust-lld.exe" # Use LLD Linker 29 | rustflags = ["-Zshare-generics=n"] 30 | 31 | # Optional: Uncommenting the following improves compile times, but reduces the amount of debug info to 'line number tables only' 32 | # In most cases the gains are negligible, but if you are on macos and have slow compile times you should see significant gains. 33 | #[profile.dev] 34 | #debug = 1 -------------------------------------------------------------------------------- /.envrc: -------------------------------------------------------------------------------- 1 | use flake 2 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | /target 2 | result 3 | .direnv/ 4 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Changelog 2 | 3 | All notable changes to this project will be documented in this file. 4 | 5 | ## [0.8.0] - 2024-01-28 6 | 7 | [52c12d7](52c12d7c4d0a0c75db5791ae681d63968516376d)...[2cdf926](2cdf926b0dd8da86d36b9b1644d04a2408c54c17) 8 | 9 | ### Bug Fixes 10 | 11 | - Allow for parsing failures ([2cdf926](2cdf926b0dd8da86d36b9b1644d04a2408c54c17)) 12 | 13 | ### Features 14 | 15 | - Lecoqjacob -> nix-community ([b13acc6](b13acc6f2ef0d0eddb043907e3ff8bf7c488940e)) 16 | 17 | ### Miscellaneous Tasks 18 | 19 | - Update deps, run clippy + formatter + add fast compiles on nightly ([7d1da5f](7d1da5f1d243f205db3331e3ae3ca6ed813ff2fe)) 20 | 21 | ## [0.6.0] - 2020-09-13 22 | 23 | ### README.md 24 | 25 | - Fix flake url ([ad87276](ad87276be16ff219d5108765b37ca80de4032a90)) 26 | 27 | 28 | -------------------------------------------------------------------------------- /Cargo.lock: -------------------------------------------------------------------------------- 1 | # This file is automatically @generated by Cargo. 2 | # It is not intended for manual editing. 3 | version = 3 4 | 5 | [[package]] 6 | name = "anstream" 7 | version = "0.6.11" 8 | source = "registry+https://github.com/rust-lang/crates.io-index" 9 | checksum = "6e2e1ebcb11de5c03c67de28a7df593d32191b44939c482e97702baaaa6ab6a5" 10 | dependencies = [ 11 | "anstyle", 12 | "anstyle-parse", 13 | "anstyle-query", 14 | "anstyle-wincon", 15 | "colorchoice", 16 | "utf8parse", 17 | ] 18 | 19 | [[package]] 20 | name = "anstyle" 21 | version = "1.0.4" 22 | source = "registry+https://github.com/rust-lang/crates.io-index" 23 | checksum = "7079075b41f533b8c61d2a4d073c4676e1f8b249ff94a393b0595db304e0dd87" 24 | 25 | [[package]] 26 | name = "anstyle-parse" 27 | version = "0.2.3" 28 | source = "registry+https://github.com/rust-lang/crates.io-index" 29 | checksum = "c75ac65da39e5fe5ab759307499ddad880d724eed2f6ce5b5e8a26f4f387928c" 30 | dependencies = [ 31 | "utf8parse", 32 | ] 33 | 34 | [[package]] 35 | name = "anstyle-query" 36 | version = "1.0.2" 37 | source = "registry+https://github.com/rust-lang/crates.io-index" 38 | checksum = "e28923312444cdd728e4738b3f9c9cac739500909bb3d3c94b43551b16517648" 39 | dependencies = [ 40 | "windows-sys 0.52.0", 41 | ] 42 | 43 | [[package]] 44 | name = "anstyle-wincon" 45 | version = "3.0.2" 46 | source = "registry+https://github.com/rust-lang/crates.io-index" 47 | checksum = "1cd54b81ec8d6180e24654d0b371ad22fc3dd083b6ff8ba325b72e00c87660a7" 48 | dependencies = [ 49 | "anstyle", 50 | "windows-sys 0.52.0", 51 | ] 52 | 53 | [[package]] 54 | name = "anyhow" 55 | version = "1.0.79" 56 | source = "registry+https://github.com/rust-lang/crates.io-index" 57 | checksum = "080e9890a082662b09c1ad45f567faeeb47f22b5fb23895fbe1e651e718e25ca" 58 | 59 | [[package]] 60 | name = "autocfg" 61 | version = "1.1.0" 62 | source = "registry+https://github.com/rust-lang/crates.io-index" 63 | checksum = "d468802bab17cbc0cc575e9b053f41e72aa36bfa6b7f55e3529ffa43161b97fa" 64 | 65 | [[package]] 66 | name = "bincode" 67 | version = "1.3.3" 68 | source = "registry+https://github.com/rust-lang/crates.io-index" 69 | checksum = "b1f45e9417d87227c7a56d22e471c6206462cba514c7590c09aff4cf6d1ddcad" 70 | dependencies = [ 71 | "serde", 72 | ] 73 | 74 | [[package]] 75 | name = "cfg-if" 76 | version = "1.0.0" 77 | source = "registry+https://github.com/rust-lang/crates.io-index" 78 | checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd" 79 | 80 | [[package]] 81 | name = "clap" 82 | version = "4.4.18" 83 | source = "registry+https://github.com/rust-lang/crates.io-index" 84 | checksum = "1e578d6ec4194633722ccf9544794b71b1385c3c027efe0c55db226fc880865c" 85 | dependencies = [ 86 | "clap_builder", 87 | "clap_derive", 88 | ] 89 | 90 | [[package]] 91 | name = "clap_builder" 92 | version = "4.4.18" 93 | source = "registry+https://github.com/rust-lang/crates.io-index" 94 | checksum = "4df4df40ec50c46000231c914968278b1eb05098cf8f1b3a518a95030e71d1c7" 95 | dependencies = [ 96 | "anstream", 97 | "anstyle", 98 | "clap_lex", 99 | "strsim", 100 | ] 101 | 102 | [[package]] 103 | name = "clap_complete" 104 | version = "4.4.9" 105 | source = "registry+https://github.com/rust-lang/crates.io-index" 106 | checksum = "df631ae429f6613fcd3a7c1adbdb65f637271e561b03680adaa6573015dfb106" 107 | dependencies = [ 108 | "clap", 109 | ] 110 | 111 | [[package]] 112 | name = "clap_complete_nushell" 113 | version = "4.4.2" 114 | source = "registry+https://github.com/rust-lang/crates.io-index" 115 | checksum = "948bf70d7e1f179635d3ef819ce8baa2d3074d0d57816ac37387cd6f9eed0c31" 116 | dependencies = [ 117 | "clap", 118 | "clap_complete", 119 | ] 120 | 121 | [[package]] 122 | name = "clap_derive" 123 | version = "4.4.7" 124 | source = "registry+https://github.com/rust-lang/crates.io-index" 125 | checksum = "cf9804afaaf59a91e75b022a30fb7229a7901f60c755489cc61c9b423b836442" 126 | dependencies = [ 127 | "heck", 128 | "proc-macro2", 129 | "quote", 130 | "syn", 131 | ] 132 | 133 | [[package]] 134 | name = "clap_lex" 135 | version = "0.6.0" 136 | source = "registry+https://github.com/rust-lang/crates.io-index" 137 | checksum = "702fc72eb24e5a1e48ce58027a675bc24edd52096d5397d4aea7c6dd9eca0bd1" 138 | 139 | [[package]] 140 | name = "clap_mangen" 141 | version = "0.2.18" 142 | source = "registry+https://github.com/rust-lang/crates.io-index" 143 | checksum = "43144ab702c764b0a3ecda5ecd2aba2e6874d8de4b9f56930bbb1e88fcecd84a" 144 | dependencies = [ 145 | "clap", 146 | "roff", 147 | ] 148 | 149 | [[package]] 150 | name = "colorchoice" 151 | version = "1.0.0" 152 | source = "registry+https://github.com/rust-lang/crates.io-index" 153 | checksum = "acbf1af155f9b9ef647e42cdc158db4b64a1b61f743629225fde6f3e0be2a7c7" 154 | 155 | [[package]] 156 | name = "colored" 157 | version = "2.1.0" 158 | source = "registry+https://github.com/rust-lang/crates.io-index" 159 | checksum = "cbf2150cce219b664a8a70df7a1f933836724b503f8a413af9365b4dcc4d90b8" 160 | dependencies = [ 161 | "lazy_static", 162 | "windows-sys 0.48.0", 163 | ] 164 | 165 | [[package]] 166 | name = "countme" 167 | version = "3.0.1" 168 | source = "registry+https://github.com/rust-lang/crates.io-index" 169 | checksum = "7704b5fdd17b18ae31c4c1da5a2e0305a2bf17b5249300a9ee9ed7b72114c636" 170 | 171 | [[package]] 172 | name = "crc32fast" 173 | version = "1.3.2" 174 | source = "registry+https://github.com/rust-lang/crates.io-index" 175 | checksum = "b540bd8bc810d3885c6ea91e2018302f68baba2129ab3e88f32389ee9370880d" 176 | dependencies = [ 177 | "cfg-if", 178 | ] 179 | 180 | [[package]] 181 | name = "crossbeam-deque" 182 | version = "0.8.5" 183 | source = "registry+https://github.com/rust-lang/crates.io-index" 184 | checksum = "613f8cc01fe9cf1a3eb3d7f488fd2fa8388403e97039e2f73692932e291a770d" 185 | dependencies = [ 186 | "crossbeam-epoch", 187 | "crossbeam-utils", 188 | ] 189 | 190 | [[package]] 191 | name = "crossbeam-epoch" 192 | version = "0.9.18" 193 | source = "registry+https://github.com/rust-lang/crates.io-index" 194 | checksum = "5b82ac4a3c2ca9c3460964f020e1402edd5753411d7737aa39c3714ad1b5420e" 195 | dependencies = [ 196 | "crossbeam-utils", 197 | ] 198 | 199 | [[package]] 200 | name = "crossbeam-utils" 201 | version = "0.8.19" 202 | source = "registry+https://github.com/rust-lang/crates.io-index" 203 | checksum = "248e3bacc7dc6baa3b21e405ee045c3047101a49145e7e9eca583ab4c2ca5345" 204 | 205 | [[package]] 206 | name = "either" 207 | version = "1.9.0" 208 | source = "registry+https://github.com/rust-lang/crates.io-index" 209 | checksum = "a26ae43d7bcc3b814de94796a5e736d4029efb0ee900c12e2d54c993ad1a1e07" 210 | 211 | [[package]] 212 | name = "hashbrown" 213 | version = "0.14.3" 214 | source = "registry+https://github.com/rust-lang/crates.io-index" 215 | checksum = "290f1a1d9242c78d09ce40a5e87e7554ee637af1351968159f4952f028f75604" 216 | 217 | [[package]] 218 | name = "heck" 219 | version = "0.4.1" 220 | source = "registry+https://github.com/rust-lang/crates.io-index" 221 | checksum = "95505c38b4572b2d910cecb0281560f54b440a19336cbbcb27bf6ce6adc6f5a8" 222 | 223 | [[package]] 224 | name = "itoa" 225 | version = "1.0.10" 226 | source = "registry+https://github.com/rust-lang/crates.io-index" 227 | checksum = "b1a46d1a171d865aa5f83f92695765caa047a9b4cbae2cbf37dbd613a793fd4c" 228 | 229 | [[package]] 230 | name = "lazy_static" 231 | version = "1.4.0" 232 | source = "registry+https://github.com/rust-lang/crates.io-index" 233 | checksum = "e2abad23fbc42b3700f2f279844dc832adb2b2eb069b2df918f455c4e18cc646" 234 | 235 | [[package]] 236 | name = "manix" 237 | version = "0.8.0-dev" 238 | dependencies = [ 239 | "anyhow", 240 | "bincode", 241 | "clap", 242 | "clap_complete", 243 | "clap_complete_nushell", 244 | "clap_mangen", 245 | "colored", 246 | "crc32fast", 247 | "lazy_static", 248 | "rayon", 249 | "rnix", 250 | "rowan", 251 | "roxmltree", 252 | "serde", 253 | "serde_json", 254 | "strum", 255 | "thiserror", 256 | "walkdir", 257 | "xdg", 258 | ] 259 | 260 | [[package]] 261 | name = "memoffset" 262 | version = "0.9.0" 263 | source = "registry+https://github.com/rust-lang/crates.io-index" 264 | checksum = "5a634b1c61a95585bd15607c6ab0c4e5b226e695ff2800ba0cdccddf208c406c" 265 | dependencies = [ 266 | "autocfg", 267 | ] 268 | 269 | [[package]] 270 | name = "proc-macro2" 271 | version = "1.0.78" 272 | source = "registry+https://github.com/rust-lang/crates.io-index" 273 | checksum = "e2422ad645d89c99f8f3e6b88a9fdeca7fabeac836b1002371c4367c8f984aae" 274 | dependencies = [ 275 | "unicode-ident", 276 | ] 277 | 278 | [[package]] 279 | name = "quote" 280 | version = "1.0.35" 281 | source = "registry+https://github.com/rust-lang/crates.io-index" 282 | checksum = "291ec9ab5efd934aaf503a6466c5d5251535d108ee747472c3977cc5acc868ef" 283 | dependencies = [ 284 | "proc-macro2", 285 | ] 286 | 287 | [[package]] 288 | name = "rayon" 289 | version = "1.8.1" 290 | source = "registry+https://github.com/rust-lang/crates.io-index" 291 | checksum = "fa7237101a77a10773db45d62004a272517633fbcc3df19d96455ede1122e051" 292 | dependencies = [ 293 | "either", 294 | "rayon-core", 295 | ] 296 | 297 | [[package]] 298 | name = "rayon-core" 299 | version = "1.12.1" 300 | source = "registry+https://github.com/rust-lang/crates.io-index" 301 | checksum = "1465873a3dfdaa8ae7cb14b4383657caab0b3e8a0aa9ae8e04b044854c8dfce2" 302 | dependencies = [ 303 | "crossbeam-deque", 304 | "crossbeam-utils", 305 | ] 306 | 307 | [[package]] 308 | name = "rnix" 309 | version = "0.11.0" 310 | source = "registry+https://github.com/rust-lang/crates.io-index" 311 | checksum = "bb35cedbeb70e0ccabef2a31bcff0aebd114f19566086300b8f42c725fc2cb5f" 312 | dependencies = [ 313 | "rowan", 314 | ] 315 | 316 | [[package]] 317 | name = "roff" 318 | version = "0.2.1" 319 | source = "registry+https://github.com/rust-lang/crates.io-index" 320 | checksum = "b833d8d034ea094b1ea68aa6d5c740e0d04bad9d16568d08ba6f76823a114316" 321 | 322 | [[package]] 323 | name = "rowan" 324 | version = "0.15.15" 325 | source = "registry+https://github.com/rust-lang/crates.io-index" 326 | checksum = "32a58fa8a7ccff2aec4f39cc45bf5f985cec7125ab271cf681c279fd00192b49" 327 | dependencies = [ 328 | "countme", 329 | "hashbrown", 330 | "memoffset", 331 | "rustc-hash", 332 | "text-size", 333 | ] 334 | 335 | [[package]] 336 | name = "roxmltree" 337 | version = "0.19.0" 338 | source = "registry+https://github.com/rust-lang/crates.io-index" 339 | checksum = "3cd14fd5e3b777a7422cca79358c57a8f6e3a703d9ac187448d0daf220c2407f" 340 | 341 | [[package]] 342 | name = "rustc-hash" 343 | version = "1.1.0" 344 | source = "registry+https://github.com/rust-lang/crates.io-index" 345 | checksum = "08d43f7aa6b08d49f382cde6a7982047c3426db949b1424bc4b7ec9ae12c6ce2" 346 | 347 | [[package]] 348 | name = "rustversion" 349 | version = "1.0.14" 350 | source = "registry+https://github.com/rust-lang/crates.io-index" 351 | checksum = "7ffc183a10b4478d04cbbbfc96d0873219d962dd5accaff2ffbd4ceb7df837f4" 352 | 353 | [[package]] 354 | name = "ryu" 355 | version = "1.0.16" 356 | source = "registry+https://github.com/rust-lang/crates.io-index" 357 | checksum = "f98d2aa92eebf49b69786be48e4477826b256916e84a57ff2a4f21923b48eb4c" 358 | 359 | [[package]] 360 | name = "same-file" 361 | version = "1.0.6" 362 | source = "registry+https://github.com/rust-lang/crates.io-index" 363 | checksum = "93fc1dc3aaa9bfed95e02e6eadabb4baf7e3078b0bd1b4d7b6b0b68378900502" 364 | dependencies = [ 365 | "winapi-util", 366 | ] 367 | 368 | [[package]] 369 | name = "serde" 370 | version = "1.0.196" 371 | source = "registry+https://github.com/rust-lang/crates.io-index" 372 | checksum = "870026e60fa08c69f064aa766c10f10b1d62db9ccd4d0abb206472bee0ce3b32" 373 | dependencies = [ 374 | "serde_derive", 375 | ] 376 | 377 | [[package]] 378 | name = "serde_derive" 379 | version = "1.0.196" 380 | source = "registry+https://github.com/rust-lang/crates.io-index" 381 | checksum = "33c85360c95e7d137454dc81d9a4ed2b8efd8fbe19cee57357b32b9771fccb67" 382 | dependencies = [ 383 | "proc-macro2", 384 | "quote", 385 | "syn", 386 | ] 387 | 388 | [[package]] 389 | name = "serde_json" 390 | version = "1.0.112" 391 | source = "registry+https://github.com/rust-lang/crates.io-index" 392 | checksum = "4d1bd37ce2324cf3bf85e5a25f96eb4baf0d5aa6eba43e7ae8958870c4ec48ed" 393 | dependencies = [ 394 | "itoa", 395 | "ryu", 396 | "serde", 397 | ] 398 | 399 | [[package]] 400 | name = "strsim" 401 | version = "0.10.0" 402 | source = "registry+https://github.com/rust-lang/crates.io-index" 403 | checksum = "73473c0e59e6d5812c5dfe2a064a6444949f089e20eec9a2e5506596494e4623" 404 | 405 | [[package]] 406 | name = "strum" 407 | version = "0.26.1" 408 | source = "registry+https://github.com/rust-lang/crates.io-index" 409 | checksum = "723b93e8addf9aa965ebe2d11da6d7540fa2283fcea14b3371ff055f7ba13f5f" 410 | dependencies = [ 411 | "strum_macros", 412 | ] 413 | 414 | [[package]] 415 | name = "strum_macros" 416 | version = "0.26.1" 417 | source = "registry+https://github.com/rust-lang/crates.io-index" 418 | checksum = "7a3417fc93d76740d974a01654a09777cb500428cc874ca9f45edfe0c4d4cd18" 419 | dependencies = [ 420 | "heck", 421 | "proc-macro2", 422 | "quote", 423 | "rustversion", 424 | "syn", 425 | ] 426 | 427 | [[package]] 428 | name = "syn" 429 | version = "2.0.48" 430 | source = "registry+https://github.com/rust-lang/crates.io-index" 431 | checksum = "0f3531638e407dfc0814761abb7c00a5b54992b849452a0646b7f65c9f770f3f" 432 | dependencies = [ 433 | "proc-macro2", 434 | "quote", 435 | "unicode-ident", 436 | ] 437 | 438 | [[package]] 439 | name = "text-size" 440 | version = "1.1.1" 441 | source = "registry+https://github.com/rust-lang/crates.io-index" 442 | checksum = "f18aa187839b2bdb1ad2fa35ead8c4c2976b64e4363c386d45ac0f7ee85c9233" 443 | 444 | [[package]] 445 | name = "thiserror" 446 | version = "1.0.56" 447 | source = "registry+https://github.com/rust-lang/crates.io-index" 448 | checksum = "d54378c645627613241d077a3a79db965db602882668f9136ac42af9ecb730ad" 449 | dependencies = [ 450 | "thiserror-impl", 451 | ] 452 | 453 | [[package]] 454 | name = "thiserror-impl" 455 | version = "1.0.56" 456 | source = "registry+https://github.com/rust-lang/crates.io-index" 457 | checksum = "fa0faa943b50f3db30a20aa7e265dbc66076993efed8463e8de414e5d06d3471" 458 | dependencies = [ 459 | "proc-macro2", 460 | "quote", 461 | "syn", 462 | ] 463 | 464 | [[package]] 465 | name = "unicode-ident" 466 | version = "1.0.12" 467 | source = "registry+https://github.com/rust-lang/crates.io-index" 468 | checksum = "3354b9ac3fae1ff6755cb6db53683adb661634f67557942dea4facebec0fee4b" 469 | 470 | [[package]] 471 | name = "utf8parse" 472 | version = "0.2.1" 473 | source = "registry+https://github.com/rust-lang/crates.io-index" 474 | checksum = "711b9620af191e0cdc7468a8d14e709c3dcdb115b36f838e601583af800a370a" 475 | 476 | [[package]] 477 | name = "walkdir" 478 | version = "2.4.0" 479 | source = "registry+https://github.com/rust-lang/crates.io-index" 480 | checksum = "d71d857dc86794ca4c280d616f7da00d2dbfd8cd788846559a6813e6aa4b54ee" 481 | dependencies = [ 482 | "same-file", 483 | "winapi-util", 484 | ] 485 | 486 | [[package]] 487 | name = "winapi" 488 | version = "0.3.9" 489 | source = "registry+https://github.com/rust-lang/crates.io-index" 490 | checksum = "5c839a674fcd7a98952e593242ea400abe93992746761e38641405d28b00f419" 491 | dependencies = [ 492 | "winapi-i686-pc-windows-gnu", 493 | "winapi-x86_64-pc-windows-gnu", 494 | ] 495 | 496 | [[package]] 497 | name = "winapi-i686-pc-windows-gnu" 498 | version = "0.4.0" 499 | source = "registry+https://github.com/rust-lang/crates.io-index" 500 | checksum = "ac3b87c63620426dd9b991e5ce0329eff545bccbbb34f3be09ff6fb6ab51b7b6" 501 | 502 | [[package]] 503 | name = "winapi-util" 504 | version = "0.1.6" 505 | source = "registry+https://github.com/rust-lang/crates.io-index" 506 | checksum = "f29e6f9198ba0d26b4c9f07dbe6f9ed633e1f3d5b8b414090084349e46a52596" 507 | dependencies = [ 508 | "winapi", 509 | ] 510 | 511 | [[package]] 512 | name = "winapi-x86_64-pc-windows-gnu" 513 | version = "0.4.0" 514 | source = "registry+https://github.com/rust-lang/crates.io-index" 515 | checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f" 516 | 517 | [[package]] 518 | name = "windows-sys" 519 | version = "0.48.0" 520 | source = "registry+https://github.com/rust-lang/crates.io-index" 521 | checksum = "677d2418bec65e3338edb076e806bc1ec15693c5d0104683f2efe857f61056a9" 522 | dependencies = [ 523 | "windows-targets 0.48.5", 524 | ] 525 | 526 | [[package]] 527 | name = "windows-sys" 528 | version = "0.52.0" 529 | source = "registry+https://github.com/rust-lang/crates.io-index" 530 | checksum = "282be5f36a8ce781fad8c8ae18fa3f9beff57ec1b52cb3de0789201425d9a33d" 531 | dependencies = [ 532 | "windows-targets 0.52.0", 533 | ] 534 | 535 | [[package]] 536 | name = "windows-targets" 537 | version = "0.48.5" 538 | source = "registry+https://github.com/rust-lang/crates.io-index" 539 | checksum = "9a2fa6e2155d7247be68c096456083145c183cbbbc2764150dda45a87197940c" 540 | dependencies = [ 541 | "windows_aarch64_gnullvm 0.48.5", 542 | "windows_aarch64_msvc 0.48.5", 543 | "windows_i686_gnu 0.48.5", 544 | "windows_i686_msvc 0.48.5", 545 | "windows_x86_64_gnu 0.48.5", 546 | "windows_x86_64_gnullvm 0.48.5", 547 | "windows_x86_64_msvc 0.48.5", 548 | ] 549 | 550 | [[package]] 551 | name = "windows-targets" 552 | version = "0.52.0" 553 | source = "registry+https://github.com/rust-lang/crates.io-index" 554 | checksum = "8a18201040b24831fbb9e4eb208f8892e1f50a37feb53cc7ff887feb8f50e7cd" 555 | dependencies = [ 556 | "windows_aarch64_gnullvm 0.52.0", 557 | "windows_aarch64_msvc 0.52.0", 558 | "windows_i686_gnu 0.52.0", 559 | "windows_i686_msvc 0.52.0", 560 | "windows_x86_64_gnu 0.52.0", 561 | "windows_x86_64_gnullvm 0.52.0", 562 | "windows_x86_64_msvc 0.52.0", 563 | ] 564 | 565 | [[package]] 566 | name = "windows_aarch64_gnullvm" 567 | version = "0.48.5" 568 | source = "registry+https://github.com/rust-lang/crates.io-index" 569 | checksum = "2b38e32f0abccf9987a4e3079dfb67dcd799fb61361e53e2882c3cbaf0d905d8" 570 | 571 | [[package]] 572 | name = "windows_aarch64_gnullvm" 573 | version = "0.52.0" 574 | source = "registry+https://github.com/rust-lang/crates.io-index" 575 | checksum = "cb7764e35d4db8a7921e09562a0304bf2f93e0a51bfccee0bd0bb0b666b015ea" 576 | 577 | [[package]] 578 | name = "windows_aarch64_msvc" 579 | version = "0.48.5" 580 | source = "registry+https://github.com/rust-lang/crates.io-index" 581 | checksum = "dc35310971f3b2dbbf3f0690a219f40e2d9afcf64f9ab7cc1be722937c26b4bc" 582 | 583 | [[package]] 584 | name = "windows_aarch64_msvc" 585 | version = "0.52.0" 586 | source = "registry+https://github.com/rust-lang/crates.io-index" 587 | checksum = "bbaa0368d4f1d2aaefc55b6fcfee13f41544ddf36801e793edbbfd7d7df075ef" 588 | 589 | [[package]] 590 | name = "windows_i686_gnu" 591 | version = "0.48.5" 592 | source = "registry+https://github.com/rust-lang/crates.io-index" 593 | checksum = "a75915e7def60c94dcef72200b9a8e58e5091744960da64ec734a6c6e9b3743e" 594 | 595 | [[package]] 596 | name = "windows_i686_gnu" 597 | version = "0.52.0" 598 | source = "registry+https://github.com/rust-lang/crates.io-index" 599 | checksum = "a28637cb1fa3560a16915793afb20081aba2c92ee8af57b4d5f28e4b3e7df313" 600 | 601 | [[package]] 602 | name = "windows_i686_msvc" 603 | version = "0.48.5" 604 | source = "registry+https://github.com/rust-lang/crates.io-index" 605 | checksum = "8f55c233f70c4b27f66c523580f78f1004e8b5a8b659e05a4eb49d4166cca406" 606 | 607 | [[package]] 608 | name = "windows_i686_msvc" 609 | version = "0.52.0" 610 | source = "registry+https://github.com/rust-lang/crates.io-index" 611 | checksum = "ffe5e8e31046ce6230cc7215707b816e339ff4d4d67c65dffa206fd0f7aa7b9a" 612 | 613 | [[package]] 614 | name = "windows_x86_64_gnu" 615 | version = "0.48.5" 616 | source = "registry+https://github.com/rust-lang/crates.io-index" 617 | checksum = "53d40abd2583d23e4718fddf1ebec84dbff8381c07cae67ff7768bbf19c6718e" 618 | 619 | [[package]] 620 | name = "windows_x86_64_gnu" 621 | version = "0.52.0" 622 | source = "registry+https://github.com/rust-lang/crates.io-index" 623 | checksum = "3d6fa32db2bc4a2f5abeacf2b69f7992cd09dca97498da74a151a3132c26befd" 624 | 625 | [[package]] 626 | name = "windows_x86_64_gnullvm" 627 | version = "0.48.5" 628 | source = "registry+https://github.com/rust-lang/crates.io-index" 629 | checksum = "0b7b52767868a23d5bab768e390dc5f5c55825b6d30b86c844ff2dc7414044cc" 630 | 631 | [[package]] 632 | name = "windows_x86_64_gnullvm" 633 | version = "0.52.0" 634 | source = "registry+https://github.com/rust-lang/crates.io-index" 635 | checksum = "1a657e1e9d3f514745a572a6846d3c7aa7dbe1658c056ed9c3344c4109a6949e" 636 | 637 | [[package]] 638 | name = "windows_x86_64_msvc" 639 | version = "0.48.5" 640 | source = "registry+https://github.com/rust-lang/crates.io-index" 641 | checksum = "ed94fce61571a4006852b7389a063ab983c02eb1bb37b47f8272ce92d06d9538" 642 | 643 | [[package]] 644 | name = "windows_x86_64_msvc" 645 | version = "0.52.0" 646 | source = "registry+https://github.com/rust-lang/crates.io-index" 647 | checksum = "dff9641d1cd4be8d1a070daf9e3773c5f67e78b4d9d42263020c057706765c04" 648 | 649 | [[package]] 650 | name = "xdg" 651 | version = "2.5.2" 652 | source = "registry+https://github.com/rust-lang/crates.io-index" 653 | checksum = "213b7324336b53d2414b2db8537e56544d981803139155afa84f76eeebb7a546" 654 | -------------------------------------------------------------------------------- /Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "manix" 3 | version = "0.8.0-dev" 4 | edition = "2021" 5 | license = "Apache-2.0" 6 | description = "Nix documentation searcher" 7 | authors = ["mlvzk ", "n16hth4wk "] 8 | 9 | [dependencies] 10 | anyhow = "1" 11 | bincode = "1" 12 | clap_complete = "4.4.9" 13 | clap_complete_nushell = "4.4.2" 14 | clap_mangen = "0.2.18" 15 | colored = "2" 16 | crc32fast = "1" 17 | lazy_static = "1" 18 | rayon = "1" 19 | rnix = "0.11" 20 | rowan = "0.15" 21 | roxmltree = "0.19" 22 | serde = { version = "1", features = ["derive"] } 23 | serde_json = "1" 24 | strum = { version = "0.26.1", features = ["derive"] } 25 | thiserror = "1" 26 | walkdir = "2" 27 | xdg = "2.5" 28 | 29 | [dependencies.clap] 30 | version = "4.4.18" 31 | features = [ "derive" ] 32 | 33 | [profile.release] 34 | lto = true 35 | codegen-units = 1 36 | strip = true 37 | debug = false 38 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Apache License 2 | Version 2.0, January 2004 3 | http://www.apache.org/licenses/ 4 | 5 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 6 | 7 | 1. Definitions. 8 | 9 | "License" shall mean the terms and conditions for use, reproduction, 10 | and distribution as defined by Sections 1 through 9 of this document. 11 | 12 | "Licensor" shall mean the copyright owner or entity authorized by 13 | the copyright owner that is granting the License. 14 | 15 | "Legal Entity" shall mean the union of the acting entity and all 16 | other entities that control, are controlled by, or are under common 17 | control with that entity. For the purposes of this definition, 18 | "control" means (i) the power, direct or indirect, to cause the 19 | direction or management of such entity, whether by contract or 20 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 21 | outstanding shares, or (iii) beneficial ownership of such entity. 22 | 23 | "You" (or "Your") shall mean an individual or Legal Entity 24 | exercising permissions granted by this License. 25 | 26 | "Source" form shall mean the preferred form for making modifications, 27 | including but not limited to software source code, documentation 28 | source, and configuration files. 29 | 30 | "Object" form shall mean any form resulting from mechanical 31 | transformation or translation of a Source form, including but 32 | not limited to compiled object code, generated documentation, 33 | and conversions to other media types. 34 | 35 | "Work" shall mean the work of authorship, whether in Source or 36 | Object form, made available under the License, as indicated by a 37 | copyright notice that is included in or attached to the work 38 | (an example is provided in the Appendix below). 39 | 40 | "Derivative Works" shall mean any work, whether in Source or Object 41 | form, that is based on (or derived from) the Work and for which the 42 | editorial revisions, annotations, elaborations, or other modifications 43 | represent, as a whole, an original work of authorship. For the purposes 44 | of this License, Derivative Works shall not include works that remain 45 | separable from, or merely link (or bind by name) to the interfaces of, 46 | the Work and Derivative Works thereof. 47 | 48 | "Contribution" shall mean any work of authorship, including 49 | the original version of the Work and any modifications or additions 50 | to that Work or Derivative Works thereof, that is intentionally 51 | submitted to Licensor for inclusion in the Work by the copyright owner 52 | or by an individual or Legal Entity authorized to submit on behalf of 53 | the copyright owner. For the purposes of this definition, "submitted" 54 | means any form of electronic, verbal, or written communication sent 55 | to the Licensor or its representatives, including but not limited to 56 | communication on electronic mailing lists, source code control systems, 57 | and issue tracking systems that are managed by, or on behalf of, the 58 | Licensor for the purpose of discussing and improving the Work, but 59 | excluding communication that is conspicuously marked or otherwise 60 | designated in writing by the copyright owner as "Not a Contribution." 61 | 62 | "Contributor" shall mean Licensor and any individual or Legal Entity 63 | on behalf of whom a Contribution has been received by Licensor and 64 | subsequently incorporated within the Work. 65 | 66 | 2. Grant of Copyright License. Subject to the terms and conditions of 67 | this License, each Contributor hereby grants to You a perpetual, 68 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 69 | copyright license to reproduce, prepare Derivative Works of, 70 | publicly display, publicly perform, sublicense, and distribute the 71 | Work and such Derivative Works in Source or Object form. 72 | 73 | 3. Grant of Patent License. Subject to the terms and conditions of 74 | this License, each Contributor hereby grants to You a perpetual, 75 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 76 | (except as stated in this section) patent license to make, have made, 77 | use, offer to sell, sell, import, and otherwise transfer the Work, 78 | where such license applies only to those patent claims licensable 79 | by such Contributor that are necessarily infringed by their 80 | Contribution(s) alone or by combination of their Contribution(s) 81 | with the Work to which such Contribution(s) was submitted. If You 82 | institute patent litigation against any entity (including a 83 | cross-claim or counterclaim in a lawsuit) alleging that the Work 84 | or a Contribution incorporated within the Work constitutes direct 85 | or contributory patent infringement, then any patent licenses 86 | granted to You under this License for that Work shall terminate 87 | as of the date such litigation is filed. 88 | 89 | 4. Redistribution. You may reproduce and distribute copies of the 90 | Work or Derivative Works thereof in any medium, with or without 91 | modifications, and in Source or Object form, provided that You 92 | meet the following conditions: 93 | 94 | (a) You must give any other recipients of the Work or 95 | Derivative Works a copy of this License; and 96 | 97 | (b) You must cause any modified files to carry prominent notices 98 | stating that You changed the files; and 99 | 100 | (c) You must retain, in the Source form of any Derivative Works 101 | that You distribute, all copyright, patent, trademark, and 102 | attribution notices from the Source form of the Work, 103 | excluding those notices that do not pertain to any part of 104 | the Derivative Works; and 105 | 106 | (d) If the Work includes a "NOTICE" text file as part of its 107 | distribution, then any Derivative Works that You distribute must 108 | include a readable copy of the attribution notices contained 109 | within such NOTICE file, excluding those notices that do not 110 | pertain to any part of the Derivative Works, in at least one 111 | of the following places: within a NOTICE text file distributed 112 | as part of the Derivative Works; within the Source form or 113 | documentation, if provided along with the Derivative Works; or, 114 | within a display generated by the Derivative Works, if and 115 | wherever such third-party notices normally appear. The contents 116 | of the NOTICE file are for informational purposes only and 117 | do not modify the License. You may add Your own attribution 118 | notices within Derivative Works that You distribute, alongside 119 | or as an addendum to the NOTICE text from the Work, provided 120 | that such additional attribution notices cannot be construed 121 | as modifying the License. 122 | 123 | You may add Your own copyright statement to Your modifications and 124 | may provide additional or different license terms and conditions 125 | for use, reproduction, or distribution of Your modifications, or 126 | for any such Derivative Works as a whole, provided Your use, 127 | reproduction, and distribution of the Work otherwise complies with 128 | the conditions stated in this License. 129 | 130 | 5. Submission of Contributions. Unless You explicitly state otherwise, 131 | any Contribution intentionally submitted for inclusion in the Work 132 | by You to the Licensor shall be under the terms and conditions of 133 | this License, without any additional terms or conditions. 134 | Notwithstanding the above, nothing herein shall supersede or modify 135 | the terms of any separate license agreement you may have executed 136 | with Licensor regarding such Contributions. 137 | 138 | 6. Trademarks. This License does not grant permission to use the trade 139 | names, trademarks, service marks, or product names of the Licensor, 140 | except as required for reasonable and customary use in describing the 141 | origin of the Work and reproducing the content of the NOTICE file. 142 | 143 | 7. Disclaimer of Warranty. Unless required by applicable law or 144 | agreed to in writing, Licensor provides the Work (and each 145 | Contributor provides its Contributions) on an "AS IS" BASIS, 146 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 147 | implied, including, without limitation, any warranties or conditions 148 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 149 | PARTICULAR PURPOSE. You are solely responsible for determining the 150 | appropriateness of using or redistributing the Work and assume any 151 | risks associated with Your exercise of permissions under this License. 152 | 153 | 8. Limitation of Liability. In no event and under no legal theory, 154 | whether in tort (including negligence), contract, or otherwise, 155 | unless required by applicable law (such as deliberate and grossly 156 | negligent acts) or agreed to in writing, shall any Contributor be 157 | liable to You for damages, including any direct, indirect, special, 158 | incidental, or consequential damages of any character arising as a 159 | result of this License or out of the use or inability to use the 160 | Work (including but not limited to damages for loss of goodwill, 161 | work stoppage, computer failure or malfunction, or any and all 162 | other commercial damages or losses), even if such Contributor 163 | has been advised of the possibility of such damages. 164 | 165 | 9. Accepting Warranty or Additional Liability. While redistributing 166 | the Work or Derivative Works thereof, You may choose to offer, 167 | and charge a fee for, acceptance of support, warranty, indemnity, 168 | or other liability obligations and/or rights consistent with this 169 | License. However, in accepting such obligations, You may act only 170 | on Your own behalf and on Your sole responsibility, not on behalf 171 | of any other Contributor, and only if You agree to indemnify, 172 | defend, and hold each Contributor harmless for any liability 173 | incurred by, or claims asserted against, such Contributor by reason 174 | of your accepting any such warranty or additional liability. 175 | 176 | END OF TERMS AND CONDITIONS 177 | 178 | APPENDIX: How to apply the Apache License to your work. 179 | 180 | To apply the Apache License to your work, attach the following 181 | boilerplate notice, with the fields enclosed by brackets "[]" 182 | replaced with your own identifying information. (Don't include 183 | the brackets!) The text should be enclosed in the appropriate 184 | comment syntax for the file format. We also recommend that a 185 | file or class name and description of purpose be included on the 186 | same "printed page" as the copyright notice for easier 187 | identification within third-party archives. 188 | 189 | Copyright [yyyy] [name of copyright owner] 190 | 191 | Licensed under the Apache License, Version 2.0 (the "License"); 192 | you may not use this file except in compliance with the License. 193 | You may obtain a copy of the License at 194 | 195 | http://www.apache.org/licenses/LICENSE-2.0 196 | 197 | Unless required by applicable law or agreed to in writing, software 198 | distributed under the License is distributed on an "AS IS" BASIS, 199 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 200 | See the License for the specific language governing permissions and 201 | limitations under the License. -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Manix 2 | 3 | A fast CLI documentation searcher for Nix. 4 | 5 | ## Supported sources 6 | 7 | - Nixpkgs Documentation 8 | - Nixpkgs Comments 9 | - Nixpkgs Tree (pkgs, pkgs.lib) 10 | - NixOS Options 11 | - Nix-Darwin Options 12 | - Home-Manager Options 13 | 14 | ## Usage 15 | 16 | ```sh 17 | manix --help 18 | manix mergeattr 19 | manix --strict mergeattr 20 | manix --update-cache mergeattr 21 | ``` 22 | 23 | ### rnix-lsp 24 | 25 | If you want to use it in your editor, check [ElKowar's rnix-lsp fork](https://github.com/elkowar/rnix-lsp), which uses it to provide documentation on hover and autocompletion. 26 | 27 | ![manix](/manix.png) 28 | 29 | ### fzf 30 | 31 | ```sh 32 | manix "" | grep '^# ' | sed 's/^# \(.*\) (.*/\1/;s/ (.*//;s/^# //' | fzf --preview="manix '{}'" | xargs manix 33 | ``` 34 | 35 | ## Installation 36 | 37 | ### Update 38 | 39 | Manix is now available in nixpkgs. If you use the unstable channel installing is as easy as adding `manix` to your `environment.systemPackages` or `home.packages`. 40 | 41 | ### Github Releases 42 | 43 | Since it can take some time to compile Manix, you can download statically-built executables from Github Releases. 44 | 45 | ```sh 46 | wget https://github.com/nix-community/manix/releases/latest/download/manix 47 | chmod +x manix 48 | mv manix ~/bin/ # or some other location in your $PATH 49 | ``` 50 | 51 | ### nix-env 52 | 53 | ```sh 54 | # If you have the unstable channel on your system 55 | nix-env -f '' -iA manix 56 | # OR 57 | nix-env -i -f https://github.com/nix-community/manix/archive/master.tar.gz 58 | # OR 59 | nix profile install github:nix-community/manix/latest 60 | ``` 61 | 62 | ### Nix with flakes enabled 63 | 64 | ``` sh 65 | nix run 'github:nix-community/manix' mapAttrs 66 | ``` 67 | 68 | ## Kudos 69 | 70 | The original [manix](https://github.com/mlvzk/manix). mlvzk has been inactive for over a year, we thank him for his hard work. 71 | The inspiration for this project came from [nix-doc](https://github.com/lf-/nix-doc) 72 | -------------------------------------------------------------------------------- /cliff.toml: -------------------------------------------------------------------------------- 1 | # git-cliff ~ configuration file 2 | # https://git-cliff.org/docs/configuration 3 | 4 | [changelog] 5 | # changelog header 6 | header = """ 7 | # Changelog\n 8 | All notable changes to this project will be documented in this file. 9 | 10 | The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), 11 | and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).\n 12 | """ 13 | # template for the changelog body 14 | # https://keats.github.io/tera/docs/#introduction 15 | body = """ 16 | {% if version -%} 17 | ## [{{ version | trim_start_matches(pat="v") }}] - {{ timestamp | date(format="%Y-%m-%d") }} 18 | {% else -%} 19 | ## [Unreleased] 20 | {% endif -%} 21 | {% for group, commits in commits | group_by(attribute="group") %} 22 | ### {{ group | upper_first }} 23 | {% for commit in commits %} 24 | - {{ commit.message | upper_first }}\ 25 | {% endfor %} 26 | {% endfor %}\n 27 | """ 28 | # template for the changelog footer 29 | footer = """ 30 | {% for release in releases -%} 31 | {% if release.version -%} 32 | {% if release.previous.version -%} 33 | [{{ release.version | trim_start_matches(pat="v") }}]: \ 34 | https://github.com/{{ remote.github.owner }}/{{ remote.github.repo }}\ 35 | /compare/{{ release.previous.version }}..{{ release.version }} 36 | {% endif -%} 37 | {% else -%} 38 | [unreleased]: https://github.com/{{ remote.github.owner }}/{{ remote.github.repo }}\ 39 | /compare/{{ release.previous.version }}..HEAD 40 | {% endif -%} 41 | {% endfor %} 42 | 43 | """ 44 | # remove the leading and trailing whitespace from the templates 45 | trim = true 46 | 47 | [git] 48 | # parse the commits based on https://www.conventionalcommits.org 49 | conventional_commits = true 50 | # filter out the commits that are not conventional 51 | filter_unconventional = true 52 | # process each line of a commit as an individual commit 53 | split_commits = false 54 | # regex for parsing and grouping commits 55 | commit_parsers = [ 56 | { message = "^.*: add", group = "Added" }, 57 | { message = "^.*: support", group = "Added" }, 58 | { message = "^.*: remove", group = "Removed" }, 59 | { message = "^.*: delete", group = "Removed" }, 60 | { message = "^test", group = "Fixed" }, 61 | { message = "^fix", group = "Fixed" }, 62 | { message = "^.*: fix", group = "Fixed" }, 63 | { message = "^.*", group = "Changed" }, 64 | ] 65 | # protect breaking changes from being skipped due to matching a skipping commit_parser 66 | protect_breaking_commits = false 67 | # filter out the commits that are not matched by commit parsers 68 | filter_commits = true 69 | # regex for matching git tags 70 | tag_pattern = "v[0-9].*" 71 | # regex for skipping tags 72 | skip_tags = "v0.1.0-beta.1" 73 | # regex for ignoring tags 74 | ignore_tags = "" 75 | # sort the tags topologically 76 | topo_order = false 77 | # sort the commits inside sections by oldest/newest order 78 | sort_commits = "oldest" 79 | -------------------------------------------------------------------------------- /flake.lock: -------------------------------------------------------------------------------- 1 | { 2 | "nodes": { 3 | "flake-utils": { 4 | "inputs": { 5 | "systems": "systems" 6 | }, 7 | "locked": { 8 | "lastModified": 1705309234, 9 | "narHash": "sha256-uNRRNRKmJyCRC/8y1RqBkqWBLM034y4qN7EprSdmgyA=", 10 | "owner": "numtide", 11 | "repo": "flake-utils", 12 | "rev": "1ef2e671c3b0c19053962c07dbda38332dcebf26", 13 | "type": "github" 14 | }, 15 | "original": { 16 | "owner": "numtide", 17 | "repo": "flake-utils", 18 | "type": "github" 19 | } 20 | }, 21 | "naersk": { 22 | "inputs": { 23 | "nixpkgs": "nixpkgs" 24 | }, 25 | "locked": { 26 | "lastModified": 1698420672, 27 | "narHash": "sha256-/TdeHMPRjjdJub7p7+w55vyABrsJlt5QkznPYy55vKA=", 28 | "owner": "nix-community", 29 | "repo": "naersk", 30 | "rev": "aeb58d5e8faead8980a807c840232697982d47b9", 31 | "type": "github" 32 | }, 33 | "original": { 34 | "owner": "nix-community", 35 | "repo": "naersk", 36 | "type": "github" 37 | } 38 | }, 39 | "nixpkgs": { 40 | "locked": { 41 | "lastModified": 1706173671, 42 | "narHash": "sha256-lciR7kQUK2FCAYuszyd7zyRRmTaXVeoZsCyK6QFpGdk=", 43 | "owner": "NixOS", 44 | "repo": "nixpkgs", 45 | "rev": "4fddc9be4eaf195d631333908f2a454b03628ee5", 46 | "type": "github" 47 | }, 48 | "original": { 49 | "id": "nixpkgs", 50 | "type": "indirect" 51 | } 52 | }, 53 | "nixpkgs_2": { 54 | "locked": { 55 | "lastModified": 1706173671, 56 | "narHash": "sha256-lciR7kQUK2FCAYuszyd7zyRRmTaXVeoZsCyK6QFpGdk=", 57 | "owner": "NixOS", 58 | "repo": "nixpkgs", 59 | "rev": "4fddc9be4eaf195d631333908f2a454b03628ee5", 60 | "type": "github" 61 | }, 62 | "original": { 63 | "owner": "NixOS", 64 | "ref": "nixpkgs-unstable", 65 | "repo": "nixpkgs", 66 | "type": "github" 67 | } 68 | }, 69 | "root": { 70 | "inputs": { 71 | "flake-utils": "flake-utils", 72 | "naersk": "naersk", 73 | "nixpkgs": "nixpkgs_2" 74 | } 75 | }, 76 | "systems": { 77 | "locked": { 78 | "lastModified": 1681028828, 79 | "narHash": "sha256-Vy1rq5AaRuLzOxct8nz4T6wlgyUR7zLU309k9mBC768=", 80 | "owner": "nix-systems", 81 | "repo": "default", 82 | "rev": "da67096a3b9bf56a91d16901293e51ba5b49a27e", 83 | "type": "github" 84 | }, 85 | "original": { 86 | "owner": "nix-systems", 87 | "repo": "default", 88 | "type": "github" 89 | } 90 | } 91 | }, 92 | "root": "root", 93 | "version": 7 94 | } 95 | -------------------------------------------------------------------------------- /flake.nix: -------------------------------------------------------------------------------- 1 | { 2 | description = "A fast CLI documentation searcher for Nix."; 3 | 4 | inputs.naersk.url = "github:nix-community/naersk"; 5 | inputs.flake-utils.url = "github:numtide/flake-utils"; 6 | inputs.nixpkgs.url = "github:NixOS/nixpkgs/nixpkgs-unstable"; 7 | 8 | outputs = 9 | { self 10 | , naersk 11 | , nixpkgs 12 | , flake-utils 13 | , ... 14 | }: 15 | flake-utils.lib.eachSystem flake-utils.lib.allSystems (system: 16 | let 17 | pkgs = (import nixpkgs) { 18 | inherit system; 19 | }; 20 | 21 | naersk' = pkgs.callPackage naersk { }; 22 | in 23 | { 24 | defaultPackage = self.packages.${system}.manix; 25 | packages.manix = naersk'.buildPackage { 26 | src = ./.; 27 | }; 28 | 29 | # For `nix develop` (optional, can be skipped): 30 | devShell = pkgs.mkShell { 31 | nativeBuildInputs = with pkgs; [ rustc cargo rust-analyzer ]; 32 | }; 33 | }); 34 | } 35 | -------------------------------------------------------------------------------- /manix.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/nix-community/manix/01c95921122a849539c17f448c58865a99263da2/manix.png -------------------------------------------------------------------------------- /release.toml: -------------------------------------------------------------------------------- 1 | pre-release-replacements = [ 2 | { file = "CHANGELOG.md", search = "Unreleased", replace = "{{version}}" }, 3 | { file = "CHANGELOG.md", search = "\\.\\.\\.HEAD", replace = "...{{tag_name}}", exactly = 1 }, 4 | { file = "CHANGELOG.md", search = "ReleaseDate", replace = "{{date}}" }, 5 | { file = "CHANGELOG.md", search = "", replace = "\n\n## [Unreleased] - ReleaseDate", exactly = 1 }, 6 | { file = "CHANGELOG.md", search = "", replace = "\n[Unreleased]: https://github.com/assert-rs/predicates-rs/compare/{{tag_name}}...HEAD", exactly = 1 }, 7 | ] 8 | -------------------------------------------------------------------------------- /rust-toolchain.toml: -------------------------------------------------------------------------------- 1 | [toolchain] 2 | channel = "nightly" 3 | targets = ["x86_64-unknown-linux-gnu", "x86_64-unknown-linux-musl", "aarch64-unknown-linux-gnu", "aarch64-unknown-linux-musl"] 4 | -------------------------------------------------------------------------------- /rustfmt.toml: -------------------------------------------------------------------------------- 1 | comment_width = 100 2 | format_code_in_doc_comments = true 3 | imports_granularity = "Crate" 4 | imports_layout = "Vertical" 5 | wrap_comments = true 6 | 7 | # The following lines may be uncommented on nightly Rust. 8 | # Once these features have stabilized, they should be added to the always-enabled options above. 9 | # unstable_features = true 10 | # imports_granularity = "Crate" 11 | # wrap_comments = true 12 | # comment_width = 100 13 | # normalize_comments = true 14 | -------------------------------------------------------------------------------- /src/bin/manix.rs: -------------------------------------------------------------------------------- 1 | use anyhow::{ 2 | Context, 3 | Result, 4 | }; 5 | use colored::*; 6 | use comments_docsource::CommentsDatabase; 7 | use strum::VariantNames; 8 | use manix::*; 9 | use options_docsource::{ 10 | OptionsDatabase, 11 | OptionsDatabaseType, 12 | }; 13 | use std::{path::PathBuf, io}; 14 | use clap::{Parser, ValueEnum, ValueHint, Command, CommandFactory}; 15 | use lazy_static::lazy_static; 16 | use clap_complete::{generate, Generator, Shell, generator}; 17 | use clap_mangen::Man; 18 | 19 | #[derive(Debug, PartialEq, Clone, ValueEnum, VariantNames)] 20 | #[allow(non_camel_case_types)] 21 | #[strum(serialize_all = "kebab-case")] 22 | enum Source { 23 | hm_options, 24 | nd_options, 25 | nixos_options, 26 | nixpkgs_doc, 27 | nixpkgs_tree, 28 | nixpkgs_comments, 29 | } 30 | 31 | lazy_static! { 32 | static ref SOURCE_VARIANTS: String = Source::VARIANTS.join(",").to_string(); 33 | } 34 | 35 | #[derive(Clone, Copy, Debug, Eq, PartialEq, ValueEnum)] 36 | pub enum ShellCompletion { 37 | Bash, 38 | Elvish, 39 | Fish, 40 | Nu, 41 | Powershell, 42 | Zsh, 43 | } 44 | 45 | #[derive(Parser)] 46 | #[clap(name = "manix")] 47 | struct Opt { 48 | /// Force update cache 49 | #[arg(short, long)] 50 | update_cache: bool, 51 | 52 | /// Matches entries stricly 53 | #[arg(short, long)] 54 | strict: bool, 55 | 56 | /// Restrict search to chosen sources 57 | #[arg(long, value_enum, default_value = &**SOURCE_VARIANTS, use_value_delimiter = true, value_hint = ValueHint::Other)] 58 | source: Vec, 59 | 60 | /// Query to search for 61 | #[arg(name = "QUERY", value_hint = ValueHint::CommandString)] 62 | query: String, 63 | 64 | /// Generate completions for the specified shell 65 | #[arg(long = "generate", value_enum)] 66 | generator: Option, 67 | 68 | #[arg(long = "print-man")] 69 | man: bool, 70 | } 71 | 72 | fn build_source_and_add( 73 | mut source: T, 74 | name: &str, 75 | path: &PathBuf, 76 | aggregate: Option<&mut AggregateDocSource>, 77 | ) -> Option<()> 78 | where 79 | T: 'static + DocSource + Cache + Sync, 80 | { 81 | eprintln!("Building {} cache...", name); 82 | if let Err(e) = source 83 | .update() 84 | .with_context(|| anyhow::anyhow!("Failed to update {}", name)) 85 | { 86 | eprintln!("{:?}", e); 87 | return None; 88 | } 89 | 90 | if let Err(e) = source 91 | .save(path) 92 | .with_context(|| format!("Failed to save {} cache", name)) 93 | { 94 | eprintln!("{:?}", e); 95 | return None; 96 | } 97 | 98 | if let Some(aggregate) = aggregate { 99 | aggregate.add_source(Box::new(source)); 100 | } 101 | Some(()) 102 | } 103 | 104 | fn load_source_and_add( 105 | load_result: Result, std::io::Error>, 106 | name: &str, 107 | aggregate: &mut AggregateDocSource, 108 | ignore_file_io_error: bool, 109 | ) -> Option<()> 110 | where 111 | T: 'static + DocSource + Cache + Sync, 112 | { 113 | let load_result = match load_result { 114 | Err(e) => { 115 | if !ignore_file_io_error { 116 | eprintln!("Failed to load {} cache file: {:?}", name, e); 117 | } 118 | return None; 119 | } 120 | Ok(r) => r, 121 | }; 122 | 123 | match load_result.with_context(|| anyhow::anyhow!("Failed to load {}", name)) { 124 | Err(e) => { 125 | eprintln!("{:?}", e); 126 | None 127 | } 128 | Ok(source) => { 129 | aggregate.add_source(Box::new(source)); 130 | Some(()) 131 | } 132 | } 133 | } 134 | 135 | fn print_completions(gen: G, cmd: &mut Command) { 136 | generate(gen, cmd, cmd.get_name().to_string(), &mut io::stdout()); 137 | } 138 | 139 | fn main() -> Result<()> { 140 | let opt: Opt = Opt::parse(); 141 | 142 | if let Some(generator) = opt.generator { 143 | let mut cmd = Opt::command(); 144 | eprintln!("Generating completion file for {generator:?}..."); 145 | 146 | match generator { 147 | ShellCompletion::Bash => print_completions(Shell::Bash, &mut cmd), 148 | ShellCompletion::Elvish => print_completions(Shell::Elvish, &mut cmd), 149 | ShellCompletion::Fish => print_completions(Shell::Fish , &mut cmd), 150 | ShellCompletion::Nu => print_completions(clap_complete_nushell::Nushell , &mut cmd), 151 | ShellCompletion::Powershell => print_completions(Shell::PowerShell, &mut cmd), 152 | ShellCompletion::Zsh => print_completions(Shell::Zsh , &mut cmd) 153 | } 154 | 155 | Ok(()) 156 | } else { 157 | 158 | if opt.man { 159 | let cmd = Opt::command(); 160 | eprintln!("Generating manpage..."); 161 | 162 | Man::new(cmd).render(&mut io::stdout()).unwrap(); 163 | 164 | Ok(()) 165 | } else { 166 | 167 | let cache_dir = 168 | xdg::BaseDirectories::with_prefix("manix").context("Failed to get a cache directory")?; 169 | 170 | let last_version_path = cache_dir 171 | .place_cache_file("last_version.txt") 172 | .context("Failed to place last version file")?; 173 | 174 | let options_nd_cache_path = cache_dir 175 | .place_cache_file("options_nd_database.bin") 176 | .context("Failed to place nix-darwin options cache file")?; 177 | let options_nixos_cache_path = cache_dir 178 | .place_cache_file("options_nixos_database.bin") 179 | .context("Failed to place NixOS options cache file")?; 180 | let options_hm_cache_path = cache_dir 181 | .place_cache_file("options_hm_database.bin") 182 | .context("Failed to place home-manager options cache file")?; 183 | let comment_cache_path = cache_dir 184 | .place_cache_file("comments.bin") 185 | .context("Failed to place cache file")?; 186 | let nixpkgs_tree_cache_path = cache_dir 187 | .place_cache_file("nixpkgs_tree.bin") 188 | .context("Failed to place nixpkgs tree cache file")?; 189 | let nixpkgs_doc_cache_path = cache_dir 190 | .place_cache_file("nixpkgs_doc_database.bin") 191 | .context("Failed to place Nixpkgs Documentation cache file")?; 192 | 193 | let version = std::env!("CARGO_PKG_VERSION"); 194 | let last_version = std::fs::read(&last_version_path) 195 | .map(String::from_utf8) 196 | .unwrap_or(Ok(version.to_string()))?; 197 | 198 | let should_invalidate_cache = version != last_version; 199 | 200 | let mut aggregate_source = AggregateDocSource::default(); 201 | 202 | let mut comment_db = if !should_invalidate_cache && comment_cache_path.exists() { 203 | CommentsDatabase::load(&std::fs::read(&comment_cache_path)?) 204 | .map_err(|e| anyhow::anyhow!("Failed to load Nixpkgs comments database: {:?}", e))? 205 | } else { 206 | CommentsDatabase::new() 207 | }; 208 | if comment_db.hash_to_defs.is_empty() { 209 | eprintln!("Building Nixpkgs comments cache..."); 210 | } 211 | 212 | let cache_invalid = comment_db 213 | .update() 214 | .map_err(|e| anyhow::anyhow!(e)) 215 | .context("Failed to update cache")?; 216 | comment_db.save(&comment_cache_path)?; 217 | if opt.source.contains(&Source::nixpkgs_comments) { 218 | aggregate_source.add_source(Box::new(comment_db)); 219 | } 220 | 221 | if should_invalidate_cache || opt.update_cache || cache_invalid { 222 | if build_source_and_add( 223 | OptionsDatabase::new(OptionsDatabaseType::HomeManager), 224 | "Home Manager Options", 225 | &options_hm_cache_path, 226 | if opt.source.contains(&Source::hm_options) { 227 | Some(&mut aggregate_source) 228 | } else { 229 | None 230 | }, 231 | ) 232 | .is_none() 233 | { 234 | eprintln!("Tip: If you installed your home-manager through configuration.nix you can fix this error by adding the home-manager channel with this command: {}", "nix-channel --add https://github.com/rycee/home-manager/archive/master.tar.gz home-manager && nix-channel --update".bold()); 235 | } 236 | 237 | if build_source_and_add( 238 | OptionsDatabase::new(OptionsDatabaseType::NixDarwin), 239 | "Nix-Darwin Options", 240 | &options_nd_cache_path, 241 | if opt.source.contains(&Source::nd_options) { 242 | Some(&mut aggregate_source) 243 | } else { 244 | None 245 | }, 246 | ) 247 | .is_none() 248 | { 249 | eprintln!("Tip: Ensure darwin is set in your NIX_PATH"); 250 | } 251 | 252 | build_source_and_add( 253 | OptionsDatabase::new(OptionsDatabaseType::NixOS), 254 | "NixOS Options", 255 | &options_nixos_cache_path, 256 | if opt.source.contains(&Source::nixos_options) { 257 | Some(&mut aggregate_source) 258 | } else { 259 | None 260 | }, 261 | ); 262 | 263 | build_source_and_add( 264 | nixpkgs_tree_docsource::NixpkgsTreeDatabase::new(), 265 | "Nixpkgs Tree", 266 | &nixpkgs_tree_cache_path, 267 | if opt.source.contains(&Source::nixpkgs_tree) { 268 | Some(&mut aggregate_source) 269 | } else { 270 | None 271 | }, 272 | ); 273 | 274 | build_source_and_add( 275 | xml_docsource::XmlFuncDocDatabase::new(), 276 | "Nixpkgs Documentation", 277 | &nixpkgs_doc_cache_path, 278 | if opt.source.contains(&Source::nixpkgs_doc) { 279 | Some(&mut aggregate_source) 280 | } else { 281 | None 282 | }, 283 | ); 284 | 285 | std::fs::write(&last_version_path, version)?; 286 | } else { 287 | if opt.source.contains(&Source::nixos_options) { 288 | load_source_and_add( 289 | std::fs::read(&options_nixos_cache_path).map(|c| OptionsDatabase::load(&c)), 290 | "NixOS Options", 291 | &mut aggregate_source, 292 | false, 293 | ); 294 | } 295 | 296 | if opt.source.contains(&Source::nd_options) { 297 | load_source_and_add( 298 | std::fs::read(&options_nd_cache_path).map(|c| OptionsDatabase::load(&c)), 299 | "Nix Darwin Options", 300 | &mut aggregate_source, 301 | true, 302 | ); 303 | } 304 | 305 | if opt.source.contains(&Source::hm_options) { 306 | load_source_and_add( 307 | std::fs::read(&options_hm_cache_path).map(|c| OptionsDatabase::load(&c)), 308 | "Home Manager Options", 309 | &mut aggregate_source, 310 | true, 311 | ); 312 | } 313 | 314 | if opt.source.contains(&Source::nixpkgs_tree) { 315 | load_source_and_add( 316 | std::fs::read(&nixpkgs_tree_cache_path) 317 | .map(|c| nixpkgs_tree_docsource::NixpkgsTreeDatabase::load(&c)), 318 | "Nixpkgs Tree", 319 | &mut aggregate_source, 320 | false, 321 | ); 322 | } 323 | 324 | if opt.source.contains(&Source::nixpkgs_doc) { 325 | load_source_and_add( 326 | std::fs::read(&nixpkgs_doc_cache_path) 327 | .map(|c| xml_docsource::XmlFuncDocDatabase::load(&c)), 328 | "Nixpkgs Documentation", 329 | &mut aggregate_source, 330 | false, 331 | ); 332 | } 333 | } 334 | 335 | let query_lower = opt.query.to_ascii_lowercase(); 336 | let query = manix::Lowercase(query_lower.as_bytes()); 337 | let entries = if opt.strict { 338 | aggregate_source.search(&query) 339 | } else { 340 | aggregate_source.search_liberal(&query) 341 | }; 342 | let (entries, key_only_entries): (Vec, Vec) = entries 343 | .into_iter() 344 | .partition(|e| !matches!(e, DocEntry::NixpkgsTreeDoc(_))); 345 | 346 | if !key_only_entries.is_empty() { 347 | const SHOW_MAX_LEN: usize = 50; 348 | print!("{}", "Here's what I found in nixpkgs:".bold()); 349 | for entry in key_only_entries.iter().take(SHOW_MAX_LEN) { 350 | print!(" {}", entry.name().white()); 351 | } 352 | if key_only_entries.len() > SHOW_MAX_LEN { 353 | print!(" and {} more.", key_only_entries.len() - SHOW_MAX_LEN); 354 | } 355 | println!("\n"); 356 | } 357 | 358 | for entry in entries { 359 | const LINE: &str = "────────────────────"; 360 | println!( 361 | "{}\n{}\n{}", 362 | entry.source().white(), 363 | LINE.green(), 364 | entry.pretty_printed() 365 | ); 366 | } 367 | 368 | Ok(()) 369 | }}} 370 | -------------------------------------------------------------------------------- /src/comments_docsource.rs: -------------------------------------------------------------------------------- 1 | use crate::{ 2 | contains_insensitive_ascii, 3 | starts_with_insensitive_ascii, 4 | Cache, 5 | DocEntry, 6 | DocSource, 7 | Errors, 8 | Lowercase, 9 | }; 10 | use colored::*; 11 | use lazy_static::lazy_static; 12 | use rayon::prelude::*; 13 | use rnix::{ 14 | ast::{ 15 | AttrSet, 16 | Entry, 17 | HasEntry, 18 | Ident, 19 | Lambda, 20 | }, 21 | NodeOrToken, 22 | Root, 23 | SyntaxKind, 24 | SyntaxNode, 25 | WalkEvent, 26 | }; 27 | use rowan::ast::AstNode; 28 | use serde::{ 29 | Deserialize, 30 | Serialize, 31 | }; 32 | use std::{ 33 | collections::HashMap, 34 | path::PathBuf, 35 | process::Command, 36 | }; 37 | lazy_static! { 38 | static ref NIXPKGS_PATH: PathBuf = get_nixpkgs_root(); 39 | } 40 | 41 | fn find_comments(node: SyntaxNode) -> Option> { 42 | let mut node = NodeOrToken::Node(node); 43 | let mut comments = Vec::::new(); 44 | 45 | loop { 46 | loop { 47 | if let Some(new) = node.prev_sibling_or_token() { 48 | node = new; 49 | break; 50 | } else { 51 | node = NodeOrToken::Node(node.parent()?); 52 | } 53 | } 54 | 55 | match node.kind() { 56 | SyntaxKind::TOKEN_COMMENT => match &node { 57 | NodeOrToken::Token(token) => comments.push(token.text().into()), 58 | NodeOrToken::Node(_) => unreachable!(), 59 | }, 60 | // This stuff is found as part of `the-fn = f: ...` 61 | // here: ^^^^^^^^ 62 | SyntaxKind::NODE_ATTRPATH | SyntaxKind::TOKEN_ASSIGN => (), 63 | t if t.is_trivia() => (), 64 | _ => break, 65 | } 66 | } 67 | 68 | // reverse the order because the function reads bottom-up 69 | comments.reverse(); 70 | Some(comments) 71 | } 72 | 73 | fn visit_attr_entry(entry: Entry) -> Option { 74 | let ident = Ident::cast(entry.syntax().clone())?; 75 | let lambda = Lambda::cast(entry.syntax().clone())?; 76 | 77 | let comments = find_comments(lambda.syntax().clone()).unwrap_or_default(); 78 | 79 | Some(CommentDocumentation::new(ident.to_string(), comments)) 80 | } 81 | 82 | fn visit_attrset(set: &AttrSet) -> Vec { 83 | set.entries() 84 | .flat_map(|e| visit_attr_entry(e).into_iter()) 85 | .collect() 86 | } 87 | 88 | fn walk_ast(ast: Root) -> Vec { 89 | let mut res = Vec::::new(); 90 | for ev in ast.expr().unwrap().syntax().preorder_with_tokens() { 91 | match ev { 92 | WalkEvent::Enter(enter) => { 93 | if let Some(set) = enter.into_node().and_then(AttrSet::cast) { 94 | res.append(&mut visit_attrset(&set)); 95 | } 96 | } 97 | WalkEvent::Leave(_) => {} 98 | } 99 | } 100 | 101 | res 102 | } 103 | 104 | #[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)] 105 | pub struct CommentDocumentation { 106 | pub key: String, 107 | pub path: Option, 108 | pub comments: Vec, 109 | } 110 | 111 | impl CommentDocumentation { 112 | pub fn new(key: String, comments: Vec) -> Self { 113 | Self { 114 | key, 115 | comments, 116 | path: None, 117 | } 118 | } 119 | pub fn with_path(self, path: PathBuf) -> Self { 120 | CommentDocumentation { 121 | path: Some(path), 122 | ..self 123 | } 124 | } 125 | } 126 | 127 | pub fn cleanup_comment(s: &str) -> &str { 128 | s.trim_start_matches('#') 129 | .trim_start_matches("/*") 130 | .trim_end_matches("*/") 131 | } 132 | 133 | impl CommentDocumentation { 134 | pub fn pretty_printed(&self) -> String { 135 | let heading = self.key.blue().bold(); 136 | let path = self 137 | .path 138 | .as_ref() 139 | .map(|path| { 140 | path.strip_prefix(NIXPKGS_PATH.to_owned()) 141 | .unwrap_or(path) 142 | .display() 143 | .to_string() 144 | }) 145 | .unwrap_or_default() 146 | .white(); 147 | 148 | let comment = self 149 | .comments 150 | .iter() 151 | .map(|c: &String| cleanup_comment(c)) 152 | .collect::>() 153 | .join("\n"); 154 | 155 | format!("# {} ({})\n{}\n\n", heading, path, comment) 156 | } 157 | pub fn name(&self) -> String { 158 | self.key.to_owned() 159 | } 160 | } 161 | 162 | #[derive(Debug, Serialize, Deserialize)] 163 | pub struct CommentsDatabase { 164 | pub hash_to_defs: HashMap>, 165 | } 166 | 167 | impl DocSource for CommentsDatabase { 168 | fn all_keys(&self) -> Vec<&str> { 169 | self.hash_to_defs 170 | .values() 171 | .flatten() 172 | .map(|def| def.key.as_ref()) 173 | .collect() 174 | } 175 | fn search(&self, query: &Lowercase) -> Vec { 176 | self.hash_to_defs 177 | .values() 178 | .flatten() 179 | .filter(|d| { 180 | !d.comments.is_empty() && starts_with_insensitive_ascii(d.key.as_bytes(), query) 181 | }) 182 | .cloned() 183 | .map(DocEntry::CommentDoc) 184 | .collect() 185 | } 186 | fn search_liberal(&self, query: &Lowercase) -> Vec { 187 | self.hash_to_defs 188 | .values() 189 | .flatten() 190 | .filter(|d| { 191 | !d.comments.is_empty() && contains_insensitive_ascii(d.key.as_bytes(), query) 192 | }) 193 | .cloned() 194 | .map(DocEntry::CommentDoc) 195 | .collect() 196 | } 197 | fn update(&mut self) -> Result { 198 | let files = find_nix_files(get_nixpkgs_root()) 199 | .par_iter() 200 | .filter_map(|f| match std::fs::read_to_string(f.path()) { 201 | Ok(content) => { 202 | let mut hasher = crc32fast::Hasher::new(); 203 | hasher.update(content.as_bytes()); 204 | let hash = hasher.finalize(); 205 | Some((hash, f.path().to_path_buf(), content)) 206 | } 207 | Err(_) => { 208 | eprintln!("Skipped {}", f.path().to_str()?); 209 | None 210 | } 211 | }) 212 | .collect::>(); 213 | 214 | let new_defs = files 215 | .par_iter() 216 | .filter(|(hash, _, _)| !self.is_in_cache(hash)) 217 | .map(|(hash, path, content)| { 218 | let ast = match rnix::Root::parse(content).ok() { 219 | Ok(ast) => ast, 220 | Err(e) => { 221 | eprintln!("Error parsing {}: {}", path.display(), e); 222 | return (*hash, Vec::new()); 223 | } 224 | }; 225 | 226 | let definitions = walk_ast(ast) 227 | .into_iter() 228 | .map(|def| def.with_path(path.clone())) 229 | .collect(); 230 | 231 | (*hash, definitions) 232 | }) 233 | .collect::)>>(); 234 | if new_defs.is_empty() { 235 | return Ok(false); 236 | } 237 | 238 | for (hash, defs) in new_defs { 239 | self.add_to_cache(hash, defs); 240 | } 241 | 242 | Ok(true) 243 | } 244 | } 245 | 246 | impl Cache for CommentsDatabase {} 247 | impl Default for CommentsDatabase { 248 | fn default() -> Self { 249 | Self::new() 250 | } 251 | } 252 | 253 | impl CommentsDatabase { 254 | pub fn new() -> Self { 255 | Self { 256 | hash_to_defs: HashMap::new(), 257 | } 258 | } 259 | 260 | fn is_in_cache(&self, hash: &u32) -> bool { 261 | self.hash_to_defs.contains_key(hash) 262 | } 263 | 264 | fn add_to_cache( 265 | &mut self, 266 | hash: u32, 267 | defs: Vec, 268 | ) -> Option> { 269 | self.hash_to_defs.insert(hash, defs) 270 | } 271 | } 272 | 273 | fn find_nix_files(path: PathBuf) -> Vec { 274 | walkdir::WalkDir::new(path) 275 | .into_iter() 276 | .filter_map(Result::ok) 277 | .filter(|e| !e.file_type().is_dir()) 278 | .filter(|e| !e.path().to_str().unwrap().contains("test")) 279 | .filter(|e| e.path().extension().and_then(|s| s.to_str()) == Some("nix")) 280 | .collect::>() 281 | } 282 | 283 | fn get_nixpkgs_root() -> PathBuf { 284 | let channel_path = Command::new("nix-instantiate") 285 | .arg("--eval") 286 | .arg("--strict") 287 | .arg("-E") 288 | .arg("") 289 | .output() 290 | .map(|o| String::from_utf8(o.stdout)); 291 | 292 | if let Ok(Ok(path)) = channel_path { 293 | PathBuf::from(path.trim_end()) 294 | } else { 295 | PathBuf::from(".") 296 | } 297 | } 298 | -------------------------------------------------------------------------------- /src/lib.rs: -------------------------------------------------------------------------------- 1 | use comments_docsource::CommentDocumentation; 2 | use options_docsource::{ 3 | OptionDocumentation, 4 | OptionsDatabaseType, 5 | }; 6 | use rayon::iter::{ 7 | IntoParallelRefIterator, 8 | ParallelIterator, 9 | }; 10 | use std::path::PathBuf; 11 | use thiserror::Error; 12 | use xml_docsource::XmlFuncDocumentation; 13 | 14 | pub mod comments_docsource; 15 | pub mod nixpkgs_tree_docsource; 16 | pub mod options_docsource; 17 | pub mod xml_docsource; 18 | 19 | pub trait Cache 20 | where 21 | Self: Sized + DocSource + serde::Serialize, 22 | { 23 | /// Deserializes content to Self 24 | fn load<'a>(content: &'a [u8]) -> Result 25 | where 26 | Self: serde::Deserialize<'a>, 27 | { 28 | Ok(bincode::deserialize(content)?) 29 | } 30 | /// Saves self to a file, serialized with bincode 31 | fn save(&self, filename: &PathBuf) -> Result<(), Errors> { 32 | let x = bincode::serialize(self)?; 33 | std::fs::write(filename, x)?; 34 | Ok(()) 35 | } 36 | } 37 | 38 | #[derive(Error, Debug)] 39 | pub enum Errors { 40 | #[error("IO Error for file {}: {}", .filename, .err)] 41 | FileIo { 42 | filename: String, 43 | err: std::io::Error, 44 | }, 45 | #[error("Failed to perform IO on a cache file")] 46 | CacheFileIo(#[from] std::io::Error), 47 | #[error("Failed to serialize/deserialize cache(bincode)")] 48 | Bincode(#[from] bincode::Error), 49 | #[error("Failed to serialize/deserialize cache(serde_json)")] 50 | SerdeJson(#[from] serde_json::Error), 51 | #[error("XML parsing error for file {}: {}", .filename, .err)] 52 | XmlParse { 53 | filename: String, 54 | err: roxmltree::Error, 55 | }, 56 | } 57 | 58 | #[derive(Debug, PartialEq, Eq)] 59 | pub enum DocEntry { 60 | OptionDoc(OptionsDatabaseType, OptionDocumentation), 61 | CommentDoc(CommentDocumentation), 62 | XmlFuncDoc(XmlFuncDocumentation), 63 | NixpkgsTreeDoc(String), 64 | } 65 | 66 | impl DocEntry { 67 | pub fn name(&self) -> String { 68 | match self { 69 | DocEntry::OptionDoc(_, x) => x.name(), 70 | DocEntry::CommentDoc(x) => x.name(), 71 | DocEntry::XmlFuncDoc(x) => x.name(), 72 | DocEntry::NixpkgsTreeDoc(x) => x.clone(), 73 | } 74 | } 75 | pub fn pretty_printed(&self) -> String { 76 | match self { 77 | DocEntry::OptionDoc(_, x) => x.pretty_printed(), 78 | DocEntry::CommentDoc(x) => x.pretty_printed(), 79 | DocEntry::XmlFuncDoc(x) => x.pretty_printed(), 80 | DocEntry::NixpkgsTreeDoc(x) => x.clone(), 81 | } 82 | } 83 | pub fn source(&self) -> &str { 84 | match self { 85 | DocEntry::OptionDoc(typ, _) => match typ { 86 | OptionsDatabaseType::NixOS => "NixOS Options", 87 | OptionsDatabaseType::NixDarwin => "NixDarwin Options", 88 | OptionsDatabaseType::HomeManager => "HomeManager Options", 89 | }, 90 | DocEntry::CommentDoc(_) => "Nixpkgs Comments", 91 | DocEntry::XmlFuncDoc(_) => "Nixpkgs Documentation", 92 | DocEntry::NixpkgsTreeDoc(_) => "Nixpkgs Tree", 93 | } 94 | } 95 | } 96 | 97 | pub trait DocSource { 98 | fn all_keys(&self) -> Vec<&str>; 99 | fn search(&self, query: &Lowercase) -> Vec; 100 | fn search_liberal(&self, query: &Lowercase) -> Vec; 101 | 102 | /// Updates the cache, returns true if anything changed 103 | fn update(&mut self) -> Result; 104 | } 105 | 106 | #[derive(Default)] 107 | pub struct AggregateDocSource { 108 | sources: Vec>, 109 | } 110 | 111 | impl AggregateDocSource { 112 | pub fn add_source(&mut self, source: Box) { 113 | self.sources.push(source) 114 | } 115 | } 116 | 117 | impl DocSource for AggregateDocSource { 118 | fn all_keys(&self) -> Vec<&str> { 119 | self.sources 120 | .par_iter() 121 | .flat_map(|source| source.all_keys()) 122 | .collect() 123 | } 124 | fn search(&self, query: &Lowercase) -> Vec { 125 | self.sources 126 | .par_iter() 127 | .flat_map(|source| source.search(query)) 128 | .collect() 129 | } 130 | fn search_liberal(&self, query: &Lowercase) -> Vec { 131 | self.sources 132 | .par_iter() 133 | .flat_map(|source| source.search_liberal(query)) 134 | .collect() 135 | } 136 | fn update(&mut self) -> Result { 137 | unimplemented!(); 138 | } 139 | } 140 | 141 | pub struct Lowercase<'a>(pub &'a [u8]); 142 | 143 | pub(crate) fn starts_with_insensitive_ascii(s: &[u8], prefix: &Lowercase) -> bool { 144 | let prefix = prefix.0; 145 | 146 | if s.len() < prefix.len() { 147 | return false; 148 | } 149 | 150 | for (i, b) in prefix.iter().enumerate() { 151 | // this is safe because of the earlier if check 152 | if unsafe { s.get_unchecked(i) }.to_ascii_lowercase() != *b { 153 | return false; 154 | } 155 | } 156 | 157 | true 158 | } 159 | 160 | pub(crate) fn contains_insensitive_ascii(s: &[u8], inner: &Lowercase) -> bool { 161 | let inner = inner.0; 162 | 163 | if s.len() < inner.len() { 164 | return false; 165 | } 166 | 167 | 'outer: for i in 0..(s.len() - inner.len() + 1) { 168 | let target = &s[i..i + inner.len()]; 169 | for (y, b) in target.iter().enumerate() { 170 | if *unsafe { inner.get_unchecked(y) } != b.to_ascii_lowercase() { 171 | continue 'outer; 172 | } 173 | } 174 | return true; 175 | } 176 | 177 | false 178 | } 179 | 180 | #[test] 181 | fn test_starts_with_insensitive_ascii() { 182 | assert!(starts_with_insensitive_ascii( 183 | "This is a string".as_bytes(), 184 | &Lowercase(b"this ") 185 | ),); 186 | assert!(starts_with_insensitive_ascii( 187 | "abc".as_bytes(), 188 | &Lowercase(b"abc") 189 | ),); 190 | assert!(!starts_with_insensitive_ascii( 191 | "This is a string".as_bytes(), 192 | &Lowercase(b"x") 193 | ),); 194 | assert!(!starts_with_insensitive_ascii( 195 | "thi".as_bytes(), 196 | &Lowercase(b"this ") 197 | ),); 198 | } 199 | 200 | #[test] 201 | fn test_contains_insensitive_ascii() { 202 | assert!(contains_insensitive_ascii( 203 | "abc".as_bytes(), 204 | &Lowercase(b"b") 205 | ),); 206 | assert!(contains_insensitive_ascii( 207 | "abc".as_bytes(), 208 | &Lowercase(b"abc") 209 | ),); 210 | assert!(contains_insensitive_ascii( 211 | "xabcx".as_bytes(), 212 | &Lowercase(b"abc") 213 | ),); 214 | assert!(!contains_insensitive_ascii( 215 | "abc".as_bytes(), 216 | &Lowercase(b"x") 217 | ),); 218 | assert!(!contains_insensitive_ascii( 219 | "abc".as_bytes(), 220 | &Lowercase(b"abcd") 221 | ),); 222 | } 223 | -------------------------------------------------------------------------------- /src/nix/darwin-options.nix: -------------------------------------------------------------------------------- 1 | let 2 | inherit (import {}) runCommandLocal; 3 | 4 | attempt = builtins.tryEval ; 5 | 6 | nix-darwin = 7 | if attempt.success 8 | then attempt.value 9 | else fetchTarball "https://github.com/LnL7/nix-darwin/archive/refs/heads/master.tar.gz"; 10 | 11 | eval = import nix-darwin {configuration = {...}: {};}; 12 | opts = eval.config.system.build.manual.optionsJSON; 13 | in 14 | runCommandLocal "options.json" {inherit opts;} '' 15 | cp $opts/share/doc/darwin/options.json $out 16 | '' 17 | -------------------------------------------------------------------------------- /src/nix/hm-options.nix: -------------------------------------------------------------------------------- 1 | { 2 | release ? "23.05", 3 | isReleaseBranch ? false, 4 | pkgs ? import {}, 5 | }: let 6 | hmargs = { 7 | inherit release isReleaseBranch pkgs; 8 | lib = import pkgs.lib; 9 | }; 10 | 11 | docs = import hmargs; 12 | in 13 | ( 14 | if builtins.isFunction docs 15 | then docs hmargs 16 | else docs 17 | ) 18 | .options 19 | .json 20 | -------------------------------------------------------------------------------- /src/nix/nixos-options.nix: -------------------------------------------------------------------------------- 1 | with import {}; let 2 | eval = import (pkgs.path + "/nixos/lib/eval-config.nix") {modules = [];}; 3 | opts = (nixosOptionsDoc {options = eval.options;}).optionsJSON; 4 | in 5 | runCommandLocal "options.json" {inherit opts;} 6 | "cp $opts/share/doc/nixos/options.json $out" 7 | -------------------------------------------------------------------------------- /src/nixpkgs_tree_docsource.rs: -------------------------------------------------------------------------------- 1 | use crate::{ 2 | contains_insensitive_ascii, 3 | starts_with_insensitive_ascii, 4 | Cache, 5 | DocEntry, 6 | DocSource, 7 | Errors, 8 | Lowercase, 9 | }; 10 | use serde::{ 11 | Deserialize, 12 | Serialize, 13 | }; 14 | use std::{ 15 | collections::HashMap, 16 | process::Command, 17 | }; 18 | 19 | #[derive(Debug, Serialize, Deserialize)] 20 | pub struct NixpkgsTreeDatabase { 21 | keys: Vec, 22 | } 23 | 24 | impl Default for NixpkgsTreeDatabase { 25 | fn default() -> Self { 26 | Self::new() 27 | } 28 | } 29 | 30 | impl NixpkgsTreeDatabase { 31 | pub fn new() -> Self { 32 | Self { keys: Vec::new() } 33 | } 34 | } 35 | 36 | #[derive(Serialize, Deserialize)] 37 | struct Keys(HashMap); 38 | 39 | impl From for Vec { 40 | fn from(val: Keys) -> Self { 41 | let mut res = Vec::::new(); 42 | for (mut name, keys) in val.0 { 43 | res.push(name.clone()); 44 | name.push('.'); 45 | for key in Into::>::into(keys) { 46 | let mut name = name.clone(); 47 | name.push_str(&key); 48 | res.push(name); 49 | } 50 | } 51 | res 52 | } 53 | } 54 | 55 | impl DocSource for NixpkgsTreeDatabase { 56 | fn all_keys(&self) -> Vec<&str> { 57 | self.keys.iter().map(|k| k.as_str()).collect() 58 | } 59 | fn search(&self, query: &Lowercase) -> Vec { 60 | self.keys 61 | .iter() 62 | .filter(|k| starts_with_insensitive_ascii(k.as_bytes(), query)) 63 | .map(|k| DocEntry::NixpkgsTreeDoc(k.clone())) 64 | .collect() 65 | } 66 | fn search_liberal(&self, query: &Lowercase) -> Vec { 67 | self.keys 68 | .iter() 69 | .filter(|k| contains_insensitive_ascii(k.as_bytes(), query)) 70 | .map(|k| DocEntry::NixpkgsTreeDoc(k.clone())) 71 | .collect() 72 | } 73 | fn update(&mut self) -> Result { 74 | let new_keys = gen_keys()?; 75 | let old = std::mem::replace(&mut self.keys, new_keys); 76 | 77 | Ok(old != self.keys) 78 | } 79 | } 80 | impl Cache for NixpkgsTreeDatabase {} 81 | 82 | fn gen_keys() -> Result, Errors> { 83 | const CODE: &str = r#" 84 | let 85 | pkgs = import { }; 86 | f = with builtins; v: (mapAttrs 87 | (name: value: 88 | if (tryEval value).success 89 | && ! (tryEval (pkgs.lib.isDerivation value)).value 90 | && isAttrs value 91 | then mapAttrs (_: _: {}) value 92 | else {} 93 | ) 94 | v 95 | ); 96 | in 97 | (f (pkgs // { pkgs = {}; lib = {}; })) // { lib = f pkgs.lib; } 98 | "#; 99 | 100 | let command = Command::new("nix-instantiate") 101 | .arg("--json") 102 | .arg("--strict") 103 | .arg("--eval") 104 | .arg("-E") 105 | .arg(CODE) 106 | .output()?; 107 | 108 | let keys = serde_json::from_slice::(&command.stdout)?; 109 | 110 | Ok(Into::>::into(keys)) 111 | } 112 | -------------------------------------------------------------------------------- /src/options_docsource.rs: -------------------------------------------------------------------------------- 1 | use crate::{ 2 | contains_insensitive_ascii, 3 | starts_with_insensitive_ascii, 4 | Cache, 5 | DocEntry, 6 | DocSource, 7 | Errors, 8 | Lowercase, 9 | }; 10 | use colored::*; 11 | use serde::{ 12 | Deserialize, 13 | Serialize, 14 | }; 15 | use std::{ 16 | collections::HashMap, 17 | path::PathBuf, 18 | process::Command, 19 | }; 20 | 21 | #[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)] 22 | pub struct OptionDocumentation { 23 | #[serde(default)] 24 | description: String, 25 | 26 | #[serde(default, rename(serialize = "readOnly", deserialize = "readOnly"))] 27 | read_only: bool, 28 | 29 | #[serde(rename(serialize = "loc", deserialize = "loc"))] 30 | location: Vec, 31 | 32 | #[serde(rename(serialize = "type", deserialize = "type"))] 33 | option_type: String, 34 | } 35 | 36 | impl OptionDocumentation { 37 | pub fn name(&self) -> String { 38 | self.location.join(".") 39 | } 40 | pub fn pretty_printed(&self) -> String { 41 | format!( 42 | "# {}\n{}\ntype: {}\n\n", 43 | self.name().blue().bold(), 44 | self.description, 45 | self.option_type 46 | ) 47 | } 48 | } 49 | 50 | #[derive(Debug, Copy, Clone, PartialEq, Eq, Serialize, Deserialize)] 51 | pub enum OptionsDatabaseType { 52 | NixOS, 53 | NixDarwin, 54 | HomeManager, 55 | } 56 | 57 | #[derive(Debug, Serialize, Deserialize)] 58 | pub struct OptionsDatabase { 59 | pub typ: OptionsDatabaseType, 60 | pub options: HashMap, 61 | } 62 | 63 | impl OptionsDatabase { 64 | pub fn new(typ: OptionsDatabaseType) -> Self { 65 | Self { 66 | typ, 67 | options: HashMap::new(), 68 | } 69 | } 70 | } 71 | 72 | pub fn try_from_file(path: &PathBuf) -> Result, Errors> { 73 | let options: HashMap = 74 | serde_json::from_slice(&std::fs::read(path)?)?; 75 | Ok(options) 76 | } 77 | 78 | impl DocSource for OptionsDatabase { 79 | fn all_keys(&self) -> Vec<&str> { 80 | self.options.keys().map(|x| x.as_ref()).collect() 81 | } 82 | fn search(&self, query: &Lowercase) -> Vec { 83 | self.options 84 | .iter() 85 | .filter(|(key, _)| starts_with_insensitive_ascii(key.as_bytes(), query)) 86 | .map(|(_, d)| DocEntry::OptionDoc(self.typ, d.clone())) 87 | .collect() 88 | } 89 | fn search_liberal(&self, query: &Lowercase) -> Vec { 90 | self.options 91 | .iter() 92 | .filter(|(key, _)| contains_insensitive_ascii(key.as_bytes(), query)) 93 | .map(|(_, d)| DocEntry::OptionDoc(self.typ, d.clone())) 94 | .collect() 95 | } 96 | fn update(&mut self) -> Result { 97 | let opts = match self.typ { 98 | OptionsDatabaseType::NixOS => try_from_file(&get_nixos_json_doc_path()?)?, 99 | OptionsDatabaseType::NixDarwin => try_from_file(&get_nd_json_doc_path()?)?, 100 | OptionsDatabaseType::HomeManager => try_from_file(&get_hm_json_doc_path()?)?, 101 | }; 102 | 103 | let old = std::mem::replace(&mut self.options, opts); 104 | 105 | Ok(old.keys().eq(self.options.keys())) 106 | } 107 | } 108 | 109 | impl Cache for OptionsDatabase {} 110 | 111 | pub fn get_hm_json_doc_path() -> Result { 112 | let base_path_output = Command::new("nix-build") 113 | .env("NIXPKGS_ALLOW_UNFREE", "1") 114 | .env("NIXPKGS_ALLOW_BROKEN", "1") 115 | .env("NIXPKGS_ALLOW_INSECURE", "1") 116 | .arg("-E") 117 | .arg(include_str!("nix/hm-options.nix")) 118 | .output() 119 | .map(|o| String::from_utf8(o.stdout).unwrap())?; 120 | 121 | Ok(PathBuf::from(base_path_output.trim_end_matches('\n')) 122 | .join("share/doc/home-manager/options.json")) 123 | } 124 | 125 | pub fn get_nixos_json_doc_path() -> Result { 126 | let base_path_output = Command::new("nix-build") 127 | .env("NIXPKGS_ALLOW_UNFREE", "1") 128 | .env("NIXPKGS_ALLOW_BROKEN", "1") 129 | .env("NIXPKGS_ALLOW_INSECURE", "1") 130 | .arg("--no-out-link") 131 | .arg("-E") 132 | .arg(include_str!("nix/nixos-options.nix")) 133 | .output() 134 | .map(|o| String::from_utf8(o.stdout).unwrap())?; 135 | 136 | Ok(PathBuf::from(base_path_output.trim_end_matches('\n'))) 137 | } 138 | 139 | pub fn get_nd_json_doc_path() -> Result { 140 | let base_path_output = Command::new("nix-build") 141 | .env("NIXPKGS_ALLOW_UNFREE", "1") 142 | .env("NIXPKGS_ALLOW_BROKEN", "1") 143 | .env("NIXPKGS_ALLOW_INSECURE", "1") 144 | .env("NIXPKGS_ALLOW_UNSUPPORTED_SYSTEM", "1") 145 | .arg("--no-out-link") 146 | .arg("-E") 147 | .arg(include_str!("nix/darwin-options.nix")) 148 | .output() 149 | .map(|o| String::from_utf8(o.stdout).unwrap())?; 150 | 151 | println!("{}", base_path_output.trim_end_matches('\n')); 152 | 153 | Ok(PathBuf::from(base_path_output.trim_end_matches('\n'))) 154 | } 155 | -------------------------------------------------------------------------------- /src/xml_docsource.rs: -------------------------------------------------------------------------------- 1 | use crate::{ 2 | contains_insensitive_ascii, 3 | starts_with_insensitive_ascii, 4 | Cache, 5 | DocEntry, 6 | DocSource, 7 | Errors, 8 | Lowercase, 9 | }; 10 | use colored::*; 11 | use roxmltree::{ 12 | self, 13 | Document, 14 | }; 15 | use serde::{ 16 | Deserialize, 17 | Serialize, 18 | }; 19 | use std::{ 20 | collections::HashMap, 21 | path::PathBuf, 22 | process::Command, 23 | }; 24 | use walkdir::WalkDir; 25 | 26 | #[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)] 27 | pub struct XmlFuncDocumentation { 28 | name: String, 29 | description: String, 30 | fn_type: Option, 31 | args: Vec<(String, String)>, 32 | example: Option, 33 | } 34 | 35 | impl XmlFuncDocumentation { 36 | pub fn name(&self) -> String { 37 | self.name.to_string() 38 | } 39 | 40 | pub fn pretty_printed(&self) -> String { 41 | let mut output = String::new(); 42 | if let Some(function_type) = &self.fn_type { 43 | output.push_str(&format!( 44 | "# {} ({})\n", 45 | self.name.blue().bold(), 46 | function_type.cyan() 47 | )); 48 | } else { 49 | output.push_str(&format!("# {}\n", self.name.blue())); 50 | } 51 | output.push_str(&format!("{}\n", self.description)); 52 | if !self.args.is_empty() { 53 | output.push_str("\nArguments:\n"); 54 | for (name, description) in &self.args { 55 | output.push_str(&format!(" {}: {}\n", name.green(), description)); 56 | } 57 | } 58 | if let Some(example) = &self.example { 59 | output.push_str("\nExample:\n"); 60 | for line in example.lines() { 61 | output.push_str(&format!(" {}\n", line.white())); 62 | } 63 | } 64 | output 65 | } 66 | 67 | fn from_function_section_node(node: &roxmltree::Node) -> Option { 68 | let name = node.first_element_child()?.first_element_child()?.text()?; 69 | let desc = node.descendants().find(|x| is_tag(x, "para"))?.text()?; 70 | let fn_type = node 71 | .descendants() 72 | .find(|n| { 73 | is_tag(n, "subtitle") 74 | && n.first_element_child() 75 | .map_or(false, |n| is_tag(&n, "literal")) 76 | }) 77 | .and_then(|n| n.first_element_child()) 78 | .and_then(|n| n.text()) 79 | .map(|x| x.to_string()); 80 | 81 | let args: Vec<_> = node 82 | .descendants() 83 | .find(|n| is_tag(n, "variablelist")) 84 | .map(|list| { 85 | list.children() 86 | .filter(|n| n.is_element()) 87 | .filter_map(|entry| { 88 | let name = entry.descendants().find(|n| is_tag(n, "varname")); 89 | let desc = entry.descendants().find(|n| is_tag(n, "para")); 90 | if let (Some(name), Some(desc)) = 91 | (name.and_then(|x| x.text()), desc.and_then(|x| x.text())) 92 | { 93 | Some((name.to_owned(), desc.to_owned())) 94 | } else { 95 | None 96 | } 97 | }) 98 | .collect() 99 | }) 100 | .unwrap_or_default(); 101 | 102 | let example = node 103 | .descendants() 104 | .find(|n| is_tag(n, "example")) 105 | .and_then(|n| n.descendants().find(|n| is_tag(n, "programlisting"))) 106 | .map(|n| { 107 | n.descendants() 108 | .filter_map(|n| n.text()) 109 | .collect::>() 110 | .join("") 111 | .to_string() 112 | }); 113 | Some(XmlFuncDocumentation { 114 | name: name.to_owned(), 115 | description: desc.to_owned(), 116 | fn_type, 117 | example, 118 | args, 119 | }) 120 | } 121 | } 122 | 123 | #[derive(Debug, Clone, Serialize, Deserialize)] 124 | pub struct XmlFuncDocDatabase { 125 | pub functions: HashMap, 126 | } 127 | 128 | impl Default for XmlFuncDocDatabase { 129 | fn default() -> Self { 130 | Self::new() 131 | } 132 | } 133 | 134 | impl XmlFuncDocDatabase { 135 | pub fn new() -> Self { 136 | Self { 137 | functions: HashMap::new(), 138 | } 139 | } 140 | } 141 | 142 | impl Cache for XmlFuncDocDatabase {} 143 | 144 | impl DocSource for XmlFuncDocDatabase { 145 | fn all_keys(&self) -> Vec<&str> { 146 | self.functions.keys().map(|x| x.as_str()).collect() 147 | } 148 | fn search(&self, query: &Lowercase) -> Vec { 149 | self.functions 150 | .iter() 151 | .filter(|(key, _)| starts_with_insensitive_ascii(key.as_bytes(), query)) 152 | .map(|(_, value)| DocEntry::XmlFuncDoc(value.clone())) 153 | .collect() 154 | } 155 | fn search_liberal(&self, query: &Lowercase) -> Vec { 156 | self.functions 157 | .iter() 158 | .filter(|(key, _)| contains_insensitive_ascii(key.as_bytes(), query)) 159 | .map(|(_, value)| DocEntry::XmlFuncDoc(value.clone())) 160 | .collect() 161 | } 162 | fn update(&mut self) -> Result { 163 | let doc_path = &generate_docs(); 164 | let mut result = Vec::new(); 165 | for file in xml_files_in(doc_path) { 166 | let content = std::fs::read_to_string(&file).map_err(|e| Errors::FileIo { 167 | err: e, 168 | filename: file.to_str().unwrap().to_string(), 169 | })?; 170 | let document = Document::parse(&content).map_err(|e| Errors::XmlParse { 171 | err: e, 172 | filename: file.to_str().unwrap().to_string(), 173 | })?; 174 | 175 | let mut function_entries = document 176 | .descendants() 177 | .filter(|x| is_tag(x, "section")) 178 | .filter(|x| { 179 | x.first_element_child().map_or(false, |c| { 180 | is_tag(&c, "title") 181 | && c.first_element_child() 182 | .map_or(false, |f| is_tag(&f, "function")) 183 | }) 184 | }) 185 | .filter_map(|node| XmlFuncDocumentation::from_function_section_node(&node)) 186 | .collect::>(); 187 | result.append(&mut function_entries); 188 | } 189 | 190 | let new = result.into_iter().map(|x| (x.name(), x)).collect(); 191 | let old = std::mem::replace(&mut self.functions, new); 192 | 193 | Ok(!self.functions.keys().eq(old.keys())) 194 | } 195 | } 196 | 197 | fn is_tag(x: &roxmltree::Node, name: &str) -> bool { 198 | x.tag_name().name() == name 199 | } 200 | 201 | fn xml_files_in(path: &PathBuf) -> Vec { 202 | WalkDir::new(path) 203 | .into_iter() 204 | .filter_map(Result::ok) 205 | .filter(|e| !e.file_type().is_dir()) 206 | .filter(|e| e.path().extension().and_then(|s| s.to_str()) == Some("xml")) 207 | .map(|x| x.path().to_path_buf()) 208 | .collect::>() 209 | } 210 | 211 | fn generate_docs() -> PathBuf { 212 | let doc_path = Command::new("nix-build") 213 | .arg("--no-out-link") 214 | .arg("") 215 | .output() 216 | .ok() 217 | .and_then(|o| String::from_utf8(o.stdout).ok()) 218 | .unwrap(); 219 | PathBuf::from(doc_path.trim_end_matches('\n')).join("function-docs") 220 | } 221 | --------------------------------------------------------------------------------