├── .github └── workflows │ └── build.yml ├── .gitignore ├── Cargo.lock ├── Cargo.toml ├── LICENSE ├── README.md ├── build.sh ├── docs └── AMD_workshop │ ├── README.md │ ├── v1.py │ ├── v2.py │ └── v3.py ├── install.ps1 ├── install.sh └── src ├── cmd ├── auth.rs ├── mod.rs └── submit.rs ├── main.rs ├── models └── mod.rs ├── service └── mod.rs ├── utils └── mod.rs └── views ├── loading_page.rs ├── mod.rs └── result_page.rs /.github/workflows/build.yml: -------------------------------------------------------------------------------- 1 | name: release 2 | 3 | on: 4 | push: 5 | branches: 6 | - main 7 | tags: 8 | - 'v*' # Push events to matching v*, i.e. v1.0, v20.15.10 9 | 10 | # Keep pull request builds for testing 11 | pull_request: 12 | workflow_dispatch: 13 | 14 | permissions: 15 | contents: write 16 | 17 | jobs: 18 | version: 19 | name: Generate Version 20 | runs-on: ubuntu-latest 21 | outputs: 22 | new_tag: ${{ steps.tag_version.outputs.new_tag }} 23 | steps: 24 | - uses: actions/checkout@v4 25 | with: 26 | fetch-depth: 0 27 | 28 | - name: Bump version and push tag 29 | id: tag_version 30 | uses: mathieudutour/github-tag-action@v6.1 31 | with: 32 | github_token: ${{ secrets.GITHUB_TOKEN }} 33 | default_bump: patch 34 | release_branches: main 35 | 36 | build: 37 | name: Build 38 | needs: version 39 | runs-on: ${{ matrix.os }} 40 | strategy: 41 | matrix: 42 | include: 43 | - os: ubuntu-latest 44 | target: x86_64-unknown-linux-gnu 45 | artifact_name: popcorn-cli 46 | asset_name: popcorn-cli-linux.tar.gz 47 | compress_cmd: tar -czf 48 | compress_ext: .tar.gz 49 | 50 | - os: windows-latest 51 | target: x86_64-pc-windows-msvc 52 | artifact_name: popcorn-cli 53 | asset_name: popcorn-cli-windows.zip 54 | compress_cmd: 7z a 55 | compress_ext: .zip 56 | 57 | - os: macos-latest 58 | target: aarch64-apple-darwin 59 | artifact_name: popcorn-cli 60 | asset_name: popcorn-cli-macos.tar.gz 61 | compress_cmd: tar -czf 62 | compress_ext: .tar.gz 63 | 64 | steps: 65 | - uses: actions/checkout@v4 66 | 67 | - name: Setup Rust toolchain 68 | uses: dtolnay/rust-toolchain@master 69 | with: 70 | toolchain: stable 71 | targets: ${{ matrix.target }} 72 | 73 | - name: Set up cargo cache 74 | uses: Swatinem/rust-cache@v2 75 | with: 76 | key: ${{ matrix.target }} 77 | 78 | - name: Install cross-compilation dependencies (Linux ARM) 79 | if: matrix.target == 'aarch64-unknown-linux-gnu' 80 | run: | 81 | sudo apt-get update 82 | sudo apt-get install -y gcc-aarch64-linux-gnu 83 | 84 | - name: Build release binary 85 | run: cargo build --release --target ${{ matrix.target }} 86 | 87 | - name: Prepare artifact 88 | shell: bash 89 | run: | 90 | mkdir -p dist 91 | if [[ "${{ matrix.os }}" == "windows-latest" ]]; then 92 | cp target/${{ matrix.target }}/release/popcorn-cli.exe dist/popcorn-cli.exe 93 | else 94 | cp target/${{ matrix.target }}/release/popcorn-cli dist/popcorn-cli 95 | chmod +x dist/popcorn-cli 96 | fi 97 | cd dist 98 | ${{ matrix.compress_cmd }} ../${{ matrix.asset_name }} * 99 | 100 | - name: Upload artifacts 101 | uses: actions/upload-artifact@v4 102 | with: 103 | name: ${{ matrix.asset_name }} 104 | path: ${{ matrix.asset_name }} 105 | retention-days: 7 106 | 107 | release: 108 | name: Create Release 109 | needs: [build, version] 110 | runs-on: ubuntu-latest 111 | if: startsWith(github.ref, 'refs/tags/') || github.ref == 'refs/heads/main' 112 | 113 | steps: 114 | - name: Download all artifacts 115 | uses: actions/download-artifact@v4 116 | 117 | - name: Create Release 118 | uses: softprops/action-gh-release@v1 119 | with: 120 | tag_name: ${{ needs.version.outputs.new_tag }} 121 | name: Release ${{ needs.version.outputs.new_tag }} 122 | files: | 123 | popcorn-cli-linux.tar.gz/popcorn-cli-linux.tar.gz 124 | popcorn-cli-windows.zip/popcorn-cli-windows.zip 125 | popcorn-cli-macos.tar.gz/popcorn-cli-macos.tar.gz 126 | env: 127 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 128 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | submission.* 2 | target/ 3 | scratch.md 4 | -------------------------------------------------------------------------------- /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 = "anstream" 28 | version = "0.6.18" 29 | source = "registry+https://github.com/rust-lang/crates.io-index" 30 | checksum = "8acc5369981196006228e28809f761875c0327210a891e941f4c683b3a99529b" 31 | dependencies = [ 32 | "anstyle", 33 | "anstyle-parse", 34 | "anstyle-query", 35 | "anstyle-wincon", 36 | "colorchoice", 37 | "is_terminal_polyfill", 38 | "utf8parse", 39 | ] 40 | 41 | [[package]] 42 | name = "anstyle" 43 | version = "1.0.10" 44 | source = "registry+https://github.com/rust-lang/crates.io-index" 45 | checksum = "55cc3b69f167a1ef2e161439aa98aed94e6028e5f9a59be9a6ffb47aef1651f9" 46 | 47 | [[package]] 48 | name = "anstyle-parse" 49 | version = "0.2.6" 50 | source = "registry+https://github.com/rust-lang/crates.io-index" 51 | checksum = "3b2d16507662817a6a20a9ea92df6652ee4f94f914589377d69f3b21bc5798a9" 52 | dependencies = [ 53 | "utf8parse", 54 | ] 55 | 56 | [[package]] 57 | name = "anstyle-query" 58 | version = "1.1.2" 59 | source = "registry+https://github.com/rust-lang/crates.io-index" 60 | checksum = "79947af37f4177cfead1110013d678905c37501914fba0efea834c3fe9a8d60c" 61 | dependencies = [ 62 | "windows-sys 0.59.0", 63 | ] 64 | 65 | [[package]] 66 | name = "anstyle-wincon" 67 | version = "3.0.7" 68 | source = "registry+https://github.com/rust-lang/crates.io-index" 69 | checksum = "ca3534e77181a9cc07539ad51f2141fe32f6c3ffd4df76db8ad92346b003ae4e" 70 | dependencies = [ 71 | "anstyle", 72 | "once_cell", 73 | "windows-sys 0.59.0", 74 | ] 75 | 76 | [[package]] 77 | name = "anyhow" 78 | version = "1.0.97" 79 | source = "registry+https://github.com/rust-lang/crates.io-index" 80 | checksum = "dcfed56ad506cb2c684a14971b8861fdc3baaaae314b9e5f9bb532cbe3ba7a4f" 81 | 82 | [[package]] 83 | name = "autocfg" 84 | version = "1.4.0" 85 | source = "registry+https://github.com/rust-lang/crates.io-index" 86 | checksum = "ace50bade8e6234aa140d9a2f552bbee1db4d353f69b8217bc503490fc1a9f26" 87 | 88 | [[package]] 89 | name = "backtrace" 90 | version = "0.3.74" 91 | source = "registry+https://github.com/rust-lang/crates.io-index" 92 | checksum = "8d82cb332cdfaed17ae235a638438ac4d4839913cc2af585c3c6746e8f8bee1a" 93 | dependencies = [ 94 | "addr2line", 95 | "cfg-if", 96 | "libc", 97 | "miniz_oxide", 98 | "object", 99 | "rustc-demangle", 100 | "windows-targets 0.52.6", 101 | ] 102 | 103 | [[package]] 104 | name = "base64" 105 | version = "0.21.7" 106 | source = "registry+https://github.com/rust-lang/crates.io-index" 107 | checksum = "9d297deb1925b89f2ccc13d7635fa0714f12c87adce1c75356b39ca9b7178567" 108 | 109 | [[package]] 110 | name = "base64" 111 | version = "0.22.1" 112 | source = "registry+https://github.com/rust-lang/crates.io-index" 113 | checksum = "72b3254f16251a8381aa12e40e3c4d2f0199f8c6508fbecb9d91f575e0fbb8c6" 114 | 115 | [[package]] 116 | name = "base64-url" 117 | version = "3.0.0" 118 | source = "registry+https://github.com/rust-lang/crates.io-index" 119 | checksum = "38e2b6c78c06f7288d5e3c3d683bde35a79531127c83b087e5d0d77c974b4b28" 120 | dependencies = [ 121 | "base64 0.22.1", 122 | ] 123 | 124 | [[package]] 125 | name = "bitflags" 126 | version = "1.3.2" 127 | source = "registry+https://github.com/rust-lang/crates.io-index" 128 | checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a" 129 | 130 | [[package]] 131 | name = "bitflags" 132 | version = "2.9.0" 133 | source = "registry+https://github.com/rust-lang/crates.io-index" 134 | checksum = "5c8214115b7bf84099f1309324e63141d4c5d7cc26862f97a0a857dbefe165bd" 135 | 136 | [[package]] 137 | name = "bumpalo" 138 | version = "3.17.0" 139 | source = "registry+https://github.com/rust-lang/crates.io-index" 140 | checksum = "1628fb46dfa0b37568d12e5edd512553eccf6a22a78e8bde00bb4aed84d5bdbf" 141 | 142 | [[package]] 143 | name = "bytes" 144 | version = "1.10.1" 145 | source = "registry+https://github.com/rust-lang/crates.io-index" 146 | checksum = "d71b6127be86fdcfddb610f7182ac57211d4b18a3e9c82eb2d17662f2227ad6a" 147 | 148 | [[package]] 149 | name = "cassowary" 150 | version = "0.3.0" 151 | source = "registry+https://github.com/rust-lang/crates.io-index" 152 | checksum = "df8670b8c7b9dae1793364eafadf7239c40d669904660c5960d74cfd80b46a53" 153 | 154 | [[package]] 155 | name = "castaway" 156 | version = "0.2.3" 157 | source = "registry+https://github.com/rust-lang/crates.io-index" 158 | checksum = "0abae9be0aaf9ea96a3b1b8b1b55c602ca751eba1b1500220cea4ecbafe7c0d5" 159 | dependencies = [ 160 | "rustversion", 161 | ] 162 | 163 | [[package]] 164 | name = "cc" 165 | version = "1.2.19" 166 | source = "registry+https://github.com/rust-lang/crates.io-index" 167 | checksum = "8e3a13707ac958681c13b39b458c073d0d9bc8a22cb1b2f4c8e55eb72c13f362" 168 | dependencies = [ 169 | "shlex", 170 | ] 171 | 172 | [[package]] 173 | name = "cesu8" 174 | version = "1.1.0" 175 | source = "registry+https://github.com/rust-lang/crates.io-index" 176 | checksum = "6d43a04d8753f35258c91f8ec639f792891f748a1edbd759cf1dcea3382ad83c" 177 | 178 | [[package]] 179 | name = "cfg-if" 180 | version = "1.0.0" 181 | source = "registry+https://github.com/rust-lang/crates.io-index" 182 | checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd" 183 | 184 | [[package]] 185 | name = "cfg_aliases" 186 | version = "0.2.1" 187 | source = "registry+https://github.com/rust-lang/crates.io-index" 188 | checksum = "613afe47fcd5fac7ccf1db93babcb082c5994d996f20b8b159f2ad1658eb5724" 189 | 190 | [[package]] 191 | name = "clap" 192 | version = "4.5.36" 193 | source = "registry+https://github.com/rust-lang/crates.io-index" 194 | checksum = "2df961d8c8a0d08aa9945718ccf584145eee3f3aa06cddbeac12933781102e04" 195 | dependencies = [ 196 | "clap_builder", 197 | "clap_derive", 198 | ] 199 | 200 | [[package]] 201 | name = "clap_builder" 202 | version = "4.5.36" 203 | source = "registry+https://github.com/rust-lang/crates.io-index" 204 | checksum = "132dbda40fb6753878316a489d5a1242a8ef2f0d9e47ba01c951ea8aa7d013a5" 205 | dependencies = [ 206 | "anstream", 207 | "anstyle", 208 | "clap_lex", 209 | "strsim", 210 | ] 211 | 212 | [[package]] 213 | name = "clap_derive" 214 | version = "4.5.32" 215 | source = "registry+https://github.com/rust-lang/crates.io-index" 216 | checksum = "09176aae279615badda0765c0c0b3f6ed53f4709118af73cf4655d85d1530cd7" 217 | dependencies = [ 218 | "heck", 219 | "proc-macro2", 220 | "quote", 221 | "syn", 222 | ] 223 | 224 | [[package]] 225 | name = "clap_lex" 226 | version = "0.7.4" 227 | source = "registry+https://github.com/rust-lang/crates.io-index" 228 | checksum = "f46ad14479a25103f283c0f10005961cf086d8dc42205bb44c46ac563475dca6" 229 | 230 | [[package]] 231 | name = "colorchoice" 232 | version = "1.0.3" 233 | source = "registry+https://github.com/rust-lang/crates.io-index" 234 | checksum = "5b63caa9aa9397e2d9480a9b13673856c78d8ac123288526c37d7839f2a86990" 235 | 236 | [[package]] 237 | name = "combine" 238 | version = "4.6.7" 239 | source = "registry+https://github.com/rust-lang/crates.io-index" 240 | checksum = "ba5a308b75df32fe02788e748662718f03fde005016435c444eea572398219fd" 241 | dependencies = [ 242 | "bytes", 243 | "memchr", 244 | ] 245 | 246 | [[package]] 247 | name = "compact_str" 248 | version = "0.7.1" 249 | source = "registry+https://github.com/rust-lang/crates.io-index" 250 | checksum = "f86b9c4c00838774a6d902ef931eff7470720c51d90c2e32cfe15dc304737b3f" 251 | dependencies = [ 252 | "castaway", 253 | "cfg-if", 254 | "itoa", 255 | "ryu", 256 | "static_assertions", 257 | ] 258 | 259 | [[package]] 260 | name = "core-foundation" 261 | version = "0.9.4" 262 | source = "registry+https://github.com/rust-lang/crates.io-index" 263 | checksum = "91e195e091a93c46f7102ec7818a2aa394e1e1771c3ab4825963fa03e45afb8f" 264 | dependencies = [ 265 | "core-foundation-sys", 266 | "libc", 267 | ] 268 | 269 | [[package]] 270 | name = "core-foundation-sys" 271 | version = "0.8.7" 272 | source = "registry+https://github.com/rust-lang/crates.io-index" 273 | checksum = "773648b94d0e5d620f64f280777445740e61fe701025087ec8b57f45c791888b" 274 | 275 | [[package]] 276 | name = "crossterm" 277 | version = "0.27.0" 278 | source = "registry+https://github.com/rust-lang/crates.io-index" 279 | checksum = "f476fe445d41c9e991fd07515a6f463074b782242ccf4a5b7b1d1012e70824df" 280 | dependencies = [ 281 | "bitflags 2.9.0", 282 | "crossterm_winapi", 283 | "libc", 284 | "mio 0.8.11", 285 | "parking_lot", 286 | "signal-hook", 287 | "signal-hook-mio", 288 | "winapi", 289 | ] 290 | 291 | [[package]] 292 | name = "crossterm_winapi" 293 | version = "0.9.1" 294 | source = "registry+https://github.com/rust-lang/crates.io-index" 295 | checksum = "acdd7c62a3665c7f6830a51635d9ac9b23ed385797f70a83bb8bafe9c572ab2b" 296 | dependencies = [ 297 | "winapi", 298 | ] 299 | 300 | [[package]] 301 | name = "ctrlc" 302 | version = "3.4.6" 303 | source = "registry+https://github.com/rust-lang/crates.io-index" 304 | checksum = "697b5419f348fd5ae2478e8018cb016c00a5881c7f46c717de98ffd135a5651c" 305 | dependencies = [ 306 | "nix", 307 | "windows-sys 0.59.0", 308 | ] 309 | 310 | [[package]] 311 | name = "dirs" 312 | version = "5.0.1" 313 | source = "registry+https://github.com/rust-lang/crates.io-index" 314 | checksum = "44c45a9d03d6676652bcb5e724c7e988de1acad23a711b5217ab9cbecbec2225" 315 | dependencies = [ 316 | "dirs-sys", 317 | ] 318 | 319 | [[package]] 320 | name = "dirs-sys" 321 | version = "0.4.1" 322 | source = "registry+https://github.com/rust-lang/crates.io-index" 323 | checksum = "520f05a5cbd335fae5a99ff7a6ab8627577660ee5cfd6a94a6a929b52ff0321c" 324 | dependencies = [ 325 | "libc", 326 | "option-ext", 327 | "redox_users", 328 | "windows-sys 0.48.0", 329 | ] 330 | 331 | [[package]] 332 | name = "displaydoc" 333 | version = "0.2.5" 334 | source = "registry+https://github.com/rust-lang/crates.io-index" 335 | checksum = "97369cbbc041bc366949bc74d34658d6cda5621039731c6310521892a3a20ae0" 336 | dependencies = [ 337 | "proc-macro2", 338 | "quote", 339 | "syn", 340 | ] 341 | 342 | [[package]] 343 | name = "either" 344 | version = "1.15.0" 345 | source = "registry+https://github.com/rust-lang/crates.io-index" 346 | checksum = "48c757948c5ede0e46177b7add2e67155f70e33c07fea8284df6576da70b3719" 347 | 348 | [[package]] 349 | name = "encoding_rs" 350 | version = "0.8.35" 351 | source = "registry+https://github.com/rust-lang/crates.io-index" 352 | checksum = "75030f3c4f45dafd7586dd6780965a8c7e8e285a5ecb86713e63a79c5b2766f3" 353 | dependencies = [ 354 | "cfg-if", 355 | ] 356 | 357 | [[package]] 358 | name = "equivalent" 359 | version = "1.0.2" 360 | source = "registry+https://github.com/rust-lang/crates.io-index" 361 | checksum = "877a4ace8713b0bcf2a4e7eec82529c029f1d0619886d18145fea96c3ffe5c0f" 362 | 363 | [[package]] 364 | name = "errno" 365 | version = "0.3.11" 366 | source = "registry+https://github.com/rust-lang/crates.io-index" 367 | checksum = "976dd42dc7e85965fe702eb8164f21f450704bdde31faefd6471dba214cb594e" 368 | dependencies = [ 369 | "libc", 370 | "windows-sys 0.59.0", 371 | ] 372 | 373 | [[package]] 374 | name = "fastrand" 375 | version = "2.3.0" 376 | source = "registry+https://github.com/rust-lang/crates.io-index" 377 | checksum = "37909eebbb50d72f9059c3b6d82c0463f2ff062c9e95845c43a6c9c0355411be" 378 | 379 | [[package]] 380 | name = "fnv" 381 | version = "1.0.7" 382 | source = "registry+https://github.com/rust-lang/crates.io-index" 383 | checksum = "3f9eec918d3f24069decb9af1554cad7c880e2da24a9afd88aca000531ab82c1" 384 | 385 | [[package]] 386 | name = "foldhash" 387 | version = "0.1.5" 388 | source = "registry+https://github.com/rust-lang/crates.io-index" 389 | checksum = "d9c4f5dac5e15c24eb999c26181a6ca40b39fe946cbe4c263c7209467bc83af2" 390 | 391 | [[package]] 392 | name = "foreign-types" 393 | version = "0.3.2" 394 | source = "registry+https://github.com/rust-lang/crates.io-index" 395 | checksum = "f6f339eb8adc052cd2ca78910fda869aefa38d22d5cb648e6485e4d3fc06f3b1" 396 | dependencies = [ 397 | "foreign-types-shared", 398 | ] 399 | 400 | [[package]] 401 | name = "foreign-types-shared" 402 | version = "0.1.1" 403 | source = "registry+https://github.com/rust-lang/crates.io-index" 404 | checksum = "00b0228411908ca8685dba7fc2cdd70ec9990a6e753e89b6ac91a84c40fbaf4b" 405 | 406 | [[package]] 407 | name = "form_urlencoded" 408 | version = "1.2.1" 409 | source = "registry+https://github.com/rust-lang/crates.io-index" 410 | checksum = "e13624c2627564efccf4934284bdd98cbaa14e79b0b5a141218e507b3a823456" 411 | dependencies = [ 412 | "percent-encoding", 413 | ] 414 | 415 | [[package]] 416 | name = "futures-channel" 417 | version = "0.3.31" 418 | source = "registry+https://github.com/rust-lang/crates.io-index" 419 | checksum = "2dff15bf788c671c1934e366d07e30c1814a8ef514e1af724a602e8a2fbe1b10" 420 | dependencies = [ 421 | "futures-core", 422 | ] 423 | 424 | [[package]] 425 | name = "futures-core" 426 | version = "0.3.31" 427 | source = "registry+https://github.com/rust-lang/crates.io-index" 428 | checksum = "05f29059c0c2090612e8d742178b0580d2dc940c837851ad723096f87af6663e" 429 | 430 | [[package]] 431 | name = "futures-macro" 432 | version = "0.3.31" 433 | source = "registry+https://github.com/rust-lang/crates.io-index" 434 | checksum = "162ee34ebcb7c64a8abebc059ce0fee27c2262618d7b60ed8faf72fef13c3650" 435 | dependencies = [ 436 | "proc-macro2", 437 | "quote", 438 | "syn", 439 | ] 440 | 441 | [[package]] 442 | name = "futures-sink" 443 | version = "0.3.31" 444 | source = "registry+https://github.com/rust-lang/crates.io-index" 445 | checksum = "e575fab7d1e0dcb8d0c7bcf9a63ee213816ab51902e6d244a95819acacf1d4f7" 446 | 447 | [[package]] 448 | name = "futures-task" 449 | version = "0.3.31" 450 | source = "registry+https://github.com/rust-lang/crates.io-index" 451 | checksum = "f90f7dce0722e95104fcb095585910c0977252f286e354b5e3bd38902cd99988" 452 | 453 | [[package]] 454 | name = "futures-util" 455 | version = "0.3.31" 456 | source = "registry+https://github.com/rust-lang/crates.io-index" 457 | checksum = "9fa08315bb612088cc391249efdc3bc77536f16c91f6cf495e6fbe85b20a4a81" 458 | dependencies = [ 459 | "futures-core", 460 | "futures-macro", 461 | "futures-task", 462 | "pin-project-lite", 463 | "pin-utils", 464 | "slab", 465 | ] 466 | 467 | [[package]] 468 | name = "getrandom" 469 | version = "0.2.15" 470 | source = "registry+https://github.com/rust-lang/crates.io-index" 471 | checksum = "c4567c8db10ae91089c99af84c68c38da3ec2f087c3f82960bcdbf3656b6f4d7" 472 | dependencies = [ 473 | "cfg-if", 474 | "libc", 475 | "wasi 0.11.0+wasi-snapshot-preview1", 476 | ] 477 | 478 | [[package]] 479 | name = "getrandom" 480 | version = "0.3.2" 481 | source = "registry+https://github.com/rust-lang/crates.io-index" 482 | checksum = "73fea8450eea4bac3940448fb7ae50d91f034f941199fcd9d909a5a07aa455f0" 483 | dependencies = [ 484 | "cfg-if", 485 | "libc", 486 | "r-efi", 487 | "wasi 0.14.2+wasi-0.2.4", 488 | ] 489 | 490 | [[package]] 491 | name = "gimli" 492 | version = "0.31.1" 493 | source = "registry+https://github.com/rust-lang/crates.io-index" 494 | checksum = "07e28edb80900c19c28f1072f2e8aeca7fa06b23cd4169cefe1af5aa3260783f" 495 | 496 | [[package]] 497 | name = "h2" 498 | version = "0.3.26" 499 | source = "registry+https://github.com/rust-lang/crates.io-index" 500 | checksum = "81fe527a889e1532da5c525686d96d4c2e74cdd345badf8dfef9f6b39dd5f5e8" 501 | dependencies = [ 502 | "bytes", 503 | "fnv", 504 | "futures-core", 505 | "futures-sink", 506 | "futures-util", 507 | "http", 508 | "indexmap", 509 | "slab", 510 | "tokio", 511 | "tokio-util", 512 | "tracing", 513 | ] 514 | 515 | [[package]] 516 | name = "hashbrown" 517 | version = "0.15.2" 518 | source = "registry+https://github.com/rust-lang/crates.io-index" 519 | checksum = "bf151400ff0baff5465007dd2f3e717f3fe502074ca563069ce3a6629d07b289" 520 | dependencies = [ 521 | "allocator-api2", 522 | "equivalent", 523 | "foldhash", 524 | ] 525 | 526 | [[package]] 527 | name = "heck" 528 | version = "0.5.0" 529 | source = "registry+https://github.com/rust-lang/crates.io-index" 530 | checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea" 531 | 532 | [[package]] 533 | name = "home" 534 | version = "0.5.11" 535 | source = "registry+https://github.com/rust-lang/crates.io-index" 536 | checksum = "589533453244b0995c858700322199b2becb13b627df2851f64a2775d024abcf" 537 | dependencies = [ 538 | "windows-sys 0.59.0", 539 | ] 540 | 541 | [[package]] 542 | name = "http" 543 | version = "0.2.12" 544 | source = "registry+https://github.com/rust-lang/crates.io-index" 545 | checksum = "601cbb57e577e2f5ef5be8e7b83f0f63994f25aa94d673e54a92d5c516d101f1" 546 | dependencies = [ 547 | "bytes", 548 | "fnv", 549 | "itoa", 550 | ] 551 | 552 | [[package]] 553 | name = "http-body" 554 | version = "0.4.6" 555 | source = "registry+https://github.com/rust-lang/crates.io-index" 556 | checksum = "7ceab25649e9960c0311ea418d17bee82c0dcec1bd053b5f9a66e265a693bed2" 557 | dependencies = [ 558 | "bytes", 559 | "http", 560 | "pin-project-lite", 561 | ] 562 | 563 | [[package]] 564 | name = "httparse" 565 | version = "1.10.1" 566 | source = "registry+https://github.com/rust-lang/crates.io-index" 567 | checksum = "6dbf3de79e51f3d586ab4cb9d5c3e2c14aa28ed23d180cf89b4df0454a69cc87" 568 | 569 | [[package]] 570 | name = "httpdate" 571 | version = "1.0.3" 572 | source = "registry+https://github.com/rust-lang/crates.io-index" 573 | checksum = "df3b46402a9d5adb4c86a0cf463f42e19994e3ee891101b1841f30a545cb49a9" 574 | 575 | [[package]] 576 | name = "hyper" 577 | version = "0.14.32" 578 | source = "registry+https://github.com/rust-lang/crates.io-index" 579 | checksum = "41dfc780fdec9373c01bae43289ea34c972e40ee3c9f6b3c8801a35f35586ce7" 580 | dependencies = [ 581 | "bytes", 582 | "futures-channel", 583 | "futures-core", 584 | "futures-util", 585 | "h2", 586 | "http", 587 | "http-body", 588 | "httparse", 589 | "httpdate", 590 | "itoa", 591 | "pin-project-lite", 592 | "socket2", 593 | "tokio", 594 | "tower-service", 595 | "tracing", 596 | "want", 597 | ] 598 | 599 | [[package]] 600 | name = "hyper-tls" 601 | version = "0.5.0" 602 | source = "registry+https://github.com/rust-lang/crates.io-index" 603 | checksum = "d6183ddfa99b85da61a140bea0efc93fdf56ceaa041b37d553518030827f9905" 604 | dependencies = [ 605 | "bytes", 606 | "hyper", 607 | "native-tls", 608 | "tokio", 609 | "tokio-native-tls", 610 | ] 611 | 612 | [[package]] 613 | name = "icu_collections" 614 | version = "1.5.0" 615 | source = "registry+https://github.com/rust-lang/crates.io-index" 616 | checksum = "db2fa452206ebee18c4b5c2274dbf1de17008e874b4dc4f0aea9d01ca79e4526" 617 | dependencies = [ 618 | "displaydoc", 619 | "yoke", 620 | "zerofrom", 621 | "zerovec", 622 | ] 623 | 624 | [[package]] 625 | name = "icu_locid" 626 | version = "1.5.0" 627 | source = "registry+https://github.com/rust-lang/crates.io-index" 628 | checksum = "13acbb8371917fc971be86fc8057c41a64b521c184808a698c02acc242dbf637" 629 | dependencies = [ 630 | "displaydoc", 631 | "litemap", 632 | "tinystr", 633 | "writeable", 634 | "zerovec", 635 | ] 636 | 637 | [[package]] 638 | name = "icu_locid_transform" 639 | version = "1.5.0" 640 | source = "registry+https://github.com/rust-lang/crates.io-index" 641 | checksum = "01d11ac35de8e40fdeda00d9e1e9d92525f3f9d887cdd7aa81d727596788b54e" 642 | dependencies = [ 643 | "displaydoc", 644 | "icu_locid", 645 | "icu_locid_transform_data", 646 | "icu_provider", 647 | "tinystr", 648 | "zerovec", 649 | ] 650 | 651 | [[package]] 652 | name = "icu_locid_transform_data" 653 | version = "1.5.1" 654 | source = "registry+https://github.com/rust-lang/crates.io-index" 655 | checksum = "7515e6d781098bf9f7205ab3fc7e9709d34554ae0b21ddbcb5febfa4bc7df11d" 656 | 657 | [[package]] 658 | name = "icu_normalizer" 659 | version = "1.5.0" 660 | source = "registry+https://github.com/rust-lang/crates.io-index" 661 | checksum = "19ce3e0da2ec68599d193c93d088142efd7f9c5d6fc9b803774855747dc6a84f" 662 | dependencies = [ 663 | "displaydoc", 664 | "icu_collections", 665 | "icu_normalizer_data", 666 | "icu_properties", 667 | "icu_provider", 668 | "smallvec", 669 | "utf16_iter", 670 | "utf8_iter", 671 | "write16", 672 | "zerovec", 673 | ] 674 | 675 | [[package]] 676 | name = "icu_normalizer_data" 677 | version = "1.5.1" 678 | source = "registry+https://github.com/rust-lang/crates.io-index" 679 | checksum = "c5e8338228bdc8ab83303f16b797e177953730f601a96c25d10cb3ab0daa0cb7" 680 | 681 | [[package]] 682 | name = "icu_properties" 683 | version = "1.5.1" 684 | source = "registry+https://github.com/rust-lang/crates.io-index" 685 | checksum = "93d6020766cfc6302c15dbbc9c8778c37e62c14427cb7f6e601d849e092aeef5" 686 | dependencies = [ 687 | "displaydoc", 688 | "icu_collections", 689 | "icu_locid_transform", 690 | "icu_properties_data", 691 | "icu_provider", 692 | "tinystr", 693 | "zerovec", 694 | ] 695 | 696 | [[package]] 697 | name = "icu_properties_data" 698 | version = "1.5.1" 699 | source = "registry+https://github.com/rust-lang/crates.io-index" 700 | checksum = "85fb8799753b75aee8d2a21d7c14d9f38921b54b3dbda10f5a3c7a7b82dba5e2" 701 | 702 | [[package]] 703 | name = "icu_provider" 704 | version = "1.5.0" 705 | source = "registry+https://github.com/rust-lang/crates.io-index" 706 | checksum = "6ed421c8a8ef78d3e2dbc98a973be2f3770cb42b606e3ab18d6237c4dfde68d9" 707 | dependencies = [ 708 | "displaydoc", 709 | "icu_locid", 710 | "icu_provider_macros", 711 | "stable_deref_trait", 712 | "tinystr", 713 | "writeable", 714 | "yoke", 715 | "zerofrom", 716 | "zerovec", 717 | ] 718 | 719 | [[package]] 720 | name = "icu_provider_macros" 721 | version = "1.5.0" 722 | source = "registry+https://github.com/rust-lang/crates.io-index" 723 | checksum = "1ec89e9337638ecdc08744df490b221a7399bf8d164eb52a665454e60e075ad6" 724 | dependencies = [ 725 | "proc-macro2", 726 | "quote", 727 | "syn", 728 | ] 729 | 730 | [[package]] 731 | name = "idna" 732 | version = "1.0.3" 733 | source = "registry+https://github.com/rust-lang/crates.io-index" 734 | checksum = "686f825264d630750a544639377bae737628043f20d38bbc029e8f29ea968a7e" 735 | dependencies = [ 736 | "idna_adapter", 737 | "smallvec", 738 | "utf8_iter", 739 | ] 740 | 741 | [[package]] 742 | name = "idna_adapter" 743 | version = "1.2.0" 744 | source = "registry+https://github.com/rust-lang/crates.io-index" 745 | checksum = "daca1df1c957320b2cf139ac61e7bd64fed304c5040df000a745aa1de3b4ef71" 746 | dependencies = [ 747 | "icu_normalizer", 748 | "icu_properties", 749 | ] 750 | 751 | [[package]] 752 | name = "indexmap" 753 | version = "2.9.0" 754 | source = "registry+https://github.com/rust-lang/crates.io-index" 755 | checksum = "cea70ddb795996207ad57735b50c5982d8844f38ba9ee5f1aedcfb708a2aa11e" 756 | dependencies = [ 757 | "equivalent", 758 | "hashbrown", 759 | ] 760 | 761 | [[package]] 762 | name = "ipnet" 763 | version = "2.11.0" 764 | source = "registry+https://github.com/rust-lang/crates.io-index" 765 | checksum = "469fb0b9cefa57e3ef31275ee7cacb78f2fdca44e4765491884a2b119d4eb130" 766 | 767 | [[package]] 768 | name = "is_terminal_polyfill" 769 | version = "1.70.1" 770 | source = "registry+https://github.com/rust-lang/crates.io-index" 771 | checksum = "7943c866cc5cd64cbc25b2e01621d07fa8eb2a1a23160ee81ce38704e97b8ecf" 772 | 773 | [[package]] 774 | name = "itertools" 775 | version = "0.12.1" 776 | source = "registry+https://github.com/rust-lang/crates.io-index" 777 | checksum = "ba291022dbbd398a455acf126c1e341954079855bc60dfdda641363bd6922569" 778 | dependencies = [ 779 | "either", 780 | ] 781 | 782 | [[package]] 783 | name = "itertools" 784 | version = "0.13.0" 785 | source = "registry+https://github.com/rust-lang/crates.io-index" 786 | checksum = "413ee7dfc52ee1a4949ceeb7dbc8a33f2d6c088194d9f922fb8318faf1f01186" 787 | dependencies = [ 788 | "either", 789 | ] 790 | 791 | [[package]] 792 | name = "itoa" 793 | version = "1.0.15" 794 | source = "registry+https://github.com/rust-lang/crates.io-index" 795 | checksum = "4a5f13b858c8d314ee3e8f639011f7ccefe71f97f96e50151fb991f267928e2c" 796 | 797 | [[package]] 798 | name = "jni" 799 | version = "0.21.1" 800 | source = "registry+https://github.com/rust-lang/crates.io-index" 801 | checksum = "1a87aa2bb7d2af34197c04845522473242e1aa17c12f4935d5856491a7fb8c97" 802 | dependencies = [ 803 | "cesu8", 804 | "cfg-if", 805 | "combine", 806 | "jni-sys", 807 | "log", 808 | "thiserror", 809 | "walkdir", 810 | "windows-sys 0.45.0", 811 | ] 812 | 813 | [[package]] 814 | name = "jni-sys" 815 | version = "0.3.0" 816 | source = "registry+https://github.com/rust-lang/crates.io-index" 817 | checksum = "8eaf4bc02d17cbdd7ff4c7438cafcdf7fb9a4613313ad11b4f8fefe7d3fa0130" 818 | 819 | [[package]] 820 | name = "js-sys" 821 | version = "0.3.77" 822 | source = "registry+https://github.com/rust-lang/crates.io-index" 823 | checksum = "1cfaf33c695fc6e08064efbc1f72ec937429614f25eef83af942d0e227c3a28f" 824 | dependencies = [ 825 | "once_cell", 826 | "wasm-bindgen", 827 | ] 828 | 829 | [[package]] 830 | name = "libc" 831 | version = "0.2.171" 832 | source = "registry+https://github.com/rust-lang/crates.io-index" 833 | checksum = "c19937216e9d3aa9956d9bb8dfc0b0c8beb6058fc4f7a4dc4d850edf86a237d6" 834 | 835 | [[package]] 836 | name = "libredox" 837 | version = "0.1.3" 838 | source = "registry+https://github.com/rust-lang/crates.io-index" 839 | checksum = "c0ff37bd590ca25063e35af745c343cb7a0271906fb7b37e4813e8f79f00268d" 840 | dependencies = [ 841 | "bitflags 2.9.0", 842 | "libc", 843 | ] 844 | 845 | [[package]] 846 | name = "linux-raw-sys" 847 | version = "0.9.4" 848 | source = "registry+https://github.com/rust-lang/crates.io-index" 849 | checksum = "cd945864f07fe9f5371a27ad7b52a172b4b499999f1d97574c9fa68373937e12" 850 | 851 | [[package]] 852 | name = "litemap" 853 | version = "0.7.5" 854 | source = "registry+https://github.com/rust-lang/crates.io-index" 855 | checksum = "23fb14cb19457329c82206317a5663005a4d404783dc74f4252769b0d5f42856" 856 | 857 | [[package]] 858 | name = "lock_api" 859 | version = "0.4.12" 860 | source = "registry+https://github.com/rust-lang/crates.io-index" 861 | checksum = "07af8b9cdd281b7915f413fa73f29ebd5d55d0d3f0155584dade1ff18cea1b17" 862 | dependencies = [ 863 | "autocfg", 864 | "scopeguard", 865 | ] 866 | 867 | [[package]] 868 | name = "log" 869 | version = "0.4.27" 870 | source = "registry+https://github.com/rust-lang/crates.io-index" 871 | checksum = "13dc2df351e3202783a1fe0d44375f7295ffb4049267b0f3018346dc122a1d94" 872 | 873 | [[package]] 874 | name = "lru" 875 | version = "0.12.5" 876 | source = "registry+https://github.com/rust-lang/crates.io-index" 877 | checksum = "234cf4f4a04dc1f57e24b96cc0cd600cf2af460d4161ac5ecdd0af8e1f3b2a38" 878 | dependencies = [ 879 | "hashbrown", 880 | ] 881 | 882 | [[package]] 883 | name = "malloc_buf" 884 | version = "0.0.6" 885 | source = "registry+https://github.com/rust-lang/crates.io-index" 886 | checksum = "62bb907fe88d54d8d9ce32a3cceab4218ed2f6b7d35617cafe9adf84e43919cb" 887 | dependencies = [ 888 | "libc", 889 | ] 890 | 891 | [[package]] 892 | name = "memchr" 893 | version = "2.7.4" 894 | source = "registry+https://github.com/rust-lang/crates.io-index" 895 | checksum = "78ca9ab1a0babb1e7d5695e3530886289c18cf2f87ec19a575a0abdce112e3a3" 896 | 897 | [[package]] 898 | name = "mime" 899 | version = "0.3.17" 900 | source = "registry+https://github.com/rust-lang/crates.io-index" 901 | checksum = "6877bb514081ee2a7ff5ef9de3281f14a4dd4bceac4c09388074a6b5df8a139a" 902 | 903 | [[package]] 904 | name = "mime_guess" 905 | version = "2.0.5" 906 | source = "registry+https://github.com/rust-lang/crates.io-index" 907 | checksum = "f7c44f8e672c00fe5308fa235f821cb4198414e1c77935c1ab6948d3fd78550e" 908 | dependencies = [ 909 | "mime", 910 | "unicase", 911 | ] 912 | 913 | [[package]] 914 | name = "miniz_oxide" 915 | version = "0.8.8" 916 | source = "registry+https://github.com/rust-lang/crates.io-index" 917 | checksum = "3be647b768db090acb35d5ec5db2b0e1f1de11133ca123b9eacf5137868f892a" 918 | dependencies = [ 919 | "adler2", 920 | ] 921 | 922 | [[package]] 923 | name = "mio" 924 | version = "0.8.11" 925 | source = "registry+https://github.com/rust-lang/crates.io-index" 926 | checksum = "a4a650543ca06a924e8b371db273b2756685faae30f8487da1b56505a8f78b0c" 927 | dependencies = [ 928 | "libc", 929 | "log", 930 | "wasi 0.11.0+wasi-snapshot-preview1", 931 | "windows-sys 0.48.0", 932 | ] 933 | 934 | [[package]] 935 | name = "mio" 936 | version = "1.0.3" 937 | source = "registry+https://github.com/rust-lang/crates.io-index" 938 | checksum = "2886843bf800fba2e3377cff24abf6379b4c4d5c6681eaf9ea5b0d15090450bd" 939 | dependencies = [ 940 | "libc", 941 | "wasi 0.11.0+wasi-snapshot-preview1", 942 | "windows-sys 0.52.0", 943 | ] 944 | 945 | [[package]] 946 | name = "native-tls" 947 | version = "0.2.14" 948 | source = "registry+https://github.com/rust-lang/crates.io-index" 949 | checksum = "87de3442987e9dbec73158d5c715e7ad9072fda936bb03d19d7fa10e00520f0e" 950 | dependencies = [ 951 | "libc", 952 | "log", 953 | "openssl", 954 | "openssl-probe", 955 | "openssl-sys", 956 | "schannel", 957 | "security-framework", 958 | "security-framework-sys", 959 | "tempfile", 960 | ] 961 | 962 | [[package]] 963 | name = "ndk-context" 964 | version = "0.1.1" 965 | source = "registry+https://github.com/rust-lang/crates.io-index" 966 | checksum = "27b02d87554356db9e9a873add8782d4ea6e3e58ea071a9adb9a2e8ddb884a8b" 967 | 968 | [[package]] 969 | name = "nix" 970 | version = "0.29.0" 971 | source = "registry+https://github.com/rust-lang/crates.io-index" 972 | checksum = "71e2746dc3a24dd78b3cfcb7be93368c6de9963d30f43a6a73998a9cf4b17b46" 973 | dependencies = [ 974 | "bitflags 2.9.0", 975 | "cfg-if", 976 | "cfg_aliases", 977 | "libc", 978 | ] 979 | 980 | [[package]] 981 | name = "objc" 982 | version = "0.2.7" 983 | source = "registry+https://github.com/rust-lang/crates.io-index" 984 | checksum = "915b1b472bc21c53464d6c8461c9d3af805ba1ef837e1cac254428f4a77177b1" 985 | dependencies = [ 986 | "malloc_buf", 987 | ] 988 | 989 | [[package]] 990 | name = "object" 991 | version = "0.36.7" 992 | source = "registry+https://github.com/rust-lang/crates.io-index" 993 | checksum = "62948e14d923ea95ea2c7c86c71013138b66525b86bdc08d2dcc262bdb497b87" 994 | dependencies = [ 995 | "memchr", 996 | ] 997 | 998 | [[package]] 999 | name = "once_cell" 1000 | version = "1.21.3" 1001 | source = "registry+https://github.com/rust-lang/crates.io-index" 1002 | checksum = "42f5e15c9953c5e4ccceeb2e7382a716482c34515315f7b03532b8b4e8393d2d" 1003 | 1004 | [[package]] 1005 | name = "openssl" 1006 | version = "0.10.72" 1007 | source = "registry+https://github.com/rust-lang/crates.io-index" 1008 | checksum = "fedfea7d58a1f73118430a55da6a286e7b044961736ce96a16a17068ea25e5da" 1009 | dependencies = [ 1010 | "bitflags 2.9.0", 1011 | "cfg-if", 1012 | "foreign-types", 1013 | "libc", 1014 | "once_cell", 1015 | "openssl-macros", 1016 | "openssl-sys", 1017 | ] 1018 | 1019 | [[package]] 1020 | name = "openssl-macros" 1021 | version = "0.1.1" 1022 | source = "registry+https://github.com/rust-lang/crates.io-index" 1023 | checksum = "a948666b637a0f465e8564c73e89d4dde00d72d4d473cc972f390fc3dcee7d9c" 1024 | dependencies = [ 1025 | "proc-macro2", 1026 | "quote", 1027 | "syn", 1028 | ] 1029 | 1030 | [[package]] 1031 | name = "openssl-probe" 1032 | version = "0.1.6" 1033 | source = "registry+https://github.com/rust-lang/crates.io-index" 1034 | checksum = "d05e27ee213611ffe7d6348b942e8f942b37114c00cc03cec254295a4a17852e" 1035 | 1036 | [[package]] 1037 | name = "openssl-sys" 1038 | version = "0.9.107" 1039 | source = "registry+https://github.com/rust-lang/crates.io-index" 1040 | checksum = "8288979acd84749c744a9014b4382d42b8f7b2592847b5afb2ed29e5d16ede07" 1041 | dependencies = [ 1042 | "cc", 1043 | "libc", 1044 | "pkg-config", 1045 | "vcpkg", 1046 | ] 1047 | 1048 | [[package]] 1049 | name = "option-ext" 1050 | version = "0.2.0" 1051 | source = "registry+https://github.com/rust-lang/crates.io-index" 1052 | checksum = "04744f49eae99ab78e0d5c0b603ab218f515ea8cfe5a456d7629ad883a3b6e7d" 1053 | 1054 | [[package]] 1055 | name = "parking_lot" 1056 | version = "0.12.3" 1057 | source = "registry+https://github.com/rust-lang/crates.io-index" 1058 | checksum = "f1bf18183cf54e8d6059647fc3063646a1801cf30896933ec2311622cc4b9a27" 1059 | dependencies = [ 1060 | "lock_api", 1061 | "parking_lot_core", 1062 | ] 1063 | 1064 | [[package]] 1065 | name = "parking_lot_core" 1066 | version = "0.9.10" 1067 | source = "registry+https://github.com/rust-lang/crates.io-index" 1068 | checksum = "1e401f977ab385c9e4e3ab30627d6f26d00e2c73eef317493c4ec6d468726cf8" 1069 | dependencies = [ 1070 | "cfg-if", 1071 | "libc", 1072 | "redox_syscall", 1073 | "smallvec", 1074 | "windows-targets 0.52.6", 1075 | ] 1076 | 1077 | [[package]] 1078 | name = "paste" 1079 | version = "1.0.15" 1080 | source = "registry+https://github.com/rust-lang/crates.io-index" 1081 | checksum = "57c0d7b74b563b49d38dae00a0c37d4d6de9b432382b2892f0574ddcae73fd0a" 1082 | 1083 | [[package]] 1084 | name = "percent-encoding" 1085 | version = "2.3.1" 1086 | source = "registry+https://github.com/rust-lang/crates.io-index" 1087 | checksum = "e3148f5046208a5d56bcfc03053e3ca6334e51da8dfb19b6cdc8b306fae3283e" 1088 | 1089 | [[package]] 1090 | name = "pin-project-lite" 1091 | version = "0.2.16" 1092 | source = "registry+https://github.com/rust-lang/crates.io-index" 1093 | checksum = "3b3cff922bd51709b605d9ead9aa71031d81447142d828eb4a6eba76fe619f9b" 1094 | 1095 | [[package]] 1096 | name = "pin-utils" 1097 | version = "0.1.0" 1098 | source = "registry+https://github.com/rust-lang/crates.io-index" 1099 | checksum = "8b870d8c151b6f2fb93e84a13146138f05d02ed11c7e7c54f8826aaaf7c9f184" 1100 | 1101 | [[package]] 1102 | name = "pkg-config" 1103 | version = "0.3.32" 1104 | source = "registry+https://github.com/rust-lang/crates.io-index" 1105 | checksum = "7edddbd0b52d732b21ad9a5fab5c704c14cd949e5e9a1ec5929a24fded1b904c" 1106 | 1107 | [[package]] 1108 | name = "popcorn-cli" 1109 | version = "0.1.0" 1110 | dependencies = [ 1111 | "anyhow", 1112 | "base64-url", 1113 | "bytes", 1114 | "clap", 1115 | "crossterm", 1116 | "ctrlc", 1117 | "dirs", 1118 | "futures-util", 1119 | "ratatui", 1120 | "reqwest", 1121 | "serde", 1122 | "serde_json", 1123 | "serde_yaml", 1124 | "tokio", 1125 | "urlencoding", 1126 | "webbrowser", 1127 | ] 1128 | 1129 | [[package]] 1130 | name = "proc-macro2" 1131 | version = "1.0.94" 1132 | source = "registry+https://github.com/rust-lang/crates.io-index" 1133 | checksum = "a31971752e70b8b2686d7e46ec17fb38dad4051d94024c88df49b667caea9c84" 1134 | dependencies = [ 1135 | "unicode-ident", 1136 | ] 1137 | 1138 | [[package]] 1139 | name = "quote" 1140 | version = "1.0.40" 1141 | source = "registry+https://github.com/rust-lang/crates.io-index" 1142 | checksum = "1885c039570dc00dcb4ff087a89e185fd56bae234ddc7f056a945bf36467248d" 1143 | dependencies = [ 1144 | "proc-macro2", 1145 | ] 1146 | 1147 | [[package]] 1148 | name = "r-efi" 1149 | version = "5.2.0" 1150 | source = "registry+https://github.com/rust-lang/crates.io-index" 1151 | checksum = "74765f6d916ee2faa39bc8e68e4f3ed8949b48cccdac59983d287a7cb71ce9c5" 1152 | 1153 | [[package]] 1154 | name = "ratatui" 1155 | version = "0.26.3" 1156 | source = "registry+https://github.com/rust-lang/crates.io-index" 1157 | checksum = "f44c9e68fd46eda15c646fbb85e1040b657a58cdc8c98db1d97a55930d991eef" 1158 | dependencies = [ 1159 | "bitflags 2.9.0", 1160 | "cassowary", 1161 | "compact_str", 1162 | "crossterm", 1163 | "itertools 0.12.1", 1164 | "lru", 1165 | "paste", 1166 | "stability", 1167 | "strum", 1168 | "unicode-segmentation", 1169 | "unicode-truncate", 1170 | "unicode-width", 1171 | ] 1172 | 1173 | [[package]] 1174 | name = "raw-window-handle" 1175 | version = "0.5.2" 1176 | source = "registry+https://github.com/rust-lang/crates.io-index" 1177 | checksum = "f2ff9a1f06a88b01621b7ae906ef0211290d1c8a168a15542486a8f61c0833b9" 1178 | 1179 | [[package]] 1180 | name = "redox_syscall" 1181 | version = "0.5.11" 1182 | source = "registry+https://github.com/rust-lang/crates.io-index" 1183 | checksum = "d2f103c6d277498fbceb16e84d317e2a400f160f46904d5f5410848c829511a3" 1184 | dependencies = [ 1185 | "bitflags 2.9.0", 1186 | ] 1187 | 1188 | [[package]] 1189 | name = "redox_users" 1190 | version = "0.4.6" 1191 | source = "registry+https://github.com/rust-lang/crates.io-index" 1192 | checksum = "ba009ff324d1fc1b900bd1fdb31564febe58a8ccc8a6fdbb93b543d33b13ca43" 1193 | dependencies = [ 1194 | "getrandom 0.2.15", 1195 | "libredox", 1196 | "thiserror", 1197 | ] 1198 | 1199 | [[package]] 1200 | name = "reqwest" 1201 | version = "0.11.27" 1202 | source = "registry+https://github.com/rust-lang/crates.io-index" 1203 | checksum = "dd67538700a17451e7cba03ac727fb961abb7607553461627b97de0b89cf4a62" 1204 | dependencies = [ 1205 | "base64 0.21.7", 1206 | "bytes", 1207 | "encoding_rs", 1208 | "futures-core", 1209 | "futures-util", 1210 | "h2", 1211 | "http", 1212 | "http-body", 1213 | "hyper", 1214 | "hyper-tls", 1215 | "ipnet", 1216 | "js-sys", 1217 | "log", 1218 | "mime", 1219 | "mime_guess", 1220 | "native-tls", 1221 | "once_cell", 1222 | "percent-encoding", 1223 | "pin-project-lite", 1224 | "rustls-pemfile", 1225 | "serde", 1226 | "serde_json", 1227 | "serde_urlencoded", 1228 | "sync_wrapper", 1229 | "system-configuration", 1230 | "tokio", 1231 | "tokio-native-tls", 1232 | "tower-service", 1233 | "url", 1234 | "wasm-bindgen", 1235 | "wasm-bindgen-futures", 1236 | "web-sys", 1237 | "winreg", 1238 | ] 1239 | 1240 | [[package]] 1241 | name = "rustc-demangle" 1242 | version = "0.1.24" 1243 | source = "registry+https://github.com/rust-lang/crates.io-index" 1244 | checksum = "719b953e2095829ee67db738b3bfa9fa368c94900df327b3f07fe6e794d2fe1f" 1245 | 1246 | [[package]] 1247 | name = "rustix" 1248 | version = "1.0.5" 1249 | source = "registry+https://github.com/rust-lang/crates.io-index" 1250 | checksum = "d97817398dd4bb2e6da002002db259209759911da105da92bec29ccb12cf58bf" 1251 | dependencies = [ 1252 | "bitflags 2.9.0", 1253 | "errno", 1254 | "libc", 1255 | "linux-raw-sys", 1256 | "windows-sys 0.59.0", 1257 | ] 1258 | 1259 | [[package]] 1260 | name = "rustls-pemfile" 1261 | version = "1.0.4" 1262 | source = "registry+https://github.com/rust-lang/crates.io-index" 1263 | checksum = "1c74cae0a4cf6ccbbf5f359f08efdf8ee7e1dc532573bf0db71968cb56b1448c" 1264 | dependencies = [ 1265 | "base64 0.21.7", 1266 | ] 1267 | 1268 | [[package]] 1269 | name = "rustversion" 1270 | version = "1.0.20" 1271 | source = "registry+https://github.com/rust-lang/crates.io-index" 1272 | checksum = "eded382c5f5f786b989652c49544c4877d9f015cc22e145a5ea8ea66c2921cd2" 1273 | 1274 | [[package]] 1275 | name = "ryu" 1276 | version = "1.0.20" 1277 | source = "registry+https://github.com/rust-lang/crates.io-index" 1278 | checksum = "28d3b2b1366ec20994f1fd18c3c594f05c5dd4bc44d8bb0c1c632c8d6829481f" 1279 | 1280 | [[package]] 1281 | name = "same-file" 1282 | version = "1.0.6" 1283 | source = "registry+https://github.com/rust-lang/crates.io-index" 1284 | checksum = "93fc1dc3aaa9bfed95e02e6eadabb4baf7e3078b0bd1b4d7b6b0b68378900502" 1285 | dependencies = [ 1286 | "winapi-util", 1287 | ] 1288 | 1289 | [[package]] 1290 | name = "schannel" 1291 | version = "0.1.27" 1292 | source = "registry+https://github.com/rust-lang/crates.io-index" 1293 | checksum = "1f29ebaa345f945cec9fbbc532eb307f0fdad8161f281b6369539c8d84876b3d" 1294 | dependencies = [ 1295 | "windows-sys 0.59.0", 1296 | ] 1297 | 1298 | [[package]] 1299 | name = "scopeguard" 1300 | version = "1.2.0" 1301 | source = "registry+https://github.com/rust-lang/crates.io-index" 1302 | checksum = "94143f37725109f92c262ed2cf5e59bce7498c01bcc1502d7b9afe439a4e9f49" 1303 | 1304 | [[package]] 1305 | name = "security-framework" 1306 | version = "2.11.1" 1307 | source = "registry+https://github.com/rust-lang/crates.io-index" 1308 | checksum = "897b2245f0b511c87893af39b033e5ca9cce68824c4d7e7630b5a1d339658d02" 1309 | dependencies = [ 1310 | "bitflags 2.9.0", 1311 | "core-foundation", 1312 | "core-foundation-sys", 1313 | "libc", 1314 | "security-framework-sys", 1315 | ] 1316 | 1317 | [[package]] 1318 | name = "security-framework-sys" 1319 | version = "2.14.0" 1320 | source = "registry+https://github.com/rust-lang/crates.io-index" 1321 | checksum = "49db231d56a190491cb4aeda9527f1ad45345af50b0851622a7adb8c03b01c32" 1322 | dependencies = [ 1323 | "core-foundation-sys", 1324 | "libc", 1325 | ] 1326 | 1327 | [[package]] 1328 | name = "serde" 1329 | version = "1.0.219" 1330 | source = "registry+https://github.com/rust-lang/crates.io-index" 1331 | checksum = "5f0e2c6ed6606019b4e29e69dbaba95b11854410e5347d525002456dbbb786b6" 1332 | dependencies = [ 1333 | "serde_derive", 1334 | ] 1335 | 1336 | [[package]] 1337 | name = "serde_derive" 1338 | version = "1.0.219" 1339 | source = "registry+https://github.com/rust-lang/crates.io-index" 1340 | checksum = "5b0276cf7f2c73365f7157c8123c21cd9a50fbbd844757af28ca1f5925fc2a00" 1341 | dependencies = [ 1342 | "proc-macro2", 1343 | "quote", 1344 | "syn", 1345 | ] 1346 | 1347 | [[package]] 1348 | name = "serde_json" 1349 | version = "1.0.140" 1350 | source = "registry+https://github.com/rust-lang/crates.io-index" 1351 | checksum = "20068b6e96dc6c9bd23e01df8827e6c7e1f2fddd43c21810382803c136b99373" 1352 | dependencies = [ 1353 | "itoa", 1354 | "memchr", 1355 | "ryu", 1356 | "serde", 1357 | ] 1358 | 1359 | [[package]] 1360 | name = "serde_urlencoded" 1361 | version = "0.7.1" 1362 | source = "registry+https://github.com/rust-lang/crates.io-index" 1363 | checksum = "d3491c14715ca2294c4d6a88f15e84739788c1d030eed8c110436aafdaa2f3fd" 1364 | dependencies = [ 1365 | "form_urlencoded", 1366 | "itoa", 1367 | "ryu", 1368 | "serde", 1369 | ] 1370 | 1371 | [[package]] 1372 | name = "serde_yaml" 1373 | version = "0.9.34+deprecated" 1374 | source = "registry+https://github.com/rust-lang/crates.io-index" 1375 | checksum = "6a8b1a1a2ebf674015cc02edccce75287f1a0130d394307b36743c2f5d504b47" 1376 | dependencies = [ 1377 | "indexmap", 1378 | "itoa", 1379 | "ryu", 1380 | "serde", 1381 | "unsafe-libyaml", 1382 | ] 1383 | 1384 | [[package]] 1385 | name = "shlex" 1386 | version = "1.3.0" 1387 | source = "registry+https://github.com/rust-lang/crates.io-index" 1388 | checksum = "0fda2ff0d084019ba4d7c6f371c95d8fd75ce3524c3cb8fb653a3023f6323e64" 1389 | 1390 | [[package]] 1391 | name = "signal-hook" 1392 | version = "0.3.17" 1393 | source = "registry+https://github.com/rust-lang/crates.io-index" 1394 | checksum = "8621587d4798caf8eb44879d42e56b9a93ea5dcd315a6487c357130095b62801" 1395 | dependencies = [ 1396 | "libc", 1397 | "signal-hook-registry", 1398 | ] 1399 | 1400 | [[package]] 1401 | name = "signal-hook-mio" 1402 | version = "0.2.4" 1403 | source = "registry+https://github.com/rust-lang/crates.io-index" 1404 | checksum = "34db1a06d485c9142248b7a054f034b349b212551f3dfd19c94d45a754a217cd" 1405 | dependencies = [ 1406 | "libc", 1407 | "mio 0.8.11", 1408 | "signal-hook", 1409 | ] 1410 | 1411 | [[package]] 1412 | name = "signal-hook-registry" 1413 | version = "1.4.2" 1414 | source = "registry+https://github.com/rust-lang/crates.io-index" 1415 | checksum = "a9e9e0b4211b72e7b8b6e85c807d36c212bdb33ea8587f7569562a84df5465b1" 1416 | dependencies = [ 1417 | "libc", 1418 | ] 1419 | 1420 | [[package]] 1421 | name = "slab" 1422 | version = "0.4.9" 1423 | source = "registry+https://github.com/rust-lang/crates.io-index" 1424 | checksum = "8f92a496fb766b417c996b9c5e57daf2f7ad3b0bebe1ccfca4856390e3d3bb67" 1425 | dependencies = [ 1426 | "autocfg", 1427 | ] 1428 | 1429 | [[package]] 1430 | name = "smallvec" 1431 | version = "1.15.0" 1432 | source = "registry+https://github.com/rust-lang/crates.io-index" 1433 | checksum = "8917285742e9f3e1683f0a9c4e6b57960b7314d0b08d30d1ecd426713ee2eee9" 1434 | 1435 | [[package]] 1436 | name = "socket2" 1437 | version = "0.5.9" 1438 | source = "registry+https://github.com/rust-lang/crates.io-index" 1439 | checksum = "4f5fd57c80058a56cf5c777ab8a126398ece8e442983605d280a44ce79d0edef" 1440 | dependencies = [ 1441 | "libc", 1442 | "windows-sys 0.52.0", 1443 | ] 1444 | 1445 | [[package]] 1446 | name = "stability" 1447 | version = "0.2.1" 1448 | source = "registry+https://github.com/rust-lang/crates.io-index" 1449 | checksum = "d904e7009df136af5297832a3ace3370cd14ff1546a232f4f185036c2736fcac" 1450 | dependencies = [ 1451 | "quote", 1452 | "syn", 1453 | ] 1454 | 1455 | [[package]] 1456 | name = "stable_deref_trait" 1457 | version = "1.2.0" 1458 | source = "registry+https://github.com/rust-lang/crates.io-index" 1459 | checksum = "a8f112729512f8e442d81f95a8a7ddf2b7c6b8a1a6f509a95864142b30cab2d3" 1460 | 1461 | [[package]] 1462 | name = "static_assertions" 1463 | version = "1.1.0" 1464 | source = "registry+https://github.com/rust-lang/crates.io-index" 1465 | checksum = "a2eb9349b6444b326872e140eb1cf5e7c522154d69e7a0ffb0fb81c06b37543f" 1466 | 1467 | [[package]] 1468 | name = "strsim" 1469 | version = "0.11.1" 1470 | source = "registry+https://github.com/rust-lang/crates.io-index" 1471 | checksum = "7da8b5736845d9f2fcb837ea5d9e2628564b3b043a70948a3f0b778838c5fb4f" 1472 | 1473 | [[package]] 1474 | name = "strum" 1475 | version = "0.26.3" 1476 | source = "registry+https://github.com/rust-lang/crates.io-index" 1477 | checksum = "8fec0f0aef304996cf250b31b5a10dee7980c85da9d759361292b8bca5a18f06" 1478 | dependencies = [ 1479 | "strum_macros", 1480 | ] 1481 | 1482 | [[package]] 1483 | name = "strum_macros" 1484 | version = "0.26.4" 1485 | source = "registry+https://github.com/rust-lang/crates.io-index" 1486 | checksum = "4c6bee85a5a24955dc440386795aa378cd9cf82acd5f764469152d2270e581be" 1487 | dependencies = [ 1488 | "heck", 1489 | "proc-macro2", 1490 | "quote", 1491 | "rustversion", 1492 | "syn", 1493 | ] 1494 | 1495 | [[package]] 1496 | name = "syn" 1497 | version = "2.0.100" 1498 | source = "registry+https://github.com/rust-lang/crates.io-index" 1499 | checksum = "b09a44accad81e1ba1cd74a32461ba89dee89095ba17b32f5d03683b1b1fc2a0" 1500 | dependencies = [ 1501 | "proc-macro2", 1502 | "quote", 1503 | "unicode-ident", 1504 | ] 1505 | 1506 | [[package]] 1507 | name = "sync_wrapper" 1508 | version = "0.1.2" 1509 | source = "registry+https://github.com/rust-lang/crates.io-index" 1510 | checksum = "2047c6ded9c721764247e62cd3b03c09ffc529b2ba5b10ec482ae507a4a70160" 1511 | 1512 | [[package]] 1513 | name = "synstructure" 1514 | version = "0.13.1" 1515 | source = "registry+https://github.com/rust-lang/crates.io-index" 1516 | checksum = "c8af7666ab7b6390ab78131fb5b0fce11d6b7a6951602017c35fa82800708971" 1517 | dependencies = [ 1518 | "proc-macro2", 1519 | "quote", 1520 | "syn", 1521 | ] 1522 | 1523 | [[package]] 1524 | name = "system-configuration" 1525 | version = "0.5.1" 1526 | source = "registry+https://github.com/rust-lang/crates.io-index" 1527 | checksum = "ba3a3adc5c275d719af8cb4272ea1c4a6d668a777f37e115f6d11ddbc1c8e0e7" 1528 | dependencies = [ 1529 | "bitflags 1.3.2", 1530 | "core-foundation", 1531 | "system-configuration-sys", 1532 | ] 1533 | 1534 | [[package]] 1535 | name = "system-configuration-sys" 1536 | version = "0.5.0" 1537 | source = "registry+https://github.com/rust-lang/crates.io-index" 1538 | checksum = "a75fb188eb626b924683e3b95e3a48e63551fcfb51949de2f06a9d91dbee93c9" 1539 | dependencies = [ 1540 | "core-foundation-sys", 1541 | "libc", 1542 | ] 1543 | 1544 | [[package]] 1545 | name = "tempfile" 1546 | version = "3.19.1" 1547 | source = "registry+https://github.com/rust-lang/crates.io-index" 1548 | checksum = "7437ac7763b9b123ccf33c338a5cc1bac6f69b45a136c19bdd8a65e3916435bf" 1549 | dependencies = [ 1550 | "fastrand", 1551 | "getrandom 0.3.2", 1552 | "once_cell", 1553 | "rustix", 1554 | "windows-sys 0.59.0", 1555 | ] 1556 | 1557 | [[package]] 1558 | name = "thiserror" 1559 | version = "1.0.69" 1560 | source = "registry+https://github.com/rust-lang/crates.io-index" 1561 | checksum = "b6aaf5339b578ea85b50e080feb250a3e8ae8cfcdff9a461c9ec2904bc923f52" 1562 | dependencies = [ 1563 | "thiserror-impl", 1564 | ] 1565 | 1566 | [[package]] 1567 | name = "thiserror-impl" 1568 | version = "1.0.69" 1569 | source = "registry+https://github.com/rust-lang/crates.io-index" 1570 | checksum = "4fee6c4efc90059e10f81e6d42c60a18f76588c3d74cb83a0b242a2b6c7504c1" 1571 | dependencies = [ 1572 | "proc-macro2", 1573 | "quote", 1574 | "syn", 1575 | ] 1576 | 1577 | [[package]] 1578 | name = "tinystr" 1579 | version = "0.7.6" 1580 | source = "registry+https://github.com/rust-lang/crates.io-index" 1581 | checksum = "9117f5d4db391c1cf6927e7bea3db74b9a1c1add8f7eda9ffd5364f40f57b82f" 1582 | dependencies = [ 1583 | "displaydoc", 1584 | "zerovec", 1585 | ] 1586 | 1587 | [[package]] 1588 | name = "tokio" 1589 | version = "1.44.2" 1590 | source = "registry+https://github.com/rust-lang/crates.io-index" 1591 | checksum = "e6b88822cbe49de4185e3a4cbf8321dd487cf5fe0c5c65695fef6346371e9c48" 1592 | dependencies = [ 1593 | "backtrace", 1594 | "bytes", 1595 | "libc", 1596 | "mio 1.0.3", 1597 | "parking_lot", 1598 | "pin-project-lite", 1599 | "signal-hook-registry", 1600 | "socket2", 1601 | "tokio-macros", 1602 | "windows-sys 0.52.0", 1603 | ] 1604 | 1605 | [[package]] 1606 | name = "tokio-macros" 1607 | version = "2.5.0" 1608 | source = "registry+https://github.com/rust-lang/crates.io-index" 1609 | checksum = "6e06d43f1345a3bcd39f6a56dbb7dcab2ba47e68e8ac134855e7e2bdbaf8cab8" 1610 | dependencies = [ 1611 | "proc-macro2", 1612 | "quote", 1613 | "syn", 1614 | ] 1615 | 1616 | [[package]] 1617 | name = "tokio-native-tls" 1618 | version = "0.3.1" 1619 | source = "registry+https://github.com/rust-lang/crates.io-index" 1620 | checksum = "bbae76ab933c85776efabc971569dd6119c580d8f5d448769dec1764bf796ef2" 1621 | dependencies = [ 1622 | "native-tls", 1623 | "tokio", 1624 | ] 1625 | 1626 | [[package]] 1627 | name = "tokio-util" 1628 | version = "0.7.14" 1629 | source = "registry+https://github.com/rust-lang/crates.io-index" 1630 | checksum = "6b9590b93e6fcc1739458317cccd391ad3955e2bde8913edf6f95f9e65a8f034" 1631 | dependencies = [ 1632 | "bytes", 1633 | "futures-core", 1634 | "futures-sink", 1635 | "pin-project-lite", 1636 | "tokio", 1637 | ] 1638 | 1639 | [[package]] 1640 | name = "tower-service" 1641 | version = "0.3.3" 1642 | source = "registry+https://github.com/rust-lang/crates.io-index" 1643 | checksum = "8df9b6e13f2d32c91b9bd719c00d1958837bc7dec474d94952798cc8e69eeec3" 1644 | 1645 | [[package]] 1646 | name = "tracing" 1647 | version = "0.1.41" 1648 | source = "registry+https://github.com/rust-lang/crates.io-index" 1649 | checksum = "784e0ac535deb450455cbfa28a6f0df145ea1bb7ae51b821cf5e7927fdcfbdd0" 1650 | dependencies = [ 1651 | "pin-project-lite", 1652 | "tracing-core", 1653 | ] 1654 | 1655 | [[package]] 1656 | name = "tracing-core" 1657 | version = "0.1.33" 1658 | source = "registry+https://github.com/rust-lang/crates.io-index" 1659 | checksum = "e672c95779cf947c5311f83787af4fa8fffd12fb27e4993211a84bdfd9610f9c" 1660 | dependencies = [ 1661 | "once_cell", 1662 | ] 1663 | 1664 | [[package]] 1665 | name = "try-lock" 1666 | version = "0.2.5" 1667 | source = "registry+https://github.com/rust-lang/crates.io-index" 1668 | checksum = "e421abadd41a4225275504ea4d6566923418b7f05506fbc9c0fe86ba7396114b" 1669 | 1670 | [[package]] 1671 | name = "unicase" 1672 | version = "2.8.1" 1673 | source = "registry+https://github.com/rust-lang/crates.io-index" 1674 | checksum = "75b844d17643ee918803943289730bec8aac480150456169e647ed0b576ba539" 1675 | 1676 | [[package]] 1677 | name = "unicode-ident" 1678 | version = "1.0.18" 1679 | source = "registry+https://github.com/rust-lang/crates.io-index" 1680 | checksum = "5a5f39404a5da50712a4c1eecf25e90dd62b613502b7e925fd4e4d19b5c96512" 1681 | 1682 | [[package]] 1683 | name = "unicode-segmentation" 1684 | version = "1.12.0" 1685 | source = "registry+https://github.com/rust-lang/crates.io-index" 1686 | checksum = "f6ccf251212114b54433ec949fd6a7841275f9ada20dddd2f29e9ceea4501493" 1687 | 1688 | [[package]] 1689 | name = "unicode-truncate" 1690 | version = "1.1.0" 1691 | source = "registry+https://github.com/rust-lang/crates.io-index" 1692 | checksum = "b3644627a5af5fa321c95b9b235a72fd24cd29c648c2c379431e6628655627bf" 1693 | dependencies = [ 1694 | "itertools 0.13.0", 1695 | "unicode-segmentation", 1696 | "unicode-width", 1697 | ] 1698 | 1699 | [[package]] 1700 | name = "unicode-width" 1701 | version = "0.1.14" 1702 | source = "registry+https://github.com/rust-lang/crates.io-index" 1703 | checksum = "7dd6e30e90baa6f72411720665d41d89b9a3d039dc45b8faea1ddd07f617f6af" 1704 | 1705 | [[package]] 1706 | name = "unsafe-libyaml" 1707 | version = "0.2.11" 1708 | source = "registry+https://github.com/rust-lang/crates.io-index" 1709 | checksum = "673aac59facbab8a9007c7f6108d11f63b603f7cabff99fabf650fea5c32b861" 1710 | 1711 | [[package]] 1712 | name = "url" 1713 | version = "2.5.4" 1714 | source = "registry+https://github.com/rust-lang/crates.io-index" 1715 | checksum = "32f8b686cadd1473f4bd0117a5d28d36b1ade384ea9b5069a1c40aefed7fda60" 1716 | dependencies = [ 1717 | "form_urlencoded", 1718 | "idna", 1719 | "percent-encoding", 1720 | ] 1721 | 1722 | [[package]] 1723 | name = "urlencoding" 1724 | version = "2.1.3" 1725 | source = "registry+https://github.com/rust-lang/crates.io-index" 1726 | checksum = "daf8dba3b7eb870caf1ddeed7bc9d2a049f3cfdfae7cb521b087cc33ae4c49da" 1727 | 1728 | [[package]] 1729 | name = "utf16_iter" 1730 | version = "1.0.5" 1731 | source = "registry+https://github.com/rust-lang/crates.io-index" 1732 | checksum = "c8232dd3cdaed5356e0f716d285e4b40b932ac434100fe9b7e0e8e935b9e6246" 1733 | 1734 | [[package]] 1735 | name = "utf8_iter" 1736 | version = "1.0.4" 1737 | source = "registry+https://github.com/rust-lang/crates.io-index" 1738 | checksum = "b6c140620e7ffbb22c2dee59cafe6084a59b5ffc27a8859a5f0d494b5d52b6be" 1739 | 1740 | [[package]] 1741 | name = "utf8parse" 1742 | version = "0.2.2" 1743 | source = "registry+https://github.com/rust-lang/crates.io-index" 1744 | checksum = "06abde3611657adf66d383f00b093d7faecc7fa57071cce2578660c9f1010821" 1745 | 1746 | [[package]] 1747 | name = "vcpkg" 1748 | version = "0.2.15" 1749 | source = "registry+https://github.com/rust-lang/crates.io-index" 1750 | checksum = "accd4ea62f7bb7a82fe23066fb0957d48ef677f6eeb8215f372f52e48bb32426" 1751 | 1752 | [[package]] 1753 | name = "walkdir" 1754 | version = "2.5.0" 1755 | source = "registry+https://github.com/rust-lang/crates.io-index" 1756 | checksum = "29790946404f91d9c5d06f9874efddea1dc06c5efe94541a7d6863108e3a5e4b" 1757 | dependencies = [ 1758 | "same-file", 1759 | "winapi-util", 1760 | ] 1761 | 1762 | [[package]] 1763 | name = "want" 1764 | version = "0.3.1" 1765 | source = "registry+https://github.com/rust-lang/crates.io-index" 1766 | checksum = "bfa7760aed19e106de2c7c0b581b509f2f25d3dacaf737cb82ac61bc6d760b0e" 1767 | dependencies = [ 1768 | "try-lock", 1769 | ] 1770 | 1771 | [[package]] 1772 | name = "wasi" 1773 | version = "0.11.0+wasi-snapshot-preview1" 1774 | source = "registry+https://github.com/rust-lang/crates.io-index" 1775 | checksum = "9c8d87e72b64a3b4db28d11ce29237c246188f4f51057d65a7eab63b7987e423" 1776 | 1777 | [[package]] 1778 | name = "wasi" 1779 | version = "0.14.2+wasi-0.2.4" 1780 | source = "registry+https://github.com/rust-lang/crates.io-index" 1781 | checksum = "9683f9a5a998d873c0d21fcbe3c083009670149a8fab228644b8bd36b2c48cb3" 1782 | dependencies = [ 1783 | "wit-bindgen-rt", 1784 | ] 1785 | 1786 | [[package]] 1787 | name = "wasm-bindgen" 1788 | version = "0.2.100" 1789 | source = "registry+https://github.com/rust-lang/crates.io-index" 1790 | checksum = "1edc8929d7499fc4e8f0be2262a241556cfc54a0bea223790e71446f2aab1ef5" 1791 | dependencies = [ 1792 | "cfg-if", 1793 | "once_cell", 1794 | "rustversion", 1795 | "wasm-bindgen-macro", 1796 | ] 1797 | 1798 | [[package]] 1799 | name = "wasm-bindgen-backend" 1800 | version = "0.2.100" 1801 | source = "registry+https://github.com/rust-lang/crates.io-index" 1802 | checksum = "2f0a0651a5c2bc21487bde11ee802ccaf4c51935d0d3d42a6101f98161700bc6" 1803 | dependencies = [ 1804 | "bumpalo", 1805 | "log", 1806 | "proc-macro2", 1807 | "quote", 1808 | "syn", 1809 | "wasm-bindgen-shared", 1810 | ] 1811 | 1812 | [[package]] 1813 | name = "wasm-bindgen-futures" 1814 | version = "0.4.50" 1815 | source = "registry+https://github.com/rust-lang/crates.io-index" 1816 | checksum = "555d470ec0bc3bb57890405e5d4322cc9ea83cebb085523ced7be4144dac1e61" 1817 | dependencies = [ 1818 | "cfg-if", 1819 | "js-sys", 1820 | "once_cell", 1821 | "wasm-bindgen", 1822 | "web-sys", 1823 | ] 1824 | 1825 | [[package]] 1826 | name = "wasm-bindgen-macro" 1827 | version = "0.2.100" 1828 | source = "registry+https://github.com/rust-lang/crates.io-index" 1829 | checksum = "7fe63fc6d09ed3792bd0897b314f53de8e16568c2b3f7982f468c0bf9bd0b407" 1830 | dependencies = [ 1831 | "quote", 1832 | "wasm-bindgen-macro-support", 1833 | ] 1834 | 1835 | [[package]] 1836 | name = "wasm-bindgen-macro-support" 1837 | version = "0.2.100" 1838 | source = "registry+https://github.com/rust-lang/crates.io-index" 1839 | checksum = "8ae87ea40c9f689fc23f209965b6fb8a99ad69aeeb0231408be24920604395de" 1840 | dependencies = [ 1841 | "proc-macro2", 1842 | "quote", 1843 | "syn", 1844 | "wasm-bindgen-backend", 1845 | "wasm-bindgen-shared", 1846 | ] 1847 | 1848 | [[package]] 1849 | name = "wasm-bindgen-shared" 1850 | version = "0.2.100" 1851 | source = "registry+https://github.com/rust-lang/crates.io-index" 1852 | checksum = "1a05d73b933a847d6cccdda8f838a22ff101ad9bf93e33684f39c1f5f0eece3d" 1853 | dependencies = [ 1854 | "unicode-ident", 1855 | ] 1856 | 1857 | [[package]] 1858 | name = "web-sys" 1859 | version = "0.3.77" 1860 | source = "registry+https://github.com/rust-lang/crates.io-index" 1861 | checksum = "33b6dd2ef9186f1f2072e409e99cd22a975331a6b3591b12c764e0e55c60d5d2" 1862 | dependencies = [ 1863 | "js-sys", 1864 | "wasm-bindgen", 1865 | ] 1866 | 1867 | [[package]] 1868 | name = "webbrowser" 1869 | version = "0.8.15" 1870 | source = "registry+https://github.com/rust-lang/crates.io-index" 1871 | checksum = "db67ae75a9405634f5882791678772c94ff5f16a66535aae186e26aa0841fc8b" 1872 | dependencies = [ 1873 | "core-foundation", 1874 | "home", 1875 | "jni", 1876 | "log", 1877 | "ndk-context", 1878 | "objc", 1879 | "raw-window-handle", 1880 | "url", 1881 | "web-sys", 1882 | ] 1883 | 1884 | [[package]] 1885 | name = "winapi" 1886 | version = "0.3.9" 1887 | source = "registry+https://github.com/rust-lang/crates.io-index" 1888 | checksum = "5c839a674fcd7a98952e593242ea400abe93992746761e38641405d28b00f419" 1889 | dependencies = [ 1890 | "winapi-i686-pc-windows-gnu", 1891 | "winapi-x86_64-pc-windows-gnu", 1892 | ] 1893 | 1894 | [[package]] 1895 | name = "winapi-i686-pc-windows-gnu" 1896 | version = "0.4.0" 1897 | source = "registry+https://github.com/rust-lang/crates.io-index" 1898 | checksum = "ac3b87c63620426dd9b991e5ce0329eff545bccbbb34f3be09ff6fb6ab51b7b6" 1899 | 1900 | [[package]] 1901 | name = "winapi-util" 1902 | version = "0.1.9" 1903 | source = "registry+https://github.com/rust-lang/crates.io-index" 1904 | checksum = "cf221c93e13a30d793f7645a0e7762c55d169dbb0a49671918a2319d289b10bb" 1905 | dependencies = [ 1906 | "windows-sys 0.59.0", 1907 | ] 1908 | 1909 | [[package]] 1910 | name = "winapi-x86_64-pc-windows-gnu" 1911 | version = "0.4.0" 1912 | source = "registry+https://github.com/rust-lang/crates.io-index" 1913 | checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f" 1914 | 1915 | [[package]] 1916 | name = "windows-sys" 1917 | version = "0.45.0" 1918 | source = "registry+https://github.com/rust-lang/crates.io-index" 1919 | checksum = "75283be5efb2831d37ea142365f009c02ec203cd29a3ebecbc093d52315b66d0" 1920 | dependencies = [ 1921 | "windows-targets 0.42.2", 1922 | ] 1923 | 1924 | [[package]] 1925 | name = "windows-sys" 1926 | version = "0.48.0" 1927 | source = "registry+https://github.com/rust-lang/crates.io-index" 1928 | checksum = "677d2418bec65e3338edb076e806bc1ec15693c5d0104683f2efe857f61056a9" 1929 | dependencies = [ 1930 | "windows-targets 0.48.5", 1931 | ] 1932 | 1933 | [[package]] 1934 | name = "windows-sys" 1935 | version = "0.52.0" 1936 | source = "registry+https://github.com/rust-lang/crates.io-index" 1937 | checksum = "282be5f36a8ce781fad8c8ae18fa3f9beff57ec1b52cb3de0789201425d9a33d" 1938 | dependencies = [ 1939 | "windows-targets 0.52.6", 1940 | ] 1941 | 1942 | [[package]] 1943 | name = "windows-sys" 1944 | version = "0.59.0" 1945 | source = "registry+https://github.com/rust-lang/crates.io-index" 1946 | checksum = "1e38bc4d79ed67fd075bcc251a1c39b32a1776bbe92e5bef1f0bf1f8c531853b" 1947 | dependencies = [ 1948 | "windows-targets 0.52.6", 1949 | ] 1950 | 1951 | [[package]] 1952 | name = "windows-targets" 1953 | version = "0.42.2" 1954 | source = "registry+https://github.com/rust-lang/crates.io-index" 1955 | checksum = "8e5180c00cd44c9b1c88adb3693291f1cd93605ded80c250a75d472756b4d071" 1956 | dependencies = [ 1957 | "windows_aarch64_gnullvm 0.42.2", 1958 | "windows_aarch64_msvc 0.42.2", 1959 | "windows_i686_gnu 0.42.2", 1960 | "windows_i686_msvc 0.42.2", 1961 | "windows_x86_64_gnu 0.42.2", 1962 | "windows_x86_64_gnullvm 0.42.2", 1963 | "windows_x86_64_msvc 0.42.2", 1964 | ] 1965 | 1966 | [[package]] 1967 | name = "windows-targets" 1968 | version = "0.48.5" 1969 | source = "registry+https://github.com/rust-lang/crates.io-index" 1970 | checksum = "9a2fa6e2155d7247be68c096456083145c183cbbbc2764150dda45a87197940c" 1971 | dependencies = [ 1972 | "windows_aarch64_gnullvm 0.48.5", 1973 | "windows_aarch64_msvc 0.48.5", 1974 | "windows_i686_gnu 0.48.5", 1975 | "windows_i686_msvc 0.48.5", 1976 | "windows_x86_64_gnu 0.48.5", 1977 | "windows_x86_64_gnullvm 0.48.5", 1978 | "windows_x86_64_msvc 0.48.5", 1979 | ] 1980 | 1981 | [[package]] 1982 | name = "windows-targets" 1983 | version = "0.52.6" 1984 | source = "registry+https://github.com/rust-lang/crates.io-index" 1985 | checksum = "9b724f72796e036ab90c1021d4780d4d3d648aca59e491e6b98e725b84e99973" 1986 | dependencies = [ 1987 | "windows_aarch64_gnullvm 0.52.6", 1988 | "windows_aarch64_msvc 0.52.6", 1989 | "windows_i686_gnu 0.52.6", 1990 | "windows_i686_gnullvm", 1991 | "windows_i686_msvc 0.52.6", 1992 | "windows_x86_64_gnu 0.52.6", 1993 | "windows_x86_64_gnullvm 0.52.6", 1994 | "windows_x86_64_msvc 0.52.6", 1995 | ] 1996 | 1997 | [[package]] 1998 | name = "windows_aarch64_gnullvm" 1999 | version = "0.42.2" 2000 | source = "registry+https://github.com/rust-lang/crates.io-index" 2001 | checksum = "597a5118570b68bc08d8d59125332c54f1ba9d9adeedeef5b99b02ba2b0698f8" 2002 | 2003 | [[package]] 2004 | name = "windows_aarch64_gnullvm" 2005 | version = "0.48.5" 2006 | source = "registry+https://github.com/rust-lang/crates.io-index" 2007 | checksum = "2b38e32f0abccf9987a4e3079dfb67dcd799fb61361e53e2882c3cbaf0d905d8" 2008 | 2009 | [[package]] 2010 | name = "windows_aarch64_gnullvm" 2011 | version = "0.52.6" 2012 | source = "registry+https://github.com/rust-lang/crates.io-index" 2013 | checksum = "32a4622180e7a0ec044bb555404c800bc9fd9ec262ec147edd5989ccd0c02cd3" 2014 | 2015 | [[package]] 2016 | name = "windows_aarch64_msvc" 2017 | version = "0.42.2" 2018 | source = "registry+https://github.com/rust-lang/crates.io-index" 2019 | checksum = "e08e8864a60f06ef0d0ff4ba04124db8b0fb3be5776a5cd47641e942e58c4d43" 2020 | 2021 | [[package]] 2022 | name = "windows_aarch64_msvc" 2023 | version = "0.48.5" 2024 | source = "registry+https://github.com/rust-lang/crates.io-index" 2025 | checksum = "dc35310971f3b2dbbf3f0690a219f40e2d9afcf64f9ab7cc1be722937c26b4bc" 2026 | 2027 | [[package]] 2028 | name = "windows_aarch64_msvc" 2029 | version = "0.52.6" 2030 | source = "registry+https://github.com/rust-lang/crates.io-index" 2031 | checksum = "09ec2a7bb152e2252b53fa7803150007879548bc709c039df7627cabbd05d469" 2032 | 2033 | [[package]] 2034 | name = "windows_i686_gnu" 2035 | version = "0.42.2" 2036 | source = "registry+https://github.com/rust-lang/crates.io-index" 2037 | checksum = "c61d927d8da41da96a81f029489353e68739737d3beca43145c8afec9a31a84f" 2038 | 2039 | [[package]] 2040 | name = "windows_i686_gnu" 2041 | version = "0.48.5" 2042 | source = "registry+https://github.com/rust-lang/crates.io-index" 2043 | checksum = "a75915e7def60c94dcef72200b9a8e58e5091744960da64ec734a6c6e9b3743e" 2044 | 2045 | [[package]] 2046 | name = "windows_i686_gnu" 2047 | version = "0.52.6" 2048 | source = "registry+https://github.com/rust-lang/crates.io-index" 2049 | checksum = "8e9b5ad5ab802e97eb8e295ac6720e509ee4c243f69d781394014ebfe8bbfa0b" 2050 | 2051 | [[package]] 2052 | name = "windows_i686_gnullvm" 2053 | version = "0.52.6" 2054 | source = "registry+https://github.com/rust-lang/crates.io-index" 2055 | checksum = "0eee52d38c090b3caa76c563b86c3a4bd71ef1a819287c19d586d7334ae8ed66" 2056 | 2057 | [[package]] 2058 | name = "windows_i686_msvc" 2059 | version = "0.42.2" 2060 | source = "registry+https://github.com/rust-lang/crates.io-index" 2061 | checksum = "44d840b6ec649f480a41c8d80f9c65108b92d89345dd94027bfe06ac444d1060" 2062 | 2063 | [[package]] 2064 | name = "windows_i686_msvc" 2065 | version = "0.48.5" 2066 | source = "registry+https://github.com/rust-lang/crates.io-index" 2067 | checksum = "8f55c233f70c4b27f66c523580f78f1004e8b5a8b659e05a4eb49d4166cca406" 2068 | 2069 | [[package]] 2070 | name = "windows_i686_msvc" 2071 | version = "0.52.6" 2072 | source = "registry+https://github.com/rust-lang/crates.io-index" 2073 | checksum = "240948bc05c5e7c6dabba28bf89d89ffce3e303022809e73deaefe4f6ec56c66" 2074 | 2075 | [[package]] 2076 | name = "windows_x86_64_gnu" 2077 | version = "0.42.2" 2078 | source = "registry+https://github.com/rust-lang/crates.io-index" 2079 | checksum = "8de912b8b8feb55c064867cf047dda097f92d51efad5b491dfb98f6bbb70cb36" 2080 | 2081 | [[package]] 2082 | name = "windows_x86_64_gnu" 2083 | version = "0.48.5" 2084 | source = "registry+https://github.com/rust-lang/crates.io-index" 2085 | checksum = "53d40abd2583d23e4718fddf1ebec84dbff8381c07cae67ff7768bbf19c6718e" 2086 | 2087 | [[package]] 2088 | name = "windows_x86_64_gnu" 2089 | version = "0.52.6" 2090 | source = "registry+https://github.com/rust-lang/crates.io-index" 2091 | checksum = "147a5c80aabfbf0c7d901cb5895d1de30ef2907eb21fbbab29ca94c5b08b1a78" 2092 | 2093 | [[package]] 2094 | name = "windows_x86_64_gnullvm" 2095 | version = "0.42.2" 2096 | source = "registry+https://github.com/rust-lang/crates.io-index" 2097 | checksum = "26d41b46a36d453748aedef1486d5c7a85db22e56aff34643984ea85514e94a3" 2098 | 2099 | [[package]] 2100 | name = "windows_x86_64_gnullvm" 2101 | version = "0.48.5" 2102 | source = "registry+https://github.com/rust-lang/crates.io-index" 2103 | checksum = "0b7b52767868a23d5bab768e390dc5f5c55825b6d30b86c844ff2dc7414044cc" 2104 | 2105 | [[package]] 2106 | name = "windows_x86_64_gnullvm" 2107 | version = "0.52.6" 2108 | source = "registry+https://github.com/rust-lang/crates.io-index" 2109 | checksum = "24d5b23dc417412679681396f2b49f3de8c1473deb516bd34410872eff51ed0d" 2110 | 2111 | [[package]] 2112 | name = "windows_x86_64_msvc" 2113 | version = "0.42.2" 2114 | source = "registry+https://github.com/rust-lang/crates.io-index" 2115 | checksum = "9aec5da331524158c6d1a4ac0ab1541149c0b9505fde06423b02f5ef0106b9f0" 2116 | 2117 | [[package]] 2118 | name = "windows_x86_64_msvc" 2119 | version = "0.48.5" 2120 | source = "registry+https://github.com/rust-lang/crates.io-index" 2121 | checksum = "ed94fce61571a4006852b7389a063ab983c02eb1bb37b47f8272ce92d06d9538" 2122 | 2123 | [[package]] 2124 | name = "windows_x86_64_msvc" 2125 | version = "0.52.6" 2126 | source = "registry+https://github.com/rust-lang/crates.io-index" 2127 | checksum = "589f6da84c646204747d1270a2a5661ea66ed1cced2631d546fdfb155959f9ec" 2128 | 2129 | [[package]] 2130 | name = "winreg" 2131 | version = "0.50.0" 2132 | source = "registry+https://github.com/rust-lang/crates.io-index" 2133 | checksum = "524e57b2c537c0f9b1e69f1965311ec12182b4122e45035b1508cd24d2adadb1" 2134 | dependencies = [ 2135 | "cfg-if", 2136 | "windows-sys 0.48.0", 2137 | ] 2138 | 2139 | [[package]] 2140 | name = "wit-bindgen-rt" 2141 | version = "0.39.0" 2142 | source = "registry+https://github.com/rust-lang/crates.io-index" 2143 | checksum = "6f42320e61fe2cfd34354ecb597f86f413484a798ba44a8ca1165c58d42da6c1" 2144 | dependencies = [ 2145 | "bitflags 2.9.0", 2146 | ] 2147 | 2148 | [[package]] 2149 | name = "write16" 2150 | version = "1.0.0" 2151 | source = "registry+https://github.com/rust-lang/crates.io-index" 2152 | checksum = "d1890f4022759daae28ed4fe62859b1236caebfc61ede2f63ed4e695f3f6d936" 2153 | 2154 | [[package]] 2155 | name = "writeable" 2156 | version = "0.5.5" 2157 | source = "registry+https://github.com/rust-lang/crates.io-index" 2158 | checksum = "1e9df38ee2d2c3c5948ea468a8406ff0db0b29ae1ffde1bcf20ef305bcc95c51" 2159 | 2160 | [[package]] 2161 | name = "yoke" 2162 | version = "0.7.5" 2163 | source = "registry+https://github.com/rust-lang/crates.io-index" 2164 | checksum = "120e6aef9aa629e3d4f52dc8cc43a015c7724194c97dfaf45180d2daf2b77f40" 2165 | dependencies = [ 2166 | "serde", 2167 | "stable_deref_trait", 2168 | "yoke-derive", 2169 | "zerofrom", 2170 | ] 2171 | 2172 | [[package]] 2173 | name = "yoke-derive" 2174 | version = "0.7.5" 2175 | source = "registry+https://github.com/rust-lang/crates.io-index" 2176 | checksum = "2380878cad4ac9aac1e2435f3eb4020e8374b5f13c296cb75b4620ff8e229154" 2177 | dependencies = [ 2178 | "proc-macro2", 2179 | "quote", 2180 | "syn", 2181 | "synstructure", 2182 | ] 2183 | 2184 | [[package]] 2185 | name = "zerofrom" 2186 | version = "0.1.6" 2187 | source = "registry+https://github.com/rust-lang/crates.io-index" 2188 | checksum = "50cc42e0333e05660c3587f3bf9d0478688e15d870fab3346451ce7f8c9fbea5" 2189 | dependencies = [ 2190 | "zerofrom-derive", 2191 | ] 2192 | 2193 | [[package]] 2194 | name = "zerofrom-derive" 2195 | version = "0.1.6" 2196 | source = "registry+https://github.com/rust-lang/crates.io-index" 2197 | checksum = "d71e5d6e06ab090c67b5e44993ec16b72dcbaabc526db883a360057678b48502" 2198 | dependencies = [ 2199 | "proc-macro2", 2200 | "quote", 2201 | "syn", 2202 | "synstructure", 2203 | ] 2204 | 2205 | [[package]] 2206 | name = "zerovec" 2207 | version = "0.10.4" 2208 | source = "registry+https://github.com/rust-lang/crates.io-index" 2209 | checksum = "aa2b893d79df23bfb12d5461018d408ea19dfafe76c2c7ef6d4eba614f8ff079" 2210 | dependencies = [ 2211 | "yoke", 2212 | "zerofrom", 2213 | "zerovec-derive", 2214 | ] 2215 | 2216 | [[package]] 2217 | name = "zerovec-derive" 2218 | version = "0.10.3" 2219 | source = "registry+https://github.com/rust-lang/crates.io-index" 2220 | checksum = "6eafa6dfb17584ea3e2bd6e76e0cc15ad7af12b09abdd1ca55961bed9b1063c6" 2221 | dependencies = [ 2222 | "proc-macro2", 2223 | "quote", 2224 | "syn", 2225 | ] 2226 | -------------------------------------------------------------------------------- /Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "popcorn-cli" 3 | version = "0.1.0" 4 | edition = "2021" 5 | 6 | # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html 7 | 8 | [dependencies] 9 | clap = { version = "4.5.3", features = ["derive"] } 10 | reqwest = { version = "0.11", features = ["json", "multipart"] } 11 | tokio = { version = "1", features = ["full"] } 12 | serde = { version = "1.0", features = ["derive"] } 13 | serde_json = "1.0" 14 | ratatui = "0.26.1" 15 | crossterm = "0.27.0" 16 | anyhow = "1.0" 17 | ctrlc = "3.4.6" 18 | dirs = "5.0" 19 | serde_yaml = "0.9" 20 | webbrowser = "0.8" 21 | base64-url = "3.0.0" 22 | urlencoding = "2.1.3" 23 | bytes = "1.10.1" 24 | futures-util = "0.3.31" 25 | 26 | 27 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2025 GPU MODE 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 | # Popcorn CLI 2 | 3 | A command-line interface tool for submitting solutions to the [Popcorn Discord Bot](https://github.com/gpu-mode/discord-cluster-manager) 4 | Screenshot 2025-06-10 at 11 17 45 AM 5 | 6 | Tested on linux and mac but should just work on Windows as well. 7 | 8 | ## Installation 9 | 10 | ### Option 1: Using pre-built binaries (Recommended) 11 | 12 | 1. Download the latest release for your platform from the releases page 13 | 2. Extract the archive 14 | 3. Move the binary to a location in your PATH 15 | 16 | ### Option 2: Building from source 17 | 18 | 1. Download rust `curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh` 19 | 2. `cd popcorn-cli && ./build.sh` 20 | 21 | ## Authentication 22 | 23 | Since we're effectively giving out GPUs for free we rely on either github or discord authentication to prove that you're a real human before you access our service. 24 | 25 | 1. Go to the [GPU Mode Discord server](https://discord.gg/gpumode) and type in `/get-api-url` 26 | 2. Copy paste that url out `export POPCORN_API_URL="result_of_get_api_url"` 27 | 3. We recommend you authenticate via your Discord as this will guarantee that your name will show up correctly on the leaderboard, you can do this via `popcorn-cli register discord`. However in case this doesn't work for you we also support Github based authentication with `popcorn-cli register github` 28 | 4. To ensure the above worked you can run `cat $HOME/.popcorn.yaml` which should print your client ID which is what will be sent to us on every request 29 | 30 | Sometimes you'll get an error that you're already authenticated despite being unable to submit in which case you can run `popcorn-cli reregister [discord|github]`. 31 | 32 | ## Make your first submission 33 | 34 | ```bash 35 | wget https://raw.githubusercontent.com/gpu-mode/reference-kernels/refs/heads/main/problems/pmpp/grayscale_py/submission.py 36 | popcorn-cli submit --gpu A100 --leaderboard grayscale --mode leaderboard submission.py 37 | ``` 38 | 39 | ## Discover new problems 40 | 41 | The CLI supports (almost) everything Discord does, so you can also discovery which leaderboards are available. To make discovery more pleasant we also offer a TUI experience. 42 | 43 | ```bash 44 | popcorn-cli submit 45 | ``` 46 | 47 | glhf! 48 | -------------------------------------------------------------------------------- /build.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | set -e 3 | 4 | echo "Building Popcorn CLI (Rust version)..." 5 | cargo build --release 6 | 7 | echo "Build complete! Binary is available at: target/release/popcorn-cli" 8 | echo "Run with: ./target/release/popcorn-cli " -------------------------------------------------------------------------------- /docs/AMD_workshop/README.md: -------------------------------------------------------------------------------- 1 | # 🍿 Popcorn CLI - Hackathon Quick Install 2 | 3 | Get started with Popcorn CLI in seconds! Choose your installation method based on your operating system. 4 | 5 | ## 🚀 One-Line Install Commands 6 | 7 | ### For Linux/macOS/Unix: 8 | ```bash 9 | curl -fsSL https://raw.githubusercontent.com/gpu-mode/popcorn-cli/main/install.sh | bash 10 | ``` 11 | 12 | ### For Windows (PowerShell): 13 | ```powershell 14 | powershell -ExecutionPolicy Bypass -Command "iwr -UseBasicParsing https://raw.githubusercontent.com/gpu-mode/popcorn-cli/main/install.ps1 | iex" 15 | ``` 16 | 17 | ## 📋 Quick Start After Installation 18 | 19 | 1. **Restart your terminal** (or run `source ~/.bashrc` / `source ~/.zshrc`) 20 | 21 | 2. **Register with GitHub** (one-time setup): 22 | ```bash 23 | popcorn-cli register github 24 | ``` 25 | 26 | ## 🏃 Run Examples 27 | 28 | Try out the example implementations to get familiar with the system: 29 | 30 | ### For Linux/macOS: 31 | ```bash 32 | # Download and test v1.py (reference implementation) 33 | wget https://raw.githubusercontent.com/gpu-mode/popcorn-cli/main/docs/AMD_workshop/v1.py 34 | popcorn-cli submit --gpu MI300 --leaderboard amd-fp8-mm --mode test v1.py 35 | 36 | # Download and test v2.py (basic optimization) 37 | wget https://raw.githubusercontent.com/gpu-mode/popcorn-cli/main/docs/AMD_workshop/v2.py 38 | popcorn-cli submit --gpu MI300 --leaderboard amd-fp8-mm --mode test v2.py 39 | 40 | # Download and test v3.py (advanced optimization) 41 | wget https://raw.githubusercontent.com/gpu-mode/popcorn-cli/main/docs/AMD_workshop/v3.py 42 | popcorn-cli submit --gpu MI300 --leaderboard amd-fp8-mm --mode test v3.py 43 | ``` 44 | 45 | ### For Windows (PowerShell): 46 | ```powershell 47 | # Download and test v1.py (reference implementation) 48 | Invoke-WebRequest -Uri "https://raw.githubusercontent.com/gpu-mode/popcorn-cli/main/docs/AMD_workshop/v1.py" -OutFile "v1.py" 49 | popcorn-cli submit --gpu MI300 --leaderboard amd-fp8-mm --mode test v1.py 50 | 51 | # Download and test v2.py (basic optimization) 52 | Invoke-WebRequest -Uri "https://raw.githubusercontent.com/gpu-mode/popcorn-cli/main/docs/AMD_workshop/v2.py" -OutFile "v2.py" 53 | popcorn-cli submit --gpu MI300 --leaderboard amd-fp8-mm --mode test v2.py 54 | 55 | # Download and test v3.py (advanced optimization) 56 | Invoke-WebRequest -Uri "https://raw.githubusercontent.com/gpu-mode/popcorn-cli/main/docs/AMD_workshop/v3.py" -OutFile "v3.py" 57 | popcorn-cli submit --gpu MI300 --leaderboard amd-fp8-mm --mode test v3.py 58 | ``` 59 | 60 | ### 💡 Pro Tips: 61 | - Start with **v1.py** (reference implementation) to understand the baseline 62 | - Try **v2.py** for basic optimizations 63 | - Challenge yourself with **v3.py** for advanced Triton optimizations 64 | - Use `--mode benchmark` instead of `--mode test` to see performance metrics 65 | 66 | 67 | ## 🛠️ Manual Installation 68 | 69 | If the scripts don't work, you can manually install: 70 | 71 | 1. Download the binary for your OS from [releases](https://github.com/gpu-mode/popcorn-cli/releases/latest) 72 | 2. Extract the archive 73 | 3. Move the binary to a directory in your PATH 74 | 4. Make it executable (Linux/macOS): `chmod +x popcorn-cli` 75 | 76 | ## 🆘 Troubleshooting 77 | 78 | ### Command not found after installation 79 | - Restart your terminal 80 | - Check if the install directory is in your PATH: 81 | - Linux/macOS: `echo $PATH` 82 | - Windows: `echo $env:PATH` 83 | - Check if POPCORN_API_URL is set to https://discord-cluster-manager-1f6c4782e60a.herokuapp.com 84 | - Linux/macOS: `echo $POPCORN_API_URL` 85 | - Windows: `echo $env:POPCORN_API_URL` 86 | 87 | ## 💡 Need Help? 88 | 89 | - Run `popcorn-cli --help` for usage information 90 | - Check the [main repository](https://github.com/gpu-mode/popcorn-cli) and open an issue 91 | - Join the [GPU Mode Discord](https://discord.gg/gpumode) and ask a question in #amd-competition 92 | 93 | ## 🧑‍🎓 Learn more from our favorite writeups 94 | 95 | * https://github.com/luongthecong123/fp8-quant-matmul 96 | * https://seb-v.github.io/optimization/update/2025/01/20/Fast-GPU-Matrix-multiplication.html 97 | * https://akashkarnatak.github.io/amd-challenge/ 98 | * https://www.bilibili.com/read/cv41954307/?opus_fallback=1 99 | * https://github.com/Snektron/gpumode-amd-fp8-mm -------------------------------------------------------------------------------- /docs/AMD_workshop/v1.py: -------------------------------------------------------------------------------- 1 | #!POPCORN leaderboard amd-fp8-mm 2 | #!POPCORN gpu MI300 3 | 4 | import torch 5 | from task import input_t, output_t 6 | 7 | def custom_kernel(data: input_t) -> output_t: 8 | """ 9 | Reference implementation of block-scale fp8 gemm 10 | Args: 11 | data: Tuple that expands to: 12 | a: torch.Tensor[float8_e4m3fnuz] of shape [m, k], 13 | b: torch.Tensor[float8_e4m3fnuz] of shape [n, k], 14 | a_scale: torch.Tensor[float32] of shape [m, k // 128], 15 | b_scale: torch.Tensor[float32] of shape [n // 128, k // 128], 16 | c: torch.Tensor[bfloat16] of shape [m, n] 17 | Returns: 18 | Tensor containing output in bf16 19 | """ 20 | # c: [m, n] is pre-allocated memory to avoid timing allocation overhead. 21 | a, b, a_scale, b_scale, c = data 22 | 23 | # a is M x K in column-major order, we convert here for simplicity. 24 | a = a.contiguous() 25 | a_scale = a_scale.contiguous() 26 | b_scale = b_scale.contiguous() 27 | 28 | # constants 29 | m = a.shape[0] 30 | n = b.shape[0] 31 | k = a.shape[1] 32 | block_shape_n = 128 33 | block_shape_k = 128 34 | scale_n = b_scale.shape[0] 35 | scale_k = b_scale.shape[1] 36 | 37 | # Apply scaling to input 'a' 38 | a_scale = a_scale.unsqueeze(-1).repeat(1, 1, block_shape_k) # Shape: [m, scale_k, block_shape_k] 39 | a_scale = a_scale.reshape(m, scale_k * block_shape_k) 40 | a_scale = a_scale[:, :k] 41 | 42 | # Dequantize 'a', in your implementation you should do this at the end. 43 | a = a.to(a_scale.dtype) * a_scale 44 | 45 | # Apply scaling to input 'b' 46 | b_scale = ( 47 | b_scale.view(-1, 1) 48 | .repeat(1, block_shape_n * block_shape_k) 49 | .view(scale_n, scale_k, block_shape_n, block_shape_k) 50 | .permute(0, 2, 1, 3) # Reorder dimensions: [scale_n, blk_n, scale_k, blk_k] 51 | .reshape(scale_n * block_shape_n, scale_k * block_shape_k) 52 | ) 53 | b_scale = b_scale[:n, :k] 54 | 55 | # Dequantize 'b', in your implementation you should do this at the end. 56 | b = b.to(b_scale.dtype) * b_scale 57 | 58 | c[...] = (a @ b.T).to(torch.bfloat16) 59 | return c -------------------------------------------------------------------------------- /docs/AMD_workshop/v2.py: -------------------------------------------------------------------------------- 1 | #!POPCORN leaderboard amd-fp8-mm 2 | #!POPCORN gpu MI300 3 | 4 | from task import input_t, output_t 5 | import torch 6 | import triton 7 | import triton.language as tl 8 | 9 | 10 | @triton.jit 11 | def kernel( 12 | A_ptr, 13 | B_ptr, 14 | A_scale_ptr, 15 | B_scale_ptr, 16 | C_ptr, 17 | M: tl.constexpr, 18 | N: tl.constexpr, 19 | K: tl.constexpr, 20 | BLOCK_M: tl.constexpr, 21 | BLOCK_N: tl.constexpr, 22 | BLOCK_K: tl.constexpr, 23 | BLOCK_Q: tl.constexpr = 128, 24 | ): 25 | program_id = tl.program_id(0) 26 | num_pid_across_n = tl.cdiv(N, BLOCK_N) 27 | 28 | program_id_m = program_id // num_pid_across_n 29 | program_id_n = program_id % num_pid_across_n 30 | 31 | # Simple stride assumptions (no transpose) 32 | A_stride_m, A_stride_k = 1, M 33 | B_stride_n, B_stride_k = 1, N 34 | C_stride_m, C_stride_n = N, 1 35 | 36 | # Scale matrices: A is 1x128, B is 128x128 chunks 37 | A_scale_stride_m, A_scale_stride_k = 1, M 38 | B_scale_stride_n, B_scale_stride_k = 1, tl.cdiv(N, BLOCK_Q) 39 | 40 | # Calculate output block position 41 | offset_m = program_id_m * BLOCK_M 42 | offset_n = program_id_n * BLOCK_N 43 | 44 | # Create block offset arrays 45 | block_offsets_m = offset_m + tl.arange(0, BLOCK_M) 46 | block_offsets_n = offset_n + tl.arange(0, BLOCK_N) 47 | block_offsets_k = tl.arange(0, BLOCK_K) 48 | 49 | # Create pointers for A and B blocks 50 | A_block_ptrs = A_ptr + ( 51 | block_offsets_m[:, None] * A_stride_m + block_offsets_k[None, :] * A_stride_k 52 | ) 53 | B_block_ptrs = B_ptr + ( 54 | block_offsets_k[:, None] * B_stride_k + block_offsets_n[None, :] * B_stride_n 55 | ) 56 | 57 | # Scale pointers 58 | A_scale_block_ptrs = A_scale_ptr + (block_offsets_m[:, None] * A_scale_stride_m) 59 | B_scale_block_ptrs = B_scale_ptr + (offset_n // BLOCK_Q) * B_scale_stride_n 60 | 61 | # Main accumulator 62 | master_accumulator = tl.zeros((BLOCK_M, BLOCK_N), dtype=tl.float32) 63 | 64 | # Process K dimension in BLOCK_Q chunks (128 elements at a time) 65 | num_k_iters = K // BLOCK_Q 66 | for _ in range(0, num_k_iters): 67 | # Inner accumulator for current 128-element K chunk 68 | inner_accumulator = tl.zeros((BLOCK_M, BLOCK_N), dtype=tl.float32) 69 | 70 | # Process the 128-element chunk in smaller BLOCK_K pieces 71 | for _ in tl.range(0, BLOCK_Q // BLOCK_K): 72 | A_block = tl.load(A_block_ptrs) # (BLOCK_M, BLOCK_K) 73 | B_block = tl.load(B_block_ptrs) # (BLOCK_K, BLOCK_N) 74 | inner_accumulator = tl.dot(A_block, B_block, inner_accumulator) 75 | 76 | # Move to next K chunk 77 | A_block_ptrs += BLOCK_K * A_stride_k 78 | B_block_ptrs += BLOCK_K * B_stride_k 79 | 80 | # Load scales and apply to inner result 81 | A_scales = tl.load(A_scale_block_ptrs) # (BLOCK_M, 1) 82 | B_scales = tl.load(B_scale_block_ptrs) # scalar 83 | master_accumulator += inner_accumulator * (A_scales * B_scales) 84 | 85 | # Move to next scale block 86 | A_scale_block_ptrs += A_scale_stride_k 87 | B_scale_block_ptrs += B_scale_stride_k 88 | 89 | # Store final result 90 | block_offsets_m = (program_id_m * BLOCK_M + tl.arange(0, BLOCK_M)[:, None]) 91 | block_offsets_n = (program_id_n * BLOCK_N + tl.arange(0, BLOCK_N)[None, :]) 92 | mask = (block_offsets_m < M) & (block_offsets_n < N) 93 | C_block_ptrs = C_ptr + (block_offsets_m * C_stride_m + block_offsets_n * C_stride_n) 94 | tl.store(C_block_ptrs, master_accumulator, mask=mask) 95 | 96 | 97 | def custom_kernel(data: input_t) -> output_t: 98 | A_tensor, B_tensor, A_scale_tensor, B_scale_tensor, C_tensor = data 99 | 100 | M, K = A_tensor.shape 101 | N, _ = B_tensor.shape 102 | 103 | # Fixed, simple configuration - no dynamic tuning 104 | BLOCK_M = 64 105 | BLOCK_N = 64 106 | BLOCK_K = 32 107 | 108 | # Launch grid 109 | num_blocks = triton.cdiv(M, BLOCK_M) * triton.cdiv(N, BLOCK_N) 110 | 111 | kernel[(num_blocks,)]( 112 | A_tensor, 113 | B_tensor, 114 | A_scale_tensor, 115 | B_scale_tensor, 116 | C_tensor, 117 | M, N, K, 118 | BLOCK_M=BLOCK_M, 119 | BLOCK_N=BLOCK_N, 120 | BLOCK_K=BLOCK_K, 121 | num_warps=4, 122 | num_stages=2, 123 | ) 124 | 125 | return C_tensor -------------------------------------------------------------------------------- /docs/AMD_workshop/v3.py: -------------------------------------------------------------------------------- 1 | #!POPCORN leaderboard amd-fp8-mm 2 | #!POPCORN gpu MI300 3 | 4 | from task import input_t, output_t 5 | import torch 6 | import triton 7 | import triton.language as tl 8 | 9 | NUM_SMS = torch.cuda.get_device_properties("cuda").multi_processor_count 10 | 11 | 12 | @triton.jit 13 | def kernel( 14 | A_ptr, 15 | B_ptr, 16 | A_scale_ptr, 17 | B_scale_ptr, 18 | C_ptr, 19 | M: tl.constexpr, 20 | N: tl.constexpr, 21 | K: tl.constexpr, 22 | BLOCK_M: tl.constexpr, 23 | BLOCK_N: tl.constexpr, 24 | BLOCK_K: tl.constexpr, 25 | BLOCK_Q: tl.constexpr = 128, 26 | TRANSPOSE: tl.constexpr = False, 27 | ): 28 | program_id = tl.program_id(0) 29 | num_pid_across_n = tl.cdiv(N, BLOCK_N) 30 | 31 | program_id_m = program_id // num_pid_across_n 32 | program_id_n = program_id % num_pid_across_n 33 | 34 | if not TRANSPOSE: 35 | A_stride_m, A_stride_k = 1, M 36 | B_stride_n, B_stride_k = 1, N 37 | else: 38 | A_stride_m, A_stride_k = K, 1 39 | B_stride_n, B_stride_k = K, 1 40 | C_stride_m, C_stride_n = N, 1 41 | # Scale matrices are stored in column-major order, with A being 1x128 and B being 128x128 chunks 42 | # BLOCK_Q is 128 43 | A_scale_stride_m, A_scale_stride_k = 1, M 44 | B_scale_stride_n, B_scale_stride_k = 1, tl.cdiv(N, BLOCK_Q) 45 | 46 | # Calculate the row and column indices in the output matrix for the current pid 47 | offset_m = program_id_m * BLOCK_M 48 | offset_n = program_id_n * BLOCK_N 49 | 50 | # Arange to make a row and column ptrs 51 | block_offsets_m = offset_m + tl.arange(0, BLOCK_M) 52 | block_offsets_n = offset_n + tl.arange(0, BLOCK_N) 53 | block_offsets_k = tl.arange(0, BLOCK_K) 54 | 55 | # ptrs for BLOCK_M rows of A and BLOCK_N columns of B 56 | A_block_ptrs = A_ptr + ( 57 | block_offsets_m[:, None] * A_stride_m + block_offsets_k[None, :] * A_stride_k 58 | ) 59 | B_block_ptrs = B_ptr + ( 60 | block_offsets_k[:, None] * B_stride_k + block_offsets_n[None, :] * B_stride_n 61 | ) 62 | # since a_scales are 1x128, a_scale_ptrs need to be of shape (BLOCK_M, 1) 63 | # since N, K <= BLOCK_Q, b_scale_ptrs is always a scalar ptr 64 | A_scale_block_ptrs = A_scale_ptr + (block_offsets_m[:, None] * A_scale_stride_m) 65 | B_scale_block_ptrs = B_scale_ptr + (offset_n // BLOCK_Q) * B_scale_stride_n 66 | 67 | # Initialize accumulator for the currrent pid (responsible for BLOCK_M * BLOCK_N elements) 68 | master_accumulator = tl.zeros((BLOCK_M, BLOCK_N), dtype=tl.float32) 69 | 70 | # In each iteration we we load BLOCK_Q elements from K dimension for BLOCK_M rows, resp. BLOCK_N columns 71 | # We choose this to use only 1 scale per iteration 72 | num_k_iters = K // BLOCK_Q 73 | for _ in range(0, num_k_iters): 74 | # Initialize accumulator for the current k iteration 75 | inner_accumulator = tl.zeros((BLOCK_M, BLOCK_N), dtype=tl.float32) 76 | # In each iteration we load BLOCK_K elements from K dimension for BLOCK_M rows, resp. BLOCK_N columns 77 | # We choose this to use small `tl.dot` for the inner accumulator 78 | for _ in tl.range(0, BLOCK_Q // BLOCK_K): 79 | A_block = tl.load(A_block_ptrs) # (BLOCK_M, BLOCK_K) 80 | B_block = tl.load(B_block_ptrs) # (BLOCK_K, BLOCK_N) 81 | inner_accumulator = tl.dot( 82 | A_block, B_block, inner_accumulator 83 | ) # (BLOCK_M, BLOCK_N) 84 | 85 | # Move along the K dimension of A, B 86 | A_block_ptrs += BLOCK_K * A_stride_k 87 | B_block_ptrs += BLOCK_K * B_stride_k 88 | 89 | A_scales = tl.load(A_scale_block_ptrs) # (BLOCK_M, 1) 90 | B_scales = tl.load(B_scale_block_ptrs) # () 91 | master_accumulator += inner_accumulator * (A_scales * B_scales) 92 | 93 | # Move along the K dimension of A, B scales 94 | A_scale_block_ptrs += A_scale_stride_k 95 | B_scale_block_ptrs += B_scale_stride_k 96 | 97 | # Store the result for the current pid 98 | block_offsets_m = ( 99 | program_id_m * BLOCK_M + tl.arange(0, BLOCK_M)[:, None] 100 | ) # (BLOCK_M, 1) 101 | block_offsets_n = ( 102 | program_id_n * BLOCK_N + tl.arange(0, BLOCK_N)[None, :] 103 | ) # (1, BLOCK_N) 104 | mask = (block_offsets_m < M) & (block_offsets_n < N) # (BLOCK_M, BLOCK_N) 105 | C_block_ptrs = C_ptr + (block_offsets_m * C_stride_m + block_offsets_n * C_stride_n) 106 | tl.store(C_block_ptrs, master_accumulator, mask=mask) 107 | 108 | 109 | @torch.compile(dynamic=False, mode="max-autotune-no-cudagraphs") 110 | def contiguous(x): 111 | return x.contiguous() 112 | 113 | 114 | def get_config(M, N, K): 115 | num_blocks_ref = (M // 128) * (N // 128) 116 | TRANSPOSE = False 117 | matrix_instr_nonkdim = 16 118 | BLOCK_M, BLOCK_N, BLOCK_K = (128, 128, 64) 119 | if num_blocks_ref * 8 < NUM_SMS: # 2 and 7 120 | BLOCK_M, BLOCK_N, BLOCK_K = (32, 64, 128) 121 | matrix_instr_nonkdim = 16 122 | elif num_blocks_ref < NUM_SMS: 123 | BLOCK_M, BLOCK_N, BLOCK_K = (64, 64, 64) 124 | 125 | config = dict( 126 | BLOCK_M=BLOCK_M, 127 | BLOCK_N=BLOCK_N, 128 | BLOCK_K=BLOCK_K, 129 | waves_per_eu=2, 130 | matrix_instr_nonkdim=matrix_instr_nonkdim, 131 | num_warps=4, 132 | num_stages=2, 133 | TRANSPOSE=TRANSPOSE, 134 | ) 135 | return config 136 | 137 | 138 | def custom_kernel(data: input_t) -> output_t: 139 | A_tensor, B_tensor, A_scale_tensor, B_scale_tensor, C_tensor = data 140 | 141 | M, K = A_tensor.shape 142 | N, _ = B_tensor.shape 143 | 144 | # heuristic 145 | config = get_config(M, N, K) 146 | 147 | num_blocks = triton.cdiv(M, config["BLOCK_M"]) * triton.cdiv(N, config["BLOCK_N"]) 148 | kernel[(num_blocks,)]( 149 | A_tensor, B_tensor, A_scale_tensor, B_scale_tensor, C_tensor, M, N, K, **config 150 | ) 151 | 152 | return C_tensor 153 | -------------------------------------------------------------------------------- /install.ps1: -------------------------------------------------------------------------------- 1 | # Popcorn CLI Hackathon Installer for Windows 2 | # Run with: powershell -ExecutionPolicy Bypass -File install.ps1 3 | 4 | param( 5 | [switch]$Force = $false 6 | ) 7 | 8 | Write-Host "Installing Popcorn CLI for Hackathon (Windows)..." -ForegroundColor Yellow 9 | 10 | # Check if running as administrator (optional but recommended) 11 | $isAdmin = ([Security.Principal.WindowsPrincipal] [Security.Principal.WindowsIdentity]::GetCurrent()).IsInRole([Security.Principal.WindowsBuiltInRole]::Administrator) 12 | if (-not $isAdmin) { 13 | Write-Host "Not running as administrator. Installation will be user-scoped." -ForegroundColor Yellow 14 | } 15 | 16 | # Set variables 17 | $downloadUrl = "https://github.com/gpu-mode/popcorn-cli/releases/latest/download/popcorn-cli-windows.zip" 18 | $tempDir = "$env:TEMP\popcorn-cli-install" 19 | $installDir = "$env:LOCALAPPDATA\popcorn-cli" 20 | $binaryPath = "$installDir\popcorn-cli.exe" 21 | 22 | # Create directories 23 | try { 24 | if (Test-Path $tempDir) { 25 | Remove-Item $tempDir -Recurse -Force 26 | } 27 | New-Item -ItemType Directory -Path $tempDir -Force | Out-Null 28 | New-Item -ItemType Directory -Path $installDir -Force | Out-Null 29 | Write-Host "Created installation directories" -ForegroundColor Green 30 | } catch { 31 | Write-Host "Failed to create directories: $_" -ForegroundColor Red 32 | exit 1 33 | } 34 | 35 | # Download the binary 36 | Write-Host "Downloading from: $downloadUrl" -ForegroundColor Cyan 37 | try { 38 | $zipPath = "$tempDir\popcorn-cli-windows.zip" 39 | Invoke-WebRequest -Uri $downloadUrl -OutFile $zipPath -UseBasicParsing 40 | Write-Host "Download completed" -ForegroundColor Green 41 | } catch { 42 | Write-Host "Download failed: $_" -ForegroundColor Red 43 | exit 1 44 | } 45 | 46 | # Extract the binary 47 | Write-Host "Extracting binary..." -ForegroundColor Cyan 48 | try { 49 | Expand-Archive -Path $zipPath -DestinationPath $tempDir -Force 50 | 51 | # Find the binary (it might be in a subdirectory) 52 | $binarySource = Get-ChildItem -Path $tempDir -Name "popcorn-cli.exe" -Recurse | Select-Object -First 1 53 | if ($binarySource) { 54 | $fullBinaryPath = Join-Path $tempDir $binarySource 55 | Copy-Item $fullBinaryPath $binaryPath -Force 56 | Write-Host "Binary extracted and copied" -ForegroundColor Green 57 | } else { 58 | Write-Host "popcorn-cli.exe not found in archive" -ForegroundColor Red 59 | exit 1 60 | } 61 | } catch { 62 | Write-Host "Extraction failed: $_" -ForegroundColor Red 63 | exit 1 64 | } 65 | 66 | # Add to PATH 67 | Write-Host "Adding to PATH..." -ForegroundColor Cyan 68 | try { 69 | $userPath = [Environment]::GetEnvironmentVariable("PATH", "User") 70 | if ($userPath -notlike "*$installDir*") { 71 | $newPath = "$installDir;$userPath" 72 | [Environment]::SetEnvironmentVariable("PATH", $newPath, "User") 73 | Write-Host "Added $installDir to user PATH" -ForegroundColor Green 74 | Write-Host "Please restart your terminal or PowerShell session" -ForegroundColor Yellow 75 | } else { 76 | Write-Host "$installDir already in PATH" -ForegroundColor Green 77 | } 78 | 79 | # Also add to current session 80 | $env:PATH = "$installDir;$env:PATH" 81 | } catch { 82 | Write-Host "Could not modify PATH automatically: $_" -ForegroundColor Yellow 83 | Write-Host "Please manually add $installDir to your PATH" -ForegroundColor Yellow 84 | } 85 | 86 | # Cleanup 87 | Remove-Item $tempDir -Recurse -Force -ErrorAction SilentlyContinue 88 | 89 | # Test installation 90 | Write-Host "Testing installation..." -ForegroundColor Cyan 91 | try { 92 | $version = & $binaryPath --version 2>$null 93 | if ($LASTEXITCODE -eq 0) { 94 | Write-Host "Installation successful!" -ForegroundColor Green 95 | } else { 96 | Write-Host "Binary installed but may not be working correctly" -ForegroundColor Yellow 97 | } 98 | } catch { 99 | Write-Host "Could not test binary: $_" -ForegroundColor Yellow 100 | } 101 | 102 | Write-Host "" 103 | Write-Host "Popcorn CLI installed and ready for hackathon!" -ForegroundColor Green 104 | Write-Host "" 105 | Write-Host "Quick Start:" -ForegroundColor Cyan 106 | Write-Host " 1. Restart your terminal/PowerShell" -ForegroundColor White 107 | Write-Host " 2. Register with GitHub: popcorn-cli register github" -ForegroundColor White 108 | Write-Host " 3. Submit your solution: popcorn-cli submit --gpu MI300 --leaderboard amd-fp8-mm --mode test " -ForegroundColor White 109 | Write-Host "" 110 | Write-Host "Hackathon mode features:" -ForegroundColor Cyan 111 | Write-Host " - API URL pre-configured" -ForegroundColor White 112 | Write-Host " - GitHub authentication (no Discord setup needed)" -ForegroundColor White 113 | Write-Host " - All modes available: test, benchmark, leaderboard, profile" -ForegroundColor White 114 | Write-Host " - Clean user identification" -ForegroundColor White 115 | Write-Host "" 116 | Write-Host "Need help? Run: popcorn-cli --help" -ForegroundColor White 117 | Write-Host "Example: popcorn-cli submit --gpu MI300 --leaderboard amd-fp8-mm --mode test example.py" -ForegroundColor White 118 | Write-Host "" 119 | Write-Host "Installation location: $installDir" -ForegroundColor Gray -------------------------------------------------------------------------------- /install.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | set -e 4 | 5 | # Popcorn CLI Hackathon Installer (Unix/Linux/macOS) 6 | # For Windows users: Use install.ps1 instead 7 | echo "🍿 Installing Popcorn CLI for Hackathon (Unix/Linux/macOS)..." 8 | 9 | # Check if we're on Windows 10 | if [[ "$OSTYPE" == "msys" ]] || [[ "$OSTYPE" == "win32" ]] || [[ "$OSTYPE" == "cygwin" ]]; then 11 | echo "⚠️ Detected Windows environment" 12 | echo "For native Windows, please use install.ps1 instead:" 13 | echo " powershell -ExecutionPolicy Bypass -File install.ps1" 14 | echo "" 15 | echo "This script will continue assuming you're in a Unix-like environment (WSL/Git Bash/MSYS2)" 16 | read -p "Continue? (y/N): " -n 1 -r 17 | echo 18 | if [[ ! $REPLY =~ ^[Yy]$ ]]; then 19 | exit 0 20 | fi 21 | fi 22 | 23 | # Detect OS 24 | OS="" 25 | ARCH="" 26 | BINARY_NAME="" 27 | EXTENSION="" 28 | 29 | if [[ "$OSTYPE" == "linux-gnu"* ]]; then 30 | OS="linux" 31 | EXTENSION=".tar.gz" 32 | BINARY_NAME="popcorn-cli" 33 | elif [[ "$OSTYPE" == "darwin"* ]]; then 34 | OS="macos" 35 | EXTENSION=".tar.gz" 36 | BINARY_NAME="popcorn-cli" 37 | elif [[ "$OSTYPE" == "msys" ]] || [[ "$OSTYPE" == "win32" ]] || [[ "$OSTYPE" == "cygwin" ]]; then 38 | OS="windows" 39 | EXTENSION=".zip" 40 | BINARY_NAME="popcorn-cli.exe" 41 | else 42 | echo "❌ Unsupported operating system: $OSTYPE" 43 | exit 1 44 | fi 45 | 46 | echo "✅ Detected OS: $OS" 47 | 48 | # Download URL 49 | DOWNLOAD_URL="https://github.com/gpu-mode/popcorn-cli/releases/latest/download/popcorn-cli-${OS}${EXTENSION}" 50 | TEMP_DIR="/tmp/popcorn-cli-install" 51 | INSTALL_DIR="$HOME/.local/bin" 52 | 53 | # Create directories 54 | mkdir -p "$TEMP_DIR" 55 | mkdir -p "$INSTALL_DIR" 56 | 57 | echo "📥 Downloading from: $DOWNLOAD_URL" 58 | 59 | # Download the binary 60 | if command -v curl >/dev/null 2>&1; then 61 | curl -L -o "$TEMP_DIR/popcorn-cli${EXTENSION}" "$DOWNLOAD_URL" 62 | elif command -v wget >/dev/null 2>&1; then 63 | wget -O "$TEMP_DIR/popcorn-cli${EXTENSION}" "$DOWNLOAD_URL" 64 | else 65 | echo "❌ Neither curl nor wget found. Please install one of them." 66 | exit 1 67 | fi 68 | 69 | echo "📦 Extracting binary..." 70 | 71 | # Extract the binary 72 | cd "$TEMP_DIR" 73 | if [[ "$EXTENSION" == ".tar.gz" ]]; then 74 | tar -xzf "popcorn-cli${EXTENSION}" 75 | elif [[ "$EXTENSION" == ".zip" ]]; then 76 | unzip "popcorn-cli${EXTENSION}" 77 | fi 78 | 79 | # Find and move the binary 80 | if [[ -f "$BINARY_NAME" ]]; then 81 | chmod +x "$BINARY_NAME" 82 | mv "$BINARY_NAME" "$INSTALL_DIR/" 83 | echo "✅ Binary installed to $INSTALL_DIR/$BINARY_NAME" 84 | else 85 | echo "❌ Binary not found after extraction" 86 | exit 1 87 | fi 88 | 89 | # Add to PATH 90 | SHELL_RC="" 91 | if [[ -n "$ZSH_VERSION" ]]; then 92 | SHELL_RC="$HOME/.zshrc" 93 | elif [[ -n "$BASH_VERSION" ]]; then 94 | SHELL_RC="$HOME/.bashrc" 95 | else 96 | # Try to detect shell 97 | case "$SHELL" in 98 | */zsh) 99 | SHELL_RC="$HOME/.zshrc" 100 | ;; 101 | */bash) 102 | SHELL_RC="$HOME/.bashrc" 103 | ;; 104 | *) 105 | SHELL_RC="$HOME/.profile" 106 | ;; 107 | esac 108 | fi 109 | 110 | # Check if PATH already contains the directory 111 | if [[ ":$PATH:" != *":$INSTALL_DIR:"* ]]; then 112 | echo "🔧 Adding $INSTALL_DIR to PATH in $SHELL_RC" 113 | echo "" >> "$SHELL_RC" 114 | echo "# Added by Popcorn CLI installer" >> "$SHELL_RC" 115 | echo "export PATH=\"$INSTALL_DIR:\$PATH\"" >> "$SHELL_RC" 116 | export PATH="$INSTALL_DIR:$PATH" 117 | else 118 | echo "✅ $INSTALL_DIR already in PATH" 119 | fi 120 | 121 | # Cleanup 122 | rm -rf "$TEMP_DIR" 123 | 124 | echo "" 125 | echo "🎉 Popcorn CLI installed and ready for hackathon!" 126 | echo "" 127 | echo "📋 Quick Start:" 128 | echo " 1. Restart your terminal or run: source $SHELL_RC" 129 | echo " 2. Register with GitHub: popcorn-cli register github" 130 | echo " 3. Submit your solution: popcorn-cli submit --gpu MI300 --leaderboard amd-fp8-mm --mode test " 131 | echo "" 132 | echo "🚀 Hackathon mode features:" 133 | echo " - ✅ API URL pre-configured" 134 | echo " - ✅ GitHub authentication (no Discord setup needed)" 135 | echo " - ✅ All modes available: test, benchmark, leaderboard, profile" 136 | echo " - ✅ Clean user identification" 137 | echo "" 138 | echo "💡 Need help? Run: popcorn-cli --help" 139 | echo "🔗 Example: popcorn-cli submit --gpu MI300 --leaderboard amd-fp8-mm --mode test example.py" -------------------------------------------------------------------------------- /src/cmd/auth.rs: -------------------------------------------------------------------------------- 1 | use anyhow::{anyhow, Result}; 2 | use base64_url; 3 | use dirs; 4 | use serde::{Deserialize, Serialize}; 5 | use serde_yaml; 6 | use std::fs::{File, OpenOptions}; 7 | use std::path::PathBuf; 8 | use webbrowser; 9 | 10 | use crate::service; // Assuming service::create_client is needed 11 | 12 | // Configuration structure 13 | #[derive(Serialize, Deserialize, Debug, Default)] 14 | struct Config { 15 | cli_id: Option, 16 | } 17 | 18 | // Helper function to get the config file path 19 | fn get_config_path() -> Result { 20 | dirs::home_dir() 21 | .map(|mut path| { 22 | path.push(".popcorn.yaml"); 23 | path 24 | }) 25 | .ok_or_else(|| anyhow!("Could not find home directory")) 26 | } 27 | 28 | // Helper function to load config 29 | fn load_config() -> Result { 30 | let path = get_config_path()?; 31 | if !path.exists() { 32 | return Ok(Config::default()); 33 | } 34 | let file = File::open(path)?; 35 | serde_yaml::from_reader(file).map_err(|e| anyhow!("Failed to parse config file: {}", e)) 36 | } 37 | 38 | // Helper function to save config 39 | fn save_config(config: &Config) -> Result<()> { 40 | let path = get_config_path()?; 41 | let file = OpenOptions::new() 42 | .write(true) 43 | .create(true) 44 | .truncate(true) // Overwrite existing file 45 | .open(path)?; 46 | serde_yaml::to_writer(file, config).map_err(|e| anyhow!("Failed to write config file: {}", e)) 47 | } 48 | 49 | // Structure for the API response 50 | #[derive(Deserialize)] 51 | struct AuthInitResponse { 52 | state: String, // This is the cli_id 53 | } 54 | 55 | // Function to handle the login logic 56 | pub async fn run_auth(reset: bool, auth_provider: &str) -> Result<()> { 57 | println!("Attempting authentication via {}...", auth_provider); 58 | 59 | let popcorn_api_url = std::env::var("POPCORN_API_URL") 60 | .map_err(|_| anyhow!("POPCORN_API_URL environment variable not set"))?; 61 | 62 | let client = service::create_client(None)?; 63 | 64 | let init_url = format!("{}/auth/init?provider={}", popcorn_api_url, auth_provider); 65 | println!("Requesting CLI ID from {}", init_url); 66 | 67 | let init_resp = client.get(&init_url).send().await?; 68 | 69 | let status = init_resp.status(); 70 | 71 | if !status.is_success() { 72 | let error_text = init_resp.text().await?; 73 | return Err(anyhow!( 74 | "Failed to initialize auth ({}): {}", 75 | status, 76 | error_text 77 | )); 78 | } 79 | 80 | let auth_init_data: AuthInitResponse = init_resp.json().await?; 81 | let cli_id = auth_init_data.state; 82 | println!("Received CLI ID: {}", cli_id); 83 | 84 | let state_json = serde_json::json!({ 85 | "cli_id": cli_id, 86 | "is_reset": reset 87 | }) 88 | .to_string(); 89 | let state_b64 = base64_url::encode(&state_json); 90 | 91 | let auth_url = match auth_provider { 92 | "discord" => { 93 | let base_auth_url = "https://discord.com/oauth2/authorize?client_id=1361364685491802243&response_type=code&redirect_uri=https%3A%2F%2Fdiscord-cluster-manager-1f6c4782e60a.herokuapp.com%2Fauth%2Fcli%2Fdiscord&scope=identify"; 94 | format!("{}&state={}", base_auth_url, state_b64) 95 | } 96 | "github" => { 97 | let client_id = "Ov23lieFd2onYk4OnKIR"; 98 | let redirect_uri = 99 | "https://discord-cluster-manager-1f6c4782e60a.herokuapp.com/auth/cli/github"; 100 | let encoded_redirect_uri = urlencoding::encode(redirect_uri); 101 | format!( 102 | "https://github.com/login/oauth/authorize?client_id={}&state={}&redirect_uri={}", 103 | client_id, state_b64, encoded_redirect_uri 104 | ) 105 | } 106 | _ => { 107 | return Err(anyhow!( 108 | "Unsupported authentication provider: {}", 109 | auth_provider 110 | )) 111 | } 112 | }; 113 | 114 | println!( 115 | "\n>>> Please open the following URL in your browser to log in via {}:", 116 | auth_provider 117 | ); 118 | println!("{}", auth_url); 119 | println!("\nWaiting for you to complete the authentication in your browser..."); 120 | println!( 121 | "After successful authentication with {}, the CLI ID will be saved.", 122 | auth_provider 123 | ); 124 | 125 | if webbrowser::open(&auth_url).is_err() { 126 | println!( 127 | "Could not automatically open the browser. Please copy the URL above and paste it manually." 128 | ); 129 | } 130 | 131 | // Save the cli_id to config file optimistically 132 | let mut config = load_config().unwrap_or_default(); 133 | config.cli_id = Some(cli_id.clone()); 134 | save_config(&config)?; 135 | 136 | println!( 137 | "\nSuccessfully initiated authentication. Your CLI ID ({}) has been saved to {}. To use the CLI on different machines, you can copy the config file.", 138 | cli_id, 139 | get_config_path()?.display() 140 | ); 141 | println!("You can now use other commands that require authentication."); 142 | 143 | Ok(()) 144 | } 145 | -------------------------------------------------------------------------------- /src/cmd/mod.rs: -------------------------------------------------------------------------------- 1 | use anyhow::{anyhow, Result}; 2 | use clap::{Parser, Subcommand}; 3 | use dirs; 4 | use serde::{Deserialize, Serialize}; 5 | use serde_yaml; 6 | use std::fs::File; 7 | use std::path::PathBuf; 8 | 9 | mod auth; 10 | mod submit; 11 | 12 | #[derive(Serialize, Deserialize, Debug, Default)] 13 | struct Config { 14 | cli_id: Option, 15 | } 16 | 17 | fn get_config_path() -> Result { 18 | dirs::home_dir() 19 | .map(|mut path| { 20 | path.push(".popcorn.yaml"); 21 | path 22 | }) 23 | .ok_or_else(|| anyhow!("Could not find home directory")) 24 | } 25 | 26 | fn load_config() -> Result { 27 | let path = get_config_path()?; 28 | if !path.exists() { 29 | return Err(anyhow!( 30 | "Config file not found at {}. Please run `popcorn register` first.", 31 | path.display() 32 | )); 33 | } 34 | let file = File::open(path)?; 35 | serde_yaml::from_reader(file).map_err(|e| anyhow!("Failed to parse config file: {}", e)) 36 | } 37 | 38 | #[derive(Parser, Debug)] 39 | #[command(author, version, about, long_about = None)] 40 | pub struct Cli { 41 | #[command(subcommand)] 42 | command: Option, 43 | 44 | /// Optional: Path to the solution file 45 | filepath: Option, 46 | 47 | /// Optional: Directly specify the GPU to use (e.g., "mi300") 48 | #[arg(long)] 49 | pub gpu: Option, 50 | 51 | /// Optional: Directly specify the leaderboard (e.g., "fp8") 52 | #[arg(long)] 53 | pub leaderboard: Option, 54 | 55 | /// Optional: Specify submission mode (test, benchmark, leaderboard, profile) 56 | #[arg(long)] 57 | pub mode: Option, 58 | } 59 | 60 | #[derive(Subcommand, Debug)] 61 | enum AuthProvider { 62 | Discord, 63 | Github, 64 | } 65 | 66 | #[derive(Subcommand, Debug)] 67 | enum Commands { 68 | Reregister { 69 | #[command(subcommand)] 70 | provider: AuthProvider, 71 | }, 72 | Register { 73 | #[command(subcommand)] 74 | provider: AuthProvider, 75 | }, 76 | Submit { 77 | /// Optional: Path to the solution file (can also be provided as a top-level argument) 78 | filepath: Option, 79 | 80 | /// Optional: Directly specify the GPU to use (e.g., "MI300") 81 | #[arg(long)] 82 | gpu: Option, 83 | 84 | /// Optional: Directly specify the leaderboard (e.g., "amd-fp8-mm") 85 | #[arg(long)] 86 | leaderboard: Option, 87 | 88 | /// Optional: Specify submission mode (test, benchmark, leaderboard, profile) 89 | #[arg(long)] 90 | mode: Option, 91 | }, 92 | } 93 | 94 | pub async fn execute(cli: Cli) -> Result<()> { 95 | match cli.command { 96 | Some(Commands::Reregister { provider }) => { 97 | let provider_str = match provider { 98 | AuthProvider::Discord => "discord", 99 | AuthProvider::Github => "github", 100 | }; 101 | auth::run_auth(true, provider_str).await 102 | } 103 | Some(Commands::Register { provider }) => { 104 | let provider_str = match provider { 105 | AuthProvider::Discord => "discord", 106 | AuthProvider::Github => "github", 107 | }; 108 | auth::run_auth(false, provider_str).await 109 | } 110 | Some(Commands::Submit { filepath, gpu, leaderboard, mode }) => { 111 | let config = load_config()?; 112 | let cli_id = config.cli_id.ok_or_else(|| { 113 | anyhow!( 114 | "cli_id not found in config file ({}). Please run 'popcorn-cli register' first.", 115 | get_config_path() 116 | .map_or_else(|_| "unknown path".to_string(), |p| p.display().to_string()) 117 | ) 118 | })?; 119 | 120 | // Use filepath from Submit command first, fallback to top-level filepath 121 | let final_filepath = filepath.or(cli.filepath); 122 | submit::run_submit_tui( 123 | final_filepath, // Resolved filepath 124 | gpu, // From Submit command 125 | leaderboard, // From Submit command 126 | mode, // From Submit command 127 | cli_id, 128 | ) 129 | .await 130 | } 131 | None => { 132 | // Check if any of the submission-related flags were used at the top level 133 | if cli.gpu.is_some() || cli.leaderboard.is_some() || cli.mode.is_some() { 134 | return Err(anyhow!( 135 | "Please use the 'submit' subcommand when specifying submission options:\n\ 136 | popcorn-cli submit [--gpu GPU] [--leaderboard LEADERBOARD] [--mode MODE] FILEPATH" 137 | )); 138 | } 139 | 140 | // Handle the case where only a filepath is provided (for backward compatibility) 141 | if let Some(top_level_filepath) = cli.filepath { 142 | let config = load_config()?; 143 | let cli_id = config.cli_id.ok_or_else(|| { 144 | anyhow!( 145 | "cli_id not found in config file ({}). Please run `popcorn register` first.", 146 | get_config_path() 147 | .map_or_else(|_| "unknown path".to_string(), |p| p.display().to_string()) 148 | ) 149 | })?; 150 | 151 | // Run TUI with only filepath, no other options 152 | submit::run_submit_tui( 153 | Some(top_level_filepath), 154 | None, // No GPU option 155 | None, // No leaderboard option 156 | None, // No mode option 157 | cli_id, 158 | ) 159 | .await 160 | } else { 161 | Err(anyhow!("No command or submission file specified. Use --help for usage.")) 162 | } 163 | } 164 | } 165 | } 166 | -------------------------------------------------------------------------------- /src/cmd/submit.rs: -------------------------------------------------------------------------------- 1 | use std::fs::File; 2 | use std::io::{self, Read}; 3 | use std::path::Path; 4 | 5 | use anyhow::{anyhow, Result}; 6 | use crossterm::event::{self, Event, KeyCode, KeyEvent, KeyEventKind, KeyModifiers}; 7 | use crossterm::terminal::{disable_raw_mode, enable_raw_mode, EnterAlternateScreen}; 8 | use ratatui::prelude::*; 9 | use ratatui::style::{Color, Style, Stylize}; 10 | use ratatui::text::{Line, Span}; 11 | use ratatui::widgets::{Block, Borders, List, ListItem, ListState}; 12 | use tokio::task::JoinHandle; 13 | 14 | use crate::models::{AppState, GpuItem, LeaderboardItem, SubmissionModeItem}; 15 | use crate::service; 16 | use crate::utils; 17 | use crate::views::loading_page::{LoadingPage, LoadingPageState}; 18 | use crate::views::result_page::{ResultPage, ResultPageState}; 19 | 20 | #[derive(Default, Debug)] 21 | pub struct App { 22 | pub filepath: String, 23 | pub cli_id: String, 24 | 25 | pub leaderboards: Vec, 26 | pub leaderboards_state: ListState, 27 | pub selected_leaderboard: Option, 28 | 29 | pub gpus: Vec, 30 | pub gpus_state: ListState, 31 | pub selected_gpu: Option, 32 | 33 | pub submission_modes: Vec, 34 | pub submission_modes_state: ListState, 35 | pub selected_submission_mode: Option, 36 | 37 | pub app_state: AppState, 38 | pub final_status: Option, 39 | 40 | pub should_quit: bool, 41 | pub submission_task: Option>>, 42 | pub leaderboards_task: Option, anyhow::Error>>>, 43 | pub gpus_task: Option, anyhow::Error>>>, 44 | 45 | pub loading_page_state: LoadingPageState, 46 | 47 | pub result_page_state: ResultPageState, 48 | } 49 | 50 | impl App { 51 | pub fn new>(filepath: P, cli_id: String) -> Self { 52 | let submission_modes = vec![ 53 | SubmissionModeItem::new( 54 | "Test".to_string(), 55 | "Test the solution and give detailed results about passed/failed tests.".to_string(), 56 | "test".to_string(), 57 | ), 58 | SubmissionModeItem::new( 59 | "Benchmark".to_string(), 60 | "Benchmark the solution, this also runs the tests and afterwards runs the benchmark, returning detailed timing results".to_string(), 61 | "benchmark".to_string(), 62 | ), 63 | SubmissionModeItem::new( 64 | "Leaderboard".to_string(), 65 | "Submit to the leaderboard, this first runs public tests and then private tests. If both pass, the submission is evaluated and submit to the leaderboard.".to_string(), 66 | "leaderboard".to_string(), 67 | ), 68 | SubmissionModeItem::new( 69 | "Profile".to_string(), 70 | "Work in progress...".to_string(), 71 | "profile".to_string(), 72 | ), 73 | ]; 74 | 75 | let mut app = Self { 76 | filepath: filepath.as_ref().to_string_lossy().to_string(), 77 | cli_id, 78 | submission_modes, 79 | selected_submission_mode: None, 80 | ..Default::default() 81 | }; 82 | 83 | app.leaderboards_state.select(Some(0)); 84 | app.gpus_state.select(Some(0)); 85 | app.submission_modes_state.select(Some(0)); 86 | app 87 | } 88 | 89 | pub fn update_loading_page_state(&mut self, terminal_width: u16) { 90 | if self.app_state != AppState::WaitingForResult { 91 | return; 92 | } 93 | 94 | let st = &mut self.loading_page_state; 95 | st.progress_column = { 96 | if st.progress_column < terminal_width { 97 | st.progress_column + 1 98 | } else { 99 | st.loop_count += 1; 100 | 0 101 | } 102 | }; 103 | st.progress_bar = f64::from(st.progress_column) * 100.0 / f64::from(terminal_width); 104 | } 105 | 106 | pub fn initialize_with_directives(&mut self, popcorn_directives: utils::PopcornDirectives) { 107 | if !popcorn_directives.leaderboard_name.is_empty() { 108 | self.selected_leaderboard = Some(popcorn_directives.leaderboard_name); 109 | 110 | if !popcorn_directives.gpus.is_empty() { 111 | self.selected_gpu = Some(popcorn_directives.gpus[0].clone()); 112 | self.app_state = AppState::SubmissionModeSelection; 113 | } else { 114 | self.app_state = AppState::GpuSelection; 115 | } 116 | } else if !popcorn_directives.gpus.is_empty() { 117 | self.selected_gpu = Some(popcorn_directives.gpus[0].clone()); 118 | if !popcorn_directives.leaderboard_name.is_empty() { 119 | self.selected_leaderboard = Some(popcorn_directives.leaderboard_name); 120 | self.app_state = AppState::SubmissionModeSelection; 121 | } else { 122 | self.app_state = AppState::LeaderboardSelection; 123 | } 124 | } else { 125 | self.app_state = AppState::LeaderboardSelection; 126 | } 127 | } 128 | 129 | pub fn handle_key_event(&mut self, key: KeyEvent) -> Result { 130 | // Allow quitting anytime, even while loading 131 | if key.code == KeyCode::Char('c') && key.modifiers.contains(KeyModifiers::CONTROL) { 132 | self.should_quit = true; 133 | return Ok(true); 134 | } 135 | 136 | match key.code { 137 | KeyCode::Char('q') => { 138 | self.should_quit = true; 139 | return Ok(true); 140 | } 141 | KeyCode::Enter => match self.app_state { 142 | AppState::LeaderboardSelection => { 143 | if let Some(idx) = self.leaderboards_state.selected() { 144 | if idx < self.leaderboards.len() { 145 | self.selected_leaderboard = 146 | Some(self.leaderboards[idx].title_text.clone()); 147 | 148 | if self.selected_gpu.is_none() { 149 | self.app_state = AppState::GpuSelection; 150 | if let Err(e) = self.spawn_load_gpus() { 151 | self.set_error_and_quit(format!( 152 | "Error starting GPU fetch: {}", 153 | e 154 | )); 155 | } 156 | } else { 157 | self.app_state = AppState::SubmissionModeSelection; 158 | } 159 | return Ok(true); 160 | } 161 | } 162 | } 163 | AppState::GpuSelection => { 164 | if let Some(idx) = self.gpus_state.selected() { 165 | if idx < self.gpus.len() { 166 | self.selected_gpu = Some(self.gpus[idx].title_text.clone()); 167 | self.app_state = AppState::SubmissionModeSelection; 168 | return Ok(true); 169 | } 170 | } 171 | } 172 | AppState::SubmissionModeSelection => { 173 | if let Some(idx) = self.submission_modes_state.selected() { 174 | if idx < self.submission_modes.len() { 175 | self.selected_submission_mode = 176 | Some(self.submission_modes[idx].value.clone()); 177 | self.app_state = AppState::WaitingForResult; 178 | if let Err(e) = self.spawn_submit_solution() { 179 | self.set_error_and_quit(format!( 180 | "Error starting submission: {}", 181 | e 182 | )); 183 | } 184 | return Ok(true); 185 | } 186 | } 187 | } 188 | _ => {} 189 | }, 190 | KeyCode::Up => { 191 | self.move_selection_up(); 192 | return Ok(true); 193 | } 194 | KeyCode::Down => { 195 | self.move_selection_down(); 196 | return Ok(true); 197 | } 198 | _ => {} 199 | } 200 | Ok(false) 201 | } 202 | 203 | fn set_error_and_quit(&mut self, error_message: String) { 204 | self.final_status = Some(error_message); 205 | self.should_quit = true; 206 | } 207 | 208 | fn move_selection_up(&mut self) { 209 | match self.app_state { 210 | AppState::LeaderboardSelection => { 211 | if let Some(idx) = self.leaderboards_state.selected() { 212 | if idx > 0 { 213 | self.leaderboards_state.select(Some(idx - 1)); 214 | } 215 | } 216 | } 217 | AppState::GpuSelection => { 218 | if let Some(idx) = self.gpus_state.selected() { 219 | if idx > 0 { 220 | self.gpus_state.select(Some(idx - 1)); 221 | } 222 | } 223 | } 224 | AppState::SubmissionModeSelection => { 225 | if let Some(idx) = self.submission_modes_state.selected() { 226 | if idx > 0 { 227 | self.submission_modes_state.select(Some(idx - 1)); 228 | } 229 | } 230 | } 231 | _ => {} 232 | } 233 | } 234 | 235 | fn move_selection_down(&mut self) { 236 | match self.app_state { 237 | AppState::LeaderboardSelection => { 238 | if let Some(idx) = self.leaderboards_state.selected() { 239 | if idx < self.leaderboards.len().saturating_sub(1) { 240 | self.leaderboards_state.select(Some(idx + 1)); 241 | } 242 | } 243 | } 244 | AppState::GpuSelection => { 245 | if let Some(idx) = self.gpus_state.selected() { 246 | if idx < self.gpus.len().saturating_sub(1) { 247 | self.gpus_state.select(Some(idx + 1)); 248 | } 249 | } 250 | } 251 | AppState::SubmissionModeSelection => { 252 | if let Some(idx) = self.submission_modes_state.selected() { 253 | if idx < self.submission_modes.len().saturating_sub(1) { 254 | self.submission_modes_state.select(Some(idx + 1)); 255 | } 256 | } 257 | } 258 | _ => {} 259 | } 260 | } 261 | 262 | pub fn spawn_load_leaderboards(&mut self) -> Result<()> { 263 | let client = service::create_client(Some(self.cli_id.clone()))?; 264 | self.leaderboards_task = Some(tokio::spawn(async move { 265 | service::fetch_leaderboards(&client).await 266 | })); 267 | Ok(()) 268 | } 269 | 270 | pub fn spawn_load_gpus(&mut self) -> Result<()> { 271 | let client = service::create_client(Some(self.cli_id.clone()))?; 272 | let leaderboard_name = self 273 | .selected_leaderboard 274 | .clone() 275 | .ok_or_else(|| anyhow!("Leaderboard not selected"))?; 276 | self.gpus_task = Some(tokio::spawn(async move { 277 | service::fetch_gpus(&client, &leaderboard_name).await 278 | })); 279 | Ok(()) 280 | } 281 | 282 | pub fn spawn_submit_solution(&mut self) -> Result<()> { 283 | let client = service::create_client(Some(self.cli_id.clone()))?; 284 | let filepath = self.filepath.clone(); 285 | let leaderboard = self 286 | .selected_leaderboard 287 | .clone() 288 | .ok_or_else(|| anyhow!("Leaderboard not selected"))?; 289 | let gpu = self 290 | .selected_gpu 291 | .clone() 292 | .ok_or_else(|| anyhow!("GPU not selected"))?; 293 | let mode = self 294 | .selected_submission_mode 295 | .clone() 296 | .ok_or_else(|| anyhow!("Submission mode not selected"))?; 297 | 298 | // Read file content 299 | let mut file = File::open(&filepath)?; 300 | let mut file_content = String::new(); 301 | file.read_to_string(&mut file_content)?; 302 | 303 | self.submission_task = Some(tokio::spawn(async move { 304 | service::submit_solution(&client, &filepath, &file_content, &leaderboard, &gpu, &mode) 305 | .await 306 | })); 307 | Ok(()) 308 | } 309 | 310 | pub async fn check_leaderboard_task(&mut self) { 311 | if let Some(handle) = &mut self.leaderboards_task { 312 | if handle.is_finished() { 313 | let task = self.leaderboards_task.take().unwrap(); 314 | match task.await { 315 | Ok(Ok(leaderboards)) => { 316 | self.leaderboards = leaderboards; 317 | if let Some(selected_name) = &self.selected_leaderboard { 318 | if let Some(index) = self 319 | .leaderboards 320 | .iter() 321 | .position(|lb| &lb.title_text == selected_name) 322 | { 323 | self.leaderboards_state.select(Some(index)); 324 | if self.selected_gpu.is_some() { 325 | self.app_state = AppState::SubmissionModeSelection; 326 | } else { 327 | self.app_state = AppState::GpuSelection; 328 | if let Err(e) = self.spawn_load_gpus() { 329 | self.set_error_and_quit(format!( 330 | "Error starting GPU fetch: {}", 331 | e 332 | )); 333 | return; 334 | } 335 | } 336 | } else { 337 | self.selected_leaderboard = None; 338 | self.leaderboards_state.select(Some(0)); 339 | self.app_state = AppState::LeaderboardSelection; 340 | } 341 | } else { 342 | self.leaderboards_state.select(Some(0)); 343 | } 344 | } 345 | Ok(Err(e)) => { 346 | self.set_error_and_quit(format!("Error fetching leaderboards: {}", e)) 347 | } 348 | Err(e) => self.set_error_and_quit(format!("Task join error: {}", e)), 349 | } 350 | } 351 | } 352 | } 353 | 354 | pub async fn check_gpu_task(&mut self) { 355 | if let Some(handle) = &mut self.gpus_task { 356 | if handle.is_finished() { 357 | let task = self.gpus_task.take().unwrap(); 358 | match task.await { 359 | Ok(Ok(gpus)) => { 360 | self.gpus = gpus; 361 | if let Some(selected_name) = &self.selected_gpu { 362 | if let Some(index) = self 363 | .gpus 364 | .iter() 365 | .position(|gpu| &gpu.title_text == selected_name) 366 | { 367 | self.gpus_state.select(Some(index)); 368 | self.app_state = AppState::SubmissionModeSelection; 369 | } else { 370 | self.selected_gpu = None; 371 | self.gpus_state.select(Some(0)); 372 | self.app_state = AppState::GpuSelection; 373 | } 374 | } else { 375 | self.gpus_state.select(Some(0)); 376 | } 377 | } 378 | Ok(Err(e)) => self.set_error_and_quit(format!("Error fetching GPUs: {}", e)), 379 | Err(e) => self.set_error_and_quit(format!("Task join error: {}", e)), 380 | } 381 | } 382 | } 383 | } 384 | 385 | pub async fn check_submission_task(&mut self) { 386 | if let Some(handle) = &mut self.submission_task { 387 | if handle.is_finished() { 388 | let task = self.submission_task.take().unwrap(); 389 | match task.await { 390 | Ok(Ok(status)) => { 391 | self.final_status = Some(status); 392 | self.should_quit = true; // Quit after showing final status 393 | } 394 | Ok(Err(e)) => self.set_error_and_quit(format!("Submission error: {}", e)), 395 | Err(e) => self.set_error_and_quit(format!("Task join error: {}", e)), 396 | } 397 | } 398 | } 399 | } 400 | } 401 | 402 | pub fn ui(app: &App, frame: &mut Frame) { 403 | let main_layout = Layout::default() 404 | .direction(Direction::Vertical) 405 | .constraints([Constraint::Min(0)].as_ref()) 406 | .split(frame.size()); 407 | 408 | let list_area = main_layout[0]; 409 | let available_width = list_area.width.saturating_sub(4) as usize; 410 | 411 | let list_block = Block::default().borders(Borders::ALL); 412 | let list_style = Style::default().fg(Color::White); 413 | 414 | match app.app_state { 415 | AppState::LeaderboardSelection => { 416 | let items: Vec = app 417 | .leaderboards 418 | .iter() 419 | .map(|lb| { 420 | let title_line = Line::from(Span::styled( 421 | lb.title_text.clone(), 422 | Style::default().fg(Color::White).bold(), 423 | )); 424 | let mut lines = vec![title_line]; 425 | for desc_part in lb.task_description.split('\n') { 426 | lines.push(Line::from(Span::styled( 427 | desc_part.to_string(), 428 | Style::default().fg(Color::Gray).dim(), 429 | ))); 430 | } 431 | ListItem::new(lines) 432 | }) 433 | .collect(); 434 | let list = List::new(items) 435 | .block(list_block.title("Select Leaderboard")) 436 | .style(list_style) 437 | .highlight_style(Style::default().bg(Color::DarkGray)) 438 | .highlight_symbol("> "); 439 | frame.render_stateful_widget(list, main_layout[0], &mut app.leaderboards_state.clone()); 440 | } 441 | AppState::GpuSelection => { 442 | let items: Vec = app 443 | .gpus 444 | .iter() 445 | .map(|gpu| { 446 | let line = Line::from(vec![Span::styled( 447 | gpu.title_text.clone(), 448 | Style::default().fg(Color::White).bold(), 449 | )]); 450 | ListItem::new(line) 451 | }) 452 | .collect(); 453 | let list = List::new(items) 454 | .block(list_block.title(format!( 455 | "Select GPU for '{}'", 456 | app.selected_leaderboard.as_deref().unwrap_or("N/A") 457 | ))) 458 | .style(list_style) 459 | .highlight_style(Style::default().bg(Color::DarkGray)) 460 | .highlight_symbol("> "); 461 | frame.render_stateful_widget(list, main_layout[0], &mut app.gpus_state.clone()); 462 | } 463 | AppState::SubmissionModeSelection => { 464 | let items: Vec = app 465 | .submission_modes 466 | .iter() 467 | .map(|mode| { 468 | let strings = utils::custom_wrap( 469 | mode.title_text.clone(), 470 | mode.description_text.clone(), 471 | available_width, 472 | ); 473 | 474 | let lines: Vec = strings 475 | .into_iter() 476 | .enumerate() 477 | .map(|(i, line)| { 478 | if i == 0 { 479 | Line::from(Span::styled( 480 | line, 481 | Style::default().fg(Color::White).bold(), 482 | )) 483 | } else { 484 | Line::from(Span::styled( 485 | line.clone(), 486 | Style::default().fg(Color::Gray).dim(), 487 | )) 488 | } 489 | }) 490 | .collect::>(); 491 | ListItem::new(lines) 492 | }) 493 | .collect::>(); 494 | let list = List::new(items) 495 | .block(list_block.title(format!( 496 | "Select Submission Mode for '{}' on '{}'", 497 | app.selected_leaderboard.as_deref().unwrap_or("N/A"), 498 | app.selected_gpu.as_deref().unwrap_or("N/A") 499 | ))) 500 | .style(list_style) 501 | .highlight_style(Style::default().bg(Color::DarkGray)) 502 | .highlight_symbol("> "); 503 | frame.render_stateful_widget( 504 | list, 505 | main_layout[0], 506 | &mut app.submission_modes_state.clone(), 507 | ); 508 | } 509 | AppState::WaitingForResult => { 510 | let loading_page = LoadingPage::default(); 511 | frame.render_stateful_widget( 512 | &loading_page, 513 | main_layout[0], 514 | &mut app.loading_page_state.clone(), 515 | ) 516 | } 517 | } 518 | } 519 | 520 | pub async fn run_submit_tui( 521 | filepath: Option, 522 | gpu: Option, 523 | leaderboard: Option, 524 | mode: Option, 525 | cli_id: String, 526 | ) -> Result<()> { 527 | let file_to_submit = match filepath { 528 | Some(fp) => fp, 529 | None => { 530 | // Prompt user for filepath if not provided 531 | println!("Please enter the path to your solution file:"); 532 | let mut input = String::new(); 533 | io::stdin().read_line(&mut input)?; 534 | input.trim().to_string() 535 | } 536 | }; 537 | 538 | if !Path::new(&file_to_submit).exists() { 539 | return Err(anyhow!("File not found: {}", file_to_submit)); 540 | } 541 | 542 | let (directives, has_multiple_gpus) = utils::get_popcorn_directives(&file_to_submit)?; 543 | 544 | if has_multiple_gpus { 545 | return Err(anyhow!( 546 | "Multiple GPUs are not supported yet. Please specify only one GPU." 547 | )); 548 | } 549 | 550 | let mut app = App::new(&file_to_submit, cli_id); 551 | 552 | // Override directives with CLI flags if provided 553 | if let Some(gpu_flag) = gpu { 554 | app.selected_gpu = Some(gpu_flag); 555 | } 556 | if let Some(leaderboard_flag) = leaderboard { 557 | app.selected_leaderboard = Some(leaderboard_flag); 558 | } 559 | if let Some(mode_flag) = mode { 560 | app.selected_submission_mode = Some(mode_flag); 561 | // Skip to submission if we have all required fields 562 | if app.selected_gpu.is_some() && app.selected_leaderboard.is_some() { 563 | app.app_state = AppState::WaitingForResult; 564 | } 565 | } 566 | 567 | // If no CLI flags, use directives 568 | if app.selected_gpu.is_none() && app.selected_leaderboard.is_none() { 569 | app.initialize_with_directives(directives); 570 | } 571 | 572 | // Spawn the initial task based on the starting state BEFORE setting up the TUI 573 | // If spawning fails here, we just return the error directly without TUI cleanup. 574 | match app.app_state { 575 | AppState::LeaderboardSelection => { 576 | if let Err(e) = app.spawn_load_leaderboards() { 577 | return Err(anyhow!("Error starting leaderboard fetch: {}", e)); 578 | } 579 | } 580 | AppState::GpuSelection => { 581 | if let Err(e) = app.spawn_load_gpus() { 582 | return Err(anyhow!("Error starting GPU fetch: {}", e)); 583 | } 584 | } 585 | AppState::WaitingForResult => { 586 | if let Err(e) = app.spawn_submit_solution() { 587 | return Err(anyhow!("Error starting submission: {}", e)); 588 | } 589 | } 590 | _ => {} 591 | } 592 | 593 | // Now, set up the TUI 594 | enable_raw_mode()?; 595 | let mut stdout = io::stdout(); 596 | crossterm::execute!(stdout, EnterAlternateScreen)?; 597 | let backend = CrosstermBackend::new(stdout); 598 | let mut terminal = Terminal::new(backend)?; 599 | 600 | while !app.should_quit { 601 | terminal.draw(|f| ui(&app, f))?; 602 | 603 | app.check_leaderboard_task().await; 604 | app.check_gpu_task().await; 605 | app.check_submission_task().await; 606 | 607 | app.update_loading_page_state(terminal.size()?.width); 608 | 609 | if event::poll(std::time::Duration::from_millis(50))? { 610 | if let Event::Key(key) = event::read()? { 611 | if key.kind == KeyEventKind::Press { 612 | app.handle_key_event(key)?; 613 | } 614 | } 615 | } 616 | } 617 | 618 | let mut result_text = "Submission cancelled.".to_string(); 619 | 620 | if let Some(status) = app.final_status { 621 | let trimmed = status.trim(); 622 | let content = if trimmed.starts_with('[') && trimmed.ends_with(']') && trimmed.len() >= 2 { 623 | &trimmed[1..trimmed.len() - 1] 624 | } else { 625 | trimmed 626 | }; 627 | 628 | let content = content.replace("\\n", "\n"); 629 | 630 | result_text = content.to_string(); 631 | } 632 | 633 | let state = &mut app.result_page_state; 634 | 635 | let mut result_page = ResultPage::new(result_text.clone(), state); 636 | let mut last_draw = std::time::Instant::now(); 637 | while !state.ack { 638 | // Force redraw every 100ms for smooth animation 639 | let now = std::time::Instant::now(); 640 | if now.duration_since(last_draw) >= std::time::Duration::from_millis(100) { 641 | terminal 642 | .draw(|frame: &mut Frame| { 643 | frame.render_stateful_widget(&result_page, frame.size(), state); 644 | }) 645 | .unwrap(); 646 | last_draw = now; 647 | } 648 | result_page.handle_key_event(state); 649 | } 650 | 651 | // Restore terminal 652 | disable_raw_mode()?; 653 | crossterm::execute!( 654 | terminal.backend_mut(), 655 | crossterm::terminal::LeaveAlternateScreen 656 | )?; 657 | terminal.show_cursor()?; 658 | 659 | // utils::display_ascii_art(); 660 | 661 | Ok(()) 662 | } 663 | -------------------------------------------------------------------------------- /src/main.rs: -------------------------------------------------------------------------------- 1 | mod cmd; 2 | mod models; 3 | mod service; 4 | mod utils; 5 | mod views; 6 | 7 | use crate::cmd::Cli; 8 | use clap::Parser; 9 | use std::env; 10 | use std::process; 11 | 12 | #[tokio::main] 13 | async fn main() { 14 | // Set the API URL FIRST - before anything else 15 | if env::var("POPCORN_API_URL").is_err() { 16 | env::set_var("POPCORN_API_URL", "https://discord-cluster-manager-1f6c4782e60a.herokuapp.com"); 17 | } 18 | // Parse command line arguments 19 | let cli = Cli::parse(); 20 | 21 | // Execute the parsed command 22 | if let Err(e) = cmd::execute(cli).await { 23 | eprintln!("Application error: {}", e); 24 | process::exit(1); 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /src/models/mod.rs: -------------------------------------------------------------------------------- 1 | use serde::{Deserialize, Serialize}; 2 | 3 | #[derive(Clone, Debug)] 4 | pub struct LeaderboardItem { 5 | pub title_text: String, 6 | pub task_description: String, 7 | } 8 | 9 | impl LeaderboardItem { 10 | pub fn new(title_text: String, task_description: String) -> Self { 11 | Self { 12 | title_text, 13 | task_description, 14 | } 15 | } 16 | } 17 | 18 | #[derive(Clone, Debug)] 19 | pub struct GpuItem { 20 | pub title_text: String, 21 | } 22 | 23 | impl GpuItem { 24 | pub fn new(title_text: String) -> Self { 25 | Self { title_text } 26 | } 27 | } 28 | 29 | #[derive(Clone, Debug)] 30 | pub struct SubmissionModeItem { 31 | pub title_text: String, 32 | pub description_text: String, 33 | pub value: String, 34 | } 35 | 36 | impl SubmissionModeItem { 37 | pub fn new(title_text: String, description_text: String, value: String) -> Self { 38 | Self { 39 | title_text, 40 | description_text, 41 | value, 42 | } 43 | } 44 | } 45 | 46 | #[derive(Clone, Copy, Debug, PartialEq, Default)] 47 | pub enum AppState { 48 | #[default] 49 | LeaderboardSelection, 50 | GpuSelection, 51 | SubmissionModeSelection, 52 | WaitingForResult, 53 | } 54 | 55 | #[derive(Debug, Serialize, Deserialize)] 56 | pub struct SubmissionResultMsg(pub String); 57 | -------------------------------------------------------------------------------- /src/service/mod.rs: -------------------------------------------------------------------------------- 1 | use anyhow::{anyhow, Result}; 2 | use reqwest::header::{HeaderMap, HeaderValue}; 3 | use reqwest::multipart::{Form, Part}; 4 | use reqwest::Client; 5 | use serde_json::Value; 6 | use std::env; 7 | use std::path::Path; 8 | use std::time::Duration; 9 | use tokio::io::AsyncWriteExt; 10 | 11 | use crate::models::{GpuItem, LeaderboardItem}; 12 | 13 | // Helper function to create a reusable reqwest client 14 | pub fn create_client(cli_id: Option) -> Result { 15 | let mut default_headers = HeaderMap::new(); 16 | 17 | if let Some(id) = cli_id { 18 | match HeaderValue::from_str(&id) { 19 | Ok(val) => { 20 | default_headers.insert("X-Popcorn-Cli-Id", val); 21 | } 22 | Err(_) => { 23 | return Err(anyhow!("Invalid cli_id format for HTTP header")); 24 | } 25 | } 26 | } 27 | 28 | Client::builder() 29 | .timeout(Duration::from_secs(180)) 30 | .default_headers(default_headers) 31 | .build() 32 | .map_err(|e| anyhow!("Failed to create HTTP client: {}", e)) 33 | } 34 | 35 | pub async fn fetch_leaderboards(client: &Client) -> Result> { 36 | let base_url = 37 | env::var("POPCORN_API_URL").map_err(|_| anyhow!("POPCORN_API_URL is not set"))?; 38 | 39 | let resp = client 40 | .get(format!("{}/leaderboards", base_url)) 41 | .timeout(Duration::from_secs(30)) 42 | .send() 43 | .await?; 44 | 45 | let status = resp.status(); 46 | if !status.is_success() { 47 | let error_text = resp.text().await?; 48 | return Err(anyhow!("Server returned status {}: {}", status, error_text)); 49 | } 50 | 51 | let leaderboards: Vec = resp.json().await?; 52 | 53 | let mut leaderboard_items = Vec::new(); 54 | for lb in leaderboards { 55 | let task = lb["task"] 56 | .as_object() 57 | .ok_or_else(|| anyhow!("Invalid JSON structure"))?; 58 | let name = lb["name"] 59 | .as_str() 60 | .ok_or_else(|| anyhow!("Invalid JSON structure"))?; 61 | let description = task["description"] 62 | .as_str() 63 | .ok_or_else(|| anyhow!("Invalid JSON structure"))?; 64 | 65 | leaderboard_items.push(LeaderboardItem::new( 66 | name.to_string(), 67 | description.to_string(), 68 | )); 69 | } 70 | 71 | Ok(leaderboard_items) 72 | } 73 | 74 | pub async fn fetch_gpus(client: &Client, leaderboard: &str) -> Result> { 75 | let base_url = 76 | env::var("POPCORN_API_URL").map_err(|_| anyhow!("POPCORN_API_URL is not set"))?; 77 | 78 | let resp = client 79 | .get(format!("{}/gpus/{}", base_url, leaderboard)) 80 | .timeout(Duration::from_secs(120)) 81 | .send() 82 | .await?; 83 | 84 | let status = resp.status(); 85 | if !status.is_success() { 86 | let error_text = resp.text().await?; 87 | return Err(anyhow!("Server returned status {}: {}", status, error_text)); 88 | } 89 | 90 | let gpus: Vec = resp.json().await?; 91 | 92 | let gpu_items = gpus.into_iter().map(|gpu| GpuItem::new(gpu)).collect(); 93 | 94 | Ok(gpu_items) 95 | } 96 | 97 | pub async fn submit_solution>( 98 | client: &Client, 99 | filepath: P, 100 | file_content: &str, 101 | leaderboard: &str, 102 | gpu: &str, 103 | submission_mode: &str, 104 | ) -> Result { 105 | let base_url = 106 | env::var("POPCORN_API_URL").map_err(|_| anyhow!("POPCORN_API_URL is not set"))?; 107 | 108 | let filename = filepath 109 | .as_ref() 110 | .file_name() 111 | .ok_or_else(|| anyhow!("Invalid filepath"))? 112 | .to_string_lossy(); 113 | 114 | let part = Part::bytes(file_content.as_bytes().to_vec()).file_name(filename.to_string()); 115 | 116 | let form = Form::new().part("file", part); 117 | 118 | let url = format!( 119 | "{}/{}/{}/{}", 120 | base_url, 121 | leaderboard.to_lowercase(), 122 | gpu, 123 | submission_mode.to_lowercase() 124 | ); 125 | 126 | let resp = client 127 | .post(&url) 128 | .multipart(form) 129 | .timeout(Duration::from_secs(3600)) 130 | .send() 131 | .await?; 132 | 133 | let status = resp.status(); 134 | if !status.is_success() { 135 | let error_text = resp.text().await?; 136 | let detail = serde_json::from_str::(&error_text) 137 | .ok() 138 | .and_then(|v| v.get("detail").and_then(|d| d.as_str()).map(str::to_string)); 139 | 140 | return Err(anyhow!( 141 | "Server returned status {}: {}", 142 | status, 143 | detail.unwrap_or(error_text) 144 | )); 145 | } 146 | 147 | if resp 148 | .headers() 149 | .get(reqwest::header::CONTENT_TYPE) 150 | .and_then(|v| v.to_str().ok()) 151 | .map_or(false, |s| s.starts_with("text/event-stream")) 152 | { 153 | let mut resp = resp; 154 | let mut buffer = String::new(); 155 | let mut stderr = tokio::io::stderr(); 156 | 157 | while let Some(chunk) = resp.chunk().await? { 158 | buffer.push_str(&String::from_utf8_lossy(&chunk)); 159 | 160 | while let Some(pos) = buffer.find("\n\n") { 161 | let message_str = buffer.drain(..pos + 2).collect::(); 162 | let mut event_type = None; 163 | let mut data_json = None; 164 | 165 | for line in message_str.lines() { 166 | if line.starts_with("event:") { 167 | event_type = Some(line["event:".len()..].trim()); 168 | } else if line.starts_with("data:") { 169 | data_json = Some(line["data:".len()..].trim()); 170 | } 171 | } 172 | 173 | if let (Some(event), Some(data)) = (event_type, data_json) { 174 | match event { 175 | "status" => (), 176 | "result" => { 177 | let result_val: Value = serde_json::from_str(data)?; 178 | let reports = result_val.get("reports").unwrap(); 179 | return Ok(reports.to_string()); 180 | } 181 | "error" => { 182 | let error_val: Value = serde_json::from_str(data)?; 183 | let detail = error_val 184 | .get("detail") 185 | .and_then(|d| d.as_str()) 186 | .unwrap_or("Unknown server error"); 187 | let status_code = error_val.get("status_code").and_then(|s| s.as_i64()); 188 | let raw_error = error_val.get("raw_error").and_then(|e| e.as_str()); 189 | 190 | let mut error_msg = format!("Server processing error: {}", detail); 191 | if let Some(sc) = status_code { 192 | error_msg.push_str(&format!(" (Status Code: {})", sc)); 193 | } 194 | if let Some(re) = raw_error { 195 | error_msg.push_str(&format!(" | Raw Error: {}", re)); 196 | } 197 | 198 | return Err(anyhow!(error_msg)); 199 | } 200 | _ => { 201 | stderr 202 | .write_all( 203 | format!("Ignoring unknown SSE event: {}\n", event).as_bytes(), 204 | ) 205 | .await?; 206 | stderr.flush().await?; 207 | } 208 | } 209 | } 210 | } 211 | } 212 | Err(anyhow!( 213 | "Stream ended unexpectedly without a final result or error event." 214 | )) 215 | } else { 216 | let result: Value = resp.json().await?; 217 | let pretty_result = match result.get("results") { 218 | Some(result_obj) => serde_json::to_string_pretty(result_obj)?, 219 | None => return Err(anyhow!("Invalid non-streaming response structure")), 220 | }; 221 | Ok(pretty_result) 222 | } 223 | } 224 | -------------------------------------------------------------------------------- /src/utils/mod.rs: -------------------------------------------------------------------------------- 1 | use std::fs; 2 | use std::path::Path; 3 | use anyhow::Result; 4 | 5 | pub struct PopcornDirectives { 6 | pub leaderboard_name: String, 7 | pub gpus: Vec, 8 | } 9 | 10 | pub fn get_popcorn_directives>(filepath: P) -> Result<(PopcornDirectives, bool)> { 11 | let content = fs::read_to_string(filepath)?; 12 | 13 | let mut gpus: Vec = Vec::new(); 14 | let mut leaderboard_name = String::new(); 15 | let mut has_multiple_gpus = false; 16 | 17 | for line in content.lines() { 18 | if !line.starts_with("//") && !line.starts_with("#") { 19 | continue; 20 | } 21 | 22 | let parts: Vec<&str> = line.split_whitespace().collect(); 23 | if parts.len() < 2 { 24 | continue; 25 | } 26 | 27 | if parts[0] == "//!POPCORN" || parts[0] == "#!POPCORN" { 28 | let arg = parts[1].to_lowercase(); 29 | if arg == "gpu" || arg == "gpus" { 30 | gpus = parts[2..].iter().map(|s| s.to_string()).collect(); 31 | } else if arg == "leaderboard" && parts.len() > 2 { 32 | leaderboard_name = parts[2].to_string(); 33 | } 34 | } 35 | } 36 | 37 | if gpus.len() > 1 { 38 | has_multiple_gpus = true; 39 | gpus = vec![gpus[0].clone()]; 40 | } 41 | 42 | Ok(( 43 | PopcornDirectives { 44 | leaderboard_name, 45 | gpus, 46 | }, 47 | has_multiple_gpus 48 | )) 49 | } 50 | 51 | pub fn get_ascii_art_frame(frame: u16) -> String { 52 | let frame = frame % 3; 53 | match frame { 54 | 0 => r#" 55 | ▗▖ ▗▖▗▄▄▄▖▗▄▄▖ ▗▖ ▗▖▗▄▄▄▖▗▖ ▗▄▄▖ ▗▄▖ ▗▄▄▄▖ 56 | ▐▌▗▞▘▐▌ ▐▌ ▐▌▐▛▚▖▐▌▐▌ ▐▌ ▐▌ ▐▌▐▌ ▐▌ █ 57 | ▐▛▚▖ ▐▛▀▀▘▐▛▀▚▖▐▌ ▝▜▌▐▛▀▀▘▐▌ ▐▛▀▚▖▐▌ ▐▌ █ 58 | ▐▌ ▐▌▐▙▄▄▖▐▌ ▐▌▐▌ ▐▌▐▙▄▄▖▐▙▄▄▖▐▙▄▞▘▝▚▄▞▘ █ 59 | 60 | POPCORN CLI - GPU MODE 61 | 62 | ┌────────────────────────────────────────────┐ 63 | │ ╔══════════════════════════════════╗ ϟ │ 64 | │ ║ ▄▄ Graphics Processing Unit ▄▄║ ║ │▒ 65 | │ ║ ██████ 80GB HBM3 MEMORY █║ ║ │▒ 66 | │ ║ ▀▀▀▀▀▀ 700W TDP █║ ║ │▒ 67 | │ ╚══════════════════════════════════╝ │▒ 68 | │ ┌─────┐┌─────┐┌─────┐┌─────┐┌─────┐ │▒ 69 | │ │:::::││:::::││:::::││:::::││:::::│ │▒ 70 | │ └─────┘└─────┘└─────┘└─────┘└─────┘ │▒ 71 | │ ┌──────────────────────────────────┐ │▒ 72 | │ │ discord.com/invite/gpumode │ │▒ 73 | │ │ ═══╧═══╧═══╧═══╧═══╧═══╧═══ │ │▒ 74 | │ └──────────────────────────────────┘ │▒ 75 | └────────────────────────────────────────────┘▒ 76 | ▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒ 77 | ▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀ ▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀"#.to_string(), 78 | 1 => r#" 79 | ▗▖ ▗▖▗▄▄▄▖▗▄▄▖ ▗▖ ▗▖▗▄▄▄▖▗▖ ▗▄▄▖ ▗▄▖ ▗▄▄▄▖ 80 | ▐▌▗▞▘▐▌ ▐▌ ▐▌▐▛▚▖▐▌▐▌ ▐▌ ▐▌ ▐▌▐▌ ▐▌ █ 81 | ▐▛▚▖ ▐▛▀▀▘▐▛▀▚▖▐▌ ▝▜▌▐▛▀▀▘▐▌ ▐▛▀▚▖▐▌ ▐▌ █ 82 | ▐▌ ▐▌▐▙▄▄▖▐▌ ▐▌▐▌ ▐▌▐▙▄▄▖▐▙▄▄▖▐▙▄▞▘▝▚▄▞▘ █ 83 | 84 | POPCORN CLI - GPU MODE 85 | 86 | ┌────────────────────────────────────────────┐ 87 | │ ╔══════════════════════════════════╗ ϟϟ │ 88 | │ ║ ▄▄ Graphics Processing Unit ▄▄║ ║ │▒ 89 | │ ║ ██████ 80GB HBM3 MEMORY ███║ ║ │▒ 90 | │ ║ ▀▀▀▀▀▀ 700W TDP ███║ ║ │▒ 91 | │ ╚══════════════════════════════════╝ │▒ 92 | │ ┌─────┐┌─────┐┌─────┐┌─────┐┌─────┐ │▒ 93 | │ │:::::││:::::││:::::││:::::││:::::│ │▒ 94 | │ └─────┘└─────┘└─────┘└─────┘└─────┘ │▒ 95 | │ ┌──────────────────────────────────┐ │▒ 96 | │ │ discord.com/invite/gpumode │ │▒ 97 | │ │ ═══╧═══╧═══╧═══╧═══╧═══╧═══ │ │▒ 98 | │ └──────────────────────────────────┘ │▒ 99 | └────────────────────────────────────────────┘▒ 100 | ▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒ 101 | ▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀ ▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀"#.to_string(), 102 | _ => r#" 103 | ▗▖ ▗▖▗▄▄▄▖▗▄▄▖ ▗▖ ▗▖▗▄▄▄▖▗▖ ▗▄▄▖ ▗▄▖ ▗▄▄▄▖ 104 | ▐▌▗▞▘▐▌ ▐▌ ▐▌▐▛▚▖▐▌▐▌ ▐▌ ▐▌ ▐▌▐▌ ▐▌ █ 105 | ▐▛▚▖ ▐▛▀▀▘▐▛▀▚▖▐▌ ▝▜▌▐▛▀▀▘▐▌ ▐▛▀▚▖▐▌ ▐▌ █ 106 | ▐▌ ▐▌▐▙▄▄▖▐▌ ▐▌▐▌ ▐▌▐▙▄▄▖▐▙▄▄▖▐▙▄▞▘▝▚▄▞▘ █ 107 | 108 | POPCORN CLI - GPU MODE 109 | 110 | ┌────────────────────────────────────────────┐ 111 | │ ╔══════════════════════════════════╗ ϟϟϟ │ 112 | │ ║ ▄▄ Graphics Processing Unit ▄▄║ ║ │▒ 113 | │ ║ ██████ 80GB HBM3 MEMORY █████║ ║ │▒ 114 | │ ║ ▀▀▀▀▀▀ 700W TDP █████║ ║ │▒ 115 | │ ╚══════════════════════════════════╝ │▒ 116 | │ ┌─────┐┌─────┐┌─────┐┌─────┐┌─────┐ │▒ 117 | │ │:::::││:::::││:::::││:::::││:::::│ │▒ 118 | │ └─────┘└─────┘└─────┘└─────┘└─────┘ │▒ 119 | │ ┌──────────────────────────────────┐ │▒ 120 | │ │ discord.com/invite/gpumode │ │▒ 121 | │ │ ═══╧═══╧═══╧═══╧═══╧═══╧═══ │ │▒ 122 | │ └──────────────────────────────────┘ │▒ 123 | └────────────────────────────────────────────┘▒ 124 | ▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒ 125 | ▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀ ▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀"#.to_string() 126 | } 127 | } 128 | 129 | pub fn get_ascii_art() -> String { 130 | get_ascii_art_frame(0) 131 | } 132 | 133 | pub fn display_ascii_art() { 134 | let art = get_ascii_art(); 135 | println!("{}", art); 136 | } 137 | 138 | pub fn custom_wrap(initial_text: String, remaining_text: String, available_width: usize) -> Vec { 139 | let mut lines = vec![initial_text]; 140 | let mut current_line = String::with_capacity(available_width); 141 | for word in remaining_text.split_whitespace() { 142 | if word.len() > available_width { 143 | if !current_line.is_empty() { 144 | lines.push(current_line.clone()); 145 | current_line.clear(); 146 | } 147 | lines.push(word.to_string()); 148 | } else if current_line.is_empty() { 149 | current_line.push_str(word); 150 | } else if current_line.len() + word.len() + 1 <= available_width { 151 | current_line.push(' '); 152 | current_line.push_str(word); 153 | } else { 154 | lines.push(current_line.clone()); 155 | current_line.clear(); 156 | current_line.push_str(word); 157 | } 158 | } 159 | 160 | if !current_line.is_empty() { 161 | lines.push(current_line); 162 | } 163 | lines 164 | } 165 | -------------------------------------------------------------------------------- /src/views/loading_page.rs: -------------------------------------------------------------------------------- 1 | use ratatui::{ 2 | buffer::Buffer, 3 | layout::{Alignment, Layout, Rect}, 4 | style::{Color, Stylize}, 5 | widgets::{Block, Gauge, Padding, Paragraph, StatefulWidget, Widget}, 6 | }; 7 | 8 | #[derive(Debug, Default, Clone)] 9 | pub struct LoadingPageState { 10 | pub loop_count: u16, 11 | pub progress_column: u16, 12 | pub progress_bar: f64, 13 | } 14 | 15 | #[derive(Default, Debug, PartialEq, Eq, Clone)] 16 | pub struct LoadingPage { 17 | header_area: Rect, 18 | gauge_area: Rect, 19 | footer_area: Rect, 20 | } 21 | 22 | fn get_gradient_color(progress: f64) -> Color { 23 | // Convert progress from 0-100 to 0-1 24 | let t = progress / 100.0; 25 | 26 | // Start with red (255, 0, 0) and end with green (0, 255, 0) 27 | let r = ((1.0 - t) * 255.0) as u8; 28 | let g = (t * 255.0) as u8; 29 | let b = 0; 30 | 31 | Color::Rgb(r, g, b) 32 | } 33 | 34 | impl StatefulWidget for &LoadingPage { 35 | type State = LoadingPageState; 36 | 37 | fn render(self, area: Rect, buf: &mut Buffer, state: &mut Self::State) { 38 | use ratatui::layout::Constraint::Percentage; 39 | 40 | let layout = Layout::vertical([Percentage(45), Percentage(10), Percentage(45)]); 41 | 42 | let [_, gauge_area, footer_area] = layout.areas(area); 43 | 44 | render_gauge(gauge_area, buf, state); 45 | render_footer(footer_area, buf, state); 46 | } 47 | } 48 | 49 | fn render_gauge(area: Rect, buf: &mut Buffer, state: &mut LoadingPageState) { 50 | let blk = Block::default().padding(Padding::horizontal(20)); 51 | Gauge::default() 52 | .block(blk) 53 | .gauge_style(get_gradient_color(state.progress_bar)) 54 | .ratio(state.progress_bar / 100.0) 55 | .render(area, buf); 56 | } 57 | 58 | fn get_footer_text(state: &LoadingPageState) -> String { 59 | let percentage = state.progress_bar; 60 | 61 | if state.loop_count > 0 { 62 | return "Did you know we have zero idea how long this will take?".to_string(); 63 | } 64 | 65 | if percentage > 75.0 { 66 | return "Almost there!".to_string(); 67 | } else if percentage > 35.0 { 68 | return "Crunching numbers...".to_string(); 69 | } else { 70 | return "This is taking a while, huh?".to_string(); 71 | } 72 | } 73 | 74 | fn render_footer(area: Rect, buf: &mut Buffer, state: &LoadingPageState) { 75 | let blk = Block::default().padding(Padding::vertical(1)); 76 | let text = Paragraph::new(get_footer_text(state)) 77 | .alignment(Alignment::Center) 78 | .fg(Color::White) 79 | .bold() 80 | .block(blk); 81 | 82 | text.render(area, buf); 83 | } 84 | -------------------------------------------------------------------------------- /src/views/mod.rs: -------------------------------------------------------------------------------- 1 | pub mod result_page; 2 | pub mod loading_page; 3 | -------------------------------------------------------------------------------- /src/views/result_page.rs: -------------------------------------------------------------------------------- 1 | use crate::utils; 2 | use crossterm::event::{self, Event, KeyCode, KeyEventKind}; 3 | use ratatui::{ 4 | layout::{Alignment, Constraint, Layout, Margin, Rect}, 5 | prelude::Buffer, 6 | style::{Color, Style}, 7 | symbols::scrollbar, 8 | widgets::{Block, BorderType, Paragraph, Scrollbar, ScrollbarState, StatefulWidget, Widget}, 9 | }; 10 | 11 | #[derive(Default, Debug)] 12 | pub struct ResultPageState { 13 | pub vertical_scroll: u16, 14 | pub vertical_scroll_state: ScrollbarState, 15 | pub horizontal_scroll: u16, 16 | pub horizontal_scroll_state: ScrollbarState, 17 | pub ack: bool, 18 | pub animation_frame: u16, 19 | } 20 | 21 | #[derive(Default, Debug)] 22 | pub struct ResultPage { 23 | result_text: Paragraph<'static>, 24 | } 25 | 26 | impl ResultPage { 27 | pub fn new(result_text: String, state: &mut ResultPageState) -> Self { 28 | let max_width = result_text 29 | .lines() 30 | .map(|line| line.len()) 31 | .max() 32 | .unwrap_or(0); 33 | 34 | let num_lines = result_text.lines().count(); 35 | 36 | state.vertical_scroll_state = state 37 | .vertical_scroll_state 38 | .content_length(num_lines); 39 | 40 | state.horizontal_scroll_state = state.horizontal_scroll_state.content_length(max_width); 41 | state.animation_frame = 0; 42 | 43 | Self { 44 | result_text: Paragraph::new(result_text), 45 | } 46 | } 47 | 48 | fn render_left(&self, buf: &mut Buffer, left: Rect, state: &mut ResultPageState) { 49 | let left_block = Block::bordered() 50 | .border_type(BorderType::Plain) 51 | .border_style(Style::default().fg(Color::Rgb(255, 165, 0))) 52 | .title("GPU MODE") 53 | .title_alignment(Alignment::Center); 54 | 55 | let left_text = Paragraph::new(utils::get_ascii_art_frame(state.animation_frame / 5)); 56 | 57 | left_text.block(left_block).render(left, buf); 58 | } 59 | 60 | fn render_right(&self, buf: &mut Buffer, right: Rect, state: &mut ResultPageState) { 61 | let right_block = Block::bordered() 62 | .border_type(BorderType::Plain) 63 | .border_style(Style::default().fg(Color::Rgb(255, 165, 0))) 64 | .title_alignment(Alignment::Center) 65 | .title("Submission Results") 66 | .title_bottom("Press q to quit...") 67 | .title_style(Style::default().fg(Color::Magenta)); 68 | 69 | let result_text = self 70 | .result_text 71 | .clone() 72 | .block(right_block) 73 | .scroll((state.vertical_scroll as u16, state.horizontal_scroll as u16)); 74 | result_text.render(right, buf); 75 | } 76 | 77 | pub fn handle_key_event(&mut self, state: &mut ResultPageState) { 78 | // Use a non-blocking poll 79 | if let Ok(true) = event::poll(std::time::Duration::from_millis(0)) { 80 | if let Ok(Event::Key(key)) = event::read() { 81 | if key.kind != KeyEventKind::Press { 82 | return; 83 | } 84 | if key.code == KeyCode::Char('q') { 85 | state.ack = true; 86 | } 87 | 88 | match key.code { 89 | KeyCode::Char('j') | KeyCode::Down => { 90 | state.vertical_scroll = state.vertical_scroll.saturating_add(1); 91 | state.vertical_scroll_state = state 92 | .vertical_scroll_state 93 | .position(state.vertical_scroll as usize); 94 | } 95 | KeyCode::Char('k') | KeyCode::Up => { 96 | state.vertical_scroll = state.vertical_scroll.saturating_sub(1); 97 | state.vertical_scroll_state = state 98 | .vertical_scroll_state 99 | .position(state.vertical_scroll as usize); 100 | } 101 | KeyCode::Char('h') | KeyCode::Left => { 102 | state.horizontal_scroll = state.horizontal_scroll.saturating_sub(1); 103 | state.horizontal_scroll_state = state 104 | .horizontal_scroll_state 105 | .position(state.horizontal_scroll as usize); 106 | } 107 | KeyCode::Char('l') | KeyCode::Right => { 108 | state.horizontal_scroll = state.horizontal_scroll.saturating_add(1); 109 | state.horizontal_scroll_state = state 110 | .horizontal_scroll_state 111 | .position(state.horizontal_scroll as usize); 112 | } 113 | _ => {} 114 | } 115 | } 116 | } 117 | } 118 | } 119 | 120 | impl StatefulWidget for &ResultPage { 121 | type State = ResultPageState; 122 | 123 | fn render(self, area: Rect, buf: &mut Buffer, state: &mut ResultPageState) { 124 | // Increment animation frame on every render 125 | state.animation_frame = state.animation_frame.wrapping_add(1); 126 | 127 | let layout = Layout::horizontal([Constraint::Percentage(45), Constraint::Percentage(55)]); 128 | let [left, right] = layout.areas(area); 129 | 130 | self.render_left(buf, left, state); 131 | self.render_right(buf, right, state); 132 | 133 | let vertical_scrollbar = 134 | Scrollbar::new(ratatui::widgets::ScrollbarOrientation::VerticalLeft) 135 | .symbols(scrollbar::VERTICAL); 136 | 137 | let horizontal_scrollbar = 138 | Scrollbar::new(ratatui::widgets::ScrollbarOrientation::HorizontalBottom) 139 | .symbols(scrollbar::HORIZONTAL); 140 | 141 | vertical_scrollbar.render( 142 | right.inner(&Margin { 143 | vertical: 1, 144 | horizontal: 0, 145 | }), 146 | buf, 147 | &mut state.vertical_scroll_state, 148 | ); 149 | horizontal_scrollbar.render( 150 | right.inner(&Margin { 151 | vertical: 0, 152 | horizontal: 1, 153 | }), 154 | buf, 155 | &mut state.horizontal_scroll_state, 156 | ); 157 | } 158 | } 159 | --------------------------------------------------------------------------------