├── .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 |
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 |
--------------------------------------------------------------------------------