├── .gitignore ├── Cargo.lock ├── Cargo.toml ├── LEARN.org ├── Makefile ├── README.md ├── examples └── tree1.txt ├── scripts ├── diffs.clj └── record.sh └── src └── main.rs /.gitignore: -------------------------------------------------------------------------------- 1 | /target 2 | LEARN.html -------------------------------------------------------------------------------- /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 = "ahash" 7 | version = "0.8.3" 8 | source = "registry+https://github.com/rust-lang/crates.io-index" 9 | checksum = "2c99f64d1e06488f620f932677e24bc6e2897582980441ae90a671415bd7ec2f" 10 | dependencies = [ 11 | "cfg-if", 12 | "getrandom", 13 | "once_cell", 14 | "version_check", 15 | ] 16 | 17 | [[package]] 18 | name = "anstream" 19 | version = "0.6.14" 20 | source = "registry+https://github.com/rust-lang/crates.io-index" 21 | checksum = "418c75fa768af9c03be99d17643f93f79bbba589895012a80e3452a19ddda15b" 22 | dependencies = [ 23 | "anstyle", 24 | "anstyle-parse", 25 | "anstyle-query", 26 | "anstyle-wincon", 27 | "colorchoice", 28 | "is_terminal_polyfill", 29 | "utf8parse", 30 | ] 31 | 32 | [[package]] 33 | name = "anstyle" 34 | version = "1.0.7" 35 | source = "registry+https://github.com/rust-lang/crates.io-index" 36 | checksum = "038dfcf04a5feb68e9c60b21c9625a54c2c0616e79b72b0fd87075a056ae1d1b" 37 | 38 | [[package]] 39 | name = "anstyle-parse" 40 | version = "0.2.4" 41 | source = "registry+https://github.com/rust-lang/crates.io-index" 42 | checksum = "c03a11a9034d92058ceb6ee011ce58af4a9bf61491aa7e1e59ecd24bd40d22d4" 43 | dependencies = [ 44 | "utf8parse", 45 | ] 46 | 47 | [[package]] 48 | name = "anstyle-query" 49 | version = "1.1.0" 50 | source = "registry+https://github.com/rust-lang/crates.io-index" 51 | checksum = "ad186efb764318d35165f1758e7dcef3b10628e26d41a44bc5550652e6804391" 52 | dependencies = [ 53 | "windows-sys", 54 | ] 55 | 56 | [[package]] 57 | name = "anstyle-wincon" 58 | version = "3.0.3" 59 | source = "registry+https://github.com/rust-lang/crates.io-index" 60 | checksum = "61a38449feb7068f52bb06c12759005cf459ee52bb4adc1d5a7c4322d716fb19" 61 | dependencies = [ 62 | "anstyle", 63 | "windows-sys", 64 | ] 65 | 66 | [[package]] 67 | name = "autocfg" 68 | version = "1.1.0" 69 | source = "registry+https://github.com/rust-lang/crates.io-index" 70 | checksum = "d468802bab17cbc0cc575e9b053f41e72aa36bfa6b7f55e3529ffa43161b97fa" 71 | 72 | [[package]] 73 | name = "cc" 74 | version = "1.0.79" 75 | source = "registry+https://github.com/rust-lang/crates.io-index" 76 | checksum = "50d30906286121d95be3d479533b458f87493b30a4b5f79a607db8f5d11aa91f" 77 | 78 | [[package]] 79 | name = "cfg-if" 80 | version = "1.0.0" 81 | source = "registry+https://github.com/rust-lang/crates.io-index" 82 | checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd" 83 | 84 | [[package]] 85 | name = "clap" 86 | version = "4.5.6" 87 | source = "registry+https://github.com/rust-lang/crates.io-index" 88 | checksum = "a9689a29b593160de5bc4aacab7b5d54fb52231de70122626c178e6a368994c7" 89 | dependencies = [ 90 | "clap_builder", 91 | "clap_derive", 92 | ] 93 | 94 | [[package]] 95 | name = "clap_builder" 96 | version = "4.5.6" 97 | source = "registry+https://github.com/rust-lang/crates.io-index" 98 | checksum = "2e5387378c84f6faa26890ebf9f0a92989f8873d4d380467bcd0d8d8620424df" 99 | dependencies = [ 100 | "anstream", 101 | "anstyle", 102 | "clap_lex", 103 | "strsim", 104 | ] 105 | 106 | [[package]] 107 | name = "clap_derive" 108 | version = "4.5.5" 109 | source = "registry+https://github.com/rust-lang/crates.io-index" 110 | checksum = "c780290ccf4fb26629baa7a1081e68ced113f1d3ec302fa5948f1c381ebf06c6" 111 | dependencies = [ 112 | "heck", 113 | "proc-macro2", 114 | "quote", 115 | "syn", 116 | ] 117 | 118 | [[package]] 119 | name = "clap_lex" 120 | version = "0.7.1" 121 | source = "registry+https://github.com/rust-lang/crates.io-index" 122 | checksum = "4b82cf0babdbd58558212896d1a4272303a57bdb245c2bf1147185fb45640e70" 123 | 124 | [[package]] 125 | name = "colorchoice" 126 | version = "1.0.1" 127 | source = "registry+https://github.com/rust-lang/crates.io-index" 128 | checksum = "0b6a852b24ab71dffc585bcb46eaf7959d175cb865a7152e35b348d1b2960422" 129 | 130 | [[package]] 131 | name = "crossbeam-channel" 132 | version = "0.5.8" 133 | source = "registry+https://github.com/rust-lang/crates.io-index" 134 | checksum = "a33c2bf77f2df06183c3aa30d1e96c0695a313d4f9c453cc3762a6db39f99200" 135 | dependencies = [ 136 | "cfg-if", 137 | "crossbeam-utils", 138 | ] 139 | 140 | [[package]] 141 | name = "crossbeam-utils" 142 | version = "0.8.16" 143 | source = "registry+https://github.com/rust-lang/crates.io-index" 144 | checksum = "5a22b2d63d4d1dc0b7f1b6b2747dd0088008a9be28b6ddf0b1e7d335e3037294" 145 | dependencies = [ 146 | "cfg-if", 147 | ] 148 | 149 | [[package]] 150 | name = "cursive" 151 | version = "0.20.0" 152 | source = "registry+https://github.com/rust-lang/crates.io-index" 153 | checksum = "5438eb16bdd8af51b31e74764fef5d0a9260227a5ec82ba75c9d11ce46595839" 154 | dependencies = [ 155 | "ahash", 156 | "cfg-if", 157 | "crossbeam-channel", 158 | "cursive_core", 159 | "lazy_static", 160 | "libc", 161 | "log", 162 | "maplit", 163 | "ncurses", 164 | "signal-hook", 165 | "term_size", 166 | "unicode-segmentation", 167 | "unicode-width", 168 | ] 169 | 170 | [[package]] 171 | name = "cursive_core" 172 | version = "0.3.7" 173 | source = "registry+https://github.com/rust-lang/crates.io-index" 174 | checksum = "4db3b58161228d0dcb45c7968c5e74c3f03ad39e8983e58ad7d57061aa2cd94d" 175 | dependencies = [ 176 | "ahash", 177 | "crossbeam-channel", 178 | "enum-map", 179 | "enumset", 180 | "lazy_static", 181 | "log", 182 | "num", 183 | "owning_ref", 184 | "time", 185 | "unicode-segmentation", 186 | "unicode-width", 187 | "xi-unicode", 188 | ] 189 | 190 | [[package]] 191 | name = "darling" 192 | version = "0.20.1" 193 | source = "registry+https://github.com/rust-lang/crates.io-index" 194 | checksum = "0558d22a7b463ed0241e993f76f09f30b126687447751a8638587b864e4b3944" 195 | dependencies = [ 196 | "darling_core", 197 | "darling_macro", 198 | ] 199 | 200 | [[package]] 201 | name = "darling_core" 202 | version = "0.20.1" 203 | source = "registry+https://github.com/rust-lang/crates.io-index" 204 | checksum = "ab8bfa2e259f8ee1ce5e97824a3c55ec4404a0d772ca7fa96bf19f0752a046eb" 205 | dependencies = [ 206 | "fnv", 207 | "ident_case", 208 | "proc-macro2", 209 | "quote", 210 | "syn", 211 | ] 212 | 213 | [[package]] 214 | name = "darling_macro" 215 | version = "0.20.1" 216 | source = "registry+https://github.com/rust-lang/crates.io-index" 217 | checksum = "29a358ff9f12ec09c3e61fef9b5a9902623a695a46a917b07f269bff1445611a" 218 | dependencies = [ 219 | "darling_core", 220 | "quote", 221 | "syn", 222 | ] 223 | 224 | [[package]] 225 | name = "enum-map" 226 | version = "2.6.0" 227 | source = "registry+https://github.com/rust-lang/crates.io-index" 228 | checksum = "017b207acb4cc917f4c31758ed95c0bc63ddb0f358b22eb38f80a2b2a43f6b1f" 229 | dependencies = [ 230 | "enum-map-derive", 231 | ] 232 | 233 | [[package]] 234 | name = "enum-map-derive" 235 | version = "0.12.0" 236 | source = "registry+https://github.com/rust-lang/crates.io-index" 237 | checksum = "8560b409800a72d2d7860f8e5f4e0b0bd22bea6a352ea2a9ce30ccdef7f16d2f" 238 | dependencies = [ 239 | "proc-macro2", 240 | "quote", 241 | "syn", 242 | ] 243 | 244 | [[package]] 245 | name = "enumset" 246 | version = "1.1.2" 247 | source = "registry+https://github.com/rust-lang/crates.io-index" 248 | checksum = "e875f1719c16de097dee81ed675e2d9bb63096823ed3f0ca827b7dea3028bbbb" 249 | dependencies = [ 250 | "enumset_derive", 251 | ] 252 | 253 | [[package]] 254 | name = "enumset_derive" 255 | version = "0.8.1" 256 | source = "registry+https://github.com/rust-lang/crates.io-index" 257 | checksum = "e08b6c6ab82d70f08844964ba10c7babb716de2ecaeab9be5717918a5177d3af" 258 | dependencies = [ 259 | "darling", 260 | "proc-macro2", 261 | "quote", 262 | "syn", 263 | ] 264 | 265 | [[package]] 266 | name = "fnv" 267 | version = "1.0.7" 268 | source = "registry+https://github.com/rust-lang/crates.io-index" 269 | checksum = "3f9eec918d3f24069decb9af1554cad7c880e2da24a9afd88aca000531ab82c1" 270 | 271 | [[package]] 272 | name = "getrandom" 273 | version = "0.2.10" 274 | source = "registry+https://github.com/rust-lang/crates.io-index" 275 | checksum = "be4136b2a15dd319360be1c07d9933517ccf0be8f16bf62a3bee4f0d618df427" 276 | dependencies = [ 277 | "cfg-if", 278 | "libc", 279 | "wasi", 280 | ] 281 | 282 | [[package]] 283 | name = "heck" 284 | version = "0.5.0" 285 | source = "registry+https://github.com/rust-lang/crates.io-index" 286 | checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea" 287 | 288 | [[package]] 289 | name = "ident_case" 290 | version = "1.0.1" 291 | source = "registry+https://github.com/rust-lang/crates.io-index" 292 | checksum = "b9e0384b61958566e926dc50660321d12159025e767c18e043daf26b70104c39" 293 | 294 | [[package]] 295 | name = "is_terminal_polyfill" 296 | version = "1.70.0" 297 | source = "registry+https://github.com/rust-lang/crates.io-index" 298 | checksum = "f8478577c03552c21db0e2724ffb8986a5ce7af88107e6be5d2ee6e158c12800" 299 | 300 | [[package]] 301 | name = "itoa" 302 | version = "1.0.8" 303 | source = "registry+https://github.com/rust-lang/crates.io-index" 304 | checksum = "62b02a5381cc465bd3041d84623d0fa3b66738b52b8e2fc3bab8ad63ab032f4a" 305 | 306 | [[package]] 307 | name = "lazy_static" 308 | version = "1.4.0" 309 | source = "registry+https://github.com/rust-lang/crates.io-index" 310 | checksum = "e2abad23fbc42b3700f2f279844dc832adb2b2eb069b2df918f455c4e18cc646" 311 | 312 | [[package]] 313 | name = "libc" 314 | version = "0.2.147" 315 | source = "registry+https://github.com/rust-lang/crates.io-index" 316 | checksum = "b4668fb0ea861c1df094127ac5f1da3409a82116a4ba74fca2e58ef927159bb3" 317 | 318 | [[package]] 319 | name = "log" 320 | version = "0.4.19" 321 | source = "registry+https://github.com/rust-lang/crates.io-index" 322 | checksum = "b06a4cde4c0f271a446782e3eff8de789548ce57dbc8eca9292c27f4a42004b4" 323 | 324 | [[package]] 325 | name = "maplit" 326 | version = "1.0.2" 327 | source = "registry+https://github.com/rust-lang/crates.io-index" 328 | checksum = "3e2e65a1a2e43cfcb47a895c4c8b10d1f4a61097f9f254f183aee60cad9c651d" 329 | 330 | [[package]] 331 | name = "ncurses" 332 | version = "5.101.0" 333 | source = "registry+https://github.com/rust-lang/crates.io-index" 334 | checksum = "5e2c5d34d72657dc4b638a1c25d40aae81e4f1c699062f72f467237920752032" 335 | dependencies = [ 336 | "cc", 337 | "libc", 338 | "pkg-config", 339 | ] 340 | 341 | [[package]] 342 | name = "num" 343 | version = "0.4.0" 344 | source = "registry+https://github.com/rust-lang/crates.io-index" 345 | checksum = "43db66d1170d347f9a065114077f7dccb00c1b9478c89384490a3425279a4606" 346 | dependencies = [ 347 | "num-complex", 348 | "num-integer", 349 | "num-iter", 350 | "num-rational", 351 | "num-traits", 352 | ] 353 | 354 | [[package]] 355 | name = "num-complex" 356 | version = "0.4.3" 357 | source = "registry+https://github.com/rust-lang/crates.io-index" 358 | checksum = "02e0d21255c828d6f128a1e41534206671e8c3ea0c62f32291e808dc82cff17d" 359 | dependencies = [ 360 | "num-traits", 361 | ] 362 | 363 | [[package]] 364 | name = "num-integer" 365 | version = "0.1.45" 366 | source = "registry+https://github.com/rust-lang/crates.io-index" 367 | checksum = "225d3389fb3509a24c93f5c29eb6bde2586b98d9f016636dff58d7c6f7569cd9" 368 | dependencies = [ 369 | "autocfg", 370 | "num-traits", 371 | ] 372 | 373 | [[package]] 374 | name = "num-iter" 375 | version = "0.1.43" 376 | source = "registry+https://github.com/rust-lang/crates.io-index" 377 | checksum = "7d03e6c028c5dc5cac6e2dec0efda81fc887605bb3d884578bb6d6bf7514e252" 378 | dependencies = [ 379 | "autocfg", 380 | "num-integer", 381 | "num-traits", 382 | ] 383 | 384 | [[package]] 385 | name = "num-rational" 386 | version = "0.4.1" 387 | source = "registry+https://github.com/rust-lang/crates.io-index" 388 | checksum = "0638a1c9d0a3c0914158145bc76cff373a75a627e6ecbfb71cbe6f453a5a19b0" 389 | dependencies = [ 390 | "autocfg", 391 | "num-integer", 392 | "num-traits", 393 | ] 394 | 395 | [[package]] 396 | name = "num-traits" 397 | version = "0.2.15" 398 | source = "registry+https://github.com/rust-lang/crates.io-index" 399 | checksum = "578ede34cf02f8924ab9447f50c28075b4d3e5b269972345e7e0372b38c6cdcd" 400 | dependencies = [ 401 | "autocfg", 402 | ] 403 | 404 | [[package]] 405 | name = "num_threads" 406 | version = "0.1.6" 407 | source = "registry+https://github.com/rust-lang/crates.io-index" 408 | checksum = "2819ce041d2ee131036f4fc9d6ae7ae125a3a40e97ba64d04fe799ad9dabbb44" 409 | dependencies = [ 410 | "libc", 411 | ] 412 | 413 | [[package]] 414 | name = "once_cell" 415 | version = "1.18.0" 416 | source = "registry+https://github.com/rust-lang/crates.io-index" 417 | checksum = "dd8b5dd2ae5ed71462c540258bedcb51965123ad7e7ccf4b9a8cafaa4a63576d" 418 | 419 | [[package]] 420 | name = "owning_ref" 421 | version = "0.4.1" 422 | source = "registry+https://github.com/rust-lang/crates.io-index" 423 | checksum = "6ff55baddef9e4ad00f88b6c743a2a8062d4c6ade126c2a528644b8e444d52ce" 424 | dependencies = [ 425 | "stable_deref_trait", 426 | ] 427 | 428 | [[package]] 429 | name = "pkg-config" 430 | version = "0.3.27" 431 | source = "registry+https://github.com/rust-lang/crates.io-index" 432 | checksum = "26072860ba924cbfa98ea39c8c19b4dd6a4a25423dbdf219c1eca91aa0cf6964" 433 | 434 | [[package]] 435 | name = "proc-macro2" 436 | version = "1.0.85" 437 | source = "registry+https://github.com/rust-lang/crates.io-index" 438 | checksum = "22244ce15aa966053a896d1accb3a6e68469b97c7f33f284b99f0d576879fc23" 439 | dependencies = [ 440 | "unicode-ident", 441 | ] 442 | 443 | [[package]] 444 | name = "quote" 445 | version = "1.0.29" 446 | source = "registry+https://github.com/rust-lang/crates.io-index" 447 | checksum = "573015e8ab27661678357f27dc26460738fd2b6c86e46f386fde94cb5d913105" 448 | dependencies = [ 449 | "proc-macro2", 450 | ] 451 | 452 | [[package]] 453 | name = "serde" 454 | version = "1.0.171" 455 | source = "registry+https://github.com/rust-lang/crates.io-index" 456 | checksum = "30e27d1e4fd7659406c492fd6cfaf2066ba8773de45ca75e855590f856dc34a9" 457 | 458 | [[package]] 459 | name = "signal-hook" 460 | version = "0.3.15" 461 | source = "registry+https://github.com/rust-lang/crates.io-index" 462 | checksum = "732768f1176d21d09e076c23a93123d40bba92d50c4058da34d45c8de8e682b9" 463 | dependencies = [ 464 | "libc", 465 | "signal-hook-registry", 466 | ] 467 | 468 | [[package]] 469 | name = "signal-hook-registry" 470 | version = "1.4.1" 471 | source = "registry+https://github.com/rust-lang/crates.io-index" 472 | checksum = "d8229b473baa5980ac72ef434c4415e70c4b5e71b423043adb4ba059f89c99a1" 473 | dependencies = [ 474 | "libc", 475 | ] 476 | 477 | [[package]] 478 | name = "stable_deref_trait" 479 | version = "1.2.0" 480 | source = "registry+https://github.com/rust-lang/crates.io-index" 481 | checksum = "a8f112729512f8e442d81f95a8a7ddf2b7c6b8a1a6f509a95864142b30cab2d3" 482 | 483 | [[package]] 484 | name = "strsim" 485 | version = "0.11.1" 486 | source = "registry+https://github.com/rust-lang/crates.io-index" 487 | checksum = "7da8b5736845d9f2fcb837ea5d9e2628564b3b043a70948a3f0b778838c5fb4f" 488 | 489 | [[package]] 490 | name = "syn" 491 | version = "2.0.25" 492 | source = "registry+https://github.com/rust-lang/crates.io-index" 493 | checksum = "15e3fc8c0c74267e2df136e5e5fb656a464158aa57624053375eb9c8c6e25ae2" 494 | dependencies = [ 495 | "proc-macro2", 496 | "quote", 497 | "unicode-ident", 498 | ] 499 | 500 | [[package]] 501 | name = "term_size" 502 | version = "0.3.2" 503 | source = "registry+https://github.com/rust-lang/crates.io-index" 504 | checksum = "1e4129646ca0ed8f45d09b929036bafad5377103edd06e50bf574b353d2b08d9" 505 | dependencies = [ 506 | "libc", 507 | "winapi", 508 | ] 509 | 510 | [[package]] 511 | name = "time" 512 | version = "0.3.23" 513 | source = "registry+https://github.com/rust-lang/crates.io-index" 514 | checksum = "59e399c068f43a5d116fedaf73b203fa4f9c519f17e2b34f63221d3792f81446" 515 | dependencies = [ 516 | "itoa", 517 | "libc", 518 | "num_threads", 519 | "serde", 520 | "time-core", 521 | "time-macros", 522 | ] 523 | 524 | [[package]] 525 | name = "time-core" 526 | version = "0.1.1" 527 | source = "registry+https://github.com/rust-lang/crates.io-index" 528 | checksum = "7300fbefb4dadc1af235a9cef3737cea692a9d97e1b9cbcd4ebdae6f8868e6fb" 529 | 530 | [[package]] 531 | name = "time-macros" 532 | version = "0.2.10" 533 | source = "registry+https://github.com/rust-lang/crates.io-index" 534 | checksum = "96ba15a897f3c86766b757e5ac7221554c6750054d74d5b28844fce5fb36a6c4" 535 | dependencies = [ 536 | "time-core", 537 | ] 538 | 539 | [[package]] 540 | name = "treeviewer" 541 | version = "0.1.0" 542 | dependencies = [ 543 | "clap", 544 | "cursive", 545 | ] 546 | 547 | [[package]] 548 | name = "unicode-ident" 549 | version = "1.0.10" 550 | source = "registry+https://github.com/rust-lang/crates.io-index" 551 | checksum = "22049a19f4a68748a168c0fc439f9516686aa045927ff767eca0a85101fb6e73" 552 | 553 | [[package]] 554 | name = "unicode-segmentation" 555 | version = "1.10.1" 556 | source = "registry+https://github.com/rust-lang/crates.io-index" 557 | checksum = "1dd624098567895118886609431a7c3b8f516e41d30e0643f03d94592a147e36" 558 | 559 | [[package]] 560 | name = "unicode-width" 561 | version = "0.1.10" 562 | source = "registry+https://github.com/rust-lang/crates.io-index" 563 | checksum = "c0edd1e5b14653f783770bce4a4dabb4a5108a5370a5f5d8cfe8710c361f6c8b" 564 | 565 | [[package]] 566 | name = "utf8parse" 567 | version = "0.2.2" 568 | source = "registry+https://github.com/rust-lang/crates.io-index" 569 | checksum = "06abde3611657adf66d383f00b093d7faecc7fa57071cce2578660c9f1010821" 570 | 571 | [[package]] 572 | name = "version_check" 573 | version = "0.9.4" 574 | source = "registry+https://github.com/rust-lang/crates.io-index" 575 | checksum = "49874b5167b65d7193b8aba1567f5c7d93d001cafc34600cee003eda787e483f" 576 | 577 | [[package]] 578 | name = "wasi" 579 | version = "0.11.0+wasi-snapshot-preview1" 580 | source = "registry+https://github.com/rust-lang/crates.io-index" 581 | checksum = "9c8d87e72b64a3b4db28d11ce29237c246188f4f51057d65a7eab63b7987e423" 582 | 583 | [[package]] 584 | name = "winapi" 585 | version = "0.3.9" 586 | source = "registry+https://github.com/rust-lang/crates.io-index" 587 | checksum = "5c839a674fcd7a98952e593242ea400abe93992746761e38641405d28b00f419" 588 | dependencies = [ 589 | "winapi-i686-pc-windows-gnu", 590 | "winapi-x86_64-pc-windows-gnu", 591 | ] 592 | 593 | [[package]] 594 | name = "winapi-i686-pc-windows-gnu" 595 | version = "0.4.0" 596 | source = "registry+https://github.com/rust-lang/crates.io-index" 597 | checksum = "ac3b87c63620426dd9b991e5ce0329eff545bccbbb34f3be09ff6fb6ab51b7b6" 598 | 599 | [[package]] 600 | name = "winapi-x86_64-pc-windows-gnu" 601 | version = "0.4.0" 602 | source = "registry+https://github.com/rust-lang/crates.io-index" 603 | checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f" 604 | 605 | [[package]] 606 | name = "windows-sys" 607 | version = "0.52.0" 608 | source = "registry+https://github.com/rust-lang/crates.io-index" 609 | checksum = "282be5f36a8ce781fad8c8ae18fa3f9beff57ec1b52cb3de0789201425d9a33d" 610 | dependencies = [ 611 | "windows-targets", 612 | ] 613 | 614 | [[package]] 615 | name = "windows-targets" 616 | version = "0.52.5" 617 | source = "registry+https://github.com/rust-lang/crates.io-index" 618 | checksum = "6f0713a46559409d202e70e28227288446bf7841d3211583a4b53e3f6d96e7eb" 619 | dependencies = [ 620 | "windows_aarch64_gnullvm", 621 | "windows_aarch64_msvc", 622 | "windows_i686_gnu", 623 | "windows_i686_gnullvm", 624 | "windows_i686_msvc", 625 | "windows_x86_64_gnu", 626 | "windows_x86_64_gnullvm", 627 | "windows_x86_64_msvc", 628 | ] 629 | 630 | [[package]] 631 | name = "windows_aarch64_gnullvm" 632 | version = "0.52.5" 633 | source = "registry+https://github.com/rust-lang/crates.io-index" 634 | checksum = "7088eed71e8b8dda258ecc8bac5fb1153c5cffaf2578fc8ff5d61e23578d3263" 635 | 636 | [[package]] 637 | name = "windows_aarch64_msvc" 638 | version = "0.52.5" 639 | source = "registry+https://github.com/rust-lang/crates.io-index" 640 | checksum = "9985fd1504e250c615ca5f281c3f7a6da76213ebd5ccc9561496568a2752afb6" 641 | 642 | [[package]] 643 | name = "windows_i686_gnu" 644 | version = "0.52.5" 645 | source = "registry+https://github.com/rust-lang/crates.io-index" 646 | checksum = "88ba073cf16d5372720ec942a8ccbf61626074c6d4dd2e745299726ce8b89670" 647 | 648 | [[package]] 649 | name = "windows_i686_gnullvm" 650 | version = "0.52.5" 651 | source = "registry+https://github.com/rust-lang/crates.io-index" 652 | checksum = "87f4261229030a858f36b459e748ae97545d6f1ec60e5e0d6a3d32e0dc232ee9" 653 | 654 | [[package]] 655 | name = "windows_i686_msvc" 656 | version = "0.52.5" 657 | source = "registry+https://github.com/rust-lang/crates.io-index" 658 | checksum = "db3c2bf3d13d5b658be73463284eaf12830ac9a26a90c717b7f771dfe97487bf" 659 | 660 | [[package]] 661 | name = "windows_x86_64_gnu" 662 | version = "0.52.5" 663 | source = "registry+https://github.com/rust-lang/crates.io-index" 664 | checksum = "4e4246f76bdeff09eb48875a0fd3e2af6aada79d409d33011886d3e1581517d9" 665 | 666 | [[package]] 667 | name = "windows_x86_64_gnullvm" 668 | version = "0.52.5" 669 | source = "registry+https://github.com/rust-lang/crates.io-index" 670 | checksum = "852298e482cd67c356ddd9570386e2862b5673c85bd5f88df9ab6802b334c596" 671 | 672 | [[package]] 673 | name = "windows_x86_64_msvc" 674 | version = "0.52.5" 675 | source = "registry+https://github.com/rust-lang/crates.io-index" 676 | checksum = "bec47e5bfd1bff0eeaf6d8b485cc1074891a197ab4225d504cb7a1ab88b02bf0" 677 | 678 | [[package]] 679 | name = "xi-unicode" 680 | version = "0.3.0" 681 | source = "registry+https://github.com/rust-lang/crates.io-index" 682 | checksum = "a67300977d3dc3f8034dae89778f502b6ba20b269527b3223ba59c0cf393bb8a" 683 | -------------------------------------------------------------------------------- /Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "treeviewer" 3 | version = "0.1.0" 4 | edition = "2021" 5 | 6 | # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html 7 | 8 | [dependencies] 9 | clap = { version = "4.5.6", features = ["derive"] } 10 | cursive = "0.20.0" 11 | -------------------------------------------------------------------------------- /LEARN.org: -------------------------------------------------------------------------------- 1 | * Learned 2 | ** Why do I have to use ~String::from~ for string literals? 3 | 4 | It seems to work when you remove ~String::from~, but the tree node is then of type ~&str~, not ~String~. You can't mix the two, as the compiler generates an error. Like this: 5 | 6 | #+BEGIN_EXAMPLE 7 | error[E0308]: mismatched types 8 | --> src/main.rs:9:37 9 | | 10 | 9 | children: vec![Tree {value: String::from("child"), children: vec![]}, 11 | | ^^^^^^^^^^^^^^^^^^^^^ 12 | | | 13 | | expected `&str`, found `String` 14 | | help: consider borrowing here: `&String::from("child")` 15 | #+END_EXAMPLE 16 | 17 | So ~String::from~ actually returns a ~String~, while the type of string literals is ~&str~. The difference is that ~String~ is a potentially mutable string that has a length and a capacity (so similar to a ~StringBuffer~ or ~StringBuilder~ in Java), whereas ~&str~ is the type of /immutable string slices/. 18 | 19 | ** Generic function syntax 20 | 21 | The declaration of ~print_tree~ has to be: 22 | 23 | #+BEGIN_SRC rust 24 | fn print_tree(t: &Tree) { ... } 25 | #+END_SRC 26 | 27 | Rather than the one I tried first: 28 | #+BEGIN_SRC rust 29 | fn print_tree(t: &Tree) { ... } 30 | #+END_SRC 31 | 32 | This makes sense. Because I call ~println!~ on values of type T within the function, we need to tell it “this function accepts trees of /printable/ items”, rather than “accepts arbitrary trees”. Traits start to smell like Haskell type classes. 33 | 34 | ** Mixing peekable iterators with ~for … in~ 35 | 36 | You can’t. Instead of: 37 | 38 | #+BEGIN_SRC rust 39 | let mut it = coll.iter().peekable(); // needs to be mut 40 | 41 | for elem in it { 42 | do_something(elem); 43 | do_something_else(it.peek()); // doesn’t compile 44 | } 45 | // … 46 | #+END_SRC 47 | 48 | I needed to convert the loop to a ~while~: 49 | 50 | #+BEGIN_SRC rust 51 | while let Some(elem) = it.next() { 52 | do_something(elem); 53 | do_something_else(it.peek()); // kosher 54 | } 55 | // … 56 | #+END_SRC 57 | 58 | The error originating from the first snippet is: 59 | #+BEGIN_EXAMPLE 60 | error[E0382]: borrow of moved value: `it` 61 | --> src/main.rs:15:15 62 | | 63 | 10 | let mut it = t.children.iter().peekable(); 64 | | ------ move occurs because `it` has type `Peekable>>`, which does not implement the `Copy` trait 65 | ... 66 | 13 | for child in it { 67 | | -- `it` moved due to this implicit call to `.into_iter()` 68 | 14 | // while let Some(child) = it.next() { 69 | 15 | match it.peek() { 70 | | ^^^^^^^^^ value borrowed here after move 71 | | 72 | note: `into_iter` takes ownership of the receiver `self`, which moves `it` 73 | #+END_EXAMPLE 74 | 75 | I need to meditate on it some more. 76 | 77 | ** Extending lifetime 78 | 79 | This doesn’t work: 80 | 81 | #+BEGIN_SRC rust 82 | for line in stdin.lock().lines() { 83 | append_path(&mut t, &line.unwrap()); 84 | } 85 | #+END_SRC 86 | 87 | It’s intuitively obvious why. On each iteration, ~line~ lives only as long as the ~for~ loop’s inner block, so we need to extend the string’s lifetime somehow. (Removing the borrow from ~line.unwrap()~ results in a type mismatch.) 88 | 89 | It’s unclear to me whether there exists a general approach to this. The compiler suggests 90 | “consider using a ~let~ binding to create a longer lived value”, but in this particular case, it wouldn’t help either. So I ended up putting all the lines in a long-lived vector first (which makes 91 | additional sense in my case: the vector can be sorted before passing its elements to ~append_path~, in 92 | case the input is not sorted already). 93 | 94 | ** ~Box~ vs. ~Vec~ 95 | 96 | In chapter 15, the Book discusses why it's necessary to box a type when we're defining a recursive type (spoiler: so that the compiler can know how much memory to allocate). So, then, the question is: why does the current definition of ~Tree~ work? 97 | 98 | Answer: ~Vec~ owns its elements and keeps them on the heap, so it's similar to ~Box~ in that regard. See [[https://stackoverflow.com/questions/43641728][this SO answer]]. In fact, I think ~Box~ can be thought of as a one-element vector. 99 | 100 | * To explore 101 | ** Traits 102 | ** Lifetimes 103 | 104 | The signature of ~append_path~ turned out to require an explicit lifetime declaration. I’ve read the relevant section of the Book (10.3) but I don’t fully grok it yet. 105 | 106 | Looks like structures that hold references need an explicit lifetime declaration. The Rust Book seems to imply that this would be treyf: 107 | 108 | #+BEGIN_SRC rust 109 | pub struct TreeStr { 110 | value: &str, 111 | children: Vec, 112 | } 113 | #+END_SRC 114 | 115 | Why can I instantiate ~Tree<&str>~'s when ~Tree~ is defined as a generic? 116 | 117 | ** How to test functions that print stuff? 118 | 119 | I.e., what is the Rust equivalent of Clojure’s ~with-out-str~? 120 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | all: build 2 | 3 | build: 4 | scripts/record.sh 5 | cargo build 6 | 7 | run: 8 | scripts/record.sh 9 | cargo run 10 | 11 | .PHONY: all build run 12 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # treeviewer 2 | 3 | This is a pet project to help me learn Rust. But maybe it’ll end up being of actual use for someone? 4 | 5 | The idea is to write a program that, given an input of slash-separated paths, provide a console-based tree viewer that will help visualizing these paths – with folding/unfolding subtrees, searching, etc. 6 | 7 | ## License 8 | 9 | This program is licensed under the [Opinionated Queer License, v1.1][1]. 10 | 11 | [1]: https://oql.avris.it/license?c=Daniel%20Janus%7Chttps://danieljanus.pl 12 | -------------------------------------------------------------------------------- /examples/tree1.txt: -------------------------------------------------------------------------------- 1 | raz/dwa 2 | raz/trzy/cztery 3 | raz/trzy/pięć/sześć 4 | siedem/osiem 5 | -------------------------------------------------------------------------------- /scripts/diffs.clj: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bb 2 | (require '[babashka.process :refer [shell process exec]]) 3 | (require '[babashka.fs :as fs]) 4 | (require '[clojure.java.io :as io]) 5 | (require '[clojure.string :as str]) 6 | 7 | (def root-dir (fs/parent (fs/parent (fs/normalize *file*)))) 8 | 9 | (defn init-tmpdir [] 10 | (shell {:out :string, :err :string, :dir "/tmp"} "git clone" root-dir)) 11 | 12 | (defn clean-tmpdir [] 13 | (shell "rm -rf /tmp/treeviewer")) 14 | 15 | (defn try-build [tag] 16 | (shell {:out :string, :err :string, :dir "/tmp/treeviewer"} "git checkout" tag) 17 | (shell {:out :string, :err :string, :dir "/tmp/treeviewer", :continue true} "cargo build")) 18 | 19 | (shell "mkdir -p diffs") 20 | (init-tmpdir) 21 | (let [tags (-> (shell {:out :string} "git" "tag") 22 | :out 23 | (str/split #"\n"))] 24 | (spit "diffs/build-logs.txt" 25 | (with-out-str 26 | (doseq [[prev-tag tag] (partition 2 1 tags)] 27 | (let [build (try-build tag) 28 | ok? (zero? (:exit build)) 29 | diff (shell {:out :string} "git diff" prev-tag tag)] 30 | (println "======") 31 | (printf "%s: %s\n" tag (if ok? "SUCCESS" "FAILURE")) 32 | (println "DIFF:") 33 | (println (:out diff)) 34 | (when-not ok? 35 | (println "ERRORS:") 36 | (println (:out build) (:err build)))))))) 37 | (clean-tmpdir) 38 | -------------------------------------------------------------------------------- /scripts/record.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | TAG="build-`date +%Y%m%d-%H%M%S`" 4 | COMMIT_DESC="Build attempt `date`" 5 | 6 | { 7 | 8 | git stash push -k -u 9 | git stash apply 10 | git add -A 11 | git commit -m "$COMMIT_DESC" && git tag -a -m "$COMMIT_DESC" "$TAG" && git reset --hard HEAD^ 12 | git stash pop 13 | 14 | } >/dev/null 15 | 16 | echo Created build tag: $TAG 17 | -------------------------------------------------------------------------------- /src/main.rs: -------------------------------------------------------------------------------- 1 | use std::collections::HashSet; 2 | use std::fmt::Display; 3 | use std::fs::File; 4 | use std::io::{self, BufRead}; 5 | use std::path::{Path, PathBuf}; 6 | use std::iter::{empty, Peekable}; 7 | use std::slice::Iter; 8 | use clap::Parser; 9 | 10 | use cursive::{ 11 | Cursive, 12 | event::Key, 13 | view::{Nameable, Resizable, Scrollable}, 14 | views::{OnEventView, SelectView} 15 | }; 16 | 17 | pub struct Tree { 18 | value: T, 19 | children: Vec>, 20 | } 21 | 22 | pub struct TreeIterator<'a, T> { 23 | parent_prefix: String, 24 | immediate_prefix: &'static str, 25 | parent_suffix: &'static str, 26 | value: &'a T, 27 | emitted: bool, 28 | viter: Box + 'a>, 29 | citer: Box>>>, 30 | collapsed: &'a HashSet>, 31 | current: Vec, 32 | } 33 | 34 | pub struct State { 35 | tree: Tree, 36 | collapsed: HashSet>, 37 | } 38 | 39 | fn append_path<'a>(mut t: &'a mut Tree, path: &'a str, separator: &'a str) { 40 | for node in path.split(separator) { 41 | if node.is_empty() { 42 | continue; 43 | } 44 | 45 | let match_last = match t.children.last() { 46 | None => false, 47 | Some(x) => x.value == node 48 | }; 49 | 50 | if match_last { 51 | t = t.children.last_mut().unwrap(); 52 | } else { 53 | let subtree = Tree { value: String::from(node), children: vec![] }; 54 | t.children.push(subtree); 55 | t = t.children.last_mut().unwrap(); 56 | } 57 | } 58 | } 59 | 60 | fn read_lines

