├── .gitignore ├── Cargo.lock ├── Cargo.toml ├── LICENSE ├── README.md ├── default.nix ├── flake.lock ├── flake.nix ├── manix.png ├── nix ├── sources.json └── sources.nix ├── shell.nix └── src ├── bin └── manix.rs ├── comments_docsource.rs ├── lib.rs ├── nixpkgs_tree_docsource.rs ├── options_docsource.rs └── xml_docsource.rs /.gitignore: -------------------------------------------------------------------------------- 1 | /target 2 | result 3 | -------------------------------------------------------------------------------- /Cargo.lock: -------------------------------------------------------------------------------- 1 | # This file is automatically @generated by Cargo. 2 | # It is not intended for manual editing. 3 | [[package]] 4 | name = "ansi_term" 5 | version = "0.11.0" 6 | source = "registry+https://github.com/rust-lang/crates.io-index" 7 | checksum = "ee49baf6cb617b853aa8d93bf420db2383fab46d314482ca2803b40d5fde979b" 8 | dependencies = [ 9 | "winapi", 10 | ] 11 | 12 | [[package]] 13 | name = "anyhow" 14 | version = "1.0.32" 15 | source = "registry+https://github.com/rust-lang/crates.io-index" 16 | checksum = "6b602bfe940d21c130f3895acd65221e8a61270debe89d628b9cb4e3ccb8569b" 17 | 18 | [[package]] 19 | name = "atty" 20 | version = "0.2.14" 21 | source = "registry+https://github.com/rust-lang/crates.io-index" 22 | checksum = "d9b39be18770d11421cdb1b9947a45dd3f37e93092cbf377614828a319d5fee8" 23 | dependencies = [ 24 | "hermit-abi", 25 | "libc", 26 | "winapi", 27 | ] 28 | 29 | [[package]] 30 | name = "autocfg" 31 | version = "1.0.0" 32 | source = "registry+https://github.com/rust-lang/crates.io-index" 33 | checksum = "f8aac770f1885fd7e387acedd76065302551364496e46b3dd00860b2f8359b9d" 34 | 35 | [[package]] 36 | name = "bincode" 37 | version = "1.3.1" 38 | source = "registry+https://github.com/rust-lang/crates.io-index" 39 | checksum = "f30d3a39baa26f9651f17b375061f3233dde33424a8b72b0dbe93a68a0bc896d" 40 | dependencies = [ 41 | "byteorder", 42 | "serde", 43 | ] 44 | 45 | [[package]] 46 | name = "bitflags" 47 | version = "1.2.1" 48 | source = "registry+https://github.com/rust-lang/crates.io-index" 49 | checksum = "cf1de2fe8c75bc145a2f577add951f8134889b4795d47466a54a5c846d691693" 50 | 51 | [[package]] 52 | name = "byteorder" 53 | version = "1.3.4" 54 | source = "registry+https://github.com/rust-lang/crates.io-index" 55 | checksum = "08c48aae112d48ed9f069b33538ea9e3e90aa263cfa3d1c24309612b1f7472de" 56 | 57 | [[package]] 58 | name = "cbitset" 59 | version = "0.2.0" 60 | source = "registry+https://github.com/rust-lang/crates.io-index" 61 | checksum = "29b6ad25ae296159fb0da12b970b2fe179b234584d7cd294c891e2bbb284466b" 62 | dependencies = [ 63 | "num-traits", 64 | ] 65 | 66 | [[package]] 67 | name = "cfg-if" 68 | version = "0.1.10" 69 | source = "registry+https://github.com/rust-lang/crates.io-index" 70 | checksum = "4785bdd1c96b2a846b2bd7cc02e86b6b3dbf14e7e53446c4f54c92a361040822" 71 | 72 | [[package]] 73 | name = "clap" 74 | version = "2.33.3" 75 | source = "registry+https://github.com/rust-lang/crates.io-index" 76 | checksum = "37e58ac78573c40708d45522f0d80fa2f01cc4f9b4e2bf749807255454312002" 77 | dependencies = [ 78 | "ansi_term", 79 | "atty", 80 | "bitflags", 81 | "strsim", 82 | "textwrap", 83 | "unicode-width", 84 | "vec_map", 85 | ] 86 | 87 | [[package]] 88 | name = "colored" 89 | version = "2.0.0" 90 | source = "registry+https://github.com/rust-lang/crates.io-index" 91 | checksum = "b3616f750b84d8f0de8a58bda93e08e2a81ad3f523089b05f1dffecab48c6cbd" 92 | dependencies = [ 93 | "atty", 94 | "lazy_static", 95 | "winapi", 96 | ] 97 | 98 | [[package]] 99 | name = "crc32fast" 100 | version = "1.2.0" 101 | source = "registry+https://github.com/rust-lang/crates.io-index" 102 | checksum = "ba125de2af0df55319f41944744ad91c71113bf74a4646efff39afe1f6842db1" 103 | dependencies = [ 104 | "cfg-if", 105 | ] 106 | 107 | [[package]] 108 | name = "crossbeam-deque" 109 | version = "0.7.3" 110 | source = "registry+https://github.com/rust-lang/crates.io-index" 111 | checksum = "9f02af974daeee82218205558e51ec8768b48cf524bd01d550abe5573a608285" 112 | dependencies = [ 113 | "crossbeam-epoch", 114 | "crossbeam-utils", 115 | "maybe-uninit", 116 | ] 117 | 118 | [[package]] 119 | name = "crossbeam-epoch" 120 | version = "0.8.2" 121 | source = "registry+https://github.com/rust-lang/crates.io-index" 122 | checksum = "058ed274caafc1f60c4997b5fc07bf7dc7cca454af7c6e81edffe5f33f70dace" 123 | dependencies = [ 124 | "autocfg", 125 | "cfg-if", 126 | "crossbeam-utils", 127 | "lazy_static", 128 | "maybe-uninit", 129 | "memoffset", 130 | "scopeguard", 131 | ] 132 | 133 | [[package]] 134 | name = "crossbeam-queue" 135 | version = "0.2.3" 136 | source = "registry+https://github.com/rust-lang/crates.io-index" 137 | checksum = "774ba60a54c213d409d5353bda12d49cd68d14e45036a285234c8d6f91f92570" 138 | dependencies = [ 139 | "cfg-if", 140 | "crossbeam-utils", 141 | "maybe-uninit", 142 | ] 143 | 144 | [[package]] 145 | name = "crossbeam-utils" 146 | version = "0.7.2" 147 | source = "registry+https://github.com/rust-lang/crates.io-index" 148 | checksum = "c3c7c73a2d1e9fc0886a08b93e98eb643461230d5f1925e4036204d5f2e261a8" 149 | dependencies = [ 150 | "autocfg", 151 | "cfg-if", 152 | "lazy_static", 153 | ] 154 | 155 | [[package]] 156 | name = "either" 157 | version = "1.6.0" 158 | source = "registry+https://github.com/rust-lang/crates.io-index" 159 | checksum = "cd56b59865bce947ac5958779cfa508f6c3b9497cc762b7e24a12d11ccde2c4f" 160 | 161 | [[package]] 162 | name = "heck" 163 | version = "0.3.1" 164 | source = "registry+https://github.com/rust-lang/crates.io-index" 165 | checksum = "20564e78d53d2bb135c343b3f47714a56af2061f1c928fdb541dc7b9fdd94205" 166 | dependencies = [ 167 | "unicode-segmentation", 168 | ] 169 | 170 | [[package]] 171 | name = "hermit-abi" 172 | version = "0.1.15" 173 | source = "registry+https://github.com/rust-lang/crates.io-index" 174 | checksum = "3deed196b6e7f9e44a2ae8d94225d80302d81208b1bb673fd21fe634645c85a9" 175 | dependencies = [ 176 | "libc", 177 | ] 178 | 179 | [[package]] 180 | name = "itoa" 181 | version = "0.4.6" 182 | source = "registry+https://github.com/rust-lang/crates.io-index" 183 | checksum = "dc6f3ad7b9d11a0c00842ff8de1b60ee58661048eb8049ed33c73594f359d7e6" 184 | 185 | [[package]] 186 | name = "lazy_static" 187 | version = "1.4.0" 188 | source = "registry+https://github.com/rust-lang/crates.io-index" 189 | checksum = "e2abad23fbc42b3700f2f279844dc832adb2b2eb069b2df918f455c4e18cc646" 190 | 191 | [[package]] 192 | name = "libc" 193 | version = "0.2.74" 194 | source = "registry+https://github.com/rust-lang/crates.io-index" 195 | checksum = "a2f02823cf78b754822df5f7f268fb59822e7296276d3e069d8e8cb26a14bd10" 196 | 197 | [[package]] 198 | name = "manix" 199 | version = "0.6.3" 200 | dependencies = [ 201 | "anyhow", 202 | "bincode", 203 | "colored", 204 | "crc32fast", 205 | "lazy_static", 206 | "rayon", 207 | "rnix", 208 | "roxmltree", 209 | "serde", 210 | "serde_json", 211 | "structopt", 212 | "thiserror", 213 | "walkdir", 214 | "xdg", 215 | ] 216 | 217 | [[package]] 218 | name = "maybe-uninit" 219 | version = "2.0.0" 220 | source = "registry+https://github.com/rust-lang/crates.io-index" 221 | checksum = "60302e4db3a61da70c0cb7991976248362f30319e88850c487b9b95bbf059e00" 222 | 223 | [[package]] 224 | name = "memoffset" 225 | version = "0.5.5" 226 | source = "registry+https://github.com/rust-lang/crates.io-index" 227 | checksum = "c198b026e1bbf08a937e94c6c60f9ec4a2267f5b0d2eec9c1b21b061ce2be55f" 228 | dependencies = [ 229 | "autocfg", 230 | ] 231 | 232 | [[package]] 233 | name = "num-traits" 234 | version = "0.2.12" 235 | source = "registry+https://github.com/rust-lang/crates.io-index" 236 | checksum = "ac267bcc07f48ee5f8935ab0d24f316fb722d7a1292e2913f0cc196b29ffd611" 237 | dependencies = [ 238 | "autocfg", 239 | ] 240 | 241 | [[package]] 242 | name = "num_cpus" 243 | version = "1.13.0" 244 | source = "registry+https://github.com/rust-lang/crates.io-index" 245 | checksum = "05499f3756671c15885fee9034446956fff3f243d6077b91e5767df161f766b3" 246 | dependencies = [ 247 | "hermit-abi", 248 | "libc", 249 | ] 250 | 251 | [[package]] 252 | name = "proc-macro-error" 253 | version = "1.0.4" 254 | source = "registry+https://github.com/rust-lang/crates.io-index" 255 | checksum = "da25490ff9892aab3fcf7c36f08cfb902dd3e71ca0f9f9517bea02a73a5ce38c" 256 | dependencies = [ 257 | "proc-macro-error-attr", 258 | "proc-macro2", 259 | "quote", 260 | "syn", 261 | "version_check", 262 | ] 263 | 264 | [[package]] 265 | name = "proc-macro-error-attr" 266 | version = "1.0.4" 267 | source = "registry+https://github.com/rust-lang/crates.io-index" 268 | checksum = "a1be40180e52ecc98ad80b184934baf3d0d29f979574e439af5a55274b35f869" 269 | dependencies = [ 270 | "proc-macro2", 271 | "quote", 272 | "version_check", 273 | ] 274 | 275 | [[package]] 276 | name = "proc-macro2" 277 | version = "1.0.19" 278 | source = "registry+https://github.com/rust-lang/crates.io-index" 279 | checksum = "04f5f085b5d71e2188cb8271e5da0161ad52c3f227a661a3c135fdf28e258b12" 280 | dependencies = [ 281 | "unicode-xid", 282 | ] 283 | 284 | [[package]] 285 | name = "quote" 286 | version = "1.0.7" 287 | source = "registry+https://github.com/rust-lang/crates.io-index" 288 | checksum = "aa563d17ecb180e500da1cfd2b028310ac758de548efdd203e18f283af693f37" 289 | dependencies = [ 290 | "proc-macro2", 291 | ] 292 | 293 | [[package]] 294 | name = "rayon" 295 | version = "1.3.1" 296 | source = "registry+https://github.com/rust-lang/crates.io-index" 297 | checksum = "62f02856753d04e03e26929f820d0a0a337ebe71f849801eea335d464b349080" 298 | dependencies = [ 299 | "autocfg", 300 | "crossbeam-deque", 301 | "either", 302 | "rayon-core", 303 | ] 304 | 305 | [[package]] 306 | name = "rayon-core" 307 | version = "1.7.1" 308 | source = "registry+https://github.com/rust-lang/crates.io-index" 309 | checksum = "e92e15d89083484e11353891f1af602cc661426deb9564c298b270c726973280" 310 | dependencies = [ 311 | "crossbeam-deque", 312 | "crossbeam-queue", 313 | "crossbeam-utils", 314 | "lazy_static", 315 | "num_cpus", 316 | ] 317 | 318 | [[package]] 319 | name = "rnix" 320 | version = "0.8.0" 321 | source = "registry+https://github.com/rust-lang/crates.io-index" 322 | checksum = "cbbea4c714e5bbf462fa4316ddf45875d8f0e28e5db81050b5f9ce99746c6863" 323 | dependencies = [ 324 | "cbitset", 325 | "rowan", 326 | ] 327 | 328 | [[package]] 329 | name = "rowan" 330 | version = "0.9.1" 331 | source = "registry+https://github.com/rust-lang/crates.io-index" 332 | checksum = "1ea7cadf87a9d8432e85cb4eb86bd2e765ace60c24ef86e79084dcae5d1c5a19" 333 | dependencies = [ 334 | "rustc-hash", 335 | "smol_str", 336 | "text_unit", 337 | "thin-dst", 338 | ] 339 | 340 | [[package]] 341 | name = "roxmltree" 342 | version = "0.13.0" 343 | source = "registry+https://github.com/rust-lang/crates.io-index" 344 | checksum = "17dfc6c39f846bfc7d2ec442ad12055d79608d501380789b965d22f9354451f2" 345 | dependencies = [ 346 | "xmlparser", 347 | ] 348 | 349 | [[package]] 350 | name = "rustc-hash" 351 | version = "1.1.0" 352 | source = "registry+https://github.com/rust-lang/crates.io-index" 353 | checksum = "08d43f7aa6b08d49f382cde6a7982047c3426db949b1424bc4b7ec9ae12c6ce2" 354 | 355 | [[package]] 356 | name = "ryu" 357 | version = "1.0.5" 358 | source = "registry+https://github.com/rust-lang/crates.io-index" 359 | checksum = "71d301d4193d031abdd79ff7e3dd721168a9572ef3fe51a1517aba235bd8f86e" 360 | 361 | [[package]] 362 | name = "same-file" 363 | version = "1.0.6" 364 | source = "registry+https://github.com/rust-lang/crates.io-index" 365 | checksum = "93fc1dc3aaa9bfed95e02e6eadabb4baf7e3078b0bd1b4d7b6b0b68378900502" 366 | dependencies = [ 367 | "winapi-util", 368 | ] 369 | 370 | [[package]] 371 | name = "scopeguard" 372 | version = "1.1.0" 373 | source = "registry+https://github.com/rust-lang/crates.io-index" 374 | checksum = "d29ab0c6d3fc0ee92fe66e2d99f700eab17a8d57d1c1d3b748380fb20baa78cd" 375 | 376 | [[package]] 377 | name = "serde" 378 | version = "1.0.115" 379 | source = "registry+https://github.com/rust-lang/crates.io-index" 380 | checksum = "e54c9a88f2da7238af84b5101443f0c0d0a3bbdc455e34a5c9497b1903ed55d5" 381 | dependencies = [ 382 | "serde_derive", 383 | ] 384 | 385 | [[package]] 386 | name = "serde_derive" 387 | version = "1.0.115" 388 | source = "registry+https://github.com/rust-lang/crates.io-index" 389 | checksum = "609feed1d0a73cc36a0182a840a9b37b4a82f0b1150369f0536a9e3f2a31dc48" 390 | dependencies = [ 391 | "proc-macro2", 392 | "quote", 393 | "syn", 394 | ] 395 | 396 | [[package]] 397 | name = "serde_json" 398 | version = "1.0.57" 399 | source = "registry+https://github.com/rust-lang/crates.io-index" 400 | checksum = "164eacbdb13512ec2745fb09d51fd5b22b0d65ed294a1dcf7285a360c80a675c" 401 | dependencies = [ 402 | "itoa", 403 | "ryu", 404 | "serde", 405 | ] 406 | 407 | [[package]] 408 | name = "smol_str" 409 | version = "0.1.16" 410 | source = "registry+https://github.com/rust-lang/crates.io-index" 411 | checksum = "2f7909a1d8bc166a862124d84fdc11bda0ea4ed3157ccca662296919c2972db1" 412 | 413 | [[package]] 414 | name = "strsim" 415 | version = "0.8.0" 416 | source = "registry+https://github.com/rust-lang/crates.io-index" 417 | checksum = "8ea5119cdb4c55b55d432abb513a0429384878c15dde60cc77b1c99de1a95a6a" 418 | 419 | [[package]] 420 | name = "structopt" 421 | version = "0.3.16" 422 | source = "registry+https://github.com/rust-lang/crates.io-index" 423 | checksum = "de5472fb24d7e80ae84a7801b7978f95a19ec32cb1876faea59ab711eb901976" 424 | dependencies = [ 425 | "clap", 426 | "lazy_static", 427 | "structopt-derive", 428 | ] 429 | 430 | [[package]] 431 | name = "structopt-derive" 432 | version = "0.4.9" 433 | source = "registry+https://github.com/rust-lang/crates.io-index" 434 | checksum = "1e0eb37335aeeebe51be42e2dc07f031163fbabfa6ac67d7ea68b5c2f68d5f99" 435 | dependencies = [ 436 | "heck", 437 | "proc-macro-error", 438 | "proc-macro2", 439 | "quote", 440 | "syn", 441 | ] 442 | 443 | [[package]] 444 | name = "syn" 445 | version = "1.0.38" 446 | source = "registry+https://github.com/rust-lang/crates.io-index" 447 | checksum = "e69abc24912995b3038597a7a593be5053eb0fb44f3cc5beec0deb421790c1f4" 448 | dependencies = [ 449 | "proc-macro2", 450 | "quote", 451 | "unicode-xid", 452 | ] 453 | 454 | [[package]] 455 | name = "text_unit" 456 | version = "0.1.10" 457 | source = "registry+https://github.com/rust-lang/crates.io-index" 458 | checksum = "20431e104bfecc1a40872578dbc390e10290a0e9c35fffe3ce6f73c15a9dbfc2" 459 | 460 | [[package]] 461 | name = "textwrap" 462 | version = "0.11.0" 463 | source = "registry+https://github.com/rust-lang/crates.io-index" 464 | checksum = "d326610f408c7a4eb6f51c37c330e496b08506c9457c9d34287ecc38809fb060" 465 | dependencies = [ 466 | "unicode-width", 467 | ] 468 | 469 | [[package]] 470 | name = "thin-dst" 471 | version = "1.1.0" 472 | source = "registry+https://github.com/rust-lang/crates.io-index" 473 | checksum = "db3c46be180f1af9673ebb27bc1235396f61ef6965b3fe0dbb2e624deb604f0e" 474 | 475 | [[package]] 476 | name = "thiserror" 477 | version = "1.0.20" 478 | source = "registry+https://github.com/rust-lang/crates.io-index" 479 | checksum = "7dfdd070ccd8ccb78f4ad66bf1982dc37f620ef696c6b5028fe2ed83dd3d0d08" 480 | dependencies = [ 481 | "thiserror-impl", 482 | ] 483 | 484 | [[package]] 485 | name = "thiserror-impl" 486 | version = "1.0.20" 487 | source = "registry+https://github.com/rust-lang/crates.io-index" 488 | checksum = "bd80fc12f73063ac132ac92aceea36734f04a1d93c1240c6944e23a3b8841793" 489 | dependencies = [ 490 | "proc-macro2", 491 | "quote", 492 | "syn", 493 | ] 494 | 495 | [[package]] 496 | name = "unicode-segmentation" 497 | version = "1.6.0" 498 | source = "registry+https://github.com/rust-lang/crates.io-index" 499 | checksum = "e83e153d1053cbb5a118eeff7fd5be06ed99153f00dbcd8ae310c5fb2b22edc0" 500 | 501 | [[package]] 502 | name = "unicode-width" 503 | version = "0.1.8" 504 | source = "registry+https://github.com/rust-lang/crates.io-index" 505 | checksum = "9337591893a19b88d8d87f2cec1e73fad5cdfd10e5a6f349f498ad6ea2ffb1e3" 506 | 507 | [[package]] 508 | name = "unicode-xid" 509 | version = "0.2.1" 510 | source = "registry+https://github.com/rust-lang/crates.io-index" 511 | checksum = "f7fe0bb3479651439c9112f72b6c505038574c9fbb575ed1bf3b797fa39dd564" 512 | 513 | [[package]] 514 | name = "vec_map" 515 | version = "0.8.2" 516 | source = "registry+https://github.com/rust-lang/crates.io-index" 517 | checksum = "f1bddf1187be692e79c5ffeab891132dfb0f236ed36a43c7ed39f1165ee20191" 518 | 519 | [[package]] 520 | name = "version_check" 521 | version = "0.9.2" 522 | source = "registry+https://github.com/rust-lang/crates.io-index" 523 | checksum = "b5a972e5669d67ba988ce3dc826706fb0a8b01471c088cb0b6110b805cc36aed" 524 | 525 | [[package]] 526 | name = "walkdir" 527 | version = "2.3.1" 528 | source = "registry+https://github.com/rust-lang/crates.io-index" 529 | checksum = "777182bc735b6424e1a57516d35ed72cb8019d85c8c9bf536dccb3445c1a2f7d" 530 | dependencies = [ 531 | "same-file", 532 | "winapi", 533 | "winapi-util", 534 | ] 535 | 536 | [[package]] 537 | name = "winapi" 538 | version = "0.3.9" 539 | source = "registry+https://github.com/rust-lang/crates.io-index" 540 | checksum = "5c839a674fcd7a98952e593242ea400abe93992746761e38641405d28b00f419" 541 | dependencies = [ 542 | "winapi-i686-pc-windows-gnu", 543 | "winapi-x86_64-pc-windows-gnu", 544 | ] 545 | 546 | [[package]] 547 | name = "winapi-i686-pc-windows-gnu" 548 | version = "0.4.0" 549 | source = "registry+https://github.com/rust-lang/crates.io-index" 550 | checksum = "ac3b87c63620426dd9b991e5ce0329eff545bccbbb34f3be09ff6fb6ab51b7b6" 551 | 552 | [[package]] 553 | name = "winapi-util" 554 | version = "0.1.5" 555 | source = "registry+https://github.com/rust-lang/crates.io-index" 556 | checksum = "70ec6ce85bb158151cae5e5c87f95a8e97d2c0c4b001223f33a334e3ce5de178" 557 | dependencies = [ 558 | "winapi", 559 | ] 560 | 561 | [[package]] 562 | name = "winapi-x86_64-pc-windows-gnu" 563 | version = "0.4.0" 564 | source = "registry+https://github.com/rust-lang/crates.io-index" 565 | checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f" 566 | 567 | [[package]] 568 | name = "xdg" 569 | version = "2.2.0" 570 | source = "registry+https://github.com/rust-lang/crates.io-index" 571 | checksum = "d089681aa106a86fade1b0128fb5daf07d5867a509ab036d99988dec80429a57" 572 | 573 | [[package]] 574 | name = "xmlparser" 575 | version = "0.13.2" 576 | source = "registry+https://github.com/rust-lang/crates.io-index" 577 | checksum = "52613e655f6f11f63c0fe7d1c3b5ef69e44d96df9b65dab296b441ed0e1125f5" 578 | -------------------------------------------------------------------------------- /Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "manix" 3 | description = "Nix documentation searcher" 4 | version = "0.6.3" 5 | authors = ["mlvzk "] 6 | edition = "2018" 7 | license = "MPL-2.0" 8 | 9 | # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html 10 | 11 | [dependencies] 12 | rnix = "0.8" 13 | crc32fast = "1.2" 14 | serde = { version = "1.0", features = [ "derive" ] } 15 | serde_json = "1.0" 16 | roxmltree = "0.13" 17 | bincode = "1.3" 18 | walkdir = "2.0" 19 | rayon = "1.3" 20 | colored = "2.0" 21 | xdg = "2.2" 22 | lazy_static = "1.4" 23 | anyhow = "1.0" 24 | thiserror = "1.0" 25 | structopt = "0.3" 26 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Mozilla Public License Version 2.0 2 | ================================== 3 | 4 | 1. Definitions 5 | -------------- 6 | 7 | 1.1. "Contributor" 8 | means each individual or legal entity that creates, contributes to 9 | the creation of, or owns Covered Software. 10 | 11 | 1.2. "Contributor Version" 12 | means the combination of the Contributions of others (if any) used 13 | by a Contributor and that particular Contributor's Contribution. 14 | 15 | 1.3. "Contribution" 16 | means Covered Software of a particular Contributor. 17 | 18 | 1.4. "Covered Software" 19 | means Source Code Form to which the initial Contributor has attached 20 | the notice in Exhibit A, the Executable Form of such Source Code 21 | Form, and Modifications of such Source Code Form, in each case 22 | including portions thereof. 23 | 24 | 1.5. "Incompatible With Secondary Licenses" 25 | means 26 | 27 | (a) that the initial Contributor has attached the notice described 28 | in Exhibit B to the Covered Software; or 29 | 30 | (b) that the Covered Software was made available under the terms of 31 | version 1.1 or earlier of the License, but not also under the 32 | terms of a Secondary License. 33 | 34 | 1.6. "Executable Form" 35 | means any form of the work other than Source Code Form. 36 | 37 | 1.7. "Larger Work" 38 | means a work that combines Covered Software with other material, in 39 | a separate file or files, that is not Covered Software. 40 | 41 | 1.8. "License" 42 | means this document. 43 | 44 | 1.9. "Licensable" 45 | means having the right to grant, to the maximum extent possible, 46 | whether at the time of the initial grant or subsequently, any and 47 | all of the rights conveyed by this License. 48 | 49 | 1.10. "Modifications" 50 | means any of the following: 51 | 52 | (a) any file in Source Code Form that results from an addition to, 53 | deletion from, or modification of the contents of Covered 54 | Software; or 55 | 56 | (b) any new file in Source Code Form that contains any Covered 57 | Software. 58 | 59 | 1.11. "Patent Claims" of a Contributor 60 | means any patent claim(s), including without limitation, method, 61 | process, and apparatus claims, in any patent Licensable by such 62 | Contributor that would be infringed, but for the grant of the 63 | License, by the making, using, selling, offering for sale, having 64 | made, import, or transfer of either its Contributions or its 65 | Contributor Version. 66 | 67 | 1.12. "Secondary License" 68 | means either the GNU General Public License, Version 2.0, the GNU 69 | Lesser General Public License, Version 2.1, the GNU Affero General 70 | Public License, Version 3.0, or any later versions of those 71 | licenses. 72 | 73 | 1.13. "Source Code Form" 74 | means the form of the work preferred for making modifications. 75 | 76 | 1.14. "You" (or "Your") 77 | means an individual or a legal entity exercising rights under this 78 | License. For legal entities, "You" includes any entity that 79 | controls, is controlled by, or is under common control with You. For 80 | purposes of this definition, "control" means (a) the power, direct 81 | or indirect, to cause the direction or management of such entity, 82 | whether by contract or otherwise, or (b) ownership of more than 83 | fifty percent (50%) of the outstanding shares or beneficial 84 | ownership of such entity. 85 | 86 | 2. License Grants and Conditions 87 | -------------------------------- 88 | 89 | 2.1. Grants 90 | 91 | Each Contributor hereby grants You a world-wide, royalty-free, 92 | non-exclusive license: 93 | 94 | (a) under intellectual property rights (other than patent or trademark) 95 | Licensable by such Contributor to use, reproduce, make available, 96 | modify, display, perform, distribute, and otherwise exploit its 97 | Contributions, either on an unmodified basis, with Modifications, or 98 | as part of a Larger Work; and 99 | 100 | (b) under Patent Claims of such Contributor to make, use, sell, offer 101 | for sale, have made, import, and otherwise transfer either its 102 | Contributions or its Contributor Version. 103 | 104 | 2.2. Effective Date 105 | 106 | The licenses granted in Section 2.1 with respect to any Contribution 107 | become effective for each Contribution on the date the Contributor first 108 | distributes such Contribution. 109 | 110 | 2.3. Limitations on Grant Scope 111 | 112 | The licenses granted in this Section 2 are the only rights granted under 113 | this License. No additional rights or licenses will be implied from the 114 | distribution or licensing of Covered Software under this License. 115 | Notwithstanding Section 2.1(b) above, no patent license is granted by a 116 | Contributor: 117 | 118 | (a) for any code that a Contributor has removed from Covered Software; 119 | or 120 | 121 | (b) for infringements caused by: (i) Your and any other third party's 122 | modifications of Covered Software, or (ii) the combination of its 123 | Contributions with other software (except as part of its Contributor 124 | Version); or 125 | 126 | (c) under Patent Claims infringed by Covered Software in the absence of 127 | its Contributions. 128 | 129 | This License does not grant any rights in the trademarks, service marks, 130 | or logos of any Contributor (except as may be necessary to comply with 131 | the notice requirements in Section 3.4). 132 | 133 | 2.4. Subsequent Licenses 134 | 135 | No Contributor makes additional grants as a result of Your choice to 136 | distribute the Covered Software under a subsequent version of this 137 | License (see Section 10.2) or under the terms of a Secondary License (if 138 | permitted under the terms of Section 3.3). 139 | 140 | 2.5. Representation 141 | 142 | Each Contributor represents that the Contributor believes its 143 | Contributions are its original creation(s) or it has sufficient rights 144 | to grant the rights to its Contributions conveyed by this License. 145 | 146 | 2.6. Fair Use 147 | 148 | This License is not intended to limit any rights You have under 149 | applicable copyright doctrines of fair use, fair dealing, or other 150 | equivalents. 151 | 152 | 2.7. Conditions 153 | 154 | Sections 3.1, 3.2, 3.3, and 3.4 are conditions of the licenses granted 155 | in Section 2.1. 156 | 157 | 3. Responsibilities 158 | ------------------- 159 | 160 | 3.1. Distribution of Source Form 161 | 162 | All distribution of Covered Software in Source Code Form, including any 163 | Modifications that You create or to which You contribute, must be under 164 | the terms of this License. You must inform recipients that the Source 165 | Code Form of the Covered Software is governed by the terms of this 166 | License, and how they can obtain a copy of this License. You may not 167 | attempt to alter or restrict the recipients' rights in the Source Code 168 | Form. 169 | 170 | 3.2. Distribution of Executable Form 171 | 172 | If You distribute Covered Software in Executable Form then: 173 | 174 | (a) such Covered Software must also be made available in Source Code 175 | Form, as described in Section 3.1, and You must inform recipients of 176 | the Executable Form how they can obtain a copy of such Source Code 177 | Form by reasonable means in a timely manner, at a charge no more 178 | than the cost of distribution to the recipient; and 179 | 180 | (b) You may distribute such Executable Form under the terms of this 181 | License, or sublicense it under different terms, provided that the 182 | license for the Executable Form does not attempt to limit or alter 183 | the recipients' rights in the Source Code Form under this License. 184 | 185 | 3.3. Distribution of a Larger Work 186 | 187 | You may create and distribute a Larger Work under terms of Your choice, 188 | provided that You also comply with the requirements of this License for 189 | the Covered Software. If the Larger Work is a combination of Covered 190 | Software with a work governed by one or more Secondary Licenses, and the 191 | Covered Software is not Incompatible With Secondary Licenses, this 192 | License permits You to additionally distribute such Covered Software 193 | under the terms of such Secondary License(s), so that the recipient of 194 | the Larger Work may, at their option, further distribute the Covered 195 | Software under the terms of either this License or such Secondary 196 | License(s). 197 | 198 | 3.4. Notices 199 | 200 | You may not remove or alter the substance of any license notices 201 | (including copyright notices, patent notices, disclaimers of warranty, 202 | or limitations of liability) contained within the Source Code Form of 203 | the Covered Software, except that You may alter any license notices to 204 | the extent required to remedy known factual inaccuracies. 205 | 206 | 3.5. Application of Additional Terms 207 | 208 | You may choose to offer, and to charge a fee for, warranty, support, 209 | indemnity or liability obligations to one or more recipients of Covered 210 | Software. However, You may do so only on Your own behalf, and not on 211 | behalf of any Contributor. You must make it absolutely clear that any 212 | such warranty, support, indemnity, or liability obligation is offered by 213 | You alone, and You hereby agree to indemnify every Contributor for any 214 | liability incurred by such Contributor as a result of warranty, support, 215 | indemnity or liability terms You offer. You may include additional 216 | disclaimers of warranty and limitations of liability specific to any 217 | jurisdiction. 218 | 219 | 4. Inability to Comply Due to Statute or Regulation 220 | --------------------------------------------------- 221 | 222 | If it is impossible for You to comply with any of the terms of this 223 | License with respect to some or all of the Covered Software due to 224 | statute, judicial order, or regulation then You must: (a) comply with 225 | the terms of this License to the maximum extent possible; and (b) 226 | describe the limitations and the code they affect. Such description must 227 | be placed in a text file included with all distributions of the Covered 228 | Software under this License. Except to the extent prohibited by statute 229 | or regulation, such description must be sufficiently detailed for a 230 | recipient of ordinary skill to be able to understand it. 231 | 232 | 5. Termination 233 | -------------- 234 | 235 | 5.1. The rights granted under this License will terminate automatically 236 | if You fail to comply with any of its terms. However, if You become 237 | compliant, then the rights granted under this License from a particular 238 | Contributor are reinstated (a) provisionally, unless and until such 239 | Contributor explicitly and finally terminates Your grants, and (b) on an 240 | ongoing basis, if such Contributor fails to notify You of the 241 | non-compliance by some reasonable means prior to 60 days after You have 242 | come back into compliance. Moreover, Your grants from a particular 243 | Contributor are reinstated on an ongoing basis if such Contributor 244 | notifies You of the non-compliance by some reasonable means, this is the 245 | first time You have received notice of non-compliance with this License 246 | from such Contributor, and You become compliant prior to 30 days after 247 | Your receipt of the notice. 248 | 249 | 5.2. If You initiate litigation against any entity by asserting a patent 250 | infringement claim (excluding declaratory judgment actions, 251 | counter-claims, and cross-claims) alleging that a Contributor Version 252 | directly or indirectly infringes any patent, then the rights granted to 253 | You by any and all Contributors for the Covered Software under Section 254 | 2.1 of this License shall terminate. 255 | 256 | 5.3. In the event of termination under Sections 5.1 or 5.2 above, all 257 | end user license agreements (excluding distributors and resellers) which 258 | have been validly granted by You or Your distributors under this License 259 | prior to termination shall survive termination. 260 | 261 | ************************************************************************ 262 | * * 263 | * 6. Disclaimer of Warranty * 264 | * ------------------------- * 265 | * * 266 | * Covered Software is provided under this License on an "as is" * 267 | * basis, without warranty of any kind, either expressed, implied, or * 268 | * statutory, including, without limitation, warranties that the * 269 | * Covered Software is free of defects, merchantable, fit for a * 270 | * particular purpose or non-infringing. The entire risk as to the * 271 | * quality and performance of the Covered Software is with You. * 272 | * Should any Covered Software prove defective in any respect, You * 273 | * (not any Contributor) assume the cost of any necessary servicing, * 274 | * repair, or correction. This disclaimer of warranty constitutes an * 275 | * essential part of this License. No use of any Covered Software is * 276 | * authorized under this License except under this disclaimer. * 277 | * * 278 | ************************************************************************ 279 | 280 | ************************************************************************ 281 | * * 282 | * 7. Limitation of Liability * 283 | * -------------------------- * 284 | * * 285 | * Under no circumstances and under no legal theory, whether tort * 286 | * (including negligence), contract, or otherwise, shall any * 287 | * Contributor, or anyone who distributes Covered Software as * 288 | * permitted above, be liable to You for any direct, indirect, * 289 | * special, incidental, or consequential damages of any character * 290 | * including, without limitation, damages for lost profits, loss of * 291 | * goodwill, work stoppage, computer failure or malfunction, or any * 292 | * and all other commercial damages or losses, even if such party * 293 | * shall have been informed of the possibility of such damages. This * 294 | * limitation of liability shall not apply to liability for death or * 295 | * personal injury resulting from such party's negligence to the * 296 | * extent applicable law prohibits such limitation. Some * 297 | * jurisdictions do not allow the exclusion or limitation of * 298 | * incidental or consequential damages, so this exclusion and * 299 | * limitation may not apply to You. * 300 | * * 301 | ************************************************************************ 302 | 303 | 8. Litigation 304 | ------------- 305 | 306 | Any litigation relating to this License may be brought only in the 307 | courts of a jurisdiction where the defendant maintains its principal 308 | place of business and such litigation shall be governed by laws of that 309 | jurisdiction, without reference to its conflict-of-law provisions. 310 | Nothing in this Section shall prevent a party's ability to bring 311 | cross-claims or counter-claims. 312 | 313 | 9. Miscellaneous 314 | ---------------- 315 | 316 | This License represents the complete agreement concerning the subject 317 | matter hereof. If any provision of this License is held to be 318 | unenforceable, such provision shall be reformed only to the extent 319 | necessary to make it enforceable. Any law or regulation which provides 320 | that the language of a contract shall be construed against the drafter 321 | shall not be used to construe this License against a Contributor. 322 | 323 | 10. Versions of the License 324 | --------------------------- 325 | 326 | 10.1. New Versions 327 | 328 | Mozilla Foundation is the license steward. Except as provided in Section 329 | 10.3, no one other than the license steward has the right to modify or 330 | publish new versions of this License. Each version will be given a 331 | distinguishing version number. 332 | 333 | 10.2. Effect of New Versions 334 | 335 | You may distribute the Covered Software under the terms of the version 336 | of the License under which You originally received the Covered Software, 337 | or under the terms of any subsequent version published by the license 338 | steward. 339 | 340 | 10.3. Modified Versions 341 | 342 | If you create software not governed by this License, and you want to 343 | create a new license for such software, you may create and use a 344 | modified version of this License if you rename the license and remove 345 | any references to the name of the license steward (except to note that 346 | such modified license differs from this License). 347 | 348 | 10.4. Distributing Source Code Form that is Incompatible With Secondary 349 | Licenses 350 | 351 | If You choose to distribute Source Code Form that is Incompatible With 352 | Secondary Licenses under the terms of this version of the License, the 353 | notice described in Exhibit B of this License must be attached. 354 | 355 | Exhibit A - Source Code Form License Notice 356 | ------------------------------------------- 357 | 358 | This Source Code Form is subject to the terms of the Mozilla Public 359 | License, v. 2.0. If a copy of the MPL was not distributed with this 360 | file, You can obtain one at http://mozilla.org/MPL/2.0/. 361 | 362 | If it is not possible or desirable to put the notice in a particular 363 | file, then You may include the notice in a location (such as a LICENSE 364 | file in a relevant directory) where a recipient would be likely to look 365 | for such a notice. 366 | 367 | You may add additional accurate notices of copyright ownership. 368 | 369 | Exhibit B - "Incompatible With Secondary Licenses" Notice 370 | --------------------------------------------------------- 371 | 372 | This Source Code Form is "Incompatible With Secondary Licenses", as 373 | defined by the Mozilla Public License, v. 2.0. 374 | -------------------------------------------------------------------------------- /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 | - Home-Manager Options 12 | 13 | ## Usage 14 | 15 | ```sh 16 | manix --help 17 | manix mergeattr 18 | manix --strict mergeattr 19 | manix --update-cache mergeattr 20 | ``` 21 | 22 | ### rnix-lsp 23 | 24 | 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. 25 | 26 | ![manix](/manix.png) 27 | 28 | ### fzf 29 | 30 | ```sh 31 | manix "" | grep '^# ' | sed 's/^# \(.*\) (.*/\1/;s/ (.*//;s/^# //' | fzf --preview="manix '{}'" | xargs manix 32 | ``` 33 | 34 | ## Installation 35 | 36 | ### Update 37 | 38 | 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`. 39 | 40 | ### Github Releases 41 | 42 | Since it can take some time to compile Manix, you can download statically-built executables from Github Releases. 43 | 44 | ```sh 45 | wget https://github.com/mlvzk/manix/releases/latest/download/manix 46 | chmod +x manix 47 | mv manix ~/bin/ # or some other location in your $PATH 48 | ``` 49 | 50 | ### nix-env 51 | 52 | ```sh 53 | # If you have the unstable channel on your system 54 | nix-env -f '' -iA manix 55 | # OR 56 | nix-env -i -f https://github.com/mlvzk/manix/archive/master.tar.gz 57 | ``` 58 | 59 | ### Nix with flakes enabled 60 | 61 | ``` sh 62 | $ nix run 'github:mlvzk/manix' mapAttrs 63 | ``` 64 | 65 | ## Kudos 66 | 67 | The inspiration for this project came from [nix-doc](https://github.com/lf-/nix-doc) 68 | -------------------------------------------------------------------------------- /default.nix: -------------------------------------------------------------------------------- 1 | { pkgs ? import {} }: 2 | let 3 | sources = import ./nix/sources.nix; 4 | naersk = pkgs.callPackage sources.naersk {}; 5 | in naersk.buildPackage ./. 6 | -------------------------------------------------------------------------------- /flake.lock: -------------------------------------------------------------------------------- 1 | { 2 | "nodes": { 3 | "flake-utils": { 4 | "locked": { 5 | "lastModified": 1598466101, 6 | "narHash": "sha256-JPhv+Ay98KMWRVRFlLEK9+eLpvNjhTBGWdFKZsE97ck=", 7 | "owner": "numtide", 8 | "repo": "flake-utils", 9 | "rev": "a586a6b966d59f53f45a04f8891fbc017dc09dbe", 10 | "type": "github" 11 | }, 12 | "original": { 13 | "owner": "numtide", 14 | "repo": "flake-utils", 15 | "type": "github" 16 | } 17 | }, 18 | "nixpkgs": { 19 | "locked": { 20 | "lastModified": 1599050737, 21 | "narHash": "sha256-jxUhBQ49DYMnL17SrPNGTEp+Wb5Pj89CxW8OLwnXl1g=", 22 | "owner": "NixOS", 23 | "repo": "nixpkgs", 24 | "rev": "18348c7829ae93ebe436497ca7ad96cdb8d39935", 25 | "type": "github" 26 | }, 27 | "original": { 28 | "owner": "NixOS", 29 | "repo": "nixpkgs", 30 | "type": "github" 31 | } 32 | }, 33 | "root": { 34 | "inputs": { 35 | "flake-utils": "flake-utils", 36 | "nixpkgs": "nixpkgs" 37 | } 38 | } 39 | }, 40 | "root": "root", 41 | "version": 7 42 | } 43 | -------------------------------------------------------------------------------- /flake.nix: -------------------------------------------------------------------------------- 1 | { 2 | description = "A fast CLI documentation searcher for Nix."; 3 | 4 | inputs.nixpkgs.url = "github:NixOS/nixpkgs"; 5 | inputs.flake-utils.url = "github:numtide/flake-utils"; 6 | 7 | outputs = { self, nixpkgs, flake-utils }: 8 | flake-utils.lib.eachSystem flake-utils.lib.allSystems (system: { 9 | packages.manix = nixpkgs.legacyPackages.${system}.callPackage ./. {}; 10 | defaultPackage = self.packages.${system}.manix; 11 | }); 12 | } 13 | -------------------------------------------------------------------------------- /manix.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mlvzk/manix/d08e7ca185445b929f097f8bfb1243a8ef3e10e4/manix.png -------------------------------------------------------------------------------- /nix/sources.json: -------------------------------------------------------------------------------- 1 | { 2 | "naersk": { 3 | "branch": "master", 4 | "description": "Build rust crates in Nix. No configuration, no code generation, no IFD. Sandbox friendly.", 5 | "homepage": "", 6 | "owner": "nmattia", 7 | "repo": "naersk", 8 | "rev": "529e910a3f423a8211f8739290014b754b2555b6", 9 | "sha256": "0bcy9nmyaan5jvp0wg80wkizc9j166ns685rdr1kbhkvdpywv46y", 10 | "type": "tarball", 11 | "url": "https://github.com/nmattia/naersk/archive/529e910a3f423a8211f8739290014b754b2555b6.tar.gz", 12 | "url_template": "https://github.com///archive/.tar.gz" 13 | }, 14 | "niv": { 15 | "branch": "master", 16 | "description": "Easy dependency management for Nix projects", 17 | "homepage": "https://github.com/nmattia/niv", 18 | "owner": "nmattia", 19 | "repo": "niv", 20 | "rev": "e82eb322ea32a747a51c431d7787221bcc6d9038", 21 | "sha256": "1fy4dcr05d80diwlxmh42xnjm5ki1pkbky38smvlqjaky2y2f71f", 22 | "type": "tarball", 23 | "url": "https://github.com/nmattia/niv/archive/e82eb322ea32a747a51c431d7787221bcc6d9038.tar.gz", 24 | "url_template": "https://github.com///archive/.tar.gz" 25 | }, 26 | "nixpkgs": { 27 | "branch": "nixos-19.09", 28 | "description": "A read-only mirror of NixOS/nixpkgs tracking the released channels. Send issues and PRs to", 29 | "homepage": "https://github.com/NixOS/nixpkgs", 30 | "owner": "NixOS", 31 | "repo": "nixpkgs-channels", 32 | "rev": "289466dd6a11c65a7de4a954d6ebf66c1ad07652", 33 | "sha256": "0r5ja052s86fr54fm1zlhld3fwawz2w1d1gd6vbvpjrpjfyajibn", 34 | "type": "tarball", 35 | "url": "https://github.com/NixOS/nixpkgs-channels/archive/289466dd6a11c65a7de4a954d6ebf66c1ad07652.tar.gz", 36 | "url_template": "https://github.com///archive/.tar.gz" 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /nix/sources.nix: -------------------------------------------------------------------------------- 1 | # This file has been generated by Niv. 2 | 3 | let 4 | 5 | # 6 | # The fetchers. fetch_ fetches specs of type . 7 | # 8 | 9 | fetch_file = pkgs: spec: 10 | if spec.builtin or true then 11 | builtins_fetchurl { inherit (spec) url sha256; } 12 | else 13 | pkgs.fetchurl { inherit (spec) url sha256; }; 14 | 15 | fetch_tarball = pkgs: spec: 16 | if spec.builtin or true then 17 | builtins_fetchTarball { inherit (spec) url sha256; } 18 | else 19 | pkgs.fetchzip { inherit (spec) url sha256; }; 20 | 21 | fetch_git = spec: 22 | builtins.fetchGit { url = spec.repo; inherit (spec) rev ref; }; 23 | 24 | fetch_builtin-tarball = spec: 25 | builtins.trace 26 | '' 27 | WARNING: 28 | The niv type "builtin-tarball" will soon be deprecated. You should 29 | instead use `builtin = true`. 30 | 31 | $ niv modify -a type=tarball -a builtin=true 32 | '' 33 | builtins_fetchTarball { inherit (spec) url sha256; }; 34 | 35 | fetch_builtin-url = spec: 36 | builtins.trace 37 | '' 38 | WARNING: 39 | The niv type "builtin-url" will soon be deprecated. You should 40 | instead use `builtin = true`. 41 | 42 | $ niv modify -a type=file -a builtin=true 43 | '' 44 | (builtins_fetchurl { inherit (spec) url sha256; }); 45 | 46 | # 47 | # Various helpers 48 | # 49 | 50 | # The set of packages used when specs are fetched using non-builtins. 51 | mkPkgs = sources: 52 | let 53 | sourcesNixpkgs = 54 | import (builtins_fetchTarball { inherit (sources.nixpkgs) url sha256; }) {}; 55 | hasNixpkgsPath = builtins.any (x: x.prefix == "nixpkgs") builtins.nixPath; 56 | hasThisAsNixpkgsPath = == ./.; 57 | in 58 | if builtins.hasAttr "nixpkgs" sources 59 | then sourcesNixpkgs 60 | else if hasNixpkgsPath && ! hasThisAsNixpkgsPath then 61 | import {} 62 | else 63 | abort 64 | '' 65 | Please specify either (through -I or NIX_PATH=nixpkgs=...) or 66 | add a package called "nixpkgs" to your sources.json. 67 | ''; 68 | 69 | # The actual fetching function. 70 | fetch = pkgs: name: spec: 71 | 72 | if ! builtins.hasAttr "type" spec then 73 | abort "ERROR: niv spec ${name} does not have a 'type' attribute" 74 | else if spec.type == "file" then fetch_file pkgs spec 75 | else if spec.type == "tarball" then fetch_tarball pkgs spec 76 | else if spec.type == "git" then fetch_git spec 77 | else if spec.type == "builtin-tarball" then fetch_builtin-tarball spec 78 | else if spec.type == "builtin-url" then fetch_builtin-url spec 79 | else 80 | abort "ERROR: niv spec ${name} has unknown type ${builtins.toJSON spec.type}"; 81 | 82 | # Ports of functions for older nix versions 83 | 84 | # a Nix version of mapAttrs if the built-in doesn't exist 85 | mapAttrs = builtins.mapAttrs or ( 86 | f: set: with builtins; 87 | listToAttrs (map (attr: { name = attr; value = f attr set.${attr}; }) (attrNames set)) 88 | ); 89 | 90 | # fetchTarball version that is compatible between all the versions of Nix 91 | builtins_fetchTarball = { url, sha256 }@attrs: 92 | let 93 | inherit (builtins) lessThan nixVersion fetchTarball; 94 | in 95 | if lessThan nixVersion "1.12" then 96 | fetchTarball { inherit url; } 97 | else 98 | fetchTarball attrs; 99 | 100 | # fetchurl version that is compatible between all the versions of Nix 101 | builtins_fetchurl = { url, sha256 }@attrs: 102 | let 103 | inherit (builtins) lessThan nixVersion fetchurl; 104 | in 105 | if lessThan nixVersion "1.12" then 106 | fetchurl { inherit url; } 107 | else 108 | fetchurl attrs; 109 | 110 | # Create the final "sources" from the config 111 | mkSources = config: 112 | mapAttrs ( 113 | name: spec: 114 | if builtins.hasAttr "outPath" spec 115 | then abort 116 | "The values in sources.json should not have an 'outPath' attribute" 117 | else 118 | spec // { outPath = fetch config.pkgs name spec; } 119 | ) config.sources; 120 | 121 | # The "config" used by the fetchers 122 | mkConfig = 123 | { sourcesFile ? ./sources.json 124 | , sources ? builtins.fromJSON (builtins.readFile sourcesFile) 125 | , pkgs ? mkPkgs sources 126 | }: rec { 127 | # The sources, i.e. the attribute set of spec name to spec 128 | inherit sources; 129 | 130 | # The "pkgs" (evaluated nixpkgs) to use for e.g. non-builtin fetchers 131 | inherit pkgs; 132 | }; 133 | in 134 | mkSources (mkConfig {}) // { __functor = _: settings: mkSources (mkConfig settings); } 135 | -------------------------------------------------------------------------------- /shell.nix: -------------------------------------------------------------------------------- 1 | { pkgs ? import {} 2 | }: let 3 | unstable = import {}; 4 | in pkgs.mkShell { 5 | buildInputs = [ unstable.rust-analyzer unstable.cargo-flamegraph ]; 6 | } 7 | -------------------------------------------------------------------------------- /src/bin/manix.rs: -------------------------------------------------------------------------------- 1 | use anyhow::{Context, Result}; 2 | use colored::*; 3 | use comments_docsource::CommentsDatabase; 4 | use lazy_static::lazy_static; 5 | use manix::*; 6 | use options_docsource::{OptionsDatabase, OptionsDatabaseType}; 7 | use std::path::PathBuf; 8 | use structopt::clap::arg_enum; 9 | use structopt::StructOpt; 10 | 11 | arg_enum! { 12 | #[derive(Debug, PartialEq)] 13 | #[allow(non_camel_case_types)] 14 | enum Source { 15 | nixos_options, 16 | hm_options, 17 | nixpkgs_doc, 18 | nixpkgs_tree, 19 | nixpkgs_comments, 20 | } 21 | } 22 | 23 | lazy_static! { 24 | static ref SOURCE_VARIANTS: String = Source::variants().join(","); 25 | } 26 | 27 | #[derive(StructOpt)] 28 | #[structopt(name = "manix")] 29 | struct Opt { 30 | /// Force update cache 31 | #[structopt(short, long)] 32 | update_cache: bool, 33 | /// Matches entries stricly 34 | #[structopt(short, long)] 35 | strict: bool, 36 | /// Restrict search to chosen sources 37 | #[structopt(long, possible_values = &Source::variants(), default_value = &SOURCE_VARIANTS, use_delimiter = true)] 38 | source: Vec, 39 | #[structopt(name = "QUERY")] 40 | query: String, 41 | } 42 | 43 | fn build_source_and_add( 44 | mut source: T, 45 | name: &str, 46 | path: &PathBuf, 47 | aggregate: Option<&mut AggregateDocSource>, 48 | ) -> Option<()> 49 | where 50 | T: 'static + DocSource + Cache + Sync, 51 | { 52 | eprintln!("Building {} cache...", name); 53 | if let Err(e) = source 54 | .update() 55 | .with_context(|| anyhow::anyhow!("Failed to update {}", name)) 56 | { 57 | eprintln!("{:?}", e); 58 | return None; 59 | } 60 | 61 | if let Err(e) = source 62 | .save(&path) 63 | .with_context(|| format!("Failed to save {} cache", name)) 64 | { 65 | eprintln!("{:?}", e); 66 | return None; 67 | } 68 | 69 | if let Some(aggregate) = aggregate { 70 | aggregate.add_source(Box::new(source)); 71 | } 72 | Some(()) 73 | } 74 | 75 | fn load_source_and_add( 76 | load_result: Result, std::io::Error>, 77 | name: &str, 78 | aggregate: &mut AggregateDocSource, 79 | ignore_file_io_error: bool, 80 | ) -> Option<()> 81 | where 82 | T: 'static + DocSource + Cache + Sync, 83 | { 84 | let load_result = match load_result { 85 | Err(e) => { 86 | if !ignore_file_io_error { 87 | eprintln!("Failed to load {} cache file: {:?}", name, e); 88 | } 89 | return None; 90 | } 91 | Ok(r) => r, 92 | }; 93 | 94 | match load_result.with_context(|| anyhow::anyhow!("Failed to load {}", name)) { 95 | Err(e) => { 96 | eprintln!("{:?}", e); 97 | None 98 | } 99 | Ok(source) => { 100 | aggregate.add_source(Box::new(source)); 101 | Some(()) 102 | } 103 | } 104 | } 105 | 106 | fn main() -> Result<()> { 107 | let opt: Opt = Opt::from_args(); 108 | 109 | let cache_dir = 110 | xdg::BaseDirectories::with_prefix("manix").context("Failed to get a cache directory")?; 111 | 112 | let last_version_path = cache_dir 113 | .place_cache_file("last_version.txt") 114 | .context("Failed to place last version file")?; 115 | 116 | let comment_cache_path = cache_dir 117 | .place_cache_file("comments.bin") 118 | .context("Failed to place cache file")?; 119 | let nixpkgs_tree_cache_path = cache_dir 120 | .place_cache_file("nixpkgs_tree.bin") 121 | .context("Failed to place nixpkgs tree cache file")?; 122 | let options_hm_cache_path = cache_dir 123 | .place_cache_file("options_hm_database.bin") 124 | .context("Failed to place home-manager options cache file")?; 125 | let options_nixos_cache_path = cache_dir 126 | .place_cache_file("options_nixos_database.bin") 127 | .context("Failed to place NixOS options cache file")?; 128 | let nixpkgs_doc_cache_path = cache_dir 129 | .place_cache_file("nixpkgs_doc_database.bin") 130 | .context("Failed to place Nixpkgs Documentation cache file")?; 131 | 132 | let version = std::env!("CARGO_PKG_VERSION"); 133 | let last_version = std::fs::read(&last_version_path) 134 | .map(|c| String::from_utf8(c)) 135 | .unwrap_or(Ok(version.to_string()))?; 136 | 137 | let should_invalidate_cache = version != last_version; 138 | 139 | let mut aggregate_source = AggregateDocSource::default(); 140 | 141 | let mut comment_db = if !should_invalidate_cache && comment_cache_path.exists() { 142 | CommentsDatabase::load(&std::fs::read(&comment_cache_path)?) 143 | .map_err(|e| anyhow::anyhow!("Failed to load Nixpkgs comments database: {:?}", e))? 144 | } else { 145 | CommentsDatabase::new() 146 | }; 147 | if comment_db.hash_to_defs.len() == 0 { 148 | eprintln!("Building Nixpkgs comments cache..."); 149 | } 150 | let cache_invalid = comment_db 151 | .update() 152 | .map_err(|e| anyhow::anyhow!(e)) 153 | .context("Failed to update cache")?; 154 | comment_db.save(&comment_cache_path)?; 155 | if opt.source.contains(&Source::nixpkgs_comments) { 156 | aggregate_source.add_source(Box::new(comment_db)); 157 | } 158 | 159 | if should_invalidate_cache || opt.update_cache || cache_invalid { 160 | if let None = build_source_and_add( 161 | OptionsDatabase::new(OptionsDatabaseType::HomeManager), 162 | "Home Manager Options", 163 | &options_hm_cache_path, 164 | if opt.source.contains(&Source::hm_options) { 165 | Some(&mut aggregate_source) 166 | } else { 167 | None 168 | }, 169 | ) { 170 | 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()); 171 | } 172 | 173 | build_source_and_add( 174 | OptionsDatabase::new(OptionsDatabaseType::NixOS), 175 | "NixOS Options", 176 | &options_nixos_cache_path, 177 | if opt.source.contains(&Source::nixos_options) { 178 | Some(&mut aggregate_source) 179 | } else { 180 | None 181 | }, 182 | ); 183 | 184 | build_source_and_add( 185 | nixpkgs_tree_docsource::NixpkgsTreeDatabase::new(), 186 | "Nixpkgs Tree", 187 | &nixpkgs_tree_cache_path, 188 | if opt.source.contains(&Source::nixpkgs_tree) { 189 | Some(&mut aggregate_source) 190 | } else { 191 | None 192 | }, 193 | ); 194 | 195 | build_source_and_add( 196 | xml_docsource::XmlFuncDocDatabase::new(), 197 | "Nixpkgs Documentation", 198 | &nixpkgs_doc_cache_path, 199 | if opt.source.contains(&Source::nixpkgs_doc) { 200 | Some(&mut aggregate_source) 201 | } else { 202 | None 203 | }, 204 | ); 205 | 206 | std::fs::write(&last_version_path, version)?; 207 | } else { 208 | if opt.source.contains(&Source::hm_options) { 209 | load_source_and_add( 210 | std::fs::read(&options_hm_cache_path).map(|c| OptionsDatabase::load(&c)), 211 | "Home Manager Options", 212 | &mut aggregate_source, 213 | true, 214 | ); 215 | } 216 | 217 | if opt.source.contains(&Source::nixos_options) { 218 | load_source_and_add( 219 | std::fs::read(&options_nixos_cache_path).map(|c| OptionsDatabase::load(&c)), 220 | "NixOS Options", 221 | &mut aggregate_source, 222 | false, 223 | ); 224 | } 225 | 226 | if opt.source.contains(&Source::nixpkgs_tree) { 227 | load_source_and_add( 228 | std::fs::read(&nixpkgs_tree_cache_path) 229 | .map(|c| nixpkgs_tree_docsource::NixpkgsTreeDatabase::load(&c)), 230 | "Nixpkgs Tree", 231 | &mut aggregate_source, 232 | false, 233 | ); 234 | } 235 | 236 | if opt.source.contains(&Source::nixpkgs_doc) { 237 | load_source_and_add( 238 | std::fs::read(&nixpkgs_doc_cache_path) 239 | .map(|c| xml_docsource::XmlFuncDocDatabase::load(&c)), 240 | "Nixpkgs Documentation", 241 | &mut aggregate_source, 242 | false, 243 | ); 244 | } 245 | } 246 | 247 | let query_lower = opt.query.to_ascii_lowercase(); 248 | let query = manix::Lowercase(query_lower.as_bytes()); 249 | let entries = if opt.strict { 250 | aggregate_source.search(&query) 251 | } else { 252 | aggregate_source.search_liberal(&query) 253 | }; 254 | let (entries, key_only_entries): (Vec, Vec) = 255 | entries.into_iter().partition(|e| { 256 | if let DocEntry::NixpkgsTreeDoc(_) = e { 257 | false 258 | } else { 259 | true 260 | } 261 | }); 262 | 263 | if !key_only_entries.is_empty() { 264 | const SHOW_MAX_LEN: usize = 50; 265 | print!("{}", "Here's what I found in nixpkgs:".bold()); 266 | for entry in key_only_entries.iter().take(SHOW_MAX_LEN) { 267 | print!(" {}", entry.name().white()); 268 | } 269 | if key_only_entries.len() > SHOW_MAX_LEN { 270 | print!(" and {} more.", key_only_entries.len() - SHOW_MAX_LEN); 271 | } 272 | println!("\n"); 273 | } 274 | 275 | for entry in entries { 276 | const LINE: &str = "────────────────────"; 277 | println!( 278 | "{}\n{}\n{}", 279 | entry.source().white(), 280 | LINE.green(), 281 | entry.pretty_printed() 282 | ); 283 | } 284 | 285 | Ok(()) 286 | } 287 | -------------------------------------------------------------------------------- /src/comments_docsource.rs: -------------------------------------------------------------------------------- 1 | use crate::{ 2 | contains_insensitive_ascii, starts_with_insensitive_ascii, Cache, DocEntry, DocSource, Errors, 3 | Lowercase, 4 | }; 5 | use colored::*; 6 | use lazy_static::lazy_static; 7 | use rayon::prelude::*; 8 | use rnix::{ 9 | types::{AttrSet, EntryHolder, Ident, KeyValue, Lambda, TypedNode}, 10 | NodeOrToken, SyntaxKind, SyntaxNode, WalkEvent, 11 | }; 12 | use serde::{Deserialize, Serialize}; 13 | use std::collections::HashMap; 14 | use std::{path::PathBuf, process::Command}; 15 | 16 | lazy_static! { 17 | static ref NIXPKGS_PATH: PathBuf = get_nixpkgs_root(); 18 | } 19 | 20 | fn find_comments(node: SyntaxNode) -> Option> { 21 | let mut node = NodeOrToken::Node(node); 22 | let mut comments = Vec::::new(); 23 | 24 | loop { 25 | loop { 26 | if let Some(new) = node.prev_sibling_or_token() { 27 | node = new; 28 | break; 29 | } else { 30 | node = NodeOrToken::Node(node.parent()?); 31 | } 32 | } 33 | 34 | match node.kind() { 35 | SyntaxKind::TOKEN_COMMENT => match &node { 36 | NodeOrToken::Token(token) => comments.push(token.text().clone().into()), 37 | NodeOrToken::Node(_) => unreachable!(), 38 | }, 39 | // This stuff is found as part of `the-fn = f: ...` 40 | // here: ^^^^^^^^ 41 | SyntaxKind::NODE_KEY | SyntaxKind::TOKEN_ASSIGN => (), 42 | t if t.is_trivia() => (), 43 | _ => break, 44 | } 45 | } 46 | 47 | // reverse the order because the function reads bottom-up 48 | comments.reverse(); 49 | Some(comments) 50 | } 51 | 52 | fn visit_attr_entry(entry: KeyValue) -> Option { 53 | let ident = Ident::cast(entry.key()?.path().nth(0)?)?.node().text(); 54 | let lambda = Lambda::cast(entry.value()?)?; 55 | 56 | let comments = find_comments(lambda.node().clone()).unwrap_or_default(); 57 | 58 | Some(CommentDocumentation::new(ident.to_string(), comments)) 59 | } 60 | 61 | fn visit_attrset(set: &AttrSet) -> Vec { 62 | set.entries() 63 | .flat_map(|e| visit_attr_entry(e).into_iter()) 64 | .collect() 65 | } 66 | 67 | fn walk_ast(ast: rnix::AST) -> Vec { 68 | let mut res = Vec::::new(); 69 | for ev in ast.node().preorder_with_tokens() { 70 | match ev { 71 | WalkEvent::Enter(enter) => { 72 | if let Some(set) = enter.into_node().and_then(AttrSet::cast) { 73 | res.append(&mut visit_attrset(&set)); 74 | } 75 | } 76 | WalkEvent::Leave(_) => {} 77 | } 78 | } 79 | 80 | res 81 | } 82 | 83 | #[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)] 84 | pub struct CommentDocumentation { 85 | pub key: String, 86 | pub path: Option, 87 | pub comments: Vec, 88 | } 89 | 90 | impl CommentDocumentation { 91 | pub fn new(key: String, comments: Vec) -> Self { 92 | Self { 93 | key, 94 | comments, 95 | path: None, 96 | } 97 | } 98 | pub fn with_path(self, path: PathBuf) -> Self { 99 | CommentDocumentation { 100 | path: Some(path), 101 | ..self 102 | } 103 | } 104 | } 105 | 106 | pub fn cleanup_comment(s: &str) -> &str { 107 | s.trim_start_matches("#") 108 | .trim_start_matches("/*") 109 | .trim_end_matches("*/") 110 | } 111 | 112 | impl CommentDocumentation { 113 | pub fn pretty_printed(&self) -> String { 114 | let heading = self.key.blue().bold(); 115 | let path = self 116 | .path 117 | .as_ref() 118 | .map(|path| { 119 | path.strip_prefix(NIXPKGS_PATH.to_owned()) 120 | .unwrap_or(path) 121 | .display() 122 | .to_string() 123 | }) 124 | .unwrap_or_default() 125 | .white(); 126 | 127 | let comment = self 128 | .comments 129 | .iter() 130 | .map(|c: &String| cleanup_comment(c)) 131 | .collect::>() 132 | .join("\n"); 133 | 134 | format!("# {} ({})\n{}\n\n", heading, path, comment) 135 | } 136 | pub fn name(&self) -> String { 137 | self.key.to_owned() 138 | } 139 | } 140 | 141 | #[derive(Debug, Serialize, Deserialize)] 142 | pub struct CommentsDatabase { 143 | pub hash_to_defs: HashMap>, 144 | } 145 | 146 | impl DocSource for CommentsDatabase { 147 | fn all_keys(&self) -> Vec<&str> { 148 | self.hash_to_defs 149 | .values() 150 | .flatten() 151 | .map(|def| def.key.as_ref()) 152 | .collect() 153 | } 154 | fn search(&self, query: &Lowercase) -> Vec { 155 | self.hash_to_defs 156 | .values() 157 | .flatten() 158 | .filter(|d| { 159 | d.comments.len() > 0 && starts_with_insensitive_ascii(d.key.as_bytes(), query) 160 | }) 161 | .cloned() 162 | .map(DocEntry::CommentDoc) 163 | .collect() 164 | } 165 | fn search_liberal(&self, query: &Lowercase) -> Vec { 166 | self.hash_to_defs 167 | .values() 168 | .flatten() 169 | .filter(|d| d.comments.len() > 0 && contains_insensitive_ascii(d.key.as_bytes(), query)) 170 | .cloned() 171 | .map(DocEntry::CommentDoc) 172 | .collect() 173 | } 174 | fn update(&mut self) -> Result { 175 | let files = find_nix_files(get_nixpkgs_root()) 176 | .par_iter() 177 | .map(|f| { 178 | let content = std::fs::read_to_string(f.path()).unwrap(); 179 | let mut hasher = crc32fast::Hasher::new(); 180 | hasher.update(content.as_bytes()); 181 | let hash = hasher.finalize(); 182 | (hash, f.path().to_path_buf(), content) 183 | }) 184 | .collect::>(); 185 | 186 | let new_defs = files 187 | .par_iter() 188 | .filter(|(hash, _, _)| !self.is_in_cache(hash)) 189 | .map(|(hash, path, content)| { 190 | let ast = rnix::parse(&content); 191 | let definitions = walk_ast(ast) 192 | .into_iter() 193 | .map(|def| def.with_path(path.clone())) 194 | .collect(); 195 | (hash, definitions) 196 | }) 197 | .collect::)>>(); 198 | if new_defs.is_empty() { 199 | return Ok(false); 200 | } 201 | 202 | for (hash, defs) in new_defs { 203 | self.add_to_cache(*hash, defs); 204 | } 205 | 206 | Ok(true) 207 | } 208 | } 209 | impl Cache for CommentsDatabase {} 210 | 211 | impl CommentsDatabase { 212 | pub fn new() -> Self { 213 | Self { 214 | hash_to_defs: HashMap::new(), 215 | } 216 | } 217 | 218 | fn is_in_cache(&self, hash: &u32) -> bool { 219 | self.hash_to_defs.contains_key(hash) 220 | } 221 | 222 | fn add_to_cache( 223 | &mut self, 224 | hash: u32, 225 | defs: Vec, 226 | ) -> Option> { 227 | self.hash_to_defs.insert(hash, defs) 228 | } 229 | } 230 | 231 | fn find_nix_files(path: PathBuf) -> Vec { 232 | walkdir::WalkDir::new(&path) 233 | .into_iter() 234 | .filter_map(Result::ok) 235 | .filter(|e| !e.file_type().is_dir()) 236 | .filter(|e| e.path().extension().and_then(|s| s.to_str()) == Some("nix")) 237 | .collect::>() 238 | } 239 | 240 | fn get_nixpkgs_root() -> PathBuf { 241 | let channel_path = Command::new("nix-instantiate") 242 | .arg("--eval") 243 | .arg("--strict") 244 | .arg("-E") 245 | .arg("") 246 | .output() 247 | .map(|o| String::from_utf8(o.stdout)); 248 | 249 | if let Ok(Ok(path)) = channel_path { 250 | PathBuf::from(path.trim_end()) 251 | } else { 252 | PathBuf::from(".") 253 | } 254 | } 255 | -------------------------------------------------------------------------------- /src/lib.rs: -------------------------------------------------------------------------------- 1 | use comments_docsource::CommentDocumentation; 2 | use options_docsource::{OptionDocumentation, OptionsDatabaseType}; 3 | use rayon::iter::{IntoParallelRefIterator, ParallelIterator}; 4 | use std::path::PathBuf; 5 | use thiserror::Error; 6 | use xml_docsource::XmlFuncDocumentation; 7 | 8 | pub mod comments_docsource; 9 | pub mod nixpkgs_tree_docsource; 10 | pub mod options_docsource; 11 | pub mod xml_docsource; 12 | 13 | pub trait Cache 14 | where 15 | Self: Sized + DocSource + serde::Serialize, 16 | { 17 | /// Deserializes content to Self 18 | fn load<'a>(content: &'a [u8]) -> Result 19 | where 20 | Self: serde::Deserialize<'a>, 21 | { 22 | Ok(bincode::deserialize(content)?) 23 | } 24 | /// Saves self to a file, serialized with bincode 25 | fn save(&self, filename: &PathBuf) -> Result<(), Errors> { 26 | let x = bincode::serialize(self)?; 27 | std::fs::write(filename, x)?; 28 | Ok(()) 29 | } 30 | } 31 | 32 | #[derive(Error, Debug)] 33 | pub enum Errors { 34 | #[error("IO Error for file {}: {}", .filename, .err)] 35 | FileIo { 36 | filename: String, 37 | err: std::io::Error, 38 | }, 39 | #[error("Failed to perform IO on a cache file")] 40 | CacheFileIo(#[from] std::io::Error), 41 | #[error("Failed to serialize/deserialize cache(bincode)")] 42 | Bincode(#[from] bincode::Error), 43 | #[error("Failed to serialize/deserialize cache(serde_json)")] 44 | SerdeJson(#[from] serde_json::Error), 45 | #[error("XML parsing error for file {}: {}", .filename, .err)] 46 | XmlParse { 47 | filename: String, 48 | err: roxmltree::Error, 49 | }, 50 | } 51 | 52 | #[derive(Debug, PartialEq, Eq)] 53 | pub enum DocEntry { 54 | OptionDoc(OptionsDatabaseType, OptionDocumentation), 55 | CommentDoc(CommentDocumentation), 56 | XmlFuncDoc(XmlFuncDocumentation), 57 | NixpkgsTreeDoc(String), 58 | } 59 | 60 | impl DocEntry { 61 | pub fn name(&self) -> String { 62 | match self { 63 | DocEntry::OptionDoc(_, x) => x.name(), 64 | DocEntry::CommentDoc(x) => x.name(), 65 | DocEntry::XmlFuncDoc(x) => x.name(), 66 | DocEntry::NixpkgsTreeDoc(x) => x.clone(), 67 | } 68 | } 69 | pub fn pretty_printed(&self) -> String { 70 | match self { 71 | DocEntry::OptionDoc(_, x) => x.pretty_printed(), 72 | DocEntry::CommentDoc(x) => x.pretty_printed(), 73 | DocEntry::XmlFuncDoc(x) => x.pretty_printed(), 74 | DocEntry::NixpkgsTreeDoc(x) => x.clone(), 75 | } 76 | } 77 | pub fn source(&self) -> &str { 78 | match self { 79 | DocEntry::OptionDoc(typ, _) => match typ { 80 | OptionsDatabaseType::NixOS => "NixOS Options", 81 | OptionsDatabaseType::HomeManager => "HomeManager Options", 82 | }, 83 | DocEntry::CommentDoc(_) => "Nixpkgs Comments", 84 | DocEntry::XmlFuncDoc(_) => "Nixpkgs Documentation", 85 | DocEntry::NixpkgsTreeDoc(_) => "Nixpkgs Tree", 86 | } 87 | } 88 | } 89 | 90 | pub trait DocSource { 91 | fn all_keys(&self) -> Vec<&str>; 92 | fn search(&self, query: &Lowercase) -> Vec; 93 | fn search_liberal(&self, query: &Lowercase) -> Vec; 94 | 95 | /// Updates the cache, returns true if anything changed 96 | fn update(&mut self) -> Result; 97 | } 98 | 99 | #[derive(Default)] 100 | pub struct AggregateDocSource { 101 | sources: Vec>, 102 | } 103 | 104 | impl AggregateDocSource { 105 | pub fn add_source(&mut self, source: Box) { 106 | self.sources.push(source) 107 | } 108 | } 109 | 110 | impl DocSource for AggregateDocSource { 111 | fn all_keys(&self) -> Vec<&str> { 112 | self.sources 113 | .par_iter() 114 | .flat_map(|source| source.all_keys()) 115 | .collect() 116 | } 117 | fn search(&self, query: &Lowercase) -> Vec { 118 | self.sources 119 | .par_iter() 120 | .flat_map(|source| source.search(query)) 121 | .collect() 122 | } 123 | fn search_liberal(&self, query: &Lowercase) -> Vec { 124 | self.sources 125 | .par_iter() 126 | .flat_map(|source| source.search_liberal(query)) 127 | .collect() 128 | } 129 | fn update(&mut self) -> Result { 130 | unimplemented!(); 131 | } 132 | } 133 | 134 | pub struct Lowercase<'a>(pub &'a [u8]); 135 | 136 | pub(crate) fn starts_with_insensitive_ascii(s: &[u8], prefix: &Lowercase) -> bool { 137 | let prefix = prefix.0; 138 | 139 | if s.len() < prefix.len() { 140 | return false; 141 | } 142 | 143 | for (i, b) in prefix.into_iter().enumerate() { 144 | // this is safe because of the earlier if check 145 | if unsafe { s.get_unchecked(i) }.to_ascii_lowercase() != *b { 146 | return false; 147 | } 148 | } 149 | 150 | true 151 | } 152 | 153 | pub(crate) fn contains_insensitive_ascii(s: &[u8], inner: &Lowercase) -> bool { 154 | let inner = inner.0; 155 | 156 | if s.len() < inner.len() { 157 | return false; 158 | } 159 | 160 | 'outer: for i in 0..(s.len() - inner.len() + 1) { 161 | let target = &s[i..i + inner.len()]; 162 | for (y, b) in target.into_iter().enumerate() { 163 | if *unsafe { inner.get_unchecked(y) } != b.to_ascii_lowercase() { 164 | continue 'outer; 165 | } 166 | } 167 | return true; 168 | } 169 | 170 | false 171 | } 172 | 173 | #[test] 174 | fn test_starts_with_insensitive_ascii() { 175 | assert_eq!( 176 | starts_with_insensitive_ascii("This is a string".as_bytes(), &Lowercase(b"this ")), 177 | true, 178 | ); 179 | assert_eq!( 180 | starts_with_insensitive_ascii("abc".as_bytes(), &Lowercase(b"abc")), 181 | true, 182 | ); 183 | assert_eq!( 184 | starts_with_insensitive_ascii("This is a string".as_bytes(), &Lowercase(b"x")), 185 | false, 186 | ); 187 | assert_eq!( 188 | starts_with_insensitive_ascii("thi".as_bytes(), &Lowercase(b"this ")), 189 | false, 190 | ); 191 | } 192 | 193 | #[test] 194 | fn test_contains_insensitive_ascii() { 195 | assert_eq!( 196 | contains_insensitive_ascii("abc".as_bytes(), &Lowercase(b"b")), 197 | true 198 | ); 199 | assert_eq!( 200 | contains_insensitive_ascii("abc".as_bytes(), &Lowercase(b"abc")), 201 | true 202 | ); 203 | assert_eq!( 204 | contains_insensitive_ascii("xabcx".as_bytes(), &Lowercase(b"abc")), 205 | true 206 | ); 207 | assert_eq!( 208 | contains_insensitive_ascii("abc".as_bytes(), &Lowercase(b"x")), 209 | false 210 | ); 211 | assert_eq!( 212 | contains_insensitive_ascii("abc".as_bytes(), &Lowercase(b"abcd")), 213 | false 214 | ); 215 | } 216 | -------------------------------------------------------------------------------- /src/nixpkgs_tree_docsource.rs: -------------------------------------------------------------------------------- 1 | use crate::{ 2 | contains_insensitive_ascii, starts_with_insensitive_ascii, Cache, DocEntry, DocSource, Errors, 3 | Lowercase, 4 | }; 5 | use serde::{Deserialize, Serialize}; 6 | use std::{collections::HashMap, process::Command}; 7 | 8 | #[derive(Debug, Serialize, Deserialize)] 9 | pub struct NixpkgsTreeDatabase { 10 | keys: Vec, 11 | } 12 | 13 | impl NixpkgsTreeDatabase { 14 | pub fn new() -> Self { 15 | Self { keys: Vec::new() } 16 | } 17 | } 18 | 19 | #[derive(Serialize, Deserialize)] 20 | struct Keys(HashMap); 21 | 22 | impl Into> for Keys { 23 | fn into(self) -> Vec { 24 | let mut res = Vec::::new(); 25 | for (mut name, keys) in self.0 { 26 | res.push(name.clone()); 27 | name.push('.'); 28 | for key in Into::>::into(keys) { 29 | let mut name = name.clone(); 30 | name.push_str(&key); 31 | res.push(name); 32 | } 33 | } 34 | res 35 | } 36 | } 37 | 38 | impl DocSource for NixpkgsTreeDatabase { 39 | fn all_keys(&self) -> Vec<&str> { 40 | self.keys.iter().map(|k| k.as_str()).collect() 41 | } 42 | fn search(&self, query: &Lowercase) -> Vec { 43 | self.keys 44 | .iter() 45 | .filter(|k| starts_with_insensitive_ascii(k.as_bytes(), query)) 46 | .map(|k| DocEntry::NixpkgsTreeDoc(k.clone())) 47 | .collect() 48 | } 49 | fn search_liberal(&self, query: &Lowercase) -> Vec { 50 | self.keys 51 | .iter() 52 | .filter(|k| contains_insensitive_ascii(k.as_bytes(), query)) 53 | .map(|k| DocEntry::NixpkgsTreeDoc(k.clone())) 54 | .collect() 55 | } 56 | fn update(&mut self) -> Result { 57 | let new_keys = gen_keys()?; 58 | let old = std::mem::replace(&mut self.keys, new_keys); 59 | 60 | Ok(old != self.keys) 61 | } 62 | } 63 | impl Cache for NixpkgsTreeDatabase {} 64 | 65 | fn gen_keys() -> Result, Errors> { 66 | const CODE: &str = r#" 67 | let 68 | pkgs = import { }; 69 | f = with builtins; v: (mapAttrs 70 | (name: value: 71 | if (tryEval value).success 72 | && ! (tryEval (pkgs.lib.isDerivation value)).value 73 | && isAttrs value 74 | then mapAttrs (_: _: {}) value 75 | else {} 76 | ) 77 | v 78 | ); 79 | in 80 | (f (pkgs // { pkgs = {}; lib = {}; })) // { lib = f pkgs.lib; } 81 | "#; 82 | 83 | let command = Command::new("nix-instantiate") 84 | .arg("--json") 85 | .arg("--strict") 86 | .arg("--eval") 87 | .arg("-E") 88 | .arg(CODE) 89 | .output()?; 90 | 91 | let keys = serde_json::from_slice::(&command.stdout)?; 92 | 93 | Ok(Into::>::into(keys)) 94 | } 95 | -------------------------------------------------------------------------------- /src/options_docsource.rs: -------------------------------------------------------------------------------- 1 | use crate::{ 2 | contains_insensitive_ascii, starts_with_insensitive_ascii, Cache, DocEntry, DocSource, Errors, 3 | Lowercase, 4 | }; 5 | use colored::*; 6 | use serde::{Deserialize, Serialize}; 7 | use std::{ 8 | collections::HashMap, 9 | fs::File, 10 | io::BufReader, 11 | path::{Path, PathBuf}, 12 | process::Command, 13 | }; 14 | 15 | #[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)] 16 | pub struct OptionDocumentation { 17 | #[serde(default)] 18 | description: String, 19 | 20 | #[serde(default, rename(serialize = "readOnly", deserialize = "readOnly"))] 21 | read_only: bool, 22 | 23 | #[serde(rename(serialize = "loc", deserialize = "loc"))] 24 | location: Vec, 25 | 26 | #[serde(rename(serialize = "type", deserialize = "type"))] 27 | option_type: String, 28 | } 29 | 30 | impl OptionDocumentation { 31 | pub fn name(&self) -> String { 32 | self.location.join(".") 33 | } 34 | pub fn pretty_printed(&self) -> String { 35 | format!( 36 | "# {}\n{}\ntype: {}\n\n", 37 | self.name().blue().bold(), 38 | self.description, 39 | self.option_type 40 | ) 41 | } 42 | } 43 | 44 | #[derive(Debug, Copy, Clone, PartialEq, Eq, Serialize, Deserialize)] 45 | pub enum OptionsDatabaseType { 46 | NixOS, 47 | HomeManager, 48 | } 49 | 50 | #[derive(Debug, Serialize, Deserialize)] 51 | pub struct OptionsDatabase { 52 | pub typ: OptionsDatabaseType, 53 | pub options: HashMap, 54 | } 55 | 56 | impl OptionsDatabase { 57 | pub fn new(typ: OptionsDatabaseType) -> Self { 58 | Self { 59 | typ, 60 | options: HashMap::new(), 61 | } 62 | } 63 | } 64 | 65 | pub fn try_from_file(path: &PathBuf) -> Result, Errors> { 66 | let options: HashMap = 67 | serde_json::from_slice(&std::fs::read(path)?)?; 68 | Ok(options) 69 | } 70 | 71 | impl DocSource for OptionsDatabase { 72 | fn all_keys(&self) -> Vec<&str> { 73 | self.options.keys().map(|x| x.as_ref()).collect() 74 | } 75 | fn search(&self, query: &Lowercase) -> Vec { 76 | self.options 77 | .iter() 78 | .filter(|(key, _)| starts_with_insensitive_ascii(key.as_bytes(), query)) 79 | .map(|(_, d)| DocEntry::OptionDoc(self.typ, d.clone())) 80 | .collect() 81 | } 82 | fn search_liberal(&self, query: &Lowercase) -> Vec { 83 | self.options 84 | .iter() 85 | .filter(|(key, _)| contains_insensitive_ascii(key.as_bytes(), query)) 86 | .map(|(_, d)| DocEntry::OptionDoc(self.typ, d.clone())) 87 | .collect() 88 | } 89 | fn update(&mut self) -> Result { 90 | let opts = match self.typ { 91 | OptionsDatabaseType::NixOS => try_from_file(&get_nixos_json_doc_path()?)?, 92 | OptionsDatabaseType::HomeManager => try_from_file(&get_hm_json_doc_path()?)?, 93 | }; 94 | 95 | let old = std::mem::replace(&mut self.options, opts); 96 | 97 | Ok(old.keys().eq(self.options.keys())) 98 | } 99 | } 100 | 101 | impl Cache for OptionsDatabase {} 102 | 103 | pub fn get_hm_json_doc_path() -> Result { 104 | let base_path_output = Command::new("nix-build") 105 | .arg("-E") 106 | .arg( 107 | r#"{ pkgs ? import {} }: 108 | let 109 | hmargs = { pkgs = pkgs; lib = import () pkgs.lib; }; 110 | docs = import () hmargs; 111 | in (if builtins.isFunction docs then docs hmargs else docs).options.json 112 | "#) 113 | .output() 114 | .map(|o| String::from_utf8(o.stdout).unwrap())?; 115 | 116 | Ok(PathBuf::from(base_path_output.trim_end_matches("\n")) 117 | .join("share/doc/home-manager/options.json")) 118 | } 119 | 120 | pub fn get_nixos_json_doc_path() -> Result { 121 | let base_path_output = Command::new("nix-build") 122 | .env("NIXPKGS_ALLOW_UNFREE", "1") 123 | .env("NIXPKGS_ALLOW_BROKEN", "1") 124 | .env("NIXPKGS_ALLOW_INSECURE", "1") 125 | .arg("--no-out-link") 126 | .arg("-E") 127 | .arg(r#"with import {}; let eval = import (pkgs.path + "/nixos/lib/eval-config.nix") { modules = []; }; opts = (nixosOptionsDoc { options = eval.options; }).optionsJSON; in runCommandLocal "options.json" { inherit opts; } "cp $opts/share/doc/nixos/options.json $out""#) 128 | .output() 129 | .map(|o| String::from_utf8(o.stdout).unwrap())?; 130 | 131 | Ok(PathBuf::from(base_path_output.trim_end_matches("\n"))) 132 | } 133 | -------------------------------------------------------------------------------- /src/xml_docsource.rs: -------------------------------------------------------------------------------- 1 | use colored::*; 2 | use roxmltree::{self, Document}; 3 | 4 | use crate::{ 5 | contains_insensitive_ascii, starts_with_insensitive_ascii, Cache, DocEntry, DocSource, Errors, 6 | Lowercase, 7 | }; 8 | use serde::{Deserialize, Serialize}; 9 | use std::{collections::HashMap, path::PathBuf, process::Command}; 10 | use walkdir::WalkDir; 11 | 12 | #[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)] 13 | pub struct XmlFuncDocumentation { 14 | name: String, 15 | description: String, 16 | fn_type: Option, 17 | args: Vec<(String, String)>, 18 | example: Option, 19 | } 20 | 21 | impl XmlFuncDocumentation { 22 | pub fn name(&self) -> String { 23 | self.name.to_string() 24 | } 25 | 26 | pub fn pretty_printed(&self) -> String { 27 | let mut output = String::new(); 28 | if let Some(function_type) = &self.fn_type { 29 | output.push_str(&format!( 30 | "# {} ({})\n", 31 | self.name.blue().bold(), 32 | function_type.cyan() 33 | )); 34 | } else { 35 | output.push_str(&format!("# {}\n", self.name.blue())); 36 | } 37 | output.push_str(&format!("{}\n", self.description)); 38 | if !self.args.is_empty() { 39 | output.push_str("\nArguments:\n"); 40 | for (name, description) in &self.args { 41 | output.push_str(&format!(" {}: {}\n", name.green(), description)); 42 | } 43 | } 44 | if let Some(example) = &self.example { 45 | output.push_str("\nExample:\n"); 46 | for line in example.lines() { 47 | output.push_str(&format!(" {}\n", line.white())); 48 | } 49 | } 50 | output 51 | } 52 | 53 | fn from_function_section_node(node: &roxmltree::Node) -> Option { 54 | let name = node.first_element_child()?.first_element_child()?.text()?; 55 | let desc = node.descendants().find(|x| is_tag(x, "para"))?.text()?; 56 | let fn_type = node 57 | .descendants() 58 | .find(|n| { 59 | is_tag(&n, "subtitle") 60 | && n.first_element_child() 61 | .map_or(false, |n| is_tag(&n, "literal")) 62 | }) 63 | .and_then(|n| n.first_element_child()) 64 | .and_then(|n| n.text()) 65 | .map(|x| x.to_string()); 66 | 67 | let args: Vec<_> = node 68 | .descendants() 69 | .find(|n| is_tag(&n, "variablelist")) 70 | .map(|list| { 71 | list.children() 72 | .filter(|n| n.is_element()) 73 | .filter_map(|entry| { 74 | let name = entry.descendants().find(|n| is_tag(&n, "varname")); 75 | let desc = entry.descendants().find(|n| is_tag(&n, "para")); 76 | if let (Some(name), Some(desc)) = 77 | (name.and_then(|x| x.text()), desc.and_then(|x| x.text())) 78 | { 79 | Some((name.to_owned(), desc.to_owned())) 80 | } else { 81 | None 82 | } 83 | }) 84 | .collect() 85 | }) 86 | .unwrap_or_default(); 87 | 88 | let example = node 89 | .descendants() 90 | .find(|n| is_tag(&n, "example")) 91 | .and_then(|n| n.descendants().find(|n| is_tag(&n, "programlisting"))) 92 | .map(|n| { 93 | n.descendants() 94 | .filter_map(|n| n.text()) 95 | .collect::>() 96 | .join("") 97 | .to_string() 98 | }); 99 | Some(XmlFuncDocumentation { 100 | name: name.to_owned(), 101 | description: desc.to_owned(), 102 | fn_type, 103 | example, 104 | args, 105 | }) 106 | } 107 | } 108 | 109 | #[derive(Debug, Clone, Serialize, Deserialize)] 110 | pub struct XmlFuncDocDatabase { 111 | pub functions: HashMap, 112 | } 113 | 114 | impl XmlFuncDocDatabase { 115 | pub fn new() -> Self { 116 | Self { 117 | functions: HashMap::new(), 118 | } 119 | } 120 | } 121 | 122 | impl Cache for XmlFuncDocDatabase {} 123 | 124 | impl DocSource for XmlFuncDocDatabase { 125 | fn all_keys(&self) -> Vec<&str> { 126 | self.functions.keys().map(|x| x.as_str()).collect() 127 | } 128 | fn search(&self, query: &Lowercase) -> Vec { 129 | self.functions 130 | .iter() 131 | .filter(|(key, _)| starts_with_insensitive_ascii(key.as_bytes(), query)) 132 | .map(|(_, value)| DocEntry::XmlFuncDoc(value.clone())) 133 | .collect() 134 | } 135 | fn search_liberal(&self, query: &Lowercase) -> Vec { 136 | self.functions 137 | .iter() 138 | .filter(|(key, _)| contains_insensitive_ascii(key.as_bytes(), query)) 139 | .map(|(_, value)| DocEntry::XmlFuncDoc(value.clone())) 140 | .collect() 141 | } 142 | fn update(&mut self) -> Result { 143 | let doc_path = &generate_docs(); 144 | let mut result = Vec::new(); 145 | for file in xml_files_in(doc_path) { 146 | let content = std::fs::read_to_string(&file).map_err(|e| Errors::FileIo { 147 | err: e, 148 | filename: file.to_str().unwrap().to_string(), 149 | })?; 150 | let document = Document::parse(&content).map_err(|e| Errors::XmlParse { 151 | err: e, 152 | filename: file.to_str().unwrap().to_string(), 153 | })?; 154 | 155 | let mut function_entries = document 156 | .descendants() 157 | .filter(|x| is_tag(&x, "section")) 158 | .filter(|x| { 159 | x.first_element_child().map_or(false, |c| { 160 | is_tag(&c, "title") 161 | && c.first_element_child() 162 | .map_or(false, |f| is_tag(&f, "function")) 163 | }) 164 | }) 165 | .filter_map(|node| XmlFuncDocumentation::from_function_section_node(&node)) 166 | .collect::>(); 167 | result.append(&mut function_entries); 168 | } 169 | 170 | let new = result.into_iter().map(|x| (x.name(), x)).collect(); 171 | let old = std::mem::replace(&mut self.functions, new); 172 | 173 | Ok(!self.functions.keys().eq(old.keys())) 174 | } 175 | } 176 | 177 | fn is_tag(x: &roxmltree::Node, name: &str) -> bool { 178 | x.tag_name().name() == name 179 | } 180 | 181 | fn xml_files_in(path: &PathBuf) -> Vec { 182 | WalkDir::new(path) 183 | .into_iter() 184 | .filter_map(Result::ok) 185 | .filter(|e| !e.file_type().is_dir()) 186 | .filter(|e| e.path().extension().and_then(|s| s.to_str()) == Some("xml")) 187 | .map(|x| x.path().to_path_buf()) 188 | .collect::>() 189 | } 190 | 191 | fn generate_docs() -> PathBuf { 192 | let doc_path = Command::new("nix-build") 193 | .arg("--no-out-link") 194 | .arg("") 195 | .output() 196 | .ok() 197 | .and_then(|o| String::from_utf8(o.stdout).ok()) 198 | .unwrap(); 199 | PathBuf::from(doc_path.trim_end_matches("\n")).join("function-docs") 200 | } 201 | --------------------------------------------------------------------------------