A terminal app to allow for easy voice recording
0
fork

Configure Feed

Select the types of activity you want to include in your feed.

initial version

Jen Dempsey d05d2453

+3105
+3
.gitignore
··· 1 + /target 2 + 3 + .claude/
+1781
Cargo.lock
··· 1 + # This file is automatically @generated by Cargo. 2 + # It is not intended for manual editing. 3 + version = 4 4 + 5 + [[package]] 6 + name = "aho-corasick" 7 + version = "1.1.4" 8 + source = "registry+https://github.com/rust-lang/crates.io-index" 9 + checksum = "ddd31a130427c27518df266943a5308ed92d4b226cc639f5a8f1002816174301" 10 + dependencies = [ 11 + "memchr", 12 + ] 13 + 14 + [[package]] 15 + name = "allocator-api2" 16 + version = "0.2.21" 17 + source = "registry+https://github.com/rust-lang/crates.io-index" 18 + checksum = "683d7910e743518b0e34f1186f92494becacb047c7b6bf616c96772180fef923" 19 + 20 + [[package]] 21 + name = "alsa" 22 + version = "0.9.1" 23 + source = "registry+https://github.com/rust-lang/crates.io-index" 24 + checksum = "ed7572b7ba83a31e20d1b48970ee402d2e3e0537dcfe0a3ff4d6eb7508617d43" 25 + dependencies = [ 26 + "alsa-sys", 27 + "bitflags 2.11.0", 28 + "cfg-if", 29 + "libc", 30 + ] 31 + 32 + [[package]] 33 + name = "alsa-sys" 34 + version = "0.3.1" 35 + source = "registry+https://github.com/rust-lang/crates.io-index" 36 + checksum = "db8fee663d06c4e303404ef5f40488a53e062f89ba8bfed81f42325aafad1527" 37 + dependencies = [ 38 + "libc", 39 + "pkg-config", 40 + ] 41 + 42 + [[package]] 43 + name = "android_system_properties" 44 + version = "0.1.5" 45 + source = "registry+https://github.com/rust-lang/crates.io-index" 46 + checksum = "819e7219dbd41043ac279b19830f2efc897156490d7fd6ea916720117ee66311" 47 + dependencies = [ 48 + "libc", 49 + ] 50 + 51 + [[package]] 52 + name = "anstream" 53 + version = "1.0.0" 54 + source = "registry+https://github.com/rust-lang/crates.io-index" 55 + checksum = "824a212faf96e9acacdbd09febd34438f8f711fb84e09a8916013cd7815ca28d" 56 + dependencies = [ 57 + "anstyle", 58 + "anstyle-parse", 59 + "anstyle-query", 60 + "anstyle-wincon", 61 + "colorchoice", 62 + "is_terminal_polyfill", 63 + "utf8parse", 64 + ] 65 + 66 + [[package]] 67 + name = "anstyle" 68 + version = "1.0.14" 69 + source = "registry+https://github.com/rust-lang/crates.io-index" 70 + checksum = "940b3a0ca603d1eade50a4846a2afffd5ef57a9feac2c0e2ec2e14f9ead76000" 71 + 72 + [[package]] 73 + name = "anstyle-parse" 74 + version = "1.0.0" 75 + source = "registry+https://github.com/rust-lang/crates.io-index" 76 + checksum = "52ce7f38b242319f7cabaa6813055467063ecdc9d355bbb4ce0c68908cd8130e" 77 + dependencies = [ 78 + "utf8parse", 79 + ] 80 + 81 + [[package]] 82 + name = "anstyle-query" 83 + version = "1.1.5" 84 + source = "registry+https://github.com/rust-lang/crates.io-index" 85 + checksum = "40c48f72fd53cd289104fc64099abca73db4166ad86ea0b4341abe65af83dadc" 86 + dependencies = [ 87 + "windows-sys 0.61.2", 88 + ] 89 + 90 + [[package]] 91 + name = "anstyle-wincon" 92 + version = "3.0.11" 93 + source = "registry+https://github.com/rust-lang/crates.io-index" 94 + checksum = "291e6a250ff86cd4a820112fb8898808a366d8f9f58ce16d1f538353ad55747d" 95 + dependencies = [ 96 + "anstyle", 97 + "once_cell_polyfill", 98 + "windows-sys 0.61.2", 99 + ] 100 + 101 + [[package]] 102 + name = "anyhow" 103 + version = "1.0.102" 104 + source = "registry+https://github.com/rust-lang/crates.io-index" 105 + checksum = "7f202df86484c868dbad7eaa557ef785d5c66295e41b460ef922eca0723b842c" 106 + 107 + [[package]] 108 + name = "arrayvec" 109 + version = "0.7.6" 110 + source = "registry+https://github.com/rust-lang/crates.io-index" 111 + checksum = "7c02d123df017efcdfbd739ef81735b36c5ba83ec3c59c80a9d7ecc718f92e50" 112 + 113 + [[package]] 114 + name = "autocfg" 115 + version = "1.5.0" 116 + source = "registry+https://github.com/rust-lang/crates.io-index" 117 + checksum = "c08606f8c3cbf4ce6ec8e28fb0014a2c086708fe954eaa885384a6165172e7e8" 118 + 119 + [[package]] 120 + name = "autotools" 121 + version = "0.2.7" 122 + source = "registry+https://github.com/rust-lang/crates.io-index" 123 + checksum = "ef941527c41b0fc0dd48511a8154cd5fc7e29200a0ff8b7203c5d777dbc795cf" 124 + dependencies = [ 125 + "cc", 126 + ] 127 + 128 + [[package]] 129 + name = "bindgen" 130 + version = "0.72.1" 131 + source = "registry+https://github.com/rust-lang/crates.io-index" 132 + checksum = "993776b509cfb49c750f11b8f07a46fa23e0a1386ffc01fb1e7d343efc387895" 133 + dependencies = [ 134 + "bitflags 2.11.0", 135 + "cexpr", 136 + "clang-sys", 137 + "itertools 0.13.0", 138 + "proc-macro2", 139 + "quote", 140 + "regex", 141 + "rustc-hash", 142 + "shlex", 143 + "syn", 144 + ] 145 + 146 + [[package]] 147 + name = "bitflags" 148 + version = "1.3.2" 149 + source = "registry+https://github.com/rust-lang/crates.io-index" 150 + checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a" 151 + 152 + [[package]] 153 + name = "bitflags" 154 + version = "2.11.0" 155 + source = "registry+https://github.com/rust-lang/crates.io-index" 156 + checksum = "843867be96c8daad0d758b57df9392b6d8d271134fce549de6ce169ff98a92af" 157 + 158 + [[package]] 159 + name = "bumpalo" 160 + version = "3.20.2" 161 + source = "registry+https://github.com/rust-lang/crates.io-index" 162 + checksum = "5d20789868f4b01b2f2caec9f5c4e0213b41e3e5702a50157d699ae31ced2fcb" 163 + 164 + [[package]] 165 + name = "bytemuck" 166 + version = "1.25.0" 167 + source = "registry+https://github.com/rust-lang/crates.io-index" 168 + checksum = "c8efb64bd706a16a1bdde310ae86b351e4d21550d98d056f22f8a7f7a2183fec" 169 + 170 + [[package]] 171 + name = "byteorder" 172 + version = "1.5.0" 173 + source = "registry+https://github.com/rust-lang/crates.io-index" 174 + checksum = "1fd0f2584146f6f2ef48085050886acf353beff7305ebd1ae69500e27c67f64b" 175 + 176 + [[package]] 177 + name = "bytes" 178 + version = "1.11.1" 179 + source = "registry+https://github.com/rust-lang/crates.io-index" 180 + checksum = "1e748733b7cbc798e1434b6ac524f0c1ff2ab456fe201501e6497c8417a4fc33" 181 + 182 + [[package]] 183 + name = "cassowary" 184 + version = "0.3.0" 185 + source = "registry+https://github.com/rust-lang/crates.io-index" 186 + checksum = "df8670b8c7b9dae1793364eafadf7239c40d669904660c5960d74cfd80b46a53" 187 + 188 + [[package]] 189 + name = "castaway" 190 + version = "0.2.4" 191 + source = "registry+https://github.com/rust-lang/crates.io-index" 192 + checksum = "dec551ab6e7578819132c713a93c022a05d60159dc86e7a7050223577484c55a" 193 + dependencies = [ 194 + "rustversion", 195 + ] 196 + 197 + [[package]] 198 + name = "cc" 199 + version = "1.2.58" 200 + source = "registry+https://github.com/rust-lang/crates.io-index" 201 + checksum = "e1e928d4b69e3077709075a938a05ffbedfa53a84c8f766efbf8220bb1ff60e1" 202 + dependencies = [ 203 + "find-msvc-tools", 204 + "jobserver", 205 + "libc", 206 + "shlex", 207 + ] 208 + 209 + [[package]] 210 + name = "cesu8" 211 + version = "1.1.0" 212 + source = "registry+https://github.com/rust-lang/crates.io-index" 213 + checksum = "6d43a04d8753f35258c91f8ec639f792891f748a1edbd759cf1dcea3382ad83c" 214 + 215 + [[package]] 216 + name = "cexpr" 217 + version = "0.6.0" 218 + source = "registry+https://github.com/rust-lang/crates.io-index" 219 + checksum = "6fac387a98bb7c37292057cffc56d62ecb629900026402633ae9160df93a8766" 220 + dependencies = [ 221 + "nom", 222 + ] 223 + 224 + [[package]] 225 + name = "cfg-if" 226 + version = "1.0.4" 227 + source = "registry+https://github.com/rust-lang/crates.io-index" 228 + checksum = "9330f8b2ff13f34540b44e946ef35111825727b38d33286ef986142615121801" 229 + 230 + [[package]] 231 + name = "chrono" 232 + version = "0.4.44" 233 + source = "registry+https://github.com/rust-lang/crates.io-index" 234 + checksum = "c673075a2e0e5f4a1dde27ce9dee1ea4558c7ffe648f576438a20ca1d2acc4b0" 235 + dependencies = [ 236 + "iana-time-zone", 237 + "js-sys", 238 + "num-traits", 239 + "wasm-bindgen", 240 + "windows-link", 241 + ] 242 + 243 + [[package]] 244 + name = "clang-sys" 245 + version = "1.8.1" 246 + source = "registry+https://github.com/rust-lang/crates.io-index" 247 + checksum = "0b023947811758c97c59bf9d1c188fd619ad4718dcaa767947df1cadb14f39f4" 248 + dependencies = [ 249 + "glob", 250 + "libc", 251 + "libloading", 252 + ] 253 + 254 + [[package]] 255 + name = "clap" 256 + version = "4.6.0" 257 + source = "registry+https://github.com/rust-lang/crates.io-index" 258 + checksum = "b193af5b67834b676abd72466a96c1024e6a6ad978a1f484bd90b85c94041351" 259 + dependencies = [ 260 + "clap_builder", 261 + "clap_derive", 262 + ] 263 + 264 + [[package]] 265 + name = "clap_builder" 266 + version = "4.6.0" 267 + source = "registry+https://github.com/rust-lang/crates.io-index" 268 + checksum = "714a53001bf66416adb0e2ef5ac857140e7dc3a0c48fb28b2f10762fc4b5069f" 269 + dependencies = [ 270 + "anstream", 271 + "anstyle", 272 + "clap_lex", 273 + "strsim", 274 + ] 275 + 276 + [[package]] 277 + name = "clap_derive" 278 + version = "4.6.0" 279 + source = "registry+https://github.com/rust-lang/crates.io-index" 280 + checksum = "1110bd8a634a1ab8cb04345d8d878267d57c3cf1b38d91b71af6686408bbca6a" 281 + dependencies = [ 282 + "heck", 283 + "proc-macro2", 284 + "quote", 285 + "syn", 286 + ] 287 + 288 + [[package]] 289 + name = "clap_lex" 290 + version = "1.1.0" 291 + source = "registry+https://github.com/rust-lang/crates.io-index" 292 + checksum = "c8d4a3bb8b1e0c1050499d1815f5ab16d04f0959b233085fb31653fbfc9d98f9" 293 + 294 + [[package]] 295 + name = "claxon" 296 + version = "0.4.3" 297 + source = "registry+https://github.com/rust-lang/crates.io-index" 298 + checksum = "4bfbf56724aa9eca8afa4fcfadeb479e722935bb2a0900c2d37e0cc477af0688" 299 + 300 + [[package]] 301 + name = "colorchoice" 302 + version = "1.0.5" 303 + source = "registry+https://github.com/rust-lang/crates.io-index" 304 + checksum = "1d07550c9036bf2ae0c684c4297d503f838287c83c53686d05370d0e139ae570" 305 + 306 + [[package]] 307 + name = "combine" 308 + version = "4.6.7" 309 + source = "registry+https://github.com/rust-lang/crates.io-index" 310 + checksum = "ba5a308b75df32fe02788e748662718f03fde005016435c444eea572398219fd" 311 + dependencies = [ 312 + "bytes", 313 + "memchr", 314 + ] 315 + 316 + [[package]] 317 + name = "compact_str" 318 + version = "0.7.1" 319 + source = "registry+https://github.com/rust-lang/crates.io-index" 320 + checksum = "f86b9c4c00838774a6d902ef931eff7470720c51d90c2e32cfe15dc304737b3f" 321 + dependencies = [ 322 + "castaway", 323 + "cfg-if", 324 + "itoa", 325 + "ryu", 326 + "static_assertions", 327 + ] 328 + 329 + [[package]] 330 + name = "core-foundation-sys" 331 + version = "0.8.7" 332 + source = "registry+https://github.com/rust-lang/crates.io-index" 333 + checksum = "773648b94d0e5d620f64f280777445740e61fe701025087ec8b57f45c791888b" 334 + 335 + [[package]] 336 + name = "coreaudio-rs" 337 + version = "0.11.3" 338 + source = "registry+https://github.com/rust-lang/crates.io-index" 339 + checksum = "321077172d79c662f64f5071a03120748d5bb652f5231570141be24cfcd2bace" 340 + dependencies = [ 341 + "bitflags 1.3.2", 342 + "core-foundation-sys", 343 + "coreaudio-sys", 344 + ] 345 + 346 + [[package]] 347 + name = "coreaudio-sys" 348 + version = "0.2.17" 349 + source = "registry+https://github.com/rust-lang/crates.io-index" 350 + checksum = "ceec7a6067e62d6f931a2baf6f3a751f4a892595bcec1461a3c94ef9949864b6" 351 + dependencies = [ 352 + "bindgen", 353 + ] 354 + 355 + [[package]] 356 + name = "cpal" 357 + version = "0.15.3" 358 + source = "registry+https://github.com/rust-lang/crates.io-index" 359 + checksum = "873dab07c8f743075e57f524c583985fbaf745602acbe916a01539364369a779" 360 + dependencies = [ 361 + "alsa", 362 + "core-foundation-sys", 363 + "coreaudio-rs", 364 + "dasp_sample", 365 + "jni", 366 + "js-sys", 367 + "libc", 368 + "mach2", 369 + "ndk", 370 + "ndk-context", 371 + "oboe", 372 + "wasm-bindgen", 373 + "wasm-bindgen-futures", 374 + "web-sys", 375 + "windows", 376 + ] 377 + 378 + [[package]] 379 + name = "crossterm" 380 + version = "0.27.0" 381 + source = "registry+https://github.com/rust-lang/crates.io-index" 382 + checksum = "f476fe445d41c9e991fd07515a6f463074b782242ccf4a5b7b1d1012e70824df" 383 + dependencies = [ 384 + "bitflags 2.11.0", 385 + "crossterm_winapi", 386 + "libc", 387 + "mio", 388 + "parking_lot", 389 + "signal-hook", 390 + "signal-hook-mio", 391 + "winapi", 392 + ] 393 + 394 + [[package]] 395 + name = "crossterm_winapi" 396 + version = "0.9.1" 397 + source = "registry+https://github.com/rust-lang/crates.io-index" 398 + checksum = "acdd7c62a3665c7f6830a51635d9ac9b23ed385797f70a83bb8bafe9c572ab2b" 399 + dependencies = [ 400 + "winapi", 401 + ] 402 + 403 + [[package]] 404 + name = "dasp_sample" 405 + version = "0.11.0" 406 + source = "registry+https://github.com/rust-lang/crates.io-index" 407 + checksum = "0c87e182de0887fd5361989c677c4e8f5000cd9491d6d563161a8f3a5519fc7f" 408 + 409 + [[package]] 410 + name = "either" 411 + version = "1.15.0" 412 + source = "registry+https://github.com/rust-lang/crates.io-index" 413 + checksum = "48c757948c5ede0e46177b7add2e67155f70e33c07fea8284df6576da70b3719" 414 + 415 + [[package]] 416 + name = "encoding_rs" 417 + version = "0.8.35" 418 + source = "registry+https://github.com/rust-lang/crates.io-index" 419 + checksum = "75030f3c4f45dafd7586dd6780965a8c7e8e285a5ecb86713e63a79c5b2766f3" 420 + dependencies = [ 421 + "cfg-if", 422 + ] 423 + 424 + [[package]] 425 + name = "equivalent" 426 + version = "1.0.2" 427 + source = "registry+https://github.com/rust-lang/crates.io-index" 428 + checksum = "877a4ace8713b0bcf2a4e7eec82529c029f1d0619886d18145fea96c3ffe5c0f" 429 + 430 + [[package]] 431 + name = "errno" 432 + version = "0.3.14" 433 + source = "registry+https://github.com/rust-lang/crates.io-index" 434 + checksum = "39cab71617ae0d63f51a36d69f866391735b51691dbda63cf6f96d042b63efeb" 435 + dependencies = [ 436 + "libc", 437 + "windows-sys 0.61.2", 438 + ] 439 + 440 + [[package]] 441 + name = "find-msvc-tools" 442 + version = "0.1.9" 443 + source = "registry+https://github.com/rust-lang/crates.io-index" 444 + checksum = "5baebc0774151f905a1a2cc41989300b1e6fbb29aff0ceffa1064fdd3088d582" 445 + 446 + [[package]] 447 + name = "foldhash" 448 + version = "0.1.5" 449 + source = "registry+https://github.com/rust-lang/crates.io-index" 450 + checksum = "d9c4f5dac5e15c24eb999c26181a6ca40b39fe946cbe4c263c7209467bc83af2" 451 + 452 + [[package]] 453 + name = "futures-core" 454 + version = "0.3.32" 455 + source = "registry+https://github.com/rust-lang/crates.io-index" 456 + checksum = "7e3450815272ef58cec6d564423f6e755e25379b217b0bc688e295ba24df6b1d" 457 + 458 + [[package]] 459 + name = "futures-task" 460 + version = "0.3.32" 461 + source = "registry+https://github.com/rust-lang/crates.io-index" 462 + checksum = "037711b3d59c33004d3856fbdc83b99d4ff37a24768fa1be9ce3538a1cde4393" 463 + 464 + [[package]] 465 + name = "futures-util" 466 + version = "0.3.32" 467 + source = "registry+https://github.com/rust-lang/crates.io-index" 468 + checksum = "389ca41296e6190b48053de0321d02a77f32f8a5d2461dd38762c0593805c6d6" 469 + dependencies = [ 470 + "futures-core", 471 + "futures-task", 472 + "pin-project-lite", 473 + "slab", 474 + ] 475 + 476 + [[package]] 477 + name = "getrandom" 478 + version = "0.3.4" 479 + source = "registry+https://github.com/rust-lang/crates.io-index" 480 + checksum = "899def5c37c4fd7b2664648c28120ecec138e4d395b459e5ca34f9cce2dd77fd" 481 + dependencies = [ 482 + "cfg-if", 483 + "libc", 484 + "r-efi", 485 + "wasip2", 486 + ] 487 + 488 + [[package]] 489 + name = "glob" 490 + version = "0.3.3" 491 + source = "registry+https://github.com/rust-lang/crates.io-index" 492 + checksum = "0cc23270f6e1808e30a928bdc84dea0b9b4136a8bc82338574f23baf47bbd280" 493 + 494 + [[package]] 495 + name = "hashbrown" 496 + version = "0.15.5" 497 + source = "registry+https://github.com/rust-lang/crates.io-index" 498 + checksum = "9229cfe53dfd69f0609a49f65461bd93001ea1ef889cd5529dd176593f5338a1" 499 + dependencies = [ 500 + "allocator-api2", 501 + "equivalent", 502 + "foldhash", 503 + ] 504 + 505 + [[package]] 506 + name = "hashbrown" 507 + version = "0.16.1" 508 + source = "registry+https://github.com/rust-lang/crates.io-index" 509 + checksum = "841d1cc9bed7f9236f321df977030373f4a4163ae1a7dbfe1a51a2c1a51d9100" 510 + 511 + [[package]] 512 + name = "heck" 513 + version = "0.5.0" 514 + source = "registry+https://github.com/rust-lang/crates.io-index" 515 + checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea" 516 + 517 + [[package]] 518 + name = "hound" 519 + version = "3.5.1" 520 + source = "registry+https://github.com/rust-lang/crates.io-index" 521 + checksum = "62adaabb884c94955b19907d60019f4e145d091c75345379e70d1ee696f7854f" 522 + 523 + [[package]] 524 + name = "iana-time-zone" 525 + version = "0.1.65" 526 + source = "registry+https://github.com/rust-lang/crates.io-index" 527 + checksum = "e31bc9ad994ba00e440a8aa5c9ef0ec67d5cb5e5cb0cc7f8b744a35b389cc470" 528 + dependencies = [ 529 + "android_system_properties", 530 + "core-foundation-sys", 531 + "iana-time-zone-haiku", 532 + "js-sys", 533 + "log", 534 + "wasm-bindgen", 535 + "windows-core 0.62.2", 536 + ] 537 + 538 + [[package]] 539 + name = "iana-time-zone-haiku" 540 + version = "0.1.2" 541 + source = "registry+https://github.com/rust-lang/crates.io-index" 542 + checksum = "f31827a206f56af32e590ba56d5d2d085f558508192593743f16b2306495269f" 543 + dependencies = [ 544 + "cc", 545 + ] 546 + 547 + [[package]] 548 + name = "indexmap" 549 + version = "2.13.0" 550 + source = "registry+https://github.com/rust-lang/crates.io-index" 551 + checksum = "7714e70437a7dc3ac8eb7e6f8df75fd8eb422675fc7678aff7364301092b1017" 552 + dependencies = [ 553 + "equivalent", 554 + "hashbrown 0.16.1", 555 + ] 556 + 557 + [[package]] 558 + name = "is_terminal_polyfill" 559 + version = "1.70.2" 560 + source = "registry+https://github.com/rust-lang/crates.io-index" 561 + checksum = "a6cb138bb79a146c1bd460005623e142ef0181e3d0219cb493e02f7d08a35695" 562 + 563 + [[package]] 564 + name = "itertools" 565 + version = "0.12.1" 566 + source = "registry+https://github.com/rust-lang/crates.io-index" 567 + checksum = "ba291022dbbd398a455acf126c1e341954079855bc60dfdda641363bd6922569" 568 + dependencies = [ 569 + "either", 570 + ] 571 + 572 + [[package]] 573 + name = "itertools" 574 + version = "0.13.0" 575 + source = "registry+https://github.com/rust-lang/crates.io-index" 576 + checksum = "413ee7dfc52ee1a4949ceeb7dbc8a33f2d6c088194d9f922fb8318faf1f01186" 577 + dependencies = [ 578 + "either", 579 + ] 580 + 581 + [[package]] 582 + name = "itoa" 583 + version = "1.0.18" 584 + source = "registry+https://github.com/rust-lang/crates.io-index" 585 + checksum = "8f42a60cbdf9a97f5d2305f08a87dc4e09308d1276d28c869c684d7777685682" 586 + 587 + [[package]] 588 + name = "jni" 589 + version = "0.21.1" 590 + source = "registry+https://github.com/rust-lang/crates.io-index" 591 + checksum = "1a87aa2bb7d2af34197c04845522473242e1aa17c12f4935d5856491a7fb8c97" 592 + dependencies = [ 593 + "cesu8", 594 + "cfg-if", 595 + "combine", 596 + "jni-sys 0.3.1", 597 + "log", 598 + "thiserror", 599 + "walkdir", 600 + "windows-sys 0.45.0", 601 + ] 602 + 603 + [[package]] 604 + name = "jni-sys" 605 + version = "0.3.1" 606 + source = "registry+https://github.com/rust-lang/crates.io-index" 607 + checksum = "41a652e1f9b6e0275df1f15b32661cf0d4b78d4d87ddec5e0c3c20f097433258" 608 + dependencies = [ 609 + "jni-sys 0.4.1", 610 + ] 611 + 612 + [[package]] 613 + name = "jni-sys" 614 + version = "0.4.1" 615 + source = "registry+https://github.com/rust-lang/crates.io-index" 616 + checksum = "c6377a88cb3910bee9b0fa88d4f42e1d2da8e79915598f65fb0c7ee14c878af2" 617 + dependencies = [ 618 + "jni-sys-macros", 619 + ] 620 + 621 + [[package]] 622 + name = "jni-sys-macros" 623 + version = "0.4.1" 624 + source = "registry+https://github.com/rust-lang/crates.io-index" 625 + checksum = "38c0b942f458fe50cdac086d2f946512305e5631e720728f2a61aabcd47a6264" 626 + dependencies = [ 627 + "quote", 628 + "syn", 629 + ] 630 + 631 + [[package]] 632 + name = "jobserver" 633 + version = "0.1.34" 634 + source = "registry+https://github.com/rust-lang/crates.io-index" 635 + checksum = "9afb3de4395d6b3e67a780b6de64b51c978ecf11cb9a462c66be7d4ca9039d33" 636 + dependencies = [ 637 + "getrandom", 638 + "libc", 639 + ] 640 + 641 + [[package]] 642 + name = "js-sys" 643 + version = "0.3.93" 644 + source = "registry+https://github.com/rust-lang/crates.io-index" 645 + checksum = "797146bb2677299a1eb6b7b50a890f4c361b29ef967addf5b2fa45dae1bb6d7d" 646 + dependencies = [ 647 + "cfg-if", 648 + "futures-util", 649 + "once_cell", 650 + "wasm-bindgen", 651 + ] 652 + 653 + [[package]] 654 + name = "lazy_static" 655 + version = "1.5.0" 656 + source = "registry+https://github.com/rust-lang/crates.io-index" 657 + checksum = "bbd2bcb4c963f2ddae06a2efc7e9f3591312473c50c6685e1f298068316e66fe" 658 + 659 + [[package]] 660 + name = "lewton" 661 + version = "0.10.2" 662 + source = "registry+https://github.com/rust-lang/crates.io-index" 663 + checksum = "777b48df9aaab155475a83a7df3070395ea1ac6902f5cd062b8f2b028075c030" 664 + dependencies = [ 665 + "byteorder", 666 + "ogg", 667 + "tinyvec", 668 + ] 669 + 670 + [[package]] 671 + name = "libc" 672 + version = "0.2.183" 673 + source = "registry+https://github.com/rust-lang/crates.io-index" 674 + checksum = "b5b646652bf6661599e1da8901b3b9522896f01e736bad5f723fe7a3a27f899d" 675 + 676 + [[package]] 677 + name = "libloading" 678 + version = "0.8.9" 679 + source = "registry+https://github.com/rust-lang/crates.io-index" 680 + checksum = "d7c4b02199fee7c5d21a5ae7d8cfa79a6ef5bb2fc834d6e9058e89c825efdc55" 681 + dependencies = [ 682 + "cfg-if", 683 + "windows-link", 684 + ] 685 + 686 + [[package]] 687 + name = "lock_api" 688 + version = "0.4.14" 689 + source = "registry+https://github.com/rust-lang/crates.io-index" 690 + checksum = "224399e74b87b5f3557511d98dff8b14089b3dadafcab6bb93eab67d3aace965" 691 + dependencies = [ 692 + "scopeguard", 693 + ] 694 + 695 + [[package]] 696 + name = "log" 697 + version = "0.4.29" 698 + source = "registry+https://github.com/rust-lang/crates.io-index" 699 + checksum = "5e5032e24019045c762d3c0f28f5b6b8bbf38563a65908389bf7978758920897" 700 + 701 + [[package]] 702 + name = "lru" 703 + version = "0.12.5" 704 + source = "registry+https://github.com/rust-lang/crates.io-index" 705 + checksum = "234cf4f4a04dc1f57e24b96cc0cd600cf2af460d4161ac5ecdd0af8e1f3b2a38" 706 + dependencies = [ 707 + "hashbrown 0.15.5", 708 + ] 709 + 710 + [[package]] 711 + name = "mach2" 712 + version = "0.4.3" 713 + source = "registry+https://github.com/rust-lang/crates.io-index" 714 + checksum = "d640282b302c0bb0a2a8e0233ead9035e3bed871f0b7e81fe4a1ec829765db44" 715 + dependencies = [ 716 + "libc", 717 + ] 718 + 719 + [[package]] 720 + name = "memchr" 721 + version = "2.8.0" 722 + source = "registry+https://github.com/rust-lang/crates.io-index" 723 + checksum = "f8ca58f447f06ed17d5fc4043ce1b10dd205e060fb3ce5b979b8ed8e59ff3f79" 724 + 725 + [[package]] 726 + name = "minimal-lexical" 727 + version = "0.2.1" 728 + source = "registry+https://github.com/rust-lang/crates.io-index" 729 + checksum = "68354c5c6bd36d73ff3feceb05efa59b6acb7626617f4962be322a825e61f79a" 730 + 731 + [[package]] 732 + name = "mio" 733 + version = "0.8.11" 734 + source = "registry+https://github.com/rust-lang/crates.io-index" 735 + checksum = "a4a650543ca06a924e8b371db273b2756685faae30f8487da1b56505a8f78b0c" 736 + dependencies = [ 737 + "libc", 738 + "log", 739 + "wasi", 740 + "windows-sys 0.48.0", 741 + ] 742 + 743 + [[package]] 744 + name = "mp3lame-encoder" 745 + version = "0.2.2" 746 + source = "registry+https://github.com/rust-lang/crates.io-index" 747 + checksum = "5edde3299e6e78f5fb802d2ad566bce5c410a2f99a8562a0dcd6f56e3e77448c" 748 + dependencies = [ 749 + "libc", 750 + "mp3lame-sys", 751 + ] 752 + 753 + [[package]] 754 + name = "mp3lame-sys" 755 + version = "0.1.11" 756 + source = "registry+https://github.com/rust-lang/crates.io-index" 757 + checksum = "54e3b1772db47828840702e5a2e05694527f731abadf9b931355d54035f019d8" 758 + dependencies = [ 759 + "autotools", 760 + "cc", 761 + "libc", 762 + ] 763 + 764 + [[package]] 765 + name = "ndk" 766 + version = "0.8.0" 767 + source = "registry+https://github.com/rust-lang/crates.io-index" 768 + checksum = "2076a31b7010b17a38c01907c45b945e8f11495ee4dd588309718901b1f7a5b7" 769 + dependencies = [ 770 + "bitflags 2.11.0", 771 + "jni-sys 0.3.1", 772 + "log", 773 + "ndk-sys", 774 + "num_enum", 775 + "thiserror", 776 + ] 777 + 778 + [[package]] 779 + name = "ndk-context" 780 + version = "0.1.1" 781 + source = "registry+https://github.com/rust-lang/crates.io-index" 782 + checksum = "27b02d87554356db9e9a873add8782d4ea6e3e58ea071a9adb9a2e8ddb884a8b" 783 + 784 + [[package]] 785 + name = "ndk-sys" 786 + version = "0.5.0+25.2.9519653" 787 + source = "registry+https://github.com/rust-lang/crates.io-index" 788 + checksum = "8c196769dd60fd4f363e11d948139556a344e79d451aeb2fa2fd040738ef7691" 789 + dependencies = [ 790 + "jni-sys 0.3.1", 791 + ] 792 + 793 + [[package]] 794 + name = "nom" 795 + version = "7.1.3" 796 + source = "registry+https://github.com/rust-lang/crates.io-index" 797 + checksum = "d273983c5a657a70a3e8f2a01329822f3b8c8172b73826411a55751e404a0a4a" 798 + dependencies = [ 799 + "memchr", 800 + "minimal-lexical", 801 + ] 802 + 803 + [[package]] 804 + name = "num-derive" 805 + version = "0.4.2" 806 + source = "registry+https://github.com/rust-lang/crates.io-index" 807 + checksum = "ed3955f1a9c7c0c15e092f9c887db08b1fc683305fdf6eb6684f22555355e202" 808 + dependencies = [ 809 + "proc-macro2", 810 + "quote", 811 + "syn", 812 + ] 813 + 814 + [[package]] 815 + name = "num-traits" 816 + version = "0.2.19" 817 + source = "registry+https://github.com/rust-lang/crates.io-index" 818 + checksum = "071dfc062690e90b734c0b2273ce72ad0ffa95f0c74596bc250dcfd960262841" 819 + dependencies = [ 820 + "autocfg", 821 + ] 822 + 823 + [[package]] 824 + name = "num_enum" 825 + version = "0.7.6" 826 + source = "registry+https://github.com/rust-lang/crates.io-index" 827 + checksum = "5d0bca838442ec211fa11de3a8b0e0e8f3a4522575b5c4c06ed722e005036f26" 828 + dependencies = [ 829 + "num_enum_derive", 830 + "rustversion", 831 + ] 832 + 833 + [[package]] 834 + name = "num_enum_derive" 835 + version = "0.7.6" 836 + source = "registry+https://github.com/rust-lang/crates.io-index" 837 + checksum = "680998035259dcfcafe653688bf2aa6d3e2dc05e98be6ab46afb089dc84f1df8" 838 + dependencies = [ 839 + "proc-macro-crate", 840 + "proc-macro2", 841 + "quote", 842 + "syn", 843 + ] 844 + 845 + [[package]] 846 + name = "oboe" 847 + version = "0.6.1" 848 + source = "registry+https://github.com/rust-lang/crates.io-index" 849 + checksum = "e8b61bebd49e5d43f5f8cc7ee2891c16e0f41ec7954d36bcb6c14c5e0de867fb" 850 + dependencies = [ 851 + "jni", 852 + "ndk", 853 + "ndk-context", 854 + "num-derive", 855 + "num-traits", 856 + "oboe-sys", 857 + ] 858 + 859 + [[package]] 860 + name = "oboe-sys" 861 + version = "0.6.1" 862 + source = "registry+https://github.com/rust-lang/crates.io-index" 863 + checksum = "6c8bb09a4a2b1d668170cfe0a7d5bc103f8999fb316c98099b6a9939c9f2e79d" 864 + dependencies = [ 865 + "cc", 866 + ] 867 + 868 + [[package]] 869 + name = "ogg" 870 + version = "0.8.0" 871 + source = "registry+https://github.com/rust-lang/crates.io-index" 872 + checksum = "6951b4e8bf21c8193da321bcce9c9dd2e13c858fe078bf9054a288b419ae5d6e" 873 + dependencies = [ 874 + "byteorder", 875 + ] 876 + 877 + [[package]] 878 + name = "once_cell" 879 + version = "1.21.4" 880 + source = "registry+https://github.com/rust-lang/crates.io-index" 881 + checksum = "9f7c3e4beb33f85d45ae3e3a1792185706c8e16d043238c593331cc7cd313b50" 882 + 883 + [[package]] 884 + name = "once_cell_polyfill" 885 + version = "1.70.2" 886 + source = "registry+https://github.com/rust-lang/crates.io-index" 887 + checksum = "384b8ab6d37215f3c5301a95a4accb5d64aa607f1fcb26a11b5303878451b4fe" 888 + 889 + [[package]] 890 + name = "parking_lot" 891 + version = "0.12.5" 892 + source = "registry+https://github.com/rust-lang/crates.io-index" 893 + checksum = "93857453250e3077bd71ff98b6a65ea6621a19bb0f559a85248955ac12c45a1a" 894 + dependencies = [ 895 + "lock_api", 896 + "parking_lot_core", 897 + ] 898 + 899 + [[package]] 900 + name = "parking_lot_core" 901 + version = "0.9.12" 902 + source = "registry+https://github.com/rust-lang/crates.io-index" 903 + checksum = "2621685985a2ebf1c516881c026032ac7deafcda1a2c9b7850dc81e3dfcb64c1" 904 + dependencies = [ 905 + "cfg-if", 906 + "libc", 907 + "redox_syscall", 908 + "smallvec", 909 + "windows-link", 910 + ] 911 + 912 + [[package]] 913 + name = "paste" 914 + version = "1.0.15" 915 + source = "registry+https://github.com/rust-lang/crates.io-index" 916 + checksum = "57c0d7b74b563b49d38dae00a0c37d4d6de9b432382b2892f0574ddcae73fd0a" 917 + 918 + [[package]] 919 + name = "pin-project-lite" 920 + version = "0.2.17" 921 + source = "registry+https://github.com/rust-lang/crates.io-index" 922 + checksum = "a89322df9ebe1c1578d689c92318e070967d1042b512afbe49518723f4e6d5cd" 923 + 924 + [[package]] 925 + name = "pkg-config" 926 + version = "0.3.32" 927 + source = "registry+https://github.com/rust-lang/crates.io-index" 928 + checksum = "7edddbd0b52d732b21ad9a5fab5c704c14cd949e5e9a1ec5929a24fded1b904c" 929 + 930 + [[package]] 931 + name = "proc-macro-crate" 932 + version = "3.5.0" 933 + source = "registry+https://github.com/rust-lang/crates.io-index" 934 + checksum = "e67ba7e9b2b56446f1d419b1d807906278ffa1a658a8a5d8a39dcb1f5a78614f" 935 + dependencies = [ 936 + "toml_edit", 937 + ] 938 + 939 + [[package]] 940 + name = "proc-macro2" 941 + version = "1.0.106" 942 + source = "registry+https://github.com/rust-lang/crates.io-index" 943 + checksum = "8fd00f0bb2e90d81d1044c2b32617f68fcb9fa3bb7640c23e9c748e53fb30934" 944 + dependencies = [ 945 + "unicode-ident", 946 + ] 947 + 948 + [[package]] 949 + name = "quote" 950 + version = "1.0.45" 951 + source = "registry+https://github.com/rust-lang/crates.io-index" 952 + checksum = "41f2619966050689382d2b44f664f4bc593e129785a36d6ee376ddf37259b924" 953 + dependencies = [ 954 + "proc-macro2", 955 + ] 956 + 957 + [[package]] 958 + name = "r-efi" 959 + version = "5.3.0" 960 + source = "registry+https://github.com/rust-lang/crates.io-index" 961 + checksum = "69cdb34c158ceb288df11e18b4bd39de994f6657d83847bdffdbd7f346754b0f" 962 + 963 + [[package]] 964 + name = "ratatui" 965 + version = "0.26.3" 966 + source = "registry+https://github.com/rust-lang/crates.io-index" 967 + checksum = "f44c9e68fd46eda15c646fbb85e1040b657a58cdc8c98db1d97a55930d991eef" 968 + dependencies = [ 969 + "bitflags 2.11.0", 970 + "cassowary", 971 + "compact_str", 972 + "crossterm", 973 + "itertools 0.12.1", 974 + "lru", 975 + "paste", 976 + "stability", 977 + "strum", 978 + "unicode-segmentation", 979 + "unicode-truncate", 980 + "unicode-width", 981 + ] 982 + 983 + [[package]] 984 + name = "redox_syscall" 985 + version = "0.5.18" 986 + source = "registry+https://github.com/rust-lang/crates.io-index" 987 + checksum = "ed2bf2547551a7053d6fdfafda3f938979645c44812fbfcda098faae3f1a362d" 988 + dependencies = [ 989 + "bitflags 2.11.0", 990 + ] 991 + 992 + [[package]] 993 + name = "regex" 994 + version = "1.12.3" 995 + source = "registry+https://github.com/rust-lang/crates.io-index" 996 + checksum = "e10754a14b9137dd7b1e3e5b0493cc9171fdd105e0ab477f51b72e7f3ac0e276" 997 + dependencies = [ 998 + "aho-corasick", 999 + "memchr", 1000 + "regex-automata", 1001 + "regex-syntax", 1002 + ] 1003 + 1004 + [[package]] 1005 + name = "regex-automata" 1006 + version = "0.4.14" 1007 + source = "registry+https://github.com/rust-lang/crates.io-index" 1008 + checksum = "6e1dd4122fc1595e8162618945476892eefca7b88c52820e74af6262213cae8f" 1009 + dependencies = [ 1010 + "aho-corasick", 1011 + "memchr", 1012 + "regex-syntax", 1013 + ] 1014 + 1015 + [[package]] 1016 + name = "regex-syntax" 1017 + version = "0.8.10" 1018 + source = "registry+https://github.com/rust-lang/crates.io-index" 1019 + checksum = "dc897dd8d9e8bd1ed8cdad82b5966c3e0ecae09fb1907d58efaa013543185d0a" 1020 + 1021 + [[package]] 1022 + name = "rodio" 1023 + version = "0.17.3" 1024 + source = "registry+https://github.com/rust-lang/crates.io-index" 1025 + checksum = "3b1bb7b48ee48471f55da122c0044fcc7600cfcc85db88240b89cb832935e611" 1026 + dependencies = [ 1027 + "claxon", 1028 + "cpal", 1029 + "hound", 1030 + "lewton", 1031 + "symphonia", 1032 + ] 1033 + 1034 + [[package]] 1035 + name = "rust_voice" 1036 + version = "0.1.0" 1037 + dependencies = [ 1038 + "anyhow", 1039 + "chrono", 1040 + "clap", 1041 + "cpal", 1042 + "crossterm", 1043 + "mp3lame-encoder", 1044 + "ratatui", 1045 + "rodio", 1046 + ] 1047 + 1048 + [[package]] 1049 + name = "rustc-hash" 1050 + version = "2.1.2" 1051 + source = "registry+https://github.com/rust-lang/crates.io-index" 1052 + checksum = "94300abf3f1ae2e2b8ffb7b58043de3d399c73fa6f4b73826402a5c457614dbe" 1053 + 1054 + [[package]] 1055 + name = "rustversion" 1056 + version = "1.0.22" 1057 + source = "registry+https://github.com/rust-lang/crates.io-index" 1058 + checksum = "b39cdef0fa800fc44525c84ccb54a029961a8215f9619753635a9c0d2538d46d" 1059 + 1060 + [[package]] 1061 + name = "ryu" 1062 + version = "1.0.23" 1063 + source = "registry+https://github.com/rust-lang/crates.io-index" 1064 + checksum = "9774ba4a74de5f7b1c1451ed6cd5285a32eddb5cccb8cc655a4e50009e06477f" 1065 + 1066 + [[package]] 1067 + name = "same-file" 1068 + version = "1.0.6" 1069 + source = "registry+https://github.com/rust-lang/crates.io-index" 1070 + checksum = "93fc1dc3aaa9bfed95e02e6eadabb4baf7e3078b0bd1b4d7b6b0b68378900502" 1071 + dependencies = [ 1072 + "winapi-util", 1073 + ] 1074 + 1075 + [[package]] 1076 + name = "scopeguard" 1077 + version = "1.2.0" 1078 + source = "registry+https://github.com/rust-lang/crates.io-index" 1079 + checksum = "94143f37725109f92c262ed2cf5e59bce7498c01bcc1502d7b9afe439a4e9f49" 1080 + 1081 + [[package]] 1082 + name = "serde_core" 1083 + version = "1.0.228" 1084 + source = "registry+https://github.com/rust-lang/crates.io-index" 1085 + checksum = "41d385c7d4ca58e59fc732af25c3983b67ac852c1a25000afe1175de458b67ad" 1086 + dependencies = [ 1087 + "serde_derive", 1088 + ] 1089 + 1090 + [[package]] 1091 + name = "serde_derive" 1092 + version = "1.0.228" 1093 + source = "registry+https://github.com/rust-lang/crates.io-index" 1094 + checksum = "d540f220d3187173da220f885ab66608367b6574e925011a9353e4badda91d79" 1095 + dependencies = [ 1096 + "proc-macro2", 1097 + "quote", 1098 + "syn", 1099 + ] 1100 + 1101 + [[package]] 1102 + name = "shlex" 1103 + version = "1.3.0" 1104 + source = "registry+https://github.com/rust-lang/crates.io-index" 1105 + checksum = "0fda2ff0d084019ba4d7c6f371c95d8fd75ce3524c3cb8fb653a3023f6323e64" 1106 + 1107 + [[package]] 1108 + name = "signal-hook" 1109 + version = "0.3.18" 1110 + source = "registry+https://github.com/rust-lang/crates.io-index" 1111 + checksum = "d881a16cf4426aa584979d30bd82cb33429027e42122b169753d6ef1085ed6e2" 1112 + dependencies = [ 1113 + "libc", 1114 + "signal-hook-registry", 1115 + ] 1116 + 1117 + [[package]] 1118 + name = "signal-hook-mio" 1119 + version = "0.2.5" 1120 + source = "registry+https://github.com/rust-lang/crates.io-index" 1121 + checksum = "b75a19a7a740b25bc7944bdee6172368f988763b744e3d4dfe753f6b4ece40cc" 1122 + dependencies = [ 1123 + "libc", 1124 + "mio", 1125 + "signal-hook", 1126 + ] 1127 + 1128 + [[package]] 1129 + name = "signal-hook-registry" 1130 + version = "1.4.8" 1131 + source = "registry+https://github.com/rust-lang/crates.io-index" 1132 + checksum = "c4db69cba1110affc0e9f7bcd48bbf87b3f4fc7c61fc9155afd4c469eb3d6c1b" 1133 + dependencies = [ 1134 + "errno", 1135 + "libc", 1136 + ] 1137 + 1138 + [[package]] 1139 + name = "slab" 1140 + version = "0.4.12" 1141 + source = "registry+https://github.com/rust-lang/crates.io-index" 1142 + checksum = "0c790de23124f9ab44544d7ac05d60440adc586479ce501c1d6d7da3cd8c9cf5" 1143 + 1144 + [[package]] 1145 + name = "smallvec" 1146 + version = "1.15.1" 1147 + source = "registry+https://github.com/rust-lang/crates.io-index" 1148 + checksum = "67b1b7a3b5fe4f1376887184045fcf45c69e92af734b7aaddc05fb777b6fbd03" 1149 + 1150 + [[package]] 1151 + name = "stability" 1152 + version = "0.2.1" 1153 + source = "registry+https://github.com/rust-lang/crates.io-index" 1154 + checksum = "d904e7009df136af5297832a3ace3370cd14ff1546a232f4f185036c2736fcac" 1155 + dependencies = [ 1156 + "quote", 1157 + "syn", 1158 + ] 1159 + 1160 + [[package]] 1161 + name = "static_assertions" 1162 + version = "1.1.0" 1163 + source = "registry+https://github.com/rust-lang/crates.io-index" 1164 + checksum = "a2eb9349b6444b326872e140eb1cf5e7c522154d69e7a0ffb0fb81c06b37543f" 1165 + 1166 + [[package]] 1167 + name = "strsim" 1168 + version = "0.11.1" 1169 + source = "registry+https://github.com/rust-lang/crates.io-index" 1170 + checksum = "7da8b5736845d9f2fcb837ea5d9e2628564b3b043a70948a3f0b778838c5fb4f" 1171 + 1172 + [[package]] 1173 + name = "strum" 1174 + version = "0.26.3" 1175 + source = "registry+https://github.com/rust-lang/crates.io-index" 1176 + checksum = "8fec0f0aef304996cf250b31b5a10dee7980c85da9d759361292b8bca5a18f06" 1177 + dependencies = [ 1178 + "strum_macros", 1179 + ] 1180 + 1181 + [[package]] 1182 + name = "strum_macros" 1183 + version = "0.26.4" 1184 + source = "registry+https://github.com/rust-lang/crates.io-index" 1185 + checksum = "4c6bee85a5a24955dc440386795aa378cd9cf82acd5f764469152d2270e581be" 1186 + dependencies = [ 1187 + "heck", 1188 + "proc-macro2", 1189 + "quote", 1190 + "rustversion", 1191 + "syn", 1192 + ] 1193 + 1194 + [[package]] 1195 + name = "symphonia" 1196 + version = "0.5.5" 1197 + source = "registry+https://github.com/rust-lang/crates.io-index" 1198 + checksum = "5773a4c030a19d9bfaa090f49746ff35c75dfddfa700df7a5939d5e076a57039" 1199 + dependencies = [ 1200 + "lazy_static", 1201 + "symphonia-bundle-mp3", 1202 + "symphonia-core", 1203 + "symphonia-metadata", 1204 + ] 1205 + 1206 + [[package]] 1207 + name = "symphonia-bundle-mp3" 1208 + version = "0.5.5" 1209 + source = "registry+https://github.com/rust-lang/crates.io-index" 1210 + checksum = "4872dd6bb56bf5eac799e3e957aa1981086c3e613b27e0ac23b176054f7c57ed" 1211 + dependencies = [ 1212 + "lazy_static", 1213 + "log", 1214 + "symphonia-core", 1215 + "symphonia-metadata", 1216 + ] 1217 + 1218 + [[package]] 1219 + name = "symphonia-core" 1220 + version = "0.5.5" 1221 + source = "registry+https://github.com/rust-lang/crates.io-index" 1222 + checksum = "ea00cc4f79b7f6bb7ff87eddc065a1066f3a43fe1875979056672c9ef948c2af" 1223 + dependencies = [ 1224 + "arrayvec", 1225 + "bitflags 1.3.2", 1226 + "bytemuck", 1227 + "lazy_static", 1228 + "log", 1229 + ] 1230 + 1231 + [[package]] 1232 + name = "symphonia-metadata" 1233 + version = "0.5.5" 1234 + source = "registry+https://github.com/rust-lang/crates.io-index" 1235 + checksum = "36306ff42b9ffe6e5afc99d49e121e0bd62fe79b9db7b9681d48e29fa19e6b16" 1236 + dependencies = [ 1237 + "encoding_rs", 1238 + "lazy_static", 1239 + "log", 1240 + "symphonia-core", 1241 + ] 1242 + 1243 + [[package]] 1244 + name = "syn" 1245 + version = "2.0.117" 1246 + source = "registry+https://github.com/rust-lang/crates.io-index" 1247 + checksum = "e665b8803e7b1d2a727f4023456bbbbe74da67099c585258af0ad9c5013b9b99" 1248 + dependencies = [ 1249 + "proc-macro2", 1250 + "quote", 1251 + "unicode-ident", 1252 + ] 1253 + 1254 + [[package]] 1255 + name = "thiserror" 1256 + version = "1.0.69" 1257 + source = "registry+https://github.com/rust-lang/crates.io-index" 1258 + checksum = "b6aaf5339b578ea85b50e080feb250a3e8ae8cfcdff9a461c9ec2904bc923f52" 1259 + dependencies = [ 1260 + "thiserror-impl", 1261 + ] 1262 + 1263 + [[package]] 1264 + name = "thiserror-impl" 1265 + version = "1.0.69" 1266 + source = "registry+https://github.com/rust-lang/crates.io-index" 1267 + checksum = "4fee6c4efc90059e10f81e6d42c60a18f76588c3d74cb83a0b242a2b6c7504c1" 1268 + dependencies = [ 1269 + "proc-macro2", 1270 + "quote", 1271 + "syn", 1272 + ] 1273 + 1274 + [[package]] 1275 + name = "tinyvec" 1276 + version = "1.11.0" 1277 + source = "registry+https://github.com/rust-lang/crates.io-index" 1278 + checksum = "3e61e67053d25a4e82c844e8424039d9745781b3fc4f32b8d55ed50f5f667ef3" 1279 + dependencies = [ 1280 + "tinyvec_macros", 1281 + ] 1282 + 1283 + [[package]] 1284 + name = "tinyvec_macros" 1285 + version = "0.1.1" 1286 + source = "registry+https://github.com/rust-lang/crates.io-index" 1287 + checksum = "1f3ccbac311fea05f86f61904b462b55fb3df8837a366dfc601a0161d0532f20" 1288 + 1289 + [[package]] 1290 + name = "toml_datetime" 1291 + version = "1.1.1+spec-1.1.0" 1292 + source = "registry+https://github.com/rust-lang/crates.io-index" 1293 + checksum = "3165f65f62e28e0115a00b2ebdd37eb6f3b641855f9d636d3cd4103767159ad7" 1294 + dependencies = [ 1295 + "serde_core", 1296 + ] 1297 + 1298 + [[package]] 1299 + name = "toml_edit" 1300 + version = "0.25.9+spec-1.1.0" 1301 + source = "registry+https://github.com/rust-lang/crates.io-index" 1302 + checksum = "da053d28fe57e2c9d21b48261e14e7b4c8b670b54d2c684847b91feaf4c7dac5" 1303 + dependencies = [ 1304 + "indexmap", 1305 + "toml_datetime", 1306 + "toml_parser", 1307 + "winnow", 1308 + ] 1309 + 1310 + [[package]] 1311 + name = "toml_parser" 1312 + version = "1.1.1+spec-1.1.0" 1313 + source = "registry+https://github.com/rust-lang/crates.io-index" 1314 + checksum = "39ca317ebc49f06bd748bfba29533eac9485569dc9bf80b849024b025e814fb9" 1315 + dependencies = [ 1316 + "winnow", 1317 + ] 1318 + 1319 + [[package]] 1320 + name = "unicode-ident" 1321 + version = "1.0.24" 1322 + source = "registry+https://github.com/rust-lang/crates.io-index" 1323 + checksum = "e6e4313cd5fcd3dad5cafa179702e2b244f760991f45397d14d4ebf38247da75" 1324 + 1325 + [[package]] 1326 + name = "unicode-segmentation" 1327 + version = "1.13.2" 1328 + source = "registry+https://github.com/rust-lang/crates.io-index" 1329 + checksum = "9629274872b2bfaf8d66f5f15725007f635594914870f65218920345aa11aa8c" 1330 + 1331 + [[package]] 1332 + name = "unicode-truncate" 1333 + version = "1.1.0" 1334 + source = "registry+https://github.com/rust-lang/crates.io-index" 1335 + checksum = "b3644627a5af5fa321c95b9b235a72fd24cd29c648c2c379431e6628655627bf" 1336 + dependencies = [ 1337 + "itertools 0.13.0", 1338 + "unicode-segmentation", 1339 + "unicode-width", 1340 + ] 1341 + 1342 + [[package]] 1343 + name = "unicode-width" 1344 + version = "0.1.14" 1345 + source = "registry+https://github.com/rust-lang/crates.io-index" 1346 + checksum = "7dd6e30e90baa6f72411720665d41d89b9a3d039dc45b8faea1ddd07f617f6af" 1347 + 1348 + [[package]] 1349 + name = "utf8parse" 1350 + version = "0.2.2" 1351 + source = "registry+https://github.com/rust-lang/crates.io-index" 1352 + checksum = "06abde3611657adf66d383f00b093d7faecc7fa57071cce2578660c9f1010821" 1353 + 1354 + [[package]] 1355 + name = "walkdir" 1356 + version = "2.5.0" 1357 + source = "registry+https://github.com/rust-lang/crates.io-index" 1358 + checksum = "29790946404f91d9c5d06f9874efddea1dc06c5efe94541a7d6863108e3a5e4b" 1359 + dependencies = [ 1360 + "same-file", 1361 + "winapi-util", 1362 + ] 1363 + 1364 + [[package]] 1365 + name = "wasi" 1366 + version = "0.11.1+wasi-snapshot-preview1" 1367 + source = "registry+https://github.com/rust-lang/crates.io-index" 1368 + checksum = "ccf3ec651a847eb01de73ccad15eb7d99f80485de043efb2f370cd654f4ea44b" 1369 + 1370 + [[package]] 1371 + name = "wasip2" 1372 + version = "1.0.2+wasi-0.2.9" 1373 + source = "registry+https://github.com/rust-lang/crates.io-index" 1374 + checksum = "9517f9239f02c069db75e65f174b3da828fe5f5b945c4dd26bd25d89c03ebcf5" 1375 + dependencies = [ 1376 + "wit-bindgen", 1377 + ] 1378 + 1379 + [[package]] 1380 + name = "wasm-bindgen" 1381 + version = "0.2.116" 1382 + source = "registry+https://github.com/rust-lang/crates.io-index" 1383 + checksum = "7dc0882f7b5bb01ae8c5215a1230832694481c1a4be062fd410e12ea3da5b631" 1384 + dependencies = [ 1385 + "cfg-if", 1386 + "once_cell", 1387 + "rustversion", 1388 + "wasm-bindgen-macro", 1389 + "wasm-bindgen-shared", 1390 + ] 1391 + 1392 + [[package]] 1393 + name = "wasm-bindgen-futures" 1394 + version = "0.4.66" 1395 + source = "registry+https://github.com/rust-lang/crates.io-index" 1396 + checksum = "19280959e2844181895ef62f065c63e0ca07ece4771b53d89bfdb967d97cbf05" 1397 + dependencies = [ 1398 + "js-sys", 1399 + "wasm-bindgen", 1400 + ] 1401 + 1402 + [[package]] 1403 + name = "wasm-bindgen-macro" 1404 + version = "0.2.116" 1405 + source = "registry+https://github.com/rust-lang/crates.io-index" 1406 + checksum = "75973d3066e01d035dbedaad2864c398df42f8dd7b1ea057c35b8407c015b537" 1407 + dependencies = [ 1408 + "quote", 1409 + "wasm-bindgen-macro-support", 1410 + ] 1411 + 1412 + [[package]] 1413 + name = "wasm-bindgen-macro-support" 1414 + version = "0.2.116" 1415 + source = "registry+https://github.com/rust-lang/crates.io-index" 1416 + checksum = "91af5e4be765819e0bcfee7322c14374dc821e35e72fa663a830bbc7dc199eac" 1417 + dependencies = [ 1418 + "bumpalo", 1419 + "proc-macro2", 1420 + "quote", 1421 + "syn", 1422 + "wasm-bindgen-shared", 1423 + ] 1424 + 1425 + [[package]] 1426 + name = "wasm-bindgen-shared" 1427 + version = "0.2.116" 1428 + source = "registry+https://github.com/rust-lang/crates.io-index" 1429 + checksum = "c9bf0406a78f02f336bf1e451799cca198e8acde4ffa278f0fb20487b150a633" 1430 + dependencies = [ 1431 + "unicode-ident", 1432 + ] 1433 + 1434 + [[package]] 1435 + name = "web-sys" 1436 + version = "0.3.93" 1437 + source = "registry+https://github.com/rust-lang/crates.io-index" 1438 + checksum = "749466a37ee189057f54748b200186b59a03417a117267baf3fd89cecc9fb837" 1439 + dependencies = [ 1440 + "js-sys", 1441 + "wasm-bindgen", 1442 + ] 1443 + 1444 + [[package]] 1445 + name = "winapi" 1446 + version = "0.3.9" 1447 + source = "registry+https://github.com/rust-lang/crates.io-index" 1448 + checksum = "5c839a674fcd7a98952e593242ea400abe93992746761e38641405d28b00f419" 1449 + dependencies = [ 1450 + "winapi-i686-pc-windows-gnu", 1451 + "winapi-x86_64-pc-windows-gnu", 1452 + ] 1453 + 1454 + [[package]] 1455 + name = "winapi-i686-pc-windows-gnu" 1456 + version = "0.4.0" 1457 + source = "registry+https://github.com/rust-lang/crates.io-index" 1458 + checksum = "ac3b87c63620426dd9b991e5ce0329eff545bccbbb34f3be09ff6fb6ab51b7b6" 1459 + 1460 + [[package]] 1461 + name = "winapi-util" 1462 + version = "0.1.11" 1463 + source = "registry+https://github.com/rust-lang/crates.io-index" 1464 + checksum = "c2a7b1c03c876122aa43f3020e6c3c3ee5c05081c9a00739faf7503aeba10d22" 1465 + dependencies = [ 1466 + "windows-sys 0.61.2", 1467 + ] 1468 + 1469 + [[package]] 1470 + name = "winapi-x86_64-pc-windows-gnu" 1471 + version = "0.4.0" 1472 + source = "registry+https://github.com/rust-lang/crates.io-index" 1473 + checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f" 1474 + 1475 + [[package]] 1476 + name = "windows" 1477 + version = "0.54.0" 1478 + source = "registry+https://github.com/rust-lang/crates.io-index" 1479 + checksum = "9252e5725dbed82865af151df558e754e4a3c2c30818359eb17465f1346a1b49" 1480 + dependencies = [ 1481 + "windows-core 0.54.0", 1482 + "windows-targets 0.52.6", 1483 + ] 1484 + 1485 + [[package]] 1486 + name = "windows-core" 1487 + version = "0.54.0" 1488 + source = "registry+https://github.com/rust-lang/crates.io-index" 1489 + checksum = "12661b9c89351d684a50a8a643ce5f608e20243b9fb84687800163429f161d65" 1490 + dependencies = [ 1491 + "windows-result 0.1.2", 1492 + "windows-targets 0.52.6", 1493 + ] 1494 + 1495 + [[package]] 1496 + name = "windows-core" 1497 + version = "0.62.2" 1498 + source = "registry+https://github.com/rust-lang/crates.io-index" 1499 + checksum = "b8e83a14d34d0623b51dce9581199302a221863196a1dde71a7663a4c2be9deb" 1500 + dependencies = [ 1501 + "windows-implement", 1502 + "windows-interface", 1503 + "windows-link", 1504 + "windows-result 0.4.1", 1505 + "windows-strings", 1506 + ] 1507 + 1508 + [[package]] 1509 + name = "windows-implement" 1510 + version = "0.60.2" 1511 + source = "registry+https://github.com/rust-lang/crates.io-index" 1512 + checksum = "053e2e040ab57b9dc951b72c264860db7eb3b0200ba345b4e4c3b14f67855ddf" 1513 + dependencies = [ 1514 + "proc-macro2", 1515 + "quote", 1516 + "syn", 1517 + ] 1518 + 1519 + [[package]] 1520 + name = "windows-interface" 1521 + version = "0.59.3" 1522 + source = "registry+https://github.com/rust-lang/crates.io-index" 1523 + checksum = "3f316c4a2570ba26bbec722032c4099d8c8bc095efccdc15688708623367e358" 1524 + dependencies = [ 1525 + "proc-macro2", 1526 + "quote", 1527 + "syn", 1528 + ] 1529 + 1530 + [[package]] 1531 + name = "windows-link" 1532 + version = "0.2.1" 1533 + source = "registry+https://github.com/rust-lang/crates.io-index" 1534 + checksum = "f0805222e57f7521d6a62e36fa9163bc891acd422f971defe97d64e70d0a4fe5" 1535 + 1536 + [[package]] 1537 + name = "windows-result" 1538 + version = "0.1.2" 1539 + source = "registry+https://github.com/rust-lang/crates.io-index" 1540 + checksum = "5e383302e8ec8515204254685643de10811af0ed97ea37210dc26fb0032647f8" 1541 + dependencies = [ 1542 + "windows-targets 0.52.6", 1543 + ] 1544 + 1545 + [[package]] 1546 + name = "windows-result" 1547 + version = "0.4.1" 1548 + source = "registry+https://github.com/rust-lang/crates.io-index" 1549 + checksum = "7781fa89eaf60850ac3d2da7af8e5242a5ea78d1a11c49bf2910bb5a73853eb5" 1550 + dependencies = [ 1551 + "windows-link", 1552 + ] 1553 + 1554 + [[package]] 1555 + name = "windows-strings" 1556 + version = "0.5.1" 1557 + source = "registry+https://github.com/rust-lang/crates.io-index" 1558 + checksum = "7837d08f69c77cf6b07689544538e017c1bfcf57e34b4c0ff58e6c2cd3b37091" 1559 + dependencies = [ 1560 + "windows-link", 1561 + ] 1562 + 1563 + [[package]] 1564 + name = "windows-sys" 1565 + version = "0.45.0" 1566 + source = "registry+https://github.com/rust-lang/crates.io-index" 1567 + checksum = "75283be5efb2831d37ea142365f009c02ec203cd29a3ebecbc093d52315b66d0" 1568 + dependencies = [ 1569 + "windows-targets 0.42.2", 1570 + ] 1571 + 1572 + [[package]] 1573 + name = "windows-sys" 1574 + version = "0.48.0" 1575 + source = "registry+https://github.com/rust-lang/crates.io-index" 1576 + checksum = "677d2418bec65e3338edb076e806bc1ec15693c5d0104683f2efe857f61056a9" 1577 + dependencies = [ 1578 + "windows-targets 0.48.5", 1579 + ] 1580 + 1581 + [[package]] 1582 + name = "windows-sys" 1583 + version = "0.61.2" 1584 + source = "registry+https://github.com/rust-lang/crates.io-index" 1585 + checksum = "ae137229bcbd6cdf0f7b80a31df61766145077ddf49416a728b02cb3921ff3fc" 1586 + dependencies = [ 1587 + "windows-link", 1588 + ] 1589 + 1590 + [[package]] 1591 + name = "windows-targets" 1592 + version = "0.42.2" 1593 + source = "registry+https://github.com/rust-lang/crates.io-index" 1594 + checksum = "8e5180c00cd44c9b1c88adb3693291f1cd93605ded80c250a75d472756b4d071" 1595 + dependencies = [ 1596 + "windows_aarch64_gnullvm 0.42.2", 1597 + "windows_aarch64_msvc 0.42.2", 1598 + "windows_i686_gnu 0.42.2", 1599 + "windows_i686_msvc 0.42.2", 1600 + "windows_x86_64_gnu 0.42.2", 1601 + "windows_x86_64_gnullvm 0.42.2", 1602 + "windows_x86_64_msvc 0.42.2", 1603 + ] 1604 + 1605 + [[package]] 1606 + name = "windows-targets" 1607 + version = "0.48.5" 1608 + source = "registry+https://github.com/rust-lang/crates.io-index" 1609 + checksum = "9a2fa6e2155d7247be68c096456083145c183cbbbc2764150dda45a87197940c" 1610 + dependencies = [ 1611 + "windows_aarch64_gnullvm 0.48.5", 1612 + "windows_aarch64_msvc 0.48.5", 1613 + "windows_i686_gnu 0.48.5", 1614 + "windows_i686_msvc 0.48.5", 1615 + "windows_x86_64_gnu 0.48.5", 1616 + "windows_x86_64_gnullvm 0.48.5", 1617 + "windows_x86_64_msvc 0.48.5", 1618 + ] 1619 + 1620 + [[package]] 1621 + name = "windows-targets" 1622 + version = "0.52.6" 1623 + source = "registry+https://github.com/rust-lang/crates.io-index" 1624 + checksum = "9b724f72796e036ab90c1021d4780d4d3d648aca59e491e6b98e725b84e99973" 1625 + dependencies = [ 1626 + "windows_aarch64_gnullvm 0.52.6", 1627 + "windows_aarch64_msvc 0.52.6", 1628 + "windows_i686_gnu 0.52.6", 1629 + "windows_i686_gnullvm", 1630 + "windows_i686_msvc 0.52.6", 1631 + "windows_x86_64_gnu 0.52.6", 1632 + "windows_x86_64_gnullvm 0.52.6", 1633 + "windows_x86_64_msvc 0.52.6", 1634 + ] 1635 + 1636 + [[package]] 1637 + name = "windows_aarch64_gnullvm" 1638 + version = "0.42.2" 1639 + source = "registry+https://github.com/rust-lang/crates.io-index" 1640 + checksum = "597a5118570b68bc08d8d59125332c54f1ba9d9adeedeef5b99b02ba2b0698f8" 1641 + 1642 + [[package]] 1643 + name = "windows_aarch64_gnullvm" 1644 + version = "0.48.5" 1645 + source = "registry+https://github.com/rust-lang/crates.io-index" 1646 + checksum = "2b38e32f0abccf9987a4e3079dfb67dcd799fb61361e53e2882c3cbaf0d905d8" 1647 + 1648 + [[package]] 1649 + name = "windows_aarch64_gnullvm" 1650 + version = "0.52.6" 1651 + source = "registry+https://github.com/rust-lang/crates.io-index" 1652 + checksum = "32a4622180e7a0ec044bb555404c800bc9fd9ec262ec147edd5989ccd0c02cd3" 1653 + 1654 + [[package]] 1655 + name = "windows_aarch64_msvc" 1656 + version = "0.42.2" 1657 + source = "registry+https://github.com/rust-lang/crates.io-index" 1658 + checksum = "e08e8864a60f06ef0d0ff4ba04124db8b0fb3be5776a5cd47641e942e58c4d43" 1659 + 1660 + [[package]] 1661 + name = "windows_aarch64_msvc" 1662 + version = "0.48.5" 1663 + source = "registry+https://github.com/rust-lang/crates.io-index" 1664 + checksum = "dc35310971f3b2dbbf3f0690a219f40e2d9afcf64f9ab7cc1be722937c26b4bc" 1665 + 1666 + [[package]] 1667 + name = "windows_aarch64_msvc" 1668 + version = "0.52.6" 1669 + source = "registry+https://github.com/rust-lang/crates.io-index" 1670 + checksum = "09ec2a7bb152e2252b53fa7803150007879548bc709c039df7627cabbd05d469" 1671 + 1672 + [[package]] 1673 + name = "windows_i686_gnu" 1674 + version = "0.42.2" 1675 + source = "registry+https://github.com/rust-lang/crates.io-index" 1676 + checksum = "c61d927d8da41da96a81f029489353e68739737d3beca43145c8afec9a31a84f" 1677 + 1678 + [[package]] 1679 + name = "windows_i686_gnu" 1680 + version = "0.48.5" 1681 + source = "registry+https://github.com/rust-lang/crates.io-index" 1682 + checksum = "a75915e7def60c94dcef72200b9a8e58e5091744960da64ec734a6c6e9b3743e" 1683 + 1684 + [[package]] 1685 + name = "windows_i686_gnu" 1686 + version = "0.52.6" 1687 + source = "registry+https://github.com/rust-lang/crates.io-index" 1688 + checksum = "8e9b5ad5ab802e97eb8e295ac6720e509ee4c243f69d781394014ebfe8bbfa0b" 1689 + 1690 + [[package]] 1691 + name = "windows_i686_gnullvm" 1692 + version = "0.52.6" 1693 + source = "registry+https://github.com/rust-lang/crates.io-index" 1694 + checksum = "0eee52d38c090b3caa76c563b86c3a4bd71ef1a819287c19d586d7334ae8ed66" 1695 + 1696 + [[package]] 1697 + name = "windows_i686_msvc" 1698 + version = "0.42.2" 1699 + source = "registry+https://github.com/rust-lang/crates.io-index" 1700 + checksum = "44d840b6ec649f480a41c8d80f9c65108b92d89345dd94027bfe06ac444d1060" 1701 + 1702 + [[package]] 1703 + name = "windows_i686_msvc" 1704 + version = "0.48.5" 1705 + source = "registry+https://github.com/rust-lang/crates.io-index" 1706 + checksum = "8f55c233f70c4b27f66c523580f78f1004e8b5a8b659e05a4eb49d4166cca406" 1707 + 1708 + [[package]] 1709 + name = "windows_i686_msvc" 1710 + version = "0.52.6" 1711 + source = "registry+https://github.com/rust-lang/crates.io-index" 1712 + checksum = "240948bc05c5e7c6dabba28bf89d89ffce3e303022809e73deaefe4f6ec56c66" 1713 + 1714 + [[package]] 1715 + name = "windows_x86_64_gnu" 1716 + version = "0.42.2" 1717 + source = "registry+https://github.com/rust-lang/crates.io-index" 1718 + checksum = "8de912b8b8feb55c064867cf047dda097f92d51efad5b491dfb98f6bbb70cb36" 1719 + 1720 + [[package]] 1721 + name = "windows_x86_64_gnu" 1722 + version = "0.48.5" 1723 + source = "registry+https://github.com/rust-lang/crates.io-index" 1724 + checksum = "53d40abd2583d23e4718fddf1ebec84dbff8381c07cae67ff7768bbf19c6718e" 1725 + 1726 + [[package]] 1727 + name = "windows_x86_64_gnu" 1728 + version = "0.52.6" 1729 + source = "registry+https://github.com/rust-lang/crates.io-index" 1730 + checksum = "147a5c80aabfbf0c7d901cb5895d1de30ef2907eb21fbbab29ca94c5b08b1a78" 1731 + 1732 + [[package]] 1733 + name = "windows_x86_64_gnullvm" 1734 + version = "0.42.2" 1735 + source = "registry+https://github.com/rust-lang/crates.io-index" 1736 + checksum = "26d41b46a36d453748aedef1486d5c7a85db22e56aff34643984ea85514e94a3" 1737 + 1738 + [[package]] 1739 + name = "windows_x86_64_gnullvm" 1740 + version = "0.48.5" 1741 + source = "registry+https://github.com/rust-lang/crates.io-index" 1742 + checksum = "0b7b52767868a23d5bab768e390dc5f5c55825b6d30b86c844ff2dc7414044cc" 1743 + 1744 + [[package]] 1745 + name = "windows_x86_64_gnullvm" 1746 + version = "0.52.6" 1747 + source = "registry+https://github.com/rust-lang/crates.io-index" 1748 + checksum = "24d5b23dc417412679681396f2b49f3de8c1473deb516bd34410872eff51ed0d" 1749 + 1750 + [[package]] 1751 + name = "windows_x86_64_msvc" 1752 + version = "0.42.2" 1753 + source = "registry+https://github.com/rust-lang/crates.io-index" 1754 + checksum = "9aec5da331524158c6d1a4ac0ab1541149c0b9505fde06423b02f5ef0106b9f0" 1755 + 1756 + [[package]] 1757 + name = "windows_x86_64_msvc" 1758 + version = "0.48.5" 1759 + source = "registry+https://github.com/rust-lang/crates.io-index" 1760 + checksum = "ed94fce61571a4006852b7389a063ab983c02eb1bb37b47f8272ce92d06d9538" 1761 + 1762 + [[package]] 1763 + name = "windows_x86_64_msvc" 1764 + version = "0.52.6" 1765 + source = "registry+https://github.com/rust-lang/crates.io-index" 1766 + checksum = "589f6da84c646204747d1270a2a5661ea66ed1cced2631d546fdfb155959f9ec" 1767 + 1768 + [[package]] 1769 + name = "winnow" 1770 + version = "1.0.1" 1771 + source = "registry+https://github.com/rust-lang/crates.io-index" 1772 + checksum = "09dac053f1cd375980747450bfc7250c264eaae0583872e845c0c7cd578872b5" 1773 + dependencies = [ 1774 + "memchr", 1775 + ] 1776 + 1777 + [[package]] 1778 + name = "wit-bindgen" 1779 + version = "0.51.0" 1780 + source = "registry+https://github.com/rust-lang/crates.io-index" 1781 + checksum = "d7249219f66ced02969388cf2bb044a09756a083d0fab1e566056b04d9fbcaa5"
+18
Cargo.toml
··· 1 + [package] 2 + name = "rust_voice" 3 + version = "0.1.0" 4 + edition = "2024" 5 + 6 + [[bin]] 7 + name = "rv" 8 + path = "src/main.rs" 9 + 10 + [dependencies] 11 + cpal = "0.15" 12 + mp3lame-encoder = "0.2" 13 + rodio = { version = "0.17", features = ["symphonia-mp3"] } 14 + ratatui = "0.26" 15 + crossterm = "0.27" 16 + clap = { version = "4", features = ["derive"] } 17 + chrono = "0.4" 18 + anyhow = "1"
README.md