(filename: P) -> io::Result>> 61 | where P: AsRef, { 62 | let file = File::open(filename)?; 63 | Ok(io::BufReader::new(file).lines()) 64 | } 65 | 66 | fn init_state(path: &Path, separator: &str) -> State { 67 | let mut t = Tree {value: String::from(""), children: vec![]}; 68 | 69 | for line in read_lines(path).unwrap().flatten() { 70 | append_path(&mut t, &line, separator); 71 | } 72 | 73 | State { tree: t, collapsed: HashSet::new() } 74 | } 75 | 76 | impl<'a, T> Iterator for TreeIterator<'a, T> where T: Display { 77 | type Item = String; 78 | 79 | fn next(&mut self) -> Option { 80 | if !self.emitted && !self.immediate_prefix.is_empty() { 81 | self.emitted = true; 82 | let val = format!("{}{}{}", self.parent_prefix, self.immediate_prefix, self.value); 83 | self.current.push(0); 84 | Some(val) 85 | } else if let Some(val) = self.viter.next() { 86 | let val = format!("{}", val); 87 | Some(val) 88 | } else if let Some(child) = self.citer.next() { 89 | let node_collapsed = self.collapsed.contains(&self.current); 90 | let subprefix = format!("{0}{1}", self.parent_prefix, self.parent_suffix); 91 | let last = !self.citer.peek().is_some(); 92 | let immediate_prefix = if last { "└─ " } else { "├─ " }; 93 | let parent_suffix = if last { " " } else { "│ " }; 94 | let val = if !node_collapsed { 95 | self.viter = Box::new(child.prefixed_lines(subprefix, immediate_prefix, parent_suffix, &self.collapsed, self.current.clone())); 96 | self.next() 97 | } else { 98 | let immediate_prefix = if last { "└⊞ " } else { "├⊞ " }; 99 | Some(format!("{}{}{}", subprefix, immediate_prefix, child.value)) 100 | }; 101 | *self.current.last_mut().unwrap() += 1; 102 | val 103 | } else { 104 | None 105 | } 106 | } 107 | } 108 | 109 | impl Tree { 110 | pub fn count(&self, coords: &mut Vec, collapsed: &HashSet>) -> usize { 111 | if collapsed.contains(coords) { 112 | return 1; 113 | } 114 | let mut res = 1; 115 | coords.push(0); 116 | for child in &self.children { 117 | res += child.count(coords, collapsed); 118 | *coords.last_mut().unwrap() += 1; 119 | } 120 | coords.pop(); 121 | res 122 | } 123 | } 124 | 125 | impl Tree where T: Display { 126 | pub fn prefixed_lines<'a>(&'a self, parent_prefix: String, immediate_prefix: &'static str, parent_suffix: &'static str, collapsed: &'a HashSet>, current: Vec) -> TreeIterator<'a, T> { 127 | TreeIterator { 128 | parent_prefix: parent_prefix, 129 | immediate_prefix: immediate_prefix, 130 | parent_suffix: parent_suffix, 131 | value: &self.value, 132 | emitted: false, 133 | viter: Box::new(empty()), 134 | citer: Box::new(self.children.iter().peekable()), 135 | collapsed: collapsed, 136 | current: current, 137 | } 138 | } 139 | } 140 | 141 | impl State { 142 | pub fn lines<'a>(&'a self) -> TreeIterator<'a, String> { 143 | self.tree.prefixed_lines(String::from(""), "", "", &self.collapsed, vec![0]) 144 | } 145 | 146 | fn recursive_coords(&self, tree: &Tree, mut i: usize, mut coords: Vec) -> Vec { 147 | if i == 0 { 148 | coords 149 | } else { 150 | i -= 1; 151 | let mut next_child: Option<&Tree> = None; 152 | coords.push(0); 153 | for child in &tree.children { 154 | next_child = Some(child); 155 | let cnt = child.count(&mut coords, &self.collapsed); 156 | if i >= cnt { 157 | i -= cnt; 158 | *coords.last_mut().unwrap() += 1; 159 | } else { 160 | break; 161 | } 162 | } 163 | match next_child { 164 | Some(ch) => { 165 | self.recursive_coords(ch, i, coords) 166 | }, 167 | None => coords 168 | } 169 | } 170 | } 171 | 172 | pub fn coords(&self, i: usize) -> Vec { 173 | self.recursive_coords(&self.tree, i, vec![]) 174 | } 175 | } 176 | 177 | fn redraw(siv: &mut Cursive) { 178 | // TODO: collect() feels excessive, but I can't beat the borrow checker without it 179 | let lines: Vec = siv.with_user_data(|state: &mut State| state.lines().collect()).unwrap(); 180 | siv.call_on_name("select", |select: &mut SelectView| { 181 | let id = select.selected_id(); 182 | select.clear(); 183 | select.add_all_str(lines); 184 | if let Some(id) = id { 185 | select.set_selection(id); 186 | } 187 | }); 188 | } 189 | 190 | /// Reads a list of slash-separated paths and displays them as a tree. 191 | #[derive(Parser)] 192 | struct Cli { 193 | /// Separator of nodes in paths 194 | #[arg(short, long, default_value_t = String::from("/"))] 195 | separator: String, 196 | 197 | /// The file to display 198 | file: PathBuf, 199 | } 200 | 201 | fn main() { 202 | let args = Cli::parse(); 203 | 204 | let state = init_state(&args.file, &args.separator); 205 | let mut siv = cursive::default(); 206 | let mut select = SelectView::new().with_name("select"); 207 | select.get_mut().add_all_str(state.lines()); 208 | siv.set_user_data(state); 209 | 210 | let xselect = OnEventView::new(select) 211 | .on_event(Key::Right, move |s| { 212 | let id = s.call_on_name("select", |select: &mut SelectView| { select.selected_id().unwrap() }).unwrap(); 213 | s.with_user_data(|state: &mut State| { 214 | let coords = state.coords(id + 1); 215 | state.collapsed.remove(&coords); 216 | }); 217 | redraw(s); 218 | }) 219 | .on_event(Key::Left, move |s| { 220 | let id = s.call_on_name("select", |select: &mut SelectView| { select.selected_id().unwrap() }).unwrap(); 221 | s.with_user_data(|state: &mut State| { 222 | let coords = state.coords(id + 1); 223 | state.collapsed.insert(coords); 224 | }); 225 | redraw(s); 226 | }); 227 | 228 | siv.add_fullscreen_layer(xselect.scrollable().full_screen()); 229 | siv.add_global_callback('q', Cursive::quit); 230 | siv.run(); 231 | } 232 | --------------------------------------------------------------------------------