├── .github └── workflows │ ├── build-docker.yaml │ ├── build.yaml │ ├── publish-crates.yaml │ └── test.yml ├── .gitignore ├── .rusty-hook.toml ├── Cargo.lock ├── Cargo.toml ├── Dockerfile ├── LICENSE ├── README.md ├── assets ├── demo.gif └── gate_low_res.jpg ├── docker-compose.yaml └── src ├── components ├── app.rs ├── helper │ ├── background.rs │ ├── image.rs │ ├── ja.rs │ ├── mod.rs │ └── rain.rs ├── home.rs ├── kana.rs └── mod.rs └── main.rs /.github/workflows/build-docker.yaml: -------------------------------------------------------------------------------- 1 | name: Build and push docker image to Docker Hub 2 | 3 | on: 4 | release: 5 | types: [published] 6 | 7 | jobs: 8 | push_to_registry: 9 | name: Push Docker image to Docker Hub 10 | runs-on: ubuntu-latest 11 | permissions: 12 | contents: read 13 | attestations: write 14 | id-token: write 15 | steps: 16 | - name: Check out the repo 17 | uses: actions/checkout@v4 18 | 19 | - name: Log in to Docker Hub 20 | uses: docker/login-action@f4ef78c080cd8ba55a85445d5b36e214a81df20a 21 | with: 22 | username: ${{ secrets.DOCKER_HUB_USERNAME }} 23 | password: ${{ secrets.DOCKER_HUB_TOKEN }} 24 | 25 | - name: Extract metadata (tags, labels) for Docker 26 | id: meta 27 | uses: docker/metadata-action@9ec57ed1fcdbf14dcef7dfbe97b2010124a938b7 28 | with: 29 | images: blximages/kanash 30 | 31 | - name: Build and push Docker image 32 | id: push 33 | uses: docker/build-push-action@3b5e8027fcad23fda98b2e3ac259d8d67585f671 34 | with: 35 | context: . 36 | file: ./Dockerfile 37 | push: true 38 | tags: ${{ steps.meta.outputs.tags }} 39 | labels: ${{ steps.meta.outputs.labels }} 40 | 41 | - name: Generate artifact attestation 42 | uses: actions/attest-build-provenance@v2 43 | with: 44 | subject-name: index.docker.io/blximages/kanash 45 | subject-digest: ${{ steps.push.outputs.digest }} 46 | push-to-registry: true 47 | -------------------------------------------------------------------------------- /.github/workflows/build.yaml: -------------------------------------------------------------------------------- 1 | name: Build and Release 2 | 3 | on: 4 | release: 5 | types: [published, released] 6 | 7 | permissions: 8 | contents: write 9 | 10 | env: 11 | CARGO_TERM_COLOR: always 12 | 13 | jobs: 14 | build: 15 | runs-on: ubuntu-latest 16 | strategy: 17 | matrix: 18 | toolchain: 19 | - stable 20 | steps: 21 | - uses: actions/checkout@v4 22 | - run: rustup update ${{ matrix.toolchain }} && rustup default ${{ matrix.toolchain }} 23 | - run: cargo build --verbose --release 24 | - name: Upload release artifact 25 | uses: actions/upload-artifact@v4 26 | with: 27 | name: kanash 28 | path: target/release/kanash 29 | 30 | release: 31 | needs: build 32 | runs-on: ubuntu-latest 33 | if: startsWith(github.ref, 'refs/tags/') 34 | steps: 35 | - name: Checkout 36 | uses: actions/checkout@v4 37 | 38 | - name: Download release artifact 39 | uses: actions/download-artifact@v4 40 | with: 41 | name: kanash 42 | path: . 43 | 44 | - name: Release 45 | uses: softprops/action-gh-release@v2 46 | with: 47 | files: | 48 | ./kanash 49 | -------------------------------------------------------------------------------- /.github/workflows/publish-crates.yaml: -------------------------------------------------------------------------------- 1 | 2 | name: Publish to crates.io 3 | 4 | on: 5 | release: 6 | types: [published] 7 | 8 | jobs: 9 | push_to_crates_dot_io: 10 | name: Publish to crates.io 11 | runs-on: ubuntu-latest 12 | permissions: 13 | contents: read 14 | attestations: write 15 | id-token: write 16 | steps: 17 | - uses: actions/checkout@v3 18 | - uses: actions-rs/toolchain@v1 19 | with: 20 | toolchain: stable 21 | override: true 22 | - uses: katyo/publish-crates@v2 23 | with: 24 | registry-token: ${{ secrets.CARGO_REGISTRY_TOKEN }} 25 | -------------------------------------------------------------------------------- /.github/workflows/test.yml: -------------------------------------------------------------------------------- 1 | name: Cargo Test and Format Check 2 | 3 | on: 4 | push: 5 | pull_request: 6 | 7 | env: 8 | CARGO_TERM_COLOR: always 9 | 10 | jobs: 11 | build_and_test: 12 | name: Rust project - latest 13 | runs-on: ubuntu-latest 14 | strategy: 15 | matrix: 16 | toolchain: 17 | - stable 18 | 19 | steps: 20 | - uses: actions/checkout@v4 21 | - run: rustup update ${{ matrix.toolchain }} && rustup default ${{ matrix.toolchain }} 22 | - run: cargo test --verbose 23 | - run: cargo fmt --check 24 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Generated by Cargo 2 | # will have compiled files and executables 3 | debug/ 4 | target/ 5 | 6 | # These are backup files generated by rustfmt 7 | **/*.rs.bk 8 | 9 | # MSVC Windows builds of rustc generate these, which store debugging information 10 | *.pdb 11 | 12 | # RustRover 13 | # JetBrains specific template is maintained in a separate JetBrains.gitignore that can 14 | # be found at https://github.com/github/gitignore/blob/main/Global/JetBrains.gitignore 15 | # and can be added to the global gitignore or merged into this file. For a more nuclear 16 | # option (not recommended) you can uncomment the following to ignore the entire idea folder. 17 | #.idea/ 18 | 19 | # Added by cargo 20 | 21 | /target 22 | 23 | # Manually added 24 | .ssh/ 25 | assets/* 26 | !assets/demo.gif 27 | !assets/gate_low_res.jpg 28 | demo.tape 29 | -------------------------------------------------------------------------------- /.rusty-hook.toml: -------------------------------------------------------------------------------- 1 | [hooks] 2 | pre-commit = "cargo test" 3 | pre-push = "cargo check && cargo fmt -- --check" 4 | 5 | [logging] 6 | verbose = true 7 | -------------------------------------------------------------------------------- /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 = "addr2line" 7 | version = "0.24.2" 8 | source = "registry+https://github.com/rust-lang/crates.io-index" 9 | checksum = "dfbe277e56a376000877090da837660b4427aad530e3028d44e0bffe4f89a1c1" 10 | dependencies = [ 11 | "gimli", 12 | ] 13 | 14 | [[package]] 15 | name = "adler2" 16 | version = "2.0.0" 17 | source = "registry+https://github.com/rust-lang/crates.io-index" 18 | checksum = "512761e0bb2578dd7380c6baaa0f4ce03e84f95e960231d1dec8bf4d7d6e2627" 19 | 20 | [[package]] 21 | name = "allocator-api2" 22 | version = "0.2.21" 23 | source = "registry+https://github.com/rust-lang/crates.io-index" 24 | checksum = "683d7910e743518b0e34f1186f92494becacb047c7b6bf616c96772180fef923" 25 | 26 | [[package]] 27 | name = "anpa" 28 | version = "0.9.0" 29 | source = "registry+https://github.com/rust-lang/crates.io-index" 30 | checksum = "dbd042758b51a8f57b0e4777d1d14a49db0c0a04d2b21ceae3f64d7a066384b1" 31 | 32 | [[package]] 33 | name = "ansi-to-tui" 34 | version = "7.0.0" 35 | source = "registry+https://github.com/rust-lang/crates.io-index" 36 | checksum = "67555e1f1ece39d737e28c8a017721287753af3f93225e4a445b29ccb0f5912c" 37 | dependencies = [ 38 | "nom", 39 | "ratatui", 40 | "simdutf8", 41 | "smallvec", 42 | "thiserror 1.0.69", 43 | ] 44 | 45 | [[package]] 46 | name = "ansi_term" 47 | version = "0.12.1" 48 | source = "registry+https://github.com/rust-lang/crates.io-index" 49 | checksum = "d52a9bb7ec0cf484c551830a7ce27bd20d67eac647e1befb56b0be4ee39a55d2" 50 | dependencies = [ 51 | "winapi", 52 | ] 53 | 54 | [[package]] 55 | name = "anstream" 56 | version = "0.6.18" 57 | source = "registry+https://github.com/rust-lang/crates.io-index" 58 | checksum = "8acc5369981196006228e28809f761875c0327210a891e941f4c683b3a99529b" 59 | dependencies = [ 60 | "anstyle", 61 | "anstyle-parse", 62 | "anstyle-query", 63 | "anstyle-wincon", 64 | "colorchoice", 65 | "is_terminal_polyfill", 66 | "utf8parse", 67 | ] 68 | 69 | [[package]] 70 | name = "anstyle" 71 | version = "1.0.10" 72 | source = "registry+https://github.com/rust-lang/crates.io-index" 73 | checksum = "55cc3b69f167a1ef2e161439aa98aed94e6028e5f9a59be9a6ffb47aef1651f9" 74 | 75 | [[package]] 76 | name = "anstyle-parse" 77 | version = "0.2.6" 78 | source = "registry+https://github.com/rust-lang/crates.io-index" 79 | checksum = "3b2d16507662817a6a20a9ea92df6652ee4f94f914589377d69f3b21bc5798a9" 80 | dependencies = [ 81 | "utf8parse", 82 | ] 83 | 84 | [[package]] 85 | name = "anstyle-query" 86 | version = "1.1.2" 87 | source = "registry+https://github.com/rust-lang/crates.io-index" 88 | checksum = "79947af37f4177cfead1110013d678905c37501914fba0efea834c3fe9a8d60c" 89 | dependencies = [ 90 | "windows-sys 0.59.0", 91 | ] 92 | 93 | [[package]] 94 | name = "anstyle-wincon" 95 | version = "3.0.7" 96 | source = "registry+https://github.com/rust-lang/crates.io-index" 97 | checksum = "ca3534e77181a9cc07539ad51f2141fe32f6c3ffd4df76db8ad92346b003ae4e" 98 | dependencies = [ 99 | "anstyle", 100 | "once_cell", 101 | "windows-sys 0.59.0", 102 | ] 103 | 104 | [[package]] 105 | name = "anyhow" 106 | version = "1.0.98" 107 | source = "registry+https://github.com/rust-lang/crates.io-index" 108 | checksum = "e16d2d3311acee920a9eb8d33b8cbc1787ce4a264e85f964c2404b969bdcd487" 109 | 110 | [[package]] 111 | name = "autocfg" 112 | version = "1.4.0" 113 | source = "registry+https://github.com/rust-lang/crates.io-index" 114 | checksum = "ace50bade8e6234aa140d9a2f552bbee1db4d353f69b8217bc503490fc1a9f26" 115 | 116 | [[package]] 117 | name = "backtrace" 118 | version = "0.3.74" 119 | source = "registry+https://github.com/rust-lang/crates.io-index" 120 | checksum = "8d82cb332cdfaed17ae235a638438ac4d4839913cc2af585c3c6746e8f8bee1a" 121 | dependencies = [ 122 | "addr2line", 123 | "cfg-if", 124 | "libc", 125 | "miniz_oxide", 126 | "object", 127 | "rustc-demangle", 128 | "windows-targets", 129 | ] 130 | 131 | [[package]] 132 | name = "bit_field" 133 | version = "0.10.2" 134 | source = "registry+https://github.com/rust-lang/crates.io-index" 135 | checksum = "dc827186963e592360843fb5ba4b973e145841266c1357f7180c43526f2e5b61" 136 | 137 | [[package]] 138 | name = "bitflags" 139 | version = "1.3.2" 140 | source = "registry+https://github.com/rust-lang/crates.io-index" 141 | checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a" 142 | 143 | [[package]] 144 | name = "bitflags" 145 | version = "2.9.0" 146 | source = "registry+https://github.com/rust-lang/crates.io-index" 147 | checksum = "5c8214115b7bf84099f1309324e63141d4c5d7cc26862f97a0a857dbefe165bd" 148 | 149 | [[package]] 150 | name = "bon" 151 | version = "3.6.1" 152 | source = "registry+https://github.com/rust-lang/crates.io-index" 153 | checksum = "94054366e2ff97b455acdd4fdb03913f717febc57b7bbd1741b2c3b87efae030" 154 | dependencies = [ 155 | "bon-macros", 156 | "rustversion", 157 | ] 158 | 159 | [[package]] 160 | name = "bon-macros" 161 | version = "3.6.1" 162 | source = "registry+https://github.com/rust-lang/crates.io-index" 163 | checksum = "542a990e676ce0a0a895ae54b2d94afd012434f2228a85b186c6bc1a7056cdc6" 164 | dependencies = [ 165 | "darling", 166 | "ident_case", 167 | "prettyplease", 168 | "proc-macro2", 169 | "quote", 170 | "rustversion", 171 | "syn", 172 | ] 173 | 174 | [[package]] 175 | name = "bytemuck" 176 | version = "1.22.0" 177 | source = "registry+https://github.com/rust-lang/crates.io-index" 178 | checksum = "b6b1fc10dbac614ebc03540c9dbd60e83887fda27794998c6528f1782047d540" 179 | 180 | [[package]] 181 | name = "byteorder" 182 | version = "1.5.0" 183 | source = "registry+https://github.com/rust-lang/crates.io-index" 184 | checksum = "1fd0f2584146f6f2ef48085050886acf353beff7305ebd1ae69500e27c67f64b" 185 | 186 | [[package]] 187 | name = "bytes" 188 | version = "1.10.1" 189 | source = "registry+https://github.com/rust-lang/crates.io-index" 190 | checksum = "d71b6127be86fdcfddb610f7182ac57211d4b18a3e9c82eb2d17662f2227ad6a" 191 | 192 | [[package]] 193 | name = "cassowary" 194 | version = "0.3.0" 195 | source = "registry+https://github.com/rust-lang/crates.io-index" 196 | checksum = "df8670b8c7b9dae1793364eafadf7239c40d669904660c5960d74cfd80b46a53" 197 | 198 | [[package]] 199 | name = "castaway" 200 | version = "0.2.3" 201 | source = "registry+https://github.com/rust-lang/crates.io-index" 202 | checksum = "0abae9be0aaf9ea96a3b1b8b1b55c602ca751eba1b1500220cea4ecbafe7c0d5" 203 | dependencies = [ 204 | "rustversion", 205 | ] 206 | 207 | [[package]] 208 | name = "cfg-if" 209 | version = "1.0.0" 210 | source = "registry+https://github.com/rust-lang/crates.io-index" 211 | checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd" 212 | 213 | [[package]] 214 | name = "clap" 215 | version = "4.5.37" 216 | source = "registry+https://github.com/rust-lang/crates.io-index" 217 | checksum = "eccb054f56cbd38340b380d4a8e69ef1f02f1af43db2f0cc817a4774d80ae071" 218 | dependencies = [ 219 | "clap_builder", 220 | "clap_derive", 221 | ] 222 | 223 | [[package]] 224 | name = "clap_builder" 225 | version = "4.5.37" 226 | source = "registry+https://github.com/rust-lang/crates.io-index" 227 | checksum = "efd9466fac8543255d3b1fcad4762c5e116ffe808c8a3043d4263cd4fd4862a2" 228 | dependencies = [ 229 | "anstream", 230 | "anstyle", 231 | "clap_lex", 232 | "strsim", 233 | ] 234 | 235 | [[package]] 236 | name = "clap_derive" 237 | version = "4.5.32" 238 | source = "registry+https://github.com/rust-lang/crates.io-index" 239 | checksum = "09176aae279615badda0765c0c0b3f6ed53f4709118af73cf4655d85d1530cd7" 240 | dependencies = [ 241 | "heck", 242 | "proc-macro2", 243 | "quote", 244 | "syn", 245 | ] 246 | 247 | [[package]] 248 | name = "clap_lex" 249 | version = "0.7.4" 250 | source = "registry+https://github.com/rust-lang/crates.io-index" 251 | checksum = "f46ad14479a25103f283c0f10005961cf086d8dc42205bb44c46ac563475dca6" 252 | 253 | [[package]] 254 | name = "color_quant" 255 | version = "1.1.0" 256 | source = "registry+https://github.com/rust-lang/crates.io-index" 257 | checksum = "3d7b894f5411737b7867f4827955924d7c254fc9f4d91a6aad6b097804b1018b" 258 | 259 | [[package]] 260 | name = "colorchoice" 261 | version = "1.0.3" 262 | source = "registry+https://github.com/rust-lang/crates.io-index" 263 | checksum = "5b63caa9aa9397e2d9480a9b13673856c78d8ac123288526c37d7839f2a86990" 264 | 265 | [[package]] 266 | name = "colorsys" 267 | version = "0.6.7" 268 | source = "registry+https://github.com/rust-lang/crates.io-index" 269 | checksum = "54261aba646433cb567ec89844be4c4825ca92a4f8afba52fc4dd88436e31bbd" 270 | 271 | [[package]] 272 | name = "compact_str" 273 | version = "0.8.1" 274 | source = "registry+https://github.com/rust-lang/crates.io-index" 275 | checksum = "3b79c4069c6cad78e2e0cdfcbd26275770669fb39fd308a752dc110e83b9af32" 276 | dependencies = [ 277 | "castaway", 278 | "cfg-if", 279 | "itoa", 280 | "rustversion", 281 | "ryu", 282 | "static_assertions", 283 | ] 284 | 285 | [[package]] 286 | name = "compact_str" 287 | version = "0.9.0" 288 | source = "registry+https://github.com/rust-lang/crates.io-index" 289 | checksum = "3fdb1325a1cece981e8a296ab8f0f9b63ae357bd0784a9faaf548cc7b480707a" 290 | dependencies = [ 291 | "castaway", 292 | "cfg-if", 293 | "itoa", 294 | "rustversion", 295 | "ryu", 296 | "static_assertions", 297 | ] 298 | 299 | [[package]] 300 | name = "convert_case" 301 | version = "0.7.1" 302 | source = "registry+https://github.com/rust-lang/crates.io-index" 303 | checksum = "bb402b8d4c85569410425650ce3eddc7d698ed96d39a73f941b08fb63082f1e7" 304 | dependencies = [ 305 | "unicode-segmentation", 306 | ] 307 | 308 | [[package]] 309 | name = "crc32fast" 310 | version = "1.4.2" 311 | source = "registry+https://github.com/rust-lang/crates.io-index" 312 | checksum = "a97769d94ddab943e4510d138150169a2758b5ef3eb191a9ee688de3e23ef7b3" 313 | dependencies = [ 314 | "cfg-if", 315 | ] 316 | 317 | [[package]] 318 | name = "crossbeam-deque" 319 | version = "0.8.6" 320 | source = "registry+https://github.com/rust-lang/crates.io-index" 321 | checksum = "9dd111b7b7f7d55b72c0a6ae361660ee5853c9af73f70c3c2ef6858b950e2e51" 322 | dependencies = [ 323 | "crossbeam-epoch", 324 | "crossbeam-utils", 325 | ] 326 | 327 | [[package]] 328 | name = "crossbeam-epoch" 329 | version = "0.9.18" 330 | source = "registry+https://github.com/rust-lang/crates.io-index" 331 | checksum = "5b82ac4a3c2ca9c3460964f020e1402edd5753411d7737aa39c3714ad1b5420e" 332 | dependencies = [ 333 | "crossbeam-utils", 334 | ] 335 | 336 | [[package]] 337 | name = "crossbeam-utils" 338 | version = "0.8.21" 339 | source = "registry+https://github.com/rust-lang/crates.io-index" 340 | checksum = "d0a5c400df2834b80a4c3327b3aad3a4c4cd4de0629063962b03235697506a28" 341 | 342 | [[package]] 343 | name = "crossterm" 344 | version = "0.28.1" 345 | source = "registry+https://github.com/rust-lang/crates.io-index" 346 | checksum = "829d955a0bb380ef178a640b91779e3987da38c9aea133b20614cfed8cdea9c6" 347 | dependencies = [ 348 | "bitflags 2.9.0", 349 | "crossterm_winapi", 350 | "mio", 351 | "parking_lot", 352 | "rustix 0.38.44", 353 | "signal-hook", 354 | "signal-hook-mio", 355 | "winapi", 356 | ] 357 | 358 | [[package]] 359 | name = "crossterm" 360 | version = "0.29.0" 361 | source = "registry+https://github.com/rust-lang/crates.io-index" 362 | checksum = "d8b9f2e4c67f833b660cdb0a3523065869fb35570177239812ed4c905aeff87b" 363 | dependencies = [ 364 | "bitflags 2.9.0", 365 | "crossterm_winapi", 366 | "derive_more", 367 | "document-features", 368 | "mio", 369 | "parking_lot", 370 | "rustix 1.0.5", 371 | "signal-hook", 372 | "signal-hook-mio", 373 | "winapi", 374 | ] 375 | 376 | [[package]] 377 | name = "crossterm_winapi" 378 | version = "0.9.1" 379 | source = "registry+https://github.com/rust-lang/crates.io-index" 380 | checksum = "acdd7c62a3665c7f6830a51635d9ac9b23ed385797f70a83bb8bafe9c572ab2b" 381 | dependencies = [ 382 | "winapi", 383 | ] 384 | 385 | [[package]] 386 | name = "crunchy" 387 | version = "0.2.3" 388 | source = "registry+https://github.com/rust-lang/crates.io-index" 389 | checksum = "43da5946c66ffcc7745f48db692ffbb10a83bfe0afd96235c5c2a4fb23994929" 390 | 391 | [[package]] 392 | name = "darling" 393 | version = "0.20.11" 394 | source = "registry+https://github.com/rust-lang/crates.io-index" 395 | checksum = "fc7f46116c46ff9ab3eb1597a45688b6715c6e628b5c133e288e709a29bcb4ee" 396 | dependencies = [ 397 | "darling_core", 398 | "darling_macro", 399 | ] 400 | 401 | [[package]] 402 | name = "darling_core" 403 | version = "0.20.11" 404 | source = "registry+https://github.com/rust-lang/crates.io-index" 405 | checksum = "0d00b9596d185e565c2207a0b01f8bd1a135483d02d9b7b0a54b11da8d53412e" 406 | dependencies = [ 407 | "fnv", 408 | "ident_case", 409 | "proc-macro2", 410 | "quote", 411 | "strsim", 412 | "syn", 413 | ] 414 | 415 | [[package]] 416 | name = "darling_macro" 417 | version = "0.20.11" 418 | source = "registry+https://github.com/rust-lang/crates.io-index" 419 | checksum = "fc34b93ccb385b40dc71c6fceac4b2ad23662c7eeb248cf10d529b7e055b6ead" 420 | dependencies = [ 421 | "darling_core", 422 | "quote", 423 | "syn", 424 | ] 425 | 426 | [[package]] 427 | name = "derive_builder" 428 | version = "0.20.2" 429 | source = "registry+https://github.com/rust-lang/crates.io-index" 430 | checksum = "507dfb09ea8b7fa618fcf76e953f4f5e192547945816d5358edffe39f6f94947" 431 | dependencies = [ 432 | "derive_builder_macro", 433 | ] 434 | 435 | [[package]] 436 | name = "derive_builder_core" 437 | version = "0.20.2" 438 | source = "registry+https://github.com/rust-lang/crates.io-index" 439 | checksum = "2d5bcf7b024d6835cfb3d473887cd966994907effbe9227e8c8219824d06c4e8" 440 | dependencies = [ 441 | "darling", 442 | "proc-macro2", 443 | "quote", 444 | "syn", 445 | ] 446 | 447 | [[package]] 448 | name = "derive_builder_macro" 449 | version = "0.20.2" 450 | source = "registry+https://github.com/rust-lang/crates.io-index" 451 | checksum = "ab63b0e2bf4d5928aff72e83a7dace85d7bba5fe12dcc3c5a572d78caffd3f3c" 452 | dependencies = [ 453 | "derive_builder_core", 454 | "syn", 455 | ] 456 | 457 | [[package]] 458 | name = "derive_more" 459 | version = "2.0.1" 460 | source = "registry+https://github.com/rust-lang/crates.io-index" 461 | checksum = "093242cf7570c207c83073cf82f79706fe7b8317e98620a47d5be7c3d8497678" 462 | dependencies = [ 463 | "derive_more-impl", 464 | ] 465 | 466 | [[package]] 467 | name = "derive_more-impl" 468 | version = "2.0.1" 469 | source = "registry+https://github.com/rust-lang/crates.io-index" 470 | checksum = "bda628edc44c4bb645fbe0f758797143e4e07926f7ebf4e9bdfbd3d2ce621df3" 471 | dependencies = [ 472 | "convert_case", 473 | "proc-macro2", 474 | "quote", 475 | "syn", 476 | ] 477 | 478 | [[package]] 479 | name = "document-features" 480 | version = "0.2.11" 481 | source = "registry+https://github.com/rust-lang/crates.io-index" 482 | checksum = "95249b50c6c185bee49034bcb378a49dc2b5dff0be90ff6616d31d64febab05d" 483 | dependencies = [ 484 | "litrs", 485 | ] 486 | 487 | [[package]] 488 | name = "either" 489 | version = "1.15.0" 490 | source = "registry+https://github.com/rust-lang/crates.io-index" 491 | checksum = "48c757948c5ede0e46177b7add2e67155f70e33c07fea8284df6576da70b3719" 492 | 493 | [[package]] 494 | name = "equivalent" 495 | version = "1.0.2" 496 | source = "registry+https://github.com/rust-lang/crates.io-index" 497 | checksum = "877a4ace8713b0bcf2a4e7eec82529c029f1d0619886d18145fea96c3ffe5c0f" 498 | 499 | [[package]] 500 | name = "errno" 501 | version = "0.3.11" 502 | source = "registry+https://github.com/rust-lang/crates.io-index" 503 | checksum = "976dd42dc7e85965fe702eb8164f21f450704bdde31faefd6471dba214cb594e" 504 | dependencies = [ 505 | "libc", 506 | "windows-sys 0.59.0", 507 | ] 508 | 509 | [[package]] 510 | name = "exr" 511 | version = "1.73.0" 512 | source = "registry+https://github.com/rust-lang/crates.io-index" 513 | checksum = "f83197f59927b46c04a183a619b7c29df34e63e63c7869320862268c0ef687e0" 514 | dependencies = [ 515 | "bit_field", 516 | "half", 517 | "lebe", 518 | "miniz_oxide", 519 | "rayon-core", 520 | "smallvec", 521 | "zune-inflate", 522 | ] 523 | 524 | [[package]] 525 | name = "fdeflate" 526 | version = "0.3.7" 527 | source = "registry+https://github.com/rust-lang/crates.io-index" 528 | checksum = "1e6853b52649d4ac5c0bd02320cddc5ba956bdb407c4b75a2c6b75bf51500f8c" 529 | dependencies = [ 530 | "simd-adler32", 531 | ] 532 | 533 | [[package]] 534 | name = "flate2" 535 | version = "1.1.1" 536 | source = "registry+https://github.com/rust-lang/crates.io-index" 537 | checksum = "7ced92e76e966ca2fd84c8f7aa01a4aea65b0eb6648d72f7c8f3e2764a67fece" 538 | dependencies = [ 539 | "crc32fast", 540 | "miniz_oxide", 541 | ] 542 | 543 | [[package]] 544 | name = "fnv" 545 | version = "1.0.7" 546 | source = "registry+https://github.com/rust-lang/crates.io-index" 547 | checksum = "3f9eec918d3f24069decb9af1554cad7c880e2da24a9afd88aca000531ab82c1" 548 | 549 | [[package]] 550 | name = "foldhash" 551 | version = "0.1.5" 552 | source = "registry+https://github.com/rust-lang/crates.io-index" 553 | checksum = "d9c4f5dac5e15c24eb999c26181a6ca40b39fe946cbe4c263c7209467bc83af2" 554 | 555 | [[package]] 556 | name = "font8x8" 557 | version = "0.3.1" 558 | source = "registry+https://github.com/rust-lang/crates.io-index" 559 | checksum = "875488b8711a968268c7cf5d139578713097ca4635a76044e8fe8eedf831d07e" 560 | 561 | [[package]] 562 | name = "futures" 563 | version = "0.3.31" 564 | source = "registry+https://github.com/rust-lang/crates.io-index" 565 | checksum = "65bc07b1a8bc7c85c5f2e110c476c7389b4554ba72af57d8445ea63a576b0876" 566 | dependencies = [ 567 | "futures-channel", 568 | "futures-core", 569 | "futures-executor", 570 | "futures-io", 571 | "futures-sink", 572 | "futures-task", 573 | "futures-util", 574 | ] 575 | 576 | [[package]] 577 | name = "futures-channel" 578 | version = "0.3.31" 579 | source = "registry+https://github.com/rust-lang/crates.io-index" 580 | checksum = "2dff15bf788c671c1934e366d07e30c1814a8ef514e1af724a602e8a2fbe1b10" 581 | dependencies = [ 582 | "futures-core", 583 | "futures-sink", 584 | ] 585 | 586 | [[package]] 587 | name = "futures-core" 588 | version = "0.3.31" 589 | source = "registry+https://github.com/rust-lang/crates.io-index" 590 | checksum = "05f29059c0c2090612e8d742178b0580d2dc940c837851ad723096f87af6663e" 591 | 592 | [[package]] 593 | name = "futures-executor" 594 | version = "0.3.31" 595 | source = "registry+https://github.com/rust-lang/crates.io-index" 596 | checksum = "1e28d1d997f585e54aebc3f97d39e72338912123a67330d723fdbb564d646c9f" 597 | dependencies = [ 598 | "futures-core", 599 | "futures-task", 600 | "futures-util", 601 | ] 602 | 603 | [[package]] 604 | name = "futures-io" 605 | version = "0.3.31" 606 | source = "registry+https://github.com/rust-lang/crates.io-index" 607 | checksum = "9e5c1b78ca4aae1ac06c48a526a655760685149f0d465d21f37abfe57ce075c6" 608 | 609 | [[package]] 610 | name = "futures-macro" 611 | version = "0.3.31" 612 | source = "registry+https://github.com/rust-lang/crates.io-index" 613 | checksum = "162ee34ebcb7c64a8abebc059ce0fee27c2262618d7b60ed8faf72fef13c3650" 614 | dependencies = [ 615 | "proc-macro2", 616 | "quote", 617 | "syn", 618 | ] 619 | 620 | [[package]] 621 | name = "futures-sink" 622 | version = "0.3.31" 623 | source = "registry+https://github.com/rust-lang/crates.io-index" 624 | checksum = "e575fab7d1e0dcb8d0c7bcf9a63ee213816ab51902e6d244a95819acacf1d4f7" 625 | 626 | [[package]] 627 | name = "futures-task" 628 | version = "0.3.31" 629 | source = "registry+https://github.com/rust-lang/crates.io-index" 630 | checksum = "f90f7dce0722e95104fcb095585910c0977252f286e354b5e3bd38902cd99988" 631 | 632 | [[package]] 633 | name = "futures-util" 634 | version = "0.3.31" 635 | source = "registry+https://github.com/rust-lang/crates.io-index" 636 | checksum = "9fa08315bb612088cc391249efdc3bc77536f16c91f6cf495e6fbe85b20a4a81" 637 | dependencies = [ 638 | "futures-channel", 639 | "futures-core", 640 | "futures-io", 641 | "futures-macro", 642 | "futures-sink", 643 | "futures-task", 644 | "memchr", 645 | "pin-project-lite", 646 | "pin-utils", 647 | "slab", 648 | ] 649 | 650 | [[package]] 651 | name = "getrandom" 652 | version = "0.3.2" 653 | source = "registry+https://github.com/rust-lang/crates.io-index" 654 | checksum = "73fea8450eea4bac3940448fb7ae50d91f034f941199fcd9d909a5a07aa455f0" 655 | dependencies = [ 656 | "cfg-if", 657 | "libc", 658 | "r-efi", 659 | "wasi 0.14.2+wasi-0.2.4", 660 | ] 661 | 662 | [[package]] 663 | name = "gif" 664 | version = "0.13.1" 665 | source = "registry+https://github.com/rust-lang/crates.io-index" 666 | checksum = "3fb2d69b19215e18bb912fa30f7ce15846e301408695e44e0ef719f1da9e19f2" 667 | dependencies = [ 668 | "color_quant", 669 | "weezl", 670 | ] 671 | 672 | [[package]] 673 | name = "gimli" 674 | version = "0.31.1" 675 | source = "registry+https://github.com/rust-lang/crates.io-index" 676 | checksum = "07e28edb80900c19c28f1072f2e8aeca7fa06b23cd4169cefe1af5aa3260783f" 677 | 678 | [[package]] 679 | name = "half" 680 | version = "2.6.0" 681 | source = "registry+https://github.com/rust-lang/crates.io-index" 682 | checksum = "459196ed295495a68f7d7fe1d84f6c4b7ff0e21fe3017b2f283c6fac3ad803c9" 683 | dependencies = [ 684 | "cfg-if", 685 | "crunchy", 686 | ] 687 | 688 | [[package]] 689 | name = "hashbrown" 690 | version = "0.15.2" 691 | source = "registry+https://github.com/rust-lang/crates.io-index" 692 | checksum = "bf151400ff0baff5465007dd2f3e717f3fe502074ca563069ce3a6629d07b289" 693 | dependencies = [ 694 | "allocator-api2", 695 | "equivalent", 696 | "foldhash", 697 | ] 698 | 699 | [[package]] 700 | name = "heck" 701 | version = "0.5.0" 702 | source = "registry+https://github.com/rust-lang/crates.io-index" 703 | checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea" 704 | 705 | [[package]] 706 | name = "ident_case" 707 | version = "1.0.1" 708 | source = "registry+https://github.com/rust-lang/crates.io-index" 709 | checksum = "b9e0384b61958566e926dc50660321d12159025e767c18e043daf26b70104c39" 710 | 711 | [[package]] 712 | name = "image" 713 | version = "0.24.9" 714 | source = "registry+https://github.com/rust-lang/crates.io-index" 715 | checksum = "5690139d2f55868e080017335e4b94cb7414274c74f1669c84fb5feba2c9f69d" 716 | dependencies = [ 717 | "bytemuck", 718 | "byteorder", 719 | "color_quant", 720 | "exr", 721 | "gif", 722 | "jpeg-decoder", 723 | "num-traits", 724 | "png", 725 | "qoi", 726 | "tiff", 727 | ] 728 | 729 | [[package]] 730 | name = "indoc" 731 | version = "2.0.6" 732 | source = "registry+https://github.com/rust-lang/crates.io-index" 733 | checksum = "f4c7245a08504955605670dbf141fceab975f15ca21570696aebe9d2e71576bd" 734 | 735 | [[package]] 736 | name = "instability" 737 | version = "0.3.7" 738 | source = "registry+https://github.com/rust-lang/crates.io-index" 739 | checksum = "0bf9fed6d91cfb734e7476a06bde8300a1b94e217e1b523b6f0cd1a01998c71d" 740 | dependencies = [ 741 | "darling", 742 | "indoc", 743 | "proc-macro2", 744 | "quote", 745 | "syn", 746 | ] 747 | 748 | [[package]] 749 | name = "is_terminal_polyfill" 750 | version = "1.70.1" 751 | source = "registry+https://github.com/rust-lang/crates.io-index" 752 | checksum = "7943c866cc5cd64cbc25b2e01621d07fa8eb2a1a23160ee81ce38704e97b8ecf" 753 | 754 | [[package]] 755 | name = "itertools" 756 | version = "0.10.5" 757 | source = "registry+https://github.com/rust-lang/crates.io-index" 758 | checksum = "b0fd2260e829bddf4cb6ea802289de2f86d6a7a690192fbe91b3f46e0f2c8473" 759 | dependencies = [ 760 | "either", 761 | ] 762 | 763 | [[package]] 764 | name = "itertools" 765 | version = "0.13.0" 766 | source = "registry+https://github.com/rust-lang/crates.io-index" 767 | checksum = "413ee7dfc52ee1a4949ceeb7dbc8a33f2d6c088194d9f922fb8318faf1f01186" 768 | dependencies = [ 769 | "either", 770 | ] 771 | 772 | [[package]] 773 | name = "itertools" 774 | version = "0.14.0" 775 | source = "registry+https://github.com/rust-lang/crates.io-index" 776 | checksum = "2b192c782037fadd9cfa75548310488aabdbf3d2da73885b31bd0abd03351285" 777 | dependencies = [ 778 | "either", 779 | ] 780 | 781 | [[package]] 782 | name = "itoa" 783 | version = "1.0.15" 784 | source = "registry+https://github.com/rust-lang/crates.io-index" 785 | checksum = "4a5f13b858c8d314ee3e8f639011f7ccefe71f97f96e50151fb991f267928e2c" 786 | 787 | [[package]] 788 | name = "jpeg-decoder" 789 | version = "0.3.1" 790 | source = "registry+https://github.com/rust-lang/crates.io-index" 791 | checksum = "f5d4a7da358eff58addd2877a45865158f0d78c911d43a5784ceb7bbf52833b0" 792 | dependencies = [ 793 | "rayon", 794 | ] 795 | 796 | [[package]] 797 | name = "kanash" 798 | version = "0.1.4" 799 | dependencies = [ 800 | "ansi-to-tui", 801 | "anyhow", 802 | "clap", 803 | "crossterm 0.29.0", 804 | "futures", 805 | "rand 0.9.1", 806 | "rand_core 0.6.4", 807 | "rand_pcg 0.9.0", 808 | "rascii_art", 809 | "ratatui", 810 | "tachyonfx", 811 | "tokio", 812 | "tui-big-text", 813 | "tui-rain", 814 | "wana_kana", 815 | ] 816 | 817 | [[package]] 818 | name = "lazy_static" 819 | version = "1.5.0" 820 | source = "registry+https://github.com/rust-lang/crates.io-index" 821 | checksum = "bbd2bcb4c963f2ddae06a2efc7e9f3591312473c50c6685e1f298068316e66fe" 822 | 823 | [[package]] 824 | name = "lebe" 825 | version = "0.5.2" 826 | source = "registry+https://github.com/rust-lang/crates.io-index" 827 | checksum = "03087c2bad5e1034e8cace5926dec053fb3790248370865f5117a7d0213354c8" 828 | 829 | [[package]] 830 | name = "libc" 831 | version = "0.2.172" 832 | source = "registry+https://github.com/rust-lang/crates.io-index" 833 | checksum = "d750af042f7ef4f724306de029d18836c26c1765a54a6a3f094cbd23a7267ffa" 834 | 835 | [[package]] 836 | name = "linux-raw-sys" 837 | version = "0.4.15" 838 | source = "registry+https://github.com/rust-lang/crates.io-index" 839 | checksum = "d26c52dbd32dccf2d10cac7725f8eae5296885fb5703b261f7d0a0739ec807ab" 840 | 841 | [[package]] 842 | name = "linux-raw-sys" 843 | version = "0.9.4" 844 | source = "registry+https://github.com/rust-lang/crates.io-index" 845 | checksum = "cd945864f07fe9f5371a27ad7b52a172b4b499999f1d97574c9fa68373937e12" 846 | 847 | [[package]] 848 | name = "litrs" 849 | version = "0.4.1" 850 | source = "registry+https://github.com/rust-lang/crates.io-index" 851 | checksum = "b4ce301924b7887e9d637144fdade93f9dfff9b60981d4ac161db09720d39aa5" 852 | 853 | [[package]] 854 | name = "lock_api" 855 | version = "0.4.12" 856 | source = "registry+https://github.com/rust-lang/crates.io-index" 857 | checksum = "07af8b9cdd281b7915f413fa73f29ebd5d55d0d3f0155584dade1ff18cea1b17" 858 | dependencies = [ 859 | "autocfg", 860 | "scopeguard", 861 | ] 862 | 863 | [[package]] 864 | name = "log" 865 | version = "0.4.27" 866 | source = "registry+https://github.com/rust-lang/crates.io-index" 867 | checksum = "13dc2df351e3202783a1fe0d44375f7295ffb4049267b0f3018346dc122a1d94" 868 | 869 | [[package]] 870 | name = "lru" 871 | version = "0.12.5" 872 | source = "registry+https://github.com/rust-lang/crates.io-index" 873 | checksum = "234cf4f4a04dc1f57e24b96cc0cd600cf2af460d4161ac5ecdd0af8e1f3b2a38" 874 | dependencies = [ 875 | "hashbrown", 876 | ] 877 | 878 | [[package]] 879 | name = "memchr" 880 | version = "2.7.4" 881 | source = "registry+https://github.com/rust-lang/crates.io-index" 882 | checksum = "78ca9ab1a0babb1e7d5695e3530886289c18cf2f87ec19a575a0abdce112e3a3" 883 | 884 | [[package]] 885 | name = "minimal-lexical" 886 | version = "0.2.1" 887 | source = "registry+https://github.com/rust-lang/crates.io-index" 888 | checksum = "68354c5c6bd36d73ff3feceb05efa59b6acb7626617f4962be322a825e61f79a" 889 | 890 | [[package]] 891 | name = "miniz_oxide" 892 | version = "0.8.8" 893 | source = "registry+https://github.com/rust-lang/crates.io-index" 894 | checksum = "3be647b768db090acb35d5ec5db2b0e1f1de11133ca123b9eacf5137868f892a" 895 | dependencies = [ 896 | "adler2", 897 | "simd-adler32", 898 | ] 899 | 900 | [[package]] 901 | name = "mio" 902 | version = "1.0.3" 903 | source = "registry+https://github.com/rust-lang/crates.io-index" 904 | checksum = "2886843bf800fba2e3377cff24abf6379b4c4d5c6681eaf9ea5b0d15090450bd" 905 | dependencies = [ 906 | "libc", 907 | "log", 908 | "wasi 0.11.0+wasi-snapshot-preview1", 909 | "windows-sys 0.52.0", 910 | ] 911 | 912 | [[package]] 913 | name = "nom" 914 | version = "7.1.3" 915 | source = "registry+https://github.com/rust-lang/crates.io-index" 916 | checksum = "d273983c5a657a70a3e8f2a01329822f3b8c8172b73826411a55751e404a0a4a" 917 | dependencies = [ 918 | "memchr", 919 | "minimal-lexical", 920 | ] 921 | 922 | [[package]] 923 | name = "num-traits" 924 | version = "0.2.19" 925 | source = "registry+https://github.com/rust-lang/crates.io-index" 926 | checksum = "071dfc062690e90b734c0b2273ce72ad0ffa95f0c74596bc250dcfd960262841" 927 | dependencies = [ 928 | "autocfg", 929 | ] 930 | 931 | [[package]] 932 | name = "object" 933 | version = "0.36.7" 934 | source = "registry+https://github.com/rust-lang/crates.io-index" 935 | checksum = "62948e14d923ea95ea2c7c86c71013138b66525b86bdc08d2dcc262bdb497b87" 936 | dependencies = [ 937 | "memchr", 938 | ] 939 | 940 | [[package]] 941 | name = "once_cell" 942 | version = "1.21.3" 943 | source = "registry+https://github.com/rust-lang/crates.io-index" 944 | checksum = "42f5e15c9953c5e4ccceeb2e7382a716482c34515315f7b03532b8b4e8393d2d" 945 | 946 | [[package]] 947 | name = "parking_lot" 948 | version = "0.12.3" 949 | source = "registry+https://github.com/rust-lang/crates.io-index" 950 | checksum = "f1bf18183cf54e8d6059647fc3063646a1801cf30896933ec2311622cc4b9a27" 951 | dependencies = [ 952 | "lock_api", 953 | "parking_lot_core", 954 | ] 955 | 956 | [[package]] 957 | name = "parking_lot_core" 958 | version = "0.9.10" 959 | source = "registry+https://github.com/rust-lang/crates.io-index" 960 | checksum = "1e401f977ab385c9e4e3ab30627d6f26d00e2c73eef317493c4ec6d468726cf8" 961 | dependencies = [ 962 | "cfg-if", 963 | "libc", 964 | "redox_syscall", 965 | "smallvec", 966 | "windows-targets", 967 | ] 968 | 969 | [[package]] 970 | name = "paste" 971 | version = "1.0.15" 972 | source = "registry+https://github.com/rust-lang/crates.io-index" 973 | checksum = "57c0d7b74b563b49d38dae00a0c37d4d6de9b432382b2892f0574ddcae73fd0a" 974 | 975 | [[package]] 976 | name = "pin-project-lite" 977 | version = "0.2.16" 978 | source = "registry+https://github.com/rust-lang/crates.io-index" 979 | checksum = "3b3cff922bd51709b605d9ead9aa71031d81447142d828eb4a6eba76fe619f9b" 980 | 981 | [[package]] 982 | name = "pin-utils" 983 | version = "0.1.0" 984 | source = "registry+https://github.com/rust-lang/crates.io-index" 985 | checksum = "8b870d8c151b6f2fb93e84a13146138f05d02ed11c7e7c54f8826aaaf7c9f184" 986 | 987 | [[package]] 988 | name = "png" 989 | version = "0.17.16" 990 | source = "registry+https://github.com/rust-lang/crates.io-index" 991 | checksum = "82151a2fc869e011c153adc57cf2789ccb8d9906ce52c0b39a6b5697749d7526" 992 | dependencies = [ 993 | "bitflags 1.3.2", 994 | "crc32fast", 995 | "fdeflate", 996 | "flate2", 997 | "miniz_oxide", 998 | ] 999 | 1000 | [[package]] 1001 | name = "ppv-lite86" 1002 | version = "0.2.21" 1003 | source = "registry+https://github.com/rust-lang/crates.io-index" 1004 | checksum = "85eae3c4ed2f50dcfe72643da4befc30deadb458a9b590d720cde2f2b1e97da9" 1005 | dependencies = [ 1006 | "zerocopy", 1007 | ] 1008 | 1009 | [[package]] 1010 | name = "prettyplease" 1011 | version = "0.2.32" 1012 | source = "registry+https://github.com/rust-lang/crates.io-index" 1013 | checksum = "664ec5419c51e34154eec046ebcba56312d5a2fc3b09a06da188e1ad21afadf6" 1014 | dependencies = [ 1015 | "proc-macro2", 1016 | "syn", 1017 | ] 1018 | 1019 | [[package]] 1020 | name = "proc-macro2" 1021 | version = "1.0.95" 1022 | source = "registry+https://github.com/rust-lang/crates.io-index" 1023 | checksum = "02b3e5e68a3a1a02aad3ec490a98007cbc13c37cbe84a3cd7b8e406d76e7f778" 1024 | dependencies = [ 1025 | "unicode-ident", 1026 | ] 1027 | 1028 | [[package]] 1029 | name = "qoi" 1030 | version = "0.4.1" 1031 | source = "registry+https://github.com/rust-lang/crates.io-index" 1032 | checksum = "7f6d64c71eb498fe9eae14ce4ec935c555749aef511cca85b5568910d6e48001" 1033 | dependencies = [ 1034 | "bytemuck", 1035 | ] 1036 | 1037 | [[package]] 1038 | name = "quote" 1039 | version = "1.0.40" 1040 | source = "registry+https://github.com/rust-lang/crates.io-index" 1041 | checksum = "1885c039570dc00dcb4ff087a89e185fd56bae234ddc7f056a945bf36467248d" 1042 | dependencies = [ 1043 | "proc-macro2", 1044 | ] 1045 | 1046 | [[package]] 1047 | name = "r-efi" 1048 | version = "5.2.0" 1049 | source = "registry+https://github.com/rust-lang/crates.io-index" 1050 | checksum = "74765f6d916ee2faa39bc8e68e4f3ed8949b48cccdac59983d287a7cb71ce9c5" 1051 | 1052 | [[package]] 1053 | name = "rand" 1054 | version = "0.8.5" 1055 | source = "registry+https://github.com/rust-lang/crates.io-index" 1056 | checksum = "34af8d1a0e25924bc5b7c43c079c942339d8f0a8b57c39049bef581b46327404" 1057 | dependencies = [ 1058 | "rand_core 0.6.4", 1059 | ] 1060 | 1061 | [[package]] 1062 | name = "rand" 1063 | version = "0.9.1" 1064 | source = "registry+https://github.com/rust-lang/crates.io-index" 1065 | checksum = "9fbfd9d094a40bf3ae768db9361049ace4c0e04a4fd6b359518bd7b73a73dd97" 1066 | dependencies = [ 1067 | "rand_chacha", 1068 | "rand_core 0.9.3", 1069 | ] 1070 | 1071 | [[package]] 1072 | name = "rand_chacha" 1073 | version = "0.9.0" 1074 | source = "registry+https://github.com/rust-lang/crates.io-index" 1075 | checksum = "d3022b5f1df60f26e1ffddd6c66e8aa15de382ae63b3a0c1bfc0e4d3e3f325cb" 1076 | dependencies = [ 1077 | "ppv-lite86", 1078 | "rand_core 0.9.3", 1079 | ] 1080 | 1081 | [[package]] 1082 | name = "rand_core" 1083 | version = "0.6.4" 1084 | source = "registry+https://github.com/rust-lang/crates.io-index" 1085 | checksum = "ec0be4795e2f6a28069bec0b5ff3e2ac9bafc99e6a9a7dc3547996c5c816922c" 1086 | 1087 | [[package]] 1088 | name = "rand_core" 1089 | version = "0.9.3" 1090 | source = "registry+https://github.com/rust-lang/crates.io-index" 1091 | checksum = "99d9a13982dcf210057a8a78572b2217b667c3beacbf3a0d8b454f6f82837d38" 1092 | dependencies = [ 1093 | "getrandom", 1094 | ] 1095 | 1096 | [[package]] 1097 | name = "rand_pcg" 1098 | version = "0.3.1" 1099 | source = "registry+https://github.com/rust-lang/crates.io-index" 1100 | checksum = "59cad018caf63deb318e5a4586d99a24424a364f40f1e5778c29aca23f4fc73e" 1101 | dependencies = [ 1102 | "rand_core 0.6.4", 1103 | ] 1104 | 1105 | [[package]] 1106 | name = "rand_pcg" 1107 | version = "0.9.0" 1108 | source = "registry+https://github.com/rust-lang/crates.io-index" 1109 | checksum = "b48ac3f7ffaab7fac4d2376632268aa5f89abdb55f7ebf8f4d11fffccb2320f7" 1110 | dependencies = [ 1111 | "rand_core 0.9.3", 1112 | ] 1113 | 1114 | [[package]] 1115 | name = "rascii_art" 1116 | version = "0.4.5" 1117 | source = "registry+https://github.com/rust-lang/crates.io-index" 1118 | checksum = "e27f573a36aab4d4f2dd4aae8f44e8c5409a3d2cefce01d116cb11de546b1c65" 1119 | dependencies = [ 1120 | "ansi_term", 1121 | "clap", 1122 | "image", 1123 | "unicode-segmentation", 1124 | ] 1125 | 1126 | [[package]] 1127 | name = "ratatui" 1128 | version = "0.29.0" 1129 | source = "registry+https://github.com/rust-lang/crates.io-index" 1130 | checksum = "eabd94c2f37801c20583fc49dd5cd6b0ba68c716787c2dd6ed18571e1e63117b" 1131 | dependencies = [ 1132 | "bitflags 2.9.0", 1133 | "cassowary", 1134 | "compact_str 0.8.1", 1135 | "crossterm 0.28.1", 1136 | "indoc", 1137 | "instability", 1138 | "itertools 0.13.0", 1139 | "lru", 1140 | "paste", 1141 | "strum", 1142 | "unicode-segmentation", 1143 | "unicode-truncate", 1144 | "unicode-width 0.2.0", 1145 | ] 1146 | 1147 | [[package]] 1148 | name = "rayon" 1149 | version = "1.10.0" 1150 | source = "registry+https://github.com/rust-lang/crates.io-index" 1151 | checksum = "b418a60154510ca1a002a752ca9714984e21e4241e804d32555251faf8b78ffa" 1152 | dependencies = [ 1153 | "either", 1154 | "rayon-core", 1155 | ] 1156 | 1157 | [[package]] 1158 | name = "rayon-core" 1159 | version = "1.12.1" 1160 | source = "registry+https://github.com/rust-lang/crates.io-index" 1161 | checksum = "1465873a3dfdaa8ae7cb14b4383657caab0b3e8a0aa9ae8e04b044854c8dfce2" 1162 | dependencies = [ 1163 | "crossbeam-deque", 1164 | "crossbeam-utils", 1165 | ] 1166 | 1167 | [[package]] 1168 | name = "redox_syscall" 1169 | version = "0.5.11" 1170 | source = "registry+https://github.com/rust-lang/crates.io-index" 1171 | checksum = "d2f103c6d277498fbceb16e84d317e2a400f160f46904d5f5410848c829511a3" 1172 | dependencies = [ 1173 | "bitflags 2.9.0", 1174 | ] 1175 | 1176 | [[package]] 1177 | name = "rustc-demangle" 1178 | version = "0.1.24" 1179 | source = "registry+https://github.com/rust-lang/crates.io-index" 1180 | checksum = "719b953e2095829ee67db738b3bfa9fa368c94900df327b3f07fe6e794d2fe1f" 1181 | 1182 | [[package]] 1183 | name = "rustix" 1184 | version = "0.38.44" 1185 | source = "registry+https://github.com/rust-lang/crates.io-index" 1186 | checksum = "fdb5bc1ae2baa591800df16c9ca78619bf65c0488b41b96ccec5d11220d8c154" 1187 | dependencies = [ 1188 | "bitflags 2.9.0", 1189 | "errno", 1190 | "libc", 1191 | "linux-raw-sys 0.4.15", 1192 | "windows-sys 0.59.0", 1193 | ] 1194 | 1195 | [[package]] 1196 | name = "rustix" 1197 | version = "1.0.5" 1198 | source = "registry+https://github.com/rust-lang/crates.io-index" 1199 | checksum = "d97817398dd4bb2e6da002002db259209759911da105da92bec29ccb12cf58bf" 1200 | dependencies = [ 1201 | "bitflags 2.9.0", 1202 | "errno", 1203 | "libc", 1204 | "linux-raw-sys 0.9.4", 1205 | "windows-sys 0.59.0", 1206 | ] 1207 | 1208 | [[package]] 1209 | name = "rustversion" 1210 | version = "1.0.20" 1211 | source = "registry+https://github.com/rust-lang/crates.io-index" 1212 | checksum = "eded382c5f5f786b989652c49544c4877d9f015cc22e145a5ea8ea66c2921cd2" 1213 | 1214 | [[package]] 1215 | name = "ryu" 1216 | version = "1.0.20" 1217 | source = "registry+https://github.com/rust-lang/crates.io-index" 1218 | checksum = "28d3b2b1366ec20994f1fd18c3c594f05c5dd4bc44d8bb0c1c632c8d6829481f" 1219 | 1220 | [[package]] 1221 | name = "scopeguard" 1222 | version = "1.2.0" 1223 | source = "registry+https://github.com/rust-lang/crates.io-index" 1224 | checksum = "94143f37725109f92c262ed2cf5e59bce7498c01bcc1502d7b9afe439a4e9f49" 1225 | 1226 | [[package]] 1227 | name = "signal-hook" 1228 | version = "0.3.17" 1229 | source = "registry+https://github.com/rust-lang/crates.io-index" 1230 | checksum = "8621587d4798caf8eb44879d42e56b9a93ea5dcd315a6487c357130095b62801" 1231 | dependencies = [ 1232 | "libc", 1233 | "signal-hook-registry", 1234 | ] 1235 | 1236 | [[package]] 1237 | name = "signal-hook-mio" 1238 | version = "0.2.4" 1239 | source = "registry+https://github.com/rust-lang/crates.io-index" 1240 | checksum = "34db1a06d485c9142248b7a054f034b349b212551f3dfd19c94d45a754a217cd" 1241 | dependencies = [ 1242 | "libc", 1243 | "mio", 1244 | "signal-hook", 1245 | ] 1246 | 1247 | [[package]] 1248 | name = "signal-hook-registry" 1249 | version = "1.4.2" 1250 | source = "registry+https://github.com/rust-lang/crates.io-index" 1251 | checksum = "a9e9e0b4211b72e7b8b6e85c807d36c212bdb33ea8587f7569562a84df5465b1" 1252 | dependencies = [ 1253 | "libc", 1254 | ] 1255 | 1256 | [[package]] 1257 | name = "simd-adler32" 1258 | version = "0.3.7" 1259 | source = "registry+https://github.com/rust-lang/crates.io-index" 1260 | checksum = "d66dc143e6b11c1eddc06d5c423cfc97062865baf299914ab64caa38182078fe" 1261 | 1262 | [[package]] 1263 | name = "simdutf8" 1264 | version = "0.1.5" 1265 | source = "registry+https://github.com/rust-lang/crates.io-index" 1266 | checksum = "e3a9fe34e3e7a50316060351f37187a3f546bce95496156754b601a5fa71b76e" 1267 | 1268 | [[package]] 1269 | name = "simple-easing" 1270 | version = "1.0.1" 1271 | source = "registry+https://github.com/rust-lang/crates.io-index" 1272 | checksum = "832ddd7df0d98d6fd93b973c330b7c8e0742d5cb8f1afc7dea89dba4d2531aa1" 1273 | 1274 | [[package]] 1275 | name = "slab" 1276 | version = "0.4.9" 1277 | source = "registry+https://github.com/rust-lang/crates.io-index" 1278 | checksum = "8f92a496fb766b417c996b9c5e57daf2f7ad3b0bebe1ccfca4856390e3d3bb67" 1279 | dependencies = [ 1280 | "autocfg", 1281 | ] 1282 | 1283 | [[package]] 1284 | name = "smallvec" 1285 | version = "1.15.0" 1286 | source = "registry+https://github.com/rust-lang/crates.io-index" 1287 | checksum = "8917285742e9f3e1683f0a9c4e6b57960b7314d0b08d30d1ecd426713ee2eee9" 1288 | 1289 | [[package]] 1290 | name = "socket2" 1291 | version = "0.5.9" 1292 | source = "registry+https://github.com/rust-lang/crates.io-index" 1293 | checksum = "4f5fd57c80058a56cf5c777ab8a126398ece8e442983605d280a44ce79d0edef" 1294 | dependencies = [ 1295 | "libc", 1296 | "windows-sys 0.52.0", 1297 | ] 1298 | 1299 | [[package]] 1300 | name = "static_assertions" 1301 | version = "1.1.0" 1302 | source = "registry+https://github.com/rust-lang/crates.io-index" 1303 | checksum = "a2eb9349b6444b326872e140eb1cf5e7c522154d69e7a0ffb0fb81c06b37543f" 1304 | 1305 | [[package]] 1306 | name = "strsim" 1307 | version = "0.11.1" 1308 | source = "registry+https://github.com/rust-lang/crates.io-index" 1309 | checksum = "7da8b5736845d9f2fcb837ea5d9e2628564b3b043a70948a3f0b778838c5fb4f" 1310 | 1311 | [[package]] 1312 | name = "strum" 1313 | version = "0.26.3" 1314 | source = "registry+https://github.com/rust-lang/crates.io-index" 1315 | checksum = "8fec0f0aef304996cf250b31b5a10dee7980c85da9d759361292b8bca5a18f06" 1316 | dependencies = [ 1317 | "strum_macros", 1318 | ] 1319 | 1320 | [[package]] 1321 | name = "strum_macros" 1322 | version = "0.26.4" 1323 | source = "registry+https://github.com/rust-lang/crates.io-index" 1324 | checksum = "4c6bee85a5a24955dc440386795aa378cd9cf82acd5f764469152d2270e581be" 1325 | dependencies = [ 1326 | "heck", 1327 | "proc-macro2", 1328 | "quote", 1329 | "rustversion", 1330 | "syn", 1331 | ] 1332 | 1333 | [[package]] 1334 | name = "syn" 1335 | version = "2.0.100" 1336 | source = "registry+https://github.com/rust-lang/crates.io-index" 1337 | checksum = "b09a44accad81e1ba1cd74a32461ba89dee89095ba17b32f5d03683b1b1fc2a0" 1338 | dependencies = [ 1339 | "proc-macro2", 1340 | "quote", 1341 | "unicode-ident", 1342 | ] 1343 | 1344 | [[package]] 1345 | name = "tachyonfx" 1346 | version = "0.13.0" 1347 | source = "registry+https://github.com/rust-lang/crates.io-index" 1348 | checksum = "7e12f441a3aa6821a20ebc350e51139806c875551910e8ac067f663e4ab28e82" 1349 | dependencies = [ 1350 | "anpa", 1351 | "bon", 1352 | "colorsys", 1353 | "compact_str 0.9.0", 1354 | "ratatui", 1355 | "simple-easing", 1356 | "thiserror 2.0.12", 1357 | ] 1358 | 1359 | [[package]] 1360 | name = "thiserror" 1361 | version = "1.0.69" 1362 | source = "registry+https://github.com/rust-lang/crates.io-index" 1363 | checksum = "b6aaf5339b578ea85b50e080feb250a3e8ae8cfcdff9a461c9ec2904bc923f52" 1364 | dependencies = [ 1365 | "thiserror-impl 1.0.69", 1366 | ] 1367 | 1368 | [[package]] 1369 | name = "thiserror" 1370 | version = "2.0.12" 1371 | source = "registry+https://github.com/rust-lang/crates.io-index" 1372 | checksum = "567b8a2dae586314f7be2a752ec7474332959c6460e02bde30d702a66d488708" 1373 | dependencies = [ 1374 | "thiserror-impl 2.0.12", 1375 | ] 1376 | 1377 | [[package]] 1378 | name = "thiserror-impl" 1379 | version = "1.0.69" 1380 | source = "registry+https://github.com/rust-lang/crates.io-index" 1381 | checksum = "4fee6c4efc90059e10f81e6d42c60a18f76588c3d74cb83a0b242a2b6c7504c1" 1382 | dependencies = [ 1383 | "proc-macro2", 1384 | "quote", 1385 | "syn", 1386 | ] 1387 | 1388 | [[package]] 1389 | name = "thiserror-impl" 1390 | version = "2.0.12" 1391 | source = "registry+https://github.com/rust-lang/crates.io-index" 1392 | checksum = "7f7cf42b4507d8ea322120659672cf1b9dbb93f8f2d4ecfd6e51350ff5b17a1d" 1393 | dependencies = [ 1394 | "proc-macro2", 1395 | "quote", 1396 | "syn", 1397 | ] 1398 | 1399 | [[package]] 1400 | name = "tiff" 1401 | version = "0.9.1" 1402 | source = "registry+https://github.com/rust-lang/crates.io-index" 1403 | checksum = "ba1310fcea54c6a9a4fd1aad794ecc02c31682f6bfbecdf460bf19533eed1e3e" 1404 | dependencies = [ 1405 | "flate2", 1406 | "jpeg-decoder", 1407 | "weezl", 1408 | ] 1409 | 1410 | [[package]] 1411 | name = "tokio" 1412 | version = "1.44.2" 1413 | source = "registry+https://github.com/rust-lang/crates.io-index" 1414 | checksum = "e6b88822cbe49de4185e3a4cbf8321dd487cf5fe0c5c65695fef6346371e9c48" 1415 | dependencies = [ 1416 | "backtrace", 1417 | "bytes", 1418 | "libc", 1419 | "mio", 1420 | "parking_lot", 1421 | "pin-project-lite", 1422 | "signal-hook-registry", 1423 | "socket2", 1424 | "tokio-macros", 1425 | "windows-sys 0.52.0", 1426 | ] 1427 | 1428 | [[package]] 1429 | name = "tokio-macros" 1430 | version = "2.5.0" 1431 | source = "registry+https://github.com/rust-lang/crates.io-index" 1432 | checksum = "6e06d43f1345a3bcd39f6a56dbb7dcab2ba47e68e8ac134855e7e2bdbaf8cab8" 1433 | dependencies = [ 1434 | "proc-macro2", 1435 | "quote", 1436 | "syn", 1437 | ] 1438 | 1439 | [[package]] 1440 | name = "tui-big-text" 1441 | version = "0.7.1" 1442 | source = "registry+https://github.com/rust-lang/crates.io-index" 1443 | checksum = "a97cefa9f1425ab6146db2961241cec86845d11105b5dd6bb504294b0cdd21af" 1444 | dependencies = [ 1445 | "derive_builder", 1446 | "font8x8", 1447 | "itertools 0.14.0", 1448 | "ratatui", 1449 | ] 1450 | 1451 | [[package]] 1452 | name = "tui-rain" 1453 | version = "1.0.1" 1454 | source = "registry+https://github.com/rust-lang/crates.io-index" 1455 | checksum = "d279b27af071f5e31fef4ecd68f7c8b0b14ed9b97e8900c4ec66bf7ffbb216de" 1456 | dependencies = [ 1457 | "rand 0.8.5", 1458 | "rand_pcg 0.3.1", 1459 | "ratatui", 1460 | ] 1461 | 1462 | [[package]] 1463 | name = "unicode-ident" 1464 | version = "1.0.18" 1465 | source = "registry+https://github.com/rust-lang/crates.io-index" 1466 | checksum = "5a5f39404a5da50712a4c1eecf25e90dd62b613502b7e925fd4e4d19b5c96512" 1467 | 1468 | [[package]] 1469 | name = "unicode-segmentation" 1470 | version = "1.12.0" 1471 | source = "registry+https://github.com/rust-lang/crates.io-index" 1472 | checksum = "f6ccf251212114b54433ec949fd6a7841275f9ada20dddd2f29e9ceea4501493" 1473 | 1474 | [[package]] 1475 | name = "unicode-truncate" 1476 | version = "1.1.0" 1477 | source = "registry+https://github.com/rust-lang/crates.io-index" 1478 | checksum = "b3644627a5af5fa321c95b9b235a72fd24cd29c648c2c379431e6628655627bf" 1479 | dependencies = [ 1480 | "itertools 0.13.0", 1481 | "unicode-segmentation", 1482 | "unicode-width 0.1.14", 1483 | ] 1484 | 1485 | [[package]] 1486 | name = "unicode-width" 1487 | version = "0.1.14" 1488 | source = "registry+https://github.com/rust-lang/crates.io-index" 1489 | checksum = "7dd6e30e90baa6f72411720665d41d89b9a3d039dc45b8faea1ddd07f617f6af" 1490 | 1491 | [[package]] 1492 | name = "unicode-width" 1493 | version = "0.2.0" 1494 | source = "registry+https://github.com/rust-lang/crates.io-index" 1495 | checksum = "1fc81956842c57dac11422a97c3b8195a1ff727f06e85c84ed2e8aa277c9a0fd" 1496 | 1497 | [[package]] 1498 | name = "utf8parse" 1499 | version = "0.2.2" 1500 | source = "registry+https://github.com/rust-lang/crates.io-index" 1501 | checksum = "06abde3611657adf66d383f00b093d7faecc7fa57071cce2578660c9f1010821" 1502 | 1503 | [[package]] 1504 | name = "wana_kana" 1505 | version = "4.0.0" 1506 | source = "registry+https://github.com/rust-lang/crates.io-index" 1507 | checksum = "a74666202acfcb4f9b995be2e3e9f7f530deb65e05a1407b8d0b30c9c451238a" 1508 | dependencies = [ 1509 | "fnv", 1510 | "itertools 0.10.5", 1511 | "lazy_static", 1512 | ] 1513 | 1514 | [[package]] 1515 | name = "wasi" 1516 | version = "0.11.0+wasi-snapshot-preview1" 1517 | source = "registry+https://github.com/rust-lang/crates.io-index" 1518 | checksum = "9c8d87e72b64a3b4db28d11ce29237c246188f4f51057d65a7eab63b7987e423" 1519 | 1520 | [[package]] 1521 | name = "wasi" 1522 | version = "0.14.2+wasi-0.2.4" 1523 | source = "registry+https://github.com/rust-lang/crates.io-index" 1524 | checksum = "9683f9a5a998d873c0d21fcbe3c083009670149a8fab228644b8bd36b2c48cb3" 1525 | dependencies = [ 1526 | "wit-bindgen-rt", 1527 | ] 1528 | 1529 | [[package]] 1530 | name = "weezl" 1531 | version = "0.1.8" 1532 | source = "registry+https://github.com/rust-lang/crates.io-index" 1533 | checksum = "53a85b86a771b1c87058196170769dd264f66c0782acf1ae6cc51bfd64b39082" 1534 | 1535 | [[package]] 1536 | name = "winapi" 1537 | version = "0.3.9" 1538 | source = "registry+https://github.com/rust-lang/crates.io-index" 1539 | checksum = "5c839a674fcd7a98952e593242ea400abe93992746761e38641405d28b00f419" 1540 | dependencies = [ 1541 | "winapi-i686-pc-windows-gnu", 1542 | "winapi-x86_64-pc-windows-gnu", 1543 | ] 1544 | 1545 | [[package]] 1546 | name = "winapi-i686-pc-windows-gnu" 1547 | version = "0.4.0" 1548 | source = "registry+https://github.com/rust-lang/crates.io-index" 1549 | checksum = "ac3b87c63620426dd9b991e5ce0329eff545bccbbb34f3be09ff6fb6ab51b7b6" 1550 | 1551 | [[package]] 1552 | name = "winapi-x86_64-pc-windows-gnu" 1553 | version = "0.4.0" 1554 | source = "registry+https://github.com/rust-lang/crates.io-index" 1555 | checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f" 1556 | 1557 | [[package]] 1558 | name = "windows-sys" 1559 | version = "0.52.0" 1560 | source = "registry+https://github.com/rust-lang/crates.io-index" 1561 | checksum = "282be5f36a8ce781fad8c8ae18fa3f9beff57ec1b52cb3de0789201425d9a33d" 1562 | dependencies = [ 1563 | "windows-targets", 1564 | ] 1565 | 1566 | [[package]] 1567 | name = "windows-sys" 1568 | version = "0.59.0" 1569 | source = "registry+https://github.com/rust-lang/crates.io-index" 1570 | checksum = "1e38bc4d79ed67fd075bcc251a1c39b32a1776bbe92e5bef1f0bf1f8c531853b" 1571 | dependencies = [ 1572 | "windows-targets", 1573 | ] 1574 | 1575 | [[package]] 1576 | name = "windows-targets" 1577 | version = "0.52.6" 1578 | source = "registry+https://github.com/rust-lang/crates.io-index" 1579 | checksum = "9b724f72796e036ab90c1021d4780d4d3d648aca59e491e6b98e725b84e99973" 1580 | dependencies = [ 1581 | "windows_aarch64_gnullvm", 1582 | "windows_aarch64_msvc", 1583 | "windows_i686_gnu", 1584 | "windows_i686_gnullvm", 1585 | "windows_i686_msvc", 1586 | "windows_x86_64_gnu", 1587 | "windows_x86_64_gnullvm", 1588 | "windows_x86_64_msvc", 1589 | ] 1590 | 1591 | [[package]] 1592 | name = "windows_aarch64_gnullvm" 1593 | version = "0.52.6" 1594 | source = "registry+https://github.com/rust-lang/crates.io-index" 1595 | checksum = "32a4622180e7a0ec044bb555404c800bc9fd9ec262ec147edd5989ccd0c02cd3" 1596 | 1597 | [[package]] 1598 | name = "windows_aarch64_msvc" 1599 | version = "0.52.6" 1600 | source = "registry+https://github.com/rust-lang/crates.io-index" 1601 | checksum = "09ec2a7bb152e2252b53fa7803150007879548bc709c039df7627cabbd05d469" 1602 | 1603 | [[package]] 1604 | name = "windows_i686_gnu" 1605 | version = "0.52.6" 1606 | source = "registry+https://github.com/rust-lang/crates.io-index" 1607 | checksum = "8e9b5ad5ab802e97eb8e295ac6720e509ee4c243f69d781394014ebfe8bbfa0b" 1608 | 1609 | [[package]] 1610 | name = "windows_i686_gnullvm" 1611 | version = "0.52.6" 1612 | source = "registry+https://github.com/rust-lang/crates.io-index" 1613 | checksum = "0eee52d38c090b3caa76c563b86c3a4bd71ef1a819287c19d586d7334ae8ed66" 1614 | 1615 | [[package]] 1616 | name = "windows_i686_msvc" 1617 | version = "0.52.6" 1618 | source = "registry+https://github.com/rust-lang/crates.io-index" 1619 | checksum = "240948bc05c5e7c6dabba28bf89d89ffce3e303022809e73deaefe4f6ec56c66" 1620 | 1621 | [[package]] 1622 | name = "windows_x86_64_gnu" 1623 | version = "0.52.6" 1624 | source = "registry+https://github.com/rust-lang/crates.io-index" 1625 | checksum = "147a5c80aabfbf0c7d901cb5895d1de30ef2907eb21fbbab29ca94c5b08b1a78" 1626 | 1627 | [[package]] 1628 | name = "windows_x86_64_gnullvm" 1629 | version = "0.52.6" 1630 | source = "registry+https://github.com/rust-lang/crates.io-index" 1631 | checksum = "24d5b23dc417412679681396f2b49f3de8c1473deb516bd34410872eff51ed0d" 1632 | 1633 | [[package]] 1634 | name = "windows_x86_64_msvc" 1635 | version = "0.52.6" 1636 | source = "registry+https://github.com/rust-lang/crates.io-index" 1637 | checksum = "589f6da84c646204747d1270a2a5661ea66ed1cced2631d546fdfb155959f9ec" 1638 | 1639 | [[package]] 1640 | name = "wit-bindgen-rt" 1641 | version = "0.39.0" 1642 | source = "registry+https://github.com/rust-lang/crates.io-index" 1643 | checksum = "6f42320e61fe2cfd34354ecb597f86f413484a798ba44a8ca1165c58d42da6c1" 1644 | dependencies = [ 1645 | "bitflags 2.9.0", 1646 | ] 1647 | 1648 | [[package]] 1649 | name = "zerocopy" 1650 | version = "0.8.24" 1651 | source = "registry+https://github.com/rust-lang/crates.io-index" 1652 | checksum = "2586fea28e186957ef732a5f8b3be2da217d65c5969d4b1e17f973ebbe876879" 1653 | dependencies = [ 1654 | "zerocopy-derive", 1655 | ] 1656 | 1657 | [[package]] 1658 | name = "zerocopy-derive" 1659 | version = "0.8.24" 1660 | source = "registry+https://github.com/rust-lang/crates.io-index" 1661 | checksum = "a996a8f63c5c4448cd959ac1bab0aaa3306ccfd060472f85943ee0750f0169be" 1662 | dependencies = [ 1663 | "proc-macro2", 1664 | "quote", 1665 | "syn", 1666 | ] 1667 | 1668 | [[package]] 1669 | name = "zune-inflate" 1670 | version = "0.2.54" 1671 | source = "registry+https://github.com/rust-lang/crates.io-index" 1672 | checksum = "73ab332fe2f6680068f3582b16a24f90ad7096d5d39b974d1c0aff0125116f02" 1673 | dependencies = [ 1674 | "simd-adler32", 1675 | ] 1676 | -------------------------------------------------------------------------------- /Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "kanash" 3 | version = "0.1.4" 4 | authors = ["Benoit Leroux "] 5 | edition = "2021" 6 | description = "Learn Hiragana and Katakana in a terminal !" 7 | repository = "https://github.com/benoitlx/kanash" 8 | license = "MIT" 9 | keywords = ["ratatui", "learning", "japanese", "TUI"] 10 | 11 | [dependencies] 12 | anyhow = "1.0.97" 13 | rand = "0.9.0" 14 | rand_core = "0.6.4" 15 | ratatui = "0.29.0" 16 | wana_kana = "4.0.0" 17 | tachyonfx = "0.13.0" 18 | tui-rain = "1.0.1" 19 | tokio = { version = "1.44.2", features = ["full"] } 20 | futures = "0.3.31" 21 | tui-big-text = "0.7.1" 22 | #ratatui-image = "5.0.0" 23 | #image = "0.25.6" 24 | rand_pcg = "0.9.0" 25 | ansi-to-tui = "7.0.0" 26 | rascii_art = "0.4.5" 27 | clap = { version = "4.5.37", features = ["derive"] } 28 | 29 | [dev-dependencies] 30 | crossterm = "0.29.0" 31 | -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | FROM rust:latest AS builder 2 | 3 | # Copy sources inside the builder container 4 | COPY ./Cargo.lock ./Cargo.toml ./ 5 | COPY ./src ./src 6 | RUN rustup target add x86_64-unknown-linux-musl 7 | RUN cargo build --target x86_64-unknown-linux-musl 8 | 9 | FROM alpine:latest 10 | 11 | COPY --from=builder /target/x86_64-unknown-linux-musl/debug/kanash /usr/bin 12 | RUN apk update && apk add ttyd 13 | 14 | CMD ["/usr/bin/ttyd", "-W", "/usr/bin/kanash", "--path", "/home/assets"] 15 | 16 | # LABEL \ 17 | # org.opencontainers.image.title="kanash" \ 18 | # org.opencontainers.image.description="learn kana in a terminal" \ 19 | # org.opencontainers.image.authors="Benoit Leroux" \ 20 | # org.opencontainers.image.licenses="MIT" \ 21 | # org.opencontainers.image.source="https://github.com/${BUILD_REPOSITORY}" \ 22 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2025 Benoit 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # kanash 2 | 3 | Learn Kana in a terminal ! 4 | See https://kana.rezoleo.fr for a demo. 5 | 6 | ![demo](./assets/demo.gif) 7 | 8 | If your goal is to learn Japanese, you should take a look at [Awesome-Japanese](https://github.com/yudataguy/Awesome-Japanese) first. 9 | 10 | > [!NOTE] 11 | > I'm pausing the ssh server part because I found a way to expose my TUI through http with [`ttyd`](https://github.com/tsl0922/ttyd) 12 | 13 | ## Usage 14 | 15 | ### From the binary in the [release](https://github.com/benoitlx/kanash/releases/) 16 | 17 | ``` 18 | chmod +x kanash 19 | ./kanash 20 | ``` 21 | 22 | > [!NOTE] 23 | > Only work on `x86_64` for now 24 | 25 | ### With `cargo` 26 | 27 | ``` 28 | cargo install kanash 29 | ``` 30 | 31 | ### On Arch Linux (AUR) 32 | 33 | ``` 34 | paru -S kanash 35 | ``` 36 | 37 | ### From docker image 38 | 39 | To expose it as a website : 40 | 41 | > [!TIP] 42 | > replace `./assets` with a directory containing `jpg` and `png` 43 | 44 | ``` 45 | docker run --rm -v ./assets:/home/assets -p "80:7681" blximages/kanash 46 | ``` 47 | 48 | To run it directly in your terminal 49 | 50 | ``` 51 | docker run --rm -v ./assets:/home/assets -it --entrypoint=/usr/bin/kanash blximages/kanash 52 | ``` 53 | 54 | ## TODO 55 | 56 | - [x] Rust build and test CI 57 | - [x] Use ttyd instead of gotty 58 | - [x] enum for color palette 59 | - [x] Add a parameter to the creation of a Kana Page (to know wheter to show hira kata or both, based on the selection in the Home Page) 60 | - [x] Refactor the `app.rs` using the Elm architecture 61 | - [x] move japanese helper function to another file 62 | - [x] isolate the kana ui into one component 63 | - [ ] ~~look at rust multithreading and tokio~~ (Only using `event::poll(Duration::from_millis(10)).unwrap()` in `handle_event` in order not to block the rendering) 64 | - [x] Better UI for Kana 65 | - [x] tui-rain 66 | - [x] splash screen 67 | - [ ] add a list of unused hiragana you don't want to show 68 | - [ ] look at how to do test with ratatui 69 | - [ ] take a look at ratzilla and wasm 70 | - [ ] make a login page in order to display statistic to users 71 | - [ ] ~~look at https://github.com/arthepsy/ssh-audit~~ (see the first note) 72 | 73 | ## Contribute 74 | 75 | **Advices** and **PRs** are very much apreciated 76 | 77 | ## Acknowledgments 78 | 79 | - [ratatui](https://github.com/ratatui/ratatui) :heart: 80 | - [ttyd](https://github.com/tsl0922/ttyd) 81 | - [wana-kana-rust](https://github.com/PSeitz/wana_kana_rust) 82 | 83 | Also take a look at all the dependencies in [`Cargo.toml`](./Cargo.toml) 84 | -------------------------------------------------------------------------------- /assets/demo.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/benoitlx/kanash/69ac3db84230e03eadbd4b0d9ed6856d6decf88e/assets/demo.gif -------------------------------------------------------------------------------- /assets/gate_low_res.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/benoitlx/kanash/69ac3db84230e03eadbd4b0d9ed6856d6decf88e/assets/gate_low_res.jpg -------------------------------------------------------------------------------- /docker-compose.yaml: -------------------------------------------------------------------------------- 1 | services: 2 | kanash: 3 | privileged: true 4 | image: bleroux/kanash 5 | volumes: 6 | - ./assets:/home/assets 7 | ports: 8 | - 80:7681 9 | restart: unless-stopped 10 | -------------------------------------------------------------------------------- /src/components/app.rs: -------------------------------------------------------------------------------- 1 | use super::{home::HomeModel, kana::KanaModel, *}; 2 | use crate::components::helper::rain; 3 | use ansi_to_tui::IntoText; 4 | use rascii_art::{render_to, RenderOptions}; 5 | use ratatui::text::Text; 6 | 7 | #[derive(Debug, PartialEq, Eq)] 8 | enum AppPage { 9 | Home(HomeModel), 10 | Kana(KanaModel), 11 | } 12 | 13 | #[derive(Debug, PartialEq, Eq)] 14 | pub struct App { 15 | pub exit: bool, 16 | background_widget: Box>, 17 | page: AppPage, 18 | previous_height: u16, 19 | disable_rain: bool, 20 | pub disable_background: bool, 21 | background_number: usize, 22 | pub background_paths: Vec, 23 | } 24 | 25 | impl Components for App { 26 | fn new() -> Self { 27 | let home = HomeModel::new(); 28 | 29 | Self { 30 | exit: false, 31 | page: AppPage::Home(home), 32 | background_widget: Box::new(Text::default()), 33 | previous_height: 0, 34 | disable_rain: false, 35 | disable_background: false, 36 | background_paths: vec![], 37 | background_number: 0, 38 | } 39 | } 40 | 41 | fn handle_event(&self) -> Option { 42 | match &self.page { 43 | AppPage::Home(h) => h.handle_event(), 44 | AppPage::Kana(k) => k.handle_event(), 45 | } 46 | } 47 | 48 | fn update(&mut self, msg: Message) -> Option { 49 | match &mut self.page { 50 | AppPage::Home(ref mut h) => { 51 | // quit if msg == Message::Back 52 | if msg == Message::Back { 53 | self.exit = true; 54 | return None; 55 | } 56 | 57 | if msg == Message::Home(HomeMessage::RainFx) { 58 | self.disable_rain = !self.disable_rain; 59 | return None; 60 | } 61 | 62 | if msg == Message::Home(HomeMessage::Background) { 63 | match h.background_state { 64 | BackgroundMode::Cycle => { 65 | self.background_number += 1; 66 | self.previous_height = 0; 67 | 68 | if self.background_number + 1 == self.background_paths.len() { 69 | h.key_helper_state = BackgroundMode::Disable 70 | } 71 | 72 | if self.background_number >= self.background_paths.len() { 73 | self.disable_background = true; 74 | self.background_number = 0; 75 | h.background_state = BackgroundMode::Disable; 76 | h.key_helper_state = BackgroundMode::Cycle; 77 | } 78 | } 79 | BackgroundMode::Disable => { 80 | h.background_state = BackgroundMode::Cycle; 81 | self.disable_background = false; 82 | } 83 | } 84 | 85 | return None; 86 | } 87 | 88 | if msg == Message::Home(HomeMessage::Up) || msg == Message::Home(HomeMessage::Down) 89 | { 90 | let response = h.update(msg.clone()); 91 | return response; 92 | } 93 | 94 | // transform self en App::Kana(new_kana(selected)) if msg == Message::Home(Enter) 95 | if let Message::Home(HomeMessage::Enter(mode)) = msg { 96 | let mut new_kana = KanaModel::new(); 97 | 98 | new_kana.mode = mode; 99 | new_kana.update(Message::Kana(KanaMessage::Pass)); 100 | 101 | self.page = AppPage::Kana(new_kana); 102 | } 103 | 104 | None 105 | } 106 | AppPage::Kana(ref mut k) => { 107 | let response = k.update(msg.clone()); 108 | 109 | // transform self en App::Home(new_home) if msg == Message::Back 110 | if msg == Message::Back { 111 | let home = HomeModel::new(); 112 | 113 | self.page = AppPage::Home(home); 114 | } 115 | 116 | response 117 | } 118 | } 119 | } 120 | 121 | fn view(&mut self, frame: &mut Frame, elapsed: Duration) { 122 | if !self.disable_background { 123 | self.background(frame); 124 | } 125 | if !self.disable_rain { 126 | rain::view(frame, elapsed); 127 | } 128 | match &mut self.page { 129 | AppPage::Home(ref mut h) => h.view(frame, elapsed), 130 | AppPage::Kana(ref mut k) => k.view(frame, elapsed), 131 | } 132 | } 133 | } 134 | 135 | impl App { 136 | fn background(&mut self, frame: &mut Frame) { 137 | let actual_height = frame.area().height; 138 | if self.previous_height != actual_height { 139 | self.write_background(actual_height.into()); 140 | self.previous_height = actual_height; 141 | } 142 | 143 | frame.render_widget((*self.background_widget).clone(), frame.area()); 144 | } 145 | 146 | fn write_background(&mut self, height: u32) { 147 | // needed because otherwise the render_to function take a while to overwrite the previous string 148 | let mut buffer = String::new(); 149 | 150 | if !self.background_paths.is_empty() { 151 | render_to( 152 | self.background_paths[self.background_number].clone(), 153 | &mut buffer, 154 | &RenderOptions::new().height(height).colored(true), 155 | ) 156 | .unwrap(); 157 | } else { 158 | buffer = String::from("No asset directory") 159 | } 160 | 161 | self.background_widget = Box::new(buffer.into_text().unwrap().centered()); 162 | } 163 | } 164 | -------------------------------------------------------------------------------- /src/components/helper/background.rs: -------------------------------------------------------------------------------- 1 | #![allow(dead_code)] 2 | 3 | /// From https://github.com/Levilutz/tui-rain/blob/main/src/lib.rs 4 | /// Thanks to Levi Lutz 5 | use std::{cmp::Ordering, time::Duration}; 6 | 7 | use rand::{RngCore, SeedableRng}; 8 | use rand_pcg::Pcg64Mcg; 9 | use ratatui::{ 10 | buffer::Buffer, 11 | layout::Rect, 12 | style::{Color, Style, Stylize}, 13 | widgets::Widget, 14 | }; 15 | 16 | /// A configuration for the density of the rain effect. 17 | #[derive(Copy, Clone, Eq, PartialEq, Ord, PartialOrd, Hash, Debug)] 18 | pub enum RainDensity { 19 | /// An absolute target number of drops to have in the frame. 20 | Absolute { num_drops: usize }, 21 | 22 | /// Compute the number of drops based on the frame size. Lower value is denser. 23 | /// 24 | /// Is converted to an absolute value, with 1 drop per `sparseness` pixels. 25 | Relative { sparseness: usize }, 26 | 27 | /// A dense rain. Equivalent to `Relative { sparseness: 20 }`. 28 | Dense, 29 | 30 | /// A normal rain. Equivalent to `Relative { sparseness: 50 }`. 31 | Normal, 32 | 33 | /// A sparse rain. Equivalent to `Relative { sparseness: 100 }`. 34 | Sparse, 35 | } 36 | 37 | impl RainDensity { 38 | /// Get the absolute number of drops given an area. 39 | fn num_drops(&self, area: Rect) -> usize { 40 | match self { 41 | RainDensity::Absolute { num_drops } => *num_drops, 42 | RainDensity::Relative { sparseness } if *sparseness == 0 => 0, 43 | RainDensity::Relative { sparseness } => { 44 | (area.width * area.height) as usize / *sparseness 45 | } 46 | RainDensity::Dense => RainDensity::Relative { sparseness: 20 }.num_drops(area), 47 | RainDensity::Normal => RainDensity::Relative { sparseness: 50 }.num_drops(area), 48 | RainDensity::Sparse => RainDensity::Relative { sparseness: 100 }.num_drops(area), 49 | } 50 | } 51 | } 52 | 53 | /// The speed of the rain. 54 | #[derive(Copy, Clone, PartialEq, PartialOrd, Debug)] 55 | pub enum RainSpeed { 56 | /// An absolute target speed in pixels / second. 57 | Absolute { speed: f64 }, 58 | 59 | /// A fast rain. Equivalent to `Absolute { speed: 20.0 }`. 60 | Fast, 61 | 62 | /// A normal rain. Equivalent to `Absolute { speed: 10.0 }`. 63 | Normal, 64 | 65 | /// A slow rain. Equivalent to `Absolute { speed: 5.0 }`. 66 | Slow, 67 | } 68 | 69 | impl RainSpeed { 70 | /// Get the absolute speed. 71 | fn speed(&self) -> f64 { 72 | match self { 73 | RainSpeed::Absolute { speed } => *speed, 74 | RainSpeed::Fast => 20.0, 75 | RainSpeed::Normal => 10.0, 76 | RainSpeed::Slow => 5.0, 77 | } 78 | } 79 | } 80 | 81 | /// A character set for the rain. 82 | #[derive(Clone, Eq, PartialEq, Ord, PartialOrd, Hash, Debug)] 83 | pub enum CharacterSet { 84 | /// An explicit enumeration of character options. This is the least performant. 85 | Explicit { options: Vec }, 86 | 87 | /// A range of unicode values. 88 | UnicodeRange { start: u32, len: u32 }, 89 | 90 | /// Half-width Japanese Kana characters. This is the closest to the original. 91 | /// 92 | /// Equivalent to `CharacterSet::UnicodeRange { start: 0xFF66, len: 56 }`. 93 | HalfKana, 94 | 95 | /// The lowercase English alphabet. 96 | /// 97 | /// Equivalent to `CharacterSet::UnicodeRange { start: 0x61, len: 26 }`. 98 | Lowercase, 99 | } 100 | 101 | impl CharacterSet { 102 | fn get(&self, seed: u32) -> char { 103 | match self { 104 | CharacterSet::Explicit { options } => options[seed as usize % options.len()], 105 | CharacterSet::UnicodeRange { start, len } => { 106 | char::from_u32((seed % len) + start).unwrap() 107 | } 108 | CharacterSet::HalfKana => CharacterSet::UnicodeRange { 109 | start: 0xFF66, 110 | len: 56, 111 | } 112 | .get(seed), 113 | CharacterSet::Lowercase => CharacterSet::UnicodeRange { 114 | start: 0x61, 115 | len: 26, 116 | } 117 | .get(seed), 118 | } 119 | } 120 | 121 | fn size(&self) -> usize { 122 | match self { 123 | CharacterSet::Explicit { options } => options.len(), 124 | CharacterSet::UnicodeRange { start: _, len } => *len as usize, 125 | CharacterSet::HalfKana => 56, 126 | CharacterSet::Lowercase => 26, 127 | } 128 | } 129 | } 130 | 131 | #[derive(Clone, PartialEq, Debug)] 132 | pub struct Rain { 133 | elapsed: Duration, 134 | seed: u64, 135 | rain_density: RainDensity, 136 | rain_speed: RainSpeed, 137 | rain_speed_variance: f64, 138 | tail_lifespan: Duration, 139 | color: Color, 140 | head_color: Color, 141 | bold_dim_effect: bool, 142 | noise_interval: Duration, 143 | character_set: CharacterSet, 144 | } 145 | 146 | impl Rain { 147 | /// Construct a new rain widget with defaults for matrix rain. 148 | pub fn new_matrix(elapsed: Duration) -> Rain { 149 | Rain { 150 | elapsed, 151 | seed: 1234, 152 | rain_density: RainDensity::Normal, 153 | rain_speed: RainSpeed::Slow, 154 | rain_speed_variance: 0.5, 155 | tail_lifespan: Duration::from_secs(2), 156 | color: Color::LightGreen, 157 | head_color: Color::White, 158 | bold_dim_effect: true, 159 | noise_interval: Duration::from_secs(5), 160 | character_set: CharacterSet::HalfKana, 161 | } 162 | } 163 | 164 | /// Construct a new rain widget with defaults for standard rain. 165 | pub fn new_rain(elapsed: Duration) -> Rain { 166 | Rain { 167 | elapsed, 168 | seed: 1234, 169 | rain_density: RainDensity::Dense, 170 | rain_speed: RainSpeed::Fast, 171 | rain_speed_variance: 0.5, 172 | tail_lifespan: Duration::from_millis(250), 173 | color: Color::LightBlue, 174 | head_color: Color::White, 175 | bold_dim_effect: true, 176 | noise_interval: Duration::from_secs(1), 177 | character_set: CharacterSet::UnicodeRange { 178 | start: 0x7c, 179 | len: 1, 180 | }, 181 | } 182 | } 183 | 184 | /// Construct a new rain widget with defaults for snow. 185 | pub fn new_snow(elapsed: Duration) -> Rain { 186 | Rain { 187 | elapsed, 188 | seed: 1234, 189 | rain_density: RainDensity::Dense, 190 | rain_speed: RainSpeed::Absolute { speed: 2.0 }, 191 | rain_speed_variance: 0.1, 192 | tail_lifespan: Duration::from_millis(500), 193 | color: Color::White, 194 | head_color: Color::White, 195 | bold_dim_effect: true, 196 | noise_interval: Duration::from_secs(1), 197 | character_set: CharacterSet::UnicodeRange { 198 | start: 0x2a, 199 | len: 1, 200 | }, 201 | } 202 | } 203 | 204 | /// Construct a new rain widget with defaults for emoji soup. 205 | /// 206 | /// Terminals that render emojis as two characters wide will not enjoy this. 207 | pub fn new_emoji_soup(elapsed: Duration) -> Rain { 208 | Rain { 209 | elapsed, 210 | seed: 1234, 211 | rain_density: RainDensity::Dense, 212 | rain_speed: RainSpeed::Normal, 213 | rain_speed_variance: 0.1, 214 | tail_lifespan: Duration::from_millis(500), 215 | color: Color::White, 216 | head_color: Color::White, 217 | bold_dim_effect: true, 218 | noise_interval: Duration::from_secs(1), 219 | character_set: CharacterSet::UnicodeRange { 220 | start: 0x1f600, 221 | len: 80, 222 | }, 223 | } 224 | } 225 | 226 | /// Set the random seed for the generation. 227 | /// 228 | /// The random seed can be configured. Given a constant screen size, results should 229 | /// be reproducible across executions, operating systems, and architectures. 230 | /// 231 | /// ``` 232 | /// use std::time::Duration; 233 | /// use tui_rain::Rain; 234 | /// 235 | /// let elapsed = Duration::from_secs(5); 236 | /// 237 | /// Rain::new_matrix(elapsed) 238 | /// .with_seed(1234); 239 | /// ``` 240 | pub fn with_seed(mut self, seed: u64) -> Rain { 241 | self.seed = seed; 242 | self 243 | } 244 | 245 | /// Set the target density for the rain. 246 | /// 247 | /// This can be configured as an absolute number of drops: 248 | /// 249 | /// ``` 250 | /// use std::time::Duration; 251 | /// use tui_rain::{Rain, RainDensity}; 252 | /// 253 | /// Rain::new_matrix(Duration::from_secs(0)) 254 | /// .with_rain_density(RainDensity::Absolute { 255 | /// num_drops: 100, 256 | /// }); 257 | /// ``` 258 | /// Or a ratio of screen pixels to drops (lower is more dense): 259 | /// 260 | /// ``` 261 | /// use std::time::Duration; 262 | /// use tui_rain::{Rain, RainDensity}; 263 | /// 264 | /// Rain::new_matrix(Duration::from_secs(0)) 265 | /// .with_rain_density(RainDensity::Relative { 266 | /// sparseness: 50, 267 | /// }); 268 | /// ``` 269 | /// 270 | /// The actual number of drops on the screen at any time is randomly distributed 271 | /// between 0 and twice the target. 272 | /// 273 | /// Preset relative options include: 274 | /// 275 | /// - `RainDensity::Sparse` 276 | /// - `RainDensity::Normal` 277 | /// - `RainDensity::Dense` 278 | pub fn with_rain_density(mut self, rain_density: RainDensity) -> Rain { 279 | self.rain_density = rain_density; 280 | self 281 | } 282 | 283 | /// Set the target speed for the rain. 284 | /// 285 | /// Speed can be configured as an absolute value of pixels per second, or as a 286 | /// preset. 287 | /// 288 | /// For an absolute speed in pixels per second: 289 | /// 290 | /// ``` 291 | /// use std::time::Duration; 292 | /// use tui_rain::{Rain, RainSpeed}; 293 | /// 294 | /// let elapsed = Duration::from_secs(5); 295 | /// 296 | /// Rain::new_matrix(elapsed) 297 | /// .with_rain_speed(RainSpeed::Absolute { 298 | /// speed: 10.0, 299 | /// }); 300 | /// ``` 301 | /// 302 | /// Preset options include: 303 | /// 304 | /// - `RainSpeed::Slow` 305 | /// - `RainSpeed::Normal` 306 | /// - `RainSpeed::Fast` 307 | pub fn with_rain_speed(mut self, rain_speed: RainSpeed) -> Rain { 308 | self.rain_speed = rain_speed; 309 | self 310 | } 311 | 312 | /// Set the rain speed variance. 313 | /// 314 | /// To avoid perfectly consistent patterns, you can configure some variance in the 315 | /// speed of each drop. This can also give an impression of parallax (depth). 316 | /// 317 | /// For example, a value of `0.1` will cause each drop's speed to be uniformly 318 | /// distrbuted within ±10% of the target speed: 319 | /// 320 | /// ``` 321 | /// use std::time::Duration; 322 | /// use tui_rain::Rain; 323 | /// 324 | /// let elapsed = Duration::from_secs(5); 325 | /// 326 | /// Rain::new_matrix(elapsed) 327 | /// .with_rain_speed_variance(0.1); 328 | /// ``` 329 | /// 330 | /// The speed of an individual drop will never go below 0.001 pixels / second, but 331 | /// can vary arbitrarily high. 332 | pub fn with_rain_speed_variance(mut self, rain_speed_variance: f64) -> Rain { 333 | self.rain_speed_variance = rain_speed_variance; 334 | self 335 | } 336 | 337 | /// Set the tail lifespan for the rain. 338 | /// 339 | /// You can make the rain drop tails appear shorter / longer by configuring how long 340 | /// the tail effect lasts: 341 | /// 342 | /// ``` 343 | /// use std::time::Duration; 344 | /// use tui_rain::Rain; 345 | /// 346 | /// let elapsed = Duration::from_secs(5); 347 | /// 348 | /// Rain::new_matrix(elapsed) 349 | /// .with_tail_lifespan(Duration::from_secs(5)); 350 | /// ``` 351 | /// 352 | /// The drop length is capped at the screen height to avoid strange wraparound 353 | /// effects. 354 | pub fn with_tail_lifespan(mut self, tail_lifespan: Duration) -> Rain { 355 | self.tail_lifespan = tail_lifespan; 356 | self 357 | } 358 | 359 | /// Set the color for the rain. 360 | /// 361 | /// You can change the tail color for each drop: 362 | /// 363 | /// ``` 364 | /// use std::time::Duration; 365 | /// use tui_rain::Rain; 366 | /// 367 | /// let elapsed = Duration::from_secs(5); 368 | /// 369 | /// Rain::new_matrix(elapsed) 370 | /// .with_color(ratatui::style::Color::LightGreen); 371 | /// ``` 372 | /// 373 | /// The color of the head is [independently configured](Rain::with_head_color). The 374 | /// bold / dim effects that automatically get applied over a drop's length may tweak 375 | /// the color inadvertently, but [this can be disabled](Rain::with_bold_dim_effect). 376 | pub fn with_color(mut self, color: Color) -> Rain { 377 | self.color = color; 378 | self 379 | } 380 | 381 | /// Set the head color for the rain. 382 | /// 383 | /// You can change the head color for each drop: 384 | /// 385 | /// ``` 386 | /// use std::time::Duration; 387 | /// use tui_rain::Rain; 388 | /// 389 | /// let elapsed = Duration::from_secs(5); 390 | /// 391 | /// Rain::new_matrix(elapsed) 392 | /// .with_head_color(ratatui::style::Color::Green); 393 | /// ``` 394 | /// 395 | /// The color of the tail is [independently configured](Rain::with_color). The 396 | /// bold / dim effects that automatically get applied over a drop's length may tweak 397 | /// the color inadvertently, but [this can be disabled](Rain::with_bold_dim_effect). 398 | pub fn with_head_color(mut self, head_color: Color) -> Rain { 399 | self.head_color = head_color; 400 | self 401 | } 402 | 403 | /// Set whether to apply the bold / dim effect. 404 | /// 405 | /// By default, the lower third of each drop has the bold effect applied, and the 406 | /// upper third has the dim effect applied. This produces an impression of the drop 407 | /// fading instead of abruptly ending. 408 | /// 409 | /// This may tweak the color of glyphs away from the base color on some terminals, 410 | /// so it can be disabled if desired: 411 | /// 412 | /// ``` 413 | /// use std::time::Duration; 414 | /// use tui_rain::Rain; 415 | /// 416 | /// let elapsed = Duration::from_secs(5); 417 | /// 418 | /// Rain::new_matrix(elapsed) 419 | /// .with_bold_dim_effect(false); 420 | ///``` 421 | pub fn with_bold_dim_effect(mut self, bold_dim_effect: bool) -> Rain { 422 | self.bold_dim_effect = bold_dim_effect; 423 | self 424 | } 425 | 426 | /// Set the interval between random character changes. 427 | /// 428 | /// A more subtle effect is that glyphs already rendered in a drop occasionally 429 | /// switch characters before dissapearing. The time interval between each character 430 | /// switch is per-glyph, and can be adjusted: 431 | /// 432 | /// ``` 433 | /// use std::time::Duration; 434 | /// use tui_rain::Rain; 435 | /// 436 | /// let elapsed = Duration::from_secs(5); 437 | /// 438 | /// Rain::new_matrix(elapsed) 439 | /// .with_noise_interval(Duration::from_secs(10)); 440 | /// ``` 441 | pub fn with_noise_interval(mut self, noise_interval: Duration) -> Rain { 442 | self.noise_interval = noise_interval; 443 | self 444 | } 445 | 446 | /// Set the character set for the drops. 447 | /// 448 | /// The simplest option is to provide an explicit set of characters to choose from: 449 | /// 450 | /// ``` 451 | /// use std::time::Duration; 452 | /// use tui_rain::{CharacterSet, Rain}; 453 | /// 454 | /// let elapsed = Duration::from_secs(5); 455 | /// 456 | /// Rain::new_matrix(elapsed) 457 | /// .with_character_set(CharacterSet::Explicit { 458 | /// options: vec!['a', 'b', 'c'], 459 | /// }); 460 | /// ``` 461 | /// 462 | /// More performant is to provide a unicode range: 463 | /// 464 | /// ``` 465 | /// use std::time::Duration; 466 | /// use tui_rain::{CharacterSet, Rain}; 467 | /// 468 | /// let elapsed = Duration::from_secs(5); 469 | /// 470 | /// Rain::new_matrix(elapsed) 471 | /// .with_character_set(CharacterSet::UnicodeRange { 472 | /// start: 0x61, 473 | /// len: 26, 474 | /// }); 475 | /// ``` 476 | /// 477 | /// Preset unicode ranges include: 478 | /// 479 | /// - `CharacterSet::HalfKana` is the half-width Japanese kana character set (used 480 | /// in the classic matrix rain) 481 | /// - `CharacterSet::Lowercase` is the lowercase English character set 482 | pub fn with_character_set(mut self, character_set: CharacterSet) -> Rain { 483 | self.character_set = character_set; 484 | self 485 | } 486 | 487 | /// Build the rng. Uses a fast but portable and reproducible rng. 488 | fn build_rng(&self) -> impl RngCore { 489 | Pcg64Mcg::seed_from_u64(self.seed) 490 | } 491 | 492 | /// Build a drop from the given consistent initial entropy state. 493 | /// 494 | /// The entropy vector's length becomes the drop's track length, so ensure it's at 495 | /// least the window height. 496 | fn build_drop(&self, entropy: Vec, width: u16, height: u16) -> Vec { 497 | let elapsed = self.elapsed.as_secs_f64(); 498 | let rain_speed = self.rain_speed.speed(); 499 | let tail_lifespan = self.tail_lifespan.as_secs_f64(); 500 | let noise_interval = self.noise_interval.as_secs_f64(); 501 | 502 | // A single drop can expect to be called with the exact same entropy vec on each 503 | // frame. This means we can sample the entropy vec to reproducibly generate 504 | // features every frame (e.g. speed). 505 | 506 | // Later code assumes at least 1 entry in the entropy vec, so break early if not. 507 | if entropy.is_empty() { 508 | return vec![]; 509 | } 510 | 511 | // The length of the entropy vec becomes the length of the drop's track. 512 | // This track is usually longer than the screen height by a random amount. 513 | let track_len = entropy.len() as u16; 514 | 515 | // Use some entropy to compute the drop's actual speed. 516 | // n.b. since the entropy vec is stable, the drop's speed will not vary over time. 517 | let rain_speed = uniform( 518 | entropy[0], 519 | rain_speed * (1.0 - self.rain_speed_variance), 520 | rain_speed * (1.0 + self.rain_speed_variance), 521 | ) 522 | .max(1e-3); // Prevent speed from hitting 0 (if user specifies high variance) 523 | 524 | // Compute how long our drop will take to make 1 cycle given our track len and speed 525 | let cycle_time_secs = entropy.len() as f64 / rain_speed; 526 | 527 | // Use some entropy to compute a stable random time offset for this drop. 528 | // If this value were 0, every drop would start falling with an identical y value. 529 | let initial_cycle_offset_secs = uniform(entropy[0], 0.0, cycle_time_secs); 530 | 531 | // Compute how far we are into the current cycle and current drop head height. 532 | let current_cycle_offset_secs = (elapsed + initial_cycle_offset_secs) % cycle_time_secs; 533 | let head_y = (current_cycle_offset_secs * rain_speed) as u16; 534 | 535 | // Compute drop length given speed and tail lifespan. 536 | // Cap at screen height to avoid weird wraparound when tail length is long. 537 | let drop_len = ((rain_speed * tail_lifespan) as u16).min(height); 538 | 539 | // Render each glyph in the drop. 540 | (0..drop_len) 541 | .filter_map(|y_offset| { 542 | // Compute how long ago this glyph would have first appeared 543 | let age = y_offset as f64 / rain_speed; 544 | 545 | // If it would have first appeared before the rendering began, don't render. 546 | if age > elapsed { 547 | return None; 548 | } 549 | 550 | // Compute which cycle this particular glyph is a member of 551 | let cycle_num = 552 | ((elapsed + initial_cycle_offset_secs - age) / cycle_time_secs) as usize; 553 | 554 | // Don't render glyphs from cycle 0 555 | // (prevents drops from appearing to spawn in the middle of the screen) 556 | if cycle_num == 0 { 557 | return None; 558 | } 559 | 560 | // Get stable entropy to decide what column cycle X is rendered in. 561 | // This must be per-glyph to prevent drops from jumping side-to-side when they wrap around. 562 | let x_entropy = entropy[cycle_num % entropy.len()]; 563 | let x = (x_entropy % width as u64) as u16; 564 | 565 | // Compute the y value for this glyph, and don't render if off the screen. 566 | let y = (head_y + track_len - y_offset) % track_len; 567 | if y >= height { 568 | return None; 569 | } 570 | 571 | // The 'noise' of glyphs randomly changing is actually modeled as every glyph in the track 572 | // just cycling through possible values veeeery slowly. We need a random offset for this 573 | // cycling so every glyph doesn't change at the same time. 574 | let time_offset = uniform( 575 | entropy[y as usize], 576 | 0.0, 577 | noise_interval * self.character_set.size() as f64, 578 | ); 579 | 580 | // Decide what character is rendered based on noise. 581 | let content = self 582 | .character_set 583 | .get(((time_offset + elapsed) / noise_interval) as u32); 584 | 585 | // Compute the styling for the glyph 586 | let mut style = Style::default(); 587 | 588 | // Color appropriately depending on whether this glyph is the head. 589 | if age > 0.0 { 590 | style = style.fg(self.color) 591 | } else { 592 | style = style.fg(self.head_color) 593 | } 594 | 595 | // The lowest third of glyphs is bold, the highest third is dim 596 | if self.bold_dim_effect { 597 | if y_offset < drop_len / 3 { 598 | style = style.bold().not_dim() 599 | } else if y_offset > drop_len * 2 / 3 { 600 | style = style.dim().not_bold() 601 | } else { 602 | style = style.not_bold().not_dim() 603 | } 604 | } 605 | 606 | Some(Glyph { 607 | x, 608 | y, 609 | age, 610 | content, 611 | style, 612 | }) 613 | }) 614 | .collect() 615 | } 616 | } 617 | 618 | impl Widget for Rain { 619 | fn render(self, area: Rect, buf: &mut Buffer) { 620 | let mut rng = self.build_rng(); 621 | 622 | // We don't actually have n drops with tracks equal to the screen height. 623 | // We actually have 2n drops with tracks ranging from 1.5 to 2.5 the screen height. 624 | // This introduces more randomness to the apparent n and reduces cyclic appearance. 625 | let num_drops = self.rain_density.num_drops(area) * 2; 626 | let drop_track_lens: Vec = (0..num_drops) 627 | .map(|_| (area.height as u64 * 3 / 2 + rng.next_u64() % area.height as u64) as usize) 628 | .collect(); 629 | 630 | // We construct entropy consistently every frame to mimic statefulness. 631 | // This is not a performance bottleneck, so caching wouldn't deliver much benefit. 632 | let entropy: Vec> = drop_track_lens 633 | .iter() 634 | .map(|track_len| (0..*track_len).map(|_| rng.next_u64()).collect()) 635 | .collect(); 636 | 637 | // For every entropy vec, construct a single drop (vertical line of glyphs). 638 | let mut glyphs: Vec = entropy 639 | .into_iter() 640 | .flat_map(|drop_entropy| self.build_drop(drop_entropy, area.width, area.height)) 641 | .collect(); 642 | 643 | // Sort all the glyphs by age so drop heads always render on top. 644 | // This is a moderate bottleneck when the screen is large / there's a lot of glyphs. 645 | glyphs.sort_by(|a, b| a.age.partial_cmp(&b.age).unwrap_or(Ordering::Equal)); 646 | 647 | // Actually render to the buffer. 648 | for glyph in glyphs { 649 | buf[(glyph.x, glyph.y)].set_char(glyph.content); 650 | buf[(glyph.x, glyph.y)].set_style(glyph.style); 651 | } 652 | } 653 | } 654 | 655 | /// A Glyph to be rendered on the screen. 656 | struct Glyph { 657 | x: u16, 658 | y: u16, 659 | age: f64, 660 | content: char, 661 | style: Style, 662 | } 663 | 664 | /// Map a uniform random u64 to a uniform random f64 in the range [lower, upper). 665 | fn uniform(seed: u64, lower: f64, upper: f64) -> f64 { 666 | (seed as f64 / u64::MAX as f64) * (upper - lower) + lower 667 | } 668 | -------------------------------------------------------------------------------- /src/components/helper/image.rs: -------------------------------------------------------------------------------- 1 | use ratatui::{backend::Backend, layout::Rect, Frame, Terminal}; 2 | use ratatui_image::{ 3 | picker::{Picker, ProtocolType}, 4 | protocol::StatefulProtocol, 5 | Image, StatefulImage, 6 | }; 7 | 8 | pub fn view(frame: &mut Frame, path: String, area: Rect) { 9 | let mut picker = Picker::from_query_stdio().unwrap(); 10 | // let mut picker = Picker::from_fontsize((10, 10)); 11 | // picker.set_protocol_type(ProtocolType::Sixel); 12 | 13 | let dyn_image = image::ImageReader::open(path) 14 | .unwrap() 15 | .decode() 16 | .expect("dont know"); 17 | 18 | let mut image = picker.new_resize_protocol(dyn_image); 19 | 20 | frame.render_stateful_widget(StatefulImage::default(), area, &mut image); 21 | } 22 | -------------------------------------------------------------------------------- /src/components/helper/ja.rs: -------------------------------------------------------------------------------- 1 | use rand::{seq::IndexedRandom, Rng}; 2 | use rand_pcg::Mcg128Xsl64; 3 | 4 | const HIRAGANA_NUMBER: usize = 71; 5 | const WANTED_KANA: [u16; HIRAGANA_NUMBER] = [ 6 | 12354, 12356, 12358, 12360, 12362, 12363, 12364, 12365, 12366, 12367, 12368, 12369, 12370, 7 | 12371, 12372, 12373, 12374, 12375, 12376, 12377, 12378, 12379, 12380, 12381, 12382, 12383, 8 | 12384, 12385, 12386, 12388, 12389, 12390, 12391, 12392, 12393, 12394, 12395, 12396, 12397, 9 | 12398, 12399, 12400, 12401, 12402, 12403, 12404, 12405, 12406, 12407, 12408, 12409, 12410, 10 | 12411, 12412, 12413, 12414, 12415, 12416, 12417, 12418, 12420, 12422, 12424, 12425, 12426, 11 | 12427, 12428, 12429, 12431, 12434, 12435, 12 | ]; 13 | const KATAKANA_NUMBER: usize = 81; 14 | const WANTED_KATAKANA: [u16; KATAKANA_NUMBER] = [ 15 | 12449, 12450, 12451, 12452, 12453, 12454, 12455, 12456, 12457, 12458, 12459, 12460, 12461, 16 | 12462, 12463, 12464, 12465, 12466, 12467, 12468, 12469, 12470, 12471, 12472, 12473, 12474, 17 | 12475, 12476, 12477, 12478, 12479, 12480, 12481, 12482, 12484, 12485, 12486, 12487, 12488, 18 | 12489, 12490, 12491, 12492, 12493, 12494, 12495, 12496, 12497, 12498, 12499, 12500, 12501, 19 | 12502, 12503, 12504, 12505, 12506, 12507, 12508, 12509, 12510, 12511, 12512, 12513, 12514, 20 | 12515, 12516, 12517, 12518, 12519, 12520, 12521, 12522, 12523, 12524, 12525, 12526, 12527, 21 | 12530, 12531, 12532, 22 | ]; 23 | 24 | pub fn random_hiragana(rng: &mut Mcg128Xsl64) -> String { 25 | String::from_utf16(&[*WANTED_KANA.choose(rng).unwrap()]).expect("error hiragana") 26 | } 27 | 28 | pub fn random_katakana(rng: &mut Mcg128Xsl64) -> String { 29 | String::from_utf16(&[*WANTED_KATAKANA.choose(rng).unwrap()]).expect("error katakana") 30 | } 31 | 32 | pub fn random_kana(rng: &mut Mcg128Xsl64) -> String { 33 | if rng.random_bool(0.5) { 34 | random_hiragana(rng) 35 | } else { 36 | random_katakana(rng) 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /src/components/helper/mod.rs: -------------------------------------------------------------------------------- 1 | pub mod background; 2 | // pub mod image; 3 | pub mod ja; 4 | pub mod rain; 5 | -------------------------------------------------------------------------------- /src/components/helper/rain.rs: -------------------------------------------------------------------------------- 1 | use ratatui::{style::Color, Frame}; 2 | use std::time::Duration; 3 | 4 | use super::background::{CharacterSet, Rain, RainDensity, RainSpeed}; 5 | 6 | const TAIL_COLOR: Color = Color::from_u32(0x0008dbbe); 7 | 8 | pub fn view(frame: &mut Frame, elapsed: Duration) { 9 | let rain = Rain::new_rain(elapsed) 10 | .with_rain_density(RainDensity::Relative { sparseness: 50 }) 11 | .with_rain_speed(RainSpeed::Absolute { speed: 14.0 }) 12 | .with_rain_speed_variance(0.6) 13 | .with_color(TAIL_COLOR) 14 | .with_noise_interval(Duration::from_secs(10)); 15 | 16 | let kana_tail = rain 17 | .clone() 18 | .with_character_set(CharacterSet::HalfKana) 19 | .with_tail_lifespan(Duration::from_millis(120)) 20 | .with_color(ratatui::style::Color::LightGreen); 21 | 22 | frame.render_widget(rain, frame.area()); 23 | frame.render_widget(kana_tail, frame.area()); 24 | } 25 | -------------------------------------------------------------------------------- /src/components/home.rs: -------------------------------------------------------------------------------- 1 | use super::*; 2 | 3 | const KEY_HELPER_CYCLE: &str = 4 | " Quit | Move | Select | Rain | Cycle Background "; 5 | const KEY_HELPER_NONE: &str = 6 | " Quit | Move | Select | Rain | No Background "; 7 | const TITLE: &str = " KANA SH "; 8 | const SELECTED_STYLE: Style = Style::new() 9 | .bg(ColorPalette::SELECTION) 10 | .add_modifier(Modifier::BOLD); 11 | 12 | #[derive(Debug, PartialEq, Eq, Clone)] 13 | pub enum Mode { 14 | Hira, 15 | Kata, 16 | Both, 17 | } 18 | 19 | #[derive(Debug, PartialEq, Eq, Clone)] 20 | pub enum BackgroundMode { 21 | Cycle, 22 | Disable, 23 | } 24 | 25 | #[derive(Debug, PartialEq, Eq)] 26 | pub struct HomeModel { 27 | page_list: Vec, 28 | state: ListState, 29 | pub background_state: BackgroundMode, 30 | pub key_helper_state: BackgroundMode, 31 | } 32 | 33 | #[derive(Debug, PartialEq, Eq, Clone)] 34 | pub enum HomeMessage { 35 | /// Launch a Page 36 | Enter(Mode), 37 | 38 | Up, 39 | Down, 40 | 41 | RainFx, 42 | Background, 43 | } 44 | 45 | impl Components for HomeModel { 46 | fn new() -> Self { 47 | let mut init_state = ListState::default(); 48 | init_state.select_first(); 49 | 50 | Self { 51 | page_list: vec![ 52 | "Learn Hiragana あ".into(), 53 | "Learn Katakana ア".into(), 54 | "Learn Both".into(), 55 | ], 56 | state: init_state, 57 | background_state: BackgroundMode::Cycle, 58 | key_helper_state: BackgroundMode::Cycle, 59 | } 60 | } 61 | 62 | /// Handle Event (Mostly convert key event to message) 63 | fn handle_event(&self) -> Option { 64 | if event::poll(Duration::from_millis(10)).unwrap() { 65 | if let Event::Key(key) = event::read().unwrap() { 66 | match key.code { 67 | KeyCode::Esc => Some(Message::Back), 68 | KeyCode::Enter => { 69 | if let Some(i) = self.state.selected() { 70 | match i { 71 | 0 => Some(Message::Home(HomeMessage::Enter(Mode::Hira))), 72 | 1 => Some(Message::Home(HomeMessage::Enter(Mode::Kata))), 73 | 2 => Some(Message::Home(HomeMessage::Enter(Mode::Both))), 74 | _ => None, 75 | } 76 | } else { 77 | None 78 | } 79 | } 80 | KeyCode::Char('j') | KeyCode::Down => Some(Message::Home(HomeMessage::Down)), 81 | KeyCode::Char('k') | KeyCode::Up => Some(Message::Home(HomeMessage::Up)), 82 | KeyCode::Char('x') => Some(Message::Home(HomeMessage::RainFx)), 83 | KeyCode::Char('b') => Some(Message::Home(HomeMessage::Background)), 84 | _ => None, 85 | } 86 | } else { 87 | None 88 | } 89 | } else { 90 | None 91 | } 92 | } 93 | 94 | fn update(&mut self, msg: Message) -> Option { 95 | if let Message::Home(home_msg) = msg { 96 | match home_msg { 97 | HomeMessage::Down => { 98 | self.state.select_next(); 99 | } 100 | HomeMessage::Up => { 101 | self.state.select_previous(); 102 | } 103 | _ => {} 104 | } 105 | } 106 | None 107 | } 108 | 109 | fn view(&mut self, frame: &mut Frame, _elapsed: Duration) { 110 | let n_page: u16 = self.page_list.len().try_into().unwrap(); 111 | let [_, main_area, _] = Layout::vertical([ 112 | Constraint::Min(0), 113 | Constraint::Length(n_page + 4), 114 | Constraint::Min(0), 115 | ]) 116 | .areas(frame.area()); 117 | 118 | let key_helper = match self.key_helper_state { 119 | BackgroundMode::Cycle => KEY_HELPER_CYCLE, 120 | BackgroundMode::Disable => KEY_HELPER_NONE, 121 | }; 122 | 123 | let block = Block::new() 124 | .title(Line::from(TITLE).fg(ColorPalette::TITLE).centered()) 125 | .title_bottom( 126 | Line::from(key_helper) 127 | .fg(ColorPalette::KEY_HINT) 128 | .bold() 129 | .centered(), 130 | ) 131 | .border_type(BorderType::Rounded) 132 | .padding(Padding::vertical(1)) 133 | .borders(Borders::ALL); 134 | 135 | let items: Vec = self 136 | .page_list 137 | .iter() 138 | .map(|item| ListItem::new(Line::from(item.clone()).centered())) 139 | .collect(); 140 | 141 | let list = List::new(items) 142 | .block(block) 143 | .highlight_style(SELECTED_STYLE) 144 | .highlight_spacing(HighlightSpacing::Always); 145 | 146 | frame.render_widget(Clear, main_area); 147 | frame.render_stateful_widget(list, main_area, &mut self.state); 148 | } 149 | } 150 | -------------------------------------------------------------------------------- /src/components/kana.rs: -------------------------------------------------------------------------------- 1 | use std::time::UNIX_EPOCH; 2 | 3 | // use crate::components::helper::image; 4 | use crate::components::helper::ja::random_kana; 5 | use rand::SeedableRng; 6 | use rand_pcg::{Mcg128Xsl64, Pcg64Mcg}; 7 | use wana_kana::ConvertJapanese; 8 | 9 | use super::{ 10 | helper::ja::{random_hiragana, random_katakana}, 11 | *, 12 | }; 13 | 14 | const TITLE: &str = " Hiragana "; 15 | const LEFT_TITLE: &str = " Shown: "; 16 | const RIGHT_TITLE: &str = " Correct: "; 17 | const KEY_HELPER: &str = " Main Menu | Show answer "; 18 | 19 | #[derive(Debug, PartialEq, Eq)] 20 | pub struct KanaModel { 21 | shown: u32, 22 | correct: u32, 23 | input: String, 24 | current_kana: String, 25 | display_answer: bool, 26 | rng: Mcg128Xsl64, 27 | pub mode: Mode, 28 | } 29 | 30 | #[derive(Debug, PartialEq, Eq, Clone)] 31 | pub enum KanaMessage { 32 | /// reveal the answer 33 | Answer, 34 | 35 | /// When the user is typing 36 | TypingRoma(char), 37 | 38 | /// Delete roma 39 | DeleteRoma, 40 | 41 | /// Pass, 42 | Pass, 43 | } 44 | 45 | impl Components for KanaModel { 46 | /// Create a new kana model 47 | fn new() -> Self { 48 | let mut r = Pcg64Mcg::seed_from_u64( 49 | std::time::SystemTime::now() 50 | .duration_since(UNIX_EPOCH) 51 | .unwrap() 52 | .as_secs(), 53 | ); 54 | Self { 55 | shown: 0, 56 | correct: 0, 57 | input: String::new(), 58 | current_kana: random_kana(&mut r), 59 | display_answer: false, 60 | rng: r, 61 | mode: Mode::Hira, 62 | } 63 | } 64 | 65 | /// Handle Event (Mostly convert key event to message) 66 | fn handle_event(&self) -> Option { 67 | if event::poll(Duration::from_millis(10)).unwrap() { 68 | if let Event::Key(key) = event::read().unwrap() { 69 | match key.code { 70 | KeyCode::Esc => Some(Message::Back), 71 | KeyCode::Backspace => Some(Message::Kana(KanaMessage::DeleteRoma)), 72 | KeyCode::Char(' ') => Some(Message::Kana(KanaMessage::Answer)), 73 | KeyCode::Char(c) => Some(Message::Kana(KanaMessage::TypingRoma(c))), 74 | _ => None, 75 | } 76 | } else { 77 | None 78 | } 79 | } else { 80 | None 81 | } 82 | } 83 | 84 | fn update(&mut self, msg: Message) -> Option { 85 | if let Message::Kana(kana_msg) = msg { 86 | return match kana_msg { 87 | KanaMessage::TypingRoma(c) => { 88 | self.input.push(c); 89 | 90 | if self.input == self.current_kana.to_romaji() { 91 | if self.display_answer { 92 | self.display_answer = false; 93 | } else { 94 | self.correct += 1; 95 | } 96 | self.shown += 1; 97 | return Some(Message::Kana(KanaMessage::Pass)); 98 | } 99 | None 100 | } 101 | KanaMessage::Pass => { 102 | self.input = String::new(); 103 | match self.mode { 104 | Mode::Hira => self.current_kana = random_hiragana(&mut self.rng), 105 | Mode::Kata => self.current_kana = random_katakana(&mut self.rng), 106 | Mode::Both => self.current_kana = random_kana(&mut self.rng), 107 | } 108 | 109 | None 110 | } 111 | KanaMessage::Answer => { 112 | self.display_answer = true; 113 | self.input = String::new(); 114 | None 115 | } 116 | KanaMessage::DeleteRoma => { 117 | self.input.pop(); 118 | None 119 | } 120 | }; 121 | } 122 | None 123 | } 124 | 125 | fn view(&mut self, frame: &mut Frame, _elapsed: Duration) { 126 | self.learning_zone(frame); 127 | } 128 | } 129 | 130 | impl KanaModel { 131 | fn learning_zone(&mut self, frame: &mut Frame) { 132 | let [_, v_area, _] = Layout::vertical([ 133 | Constraint::Min(0), 134 | Constraint::Length(7), 135 | Constraint::Min(0), 136 | ]) 137 | .areas(frame.area()); 138 | 139 | let [_, main_area, _] = Layout::horizontal([ 140 | Constraint::Min(0), 141 | Constraint::Length(43), 142 | Constraint::Min(0), 143 | ]) 144 | .areas(v_area); 145 | 146 | let left_title = Line::from(vec![ 147 | LEFT_TITLE.fg(ColorPalette::SUBTITLE).into(), 148 | self.shown.to_string().yellow(), 149 | " ".into(), 150 | ]) 151 | .left_aligned(); 152 | 153 | let right_title = Line::from(vec![ 154 | RIGHT_TITLE.fg(ColorPalette::SUBTITLE).into(), 155 | self.correct.to_string().yellow(), 156 | " ".into(), 157 | ]) 158 | .right_aligned(); 159 | 160 | let block = Block::new() 161 | .title(Line::from(TITLE).fg(ColorPalette::TITLE).centered()) 162 | .title(left_title) 163 | .title(right_title) 164 | .title_bottom( 165 | Line::from(KEY_HELPER) 166 | .fg(ColorPalette::KEY_HINT) 167 | .bold() 168 | .centered(), 169 | ) 170 | .border_type(BorderType::Rounded) 171 | .padding(Padding::vertical(1)) 172 | .borders(Borders::ALL); 173 | 174 | let text = vec![ 175 | Line::from(self.current_kana.clone()), 176 | if self.display_answer { 177 | Line::from(self.current_kana.to_romaji()).fg(ColorPalette::ERROR) 178 | } else { 179 | Line::default() 180 | }, 181 | Line::from(self.input.clone()), 182 | ]; 183 | 184 | let p = Paragraph::new(text).block(block).centered(); 185 | 186 | frame.render_widget(Clear, main_area); 187 | frame.render_widget(p, main_area); 188 | } 189 | } 190 | -------------------------------------------------------------------------------- /src/components/mod.rs: -------------------------------------------------------------------------------- 1 | pub mod app; 2 | pub mod helper; 3 | pub mod home; 4 | pub mod kana; 5 | 6 | pub use ratatui::{ 7 | crossterm::event::{self, Event, KeyCode}, 8 | layout::{Constraint, Layout}, 9 | style::Stylize, 10 | style::{palette::tailwind::SLATE, Color, Modifier, Style}, 11 | text::Line, 12 | widgets::{Block, Paragraph}, 13 | widgets::{BorderType, Borders, Clear, HighlightSpacing, List, ListItem, ListState, Padding}, 14 | Frame, 15 | }; 16 | 17 | use std::time::Duration; 18 | 19 | use home::{BackgroundMode, HomeMessage, Mode}; 20 | use kana::KanaMessage; 21 | 22 | pub struct ColorPalette; 23 | 24 | impl ColorPalette { 25 | pub const TITLE: Color = Color::from_u32(0x00ff33ff); 26 | pub const SUBTITLE: Color = Color::from_u32(0x00ff3399); 27 | pub const ERROR: Color = Color::from_u32(0x00ff3333); 28 | pub const KEY_HINT: Color = Color::from_u32(0x00ff9933); 29 | pub const SELECTION: Color = SLATE.c800; 30 | // 0x00ffff33 31 | // #99ff33 32 | } 33 | 34 | #[derive(Debug, PartialEq, Eq, Clone)] 35 | pub enum Message { 36 | /// Go to the previous page or quit the app 37 | Back, 38 | 39 | Home(HomeMessage), 40 | Kana(KanaMessage), 41 | } 42 | 43 | pub trait Components { 44 | fn new() -> Self; 45 | 46 | fn handle_event(&self) -> Option; 47 | 48 | fn update(&mut self, msg: Message) -> Option; 49 | 50 | fn view(&mut self, frame: &mut Frame, elapsed: Duration); 51 | } 52 | -------------------------------------------------------------------------------- /src/main.rs: -------------------------------------------------------------------------------- 1 | mod components; 2 | use components::app::App; 3 | use components::{ColorPalette, Components}; 4 | 5 | use ratatui::layout::{Constraint, Layout}; 6 | use ratatui::text::Line; 7 | use ratatui::{restore, style::Style}; 8 | 9 | use std::time::{Duration, Instant}; 10 | 11 | use tachyonfx::{fx, EffectRenderer, Interpolation}; 12 | use tui_big_text::{BigText, PixelSize}; 13 | 14 | use clap::Parser; 15 | 16 | #[derive(Parser, Debug)] 17 | struct Args { 18 | /// Path to the assets directory 19 | #[arg(short, long)] 20 | path: Option, 21 | } 22 | 23 | fn main() { 24 | let arg = Args::parse(); 25 | 26 | let mut terminal = ratatui::init(); 27 | 28 | let mut app = App::new(); 29 | if let Some(path) = arg.path { 30 | let assets = std::fs::read_dir(path) 31 | .unwrap() 32 | .map(|entry| entry.unwrap().path().to_str().unwrap().to_string()) 33 | .collect::>(); 34 | app.background_paths = assets; 35 | } 36 | 37 | let start_time = Instant::now(); 38 | 39 | let mut fade_effect = fx::dissolve((20000, Interpolation::QuadOut)); 40 | 41 | // Splash Screen Rendering 42 | while start_time.elapsed() < Duration::from_millis(2000) { 43 | // let p = Paragraph::new(Line::from("~ Kana SH ~")).centered(); 44 | let p = BigText::builder() 45 | .pixel_size(PixelSize::HalfHeight) 46 | .lines(vec![">> Kana SH <<".into()]) 47 | .centered() 48 | .style(Style::fg(Style::new(), ColorPalette::TITLE)) 49 | .build(); 50 | 51 | let credit = Line::from("@benoitlx") 52 | .centered() 53 | .style(Style::fg(Style::new(), ColorPalette::SUBTITLE)); 54 | 55 | let _ = terminal.draw(|frame| { 56 | let [_, area, _, bottom] = Layout::vertical([ 57 | Constraint::Min(0), 58 | Constraint::Length(4), 59 | Constraint::Min(0), 60 | Constraint::Length(1), 61 | ]) 62 | .areas(frame.area()); 63 | 64 | let [_, img_area, _] = Layout::horizontal([ 65 | Constraint::Min(0), 66 | Constraint::Percentage(10), 67 | Constraint::Min(0), 68 | ]) 69 | .areas(bottom); 70 | 71 | frame.render_widget(p, area); 72 | // crate::components::helper::image::view(frame, "./assets/rezo.png".to_string(), img_area); 73 | frame.render_widget(credit, img_area); 74 | if start_time.elapsed() > Duration::from_secs(1) { 75 | frame.render_effect(&mut fade_effect, area, tachyonfx::Duration::from_millis(33)); 76 | } 77 | }); 78 | } 79 | 80 | // Main app rendering 81 | while !app.exit { 82 | let _ = terminal.draw(|frame| app.view(frame, start_time.elapsed())); 83 | 84 | let mut current_msg = app.handle_event(); 85 | 86 | while current_msg.is_some() { 87 | current_msg = app.update(current_msg.unwrap()); 88 | } 89 | } 90 | 91 | restore(); 92 | } 93 | --------------------------------------------------------------------------------