This is a binary file and will not be displayed.

+24
src/main.rs
··· 1 + mod player; 2 + mod recorder; 3 + mod storage; 4 + mod ui; 5 + 6 + use anyhow::Result; 7 + 8 + fn main() -> Result<()> { 9 + if let Err(e) = storage::get_voice_notes_dir() { 10 + eprintln!("Error: {e}"); 11 + std::process::exit(1); 12 + } 13 + 14 + let mut terminal = ui::setup_terminal()?; 15 + let result = ui::app::run(&mut terminal); 16 + ui::restore_terminal(&mut terminal)?; 17 + 18 + match result? { 19 + Some(path) => println!("Last saved: {}", path.display()), 20 + None => {} 21 + } 22 + 23 + Ok(()) 24 + }
+49
src/player.rs
··· 1 + use anyhow::Result; 2 + use rodio::{Decoder, OutputStream, Sink}; 3 + use std::fs::File; 4 + use std::io::BufReader; 5 + use std::path::Path; 6 + 7 + /// Controls an active playback session. 8 + pub struct Playback { 9 + // _stream must be kept alive for audio output to work. 10 + _stream: OutputStream, 11 + sink: Sink, 12 + } 13 + 14 + impl Playback { 15 + pub fn open(path: &Path) -> Result<Self> { 16 + let (stream, stream_handle) = OutputStream::try_default()?; 17 + let sink = Sink::try_new(&stream_handle)?; 18 + 19 + let file = BufReader::new(File::open(path)?); 20 + let source = Decoder::new(file)?; 21 + sink.append(source); 22 + 23 + Ok(Playback { 24 + _stream: stream, 25 + sink, 26 + }) 27 + } 28 + 29 + pub fn is_done(&self) -> bool { 30 + self.sink.empty() 31 + } 32 + 33 + pub fn toggle_pause(&self) { 34 + if self.sink.is_paused() { 35 + self.sink.play(); 36 + } else { 37 + self.sink.pause(); 38 + } 39 + } 40 + 41 + pub fn is_paused(&self) -> bool { 42 + self.sink.is_paused() 43 + } 44 + 45 + pub fn stop(self) { 46 + self.sink.stop(); 47 + } 48 + 49 + }
+229
src/recorder.rs
··· 1 + use anyhow::{anyhow, Result}; 2 + use cpal::traits::{DeviceTrait, HostTrait, StreamTrait}; 3 + use mp3lame_encoder::{Builder, DualPcm, FlushNoGap, MonoPcm}; 4 + use std::io::Write; 5 + use std::path::PathBuf; 6 + use std::sync::{ 7 + atomic::{AtomicU32, AtomicU64, Ordering}, 8 + mpsc, Arc, 9 + }; 10 + use std::thread; 11 + 12 + /// Shared state between the recorder and the TUI. 13 + pub struct RecordState { 14 + /// Current audio RMS level, stored as f32 bits in a u32. 15 + pub level: Arc<AtomicU32>, 16 + /// Total milliseconds of audio recorded. 17 + pub duration_ms: Arc<AtomicU64>, 18 + } 19 + 20 + pub struct Recorder { 21 + // The stream must be kept alive for audio capture to continue. 22 + _stream: cpal::Stream, 23 + // Dropping this sender closes the channel and signals the encoder thread to finish. 24 + _audio_tx: mpsc::SyncSender<Vec<f32>>, 25 + encoder_handle: Option<thread::JoinHandle<Result<()>>>, 26 + pub state: RecordState, 27 + pub path: PathBuf, 28 + } 29 + 30 + impl Recorder { 31 + pub fn start(path: PathBuf) -> Result<Self> { 32 + let host = cpal::default_host(); 33 + let device = host 34 + .default_input_device() 35 + .ok_or_else(|| anyhow!("No input audio device found"))?; 36 + 37 + let supported_config = device.default_input_config()?; 38 + let channels = supported_config.channels(); 39 + let sample_rate = supported_config.sample_rate().0; 40 + 41 + let (audio_tx, audio_rx) = mpsc::sync_channel::<Vec<f32>>(256); 42 + 43 + let level = Arc::new(AtomicU32::new(0)); 44 + let duration_ms = Arc::new(AtomicU64::new(0)); 45 + 46 + let state = RecordState { 47 + level: Arc::clone(&level), 48 + duration_ms: Arc::clone(&duration_ms), 49 + }; 50 + 51 + let stream = build_input_stream(&device, &supported_config, audio_tx.clone())?; 52 + stream.play()?; 53 + 54 + let path_clone = path.clone(); 55 + let encoder_handle = thread::spawn(move || { 56 + run_encoder( 57 + audio_rx, 58 + path_clone, 59 + channels, 60 + sample_rate, 61 + level, 62 + duration_ms, 63 + ) 64 + }); 65 + 66 + Ok(Recorder { 67 + _stream: stream, 68 + _audio_tx: audio_tx, 69 + encoder_handle: Some(encoder_handle), 70 + state, 71 + path, 72 + }) 73 + } 74 + 75 + pub fn level(&self) -> f32 { 76 + f32::from_bits(self.state.level.load(Ordering::Relaxed)) 77 + } 78 + 79 + pub fn duration_ms(&self) -> u64 { 80 + self.state.duration_ms.load(Ordering::Relaxed) 81 + } 82 + 83 + /// Stop recording and flush the MP3 encoder. Returns the path of the saved file. 84 + pub fn stop(mut self) -> Result<PathBuf> { 85 + // Dropping _stream stops audio capture; dropping _audio_tx closes the channel. 86 + // Both happen automatically when self is consumed, but we need the encoder to 87 + // finish first. Drive it by taking and joining the handle after dropping the senders. 88 + drop(self._stream); 89 + drop(self._audio_tx); 90 + 91 + if let Some(handle) = self.encoder_handle.take() { 92 + handle 93 + .join() 94 + .map_err(|_| anyhow!("Encoder thread panicked"))??; 95 + } 96 + 97 + Ok(self.path) 98 + } 99 + } 100 + 101 + fn build_input_stream( 102 + device: &cpal::Device, 103 + config: &cpal::SupportedStreamConfig, 104 + tx: mpsc::SyncSender<Vec<f32>>, 105 + ) -> Result<cpal::Stream> { 106 + let stream_config: cpal::StreamConfig = config.clone().into(); 107 + 108 + let stream = match config.sample_format() { 109 + cpal::SampleFormat::F32 => device.build_input_stream( 110 + &stream_config, 111 + move |data: &[f32], _: &cpal::InputCallbackInfo| { 112 + let _ = tx.try_send(data.to_vec()); 113 + }, 114 + |err| eprintln!("Audio stream error: {err}"), 115 + None, 116 + )?, 117 + cpal::SampleFormat::I16 => { 118 + device.build_input_stream( 119 + &stream_config, 120 + move |data: &[i16], _: &cpal::InputCallbackInfo| { 121 + let converted: Vec<f32> = data 122 + .iter() 123 + .map(|&s| s as f32 / i16::MAX as f32) 124 + .collect(); 125 + let _ = tx.try_send(converted); 126 + }, 127 + |err| eprintln!("Audio stream error: {err}"), 128 + None, 129 + )? 130 + } 131 + cpal::SampleFormat::U16 => { 132 + device.build_input_stream( 133 + &stream_config, 134 + move |data: &[u16], _: &cpal::InputCallbackInfo| { 135 + let converted: Vec<f32> = data 136 + .iter() 137 + .map(|&s| (s as f32 / u16::MAX as f32) * 2.0 - 1.0) 138 + .collect(); 139 + let _ = tx.try_send(converted); 140 + }, 141 + |err| eprintln!("Audio stream error: {err}"), 142 + None, 143 + )? 144 + } 145 + fmt => return Err(anyhow!("Unsupported sample format: {fmt:?}")), 146 + }; 147 + 148 + Ok(stream) 149 + } 150 + 151 + fn run_encoder( 152 + rx: mpsc::Receiver<Vec<f32>>, 153 + path: PathBuf, 154 + channels: u16, 155 + sample_rate: u32, 156 + level: Arc<AtomicU32>, 157 + duration_ms: Arc<AtomicU64>, 158 + ) -> Result<()> { 159 + let lame_channels: u8 = if channels == 1 { 1 } else { 2 }; 160 + 161 + let mut builder = 162 + Builder::new().ok_or_else(|| anyhow!("Failed to allocate LAME encoder"))?; 163 + builder 164 + .set_num_channels(lame_channels) 165 + .map_err(|e| anyhow!("set_num_channels: {e}"))?; 166 + builder 167 + .set_sample_rate(sample_rate) 168 + .map_err(|e| anyhow!("set_sample_rate: {e}"))?; 169 + builder 170 + .set_brate(mp3lame_encoder::Bitrate::Kbps128) 171 + .map_err(|e| anyhow!("set_brate: {e}"))?; 172 + builder 173 + .set_quality(mp3lame_encoder::Quality::Good) 174 + .map_err(|e| anyhow!("set_quality: {e}"))?; 175 + let mut encoder = builder 176 + .build() 177 + .map_err(|e| anyhow!("Failed to build MP3 encoder: {e}"))?; 178 + 179 + let mut file = std::fs::File::create(&path)?; 180 + let mut out_buf: Vec<u8> = Vec::new(); 181 + 182 + for samples in &rx { 183 + level.store(compute_rms(&samples).to_bits(), Ordering::Relaxed); 184 + 185 + // Reserve output buffer space. 186 + let frame_samples = samples.len() / channels as usize; 187 + out_buf.reserve(mp3lame_encoder::max_required_buffer_size(frame_samples)); 188 + 189 + if lame_channels == 1 { 190 + // Mono: pass samples directly as f32. 191 + encoder 192 + .encode_to_vec(MonoPcm(&samples), &mut out_buf) 193 + .map_err(|e| anyhow!("MP3 encode error: {e}"))?; 194 + 195 + let frame_ms = (samples.len() as f64 / sample_rate as f64 * 1000.0).round() as u64; 196 + duration_ms.fetch_add(frame_ms, Ordering::Relaxed); 197 + } else { 198 + // Stereo: de-interleave f32 data into left/right vecs. 199 + let left: Vec<f32> = samples.chunks(2).map(|ch| ch[0]).collect(); 200 + let right: Vec<f32> = samples.chunks(2).map(|ch| ch[1]).collect(); 201 + 202 + encoder 203 + .encode_to_vec(DualPcm { left: &left, right: &right }, &mut out_buf) 204 + .map_err(|e| anyhow!("MP3 encode error: {e}"))?; 205 + 206 + let frame_ms = (left.len() as f64 / sample_rate as f64 * 1000.0).round() as u64; 207 + duration_ms.fetch_add(frame_ms, Ordering::Relaxed); 208 + } 209 + 210 + file.write_all(&out_buf)?; 211 + out_buf.clear(); 212 + } 213 + 214 + out_buf.reserve(7200); 215 + encoder 216 + .flush_to_vec::<FlushNoGap>(&mut out_buf) 217 + .map_err(|e| anyhow!("MP3 flush error: {e}"))?; 218 + file.write_all(&out_buf)?; 219 + 220 + Ok(()) 221 + } 222 + 223 + fn compute_rms(samples: &[f32]) -> f32 { 224 + if samples.is_empty() { 225 + return 0.0; 226 + } 227 + let sum_sq: f32 = samples.iter().map(|&s| s * s).sum(); 228 + (sum_sq / samples.len() as f32).sqrt() 229 + }
+86
src/storage.rs
··· 1 + use anyhow::{anyhow, Result}; 2 + use chrono::Local; 3 + use std::fs; 4 + use std::path::PathBuf; 5 + 6 + pub fn get_voice_notes_dir() -> Result<PathBuf> { 7 + let dir = std::env::var("VOICE_NOTES") 8 + .map_err(|_| anyhow!("$VOICE_NOTES environment variable is not set.\nSet it with: export VOICE_NOTES=/path/to/notes"))?; 9 + Ok(PathBuf::from(dir)) 10 + } 11 + 12 + pub fn get_todays_dir() -> Result<PathBuf> { 13 + let base = get_voice_notes_dir()?; 14 + let today = Local::now().format("%Y-%m-%d").to_string(); 15 + let dir = base.join(today); 16 + fs::create_dir_all(&dir)?; 17 + Ok(dir) 18 + } 19 + 20 + /// Determines the next recording path: $VOICE_NOTES/YYYY-MM-DD/HH-MM-SS_note-N.mp3 21 + pub fn next_recording_path() -> Result<PathBuf> { 22 + let dir = get_todays_dir()?; 23 + 24 + let count = fs::read_dir(&dir)? 25 + .filter_map(|e| e.ok()) 26 + .filter(|e| { 27 + e.path() 28 + .extension() 29 + .and_then(|ext| ext.to_str()) 30 + .map(|ext| ext.eq_ignore_ascii_case("mp3")) 31 + .unwrap_or(false) 32 + }) 33 + .count(); 34 + 35 + let filename = format!("practice-{}.mp3", count + 1); 36 + 37 + Ok(dir.join(filename)) 38 + } 39 + 40 + /// Returns (date_label, sorted_mp3_paths) pairs, newest dates first. 41 + pub fn list_recordings() -> Result<Vec<(String, Vec<PathBuf>)>> { 42 + let base = get_voice_notes_dir()?; 43 + 44 + if !base.exists() { 45 + return Ok(vec![]); 46 + } 47 + 48 + let mut entries: Vec<_> = fs::read_dir(&base)? 49 + .filter_map(|e| e.ok()) 50 + .filter(|e| e.file_type().map(|t| t.is_dir()).unwrap_or(false)) 51 + .collect(); 52 + 53 + // Newest dates first (reverse lexicographic on YYYY-MM-DD) 54 + entries.sort_by(|a, b| b.file_name().cmp(&a.file_name())); 55 + 56 + let mut result = Vec::new(); 57 + 58 + for entry in entries { 59 + let date = entry.file_name().to_string_lossy().to_string(); 60 + 61 + let mut files: Vec<PathBuf> = fs::read_dir(entry.path())? 62 + .filter_map(|e| e.ok()) 63 + .filter(|e| { 64 + e.path() 65 + .extension() 66 + .and_then(|ext| ext.to_str()) 67 + .map(|ext| ext.eq_ignore_ascii_case("mp3")) 68 + .unwrap_or(false) 69 + }) 70 + .map(|e| e.path()) 71 + .collect(); 72 + 73 + files.sort_by(|a, b| { 74 + let mt = |p: &std::path::PathBuf| { 75 + std::fs::metadata(p).and_then(|m| m.modified()).ok() 76 + }; 77 + mt(b).cmp(&mt(a)) // newest modified time first 78 + }); 79 + 80 + if !files.is_empty() { 81 + result.push((date, files)); 82 + } 83 + } 84 + 85 + Ok(result) 86 + }
+889
src/ui/app.rs
··· 1 + use crate::{player::Playback, recorder::Recorder, storage}; 2 + use anyhow::Result; 3 + use crossterm::event::{self, Event, KeyCode, KeyEventKind}; 4 + use ratatui::{ 5 + layout::{Constraint, Direction, Layout}, 6 + style::{Color, Modifier, Style}, 7 + text::{Line, Span}, 8 + widgets::{Block, Borders, Gauge, List, ListItem, ListState, Paragraph, Sparkline}, 9 + Frame, 10 + }; 11 + use std::collections::VecDeque; 12 + use std::path::PathBuf; 13 + use std::time::Duration; 14 + 15 + // ── Data ───────────────────────────────────────────────────────────────────── 16 + 17 + enum ListEntry { 18 + Header(String), 19 + File(PathBuf), 20 + } 21 + 22 + enum Mode { 23 + Idle, 24 + Recording { 25 + recorder: Recorder, 26 + level_history: VecDeque<u64>, 27 + tick: u64, 28 + }, 29 + Playing { 30 + playback: Playback, 31 + filename: String, 32 + }, 33 + Renaming { 34 + /// Characters in the name being edited (without extension). 35 + chars: Vec<char>, 36 + /// Cursor position (0..=chars.len()). 37 + cursor: usize, 38 + /// The file being renamed. 39 + target: PathBuf, 40 + }, 41 + Deleting { 42 + target: PathBuf, 43 + }, 44 + } 45 + 46 + pub struct App { 47 + entries: Vec<ListEntry>, 48 + list_state: ListState, 49 + mode: Mode, 50 + /// Set after the first `g` keypress to detect `gg`. 51 + pending_g: bool, 52 + /// One-line status notification shown below the panels. 53 + notification: Option<String>, 54 + pub should_quit: bool, 55 + /// Path of the last saved recording, returned to main after quit. 56 + pub saved_path: Option<PathBuf>, 57 + } 58 + 59 + // ── Constructor & data loading ──────────────────────────────────────────────── 60 + 61 + impl App { 62 + pub fn new() -> Result<Self> { 63 + let mut app = App { 64 + entries: Vec::new(), 65 + list_state: ListState::default(), 66 + mode: Mode::Idle, 67 + pending_g: false, 68 + notification: None, 69 + should_quit: false, 70 + saved_path: None, 71 + }; 72 + app.reload_entries()?; 73 + Ok(app) 74 + } 75 + 76 + fn reload_entries(&mut self) -> Result<()> { 77 + self.entries.clear(); 78 + let recordings = storage::list_recordings()?; 79 + for (date, files) in &recordings { 80 + self.entries.push(ListEntry::Header(date.clone())); 81 + for f in files { 82 + self.entries.push(ListEntry::File(f.clone())); 83 + } 84 + } 85 + // Keep selection valid; move to first file if nothing selected. 86 + if self.list_state.selected().map_or(true, |i| i >= self.entries.len()) { 87 + let first = self.entries.iter().position(|e| matches!(e, ListEntry::File(_))); 88 + self.list_state.select(first); 89 + } 90 + Ok(()) 91 + } 92 + } 93 + 94 + // ── Tick (called every frame) ───────────────────────────────────────────────── 95 + 96 + impl App { 97 + pub fn tick(&mut self) { 98 + // Push the latest level sample into the waveform history. 99 + if let Mode::Recording { recorder, level_history, tick } = &mut self.mode { 100 + let bar = (recorder.level() * 6.0 * 64.0).min(64.0) as u64; 101 + level_history.push_back(bar); 102 + if level_history.len() > 60 { 103 + level_history.pop_front(); 104 + } 105 + *tick = tick.wrapping_add(1); 106 + } 107 + 108 + // Auto-return to Idle when playback finishes. 109 + let done = matches!(&self.mode, Mode::Playing { playback, .. } if playback.is_done()); 110 + if done { 111 + self.mode = Mode::Idle; 112 + self.notification = Some("Playback finished.".into()); 113 + } 114 + } 115 + } 116 + 117 + // ── Key handling ────────────────────────────────────────────────────────────── 118 + 119 + impl App { 120 + pub fn handle_key(&mut self, key: KeyCode) -> Result<()> { 121 + match &self.mode { 122 + // ── Renaming mode ───────────────────────────────────────────── 123 + Mode::Renaming { .. } => { 124 + match key { 125 + KeyCode::Char(c) => { 126 + if let Mode::Renaming { chars, cursor, .. } = &mut self.mode { 127 + chars.insert(*cursor, c); 128 + *cursor += 1; 129 + } 130 + } 131 + KeyCode::Backspace => { 132 + if let Mode::Renaming { chars, cursor, .. } = &mut self.mode { 133 + if *cursor > 0 { 134 + *cursor -= 1; 135 + chars.remove(*cursor); 136 + } 137 + } 138 + } 139 + KeyCode::Delete => { 140 + if let Mode::Renaming { chars, cursor, .. } = &mut self.mode { 141 + if *cursor < chars.len() { 142 + chars.remove(*cursor); 143 + } 144 + } 145 + } 146 + KeyCode::Left => { 147 + if let Mode::Renaming { cursor, .. } = &mut self.mode { 148 + *cursor = cursor.saturating_sub(1); 149 + } 150 + } 151 + KeyCode::Right => { 152 + if let Mode::Renaming { chars, cursor, .. } = &mut self.mode { 153 + *cursor = (*cursor + 1).min(chars.len()); 154 + } 155 + } 156 + KeyCode::Home => { 157 + if let Mode::Renaming { cursor, .. } = &mut self.mode { 158 + *cursor = 0; 159 + } 160 + } 161 + KeyCode::End => { 162 + if let Mode::Renaming { chars, cursor, .. } = &mut self.mode { 163 + *cursor = chars.len(); 164 + } 165 + } 166 + KeyCode::Enter => self.confirm_rename()?, 167 + KeyCode::Esc => { 168 + self.mode = Mode::Idle; 169 + } 170 + _ => {} 171 + } 172 + } 173 + 174 + // ── Deleting mode ───────────────────────────────────────────── 175 + Mode::Deleting { .. } => { 176 + match key { 177 + KeyCode::Char('y') | KeyCode::Char('Y') | KeyCode::Enter => { 178 + self.confirm_delete()?; 179 + } 180 + _ => { 181 + self.mode = Mode::Idle; 182 + self.notification = Some("Delete cancelled.".into()); 183 + } 184 + } 185 + } 186 + 187 + // ── Recording mode ──────────────────────────────────────────── 188 + Mode::Recording { .. } => { 189 + self.pending_g = false; 190 + match key { 191 + // Space / s / Enter → stop and save 192 + KeyCode::Char(' ') | KeyCode::Char('s') | KeyCode::Enter => { 193 + self.stop_recording(true)?; 194 + } 195 + // Esc → cancel (discard) 196 + KeyCode::Esc => { 197 + self.stop_recording(false)?; 198 + self.notification = Some("Recording cancelled.".into()); 199 + } 200 + // q → save and quit 201 + KeyCode::Char('q') => { 202 + self.stop_recording(true)?; 203 + self.should_quit = true; 204 + } 205 + _ => {} 206 + } 207 + } 208 + 209 + // ── Browsing / Playing mode ─────────────────────────────────── 210 + _ => { 211 + // Playback-specific controls 212 + if matches!(&self.mode, Mode::Playing { .. }) { 213 + match key { 214 + KeyCode::Char(' ') => { 215 + if let Mode::Playing { playback, .. } = &self.mode { 216 + playback.toggle_pause(); 217 + } 218 + self.pending_g = false; 219 + return Ok(()); 220 + } 221 + KeyCode::Char('s') | KeyCode::Esc => { 222 + self.stop_playing(); 223 + self.pending_g = false; 224 + return Ok(()); 225 + } 226 + _ => {} 227 + } 228 + } 229 + 230 + // Navigation and global bindings 231 + match key { 232 + KeyCode::Char('q') => { 233 + self.stop_playing(); 234 + self.should_quit = true; 235 + self.pending_g = false; 236 + } 237 + KeyCode::Char('j') | KeyCode::Down => { 238 + self.move_selection(1); 239 + self.pending_g = false; 240 + } 241 + KeyCode::Char('k') | KeyCode::Up => { 242 + self.move_selection(-1); 243 + self.pending_g = false; 244 + } 245 + KeyCode::Char('G') => { 246 + self.select_last(); 247 + self.pending_g = false; 248 + } 249 + KeyCode::Char('g') => { 250 + if self.pending_g { 251 + self.select_first(); 252 + self.pending_g = false; 253 + } else { 254 + self.pending_g = true; 255 + } 256 + } 257 + KeyCode::Enter | KeyCode::Char('o') => { 258 + self.pending_g = false; 259 + self.stop_playing(); 260 + self.start_playing()?; 261 + } 262 + KeyCode::Char(' ') => { 263 + self.pending_g = false; 264 + self.start_recording()?; 265 + } 266 + KeyCode::Char('r') => { 267 + self.pending_g = false; 268 + self.start_renaming(); 269 + } 270 + KeyCode::Char('d') => { 271 + self.pending_g = false; 272 + self.stop_playing(); 273 + self.start_deleting(); 274 + } 275 + _ => { 276 + self.pending_g = false; 277 + } 278 + } 279 + } 280 + } 281 + Ok(()) 282 + } 283 + } 284 + 285 + // ── Actions ─────────────────────────────────────────────────────────────────── 286 + 287 + impl App { 288 + fn start_recording(&mut self) -> Result<()> { 289 + let path = storage::next_recording_path()?; 290 + let recorder = Recorder::start(path)?; 291 + self.mode = Mode::Recording { 292 + recorder, 293 + level_history: VecDeque::new(), 294 + tick: 0, 295 + }; 296 + self.notification = None; 297 + Ok(()) 298 + } 299 + 300 + fn stop_recording(&mut self, save: bool) -> Result<()> { 301 + let prev = std::mem::replace(&mut self.mode, Mode::Idle); 302 + if let Mode::Recording { recorder, .. } = prev { 303 + let path = recorder.stop()?; 304 + if save { 305 + let name = path 306 + .file_name() 307 + .map(|n| n.to_string_lossy().to_string()) 308 + .unwrap_or_default(); 309 + self.notification = Some(format!("Saved: {}", name)); 310 + self.saved_path = Some(path.clone()); 311 + self.reload_entries()?; 312 + self.select_file(&path); 313 + } else { 314 + let _ = std::fs::remove_file(&path); 315 + } 316 + } 317 + Ok(()) 318 + } 319 + 320 + fn start_playing(&mut self) -> Result<()> { 321 + if let Some(idx) = self.list_state.selected() { 322 + if let Some(ListEntry::File(path)) = self.entries.get(idx) { 323 + let path = path.clone(); 324 + let filename = path 325 + .file_name() 326 + .map(|n| n.to_string_lossy().to_string()) 327 + .unwrap_or_default(); 328 + self.mode = Mode::Playing { 329 + playback: Playback::open(&path)?, 330 + filename, 331 + }; 332 + self.notification = None; 333 + } 334 + } 335 + Ok(()) 336 + } 337 + 338 + fn stop_playing(&mut self) { 339 + let prev = std::mem::replace(&mut self.mode, Mode::Idle); 340 + if let Mode::Playing { playback, .. } = prev { 341 + playback.stop(); 342 + } 343 + } 344 + 345 + fn start_renaming(&mut self) { 346 + if let Some(idx) = self.list_state.selected() { 347 + if let Some(ListEntry::File(path)) = self.entries.get(idx) { 348 + let stem = path 349 + .file_stem() 350 + .map(|s| s.to_string_lossy().to_string()) 351 + .unwrap_or_default(); 352 + let chars: Vec<char> = stem.chars().collect(); 353 + let cursor = chars.len(); 354 + self.mode = Mode::Renaming { 355 + chars, 356 + cursor, 357 + target: path.clone(), 358 + }; 359 + } 360 + } 361 + } 362 + 363 + fn start_deleting(&mut self) { 364 + if let Some(idx) = self.list_state.selected() { 365 + if let Some(ListEntry::File(path)) = self.entries.get(idx) { 366 + self.mode = Mode::Deleting { target: path.clone() }; 367 + } 368 + } 369 + } 370 + 371 + fn confirm_delete(&mut self) -> Result<()> { 372 + let prev = std::mem::replace(&mut self.mode, Mode::Idle); 373 + if let Mode::Deleting { target } = prev { 374 + let name = target 375 + .file_name() 376 + .map(|n| n.to_string_lossy().to_string()) 377 + .unwrap_or_default(); 378 + std::fs::remove_file(&target)?; 379 + self.notification = Some(format!("Deleted '{}'.", name)); 380 + self.reload_entries()?; 381 + // Selection is automatically clamped by reload_entries. 382 + } 383 + Ok(()) 384 + } 385 + 386 + fn confirm_rename(&mut self) -> Result<()> { 387 + let prev = std::mem::replace(&mut self.mode, Mode::Idle); 388 + if let Mode::Renaming { chars, target, .. } = prev { 389 + let name: String = chars.iter().collect(); 390 + if name.is_empty() { 391 + self.notification = Some("Rename cancelled — name was empty.".into()); 392 + return Ok(()); 393 + } 394 + let new_name = if name.ends_with(".mp3") { 395 + name 396 + } else { 397 + format!("{}.mp3", name) 398 + }; 399 + let new_path = target.with_file_name(&new_name); 400 + if new_path.exists() { 401 + self.notification = Some(format!("'{}' already exists.", new_name)); 402 + return Ok(()); 403 + } 404 + std::fs::rename(&target, &new_path)?; 405 + self.notification = Some(format!("Renamed to '{}'.", new_name)); 406 + self.reload_entries()?; 407 + self.select_file(&new_path); 408 + } 409 + Ok(()) 410 + } 411 + } 412 + 413 + // ── Selection helpers ───────────────────────────────────────────────────────── 414 + 415 + impl App { 416 + fn move_selection(&mut self, delta: i32) { 417 + let len = self.entries.len() as i32; 418 + if len == 0 { 419 + return; 420 + } 421 + let cur = self.list_state.selected().unwrap_or(0) as i32; 422 + let mut next = cur + delta; 423 + for _ in 0..len { 424 + if next < 0 { 425 + next = 0; 426 + break; 427 + } 428 + if next >= len { 429 + next = len - 1; 430 + break; 431 + } 432 + if matches!(self.entries[next as usize], ListEntry::File(_)) { 433 + break; 434 + } 435 + next += delta; 436 + } 437 + let next = next.clamp(0, len - 1) as usize; 438 + if matches!(self.entries[next], ListEntry::File(_)) { 439 + self.list_state.select(Some(next)); 440 + } 441 + } 442 + 443 + fn select_first(&mut self) { 444 + if let Some(i) = self.entries.iter().position(|e| matches!(e, ListEntry::File(_))) { 445 + self.list_state.select(Some(i)); 446 + } 447 + } 448 + 449 + fn select_last(&mut self) { 450 + if let Some(i) = self.entries.iter().rposition(|e| matches!(e, ListEntry::File(_))) { 451 + self.list_state.select(Some(i)); 452 + } 453 + } 454 + 455 + fn select_file(&mut self, target: &PathBuf) { 456 + if let Some(i) = self.entries.iter().position(|e| { 457 + matches!(e, ListEntry::File(p) if p == target) 458 + }) { 459 + self.list_state.select(Some(i)); 460 + } 461 + } 462 + } 463 + 464 + // ── Drawing ─────────────────────────────────────────────────────────────────── 465 + 466 + impl App { 467 + pub fn draw(&mut self, f: &mut Frame<'_>) { 468 + let area = f.size(); 469 + 470 + // Outer layout: [left panel | right panel] + notification bar 471 + let rows = Layout::default() 472 + .direction(Direction::Vertical) 473 + .constraints([Constraint::Min(0), Constraint::Length(1)]) 474 + .split(area); 475 + 476 + let cols = Layout::default() 477 + .direction(Direction::Horizontal) 478 + .constraints([Constraint::Percentage(42), Constraint::Percentage(58)]) 479 + .split(rows[0]); 480 + 481 + self.draw_file_list(f, cols[0]); 482 + self.draw_right_panel(f, cols[1]); 483 + self.draw_status_bar(f, rows[1]); 484 + } 485 + 486 + fn draw_file_list(&mut self, f: &mut Frame<'_>, area: ratatui::layout::Rect) { 487 + let is_browsing = !matches!(self.mode, Mode::Recording { .. }); 488 + 489 + let items: Vec<ListItem> = self 490 + .entries 491 + .iter() 492 + .map(|e| match e { 493 + ListEntry::Header(date) => ListItem::new(Line::from(Span::styled( 494 + format!(" {}/", date), 495 + Style::default().fg(Color::Cyan).add_modifier(Modifier::BOLD), 496 + ))), 497 + ListEntry::File(path) => { 498 + let name = path 499 + .file_name() 500 + .map(|n| n.to_string_lossy().to_string()) 501 + .unwrap_or_default(); 502 + ListItem::new(Line::from(Span::raw(format!(" {}", name)))) 503 + } 504 + }) 505 + .collect(); 506 + 507 + let title = if self.entries.is_empty() { 508 + " Voice Notes — no recordings yet " 509 + } else { 510 + " Voice Notes " 511 + }; 512 + 513 + let highlight_style = if is_browsing { 514 + Style::default() 515 + .fg(Color::Black) 516 + .bg(Color::Green) 517 + .add_modifier(Modifier::BOLD) 518 + } else { 519 + // Dim the selection when recording so focus is on the right panel. 520 + Style::default().fg(Color::DarkGray).bg(Color::DarkGray) 521 + }; 522 + 523 + let list = List::new(items) 524 + .block(Block::default().title(title).borders(Borders::ALL)) 525 + .highlight_style(highlight_style) 526 + .highlight_symbol("▶ "); 527 + 528 + f.render_stateful_widget(list, area, &mut self.list_state); 529 + } 530 + 531 + fn draw_right_panel(&self, f: &mut Frame<'_>, area: ratatui::layout::Rect) { 532 + match &self.mode { 533 + Mode::Idle => self.draw_idle_panel(f, area), 534 + Mode::Recording { recorder, level_history, tick } => { 535 + self.draw_recording_panel(f, area, recorder, level_history, *tick) 536 + } 537 + Mode::Playing { playback, filename } => { 538 + self.draw_playing_panel(f, area, playback, filename) 539 + } 540 + Mode::Renaming { chars, cursor, target } => { 541 + self.draw_rename_panel(f, area, chars, *cursor, target) 542 + } 543 + Mode::Deleting { target } => self.draw_delete_panel(f, area, target), 544 + } 545 + } 546 + 547 + fn draw_idle_panel(&self, f: &mut Frame<'_>, area: ratatui::layout::Rect) { 548 + let chunks = Layout::default() 549 + .direction(Direction::Vertical) 550 + .constraints([Constraint::Min(0), Constraint::Length(8)]) 551 + .split(area); 552 + 553 + // Empty top area with border 554 + let top = Paragraph::new("").block( 555 + Block::default() 556 + .title(" Ready ") 557 + .borders(Borders::ALL) 558 + .style(Style::default().fg(Color::DarkGray)), 559 + ); 560 + f.render_widget(top, chunks[0]); 561 + 562 + // Help panel at the bottom of the right side 563 + let help_lines = vec![ 564 + Line::from(vec![ 565 + Span::styled(" Space", Style::default().fg(Color::Green).add_modifier(Modifier::BOLD)), 566 + Span::raw(" start recording"), 567 + ]), 568 + Line::from(vec![ 569 + Span::styled(" r", Style::default().fg(Color::Green).add_modifier(Modifier::BOLD)), 570 + Span::raw(" rename selected"), 571 + ]), 572 + Line::from(vec![ 573 + Span::styled(" d", Style::default().fg(Color::Red).add_modifier(Modifier::BOLD)), 574 + Span::raw(" delete selected"), 575 + ]), 576 + Line::from(vec![ 577 + Span::styled(" Enter / o", Style::default().fg(Color::Green).add_modifier(Modifier::BOLD)), 578 + Span::raw(" play selected"), 579 + ]), 580 + Line::from(vec![ 581 + Span::styled(" j / k", Style::default().fg(Color::Cyan)), 582 + Span::raw(" navigate"), 583 + ]), 584 + Line::from(vec![ 585 + Span::styled(" gg / G", Style::default().fg(Color::Cyan)), 586 + Span::raw(" first / last"), 587 + ]), 588 + ]; 589 + let help = Paragraph::new(help_lines).block( 590 + Block::default() 591 + .title(" Keys ") 592 + .borders(Borders::ALL) 593 + .style(Style::default().fg(Color::DarkGray)), 594 + ); 595 + f.render_widget(help, chunks[1]); 596 + } 597 + 598 + fn draw_recording_panel( 599 + &self, 600 + f: &mut Frame<'_>, 601 + area: ratatui::layout::Rect, 602 + recorder: &Recorder, 603 + level_history: &VecDeque<u64>, 604 + tick: u64, 605 + ) { 606 + let chunks = Layout::default() 607 + .direction(Direction::Vertical) 608 + .margin(0) 609 + .constraints([ 610 + Constraint::Length(3), // status + duration 611 + Constraint::Length(3), // waveform sparkline 612 + Constraint::Length(3), // level gauge 613 + Constraint::Min(1), // file path 614 + Constraint::Length(2), // controls 615 + ]) 616 + .split(area); 617 + 618 + let duration_ms = recorder.duration_ms(); 619 + let secs = duration_ms / 1000; 620 + let duration_str = format!("{:02}:{:02}", secs / 60, secs % 60); 621 + let blink_on = (tick / 6) % 2 == 0; 622 + let dot = if blink_on { "● REC" } else { " REC" }; 623 + 624 + // ── Status ──────────────────────────────────────────────────────── 625 + let status = Paragraph::new(Line::from(vec![ 626 + Span::styled(dot, Style::default().fg(Color::Red).add_modifier(Modifier::BOLD)), 627 + Span::raw(format!(" {}", duration_str)), 628 + ])) 629 + .block(Block::default().title(" Recording ").borders(Borders::ALL)); 630 + f.render_widget(status, chunks[0]); 631 + 632 + // ── Waveform ────────────────────────────────────────────────────── 633 + let sparkdata: Vec<u64> = level_history.iter().cloned().collect(); 634 + let sparkline = Sparkline::default() 635 + .block(Block::default().borders(Borders::LEFT | Borders::RIGHT | Borders::BOTTOM)) 636 + .style(Style::default().fg(Color::Green)) 637 + .data(&sparkdata) 638 + .max(64); 639 + f.render_widget(sparkline, chunks[1]); 640 + 641 + // ── Level gauge ─────────────────────────────────────────────────── 642 + let raw_level = recorder.level(); 643 + let display_level = (raw_level * 6.0).min(1.0) as f64; 644 + let gauge_color = if display_level > 0.85 { 645 + Color::Red 646 + } else if display_level > 0.55 { 647 + Color::Yellow 648 + } else { 649 + Color::Green 650 + }; 651 + let gauge = Gauge::default() 652 + .block( 653 + Block::default() 654 + .title(" Level ") 655 + .borders(Borders::LEFT | Borders::RIGHT | Borders::BOTTOM), 656 + ) 657 + .gauge_style(Style::default().fg(gauge_color).bg(Color::DarkGray)) 658 + .ratio(display_level); 659 + f.render_widget(gauge, chunks[2]); 660 + 661 + // ── File path ───────────────────────────────────────────────────── 662 + let path_str = recorder 663 + .path 664 + .to_string_lossy() 665 + .replace(&std::env::var("HOME").unwrap_or_default(), "~"); 666 + let path_widget = Paragraph::new(Line::from(vec![ 667 + Span::styled(" → ", Style::default().fg(Color::DarkGray)), 668 + Span::styled(path_str, Style::default().fg(Color::DarkGray)), 669 + ])); 670 + f.render_widget(path_widget, chunks[3]); 671 + 672 + // ── Controls ────────────────────────────────────────────────────── 673 + let controls = Paragraph::new(Line::from(vec![ 674 + Span::styled(" [Space/s/Enter]", Style::default().fg(Color::Green)), 675 + Span::raw(" Save "), 676 + Span::styled("[Esc]", Style::default().fg(Color::Red)), 677 + Span::raw(" Cancel"), 678 + ])); 679 + f.render_widget(controls, chunks[4]); 680 + } 681 + 682 + fn draw_playing_panel( 683 + &self, 684 + f: &mut Frame<'_>, 685 + area: ratatui::layout::Rect, 686 + playback: &Playback, 687 + filename: &str, 688 + ) { 689 + let chunks = Layout::default() 690 + .direction(Direction::Vertical) 691 + .constraints([Constraint::Length(5), Constraint::Min(0), Constraint::Length(2)]) 692 + .split(area); 693 + 694 + let paused = playback.is_paused(); 695 + let (icon, color) = if paused { 696 + ("⏸ PAUSED", Color::Yellow) 697 + } else { 698 + ("▶ PLAYING", Color::Green) 699 + }; 700 + 701 + let status_lines = vec![ 702 + Line::from(vec![Span::styled( 703 + icon, 704 + Style::default().fg(color).add_modifier(Modifier::BOLD), 705 + )]), 706 + Line::from(""), 707 + Line::from(vec![Span::styled( 708 + format!(" {}", filename), 709 + Style::default().fg(Color::White), 710 + )]), 711 + ]; 712 + let status = Paragraph::new(status_lines) 713 + .block(Block::default().title(" Now Playing ").borders(Borders::ALL)); 714 + f.render_widget(status, chunks[0]); 715 + 716 + let controls = Paragraph::new(Line::from(vec![ 717 + Span::styled(" [Space]", Style::default().fg(Color::Cyan)), 718 + Span::raw(" Pause "), 719 + Span::styled("[s / Esc]", Style::default().fg(Color::Red)), 720 + Span::raw(" Stop"), 721 + ])); 722 + f.render_widget(controls, chunks[2]); 723 + } 724 + 725 + fn draw_rename_panel( 726 + &self, 727 + f: &mut Frame<'_>, 728 + area: ratatui::layout::Rect, 729 + chars: &[char], 730 + cursor: usize, 731 + target: &PathBuf, 732 + ) { 733 + let chunks = Layout::default() 734 + .direction(Direction::Vertical) 735 + .constraints([Constraint::Length(5), Constraint::Min(0), Constraint::Length(2)]) 736 + .split(area); 737 + 738 + let old_name = target 739 + .file_name() 740 + .map(|n| n.to_string_lossy().to_string()) 741 + .unwrap_or_default(); 742 + 743 + // Render the editable name with a block cursor. 744 + let before: String = chars[..cursor].iter().collect(); 745 + let (at_char, after): (String, String) = if cursor < chars.len() { 746 + ( 747 + chars[cursor].to_string(), 748 + chars[cursor + 1..].iter().collect(), 749 + ) 750 + } else { 751 + (" ".to_string(), String::new()) 752 + }; 753 + 754 + let input_line = Line::from(vec![ 755 + Span::raw(" "), 756 + Span::raw(before), 757 + Span::styled(at_char, Style::default().add_modifier(Modifier::REVERSED)), 758 + Span::raw(after), 759 + ]); 760 + 761 + let panel_lines = vec![ 762 + Line::from(vec![ 763 + Span::styled(" Old: ", Style::default().fg(Color::DarkGray)), 764 + Span::styled(&old_name, Style::default().fg(Color::DarkGray)), 765 + ]), 766 + Line::from(""), 767 + input_line, 768 + ]; 769 + 770 + let panel = Paragraph::new(panel_lines) 771 + .block(Block::default().title(" Rename ").borders(Borders::ALL)); 772 + f.render_widget(panel, chunks[0]); 773 + 774 + let controls = Paragraph::new(Line::from(vec![ 775 + Span::styled(" [Enter]", Style::default().fg(Color::Green)), 776 + Span::raw(" Confirm "), 777 + Span::styled("[Esc]", Style::default().fg(Color::Red)), 778 + Span::raw(" Cancel "), 779 + Span::styled("[←/→]", Style::default().fg(Color::Cyan)), 780 + Span::raw(" Move cursor"), 781 + ])); 782 + f.render_widget(controls, chunks[2]); 783 + } 784 + 785 + fn draw_delete_panel(&self, f: &mut Frame<'_>, area: ratatui::layout::Rect, target: &PathBuf) { 786 + let name = target 787 + .file_name() 788 + .map(|n| n.to_string_lossy().to_string()) 789 + .unwrap_or_default(); 790 + 791 + let lines = vec![ 792 + Line::from(""), 793 + Line::from(vec![ 794 + Span::raw(" Delete "), 795 + Span::styled(&name, Style::default().fg(Color::Red).add_modifier(Modifier::BOLD)), 796 + Span::raw(" ?"), 797 + ]), 798 + Line::from(""), 799 + Line::from(vec![ 800 + Span::styled(" [y / Enter]", Style::default().fg(Color::Red).add_modifier(Modifier::BOLD)), 801 + Span::raw(" Yes, delete it"), 802 + ]), 803 + Line::from(vec![ 804 + Span::styled(" [any other key]", Style::default().fg(Color::Green)), 805 + Span::raw(" Cancel"), 806 + ]), 807 + ]; 808 + 809 + let panel = Paragraph::new(lines).block( 810 + Block::default() 811 + .title(" Confirm Delete ") 812 + .borders(Borders::ALL) 813 + .style(Style::default().fg(Color::Red)), 814 + ); 815 + f.render_widget(panel, area); 816 + } 817 + 818 + fn draw_status_bar(&self, f: &mut Frame<'_>, area: ratatui::layout::Rect) { 819 + let text = match &self.mode { 820 + Mode::Idle => { 821 + if let Some(msg) = &self.notification { 822 + Line::from(Span::styled( 823 + format!(" {}", msg), 824 + Style::default().fg(Color::Yellow), 825 + )) 826 + } else { 827 + Line::from(vec![ 828 + Span::styled(" Space", Style::default().fg(Color::Green)), 829 + Span::raw(" Record "), 830 + Span::styled("r", Style::default().fg(Color::Green)), 831 + Span::raw(" Rename "), 832 + Span::styled("d", Style::default().fg(Color::Red)), 833 + Span::raw(" Delete "), 834 + Span::styled("j/k", Style::default().fg(Color::Cyan)), 835 + Span::raw(" Navigate "), 836 + Span::styled("Enter", Style::default().fg(Color::Cyan)), 837 + Span::raw(" Play "), 838 + Span::styled("gg/G", Style::default().fg(Color::Cyan)), 839 + Span::raw(" Top/Bot "), 840 + Span::styled("q", Style::default().fg(Color::Red)), 841 + Span::raw(" Quit"), 842 + ]) 843 + } 844 + } 845 + Mode::Recording { .. } => Line::from(Span::styled( 846 + " Recording — Space/s/Enter: save Esc: cancel q: save+quit", 847 + Style::default().fg(Color::Red), 848 + )), 849 + Mode::Playing { .. } => Line::from(Span::styled( 850 + " Playing — Space: pause s/Esc: stop q: quit", 851 + Style::default().fg(Color::Green), 852 + )), 853 + Mode::Renaming { .. } => Line::from(Span::styled( 854 + " Renaming — type new name Enter: confirm Esc: cancel ←/→: move cursor", 855 + Style::default().fg(Color::Yellow), 856 + )), 857 + Mode::Deleting { .. } => Line::from(Span::styled( 858 + " Delete? — y/Enter: confirm any other key: cancel", 859 + Style::default().fg(Color::Red), 860 + )), 861 + }; 862 + f.render_widget(Paragraph::new(text), area); 863 + } 864 + } 865 + 866 + // ── Main event loop ─────────────────────────────────────────────────────────── 867 + 868 + pub fn run(terminal: &mut super::Term) -> Result<Option<PathBuf>> { 869 + let mut app = App::new()?; 870 + 871 + loop { 872 + app.tick(); 873 + terminal.draw(|f| app.draw(f))?; 874 + 875 + if event::poll(Duration::from_millis(50))? { 876 + if let Event::Key(key) = event::read()? { 877 + if key.kind != KeyEventKind::Press { 878 + continue; 879 + } 880 + app.handle_key(key.code)?; 881 + if app.should_quit { 882 + break; 883 + } 884 + } 885 + } 886 + } 887 + 888 + Ok(app.saved_path) 889 + }
+26
src/ui/mod.rs
··· 1 + pub mod app; 2 + 3 + use anyhow::Result; 4 + use crossterm::{ 5 + execute, 6 + terminal::{disable_raw_mode, enable_raw_mode, EnterAlternateScreen, LeaveAlternateScreen}, 7 + }; 8 + use ratatui::{backend::CrosstermBackend, Terminal}; 9 + use std::io; 10 + 11 + pub type Term = Terminal<CrosstermBackend<io::Stdout>>; 12 + 13 + pub fn setup_terminal() -> Result<Term> { 14 + enable_raw_mode()?; 15 + let mut stdout = io::stdout(); 16 + execute!(stdout, EnterAlternateScreen)?; 17 + let backend = CrosstermBackend::new(stdout); 18 + Ok(Terminal::new(backend)?) 19 + } 20 + 21 + pub fn restore_terminal(terminal: &mut Term) -> Result<()> { 22 + disable_raw_mode()?; 23 + execute!(terminal.backend_mut(), LeaveAlternateScreen)?; 24 + terminal.show_cursor()?; 25 + Ok(()) 26 + }