├── .github ├── ISSUE_TEMPLATE │ ├── bug_report.md │ └── feature_request.md ├── PULL_REQUEST_TEMPLATE │ └── pull_request_template.md └── workflows │ ├── rust-binaries.yml │ └── rust-cargo-publish.yml ├── .gitignore ├── Cargo.lock ├── Cargo.toml ├── LEVEL_DESIGN.md ├── LICENSE ├── README.md ├── REPO.md ├── design └── logo.txt ├── examples └── repo │ ├── l1t │ └── levels │ ├── test_level1.l1t │ ├── test_level2.l1t │ └── test_level3.l1t └── src ├── controls.rs ├── direction.rs ├── level.rs ├── lib.rs ├── main.rs ├── menu.rs ├── node.rs ├── repository.rs └── userdata.rs /.github/ISSUE_TEMPLATE/bug_report.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Bug report 3 | about: Create a report to help us improve 4 | title: '' 5 | labels: bug 6 | assignees: alex-laycalvert 7 | 8 | --- 9 | 10 | **Describe the bug** 11 | 12 | 13 | **To Reproduce** 14 | 15 | **Expected behavior** 16 | 17 | **Screenshots** 18 | 19 | **Operating System** 20 | 21 | **Additional context** 22 | Add any other context about the problem here. 23 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/feature_request.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Feature request 3 | about: Suggest an idea for this project 4 | title: '' 5 | labels: enhancement 6 | assignees: alex-laycalvert 7 | 8 | --- 9 | 10 | **Describe your desired feature** 11 | 12 | **Additional context** 13 | Add any other context or screenshots about the feature request here. 14 | -------------------------------------------------------------------------------- /.github/PULL_REQUEST_TEMPLATE/pull_request_template.md: -------------------------------------------------------------------------------- 1 | ## Description 2 | 3 | 4 | 5 | ## Checklist 6 | 7 | - [ ] I am using the latest version of `l1t` 8 | - [ ] My development instance produces no errors/warnings 9 | - [ ] I have testing my PR and it produces the desired output 10 | - [ ] I have updated the necessary documentation for this PR. 11 | -------------------------------------------------------------------------------- /.github/workflows/rust-binaries.yml: -------------------------------------------------------------------------------- 1 | name: Rust Platform Builds 2 | 3 | on: 4 | push: 5 | branches: ["*"] 6 | 7 | jobs: 8 | build: 9 | runs-on: ubuntu-latest 10 | 11 | steps: 12 | - name: Checkout 13 | uses: actions/checkout@v1 14 | 15 | - name: Install latest rust toolchain 16 | uses: actions-rs/toolchain@v1 17 | with: 18 | toolchain: stable 19 | default: true 20 | override: true 21 | 22 | - name: Build 23 | run: cargo build --all --release && strip target/release/l1t && mv target/release/l1t target/release/l1t_amd64 24 | 25 | - name: Release 26 | uses: softprops/action-gh-release@v1 27 | if: startsWith(github.ref, 'refs/tags/') 28 | with: 29 | files: | 30 | target/release/l1t_amd64 31 | env: 32 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 33 | 34 | build-win: 35 | runs-on: windows-latest 36 | 37 | steps: 38 | - name: Checkout 39 | uses: actions/checkout@v1 40 | 41 | - name: Install latest rust toolchain 42 | uses: actions-rs/toolchain@v1 43 | with: 44 | toolchain: stable 45 | default: true 46 | override: true 47 | 48 | - name: Build 49 | run: cargo build --all --release 50 | 51 | - name: Release 52 | uses: softprops/action-gh-release@v1 53 | if: startsWith(github.ref, 'refs/tags/') 54 | with: 55 | files: target/release/l1t.exe 56 | env: 57 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 58 | 59 | build-mac: 60 | runs-on: macos-latest 61 | 62 | steps: 63 | - name: Checkout 64 | uses: actions/checkout@v1 65 | 66 | - name: Install latest rust toolchain 67 | uses: actions-rs/toolchain@v1 68 | with: 69 | toolchain: stable 70 | target: x86_64-apple-darwin 71 | default: true 72 | override: true 73 | 74 | - name: Build for mac 75 | run: cargo build --all --release && strip target/release/l1t && mv target/release/l1t target/release/l1t_darwin 76 | 77 | - name: Release 78 | uses: softprops/action-gh-release@v1 79 | if: startsWith(github.ref, 'refs/tags/') 80 | with: 81 | files: | 82 | target/release/l1t_darwin 83 | env: 84 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 85 | -------------------------------------------------------------------------------- /.github/workflows/rust-cargo-publish.yml: -------------------------------------------------------------------------------- 1 | name: "Publish to Cargo" 2 | 3 | on: 4 | release: 5 | types: [published] 6 | 7 | jobs: 8 | publish: 9 | runs-on: ubuntu-latest 10 | 11 | steps: 12 | - uses: actions/checkout@v3 13 | - uses: actions-rs/toolchain@v1 14 | with: 15 | toolchain: stable 16 | override: true 17 | - uses: katyo/publish-crates@v2 18 | with: 19 | registry-token: ${{ secrets.CARGO_REGISTRY_TOKEN }} 20 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | /target 2 | *.tar.gz 3 | -------------------------------------------------------------------------------- /Cargo.lock: -------------------------------------------------------------------------------- 1 | # This file is automatically @generated by Cargo. 2 | # It is not intended for manual editing. 3 | version = 3 4 | 5 | [[package]] 6 | name = "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 = "android-tzdata" 22 | version = "0.1.1" 23 | source = "registry+https://github.com/rust-lang/crates.io-index" 24 | checksum = "e999941b234f3131b00bc13c22d06e8c5ff726d1b6318ac7eb276997bbb4fef0" 25 | 26 | [[package]] 27 | name = "android_system_properties" 28 | version = "0.1.5" 29 | source = "registry+https://github.com/rust-lang/crates.io-index" 30 | checksum = "819e7219dbd41043ac279b19830f2efc897156490d7fd6ea916720117ee66311" 31 | dependencies = [ 32 | "libc", 33 | ] 34 | 35 | [[package]] 36 | name = "anstream" 37 | version = "0.6.18" 38 | source = "registry+https://github.com/rust-lang/crates.io-index" 39 | checksum = "8acc5369981196006228e28809f761875c0327210a891e941f4c683b3a99529b" 40 | dependencies = [ 41 | "anstyle", 42 | "anstyle-parse", 43 | "anstyle-query", 44 | "anstyle-wincon", 45 | "colorchoice", 46 | "is_terminal_polyfill", 47 | "utf8parse", 48 | ] 49 | 50 | [[package]] 51 | name = "anstyle" 52 | version = "1.0.10" 53 | source = "registry+https://github.com/rust-lang/crates.io-index" 54 | checksum = "55cc3b69f167a1ef2e161439aa98aed94e6028e5f9a59be9a6ffb47aef1651f9" 55 | 56 | [[package]] 57 | name = "anstyle-parse" 58 | version = "0.2.6" 59 | source = "registry+https://github.com/rust-lang/crates.io-index" 60 | checksum = "3b2d16507662817a6a20a9ea92df6652ee4f94f914589377d69f3b21bc5798a9" 61 | dependencies = [ 62 | "utf8parse", 63 | ] 64 | 65 | [[package]] 66 | name = "anstyle-query" 67 | version = "1.1.2" 68 | source = "registry+https://github.com/rust-lang/crates.io-index" 69 | checksum = "79947af37f4177cfead1110013d678905c37501914fba0efea834c3fe9a8d60c" 70 | dependencies = [ 71 | "windows-sys 0.59.0", 72 | ] 73 | 74 | [[package]] 75 | name = "anstyle-wincon" 76 | version = "3.0.6" 77 | source = "registry+https://github.com/rust-lang/crates.io-index" 78 | checksum = "2109dbce0e72be3ec00bed26e6a7479ca384ad226efdd66db8fa2e3a38c83125" 79 | dependencies = [ 80 | "anstyle", 81 | "windows-sys 0.59.0", 82 | ] 83 | 84 | [[package]] 85 | name = "autocfg" 86 | version = "1.4.0" 87 | source = "registry+https://github.com/rust-lang/crates.io-index" 88 | checksum = "ace50bade8e6234aa140d9a2f552bbee1db4d353f69b8217bc503490fc1a9f26" 89 | 90 | [[package]] 91 | name = "backtrace" 92 | version = "0.3.74" 93 | source = "registry+https://github.com/rust-lang/crates.io-index" 94 | checksum = "8d82cb332cdfaed17ae235a638438ac4d4839913cc2af585c3c6746e8f8bee1a" 95 | dependencies = [ 96 | "addr2line", 97 | "cfg-if", 98 | "libc", 99 | "miniz_oxide", 100 | "object", 101 | "rustc-demangle", 102 | "windows-targets 0.52.6", 103 | ] 104 | 105 | [[package]] 106 | name = "base64" 107 | version = "0.21.7" 108 | source = "registry+https://github.com/rust-lang/crates.io-index" 109 | checksum = "9d297deb1925b89f2ccc13d7635fa0714f12c87adce1c75356b39ca9b7178567" 110 | 111 | [[package]] 112 | name = "bitflags" 113 | version = "1.3.2" 114 | source = "registry+https://github.com/rust-lang/crates.io-index" 115 | checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a" 116 | 117 | [[package]] 118 | name = "bitflags" 119 | version = "2.6.0" 120 | source = "registry+https://github.com/rust-lang/crates.io-index" 121 | checksum = "b048fb63fd8b5923fc5aa7b340d8e156aec7ec02f0c78fa8a6ddc2613f6f71de" 122 | 123 | [[package]] 124 | name = "bumpalo" 125 | version = "3.16.0" 126 | source = "registry+https://github.com/rust-lang/crates.io-index" 127 | checksum = "79296716171880943b8470b5f8d03aa55eb2e645a4874bdbb28adb49162e012c" 128 | 129 | [[package]] 130 | name = "bytes" 131 | version = "1.9.0" 132 | source = "registry+https://github.com/rust-lang/crates.io-index" 133 | checksum = "325918d6fe32f23b19878fe4b34794ae41fc19ddbe53b10571a4874d44ffd39b" 134 | 135 | [[package]] 136 | name = "cc" 137 | version = "1.2.2" 138 | source = "registry+https://github.com/rust-lang/crates.io-index" 139 | checksum = "f34d93e62b03caf570cccc334cbc6c2fceca82f39211051345108adcba3eebdc" 140 | dependencies = [ 141 | "shlex", 142 | ] 143 | 144 | [[package]] 145 | name = "cfg-if" 146 | version = "1.0.0" 147 | source = "registry+https://github.com/rust-lang/crates.io-index" 148 | checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd" 149 | 150 | [[package]] 151 | name = "chrono" 152 | version = "0.4.38" 153 | source = "registry+https://github.com/rust-lang/crates.io-index" 154 | checksum = "a21f936df1771bf62b77f047b726c4625ff2e8aa607c01ec06e5a05bd8463401" 155 | dependencies = [ 156 | "android-tzdata", 157 | "iana-time-zone", 158 | "js-sys", 159 | "num-traits", 160 | "wasm-bindgen", 161 | "windows-targets 0.52.6", 162 | ] 163 | 164 | [[package]] 165 | name = "clap" 166 | version = "4.5.22" 167 | source = "registry+https://github.com/rust-lang/crates.io-index" 168 | checksum = "69371e34337c4c984bbe322360c2547210bf632eb2814bbe78a6e87a2935bd2b" 169 | dependencies = [ 170 | "clap_builder", 171 | "clap_derive", 172 | ] 173 | 174 | [[package]] 175 | name = "clap_builder" 176 | version = "4.5.22" 177 | source = "registry+https://github.com/rust-lang/crates.io-index" 178 | checksum = "6e24c1b4099818523236a8ca881d2b45db98dadfb4625cf6608c12069fcbbde1" 179 | dependencies = [ 180 | "anstream", 181 | "anstyle", 182 | "clap_lex", 183 | "strsim", 184 | ] 185 | 186 | [[package]] 187 | name = "clap_derive" 188 | version = "4.5.18" 189 | source = "registry+https://github.com/rust-lang/crates.io-index" 190 | checksum = "4ac6a0c7b1a9e9a5186361f67dfa1b88213572f427fb9ab038efb2bd8c582dab" 191 | dependencies = [ 192 | "heck", 193 | "proc-macro2", 194 | "quote", 195 | "syn", 196 | ] 197 | 198 | [[package]] 199 | name = "clap_lex" 200 | version = "0.7.3" 201 | source = "registry+https://github.com/rust-lang/crates.io-index" 202 | checksum = "afb84c814227b90d6895e01398aee0d8033c00e7466aca416fb6a8e0eb19d8a7" 203 | 204 | [[package]] 205 | name = "colorchoice" 206 | version = "1.0.3" 207 | source = "registry+https://github.com/rust-lang/crates.io-index" 208 | checksum = "5b63caa9aa9397e2d9480a9b13673856c78d8ac123288526c37d7839f2a86990" 209 | 210 | [[package]] 211 | name = "core-foundation" 212 | version = "0.9.4" 213 | source = "registry+https://github.com/rust-lang/crates.io-index" 214 | checksum = "91e195e091a93c46f7102ec7818a2aa394e1e1771c3ab4825963fa03e45afb8f" 215 | dependencies = [ 216 | "core-foundation-sys", 217 | "libc", 218 | ] 219 | 220 | [[package]] 221 | name = "core-foundation-sys" 222 | version = "0.8.7" 223 | source = "registry+https://github.com/rust-lang/crates.io-index" 224 | checksum = "773648b94d0e5d620f64f280777445740e61fe701025087ec8b57f45c791888b" 225 | 226 | [[package]] 227 | name = "crossterm" 228 | version = "0.26.1" 229 | source = "registry+https://github.com/rust-lang/crates.io-index" 230 | checksum = "a84cda67535339806297f1b331d6dd6320470d2a0fe65381e79ee9e156dd3d13" 231 | dependencies = [ 232 | "bitflags 1.3.2", 233 | "crossterm_winapi", 234 | "libc", 235 | "mio 0.8.11", 236 | "parking_lot", 237 | "signal-hook", 238 | "signal-hook-mio", 239 | "winapi", 240 | ] 241 | 242 | [[package]] 243 | name = "crossterm_winapi" 244 | version = "0.9.1" 245 | source = "registry+https://github.com/rust-lang/crates.io-index" 246 | checksum = "acdd7c62a3665c7f6830a51635d9ac9b23ed385797f70a83bb8bafe9c572ab2b" 247 | dependencies = [ 248 | "winapi", 249 | ] 250 | 251 | [[package]] 252 | name = "displaydoc" 253 | version = "0.2.5" 254 | source = "registry+https://github.com/rust-lang/crates.io-index" 255 | checksum = "97369cbbc041bc366949bc74d34658d6cda5621039731c6310521892a3a20ae0" 256 | dependencies = [ 257 | "proc-macro2", 258 | "quote", 259 | "syn", 260 | ] 261 | 262 | [[package]] 263 | name = "encoding_rs" 264 | version = "0.8.35" 265 | source = "registry+https://github.com/rust-lang/crates.io-index" 266 | checksum = "75030f3c4f45dafd7586dd6780965a8c7e8e285a5ecb86713e63a79c5b2766f3" 267 | dependencies = [ 268 | "cfg-if", 269 | ] 270 | 271 | [[package]] 272 | name = "equivalent" 273 | version = "1.0.1" 274 | source = "registry+https://github.com/rust-lang/crates.io-index" 275 | checksum = "5443807d6dff69373d433ab9ef5378ad8df50ca6298caf15de6e52e24aaf54d5" 276 | 277 | [[package]] 278 | name = "errno" 279 | version = "0.3.10" 280 | source = "registry+https://github.com/rust-lang/crates.io-index" 281 | checksum = "33d852cb9b869c2a9b3df2f71a3074817f01e1844f839a144f5fcef059a4eb5d" 282 | dependencies = [ 283 | "libc", 284 | "windows-sys 0.59.0", 285 | ] 286 | 287 | [[package]] 288 | name = "fastrand" 289 | version = "2.2.0" 290 | source = "registry+https://github.com/rust-lang/crates.io-index" 291 | checksum = "486f806e73c5707928240ddc295403b1b93c96a02038563881c4a2fd84b81ac4" 292 | 293 | [[package]] 294 | name = "fnv" 295 | version = "1.0.7" 296 | source = "registry+https://github.com/rust-lang/crates.io-index" 297 | checksum = "3f9eec918d3f24069decb9af1554cad7c880e2da24a9afd88aca000531ab82c1" 298 | 299 | [[package]] 300 | name = "foreign-types" 301 | version = "0.3.2" 302 | source = "registry+https://github.com/rust-lang/crates.io-index" 303 | checksum = "f6f339eb8adc052cd2ca78910fda869aefa38d22d5cb648e6485e4d3fc06f3b1" 304 | dependencies = [ 305 | "foreign-types-shared", 306 | ] 307 | 308 | [[package]] 309 | name = "foreign-types-shared" 310 | version = "0.1.1" 311 | source = "registry+https://github.com/rust-lang/crates.io-index" 312 | checksum = "00b0228411908ca8685dba7fc2cdd70ec9990a6e753e89b6ac91a84c40fbaf4b" 313 | 314 | [[package]] 315 | name = "form_urlencoded" 316 | version = "1.2.1" 317 | source = "registry+https://github.com/rust-lang/crates.io-index" 318 | checksum = "e13624c2627564efccf4934284bdd98cbaa14e79b0b5a141218e507b3a823456" 319 | dependencies = [ 320 | "percent-encoding", 321 | ] 322 | 323 | [[package]] 324 | name = "futures-channel" 325 | version = "0.3.31" 326 | source = "registry+https://github.com/rust-lang/crates.io-index" 327 | checksum = "2dff15bf788c671c1934e366d07e30c1814a8ef514e1af724a602e8a2fbe1b10" 328 | dependencies = [ 329 | "futures-core", 330 | ] 331 | 332 | [[package]] 333 | name = "futures-core" 334 | version = "0.3.31" 335 | source = "registry+https://github.com/rust-lang/crates.io-index" 336 | checksum = "05f29059c0c2090612e8d742178b0580d2dc940c837851ad723096f87af6663e" 337 | 338 | [[package]] 339 | name = "futures-sink" 340 | version = "0.3.31" 341 | source = "registry+https://github.com/rust-lang/crates.io-index" 342 | checksum = "e575fab7d1e0dcb8d0c7bcf9a63ee213816ab51902e6d244a95819acacf1d4f7" 343 | 344 | [[package]] 345 | name = "futures-task" 346 | version = "0.3.31" 347 | source = "registry+https://github.com/rust-lang/crates.io-index" 348 | checksum = "f90f7dce0722e95104fcb095585910c0977252f286e354b5e3bd38902cd99988" 349 | 350 | [[package]] 351 | name = "futures-util" 352 | version = "0.3.31" 353 | source = "registry+https://github.com/rust-lang/crates.io-index" 354 | checksum = "9fa08315bb612088cc391249efdc3bc77536f16c91f6cf495e6fbe85b20a4a81" 355 | dependencies = [ 356 | "futures-core", 357 | "futures-task", 358 | "pin-project-lite", 359 | "pin-utils", 360 | ] 361 | 362 | [[package]] 363 | name = "gimli" 364 | version = "0.31.1" 365 | source = "registry+https://github.com/rust-lang/crates.io-index" 366 | checksum = "07e28edb80900c19c28f1072f2e8aeca7fa06b23cd4169cefe1af5aa3260783f" 367 | 368 | [[package]] 369 | name = "h2" 370 | version = "0.3.26" 371 | source = "registry+https://github.com/rust-lang/crates.io-index" 372 | checksum = "81fe527a889e1532da5c525686d96d4c2e74cdd345badf8dfef9f6b39dd5f5e8" 373 | dependencies = [ 374 | "bytes", 375 | "fnv", 376 | "futures-core", 377 | "futures-sink", 378 | "futures-util", 379 | "http", 380 | "indexmap", 381 | "slab", 382 | "tokio", 383 | "tokio-util", 384 | "tracing", 385 | ] 386 | 387 | [[package]] 388 | name = "hashbrown" 389 | version = "0.15.2" 390 | source = "registry+https://github.com/rust-lang/crates.io-index" 391 | checksum = "bf151400ff0baff5465007dd2f3e717f3fe502074ca563069ce3a6629d07b289" 392 | 393 | [[package]] 394 | name = "heck" 395 | version = "0.5.0" 396 | source = "registry+https://github.com/rust-lang/crates.io-index" 397 | checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea" 398 | 399 | [[package]] 400 | name = "home" 401 | version = "0.5.9" 402 | source = "registry+https://github.com/rust-lang/crates.io-index" 403 | checksum = "e3d1354bf6b7235cb4a0576c2619fd4ed18183f689b12b006a0ee7329eeff9a5" 404 | dependencies = [ 405 | "windows-sys 0.52.0", 406 | ] 407 | 408 | [[package]] 409 | name = "http" 410 | version = "0.2.12" 411 | source = "registry+https://github.com/rust-lang/crates.io-index" 412 | checksum = "601cbb57e577e2f5ef5be8e7b83f0f63994f25aa94d673e54a92d5c516d101f1" 413 | dependencies = [ 414 | "bytes", 415 | "fnv", 416 | "itoa", 417 | ] 418 | 419 | [[package]] 420 | name = "http-body" 421 | version = "0.4.6" 422 | source = "registry+https://github.com/rust-lang/crates.io-index" 423 | checksum = "7ceab25649e9960c0311ea418d17bee82c0dcec1bd053b5f9a66e265a693bed2" 424 | dependencies = [ 425 | "bytes", 426 | "http", 427 | "pin-project-lite", 428 | ] 429 | 430 | [[package]] 431 | name = "httparse" 432 | version = "1.9.5" 433 | source = "registry+https://github.com/rust-lang/crates.io-index" 434 | checksum = "7d71d3574edd2771538b901e6549113b4006ece66150fb69c0fb6d9a2adae946" 435 | 436 | [[package]] 437 | name = "httpdate" 438 | version = "1.0.3" 439 | source = "registry+https://github.com/rust-lang/crates.io-index" 440 | checksum = "df3b46402a9d5adb4c86a0cf463f42e19994e3ee891101b1841f30a545cb49a9" 441 | 442 | [[package]] 443 | name = "human-sort" 444 | version = "0.2.2" 445 | source = "registry+https://github.com/rust-lang/crates.io-index" 446 | checksum = "140a09c9305e6d5e557e2ed7cbc68e05765a7d4213975b87cb04920689cc6219" 447 | 448 | [[package]] 449 | name = "hyper" 450 | version = "0.14.31" 451 | source = "registry+https://github.com/rust-lang/crates.io-index" 452 | checksum = "8c08302e8fa335b151b788c775ff56e7a03ae64ff85c548ee820fecb70356e85" 453 | dependencies = [ 454 | "bytes", 455 | "futures-channel", 456 | "futures-core", 457 | "futures-util", 458 | "h2", 459 | "http", 460 | "http-body", 461 | "httparse", 462 | "httpdate", 463 | "itoa", 464 | "pin-project-lite", 465 | "socket2", 466 | "tokio", 467 | "tower-service", 468 | "tracing", 469 | "want", 470 | ] 471 | 472 | [[package]] 473 | name = "hyper-tls" 474 | version = "0.5.0" 475 | source = "registry+https://github.com/rust-lang/crates.io-index" 476 | checksum = "d6183ddfa99b85da61a140bea0efc93fdf56ceaa041b37d553518030827f9905" 477 | dependencies = [ 478 | "bytes", 479 | "hyper", 480 | "native-tls", 481 | "tokio", 482 | "tokio-native-tls", 483 | ] 484 | 485 | [[package]] 486 | name = "iana-time-zone" 487 | version = "0.1.61" 488 | source = "registry+https://github.com/rust-lang/crates.io-index" 489 | checksum = "235e081f3925a06703c2d0117ea8b91f042756fd6e7a6e5d901e8ca1a996b220" 490 | dependencies = [ 491 | "android_system_properties", 492 | "core-foundation-sys", 493 | "iana-time-zone-haiku", 494 | "js-sys", 495 | "wasm-bindgen", 496 | "windows-core", 497 | ] 498 | 499 | [[package]] 500 | name = "iana-time-zone-haiku" 501 | version = "0.1.2" 502 | source = "registry+https://github.com/rust-lang/crates.io-index" 503 | checksum = "f31827a206f56af32e590ba56d5d2d085f558508192593743f16b2306495269f" 504 | dependencies = [ 505 | "cc", 506 | ] 507 | 508 | [[package]] 509 | name = "icu_collections" 510 | version = "1.5.0" 511 | source = "registry+https://github.com/rust-lang/crates.io-index" 512 | checksum = "db2fa452206ebee18c4b5c2274dbf1de17008e874b4dc4f0aea9d01ca79e4526" 513 | dependencies = [ 514 | "displaydoc", 515 | "yoke", 516 | "zerofrom", 517 | "zerovec", 518 | ] 519 | 520 | [[package]] 521 | name = "icu_locid" 522 | version = "1.5.0" 523 | source = "registry+https://github.com/rust-lang/crates.io-index" 524 | checksum = "13acbb8371917fc971be86fc8057c41a64b521c184808a698c02acc242dbf637" 525 | dependencies = [ 526 | "displaydoc", 527 | "litemap", 528 | "tinystr", 529 | "writeable", 530 | "zerovec", 531 | ] 532 | 533 | [[package]] 534 | name = "icu_locid_transform" 535 | version = "1.5.0" 536 | source = "registry+https://github.com/rust-lang/crates.io-index" 537 | checksum = "01d11ac35de8e40fdeda00d9e1e9d92525f3f9d887cdd7aa81d727596788b54e" 538 | dependencies = [ 539 | "displaydoc", 540 | "icu_locid", 541 | "icu_locid_transform_data", 542 | "icu_provider", 543 | "tinystr", 544 | "zerovec", 545 | ] 546 | 547 | [[package]] 548 | name = "icu_locid_transform_data" 549 | version = "1.5.0" 550 | source = "registry+https://github.com/rust-lang/crates.io-index" 551 | checksum = "fdc8ff3388f852bede6b579ad4e978ab004f139284d7b28715f773507b946f6e" 552 | 553 | [[package]] 554 | name = "icu_normalizer" 555 | version = "1.5.0" 556 | source = "registry+https://github.com/rust-lang/crates.io-index" 557 | checksum = "19ce3e0da2ec68599d193c93d088142efd7f9c5d6fc9b803774855747dc6a84f" 558 | dependencies = [ 559 | "displaydoc", 560 | "icu_collections", 561 | "icu_normalizer_data", 562 | "icu_properties", 563 | "icu_provider", 564 | "smallvec", 565 | "utf16_iter", 566 | "utf8_iter", 567 | "write16", 568 | "zerovec", 569 | ] 570 | 571 | [[package]] 572 | name = "icu_normalizer_data" 573 | version = "1.5.0" 574 | source = "registry+https://github.com/rust-lang/crates.io-index" 575 | checksum = "f8cafbf7aa791e9b22bec55a167906f9e1215fd475cd22adfcf660e03e989516" 576 | 577 | [[package]] 578 | name = "icu_properties" 579 | version = "1.5.1" 580 | source = "registry+https://github.com/rust-lang/crates.io-index" 581 | checksum = "93d6020766cfc6302c15dbbc9c8778c37e62c14427cb7f6e601d849e092aeef5" 582 | dependencies = [ 583 | "displaydoc", 584 | "icu_collections", 585 | "icu_locid_transform", 586 | "icu_properties_data", 587 | "icu_provider", 588 | "tinystr", 589 | "zerovec", 590 | ] 591 | 592 | [[package]] 593 | name = "icu_properties_data" 594 | version = "1.5.0" 595 | source = "registry+https://github.com/rust-lang/crates.io-index" 596 | checksum = "67a8effbc3dd3e4ba1afa8ad918d5684b8868b3b26500753effea8d2eed19569" 597 | 598 | [[package]] 599 | name = "icu_provider" 600 | version = "1.5.0" 601 | source = "registry+https://github.com/rust-lang/crates.io-index" 602 | checksum = "6ed421c8a8ef78d3e2dbc98a973be2f3770cb42b606e3ab18d6237c4dfde68d9" 603 | dependencies = [ 604 | "displaydoc", 605 | "icu_locid", 606 | "icu_provider_macros", 607 | "stable_deref_trait", 608 | "tinystr", 609 | "writeable", 610 | "yoke", 611 | "zerofrom", 612 | "zerovec", 613 | ] 614 | 615 | [[package]] 616 | name = "icu_provider_macros" 617 | version = "1.5.0" 618 | source = "registry+https://github.com/rust-lang/crates.io-index" 619 | checksum = "1ec89e9337638ecdc08744df490b221a7399bf8d164eb52a665454e60e075ad6" 620 | dependencies = [ 621 | "proc-macro2", 622 | "quote", 623 | "syn", 624 | ] 625 | 626 | [[package]] 627 | name = "idna" 628 | version = "1.0.3" 629 | source = "registry+https://github.com/rust-lang/crates.io-index" 630 | checksum = "686f825264d630750a544639377bae737628043f20d38bbc029e8f29ea968a7e" 631 | dependencies = [ 632 | "idna_adapter", 633 | "smallvec", 634 | "utf8_iter", 635 | ] 636 | 637 | [[package]] 638 | name = "idna_adapter" 639 | version = "1.2.0" 640 | source = "registry+https://github.com/rust-lang/crates.io-index" 641 | checksum = "daca1df1c957320b2cf139ac61e7bd64fed304c5040df000a745aa1de3b4ef71" 642 | dependencies = [ 643 | "icu_normalizer", 644 | "icu_properties", 645 | ] 646 | 647 | [[package]] 648 | name = "indexmap" 649 | version = "2.7.0" 650 | source = "registry+https://github.com/rust-lang/crates.io-index" 651 | checksum = "62f822373a4fe84d4bb149bf54e584a7f4abec90e072ed49cda0edea5b95471f" 652 | dependencies = [ 653 | "equivalent", 654 | "hashbrown", 655 | ] 656 | 657 | [[package]] 658 | name = "ipnet" 659 | version = "2.10.1" 660 | source = "registry+https://github.com/rust-lang/crates.io-index" 661 | checksum = "ddc24109865250148c2e0f3d25d4f0f479571723792d3802153c60922a4fb708" 662 | 663 | [[package]] 664 | name = "is_terminal_polyfill" 665 | version = "1.70.1" 666 | source = "registry+https://github.com/rust-lang/crates.io-index" 667 | checksum = "7943c866cc5cd64cbc25b2e01621d07fa8eb2a1a23160ee81ce38704e97b8ecf" 668 | 669 | [[package]] 670 | name = "itoa" 671 | version = "1.0.14" 672 | source = "registry+https://github.com/rust-lang/crates.io-index" 673 | checksum = "d75a2a4b1b190afb6f5425f10f6a8f959d2ea0b9c2b1d79553551850539e4674" 674 | 675 | [[package]] 676 | name = "js-sys" 677 | version = "0.3.74" 678 | source = "registry+https://github.com/rust-lang/crates.io-index" 679 | checksum = "a865e038f7f6ed956f788f0d7d60c541fff74c7bd74272c5d4cf15c63743e705" 680 | dependencies = [ 681 | "once_cell", 682 | "wasm-bindgen", 683 | ] 684 | 685 | [[package]] 686 | name = "l1t" 687 | version = "0.2.1" 688 | dependencies = [ 689 | "chrono", 690 | "clap", 691 | "crossterm", 692 | "home", 693 | "human-sort", 694 | "reqwest", 695 | "serde", 696 | "serde_json", 697 | "tokio", 698 | ] 699 | 700 | [[package]] 701 | name = "libc" 702 | version = "0.2.167" 703 | source = "registry+https://github.com/rust-lang/crates.io-index" 704 | checksum = "09d6582e104315a817dff97f75133544b2e094ee22447d2acf4a74e189ba06fc" 705 | 706 | [[package]] 707 | name = "linux-raw-sys" 708 | version = "0.4.14" 709 | source = "registry+https://github.com/rust-lang/crates.io-index" 710 | checksum = "78b3ae25bc7c8c38cec158d1f2757ee79e9b3740fbc7ccf0e59e4b08d793fa89" 711 | 712 | [[package]] 713 | name = "litemap" 714 | version = "0.7.4" 715 | source = "registry+https://github.com/rust-lang/crates.io-index" 716 | checksum = "4ee93343901ab17bd981295f2cf0026d4ad018c7c31ba84549a4ddbb47a45104" 717 | 718 | [[package]] 719 | name = "lock_api" 720 | version = "0.4.12" 721 | source = "registry+https://github.com/rust-lang/crates.io-index" 722 | checksum = "07af8b9cdd281b7915f413fa73f29ebd5d55d0d3f0155584dade1ff18cea1b17" 723 | dependencies = [ 724 | "autocfg", 725 | "scopeguard", 726 | ] 727 | 728 | [[package]] 729 | name = "log" 730 | version = "0.4.22" 731 | source = "registry+https://github.com/rust-lang/crates.io-index" 732 | checksum = "a7a70ba024b9dc04c27ea2f0c0548feb474ec5c54bba33a7f72f873a39d07b24" 733 | 734 | [[package]] 735 | name = "memchr" 736 | version = "2.7.4" 737 | source = "registry+https://github.com/rust-lang/crates.io-index" 738 | checksum = "78ca9ab1a0babb1e7d5695e3530886289c18cf2f87ec19a575a0abdce112e3a3" 739 | 740 | [[package]] 741 | name = "mime" 742 | version = "0.3.17" 743 | source = "registry+https://github.com/rust-lang/crates.io-index" 744 | checksum = "6877bb514081ee2a7ff5ef9de3281f14a4dd4bceac4c09388074a6b5df8a139a" 745 | 746 | [[package]] 747 | name = "miniz_oxide" 748 | version = "0.8.0" 749 | source = "registry+https://github.com/rust-lang/crates.io-index" 750 | checksum = "e2d80299ef12ff69b16a84bb182e3b9df68b5a91574d3d4fa6e41b65deec4df1" 751 | dependencies = [ 752 | "adler2", 753 | ] 754 | 755 | [[package]] 756 | name = "mio" 757 | version = "0.8.11" 758 | source = "registry+https://github.com/rust-lang/crates.io-index" 759 | checksum = "a4a650543ca06a924e8b371db273b2756685faae30f8487da1b56505a8f78b0c" 760 | dependencies = [ 761 | "libc", 762 | "log", 763 | "wasi", 764 | "windows-sys 0.48.0", 765 | ] 766 | 767 | [[package]] 768 | name = "mio" 769 | version = "1.0.3" 770 | source = "registry+https://github.com/rust-lang/crates.io-index" 771 | checksum = "2886843bf800fba2e3377cff24abf6379b4c4d5c6681eaf9ea5b0d15090450bd" 772 | dependencies = [ 773 | "libc", 774 | "wasi", 775 | "windows-sys 0.52.0", 776 | ] 777 | 778 | [[package]] 779 | name = "native-tls" 780 | version = "0.2.12" 781 | source = "registry+https://github.com/rust-lang/crates.io-index" 782 | checksum = "a8614eb2c83d59d1c8cc974dd3f920198647674a0a035e1af1fa58707e317466" 783 | dependencies = [ 784 | "libc", 785 | "log", 786 | "openssl", 787 | "openssl-probe", 788 | "openssl-sys", 789 | "schannel", 790 | "security-framework", 791 | "security-framework-sys", 792 | "tempfile", 793 | ] 794 | 795 | [[package]] 796 | name = "num-traits" 797 | version = "0.2.19" 798 | source = "registry+https://github.com/rust-lang/crates.io-index" 799 | checksum = "071dfc062690e90b734c0b2273ce72ad0ffa95f0c74596bc250dcfd960262841" 800 | dependencies = [ 801 | "autocfg", 802 | ] 803 | 804 | [[package]] 805 | name = "object" 806 | version = "0.36.5" 807 | source = "registry+https://github.com/rust-lang/crates.io-index" 808 | checksum = "aedf0a2d09c573ed1d8d85b30c119153926a2b36dce0ab28322c09a117a4683e" 809 | dependencies = [ 810 | "memchr", 811 | ] 812 | 813 | [[package]] 814 | name = "once_cell" 815 | version = "1.20.2" 816 | source = "registry+https://github.com/rust-lang/crates.io-index" 817 | checksum = "1261fe7e33c73b354eab43b1273a57c8f967d0391e80353e51f764ac02cf6775" 818 | 819 | [[package]] 820 | name = "openssl" 821 | version = "0.10.68" 822 | source = "registry+https://github.com/rust-lang/crates.io-index" 823 | checksum = "6174bc48f102d208783c2c84bf931bb75927a617866870de8a4ea85597f871f5" 824 | dependencies = [ 825 | "bitflags 2.6.0", 826 | "cfg-if", 827 | "foreign-types", 828 | "libc", 829 | "once_cell", 830 | "openssl-macros", 831 | "openssl-sys", 832 | ] 833 | 834 | [[package]] 835 | name = "openssl-macros" 836 | version = "0.1.1" 837 | source = "registry+https://github.com/rust-lang/crates.io-index" 838 | checksum = "a948666b637a0f465e8564c73e89d4dde00d72d4d473cc972f390fc3dcee7d9c" 839 | dependencies = [ 840 | "proc-macro2", 841 | "quote", 842 | "syn", 843 | ] 844 | 845 | [[package]] 846 | name = "openssl-probe" 847 | version = "0.1.5" 848 | source = "registry+https://github.com/rust-lang/crates.io-index" 849 | checksum = "ff011a302c396a5197692431fc1948019154afc178baf7d8e37367442a4601cf" 850 | 851 | [[package]] 852 | name = "openssl-sys" 853 | version = "0.9.104" 854 | source = "registry+https://github.com/rust-lang/crates.io-index" 855 | checksum = "45abf306cbf99debc8195b66b7346498d7b10c210de50418b5ccd7ceba08c741" 856 | dependencies = [ 857 | "cc", 858 | "libc", 859 | "pkg-config", 860 | "vcpkg", 861 | ] 862 | 863 | [[package]] 864 | name = "parking_lot" 865 | version = "0.12.3" 866 | source = "registry+https://github.com/rust-lang/crates.io-index" 867 | checksum = "f1bf18183cf54e8d6059647fc3063646a1801cf30896933ec2311622cc4b9a27" 868 | dependencies = [ 869 | "lock_api", 870 | "parking_lot_core", 871 | ] 872 | 873 | [[package]] 874 | name = "parking_lot_core" 875 | version = "0.9.10" 876 | source = "registry+https://github.com/rust-lang/crates.io-index" 877 | checksum = "1e401f977ab385c9e4e3ab30627d6f26d00e2c73eef317493c4ec6d468726cf8" 878 | dependencies = [ 879 | "cfg-if", 880 | "libc", 881 | "redox_syscall", 882 | "smallvec", 883 | "windows-targets 0.52.6", 884 | ] 885 | 886 | [[package]] 887 | name = "percent-encoding" 888 | version = "2.3.1" 889 | source = "registry+https://github.com/rust-lang/crates.io-index" 890 | checksum = "e3148f5046208a5d56bcfc03053e3ca6334e51da8dfb19b6cdc8b306fae3283e" 891 | 892 | [[package]] 893 | name = "pin-project-lite" 894 | version = "0.2.15" 895 | source = "registry+https://github.com/rust-lang/crates.io-index" 896 | checksum = "915a1e146535de9163f3987b8944ed8cf49a18bb0056bcebcdcece385cece4ff" 897 | 898 | [[package]] 899 | name = "pin-utils" 900 | version = "0.1.0" 901 | source = "registry+https://github.com/rust-lang/crates.io-index" 902 | checksum = "8b870d8c151b6f2fb93e84a13146138f05d02ed11c7e7c54f8826aaaf7c9f184" 903 | 904 | [[package]] 905 | name = "pkg-config" 906 | version = "0.3.31" 907 | source = "registry+https://github.com/rust-lang/crates.io-index" 908 | checksum = "953ec861398dccce10c670dfeaf3ec4911ca479e9c02154b3a215178c5f566f2" 909 | 910 | [[package]] 911 | name = "proc-macro2" 912 | version = "1.0.92" 913 | source = "registry+https://github.com/rust-lang/crates.io-index" 914 | checksum = "37d3544b3f2748c54e147655edb5025752e2303145b5aefb3c3ea2c78b973bb0" 915 | dependencies = [ 916 | "unicode-ident", 917 | ] 918 | 919 | [[package]] 920 | name = "quote" 921 | version = "1.0.37" 922 | source = "registry+https://github.com/rust-lang/crates.io-index" 923 | checksum = "b5b9d34b8991d19d98081b46eacdd8eb58c6f2b201139f7c5f643cc155a633af" 924 | dependencies = [ 925 | "proc-macro2", 926 | ] 927 | 928 | [[package]] 929 | name = "redox_syscall" 930 | version = "0.5.7" 931 | source = "registry+https://github.com/rust-lang/crates.io-index" 932 | checksum = "9b6dfecf2c74bce2466cabf93f6664d6998a69eb21e39f4207930065b27b771f" 933 | dependencies = [ 934 | "bitflags 2.6.0", 935 | ] 936 | 937 | [[package]] 938 | name = "reqwest" 939 | version = "0.11.27" 940 | source = "registry+https://github.com/rust-lang/crates.io-index" 941 | checksum = "dd67538700a17451e7cba03ac727fb961abb7607553461627b97de0b89cf4a62" 942 | dependencies = [ 943 | "base64", 944 | "bytes", 945 | "encoding_rs", 946 | "futures-core", 947 | "futures-util", 948 | "h2", 949 | "http", 950 | "http-body", 951 | "hyper", 952 | "hyper-tls", 953 | "ipnet", 954 | "js-sys", 955 | "log", 956 | "mime", 957 | "native-tls", 958 | "once_cell", 959 | "percent-encoding", 960 | "pin-project-lite", 961 | "rustls-pemfile", 962 | "serde", 963 | "serde_json", 964 | "serde_urlencoded", 965 | "sync_wrapper", 966 | "system-configuration", 967 | "tokio", 968 | "tokio-native-tls", 969 | "tower-service", 970 | "url", 971 | "wasm-bindgen", 972 | "wasm-bindgen-futures", 973 | "web-sys", 974 | "winreg", 975 | ] 976 | 977 | [[package]] 978 | name = "rustc-demangle" 979 | version = "0.1.24" 980 | source = "registry+https://github.com/rust-lang/crates.io-index" 981 | checksum = "719b953e2095829ee67db738b3bfa9fa368c94900df327b3f07fe6e794d2fe1f" 982 | 983 | [[package]] 984 | name = "rustix" 985 | version = "0.38.41" 986 | source = "registry+https://github.com/rust-lang/crates.io-index" 987 | checksum = "d7f649912bc1495e167a6edee79151c84b1bad49748cb4f1f1167f459f6224f6" 988 | dependencies = [ 989 | "bitflags 2.6.0", 990 | "errno", 991 | "libc", 992 | "linux-raw-sys", 993 | "windows-sys 0.52.0", 994 | ] 995 | 996 | [[package]] 997 | name = "rustls-pemfile" 998 | version = "1.0.4" 999 | source = "registry+https://github.com/rust-lang/crates.io-index" 1000 | checksum = "1c74cae0a4cf6ccbbf5f359f08efdf8ee7e1dc532573bf0db71968cb56b1448c" 1001 | dependencies = [ 1002 | "base64", 1003 | ] 1004 | 1005 | [[package]] 1006 | name = "ryu" 1007 | version = "1.0.18" 1008 | source = "registry+https://github.com/rust-lang/crates.io-index" 1009 | checksum = "f3cb5ba0dc43242ce17de99c180e96db90b235b8a9fdc9543c96d2209116bd9f" 1010 | 1011 | [[package]] 1012 | name = "schannel" 1013 | version = "0.1.27" 1014 | source = "registry+https://github.com/rust-lang/crates.io-index" 1015 | checksum = "1f29ebaa345f945cec9fbbc532eb307f0fdad8161f281b6369539c8d84876b3d" 1016 | dependencies = [ 1017 | "windows-sys 0.59.0", 1018 | ] 1019 | 1020 | [[package]] 1021 | name = "scopeguard" 1022 | version = "1.2.0" 1023 | source = "registry+https://github.com/rust-lang/crates.io-index" 1024 | checksum = "94143f37725109f92c262ed2cf5e59bce7498c01bcc1502d7b9afe439a4e9f49" 1025 | 1026 | [[package]] 1027 | name = "security-framework" 1028 | version = "2.11.1" 1029 | source = "registry+https://github.com/rust-lang/crates.io-index" 1030 | checksum = "897b2245f0b511c87893af39b033e5ca9cce68824c4d7e7630b5a1d339658d02" 1031 | dependencies = [ 1032 | "bitflags 2.6.0", 1033 | "core-foundation", 1034 | "core-foundation-sys", 1035 | "libc", 1036 | "security-framework-sys", 1037 | ] 1038 | 1039 | [[package]] 1040 | name = "security-framework-sys" 1041 | version = "2.12.1" 1042 | source = "registry+https://github.com/rust-lang/crates.io-index" 1043 | checksum = "fa39c7303dc58b5543c94d22c1766b0d31f2ee58306363ea622b10bbc075eaa2" 1044 | dependencies = [ 1045 | "core-foundation-sys", 1046 | "libc", 1047 | ] 1048 | 1049 | [[package]] 1050 | name = "serde" 1051 | version = "1.0.215" 1052 | source = "registry+https://github.com/rust-lang/crates.io-index" 1053 | checksum = "6513c1ad0b11a9376da888e3e0baa0077f1aed55c17f50e7b2397136129fb88f" 1054 | dependencies = [ 1055 | "serde_derive", 1056 | ] 1057 | 1058 | [[package]] 1059 | name = "serde_derive" 1060 | version = "1.0.215" 1061 | source = "registry+https://github.com/rust-lang/crates.io-index" 1062 | checksum = "ad1e866f866923f252f05c889987993144fb74e722403468a4ebd70c3cd756c0" 1063 | dependencies = [ 1064 | "proc-macro2", 1065 | "quote", 1066 | "syn", 1067 | ] 1068 | 1069 | [[package]] 1070 | name = "serde_json" 1071 | version = "1.0.133" 1072 | source = "registry+https://github.com/rust-lang/crates.io-index" 1073 | checksum = "c7fceb2473b9166b2294ef05efcb65a3db80803f0b03ef86a5fc88a2b85ee377" 1074 | dependencies = [ 1075 | "itoa", 1076 | "memchr", 1077 | "ryu", 1078 | "serde", 1079 | ] 1080 | 1081 | [[package]] 1082 | name = "serde_urlencoded" 1083 | version = "0.7.1" 1084 | source = "registry+https://github.com/rust-lang/crates.io-index" 1085 | checksum = "d3491c14715ca2294c4d6a88f15e84739788c1d030eed8c110436aafdaa2f3fd" 1086 | dependencies = [ 1087 | "form_urlencoded", 1088 | "itoa", 1089 | "ryu", 1090 | "serde", 1091 | ] 1092 | 1093 | [[package]] 1094 | name = "shlex" 1095 | version = "1.3.0" 1096 | source = "registry+https://github.com/rust-lang/crates.io-index" 1097 | checksum = "0fda2ff0d084019ba4d7c6f371c95d8fd75ce3524c3cb8fb653a3023f6323e64" 1098 | 1099 | [[package]] 1100 | name = "signal-hook" 1101 | version = "0.3.17" 1102 | source = "registry+https://github.com/rust-lang/crates.io-index" 1103 | checksum = "8621587d4798caf8eb44879d42e56b9a93ea5dcd315a6487c357130095b62801" 1104 | dependencies = [ 1105 | "libc", 1106 | "signal-hook-registry", 1107 | ] 1108 | 1109 | [[package]] 1110 | name = "signal-hook-mio" 1111 | version = "0.2.4" 1112 | source = "registry+https://github.com/rust-lang/crates.io-index" 1113 | checksum = "34db1a06d485c9142248b7a054f034b349b212551f3dfd19c94d45a754a217cd" 1114 | dependencies = [ 1115 | "libc", 1116 | "mio 0.8.11", 1117 | "signal-hook", 1118 | ] 1119 | 1120 | [[package]] 1121 | name = "signal-hook-registry" 1122 | version = "1.4.2" 1123 | source = "registry+https://github.com/rust-lang/crates.io-index" 1124 | checksum = "a9e9e0b4211b72e7b8b6e85c807d36c212bdb33ea8587f7569562a84df5465b1" 1125 | dependencies = [ 1126 | "libc", 1127 | ] 1128 | 1129 | [[package]] 1130 | name = "slab" 1131 | version = "0.4.9" 1132 | source = "registry+https://github.com/rust-lang/crates.io-index" 1133 | checksum = "8f92a496fb766b417c996b9c5e57daf2f7ad3b0bebe1ccfca4856390e3d3bb67" 1134 | dependencies = [ 1135 | "autocfg", 1136 | ] 1137 | 1138 | [[package]] 1139 | name = "smallvec" 1140 | version = "1.13.2" 1141 | source = "registry+https://github.com/rust-lang/crates.io-index" 1142 | checksum = "3c5e1a9a646d36c3599cd173a41282daf47c44583ad367b8e6837255952e5c67" 1143 | 1144 | [[package]] 1145 | name = "socket2" 1146 | version = "0.5.8" 1147 | source = "registry+https://github.com/rust-lang/crates.io-index" 1148 | checksum = "c970269d99b64e60ec3bd6ad27270092a5394c4e309314b18ae3fe575695fbe8" 1149 | dependencies = [ 1150 | "libc", 1151 | "windows-sys 0.52.0", 1152 | ] 1153 | 1154 | [[package]] 1155 | name = "stable_deref_trait" 1156 | version = "1.2.0" 1157 | source = "registry+https://github.com/rust-lang/crates.io-index" 1158 | checksum = "a8f112729512f8e442d81f95a8a7ddf2b7c6b8a1a6f509a95864142b30cab2d3" 1159 | 1160 | [[package]] 1161 | name = "strsim" 1162 | version = "0.11.1" 1163 | source = "registry+https://github.com/rust-lang/crates.io-index" 1164 | checksum = "7da8b5736845d9f2fcb837ea5d9e2628564b3b043a70948a3f0b778838c5fb4f" 1165 | 1166 | [[package]] 1167 | name = "syn" 1168 | version = "2.0.90" 1169 | source = "registry+https://github.com/rust-lang/crates.io-index" 1170 | checksum = "919d3b74a5dd0ccd15aeb8f93e7006bd9e14c295087c9896a110f490752bcf31" 1171 | dependencies = [ 1172 | "proc-macro2", 1173 | "quote", 1174 | "unicode-ident", 1175 | ] 1176 | 1177 | [[package]] 1178 | name = "sync_wrapper" 1179 | version = "0.1.2" 1180 | source = "registry+https://github.com/rust-lang/crates.io-index" 1181 | checksum = "2047c6ded9c721764247e62cd3b03c09ffc529b2ba5b10ec482ae507a4a70160" 1182 | 1183 | [[package]] 1184 | name = "synstructure" 1185 | version = "0.13.1" 1186 | source = "registry+https://github.com/rust-lang/crates.io-index" 1187 | checksum = "c8af7666ab7b6390ab78131fb5b0fce11d6b7a6951602017c35fa82800708971" 1188 | dependencies = [ 1189 | "proc-macro2", 1190 | "quote", 1191 | "syn", 1192 | ] 1193 | 1194 | [[package]] 1195 | name = "system-configuration" 1196 | version = "0.5.1" 1197 | source = "registry+https://github.com/rust-lang/crates.io-index" 1198 | checksum = "ba3a3adc5c275d719af8cb4272ea1c4a6d668a777f37e115f6d11ddbc1c8e0e7" 1199 | dependencies = [ 1200 | "bitflags 1.3.2", 1201 | "core-foundation", 1202 | "system-configuration-sys", 1203 | ] 1204 | 1205 | [[package]] 1206 | name = "system-configuration-sys" 1207 | version = "0.5.0" 1208 | source = "registry+https://github.com/rust-lang/crates.io-index" 1209 | checksum = "a75fb188eb626b924683e3b95e3a48e63551fcfb51949de2f06a9d91dbee93c9" 1210 | dependencies = [ 1211 | "core-foundation-sys", 1212 | "libc", 1213 | ] 1214 | 1215 | [[package]] 1216 | name = "tempfile" 1217 | version = "3.14.0" 1218 | source = "registry+https://github.com/rust-lang/crates.io-index" 1219 | checksum = "28cce251fcbc87fac86a866eeb0d6c2d536fc16d06f184bb61aeae11aa4cee0c" 1220 | dependencies = [ 1221 | "cfg-if", 1222 | "fastrand", 1223 | "once_cell", 1224 | "rustix", 1225 | "windows-sys 0.59.0", 1226 | ] 1227 | 1228 | [[package]] 1229 | name = "tinystr" 1230 | version = "0.7.6" 1231 | source = "registry+https://github.com/rust-lang/crates.io-index" 1232 | checksum = "9117f5d4db391c1cf6927e7bea3db74b9a1c1add8f7eda9ffd5364f40f57b82f" 1233 | dependencies = [ 1234 | "displaydoc", 1235 | "zerovec", 1236 | ] 1237 | 1238 | [[package]] 1239 | name = "tokio" 1240 | version = "1.42.0" 1241 | source = "registry+https://github.com/rust-lang/crates.io-index" 1242 | checksum = "5cec9b21b0450273377fc97bd4c33a8acffc8c996c987a7c5b319a0083707551" 1243 | dependencies = [ 1244 | "backtrace", 1245 | "bytes", 1246 | "libc", 1247 | "mio 1.0.3", 1248 | "parking_lot", 1249 | "pin-project-lite", 1250 | "signal-hook-registry", 1251 | "socket2", 1252 | "tokio-macros", 1253 | "windows-sys 0.52.0", 1254 | ] 1255 | 1256 | [[package]] 1257 | name = "tokio-macros" 1258 | version = "2.4.0" 1259 | source = "registry+https://github.com/rust-lang/crates.io-index" 1260 | checksum = "693d596312e88961bc67d7f1f97af8a70227d9f90c31bba5806eec004978d752" 1261 | dependencies = [ 1262 | "proc-macro2", 1263 | "quote", 1264 | "syn", 1265 | ] 1266 | 1267 | [[package]] 1268 | name = "tokio-native-tls" 1269 | version = "0.3.1" 1270 | source = "registry+https://github.com/rust-lang/crates.io-index" 1271 | checksum = "bbae76ab933c85776efabc971569dd6119c580d8f5d448769dec1764bf796ef2" 1272 | dependencies = [ 1273 | "native-tls", 1274 | "tokio", 1275 | ] 1276 | 1277 | [[package]] 1278 | name = "tokio-util" 1279 | version = "0.7.13" 1280 | source = "registry+https://github.com/rust-lang/crates.io-index" 1281 | checksum = "d7fcaa8d55a2bdd6b83ace262b016eca0d79ee02818c5c1bcdf0305114081078" 1282 | dependencies = [ 1283 | "bytes", 1284 | "futures-core", 1285 | "futures-sink", 1286 | "pin-project-lite", 1287 | "tokio", 1288 | ] 1289 | 1290 | [[package]] 1291 | name = "tower-service" 1292 | version = "0.3.3" 1293 | source = "registry+https://github.com/rust-lang/crates.io-index" 1294 | checksum = "8df9b6e13f2d32c91b9bd719c00d1958837bc7dec474d94952798cc8e69eeec3" 1295 | 1296 | [[package]] 1297 | name = "tracing" 1298 | version = "0.1.41" 1299 | source = "registry+https://github.com/rust-lang/crates.io-index" 1300 | checksum = "784e0ac535deb450455cbfa28a6f0df145ea1bb7ae51b821cf5e7927fdcfbdd0" 1301 | dependencies = [ 1302 | "pin-project-lite", 1303 | "tracing-core", 1304 | ] 1305 | 1306 | [[package]] 1307 | name = "tracing-core" 1308 | version = "0.1.33" 1309 | source = "registry+https://github.com/rust-lang/crates.io-index" 1310 | checksum = "e672c95779cf947c5311f83787af4fa8fffd12fb27e4993211a84bdfd9610f9c" 1311 | dependencies = [ 1312 | "once_cell", 1313 | ] 1314 | 1315 | [[package]] 1316 | name = "try-lock" 1317 | version = "0.2.5" 1318 | source = "registry+https://github.com/rust-lang/crates.io-index" 1319 | checksum = "e421abadd41a4225275504ea4d6566923418b7f05506fbc9c0fe86ba7396114b" 1320 | 1321 | [[package]] 1322 | name = "unicode-ident" 1323 | version = "1.0.14" 1324 | source = "registry+https://github.com/rust-lang/crates.io-index" 1325 | checksum = "adb9e6ca4f869e1180728b7950e35922a7fc6397f7b641499e8f3ef06e50dc83" 1326 | 1327 | [[package]] 1328 | name = "url" 1329 | version = "2.5.4" 1330 | source = "registry+https://github.com/rust-lang/crates.io-index" 1331 | checksum = "32f8b686cadd1473f4bd0117a5d28d36b1ade384ea9b5069a1c40aefed7fda60" 1332 | dependencies = [ 1333 | "form_urlencoded", 1334 | "idna", 1335 | "percent-encoding", 1336 | ] 1337 | 1338 | [[package]] 1339 | name = "utf16_iter" 1340 | version = "1.0.5" 1341 | source = "registry+https://github.com/rust-lang/crates.io-index" 1342 | checksum = "c8232dd3cdaed5356e0f716d285e4b40b932ac434100fe9b7e0e8e935b9e6246" 1343 | 1344 | [[package]] 1345 | name = "utf8_iter" 1346 | version = "1.0.4" 1347 | source = "registry+https://github.com/rust-lang/crates.io-index" 1348 | checksum = "b6c140620e7ffbb22c2dee59cafe6084a59b5ffc27a8859a5f0d494b5d52b6be" 1349 | 1350 | [[package]] 1351 | name = "utf8parse" 1352 | version = "0.2.2" 1353 | source = "registry+https://github.com/rust-lang/crates.io-index" 1354 | checksum = "06abde3611657adf66d383f00b093d7faecc7fa57071cce2578660c9f1010821" 1355 | 1356 | [[package]] 1357 | name = "vcpkg" 1358 | version = "0.2.15" 1359 | source = "registry+https://github.com/rust-lang/crates.io-index" 1360 | checksum = "accd4ea62f7bb7a82fe23066fb0957d48ef677f6eeb8215f372f52e48bb32426" 1361 | 1362 | [[package]] 1363 | name = "want" 1364 | version = "0.3.1" 1365 | source = "registry+https://github.com/rust-lang/crates.io-index" 1366 | checksum = "bfa7760aed19e106de2c7c0b581b509f2f25d3dacaf737cb82ac61bc6d760b0e" 1367 | dependencies = [ 1368 | "try-lock", 1369 | ] 1370 | 1371 | [[package]] 1372 | name = "wasi" 1373 | version = "0.11.0+wasi-snapshot-preview1" 1374 | source = "registry+https://github.com/rust-lang/crates.io-index" 1375 | checksum = "9c8d87e72b64a3b4db28d11ce29237c246188f4f51057d65a7eab63b7987e423" 1376 | 1377 | [[package]] 1378 | name = "wasm-bindgen" 1379 | version = "0.2.97" 1380 | source = "registry+https://github.com/rust-lang/crates.io-index" 1381 | checksum = "d15e63b4482863c109d70a7b8706c1e364eb6ea449b201a76c5b89cedcec2d5c" 1382 | dependencies = [ 1383 | "cfg-if", 1384 | "once_cell", 1385 | "wasm-bindgen-macro", 1386 | ] 1387 | 1388 | [[package]] 1389 | name = "wasm-bindgen-backend" 1390 | version = "0.2.97" 1391 | source = "registry+https://github.com/rust-lang/crates.io-index" 1392 | checksum = "8d36ef12e3aaca16ddd3f67922bc63e48e953f126de60bd33ccc0101ef9998cd" 1393 | dependencies = [ 1394 | "bumpalo", 1395 | "log", 1396 | "once_cell", 1397 | "proc-macro2", 1398 | "quote", 1399 | "syn", 1400 | "wasm-bindgen-shared", 1401 | ] 1402 | 1403 | [[package]] 1404 | name = "wasm-bindgen-futures" 1405 | version = "0.4.47" 1406 | source = "registry+https://github.com/rust-lang/crates.io-index" 1407 | checksum = "9dfaf8f50e5f293737ee323940c7d8b08a66a95a419223d9f41610ca08b0833d" 1408 | dependencies = [ 1409 | "cfg-if", 1410 | "js-sys", 1411 | "once_cell", 1412 | "wasm-bindgen", 1413 | "web-sys", 1414 | ] 1415 | 1416 | [[package]] 1417 | name = "wasm-bindgen-macro" 1418 | version = "0.2.97" 1419 | source = "registry+https://github.com/rust-lang/crates.io-index" 1420 | checksum = "705440e08b42d3e4b36de7d66c944be628d579796b8090bfa3471478a2260051" 1421 | dependencies = [ 1422 | "quote", 1423 | "wasm-bindgen-macro-support", 1424 | ] 1425 | 1426 | [[package]] 1427 | name = "wasm-bindgen-macro-support" 1428 | version = "0.2.97" 1429 | source = "registry+https://github.com/rust-lang/crates.io-index" 1430 | checksum = "98c9ae5a76e46f4deecd0f0255cc223cfa18dc9b261213b8aa0c7b36f61b3f1d" 1431 | dependencies = [ 1432 | "proc-macro2", 1433 | "quote", 1434 | "syn", 1435 | "wasm-bindgen-backend", 1436 | "wasm-bindgen-shared", 1437 | ] 1438 | 1439 | [[package]] 1440 | name = "wasm-bindgen-shared" 1441 | version = "0.2.97" 1442 | source = "registry+https://github.com/rust-lang/crates.io-index" 1443 | checksum = "6ee99da9c5ba11bd675621338ef6fa52296b76b83305e9b6e5c77d4c286d6d49" 1444 | 1445 | [[package]] 1446 | name = "web-sys" 1447 | version = "0.3.74" 1448 | source = "registry+https://github.com/rust-lang/crates.io-index" 1449 | checksum = "a98bc3c33f0fe7e59ad7cd041b89034fa82a7c2d4365ca538dda6cdaf513863c" 1450 | dependencies = [ 1451 | "js-sys", 1452 | "wasm-bindgen", 1453 | ] 1454 | 1455 | [[package]] 1456 | name = "winapi" 1457 | version = "0.3.9" 1458 | source = "registry+https://github.com/rust-lang/crates.io-index" 1459 | checksum = "5c839a674fcd7a98952e593242ea400abe93992746761e38641405d28b00f419" 1460 | dependencies = [ 1461 | "winapi-i686-pc-windows-gnu", 1462 | "winapi-x86_64-pc-windows-gnu", 1463 | ] 1464 | 1465 | [[package]] 1466 | name = "winapi-i686-pc-windows-gnu" 1467 | version = "0.4.0" 1468 | source = "registry+https://github.com/rust-lang/crates.io-index" 1469 | checksum = "ac3b87c63620426dd9b991e5ce0329eff545bccbbb34f3be09ff6fb6ab51b7b6" 1470 | 1471 | [[package]] 1472 | name = "winapi-x86_64-pc-windows-gnu" 1473 | version = "0.4.0" 1474 | source = "registry+https://github.com/rust-lang/crates.io-index" 1475 | checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f" 1476 | 1477 | [[package]] 1478 | name = "windows-core" 1479 | version = "0.52.0" 1480 | source = "registry+https://github.com/rust-lang/crates.io-index" 1481 | checksum = "33ab640c8d7e35bf8ba19b884ba838ceb4fba93a4e8c65a9059d08afcfc683d9" 1482 | dependencies = [ 1483 | "windows-targets 0.52.6", 1484 | ] 1485 | 1486 | [[package]] 1487 | name = "windows-sys" 1488 | version = "0.48.0" 1489 | source = "registry+https://github.com/rust-lang/crates.io-index" 1490 | checksum = "677d2418bec65e3338edb076e806bc1ec15693c5d0104683f2efe857f61056a9" 1491 | dependencies = [ 1492 | "windows-targets 0.48.5", 1493 | ] 1494 | 1495 | [[package]] 1496 | name = "windows-sys" 1497 | version = "0.52.0" 1498 | source = "registry+https://github.com/rust-lang/crates.io-index" 1499 | checksum = "282be5f36a8ce781fad8c8ae18fa3f9beff57ec1b52cb3de0789201425d9a33d" 1500 | dependencies = [ 1501 | "windows-targets 0.52.6", 1502 | ] 1503 | 1504 | [[package]] 1505 | name = "windows-sys" 1506 | version = "0.59.0" 1507 | source = "registry+https://github.com/rust-lang/crates.io-index" 1508 | checksum = "1e38bc4d79ed67fd075bcc251a1c39b32a1776bbe92e5bef1f0bf1f8c531853b" 1509 | dependencies = [ 1510 | "windows-targets 0.52.6", 1511 | ] 1512 | 1513 | [[package]] 1514 | name = "windows-targets" 1515 | version = "0.48.5" 1516 | source = "registry+https://github.com/rust-lang/crates.io-index" 1517 | checksum = "9a2fa6e2155d7247be68c096456083145c183cbbbc2764150dda45a87197940c" 1518 | dependencies = [ 1519 | "windows_aarch64_gnullvm 0.48.5", 1520 | "windows_aarch64_msvc 0.48.5", 1521 | "windows_i686_gnu 0.48.5", 1522 | "windows_i686_msvc 0.48.5", 1523 | "windows_x86_64_gnu 0.48.5", 1524 | "windows_x86_64_gnullvm 0.48.5", 1525 | "windows_x86_64_msvc 0.48.5", 1526 | ] 1527 | 1528 | [[package]] 1529 | name = "windows-targets" 1530 | version = "0.52.6" 1531 | source = "registry+https://github.com/rust-lang/crates.io-index" 1532 | checksum = "9b724f72796e036ab90c1021d4780d4d3d648aca59e491e6b98e725b84e99973" 1533 | dependencies = [ 1534 | "windows_aarch64_gnullvm 0.52.6", 1535 | "windows_aarch64_msvc 0.52.6", 1536 | "windows_i686_gnu 0.52.6", 1537 | "windows_i686_gnullvm", 1538 | "windows_i686_msvc 0.52.6", 1539 | "windows_x86_64_gnu 0.52.6", 1540 | "windows_x86_64_gnullvm 0.52.6", 1541 | "windows_x86_64_msvc 0.52.6", 1542 | ] 1543 | 1544 | [[package]] 1545 | name = "windows_aarch64_gnullvm" 1546 | version = "0.48.5" 1547 | source = "registry+https://github.com/rust-lang/crates.io-index" 1548 | checksum = "2b38e32f0abccf9987a4e3079dfb67dcd799fb61361e53e2882c3cbaf0d905d8" 1549 | 1550 | [[package]] 1551 | name = "windows_aarch64_gnullvm" 1552 | version = "0.52.6" 1553 | source = "registry+https://github.com/rust-lang/crates.io-index" 1554 | checksum = "32a4622180e7a0ec044bb555404c800bc9fd9ec262ec147edd5989ccd0c02cd3" 1555 | 1556 | [[package]] 1557 | name = "windows_aarch64_msvc" 1558 | version = "0.48.5" 1559 | source = "registry+https://github.com/rust-lang/crates.io-index" 1560 | checksum = "dc35310971f3b2dbbf3f0690a219f40e2d9afcf64f9ab7cc1be722937c26b4bc" 1561 | 1562 | [[package]] 1563 | name = "windows_aarch64_msvc" 1564 | version = "0.52.6" 1565 | source = "registry+https://github.com/rust-lang/crates.io-index" 1566 | checksum = "09ec2a7bb152e2252b53fa7803150007879548bc709c039df7627cabbd05d469" 1567 | 1568 | [[package]] 1569 | name = "windows_i686_gnu" 1570 | version = "0.48.5" 1571 | source = "registry+https://github.com/rust-lang/crates.io-index" 1572 | checksum = "a75915e7def60c94dcef72200b9a8e58e5091744960da64ec734a6c6e9b3743e" 1573 | 1574 | [[package]] 1575 | name = "windows_i686_gnu" 1576 | version = "0.52.6" 1577 | source = "registry+https://github.com/rust-lang/crates.io-index" 1578 | checksum = "8e9b5ad5ab802e97eb8e295ac6720e509ee4c243f69d781394014ebfe8bbfa0b" 1579 | 1580 | [[package]] 1581 | name = "windows_i686_gnullvm" 1582 | version = "0.52.6" 1583 | source = "registry+https://github.com/rust-lang/crates.io-index" 1584 | checksum = "0eee52d38c090b3caa76c563b86c3a4bd71ef1a819287c19d586d7334ae8ed66" 1585 | 1586 | [[package]] 1587 | name = "windows_i686_msvc" 1588 | version = "0.48.5" 1589 | source = "registry+https://github.com/rust-lang/crates.io-index" 1590 | checksum = "8f55c233f70c4b27f66c523580f78f1004e8b5a8b659e05a4eb49d4166cca406" 1591 | 1592 | [[package]] 1593 | name = "windows_i686_msvc" 1594 | version = "0.52.6" 1595 | source = "registry+https://github.com/rust-lang/crates.io-index" 1596 | checksum = "240948bc05c5e7c6dabba28bf89d89ffce3e303022809e73deaefe4f6ec56c66" 1597 | 1598 | [[package]] 1599 | name = "windows_x86_64_gnu" 1600 | version = "0.48.5" 1601 | source = "registry+https://github.com/rust-lang/crates.io-index" 1602 | checksum = "53d40abd2583d23e4718fddf1ebec84dbff8381c07cae67ff7768bbf19c6718e" 1603 | 1604 | [[package]] 1605 | name = "windows_x86_64_gnu" 1606 | version = "0.52.6" 1607 | source = "registry+https://github.com/rust-lang/crates.io-index" 1608 | checksum = "147a5c80aabfbf0c7d901cb5895d1de30ef2907eb21fbbab29ca94c5b08b1a78" 1609 | 1610 | [[package]] 1611 | name = "windows_x86_64_gnullvm" 1612 | version = "0.48.5" 1613 | source = "registry+https://github.com/rust-lang/crates.io-index" 1614 | checksum = "0b7b52767868a23d5bab768e390dc5f5c55825b6d30b86c844ff2dc7414044cc" 1615 | 1616 | [[package]] 1617 | name = "windows_x86_64_gnullvm" 1618 | version = "0.52.6" 1619 | source = "registry+https://github.com/rust-lang/crates.io-index" 1620 | checksum = "24d5b23dc417412679681396f2b49f3de8c1473deb516bd34410872eff51ed0d" 1621 | 1622 | [[package]] 1623 | name = "windows_x86_64_msvc" 1624 | version = "0.48.5" 1625 | source = "registry+https://github.com/rust-lang/crates.io-index" 1626 | checksum = "ed94fce61571a4006852b7389a063ab983c02eb1bb37b47f8272ce92d06d9538" 1627 | 1628 | [[package]] 1629 | name = "windows_x86_64_msvc" 1630 | version = "0.52.6" 1631 | source = "registry+https://github.com/rust-lang/crates.io-index" 1632 | checksum = "589f6da84c646204747d1270a2a5661ea66ed1cced2631d546fdfb155959f9ec" 1633 | 1634 | [[package]] 1635 | name = "winreg" 1636 | version = "0.50.0" 1637 | source = "registry+https://github.com/rust-lang/crates.io-index" 1638 | checksum = "524e57b2c537c0f9b1e69f1965311ec12182b4122e45035b1508cd24d2adadb1" 1639 | dependencies = [ 1640 | "cfg-if", 1641 | "windows-sys 0.48.0", 1642 | ] 1643 | 1644 | [[package]] 1645 | name = "write16" 1646 | version = "1.0.0" 1647 | source = "registry+https://github.com/rust-lang/crates.io-index" 1648 | checksum = "d1890f4022759daae28ed4fe62859b1236caebfc61ede2f63ed4e695f3f6d936" 1649 | 1650 | [[package]] 1651 | name = "writeable" 1652 | version = "0.5.5" 1653 | source = "registry+https://github.com/rust-lang/crates.io-index" 1654 | checksum = "1e9df38ee2d2c3c5948ea468a8406ff0db0b29ae1ffde1bcf20ef305bcc95c51" 1655 | 1656 | [[package]] 1657 | name = "yoke" 1658 | version = "0.7.5" 1659 | source = "registry+https://github.com/rust-lang/crates.io-index" 1660 | checksum = "120e6aef9aa629e3d4f52dc8cc43a015c7724194c97dfaf45180d2daf2b77f40" 1661 | dependencies = [ 1662 | "serde", 1663 | "stable_deref_trait", 1664 | "yoke-derive", 1665 | "zerofrom", 1666 | ] 1667 | 1668 | [[package]] 1669 | name = "yoke-derive" 1670 | version = "0.7.5" 1671 | source = "registry+https://github.com/rust-lang/crates.io-index" 1672 | checksum = "2380878cad4ac9aac1e2435f3eb4020e8374b5f13c296cb75b4620ff8e229154" 1673 | dependencies = [ 1674 | "proc-macro2", 1675 | "quote", 1676 | "syn", 1677 | "synstructure", 1678 | ] 1679 | 1680 | [[package]] 1681 | name = "zerofrom" 1682 | version = "0.1.5" 1683 | source = "registry+https://github.com/rust-lang/crates.io-index" 1684 | checksum = "cff3ee08c995dee1859d998dea82f7374f2826091dd9cd47def953cae446cd2e" 1685 | dependencies = [ 1686 | "zerofrom-derive", 1687 | ] 1688 | 1689 | [[package]] 1690 | name = "zerofrom-derive" 1691 | version = "0.1.5" 1692 | source = "registry+https://github.com/rust-lang/crates.io-index" 1693 | checksum = "595eed982f7d355beb85837f651fa22e90b3c044842dc7f2c2842c086f295808" 1694 | dependencies = [ 1695 | "proc-macro2", 1696 | "quote", 1697 | "syn", 1698 | "synstructure", 1699 | ] 1700 | 1701 | [[package]] 1702 | name = "zerovec" 1703 | version = "0.10.4" 1704 | source = "registry+https://github.com/rust-lang/crates.io-index" 1705 | checksum = "aa2b893d79df23bfb12d5461018d408ea19dfafe76c2c7ef6d4eba614f8ff079" 1706 | dependencies = [ 1707 | "yoke", 1708 | "zerofrom", 1709 | "zerovec-derive", 1710 | ] 1711 | 1712 | [[package]] 1713 | name = "zerovec-derive" 1714 | version = "0.10.3" 1715 | source = "registry+https://github.com/rust-lang/crates.io-index" 1716 | checksum = "6eafa6dfb17584ea3e2bd6e76e0cc15ad7af12b09abdd1ca55961bed9b1063c6" 1717 | dependencies = [ 1718 | "proc-macro2", 1719 | "quote", 1720 | "syn", 1721 | ] 1722 | -------------------------------------------------------------------------------- /Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "l1t" 3 | version = "0.2.1" 4 | edition = "2021" 5 | authors = ["Alex Lay-Calvert"] 6 | description = "A terminal strategy game about shooting lasers and lighting statues" 7 | license = "GPL-3.0-only" 8 | homepage = "https://github.com/alex-laycalvert/l1t" 9 | repository = "https://github.com/alex-laycalvert/l1t" 10 | readme = "README.md" 11 | 12 | [dependencies] 13 | chrono = "0.4.24" 14 | clap = { version = "4.2.4", features = ["derive"] } 15 | crossterm = "0.26.1" 16 | home = "0.5.4" 17 | human-sort = "0.2.2" 18 | serde = { version = "1.0.160", features = ["derive"] } 19 | serde_json = "1.0.96" 20 | reqwest = { version = "0.11", features = ["json"] } 21 | tokio = { version = "1", features = ["full"] } 22 | -------------------------------------------------------------------------------- /LEVEL_DESIGN.md: -------------------------------------------------------------------------------- 1 | # Level Design 2 | 3 | To design a level, create a level file calle `.l1t`. An example has been provided in this repo called `test_level.l1t`. 4 | 5 | ## Structure 6 | 7 | Every level must start with the following three lines: 8 | 9 | - Level Name 10 | - Author 11 | - Description 12 | 13 | They can be left empty if desired. 14 | 15 | Example: 16 | 17 | ``` 18 | // level.l1t 19 | Test Level 20 | alex-laycalvert 21 | A test level description 22 | // rest of level... 23 | ``` 24 | 25 | The following lines represent the level grid. 26 | 27 | ## Grid 28 | 29 | The grid of the level must be at least one playable space large and be surrounded by an even box of `I` characters representing walls: 30 | 31 | Example: 32 | 33 | ``` 34 | // ... file info 35 | IIIII 36 | I I 37 | I I 38 | I I 39 | IIIII 40 | ``` 41 | 42 | Inside the whitespace, you can place any characters representing the level items that you want. 43 | 44 | ## Items/Characters 45 | 46 | | Ascii Character | Level Item | 47 | | --------------- | ------------------------------- | 48 | | `X` | Player | 49 | | `I` | Wall | 50 | | `S` | Statue | 51 | | `R` | Reverse Statue | 52 | | `Z` | Zapper | 53 | | `1` | Laser facing UP turned ON | 54 | | `2` | Laser facing DOWN turned ON | 55 | | `3` | Laser facing LEFT turned ON | 56 | | `4` | Laser facing RIGHT turned ON | 57 | | `5` | Laser facing UP turned OFF | 58 | | `6` | Laser facing DOWN turned OFF | 59 | | `7` | Laser facing LEFT turned OFF | 60 | | `8` | Laser facing RIGHT turned OFF | 61 | | `B` | Block | 62 | | `T` | Toggle Block | 63 | | `b` | Button | 64 | | `s` | Switch | 65 | | `/` | Mirror facing FORWARD | 66 | | `\` | Mirror facing BACKWARD | 67 | | `?` | Moveable Mirror facing FORWARD | 68 | | `|` | Moveable Mirror facing BACKWARD | 69 | 70 | Any other characters not listed above inside the level grid will be translated into walls. 71 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | GNU GENERAL PUBLIC LICENSE 2 | Version 3, 29 June 2007 3 | 4 | Copyright (C) 2007 Free Software Foundation, Inc. 5 | Everyone is permitted to copy and distribute verbatim copies 6 | of this license document, but changing it is not allowed. 7 | 8 | Preamble 9 | 10 | The GNU General Public License is a free, copyleft license for 11 | software and other kinds of works. 12 | 13 | The licenses for most software and other practical works are designed 14 | to take away your freedom to share and change the works. By contrast, 15 | the GNU General Public License is intended to guarantee your freedom to 16 | share and change all versions of a program--to make sure it remains free 17 | software for all its users. We, the Free Software Foundation, use the 18 | GNU General Public License for most of our software; it applies also to 19 | any other work released this way by its authors. You can apply it to 20 | your programs, too. 21 | 22 | When we speak of free software, we are referring to freedom, not 23 | price. Our General Public Licenses are designed to make sure that you 24 | have the freedom to distribute copies of free software (and charge for 25 | them if you wish), that you receive source code or can get it if you 26 | want it, that you can change the software or use pieces of it in new 27 | free programs, and that you know you can do these things. 28 | 29 | To protect your rights, we need to prevent others from denying you 30 | these rights or asking you to surrender the rights. Therefore, you have 31 | certain responsibilities if you distribute copies of the software, or if 32 | you modify it: responsibilities to respect the freedom of others. 33 | 34 | For example, if you distribute copies of such a program, whether 35 | gratis or for a fee, you must pass on to the recipients the same 36 | freedoms that you received. You must make sure that they, too, receive 37 | or can get the source code. And you must show them these terms so they 38 | know their rights. 39 | 40 | Developers that use the GNU GPL protect your rights with two steps: 41 | (1) assert copyright on the software, and (2) offer you this License 42 | giving you legal permission to copy, distribute and/or modify it. 43 | 44 | For the developers' and authors' protection, the GPL clearly explains 45 | that there is no warranty for this free software. For both users' and 46 | authors' sake, the GPL requires that modified versions be marked as 47 | changed, so that their problems will not be attributed erroneously to 48 | authors of previous versions. 49 | 50 | Some devices are designed to deny users access to install or run 51 | modified versions of the software inside them, although the manufacturer 52 | can do so. This is fundamentally incompatible with the aim of 53 | protecting users' freedom to change the software. The systematic 54 | pattern of such abuse occurs in the area of products for individuals to 55 | use, which is precisely where it is most unacceptable. Therefore, we 56 | have designed this version of the GPL to prohibit the practice for those 57 | products. If such problems arise substantially in other domains, we 58 | stand ready to extend this provision to those domains in future versions 59 | of the GPL, as needed to protect the freedom of users. 60 | 61 | Finally, every program is threatened constantly by software patents. 62 | States should not allow patents to restrict development and use of 63 | software on general-purpose computers, but in those that do, we wish to 64 | avoid the special danger that patents applied to a free program could 65 | make it effectively proprietary. To prevent this, the GPL assures that 66 | patents cannot be used to render the program non-free. 67 | 68 | The precise terms and conditions for copying, distribution and 69 | modification follow. 70 | 71 | TERMS AND CONDITIONS 72 | 73 | 0. Definitions. 74 | 75 | "This License" refers to version 3 of the GNU General Public License. 76 | 77 | "Copyright" also means copyright-like laws that apply to other kinds of 78 | works, such as semiconductor masks. 79 | 80 | "The Program" refers to any copyrightable work licensed under this 81 | License. Each licensee is addressed as "you". "Licensees" and 82 | "recipients" may be individuals or organizations. 83 | 84 | To "modify" a work means to copy from or adapt all or part of the work 85 | in a fashion requiring copyright permission, other than the making of an 86 | exact copy. The resulting work is called a "modified version" of the 87 | earlier work or a work "based on" the earlier work. 88 | 89 | A "covered work" means either the unmodified Program or a work based 90 | on the Program. 91 | 92 | To "propagate" a work means to do anything with it that, without 93 | permission, would make you directly or secondarily liable for 94 | infringement under applicable copyright law, except executing it on a 95 | computer or modifying a private copy. Propagation includes copying, 96 | distribution (with or without modification), making available to the 97 | public, and in some countries other activities as well. 98 | 99 | To "convey" a work means any kind of propagation that enables other 100 | parties to make or receive copies. Mere interaction with a user through 101 | a computer network, with no transfer of a copy, is not conveying. 102 | 103 | An interactive user interface displays "Appropriate Legal Notices" 104 | to the extent that it includes a convenient and prominently visible 105 | feature that (1) displays an appropriate copyright notice, and (2) 106 | tells the user that there is no warranty for the work (except to the 107 | extent that warranties are provided), that licensees may convey the 108 | work under this License, and how to view a copy of this License. If 109 | the interface presents a list of user commands or options, such as a 110 | menu, a prominent item in the list meets this criterion. 111 | 112 | 1. Source Code. 113 | 114 | The "source code" for a work means the preferred form of the work 115 | for making modifications to it. "Object code" means any non-source 116 | form of a work. 117 | 118 | A "Standard Interface" means an interface that either is an official 119 | standard defined by a recognized standards body, or, in the case of 120 | interfaces specified for a particular programming language, one that 121 | is widely used among developers working in that language. 122 | 123 | The "System Libraries" of an executable work include anything, other 124 | than the work as a whole, that (a) is included in the normal form of 125 | packaging a Major Component, but which is not part of that Major 126 | Component, and (b) serves only to enable use of the work with that 127 | Major Component, or to implement a Standard Interface for which an 128 | implementation is available to the public in source code form. A 129 | "Major Component", in this context, means a major essential component 130 | (kernel, window system, and so on) of the specific operating system 131 | (if any) on which the executable work runs, or a compiler used to 132 | produce the work, or an object code interpreter used to run it. 133 | 134 | The "Corresponding Source" for a work in object code form means all 135 | the source code needed to generate, install, and (for an executable 136 | work) run the object code and to modify the work, including scripts to 137 | control those activities. However, it does not include the work's 138 | System Libraries, or general-purpose tools or generally available free 139 | programs which are used unmodified in performing those activities but 140 | which are not part of the work. For example, Corresponding Source 141 | includes interface definition files associated with source files for 142 | the work, and the source code for shared libraries and dynamically 143 | linked subprograms that the work is specifically designed to require, 144 | such as by intimate data communication or control flow between those 145 | subprograms and other parts of the work. 146 | 147 | The Corresponding Source need not include anything that users 148 | can regenerate automatically from other parts of the Corresponding 149 | Source. 150 | 151 | The Corresponding Source for a work in source code form is that 152 | same work. 153 | 154 | 2. Basic Permissions. 155 | 156 | All rights granted under this License are granted for the term of 157 | copyright on the Program, and are irrevocable provided the stated 158 | conditions are met. This License explicitly affirms your unlimited 159 | permission to run the unmodified Program. The output from running a 160 | covered work is covered by this License only if the output, given its 161 | content, constitutes a covered work. This License acknowledges your 162 | rights of fair use or other equivalent, as provided by copyright law. 163 | 164 | You may make, run and propagate covered works that you do not 165 | convey, without conditions so long as your license otherwise remains 166 | in force. You may convey covered works to others for the sole purpose 167 | of having them make modifications exclusively for you, or provide you 168 | with facilities for running those works, provided that you comply with 169 | the terms of this License in conveying all material for which you do 170 | not control copyright. Those thus making or running the covered works 171 | for you must do so exclusively on your behalf, under your direction 172 | and control, on terms that prohibit them from making any copies of 173 | your copyrighted material outside their relationship with you. 174 | 175 | Conveying under any other circumstances is permitted solely under 176 | the conditions stated below. Sublicensing is not allowed; section 10 177 | makes it unnecessary. 178 | 179 | 3. Protecting Users' Legal Rights From Anti-Circumvention Law. 180 | 181 | No covered work shall be deemed part of an effective technological 182 | measure under any applicable law fulfilling obligations under article 183 | 11 of the WIPO copyright treaty adopted on 20 December 1996, or 184 | similar laws prohibiting or restricting circumvention of such 185 | measures. 186 | 187 | When you convey a covered work, you waive any legal power to forbid 188 | circumvention of technological measures to the extent such circumvention 189 | is effected by exercising rights under this License with respect to 190 | the covered work, and you disclaim any intention to limit operation or 191 | modification of the work as a means of enforcing, against the work's 192 | users, your or third parties' legal rights to forbid circumvention of 193 | technological measures. 194 | 195 | 4. Conveying Verbatim Copies. 196 | 197 | You may convey verbatim copies of the Program's source code as you 198 | receive it, in any medium, provided that you conspicuously and 199 | appropriately publish on each copy an appropriate copyright notice; 200 | keep intact all notices stating that this License and any 201 | non-permissive terms added in accord with section 7 apply to the code; 202 | keep intact all notices of the absence of any warranty; and give all 203 | recipients a copy of this License along with the Program. 204 | 205 | You may charge any price or no price for each copy that you convey, 206 | and you may offer support or warranty protection for a fee. 207 | 208 | 5. Conveying Modified Source Versions. 209 | 210 | You may convey a work based on the Program, or the modifications to 211 | produce it from the Program, in the form of source code under the 212 | terms of section 4, provided that you also meet all of these conditions: 213 | 214 | a) The work must carry prominent notices stating that you modified 215 | it, and giving a relevant date. 216 | 217 | b) The work must carry prominent notices stating that it is 218 | released under this License and any conditions added under section 219 | 7. This requirement modifies the requirement in section 4 to 220 | "keep intact all notices". 221 | 222 | c) You must license the entire work, as a whole, under this 223 | License to anyone who comes into possession of a copy. This 224 | License will therefore apply, along with any applicable section 7 225 | additional terms, to the whole of the work, and all its parts, 226 | regardless of how they are packaged. This License gives no 227 | permission to license the work in any other way, but it does not 228 | invalidate such permission if you have separately received it. 229 | 230 | d) If the work has interactive user interfaces, each must display 231 | Appropriate Legal Notices; however, if the Program has interactive 232 | interfaces that do not display Appropriate Legal Notices, your 233 | work need not make them do so. 234 | 235 | A compilation of a covered work with other separate and independent 236 | works, which are not by their nature extensions of the covered work, 237 | and which are not combined with it such as to form a larger program, 238 | in or on a volume of a storage or distribution medium, is called an 239 | "aggregate" if the compilation and its resulting copyright are not 240 | used to limit the access or legal rights of the compilation's users 241 | beyond what the individual works permit. Inclusion of a covered work 242 | in an aggregate does not cause this License to apply to the other 243 | parts of the aggregate. 244 | 245 | 6. Conveying Non-Source Forms. 246 | 247 | You may convey a covered work in object code form under the terms 248 | of sections 4 and 5, provided that you also convey the 249 | machine-readable Corresponding Source under the terms of this License, 250 | in one of these ways: 251 | 252 | a) Convey the object code in, or embodied in, a physical product 253 | (including a physical distribution medium), accompanied by the 254 | Corresponding Source fixed on a durable physical medium 255 | customarily used for software interchange. 256 | 257 | b) Convey the object code in, or embodied in, a physical product 258 | (including a physical distribution medium), accompanied by a 259 | written offer, valid for at least three years and valid for as 260 | long as you offer spare parts or customer support for that product 261 | model, to give anyone who possesses the object code either (1) a 262 | copy of the Corresponding Source for all the software in the 263 | product that is covered by this License, on a durable physical 264 | medium customarily used for software interchange, for a price no 265 | more than your reasonable cost of physically performing this 266 | conveying of source, or (2) access to copy the 267 | Corresponding Source from a network server at no charge. 268 | 269 | c) Convey individual copies of the object code with a copy of the 270 | written offer to provide the Corresponding Source. This 271 | alternative is allowed only occasionally and noncommercially, and 272 | only if you received the object code with such an offer, in accord 273 | with subsection 6b. 274 | 275 | d) Convey the object code by offering access from a designated 276 | place (gratis or for a charge), and offer equivalent access to the 277 | Corresponding Source in the same way through the same place at no 278 | further charge. You need not require recipients to copy the 279 | Corresponding Source along with the object code. If the place to 280 | copy the object code is a network server, the Corresponding Source 281 | may be on a different server (operated by you or a third party) 282 | that supports equivalent copying facilities, provided you maintain 283 | clear directions next to the object code saying where to find the 284 | Corresponding Source. Regardless of what server hosts the 285 | Corresponding Source, you remain obligated to ensure that it is 286 | available for as long as needed to satisfy these requirements. 287 | 288 | e) Convey the object code using peer-to-peer transmission, provided 289 | you inform other peers where the object code and Corresponding 290 | Source of the work are being offered to the general public at no 291 | charge under subsection 6d. 292 | 293 | A separable portion of the object code, whose source code is excluded 294 | from the Corresponding Source as a System Library, need not be 295 | included in conveying the object code work. 296 | 297 | A "User Product" is either (1) a "consumer product", which means any 298 | tangible personal property which is normally used for personal, family, 299 | or household purposes, or (2) anything designed or sold for incorporation 300 | into a dwelling. In determining whether a product is a consumer product, 301 | doubtful cases shall be resolved in favor of coverage. For a particular 302 | product received by a particular user, "normally used" refers to a 303 | typical or common use of that class of product, regardless of the status 304 | of the particular user or of the way in which the particular user 305 | actually uses, or expects or is expected to use, the product. A product 306 | is a consumer product regardless of whether the product has substantial 307 | commercial, industrial or non-consumer uses, unless such uses represent 308 | the only significant mode of use of the product. 309 | 310 | "Installation Information" for a User Product means any methods, 311 | procedures, authorization keys, or other information required to install 312 | and execute modified versions of a covered work in that User Product from 313 | a modified version of its Corresponding Source. The information must 314 | suffice to ensure that the continued functioning of the modified object 315 | code is in no case prevented or interfered with solely because 316 | modification has been made. 317 | 318 | If you convey an object code work under this section in, or with, or 319 | specifically for use in, a User Product, and the conveying occurs as 320 | part of a transaction in which the right of possession and use of the 321 | User Product is transferred to the recipient in perpetuity or for a 322 | fixed term (regardless of how the transaction is characterized), the 323 | Corresponding Source conveyed under this section must be accompanied 324 | by the Installation Information. But this requirement does not apply 325 | if neither you nor any third party retains the ability to install 326 | modified object code on the User Product (for example, the work has 327 | been installed in ROM). 328 | 329 | The requirement to provide Installation Information does not include a 330 | requirement to continue to provide support service, warranty, or updates 331 | for a work that has been modified or installed by the recipient, or for 332 | the User Product in which it has been modified or installed. Access to a 333 | network may be denied when the modification itself materially and 334 | adversely affects the operation of the network or violates the rules and 335 | protocols for communication across the network. 336 | 337 | Corresponding Source conveyed, and Installation Information provided, 338 | in accord with this section must be in a format that is publicly 339 | documented (and with an implementation available to the public in 340 | source code form), and must require no special password or key for 341 | unpacking, reading or copying. 342 | 343 | 7. Additional Terms. 344 | 345 | "Additional permissions" are terms that supplement the terms of this 346 | License by making exceptions from one or more of its conditions. 347 | Additional permissions that are applicable to the entire Program shall 348 | be treated as though they were included in this License, to the extent 349 | that they are valid under applicable law. If additional permissions 350 | apply only to part of the Program, that part may be used separately 351 | under those permissions, but the entire Program remains governed by 352 | this License without regard to the additional permissions. 353 | 354 | When you convey a copy of a covered work, you may at your option 355 | remove any additional permissions from that copy, or from any part of 356 | it. (Additional permissions may be written to require their own 357 | removal in certain cases when you modify the work.) You may place 358 | additional permissions on material, added by you to a covered work, 359 | for which you have or can give appropriate copyright permission. 360 | 361 | Notwithstanding any other provision of this License, for material you 362 | add to a covered work, you may (if authorized by the copyright holders of 363 | that material) supplement the terms of this License with terms: 364 | 365 | a) Disclaiming warranty or limiting liability differently from the 366 | terms of sections 15 and 16 of this License; or 367 | 368 | b) Requiring preservation of specified reasonable legal notices or 369 | author attributions in that material or in the Appropriate Legal 370 | Notices displayed by works containing it; or 371 | 372 | c) Prohibiting misrepresentation of the origin of that material, or 373 | requiring that modified versions of such material be marked in 374 | reasonable ways as different from the original version; or 375 | 376 | d) Limiting the use for publicity purposes of names of licensors or 377 | authors of the material; or 378 | 379 | e) Declining to grant rights under trademark law for use of some 380 | trade names, trademarks, or service marks; or 381 | 382 | f) Requiring indemnification of licensors and authors of that 383 | material by anyone who conveys the material (or modified versions of 384 | it) with contractual assumptions of liability to the recipient, for 385 | any liability that these contractual assumptions directly impose on 386 | those licensors and authors. 387 | 388 | All other non-permissive additional terms are considered "further 389 | restrictions" within the meaning of section 10. If the Program as you 390 | received it, or any part of it, contains a notice stating that it is 391 | governed by this License along with a term that is a further 392 | restriction, you may remove that term. If a license document contains 393 | a further restriction but permits relicensing or conveying under this 394 | License, you may add to a covered work material governed by the terms 395 | of that license document, provided that the further restriction does 396 | not survive such relicensing or conveying. 397 | 398 | If you add terms to a covered work in accord with this section, you 399 | must place, in the relevant source files, a statement of the 400 | additional terms that apply to those files, or a notice indicating 401 | where to find the applicable terms. 402 | 403 | Additional terms, permissive or non-permissive, may be stated in the 404 | form of a separately written license, or stated as exceptions; 405 | the above requirements apply either way. 406 | 407 | 8. Termination. 408 | 409 | You may not propagate or modify a covered work except as expressly 410 | provided under this License. Any attempt otherwise to propagate or 411 | modify it is void, and will automatically terminate your rights under 412 | this License (including any patent licenses granted under the third 413 | paragraph of section 11). 414 | 415 | However, if you cease all violation of this License, then your 416 | license from a particular copyright holder is reinstated (a) 417 | provisionally, unless and until the copyright holder explicitly and 418 | finally terminates your license, and (b) permanently, if the copyright 419 | holder fails to notify you of the violation by some reasonable means 420 | prior to 60 days after the cessation. 421 | 422 | Moreover, your license from a particular copyright holder is 423 | reinstated permanently if the copyright holder notifies you of the 424 | violation by some reasonable means, this is the first time you have 425 | received notice of violation of this License (for any work) from that 426 | copyright holder, and you cure the violation prior to 30 days after 427 | your receipt of the notice. 428 | 429 | Termination of your rights under this section does not terminate the 430 | licenses of parties who have received copies or rights from you under 431 | this License. If your rights have been terminated and not permanently 432 | reinstated, you do not qualify to receive new licenses for the same 433 | material under section 10. 434 | 435 | 9. Acceptance Not Required for Having Copies. 436 | 437 | You are not required to accept this License in order to receive or 438 | run a copy of the Program. Ancillary propagation of a covered work 439 | occurring solely as a consequence of using peer-to-peer transmission 440 | to receive a copy likewise does not require acceptance. However, 441 | nothing other than this License grants you permission to propagate or 442 | modify any covered work. These actions infringe copyright if you do 443 | not accept this License. Therefore, by modifying or propagating a 444 | covered work, you indicate your acceptance of this License to do so. 445 | 446 | 10. Automatic Licensing of Downstream Recipients. 447 | 448 | Each time you convey a covered work, the recipient automatically 449 | receives a license from the original licensors, to run, modify and 450 | propagate that work, subject to this License. You are not responsible 451 | for enforcing compliance by third parties with this License. 452 | 453 | An "entity transaction" is a transaction transferring control of an 454 | organization, or substantially all assets of one, or subdividing an 455 | organization, or merging organizations. If propagation of a covered 456 | work results from an entity transaction, each party to that 457 | transaction who receives a copy of the work also receives whatever 458 | licenses to the work the party's predecessor in interest had or could 459 | give under the previous paragraph, plus a right to possession of the 460 | Corresponding Source of the work from the predecessor in interest, if 461 | the predecessor has it or can get it with reasonable efforts. 462 | 463 | You may not impose any further restrictions on the exercise of the 464 | rights granted or affirmed under this License. For example, you may 465 | not impose a license fee, royalty, or other charge for exercise of 466 | rights granted under this License, and you may not initiate litigation 467 | (including a cross-claim or counterclaim in a lawsuit) alleging that 468 | any patent claim is infringed by making, using, selling, offering for 469 | sale, or importing the Program or any portion of it. 470 | 471 | 11. Patents. 472 | 473 | A "contributor" is a copyright holder who authorizes use under this 474 | License of the Program or a work on which the Program is based. The 475 | work thus licensed is called the contributor's "contributor version". 476 | 477 | A contributor's "essential patent claims" are all patent claims 478 | owned or controlled by the contributor, whether already acquired or 479 | hereafter acquired, that would be infringed by some manner, permitted 480 | by this License, of making, using, or selling its contributor version, 481 | but do not include claims that would be infringed only as a 482 | consequence of further modification of the contributor version. For 483 | purposes of this definition, "control" includes the right to grant 484 | patent sublicenses in a manner consistent with the requirements of 485 | this License. 486 | 487 | Each contributor grants you a non-exclusive, worldwide, royalty-free 488 | patent license under the contributor's essential patent claims, to 489 | make, use, sell, offer for sale, import and otherwise run, modify and 490 | propagate the contents of its contributor version. 491 | 492 | In the following three paragraphs, a "patent license" is any express 493 | agreement or commitment, however denominated, not to enforce a patent 494 | (such as an express permission to practice a patent or covenant not to 495 | sue for patent infringement). To "grant" such a patent license to a 496 | party means to make such an agreement or commitment not to enforce a 497 | patent against the party. 498 | 499 | If you convey a covered work, knowingly relying on a patent license, 500 | and the Corresponding Source of the work is not available for anyone 501 | to copy, free of charge and under the terms of this License, through a 502 | publicly available network server or other readily accessible means, 503 | then you must either (1) cause the Corresponding Source to be so 504 | available, or (2) arrange to deprive yourself of the benefit of the 505 | patent license for this particular work, or (3) arrange, in a manner 506 | consistent with the requirements of this License, to extend the patent 507 | license to downstream recipients. "Knowingly relying" means you have 508 | actual knowledge that, but for the patent license, your conveying the 509 | covered work in a country, or your recipient's use of the covered work 510 | in a country, would infringe one or more identifiable patents in that 511 | country that you have reason to believe are valid. 512 | 513 | If, pursuant to or in connection with a single transaction or 514 | arrangement, you convey, or propagate by procuring conveyance of, a 515 | covered work, and grant a patent license to some of the parties 516 | receiving the covered work authorizing them to use, propagate, modify 517 | or convey a specific copy of the covered work, then the patent license 518 | you grant is automatically extended to all recipients of the covered 519 | work and works based on it. 520 | 521 | A patent license is "discriminatory" if it does not include within 522 | the scope of its coverage, prohibits the exercise of, or is 523 | conditioned on the non-exercise of one or more of the rights that are 524 | specifically granted under this License. You may not convey a covered 525 | work if you are a party to an arrangement with a third party that is 526 | in the business of distributing software, under which you make payment 527 | to the third party based on the extent of your activity of conveying 528 | the work, and under which the third party grants, to any of the 529 | parties who would receive the covered work from you, a discriminatory 530 | patent license (a) in connection with copies of the covered work 531 | conveyed by you (or copies made from those copies), or (b) primarily 532 | for and in connection with specific products or compilations that 533 | contain the covered work, unless you entered into that arrangement, 534 | or that patent license was granted, prior to 28 March 2007. 535 | 536 | Nothing in this License shall be construed as excluding or limiting 537 | any implied license or other defenses to infringement that may 538 | otherwise be available to you under applicable patent law. 539 | 540 | 12. No Surrender of Others' Freedom. 541 | 542 | If conditions are imposed on you (whether by court order, agreement or 543 | otherwise) that contradict the conditions of this License, they do not 544 | excuse you from the conditions of this License. If you cannot convey a 545 | covered work so as to satisfy simultaneously your obligations under this 546 | License and any other pertinent obligations, then as a consequence you may 547 | not convey it at all. For example, if you agree to terms that obligate you 548 | to collect a royalty for further conveying from those to whom you convey 549 | the Program, the only way you could satisfy both those terms and this 550 | License would be to refrain entirely from conveying the Program. 551 | 552 | 13. Use with the GNU Affero General Public License. 553 | 554 | Notwithstanding any other provision of this License, you have 555 | permission to link or combine any covered work with a work licensed 556 | under version 3 of the GNU Affero General Public License into a single 557 | combined work, and to convey the resulting work. The terms of this 558 | License will continue to apply to the part which is the covered work, 559 | but the special requirements of the GNU Affero General Public License, 560 | section 13, concerning interaction through a network will apply to the 561 | combination as such. 562 | 563 | 14. Revised Versions of this License. 564 | 565 | The Free Software Foundation may publish revised and/or new versions of 566 | the GNU General Public License from time to time. Such new versions will 567 | be similar in spirit to the present version, but may differ in detail to 568 | address new problems or concerns. 569 | 570 | Each version is given a distinguishing version number. If the 571 | Program specifies that a certain numbered version of the GNU General 572 | Public License "or any later version" applies to it, you have the 573 | option of following the terms and conditions either of that numbered 574 | version or of any later version published by the Free Software 575 | Foundation. If the Program does not specify a version number of the 576 | GNU General Public License, you may choose any version ever published 577 | by the Free Software Foundation. 578 | 579 | If the Program specifies that a proxy can decide which future 580 | versions of the GNU General Public License can be used, that proxy's 581 | public statement of acceptance of a version permanently authorizes you 582 | to choose that version for the Program. 583 | 584 | Later license versions may give you additional or different 585 | permissions. However, no additional obligations are imposed on any 586 | author or copyright holder as a result of your choosing to follow a 587 | later version. 588 | 589 | 15. Disclaimer of Warranty. 590 | 591 | THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY 592 | APPLICABLE LAW. EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT 593 | HOLDERS AND/OR OTHER PARTIES PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY 594 | OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, 595 | THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR 596 | PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE PROGRAM 597 | IS WITH YOU. SHOULD THE PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF 598 | ALL NECESSARY SERVICING, REPAIR OR CORRECTION. 599 | 600 | 16. Limitation of Liability. 601 | 602 | IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING 603 | WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MODIFIES AND/OR CONVEYS 604 | THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY 605 | GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE 606 | USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED TO LOSS OF 607 | DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD 608 | PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER PROGRAMS), 609 | EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF 610 | SUCH DAMAGES. 611 | 612 | 17. Interpretation of Sections 15 and 16. 613 | 614 | If the disclaimer of warranty and limitation of liability provided 615 | above cannot be given local legal effect according to their terms, 616 | reviewing courts shall apply local law that most closely approximates 617 | an absolute waiver of all civil liability in connection with the 618 | Program, unless a warranty or assumption of liability accompanies a 619 | copy of the Program in return for a fee. 620 | 621 | END OF TERMS AND CONDITIONS 622 | 623 | How to Apply These Terms to Your New Programs 624 | 625 | If you develop a new program, and you want it to be of the greatest 626 | possible use to the public, the best way to achieve this is to make it 627 | free software which everyone can redistribute and change under these terms. 628 | 629 | To do so, attach the following notices to the program. It is safest 630 | to attach them to the start of each source file to most effectively 631 | state the exclusion of warranty; and each file should have at least 632 | the "copyright" line and a pointer to where the full notice is found. 633 | 634 | 635 | Copyright (C) 636 | 637 | This program is free software: you can redistribute it and/or modify 638 | it under the terms of the GNU General Public License as published by 639 | the Free Software Foundation, either version 3 of the License, or 640 | (at your option) any later version. 641 | 642 | This program is distributed in the hope that it will be useful, 643 | but WITHOUT ANY WARRANTY; without even the implied warranty of 644 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 645 | GNU General Public License for more details. 646 | 647 | You should have received a copy of the GNU General Public License 648 | along with this program. If not, see . 649 | 650 | Also add information on how to contact you by electronic and paper mail. 651 | 652 | If the program does terminal interaction, make it output a short 653 | notice like this when it starts in an interactive mode: 654 | 655 | Copyright (C) 656 | This program comes with ABSOLUTELY NO WARRANTY; for details type `show w'. 657 | This is free software, and you are welcome to redistribute it 658 | under certain conditions; type `show c' for details. 659 | 660 | The hypothetical commands `show w' and `show c' should show the appropriate 661 | parts of the General Public License. Of course, your program's commands 662 | might be different; for a GUI interface, you would use an "about box". 663 | 664 | You should also get your employer (if you work as a programmer) or school, 665 | if any, to sign a "copyright disclaimer" for the program, if necessary. 666 | For more information on this, and how to apply and follow the GNU GPL, see 667 | . 668 | 669 | The GNU General Public License does not permit incorporating your program 670 | into proprietary programs. If your program is a subroutine library, you 671 | may consider it more useful to permit linking proprietary applications with 672 | the library. If this is what you want to do, use the GNU Lesser General 673 | Public License instead of this License. But first, please read 674 | . 675 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # l1t 2 | 3 | A terminal strategy game about shooting lasers and lighting statues. Built in Rust. 4 | 5 | ![l1t-logo](https://user-images.githubusercontent.com/45835678/194675329-027fd0d9-e1ee-4149-980b-e2fc7099206e.png) 6 | 7 | In `l1t`, your goal is to use the available lasers to light up all of the 8 | statues in the level. 9 | 10 | ## Installation 11 | 12 |
13 |
14 | 15 | Arch Linux AUR 16 | 17 | 18 | `l1t` is available on the AUR under `l1t-bin`. 19 | 20 | Using the AUR helper `yay`: 21 | 22 | ```bash 23 | yay -S l1t-bin 24 | ``` 25 | 26 |
27 | 28 |
29 |
30 | 31 | Cargo/crates.io 32 | 33 | 34 | `l1t` can be installed from [crates.io](https://crates.io) using `cargo`: 35 | 36 | ```bash 37 | cargo install l1t 38 | ``` 39 | 40 |
41 | 42 |
43 |
44 | 45 | Build from Source 46 | 47 | 48 | To build `l1t` from the source code, make sure you have [cargo](https://crates.io/) from 49 | the [Rust](https://www.rust-lang.org/) toolchain. 50 | 51 | Once it's installed, you can use these steps: 52 | 53 | ```bash 54 | git clone https://github.com/alex-laycalvert/l1t 55 | cd l1t 56 | cargo build 57 | 58 | # Optionally, run directly with cargo 59 | cargo run -- # your options here... 60 | ``` 61 | 62 | The `l1t` executable will then be available under `target/release/l1t` 63 | 64 |
65 | 66 | ## Gameplay 67 | 68 | The following guide is also built in to `l1t` and can be accessed by selecting 69 | `HELP` from the main menu or pressing `Shift-H` at anytime while playing a level. 70 | 71 | ### CONTROLS 72 | 73 | - **W** - Move Up 74 | - **S**: Move Down 75 | - **A**: Move Left 76 | - **D**: Move Right 77 | - **Space**: Toggle surrounding blocks (if able) 78 | - **Shift-H**: Show this help menu 79 | - **Q**: Quit 80 | 81 | Arrow keys can also be used to move around the level 82 | 83 | ### PLAYER 84 | 85 | Hey, that's you! 86 | 87 | ### LASERS 88 | 89 | Lasers shoot laser beams in their set direction (UP, DOWN, LEFT, RIGHT). 90 | Laser beams are the key to winning the game and can affect various blocks. 91 | 92 | Lasers cannot change directions but they can be toggled on and off. 93 | 94 | If a laser beam hits you, you will die and have to restart the level. 95 | 96 | If a laser is hit by a laser beam, it will turn off and must be toggled on by the player. 97 | 98 | ### STATUES 99 | 100 | All statues in a level must be lit up by a laser beam to win the level. 101 | 102 | Statues can not be moved or manually toggled. 103 | 104 | ### REVERSE STATUES 105 | 106 | Same as statues except they must **NOT** be lit up to win the level. 107 | 108 | ### MIRRORS 109 | 110 | Mirrors reflect laser beams in different directions. 111 | 112 | Mirrors cannot be moved but their direction can be toggled by the player. 113 | 114 | ### MOVEABLE MIRRORS 115 | 116 | Moveable Mirrors are the same as mirrors except they **CAN** be moved. 117 | 118 | ### ZAPPERS 119 | 120 | If any Zappers are lit by a laser beam, you will lose the level. 121 | 122 | ### OTHER BLOCKS 123 | 124 | - **Walls**: Cannot be moved by player, will block laser beams. 125 | 126 | - **Blocks**: Can be moved around and will block laser beams. 127 | 128 | - **Toggle Blocks**: Cannot be moved. Switches and buttons can toggle these 129 | on and off. 130 | 131 | - **Switches**: When toggled, will turn toggle blocks on/off. 132 | 133 | - **Buttons**: When pressed, will turn toggle blocks on/off. Player must be 134 | next to button to press. 135 | -------------------------------------------------------------------------------- /REPO.md: -------------------------------------------------------------------------------- 1 | # Repositories 2 | 3 | Repositories allow you to how `l1t` levels on web server that anyone can access. 4 | 5 | ## Using Repositories 6 | 7 | To add a repo to your settings, add a line in your `$HOME/.l1t/repositories.l1t_conf` file: 8 | 9 | ``` 10 | My Repo Name = http://myrepourl.com 11 | ``` 12 | 13 | Where the left side of the `=` is the name of your repo and the right is the URL. 14 | 15 | ## Hosting Repositories 16 | 17 | To host a repository, you will need a web server that serves a `/l1t` route which returns the following JSON response: 18 | 19 | ```json 20 | { 21 | "levels": [ 22 | { 23 | "source": "", 24 | "name": "Level Name", 25 | "author": "Level Author", 26 | "description": "Leve Description" 27 | }, 28 | ... 29 | ] 30 | } 31 | ``` 32 | 33 | This response acts as the listing for every level that the repo hosts. All levels in this listing are expected to valid 34 | relative paths to the level file and contain the `name`, `author`, and `description` for each level. Any extra values returned 35 | will be ignored by the client. Each level must be a subroute of the `/l1t` route. 36 | 37 | For example, if your main repo path is `http://myrepo.com/` and you host one level (`Level 1`) hosted at `http://myrepo.com/l1t/level1.l1t`, 38 | then a get request to the path `http://myrepo.com/l1t` should return: 39 | 40 | ```json 41 | { 42 | "levels": [ 43 | { 44 | "source": "level1.l1t", 45 | "name": "Level 1", 46 | "author": "you", 47 | "description": "A Description" 48 | } 49 | ] 50 | } 51 | ``` 52 | 53 | If `Level 1` was instead hosted at `http://myrepo.com/l1t/levels/level1.l1t`, then `source` should be replaced with `levels/level1.l1t`. 54 | Note that since each level must have a source as a relative route of the main route, no level in this scenario can be hosted outside 55 | of the `/l1t` route directory. 56 | 57 | An example repository is provided in `examples/repo` and can be started with `python`: 58 | 59 | ```bash 60 | git clone https://github.com/alex-laycalvert/l1t 61 | cd l1t/examples/repo 62 | python -m http.server 63 | ``` 64 | 65 | Once it's started, add the following line in your `$HOME/.l1t/repositories.l1t_conf` file: 66 | 67 | ``` 68 | My Local Repo = http://localhost:8000/ 69 | ``` 70 | 71 | It can be tested by starting `l1t` and selecting `ONLINE` from the menu options. 72 | -------------------------------------------------------------------------------- /design/logo.txt: -------------------------------------------------------------------------------- 1 | /-------L 2 | ___ |__ _ 3 | |_ | <--/ | | \_ 4 | | | `| | | __| 5 | | | | | | | 6 | | |_ _|_|_ | |_ 7 | --\___\ |_____| --\__| 8 | | | 9 | v v 10 | S 11 | -------------------------------------------------------------------------------- /examples/repo/l1t: -------------------------------------------------------------------------------- 1 | { 2 | "levels": [ 3 | { 4 | "source": "levels/test_level1.l1t", 5 | "name": "Test Level 1", 6 | "author": "alex-laycalvert", 7 | "description": "A test repo level" 8 | }, 9 | { 10 | "source": "levels/test_level2.l1t", 11 | "name": "Test Level 2", 12 | "author": "alex-laycalvert", 13 | "description": "Another test repo" 14 | }, 15 | { 16 | "source": "levels/test_level3.l1t", 17 | "name": "Test Level 3", 18 | "author": "alex-laycalvert", 19 | "description": "The final test level" 20 | } 21 | ] 22 | } 23 | -------------------------------------------------------------------------------- /examples/repo/levels/test_level1.l1t: -------------------------------------------------------------------------------- 1 | IIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIII 2 | I I 3 | I / / / \ \ \ ? ? ? | | | I 4 | I I 5 | I / / / \ \ \ ? ? ? | | | I 6 | I I 7 | I / / / \ \ \ TTT I 8 | I TST I 9 | I X TTT I 10 | I I 11 | I 1 2 I 12 | I 3 8 s b S S S R Z Z I 13 | I I 14 | IIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIII 15 | -------------------------------------------------------------------------------- /examples/repo/levels/test_level2.l1t: -------------------------------------------------------------------------------- 1 | IIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIII 2 | I I 3 | I I 4 | I ? | I 5 | I I 6 | I I 7 | I / / \ \ I 8 | I I 9 | I I 10 | I I 11 | I S I 12 | I B B B I 13 | I B B B Z I 14 | I I 15 | I 1 X 5 Z I 16 | I 2 6 I 17 | I 3 7 I 18 | I 4 8 I 19 | IIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIII 20 | -------------------------------------------------------------------------------- /examples/repo/levels/test_level3.l1t: -------------------------------------------------------------------------------- 1 | IIIIIIIIIIIIIIIIIII 2 | I/ \I 3 | I1 X I 4 | I S b I 5 | I \T /I 6 | IIIIIIIIIIIIIIIIIII 7 | -------------------------------------------------------------------------------- /src/controls.rs: -------------------------------------------------------------------------------- 1 | use crossterm::event::{read, Event, KeyCode, KeyEventKind}; 2 | 3 | pub enum Control { 4 | Up, 5 | Down, 6 | Left, 7 | Right, 8 | Help, 9 | Quit, 10 | Action, 11 | Select, 12 | GotoTop, 13 | GotoBottom, 14 | None, 15 | } 16 | 17 | impl Control { 18 | pub fn read_input() -> Self { 19 | if let Ok(Event::Key(event)) = read() { 20 | if event.kind == KeyEventKind::Release { 21 | return Self::None; 22 | } 23 | return match event.code { 24 | KeyCode::Up | KeyCode::Char('w') | KeyCode::Char('k') => Self::Up, 25 | KeyCode::Down | KeyCode::Char('s') | KeyCode::Char('j') => Self::Down, 26 | KeyCode::Left | KeyCode::Char('a') | KeyCode::Char('h') => Self::Left, 27 | KeyCode::Right | KeyCode::Char('d') | KeyCode::Char('l') => Self::Right, 28 | KeyCode::Char('g') => Self::GotoTop, 29 | KeyCode::Char('G') => Self::GotoBottom, 30 | KeyCode::Char(' ') => Self::Action, 31 | KeyCode::Char('H') => Self::Help, 32 | KeyCode::Char('q') => Self::Quit, 33 | KeyCode::Enter => Self::Select, 34 | _ => Self::None, 35 | }; 36 | } 37 | Self::None 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /src/direction.rs: -------------------------------------------------------------------------------- 1 | /// A cardinal direction (`UP`, `DOWN`, `LEFT`, or `RIGHT`) that a node 2 | /// can be facing/looking in. Contains the unit-coordinates of the direction 3 | /// relative to the node. 4 | #[derive(PartialEq, Eq, Debug, Clone, Copy)] 5 | pub struct Direction(pub i16, pub i16); 6 | 7 | impl Direction { 8 | pub const UP: Self = Self(-1, 0); 9 | pub const DOWN: Self = Self(1, 0); 10 | pub const LEFT: Self = Self(0, -1); 11 | pub const RIGHT: Self = Self(0, 1); 12 | pub const FORWARD: Self = Self(0, 1); 13 | pub const BACKWARD: Self = Self(0, -1); 14 | } 15 | -------------------------------------------------------------------------------- /src/level.rs: -------------------------------------------------------------------------------- 1 | use crate::{controls::Control, direction::Direction, menu::*, node::*, repository::Repository}; 2 | use crossterm::{ 3 | cursor, execute, 4 | style::{Color, Print, SetBackgroundColor, SetForegroundColor, Stylize}, 5 | terminal::{size, Clear, ClearType}, 6 | ExecutableCommand, 7 | }; 8 | use std::{ 9 | fs, 10 | io::stdout, 11 | path::{Path, PathBuf}, 12 | }; 13 | 14 | #[derive(Debug)] 15 | pub enum LevelLossReason { 16 | Zapper, 17 | Quit, 18 | Death, 19 | } 20 | 21 | #[derive(Debug, Clone)] 22 | pub enum LevelSource { 23 | File(PathBuf), 24 | Url(String), 25 | Core(usize), 26 | } 27 | 28 | #[derive(Debug)] 29 | pub struct Level { 30 | pub info: LevelInfo, 31 | pub nodes: Vec, 32 | pub rows: u16, 33 | pub cols: u16, 34 | pub player_index: Option, 35 | } 36 | 37 | #[derive(Debug, Clone)] 38 | pub struct LevelInfo { 39 | pub source: LevelSource, 40 | pub name: String, 41 | pub author: String, 42 | pub description: String, 43 | } 44 | 45 | #[derive(Debug)] 46 | pub struct LevelResult { 47 | pub has_won: bool, 48 | pub reason_for_loss: Option, 49 | } 50 | 51 | #[derive(Debug)] 52 | struct PlayState { 53 | is_playing: bool, 54 | has_won: bool, 55 | reason_for_loss: Option, 56 | } 57 | 58 | impl Level { 59 | pub const NUM_CORE_LEVELS: usize = 5; 60 | pub const CORE_LEVELS: [&'static str; Level::NUM_CORE_LEVELS] = [ 61 | "Level 1 62 | alex-laycalvert 63 | The First Level 64 | IIIIIIIIIIIIIIIII 65 | I I 66 | I\\ / I 67 | I I 68 | I1 X S I 69 | I I 70 | IIIIIIIIIIIIIIIII", 71 | "Level 2 72 | alex-laycalvert 73 | Reverse Statues 74 | IIIIIIIIIIIIIIIII 75 | I I 76 | I R I 77 | I I 78 | I X I 79 | I I 80 | I4 / I 81 | IS \\ I 82 | IIIIIIIIIIIIIIIII", 83 | "Level 3 84 | alex-laycalvert 85 | Using Your Surroundings 86 | IIIIIIIIIIIIIIIIIII 87 | III I 88 | III R I 89 | III I 90 | III I 91 | I4 / B X I 92 | III I 93 | III I 94 | III R I 95 | III I 96 | IIIIIIIIIIIIIIIIIII", 97 | "Level 4 98 | alex-laycalvert 99 | What's That Special Block Over There? 100 | IIIIIIIIIIIIIIIIIIIII 101 | I Z \\ I 102 | I I 103 | I I 104 | I X 4 \\ I 105 | I I 106 | I I 107 | I S \\ \\ I 108 | IIIIIIIIIIIIIIIIIIIII", 109 | "Level 5 110 | alex-laycalvert 111 | Cramped 112 | IIIIIIIIIIIII 113 | I\\ \\ \\ \\ \\ \\I 114 | I I 115 | I/ / / / / /I 116 | I1 X SI 117 | IIIIIIIIIIIII", 118 | ]; 119 | 120 | fn draw_walls(&self, row_offset: u16, col_offset: u16) -> crossterm::Result<()> { 121 | let mut stdout = stdout(); 122 | for r in row_offset..(self.rows + row_offset) { 123 | for c in col_offset..(self.cols + col_offset) { 124 | if r == row_offset 125 | || r == self.rows + row_offset - 1 126 | || c == col_offset 127 | || c == self.cols + col_offset - 1 128 | { 129 | execute!( 130 | stdout, 131 | SetForegroundColor(Color::White), 132 | SetBackgroundColor(Color::White), 133 | cursor::MoveTo(c, r), 134 | Print('I'.bold()), 135 | )?; 136 | } 137 | } 138 | } 139 | execute!( 140 | stdout, 141 | SetForegroundColor(Color::Reset), 142 | SetBackgroundColor(Color::Reset), 143 | ) 144 | } 145 | 146 | fn draw_nodes(&self, row_offset: u16, col_offset: u16) -> crossterm::Result<()> { 147 | for i in 0..self.nodes.len() { 148 | self.nodes[i].draw((row_offset, col_offset))?; 149 | } 150 | Ok(()) 151 | } 152 | 153 | fn draw_node_overlays(&self, row_offset: u16, col_offset: u16) -> crossterm::Result<()> { 154 | for i in 0..self.nodes.len() { 155 | self.nodes[i].draw_overlay((row_offset, col_offset))?; 156 | } 157 | Ok(()) 158 | } 159 | 160 | fn draw(&self) -> crossterm::Result<()> { 161 | let mut stdout = stdout(); 162 | stdout.execute(Clear(ClearType::All))?; 163 | let (term_cols, term_rows) = size().unwrap_or((0, 0)); 164 | let row_offset = (term_rows - self.rows) / 2; 165 | let col_offset = (term_cols - self.cols) / 2; 166 | self.draw_walls(row_offset, col_offset)?; 167 | self.draw_node_overlays(row_offset, col_offset)?; 168 | self.draw_nodes(row_offset, col_offset)?; 169 | Ok(()) 170 | } 171 | 172 | fn set_lasers_shooting_at(&mut self) { 173 | for i in 0..self.nodes.len() { 174 | if let NodeType::Laser(l) = &self.nodes[i].node_type { 175 | if !l.on { 176 | self.nodes[i].set_shooting_at(vec![]); 177 | continue; 178 | } 179 | let mut shooting_at: Vec<(u16, u16, char, char)> = vec![]; 180 | let mut current_row: i16 = self.nodes[i].row as i16; 181 | let mut current_col: i16 = self.nodes[i].col as i16; 182 | let mut current_dir: Direction = l.dir; 183 | loop { 184 | if !self.is_valid_pos((current_row as u16, current_col as u16)) { 185 | break; 186 | } 187 | current_row += current_dir.0; 188 | current_col += current_dir.1; 189 | shooting_at.push(( 190 | current_row as u16, 191 | current_col as u16, 192 | match current_dir { 193 | Direction::UP | Direction::DOWN => '|', 194 | _ => '-', 195 | }, 196 | match current_dir { 197 | Direction::UP => '^', 198 | Direction::DOWN => 'v', 199 | Direction::LEFT => '<', 200 | _ => '>', 201 | }, 202 | )); 203 | if let Some(i) = self.node_index_at((current_row as u16, current_col as u16)) { 204 | match &self.nodes[i].node_type { 205 | NodeType::Mirror(m) => { 206 | if current_dir.0 == 0 { 207 | current_dir.0 = current_dir.1.abs(); 208 | if current_dir.1 == (m.dir.0 + m.dir.1) { 209 | current_dir.0 = -current_dir.0 210 | } 211 | current_dir.1 = 0; 212 | } else { 213 | current_dir.1 = current_dir.0.abs(); 214 | if current_dir.0 == (m.dir.0 + m.dir.1) { 215 | current_dir.1 = -current_dir.1 216 | } 217 | current_dir.0 = 0; 218 | } 219 | } 220 | NodeType::ToggleBlock(t) => { 221 | if t.visible { 222 | break; 223 | } 224 | } 225 | _ => { 226 | if self.nodes[i].is_laser_toggleable() { 227 | if let NodeType::Laser(_) = &self.nodes[i].node_type { 228 | self.nodes[i].turn_off(); 229 | } else { 230 | self.nodes[i].turn_on() 231 | } 232 | } 233 | break; 234 | } 235 | } 236 | } 237 | } 238 | self.nodes[i].set_shooting_at(shooting_at); 239 | } 240 | } 241 | } 242 | 243 | fn surrounding_nodes(&self, pos: (u16, u16)) -> Vec { 244 | let mut nodes: Vec = vec![]; 245 | for i in 0..self.nodes.len() { 246 | let n = &self.nodes[i]; 247 | if (n.row == pos.0 && (n.col == pos.1 - 1 || n.col == pos.1 + 1)) 248 | || (n.col == pos.1 && (n.row == pos.0 - 1 || n.row == pos.0 + 1)) 249 | { 250 | nodes.push(i); 251 | } 252 | } 253 | nodes 254 | } 255 | 256 | fn player_action(&mut self) { 257 | let player_index = match self.player_index { 258 | Some(i) => i, 259 | None => return, 260 | }; 261 | let surrounding_nodes = 262 | &self.surrounding_nodes((self.nodes[player_index].row, self.nodes[player_index].col)); 263 | for &i in surrounding_nodes.iter() { 264 | if !self.nodes[i].is_player_toggleable() 265 | || matches!(self.nodes[i].node_type, NodeType::Button(_)) 266 | { 267 | continue; 268 | } 269 | self.nodes[i].toggle(); 270 | if let NodeType::Switch(_) = &self.nodes[i].node_type { 271 | self.toggle_blocks(); 272 | } 273 | } 274 | } 275 | 276 | fn toggle_blocks(&mut self) { 277 | for i in 0..self.nodes.len() { 278 | if matches!(self.nodes[i].node_type, NodeType::ToggleBlock(_)) { 279 | self.nodes[i].toggle(); 280 | } 281 | } 282 | } 283 | 284 | fn node_index_at(&self, pos: (u16, u16)) -> Option { 285 | self.nodes 286 | .iter() 287 | .position(|n| n.row == pos.0 && n.col == pos.1) 288 | } 289 | 290 | fn is_valid_pos(&self, pos: (u16, u16)) -> bool { 291 | pos.0 >= 1 && pos.0 < self.rows - 1 && pos.1 >= 1 && pos.1 < self.cols - 1 292 | } 293 | 294 | fn move_player(&mut self, dir: Direction) { 295 | let player_index = match self.player_index { 296 | Some(i) => i, 297 | None => return, 298 | }; 299 | let new_pos = self.nodes[player_index].would_move_to(dir); 300 | if !self.is_valid_pos(new_pos) { 301 | return; 302 | } 303 | if let Some(i) = self.node_index_at(new_pos) { 304 | if !self.nodes[i].is_moveable() { 305 | return; 306 | } 307 | let new_pos = self.nodes[i].would_move_to(dir); 308 | if !self.is_valid_pos(new_pos) { 309 | return; 310 | } 311 | if self.node_index_at(new_pos).is_some() { 312 | return; 313 | } 314 | self.nodes[i].move_in_dir(dir); 315 | } 316 | self.nodes[player_index].move_in_dir(dir); 317 | } 318 | 319 | fn reset_statues(&mut self) { 320 | for i in 0..self.nodes.len() { 321 | if let NodeType::Statue(_) = &self.nodes[i].node_type { 322 | self.nodes[i].turn_off(); 323 | } 324 | } 325 | } 326 | 327 | fn play_state(&self) -> PlayState { 328 | let mut all_statues_lit = true; 329 | for i in 0..self.nodes.len() { 330 | match &self.nodes[i].node_type { 331 | NodeType::Statue(s) => { 332 | if s.reversed { 333 | all_statues_lit = all_statues_lit && !s.lit; 334 | } else { 335 | all_statues_lit = all_statues_lit && s.lit; 336 | } 337 | } 338 | NodeType::Zapper(z) => { 339 | if z.lit { 340 | return PlayState { 341 | is_playing: false, 342 | has_won: false, 343 | reason_for_loss: Some(LevelLossReason::Zapper), 344 | }; 345 | } 346 | } 347 | NodeType::Player(p) => { 348 | if p.dead { 349 | return PlayState { 350 | is_playing: false, 351 | has_won: false, 352 | reason_for_loss: Some(LevelLossReason::Death), 353 | }; 354 | } 355 | } 356 | _ => (), 357 | } 358 | } 359 | if !all_statues_lit { 360 | return PlayState { 361 | is_playing: true, 362 | has_won: false, 363 | reason_for_loss: None, 364 | }; 365 | } 366 | PlayState { 367 | is_playing: false, 368 | has_won: true, 369 | reason_for_loss: None, 370 | } 371 | } 372 | 373 | pub fn available_levels(level_dir: &Path) -> Result, String> { 374 | let files = match fs::read_dir(level_dir) { 375 | Ok(f) => f, 376 | Err(e) => return Err(e.to_string()), 377 | }; 378 | let mut filenames = Vec::::new(); 379 | for f in files { 380 | let f = match f { 381 | Ok(f) => f, 382 | Err(e) => return Err(e.to_string()), 383 | }; 384 | filenames.push(f.path().to_path_buf()); 385 | } 386 | let mut filenames: Vec<_> = filenames 387 | .iter() 388 | .map(|s| (s.as_path(), s.to_string_lossy())) 389 | .collect(); 390 | filenames.sort_by(|(_, s1), (_, s2)| human_sort::compare(s1, s2)); 391 | let mut levels = Vec::::new(); 392 | for (f, _) in filenames { 393 | let content = match fs::read_to_string(f) { 394 | Ok(c) => c, 395 | Err(e) => return Err(e.to_string()), 396 | }; 397 | let lines: Vec<&str> = content.split('\n').collect(); 398 | levels.push(LevelInfo { 399 | source: LevelSource::File(f.to_path_buf()), 400 | name: lines[0].to_string(), 401 | author: lines[1].to_string(), 402 | description: lines[2].to_string(), 403 | }); 404 | } 405 | Ok(levels) 406 | } 407 | 408 | fn parse_grid(content: &[&str], info: LevelInfo) -> Result { 409 | let rows = content.len() as u16; 410 | if rows < 3 { 411 | return Err("Level file must include a line for the `name`, `author`, `description`, and lines representing the level grid."); 412 | } 413 | let cols = content[0].len() as u16; 414 | if cols < 3 { 415 | return Err("Level grid must be made up of at least one grid space and an even wall of `I` characters representing the walls."); 416 | } 417 | let mut nodes: Vec = vec![]; 418 | let mut player_index: Option = None; 419 | for r in 0..rows { 420 | for (c, ch) in content[r as usize].chars().enumerate() { 421 | if r == 0 || r == rows - 1 || c == 0 || c == cols as usize - 1 { 422 | if ch != 'I' { 423 | return Err("Level grid must be made up of at least one grid space and an even wall of `I` characters representing the walls."); 424 | } 425 | continue; 426 | } 427 | if ch == ' ' { 428 | continue; 429 | } 430 | let node = Node::new(ch, r, c as u16); 431 | if matches!(node.node_type, NodeType::Player(_)) { 432 | player_index = Some(nodes.len()); 433 | } 434 | nodes.push(node); 435 | } 436 | } 437 | Ok(Level { 438 | info, 439 | nodes, 440 | rows, 441 | cols, 442 | player_index, 443 | }) 444 | } 445 | 446 | fn parse_full(content: &[&str], source: LevelSource) -> Result { 447 | if content.len() < 3 { 448 | return Err("Empty level file."); 449 | } 450 | let info = LevelInfo { 451 | source, 452 | name: content[0].to_string(), 453 | author: content[1].to_string(), 454 | description: content[2].to_string(), 455 | }; 456 | Level::parse_grid(&content[3..], info) 457 | } 458 | 459 | pub fn file(filename: PathBuf) -> Result { 460 | let content: String = fs::read_to_string(&filename).unwrap_or_default(); 461 | let content: Vec<&str> = content.trim().split('\n').collect(); 462 | Level::parse_full(&content, LevelSource::File(filename)) 463 | } 464 | 465 | pub async fn url(info: LevelInfo) -> Result { 466 | if let LevelSource::Url(url) = &info.source { 467 | let content = match Repository::download_from_url(url.to_string()).await { 468 | Ok(c) => c, 469 | Err(_) => return Err("Error downloading level"), 470 | }; 471 | let content: Vec<&str> = content.trim().split('\n').collect(); 472 | Level::parse_grid(&content, info) 473 | } else { 474 | Err("Level source was not a URL") 475 | } 476 | } 477 | 478 | pub fn core(level: usize) -> Result { 479 | let content = Level::CORE_LEVELS[level]; 480 | let content: Vec<&str> = content.trim().split('\n').collect(); 481 | Level::parse_full(&content, LevelSource::Core(level)) 482 | } 483 | 484 | pub fn play(&mut self) -> Result { 485 | loop { 486 | self.reset_statues(); 487 | self.set_lasers_shooting_at(); 488 | self.draw().ok(); 489 | let state = self.play_state(); 490 | if !state.is_playing { 491 | return Ok(LevelResult { 492 | has_won: state.has_won, 493 | reason_for_loss: state.reason_for_loss, 494 | }); 495 | } 496 | match Control::read_input() { 497 | Control::Up => self.move_player(Direction::UP), 498 | Control::Down => self.move_player(Direction::DOWN), 499 | Control::Left => self.move_player(Direction::LEFT), 500 | Control::Right => self.move_player(Direction::RIGHT), 501 | Control::Action => self.player_action(), 502 | Control::Help => { 503 | Menu::open(MenuType::HelpMenu); 504 | } 505 | Control::Quit => { 506 | if let Some(Selection::Yes) = 507 | Menu::open(MenuType::YesNoSelection("Are you sure you want to quit?")) 508 | { 509 | return Ok(LevelResult { 510 | has_won: false, 511 | reason_for_loss: Some(LevelLossReason::Quit), 512 | }); 513 | } 514 | } 515 | _ => (), 516 | } 517 | } 518 | } 519 | } 520 | -------------------------------------------------------------------------------- /src/lib.rs: -------------------------------------------------------------------------------- 1 | //! # l1t 2 | //! 3 | //! A strategy game about shooting laser beams and lighting up statues. 4 | //! Checkout out [the repo](https://github.com/alex-laycalvert/l1t) for 5 | //! more info. 6 | pub mod direction; 7 | pub mod controls; 8 | pub mod repository; 9 | pub mod level; 10 | pub mod menu; 11 | pub mod node; 12 | pub mod userdata; 13 | -------------------------------------------------------------------------------- /src/main.rs: -------------------------------------------------------------------------------- 1 | use clap::Parser; 2 | use crossterm::{ 3 | cursor, execute, 4 | terminal::{disable_raw_mode, enable_raw_mode, Clear, ClearType}, 5 | }; 6 | use home::home_dir; 7 | use l1t::level::*; 8 | use l1t::menu::*; 9 | //use l1t::repository::*; 10 | use l1t::userdata::*; 11 | use std::{ 12 | error::Error, 13 | io::stdout, 14 | path::{Path, PathBuf}, 15 | thread, time, 16 | }; 17 | 18 | const SLEEP_TIME: u64 = 500; 19 | 20 | /// A terminal based strategy game about shooting lasers and lighting statues 21 | #[derive(Parser, Debug)] 22 | #[command(author, version, about, long_about = None)] 23 | struct Args { 24 | /// The `.l1t` file to load a level from 25 | #[arg(short, long)] 26 | file: Option, 27 | ///// Repository to download levels from 28 | //#[arg(short, long)] 29 | //repo_url: Option, 30 | } 31 | 32 | enum PlayStatus<'a> { 33 | WonLevel, 34 | Quit, 35 | LostLevel, 36 | Error(&'a str), 37 | } 38 | 39 | fn setup() -> crossterm::Result<()> { 40 | enable_raw_mode()?; 41 | execute!(stdout(), cursor::Hide) 42 | } 43 | 44 | fn exit(error: Option<&str>) -> Result<(), Box> { 45 | disable_raw_mode()?; 46 | execute!( 47 | stdout(), 48 | cursor::Show, 49 | cursor::MoveTo(0, 0), 50 | Clear(ClearType::All) 51 | )?; 52 | if let Some(e) = error { 53 | eprintln!("Error: {e}"); 54 | } 55 | Ok(()) 56 | } 57 | 58 | #[tokio::main] 59 | async fn main() -> Result<(), Box> { 60 | //let repo = Repository::new( 61 | // "My Repo".to_string(), 62 | // "http://localhost:8000/l1t".to_string(), 63 | //) 64 | //.await 65 | //.ok(); 66 | 67 | let args = Args::parse(); 68 | setup().ok(); 69 | if let Some(filename) = &args.file { 70 | return play_file(filename); 71 | } 72 | 73 | let home = match home_dir() { 74 | Some(h) => h, 75 | None => return exit(Some("failed to find user's home directory")), 76 | }; 77 | let home = home.to_str().unwrap_or(""); 78 | let user_data = match UserData::read(home.to_string()) { 79 | Ok(d) => d, 80 | Err(e) => return exit(Some(&e)), 81 | }; 82 | 83 | play(user_data).await 84 | } 85 | 86 | async fn play(mut user_data: UserData) -> Result<(), Box> { 87 | loop { 88 | let selection = Menu::open(MenuType::MainSelection(&user_data.completed_core_levels)) 89 | .unwrap_or(Selection::Play(LevelSource::Core(0))); 90 | match selection { 91 | Selection::Play(level_source) => match level_source { 92 | LevelSource::Core(level) => { 93 | let mut current_level = level; 94 | loop { 95 | if current_level >= Level::NUM_CORE_LEVELS { 96 | Menu::open(MenuType::Message( 97 | "You've completed all core levels, thanks for playing!", 98 | )); 99 | break; 100 | } 101 | let mut level = match Level::core(current_level) { 102 | Ok(l) => l, 103 | Err(e) => return exit(Some(e)), 104 | }; 105 | let result = level.play(); 106 | match handle_level_result(result) { 107 | PlayStatus::WonLevel => { 108 | if let Err(e) = user_data.complete(level.info) { 109 | return exit(Some(&e)); 110 | }; 111 | current_level += 1; 112 | } 113 | PlayStatus::LostLevel => continue, 114 | PlayStatus::Quit => break, 115 | PlayStatus::Error(e) => return exit(Some(e)), 116 | } 117 | } 118 | } 119 | LevelSource::File(_) => {} 120 | LevelSource::Url(_url) => {} 121 | }, 122 | Selection::Repository => { 123 | if user_data.repositories.is_empty() { 124 | Menu::open(MenuType::Message( 125 | "You don't have any repositories setup in $HOME/.l1t/repositories.l1t_conf", 126 | )); 127 | continue; 128 | } 129 | while let Some(Selection::Item(i)) = 130 | Menu::open(MenuType::RepositorySelection(&user_data.repositories)) 131 | { 132 | if let Err(e) = user_data.repositories[i].download_listing().await { 133 | Menu::open(MenuType::Message(&e.to_string())); 134 | continue; 135 | }; 136 | loop { 137 | if let Some(selection) = Menu::open(MenuType::RepositoryLevelSelection( 138 | user_data.repositories[i].clone(), 139 | &user_data.completed_levels, 140 | )) { 141 | match selection { 142 | Selection::Item(j) => { 143 | let level_info = &user_data.repositories[i].levels[j]; 144 | loop { 145 | let mut level = match Level::url(level_info.clone()).await { 146 | Ok(l) => l, 147 | Err(e) => return exit(Some(e)), 148 | }; 149 | let result = level.play(); 150 | match handle_level_result(result) { 151 | PlayStatus::WonLevel => { 152 | if let Err(e) = user_data.complete(level.info) { 153 | return exit(Some(&e)); 154 | }; 155 | break; 156 | } 157 | PlayStatus::LostLevel => continue, 158 | PlayStatus::Quit => break, 159 | PlayStatus::Error(e) => return exit(Some(e)), 160 | } 161 | } 162 | } 163 | Selection::Quit => break, 164 | _ => continue, 165 | } 166 | } 167 | } 168 | } 169 | } 170 | Selection::Help => { 171 | Menu::open(MenuType::HelpMenu); 172 | } 173 | _ => break, 174 | } 175 | } 176 | exit(None) 177 | } 178 | 179 | fn play_file(filename: &Path) -> Result<(), Box> { 180 | loop { 181 | let mut level = match Level::file(filename.to_path_buf()) { 182 | Ok(l) => l, 183 | Err(e) => return exit(Some(e)), 184 | }; 185 | let result = level.play(); 186 | match handle_level_result(result) { 187 | PlayStatus::WonLevel | PlayStatus::Quit => break, 188 | PlayStatus::LostLevel => continue, 189 | PlayStatus::Error(e) => return exit(Some(e)), 190 | } 191 | } 192 | exit(None) 193 | } 194 | 195 | fn handle_level_result(result: Result) -> PlayStatus { 196 | match result { 197 | Ok(result) => { 198 | if result.has_won { 199 | thread::sleep(time::Duration::from_millis(SLEEP_TIME)); 200 | Menu::open(MenuType::Message("YAY, You Won!")); 201 | PlayStatus::WonLevel 202 | } else if let Some(r) = result.reason_for_loss { 203 | match r { 204 | LevelLossReason::Zapper => { 205 | thread::sleep(time::Duration::from_millis(SLEEP_TIME)); 206 | Menu::open(MenuType::Message("Uh oh, you lit a zapper!")); 207 | PlayStatus::LostLevel 208 | } 209 | LevelLossReason::Death => { 210 | thread::sleep(time::Duration::from_millis(SLEEP_TIME)); 211 | Menu::open(MenuType::Message("Uh oh, you got shot by a laser beam!")); 212 | PlayStatus::LostLevel 213 | } 214 | LevelLossReason::Quit => PlayStatus::Quit, 215 | } 216 | } else { 217 | PlayStatus::Quit 218 | } 219 | } 220 | Err(e) => PlayStatus::Error(e), 221 | } 222 | } 223 | -------------------------------------------------------------------------------- /src/menu.rs: -------------------------------------------------------------------------------- 1 | use crate::{ 2 | controls::Control, 3 | level::{Level, LevelSource}, 4 | repository::Repository, 5 | userdata::CompletedRepoLevel, 6 | }; 7 | use crossterm::{ 8 | cursor::MoveTo, 9 | execute, 10 | style::{ 11 | Attribute, Color, Print, ResetColor, SetAttribute, SetBackgroundColor, SetForegroundColor, 12 | StyledContent, Stylize, 13 | }, 14 | terminal::{size, Clear, ClearType}, 15 | }; 16 | use std::io::stdout; 17 | 18 | #[derive(Clone)] 19 | pub enum Selection { 20 | Play(LevelSource), 21 | Repository, 22 | Help, 23 | Quit, 24 | Yes, 25 | No, 26 | Item(usize), 27 | } 28 | 29 | pub enum MenuType<'a> { 30 | /// Dialog box with a single message. Press `Enter` or `q` to close. 31 | Message(&'a str), 32 | 33 | /// Dialog box that displays the message `String` and will 34 | /// show a list of the given option `Strings`. Returns the 35 | /// index of the selected option on enter. 36 | Selection(&'a str, Vec), 37 | 38 | /// Same as `MenuType::Selection` but only allows selecting 39 | /// `Yes` or `No`. 40 | YesNoSelection(&'a str), 41 | 42 | /// Same as `Message` but displays the entire help menu for 43 | /// the application in a `ScrollableMenu`. 44 | HelpMenu, 45 | 46 | /// Same as `Message` but displays more content and can be scrolled in. 47 | /// The `Vec>>` represents the list 48 | /// of `crossterm` styled lines where each inner `Vec` represents 49 | /// the list of chunks to print. 50 | ScrollableMenu(Vec>>), 51 | 52 | /// Draws the `Main Menu` of the application with the logo 53 | /// and selections for `Play`, `Help`, and `Quit`. Must 54 | /// provide a `Vec` representing the core levels the 55 | /// player has completed and a `Vec` which is 56 | /// the list of available repositories the user has. 57 | /// 58 | /// Selecting `Play` will open the `CoreLevelSelection` and 59 | /// will return a `Selection::Play(l)` where `l` is the selected 60 | /// level. Selecting `Repository` or `Online` from the menu will 61 | /// return a `Selection::Play(l)` where `l` is the selected repository 62 | /// level. 63 | MainSelection(&'a Vec), 64 | 65 | /// Draws the `Core Level` selection menu for the player 66 | /// to choose one of the built-in levels. Must be provided 67 | /// a `Vec` representing the core levels the player 68 | /// has completed. 69 | CoreLevelSelection(&'a Vec), 70 | 71 | /// Draws the `Repository` selection menu to allow 72 | /// the player to select which repo they want to play 73 | /// a level from. After selecting a repo, the `RepositoryLevelSelection` 74 | /// is opened to select the url of the level. 75 | RepositorySelection(&'a Vec), 76 | 77 | RepositoryLevelSelection(Repository, &'a Vec), 78 | } 79 | 80 | const RED: Color = Color::Rgb { r: 255, g: 0, b: 0 }; 81 | const YELLOW: Color = Color::Rgb { 82 | r: 255, 83 | g: 255, 84 | b: 0, 85 | }; 86 | 87 | pub struct Menu; 88 | 89 | impl Menu { 90 | fn draw_borders( 91 | start_row: u16, 92 | end_row: u16, 93 | start_col: u16, 94 | end_col: u16, 95 | ) -> crossterm::Result<()> { 96 | for r in start_row..=end_row { 97 | for c in start_col..=end_col { 98 | if r == start_row || r == end_row { 99 | execute!(stdout(), MoveTo(c, r), Print("─"),)?; 100 | } else if c == start_col || c == end_col { 101 | execute!(stdout(), MoveTo(c, r), Print("│"),)?; 102 | } else { 103 | execute!(stdout(), MoveTo(c, r), Print(" "),)?; 104 | } 105 | } 106 | } 107 | execute!( 108 | stdout(), 109 | MoveTo(start_col, start_row), 110 | Print("┌"), 111 | MoveTo(end_col, start_row), 112 | Print("┐"), 113 | MoveTo(start_col, end_row), 114 | Print("└"), 115 | MoveTo(end_col, end_row), 116 | Print("┘"), 117 | ) 118 | } 119 | 120 | pub fn open(menu_type: MenuType) -> Option { 121 | let row_padding = 1; 122 | let col_padding = 2; 123 | match menu_type { 124 | MenuType::MainSelection(completed_levels) => { 125 | let row_padding = 2; 126 | let col_padding = 3; 127 | let options: [Selection; 4] = [ 128 | Selection::Play(LevelSource::Core(0)), 129 | Selection::Repository, 130 | Selection::Help, 131 | Selection::Quit, 132 | ]; 133 | let mut current_selection = 0; 134 | loop { 135 | let (term_cols, term_rows) = size().unwrap_or((0, 0)); 136 | let start_row: u16 = 137 | (term_rows - options.len() as u16 * 2 - 10 - row_padding) / 2 - row_padding; 138 | let mut start_col: u16 = (term_cols - 23) / 2 - col_padding; 139 | let end_row: u16 = 140 | (term_rows + options.len() as u16 + 10 + row_padding) / 2 + row_padding; 141 | let end_col: u16 = (term_cols + 23) / 2 + col_padding; 142 | if (end_col - start_col) % 2 != 0 { 143 | start_col -= 1; 144 | } 145 | execute!(stdout(), Clear(ClearType::All)).ok(); 146 | Menu::draw_borders(start_row, end_row, start_col, end_col).ok(); 147 | execute!( 148 | stdout(), 149 | SetAttribute(Attribute::Bold), 150 | MoveTo(start_col + col_padding, start_row + row_padding + 1), 151 | Print(" /"), 152 | SetForegroundColor(RED), 153 | Print("-------"), 154 | SetBackgroundColor(RED), 155 | Print("L"), 156 | SetBackgroundColor(Color::Reset), 157 | MoveTo(start_col + col_padding, start_row + row_padding + 2), 158 | SetForegroundColor(Color::Green), 159 | Print(" ___ "), 160 | SetForegroundColor(RED), 161 | Print("|"), 162 | SetForegroundColor(Color::Green), 163 | Print("__ _"), 164 | MoveTo(start_col + col_padding, start_row + row_padding + 3), 165 | Print("|_ | "), 166 | SetForegroundColor(RED), 167 | Print("<--"), 168 | SetForegroundColor(Color::White), 169 | Print("/"), 170 | SetForegroundColor(Color::Green), 171 | Print(" | | \\_"), 172 | MoveTo(start_col + col_padding, start_row + row_padding + 4), 173 | Print(" | | `| | | __|"), 174 | MoveTo(start_col + col_padding, start_row + row_padding + 5), 175 | Print(" | | | | | |"), 176 | MoveTo(start_col + col_padding, start_row + row_padding + 6), 177 | Print(" | |_ _|_|_ | |_ "), 178 | MoveTo(start_col + col_padding, start_row + row_padding + 7), 179 | SetForegroundColor(RED), 180 | Print("--"), 181 | SetForegroundColor(Color::White), 182 | Print("\\"), 183 | SetForegroundColor(Color::Green), 184 | Print("___\\ |_____| "), 185 | SetForegroundColor(RED), 186 | Print("--"), 187 | SetForegroundColor(Color::White), 188 | Print("\\"), 189 | SetForegroundColor(Color::Green), 190 | Print("__|"), 191 | MoveTo(start_col + col_padding, start_row + row_padding + 8), 192 | SetForegroundColor(RED), 193 | Print(" | v"), 194 | MoveTo(start_col + col_padding, start_row + row_padding + 9), 195 | Print(" v"), 196 | MoveTo(start_col + col_padding, start_row + row_padding + 10), 197 | Print(" "), 198 | SetForegroundColor(YELLOW), 199 | SetBackgroundColor(YELLOW), 200 | Print("S"), 201 | ResetColor, 202 | ) 203 | .ok(); 204 | for (i, _) in options.iter().enumerate() { 205 | let option = match options[i] { 206 | Selection::Play(_) => "P L A Y", 207 | Selection::Repository => "O N L I N E", 208 | Selection::Help => "H E L P", 209 | Selection::Quit => "Q U I T", 210 | _ => "", 211 | }; 212 | execute!( 213 | stdout(), 214 | SetForegroundColor(if i == current_selection { 215 | Color::Black 216 | } else { 217 | Color::White 218 | }), 219 | SetBackgroundColor(if i == current_selection { 220 | Color::White 221 | } else { 222 | Color::Reset 223 | }), 224 | MoveTo( 225 | (term_cols - 23) / 2, 226 | start_row + row_padding * 2 + i as u16 * 2 + 10, 227 | ), 228 | SetAttribute(Attribute::Bold), 229 | Print(format!("{:^23}", option)), 230 | ResetColor, 231 | ) 232 | .ok(); 233 | } 234 | match Control::read_input() { 235 | Control::Select => match options[current_selection] { 236 | Selection::Play(_) => { 237 | if let Some(Selection::Item(i)) = 238 | Menu::open(MenuType::CoreLevelSelection(completed_levels)) 239 | { 240 | return Some(Selection::Play(LevelSource::Core(i))); 241 | } 242 | } 243 | _ => break, 244 | }, 245 | Control::Up => { 246 | if current_selection == 0 { 247 | current_selection = options.len() - 1; 248 | } else { 249 | current_selection -= 1; 250 | } 251 | } 252 | Control::Down => { 253 | current_selection = (current_selection + 1) % options.len(); 254 | } 255 | Control::Quit => return Some(Selection::Quit), 256 | _ => (), 257 | } 258 | } 259 | return Some(options[current_selection].clone()); 260 | } 261 | MenuType::Message(message) => loop { 262 | let (term_cols, term_rows) = size().unwrap_or((0, 0)); 263 | let start_row: u16 = term_rows / 2 - row_padding - 1; 264 | let start_col: u16 = 265 | (term_cols - (term_cols - 4).min(message.len() as u16)) / 2 - col_padding; 266 | let end_row: u16 = (term_rows + row_padding) / 2 + row_padding; 267 | let end_col: u16 = (term_cols + message.len() as u16) / 2 + col_padding; 268 | Menu::draw_borders(start_row, end_row, start_col, end_col).ok(); 269 | execute!( 270 | stdout(), 271 | MoveTo(start_col + 2, term_rows / 2), 272 | Print(message), 273 | ) 274 | .ok(); 275 | if let Control::Select = Control::read_input() { 276 | break; 277 | } 278 | }, 279 | MenuType::YesNoSelection(message) => { 280 | let mut current_selection = Selection::No; 281 | loop { 282 | let (term_cols, term_rows) = size().unwrap_or((0, 0)); 283 | let start_row: u16 = term_rows / 2 - row_padding - 1; 284 | let start_col: u16 = (term_cols - message.len() as u16) / 2 - col_padding; 285 | let end_row: u16 = (term_rows + row_padding) / 2 + row_padding + 2; 286 | let end_col: u16 = (term_cols + message.len() as u16) / 2 + col_padding; 287 | Menu::draw_borders(start_row, end_row, start_col, end_col).ok(); 288 | execute!( 289 | stdout(), 290 | MoveTo((term_cols - message.len() as u16) / 2, term_rows / 2), 291 | Print(message), 292 | ) 293 | .ok(); 294 | execute!( 295 | stdout(), 296 | MoveTo(term_cols / 2 - 6, end_row - row_padding - 1), 297 | SetForegroundColor(if matches!(current_selection, Selection::Yes) { 298 | Color::Black 299 | } else { 300 | Color::White 301 | }), 302 | SetBackgroundColor(if matches!(current_selection, Selection::Yes) { 303 | Color::White 304 | } else { 305 | Color::Reset 306 | }), 307 | Print(" YES ".bold()), 308 | MoveTo(term_cols / 2 + 1, end_row - row_padding - 1), 309 | SetForegroundColor(if matches!(current_selection, Selection::No) { 310 | Color::Black 311 | } else { 312 | Color::White 313 | }), 314 | SetBackgroundColor(if matches!(current_selection, Selection::No) { 315 | Color::White 316 | } else { 317 | Color::Reset 318 | }), 319 | Print(" NO ".bold()) 320 | ) 321 | .ok(); 322 | match Control::read_input() { 323 | Control::Left | Control::Right => { 324 | if matches!(current_selection, Selection::No) { 325 | current_selection = Selection::Yes 326 | } else { 327 | current_selection = Selection::No 328 | } 329 | } 330 | Control::Select => return Some(current_selection), 331 | Control::Quit => return Some(Selection::No), 332 | _ => (), 333 | } 334 | } 335 | } 336 | MenuType::HelpMenu => { 337 | return Menu::open(MenuType::ScrollableMenu(vec![ 338 | vec![], 339 | vec![ 340 | "In ".stylize(), 341 | "l1t".bold().green(), 342 | ", your goal is to use the available lasers ".stylize(), 343 | ], 344 | vec!["to light up all of the statues in the level.".stylize()], 345 | vec![], 346 | vec!["CONTROLS".bold().underlined()], 347 | vec![], 348 | vec![" W - ".bold(), "Move Up".stylize()], 349 | vec![" S - ".bold(), "Move Down".stylize()], 350 | vec![" A - ".bold(), "Move Left".stylize()], 351 | vec![" D - ".bold(), "Move Right".stylize()], 352 | vec![ 353 | " Space - ".bold(), 354 | "Toggle surrounding blocks (if able)".stylize(), 355 | ], 356 | vec![" Shift-H - ".bold(), "Show this help menu".stylize()], 357 | vec![" Q - ".bold(), "Quit".stylize()], 358 | vec![], 359 | vec!["Arrow keys can also be used to move around the ".stylize()], 360 | vec!["level".stylize()], 361 | vec![], 362 | vec![ 363 | "X".green().on_green(), 364 | " ".stylize(), 365 | "PLAYER".bold().underlined(), 366 | ], 367 | vec![], 368 | vec!["Hey, that's you!".stylize()], 369 | vec![], 370 | vec![ 371 | "L".with(RED).on(RED), 372 | " ".stylize(), 373 | "LASERS".bold().underlined(), 374 | ], 375 | vec![], 376 | vec!["Lasers shoot laser beams in their set direction".stylize()], 377 | vec![ 378 | "(".stylize(), 379 | "UP, DOWN, LEFT, RIGHT".bold(), 380 | "). Laser beams are the key".stylize(), 381 | ], 382 | vec!["to winning the game and can affect various ".stylize()], 383 | vec!["blocks.".stylize()], 384 | vec![], 385 | vec!["Lasers cannot change directions but they can".stylize()], 386 | vec!["be toggled on and off.".stylize()], 387 | vec![], 388 | vec![ 389 | "If a laser hits you, you'll ".stylize(), 390 | "die".with(RED).bold(), 391 | " and have to ".stylize(), 392 | ], 393 | vec!["restart the level.".stylize()], 394 | vec![], 395 | vec!["If a laser is hit by a laser beam, it will".stylize()], 396 | vec!["turn off and must be toggled on by the player.".stylize()], 397 | vec![], 398 | vec![ 399 | "S".with(YELLOW).on(YELLOW), 400 | " ".stylize(), 401 | "STATUES".bold().underlined(), 402 | ], 403 | vec![], 404 | vec!["All statues in a level must be lit up by a ".stylize()], 405 | vec![ 406 | "laser beam to ".stylize(), 407 | "win".with(YELLOW).bold(), 408 | " the level.".stylize(), 409 | ], 410 | vec![], 411 | vec!["Statues can not be moved or manually toggled.".stylize()], 412 | vec![], 413 | vec![ 414 | "R".bold().black().on(YELLOW), 415 | " ".stylize(), 416 | "REVERSE STATUES".bold().underlined(), 417 | ], 418 | vec![], 419 | vec![ 420 | "Same as statues except they must ".stylize(), 421 | "NOT".bold().italic(), 422 | " be lit up ".stylize(), 423 | ], 424 | vec![ 425 | "to ".stylize(), 426 | "win".with(YELLOW).bold(), 427 | " the level.".stylize(), 428 | ], 429 | vec![], 430 | vec!["/ ".bold(), "MIRRORS".bold().underlined()], 431 | vec![], 432 | vec!["Mirrors reflect laser beams in different".stylize()], 433 | vec!["directions.".stylize()], 434 | vec![], 435 | vec![" ".stylize(), "L".with(RED).on(RED)], 436 | vec![" |".bold().with(RED)], 437 | vec![ 438 | "L".with(RED).on(RED), 439 | "----".bold().with(RED), 440 | "\\".bold(), 441 | " <--".bold().with(RED), 442 | "/".bold(), 443 | ], 444 | vec![" |".with(RED).bold()], 445 | vec![" V".with(RED).bold()], 446 | vec![], 447 | vec!["Mirrors cannot be moved but their direction can ".stylize()], 448 | vec!["be toggled by the player.".stylize()], 449 | vec![], 450 | vec![ 451 | "/".black().on_white().bold(), 452 | " ".stylize(), 453 | "MOVEABLE MIRRORS".bold().underlined(), 454 | ], 455 | vec![], 456 | vec!["Moveable Mirrors are the same as mirrors except ".stylize()], 457 | vec![ 458 | "they ".stylize(), 459 | "CAN ".bold().italic(), 460 | "be moved.".stylize(), 461 | ], 462 | vec![], 463 | vec![ 464 | "Z".bold().yellow().on_black(), 465 | " ".stylize(), 466 | "ZAPPERS".bold().underlined(), 467 | ], 468 | vec![], 469 | vec!["If any Zappers are lit by a laser beam, you".stylize()], 470 | vec![ 471 | "will immediately ".stylize(), 472 | "lose".with(RED).bold(), 473 | " the level.".stylize(), 474 | ], 475 | vec![], 476 | vec![ 477 | "I".bold().white().on_white(), 478 | " ".stylize(), 479 | "B".bold().grey().on_grey(), 480 | " ".stylize(), 481 | "s".bold().black().on_red(), 482 | " ".stylize(), 483 | "OTHER BLOCKS".bold().underlined(), 484 | ], 485 | vec![], 486 | vec![ 487 | "I".bold().white().on_white(), 488 | " Walls - ".bold(), 489 | "Cannot be moved by player, will block".stylize(), 490 | ], 491 | vec![" laser beams.".stylize()], 492 | vec![], 493 | vec![ 494 | "B".bold().grey().on_grey(), 495 | " Blocks - ".bold(), 496 | "Can be moved around and will block".stylize(), 497 | ], 498 | vec![" laser beams.".stylize()], 499 | vec![], 500 | vec![ 501 | "T".bold().magenta().on_magenta(), 502 | " Toggle Blocks - ".bold(), 503 | "Cannot be moved. Switches and".stylize(), 504 | ], 505 | vec![" buttons can toggle these on".stylize()], 506 | vec![" and off.".stylize()], 507 | vec![], 508 | vec![ 509 | "s".bold().black().on_red(), 510 | " Switches - ".bold(), 511 | "When toggled, will turn toggle".stylize(), 512 | ], 513 | vec![" blocks on/off.".stylize()], 514 | vec![], 515 | vec![ 516 | "b".bold().black().on_red(), 517 | " Buttons - ".bold(), 518 | "When pressed, will turn toggle".stylize(), 519 | ], 520 | vec![" blocks on/off. Player must be".stylize()], 521 | vec![" next to button to press.".stylize()], 522 | vec![], 523 | vec!["REPOSITORIES".bold().underlined()], 524 | vec![], 525 | vec!["Repositories allow you to play levels hosted ".stylize()], 526 | vec!["online. To add a repository, add a line in ".stylize()], 527 | vec!["your `$HOME/.l1t/repositories.l1t_conf` file:".stylize()], 528 | vec![], 529 | vec!["My Repo Name = http://myrepourl.com".stylize()], 530 | vec![], 531 | vec!["The left side of the `=` is the repository ".stylize()], 532 | vec!["name and the right is the URL of the repo.".stylize()], 533 | vec![], 534 | vec!["To host your own repo, checkout the repo ".stylize()], 535 | vec!["documentation at:".stylize()], 536 | vec![], 537 | vec!["https://github.com/alex-laycalvert/l1t/".stylize()], 538 | ])) 539 | } 540 | MenuType::ScrollableMenu(content) => { 541 | let mut start_index: usize = 0; 542 | let scroll_message = " USE ARROW KEYS OR W, S TO SCROLL "; 543 | let fast_scroll_message = " USE g AND G to GOTO TOP AND BOTTOM "; 544 | loop { 545 | let (term_cols, term_rows) = size().unwrap_or((0, 0)); 546 | let lines: usize = (term_rows - row_padding * 2) as usize - 6; 547 | let start_row = (term_rows - lines as u16) / 2 - row_padding; 548 | let end_row = (term_rows + lines as u16) / 2 + row_padding; 549 | let start_col = (term_cols - 50) / 2 - col_padding; 550 | let end_col = (term_cols + 50) / 2 + col_padding; 551 | execute!( 552 | stdout(), 553 | Clear(ClearType::All), 554 | MoveTo((term_cols - scroll_message.len() as u16) / 2, start_row - 1), 555 | Print(scroll_message.on_white().black().bold()), 556 | MoveTo( 557 | (term_cols - fast_scroll_message.len() as u16) / 2, 558 | end_row + 1 559 | ), 560 | Print(fast_scroll_message.on_white().black().bold()) 561 | ) 562 | .ok(); 563 | Menu::draw_borders(start_row, end_row, start_col, end_col).ok(); 564 | for (i, line) in content 565 | .iter() 566 | .enumerate() 567 | .take((start_index + lines).min(content.len())) 568 | .skip(start_index) 569 | { 570 | execute!( 571 | stdout(), 572 | MoveTo( 573 | start_col + col_padding + 1, 574 | start_row + row_padding + (i - start_index) as u16 575 | ) 576 | ) 577 | .ok(); 578 | for piece in line.iter() { 579 | execute!(stdout(), Print(piece)).ok(); 580 | } 581 | } 582 | match Control::read_input() { 583 | Control::Up => { 584 | if start_index == 0 { 585 | continue; 586 | } 587 | start_index -= 1; 588 | } 589 | Control::Down => { 590 | if start_index + lines >= content.len() { 591 | continue; 592 | } 593 | start_index += 1; 594 | } 595 | Control::GotoTop => start_index = 0, 596 | Control::GotoBottom => start_index = content.len() - lines, 597 | Control::Select | Control::Quit => break, 598 | _ => (), 599 | } 600 | } 601 | } 602 | MenuType::CoreLevelSelection(completed_levels) => { 603 | let num_levels = Level::NUM_CORE_LEVELS as f64; 604 | let levels_per_row = num_levels.sqrt() as u16; 605 | let num_rows = (num_levels / levels_per_row as f64).ceil() as u16; 606 | let highest_available_level = match completed_levels.iter().max() { 607 | Some(n) => *n.min(&(Level::NUM_CORE_LEVELS - 1)) + 1, 608 | None => 0, 609 | }; 610 | let highest_available_level = if highest_available_level >= Level::NUM_CORE_LEVELS { 611 | Level::NUM_CORE_LEVELS - 1 612 | } else { 613 | highest_available_level 614 | }; 615 | let mut current_selection = highest_available_level; 616 | let message = " SELECT A LEVEL "; 617 | loop { 618 | let (term_cols, term_rows) = size().unwrap_or((0, 0)); 619 | let start_row: u16 = (term_rows - num_rows * 2) / 2; 620 | let mut start_col: u16 = (term_cols / 2) - levels_per_row * 2; 621 | let end_row: u16 = (term_rows + num_rows * 2) / 2; 622 | let end_col: u16 = (term_cols / 2) + levels_per_row * 2; 623 | if (end_col - start_col + 1) % 2 != 0 { 624 | start_col -= 1; 625 | } 626 | execute!( 627 | stdout(), 628 | Clear(ClearType::All), 629 | MoveTo((term_cols - message.len() as u16) / 2, start_row - 1), 630 | Print(message.on_white().black().bold()) 631 | ) 632 | .ok(); 633 | Menu::draw_borders(start_row, end_row, start_col, end_col).ok(); 634 | for i in 0..Level::NUM_CORE_LEVELS { 635 | let is_available = i <= highest_available_level; 636 | let fg_color = if is_available && current_selection != i { 637 | Color::White 638 | } else { 639 | Color::Black 640 | }; 641 | execute!( 642 | stdout(), 643 | MoveTo( 644 | (i as u16 % levels_per_row) * 2 645 | + start_col 646 | + col_padding 647 | + (i as u16 % levels_per_row) * 2, 648 | start_row + 1 + (i as u16 / levels_per_row) * 2, 649 | ), 650 | SetForegroundColor(fg_color), 651 | SetBackgroundColor(if current_selection == i { 652 | Color::White 653 | } else { 654 | Color::Reset 655 | }), 656 | Print(format!("{:0>2}", (i + 1).to_string()).bold()), 657 | ) 658 | .ok(); 659 | } 660 | match Control::read_input() { 661 | Control::Up => { 662 | if current_selection == 0 { 663 | current_selection = highest_available_level; 664 | } else if current_selection >= levels_per_row.into() { 665 | current_selection -= levels_per_row as usize; 666 | } else { 667 | current_selection = 0; 668 | } 669 | } 670 | Control::Down => { 671 | if current_selection == highest_available_level { 672 | current_selection = 0; 673 | } else if current_selection 674 | < (Level::NUM_CORE_LEVELS as i16 - levels_per_row as i16) as usize 675 | && current_selection + (levels_per_row as usize) 676 | <= highest_available_level 677 | { 678 | current_selection += levels_per_row as usize; 679 | } else { 680 | current_selection = highest_available_level; 681 | } 682 | } 683 | Control::Left => { 684 | if current_selection == 0 { 685 | current_selection = highest_available_level; 686 | } else { 687 | current_selection -= 1; 688 | } 689 | } 690 | Control::Right => { 691 | if current_selection >= highest_available_level { 692 | current_selection = 0; 693 | } else { 694 | current_selection += 1; 695 | } 696 | } 697 | Control::Quit => return None, 698 | Control::Select => return Some(Selection::Item(current_selection)), 699 | _ => (), 700 | } 701 | } 702 | } 703 | MenuType::RepositorySelection(repositories) => { 704 | let message = " SELECT A REPO "; 705 | let mut current_selection = 0; 706 | loop { 707 | let (term_cols, term_rows) = size().unwrap_or((0, 0)); 708 | let num_cols = (term_cols - 4).min(200) as usize; 709 | let repo_name_len = num_cols / 2 - 2; 710 | let repo_url_len = num_cols - 4 - repo_name_len; 711 | let start_row: u16 = (term_rows - repositories.len() as u16) / 2; 712 | let start_col: u16 = (term_cols - num_cols as u16) / 2; 713 | let end_row: u16 = (term_rows + repositories.len() as u16) / 2 + 1; 714 | let end_col: u16 = (term_cols + num_cols as u16) / 2; 715 | execute!( 716 | stdout(), 717 | Clear(ClearType::All), 718 | MoveTo((term_cols - message.len() as u16) / 2, start_row - 1), 719 | Print(message.on_white().black().bold()) 720 | ) 721 | .ok(); 722 | Menu::draw_borders(start_row, end_row, start_col, end_col).ok(); 723 | for (i, repo) in repositories.iter().enumerate() { 724 | execute!( 725 | stdout(), 726 | SetBackgroundColor(if i == current_selection { 727 | Color::White 728 | } else { 729 | Color::Reset 730 | }), 731 | SetForegroundColor(if i == current_selection { 732 | Color::Black 733 | } else { 734 | Color::White 735 | }), 736 | MoveTo(start_col + 1, start_row + i as u16 + 1), 737 | Print( 738 | format!( 739 | " {: { 751 | if current_selection == 0 { 752 | current_selection = repositories.len() - 1; 753 | } else { 754 | current_selection -= 1; 755 | } 756 | } 757 | Control::Down => { 758 | if current_selection == repositories.len() - 1 { 759 | current_selection = 0; 760 | } else { 761 | current_selection += 1; 762 | } 763 | } 764 | Control::Select => return Some(Selection::Item(current_selection)), 765 | Control::None => continue, 766 | _ => break, 767 | } 768 | } 769 | } 770 | MenuType::RepositoryLevelSelection(repository, completed_levels) => { 771 | let message = " SELECT A REPO "; 772 | let mut current_selection = 0; 773 | loop { 774 | let (term_cols, term_rows) = size().unwrap_or((0, 0)); 775 | let num_cols = (term_cols - 4).min(200) as usize; 776 | let level_name_len = num_cols / 5 - 2; 777 | let level_author_len = level_name_len; 778 | let level_desc_len = num_cols - level_name_len - level_author_len - 6; 779 | let start_row: u16 = (term_rows - repository.levels.len() as u16) / 2; 780 | let start_col: u16 = (term_cols - num_cols as u16) / 2; 781 | let end_row: u16 = (term_rows + repository.levels.len() as u16) / 2 + 1; 782 | let end_col: u16 = (term_cols + num_cols as u16) / 2; 783 | execute!( 784 | stdout(), 785 | Clear(ClearType::All), 786 | MoveTo((term_cols - message.len() as u16) / 2, start_row - 1), 787 | Print(message.on_white().black().bold()) 788 | ) 789 | .ok(); 790 | Menu::draw_borders(start_row, end_row, start_col, end_col).ok(); 791 | for (i, level) in repository.levels.iter().enumerate() { 792 | if let LevelSource::Url(url) = &level.source { 793 | let completed = completed_levels.iter().any(|l| { 794 | l.url == *url || (l.name == level.name && l.author == level.author) 795 | }); 796 | execute!( 797 | stdout(), 798 | SetBackgroundColor(if i == current_selection { 799 | Color::White 800 | } else { 801 | Color::Reset 802 | }), 803 | SetForegroundColor(if i == current_selection { 804 | Color::Black 805 | } else { 806 | Color::White 807 | }), 808 | MoveTo(start_col + 1, start_row + i as u16 + 1), 809 | Print( 810 | format!( 811 | " {} {: { 825 | if current_selection == 0 { 826 | current_selection = repository.levels.len() - 1; 827 | } else { 828 | current_selection -= 1; 829 | } 830 | } 831 | Control::Down => { 832 | if current_selection == repository.levels.len() - 1 { 833 | current_selection = 0; 834 | } else { 835 | current_selection += 1; 836 | } 837 | } 838 | Control::Select => return Some(Selection::Item(current_selection)), 839 | Control::Quit => return Some(Selection::Quit), 840 | Control::None => continue, 841 | _ => break, 842 | } 843 | } 844 | } 845 | _ => (), 846 | } 847 | None 848 | } 849 | } 850 | -------------------------------------------------------------------------------- /src/node.rs: -------------------------------------------------------------------------------- 1 | use crate::direction::Direction; 2 | use crossterm::{ 3 | cursor::MoveTo, 4 | execute, 5 | style::{Color, Print, ResetColor, SetBackgroundColor, SetForegroundColor, Stylize}, 6 | }; 7 | use std::io::stdout; 8 | 9 | #[derive(Debug)] 10 | pub struct Player { 11 | pub dead: bool, 12 | } 13 | #[derive(Debug)] 14 | pub struct Block; 15 | #[derive(Debug)] 16 | pub struct Wall; 17 | #[derive(Debug)] 18 | pub struct Switch { 19 | pub on: bool, 20 | } 21 | #[derive(Debug)] 22 | pub struct ToggleBlock { 23 | pub visible: bool, 24 | } 25 | #[derive(Debug)] 26 | pub struct Button { 27 | pub pressed: bool, 28 | } 29 | #[derive(Debug)] 30 | pub struct Mirror { 31 | pub dir: Direction, 32 | } 33 | #[derive(Debug)] 34 | pub struct Laser { 35 | pub on: bool, 36 | pub dir: Direction, 37 | pub shooting_at: Vec<(u16, u16, char, char)>, 38 | } 39 | #[derive(Debug)] 40 | pub struct Statue { 41 | pub lit: bool, 42 | pub reversed: bool, 43 | } 44 | #[derive(Debug)] 45 | pub struct Zapper { 46 | pub lit: bool, 47 | } 48 | 49 | #[derive(Debug)] 50 | pub enum NodeType { 51 | Player(Player), 52 | Block(Block), 53 | Wall(Wall), 54 | Switch(Switch), 55 | ToggleBlock(ToggleBlock), 56 | Button(Button), 57 | Mirror(Mirror), 58 | Laser(Laser), 59 | Statue(Statue), 60 | Zapper(Zapper), 61 | } 62 | 63 | #[derive(Debug)] 64 | pub struct Node { 65 | pub node_type: NodeType, 66 | pub row: u16, 67 | pub col: u16, 68 | moveable: bool, 69 | } 70 | 71 | const RED: Color = Color::Rgb { r: 255, g: 0, b: 0 }; 72 | const DIM_RED: Color = Color::Rgb { r: 100, g: 0, b: 0 }; 73 | const YELLOW: Color = Color::Rgb { 74 | r: 255, 75 | g: 255, 76 | b: 0, 77 | }; 78 | const DIM_YELLOW: Color = Color::Rgb { 79 | r: 100, 80 | g: 100, 81 | b: 0, 82 | }; 83 | 84 | impl Node { 85 | pub fn new(ch: char, row: u16, col: u16) -> Node { 86 | match ch { 87 | 'X' => Node { 88 | row, 89 | col, 90 | node_type: NodeType::Player(Player { dead: false }), 91 | moveable: true, 92 | }, 93 | 'B' => Node { 94 | row, 95 | col, 96 | node_type: NodeType::Block(Block), 97 | moveable: true, 98 | }, 99 | 'T' => Node { 100 | row, 101 | col, 102 | node_type: NodeType::ToggleBlock(ToggleBlock { visible: true }), 103 | moveable: false, 104 | }, 105 | 'b' => Node { 106 | row, 107 | col, 108 | node_type: NodeType::Button(Button { pressed: false }), 109 | moveable: false, 110 | }, 111 | 's' => Node { 112 | row, 113 | col, 114 | node_type: NodeType::Switch(Switch { on: false }), 115 | moveable: false, 116 | }, 117 | 'S' => Node { 118 | row, 119 | col, 120 | node_type: NodeType::Statue(Statue { 121 | lit: false, 122 | reversed: false, 123 | }), 124 | moveable: false, 125 | }, 126 | 'R' => Node { 127 | row, 128 | col, 129 | node_type: NodeType::Statue(Statue { 130 | lit: false, 131 | reversed: true, 132 | }), 133 | moveable: false, 134 | }, 135 | 'Z' => Node { 136 | row, 137 | col, 138 | node_type: NodeType::Zapper(Zapper { lit: false }), 139 | moveable: false, 140 | }, 141 | '/' => Node { 142 | row, 143 | col, 144 | node_type: NodeType::Mirror(Mirror { 145 | dir: Direction::FORWARD, 146 | }), 147 | moveable: false, 148 | }, 149 | '\\' => Node { 150 | row, 151 | col, 152 | node_type: NodeType::Mirror(Mirror { 153 | dir: Direction::BACKWARD, 154 | }), 155 | moveable: false, 156 | }, 157 | '?' => Node { 158 | row, 159 | col, 160 | node_type: NodeType::Mirror(Mirror { 161 | dir: Direction::FORWARD, 162 | }), 163 | moveable: true, 164 | }, 165 | '|' => Node { 166 | row, 167 | col, 168 | node_type: NodeType::Mirror(Mirror { 169 | dir: Direction::BACKWARD, 170 | }), 171 | moveable: true, 172 | }, 173 | '1' => Node { 174 | row, 175 | col, 176 | node_type: NodeType::Laser(Laser { 177 | on: true, 178 | dir: Direction::UP, 179 | shooting_at: vec![], 180 | }), 181 | moveable: false, 182 | }, 183 | '2' => Node { 184 | row, 185 | col, 186 | node_type: NodeType::Laser(Laser { 187 | on: true, 188 | dir: Direction::DOWN, 189 | shooting_at: vec![], 190 | }), 191 | moveable: false, 192 | }, 193 | '3' => Node { 194 | row, 195 | col, 196 | node_type: NodeType::Laser(Laser { 197 | on: true, 198 | dir: Direction::LEFT, 199 | shooting_at: vec![], 200 | }), 201 | moveable: false, 202 | }, 203 | '4' => Node { 204 | row, 205 | col, 206 | node_type: NodeType::Laser(Laser { 207 | on: true, 208 | dir: Direction::RIGHT, 209 | shooting_at: vec![], 210 | }), 211 | moveable: false, 212 | }, 213 | '5' => Node { 214 | row, 215 | col, 216 | node_type: NodeType::Laser(Laser { 217 | on: false, 218 | dir: Direction::UP, 219 | shooting_at: vec![], 220 | }), 221 | moveable: false, 222 | }, 223 | '6' => Node { 224 | row, 225 | col, 226 | node_type: NodeType::Laser(Laser { 227 | on: false, 228 | dir: Direction::DOWN, 229 | shooting_at: vec![], 230 | }), 231 | moveable: false, 232 | }, 233 | '7' => Node { 234 | row, 235 | col, 236 | node_type: NodeType::Laser(Laser { 237 | on: false, 238 | dir: Direction::LEFT, 239 | shooting_at: vec![], 240 | }), 241 | moveable: false, 242 | }, 243 | '8' => Node { 244 | row, 245 | col, 246 | node_type: NodeType::Laser(Laser { 247 | on: false, 248 | dir: Direction::RIGHT, 249 | shooting_at: vec![], 250 | }), 251 | moveable: false, 252 | }, 253 | _ => Node { 254 | row, 255 | col, 256 | node_type: NodeType::Wall(Wall), 257 | moveable: false, 258 | }, 259 | } 260 | } 261 | 262 | pub fn draw_overlay(&self, offset: (u16, u16)) -> crossterm::Result<()> { 263 | let mut stdout = stdout(); 264 | if let NodeType::Laser(l) = &self.node_type { 265 | if l.shooting_at.is_empty() { 266 | return Ok(()); 267 | } 268 | for i in 0..(l.shooting_at.len() - 1) { 269 | let pos = l.shooting_at[i]; 270 | execute!( 271 | stdout, 272 | SetForegroundColor(Color::Rgb { r: 255, g: 0, b: 0 }), 273 | MoveTo(pos.1 + offset.1, pos.0 + offset.0), 274 | )?; 275 | if i == l.shooting_at.len() - 2 { 276 | execute!(stdout, Print(pos.3.bold()),)?; 277 | } else { 278 | execute!(stdout, Print(pos.2.bold()),)?; 279 | } 280 | } 281 | } 282 | execute!(stdout, ResetColor) 283 | } 284 | 285 | pub fn draw(&self, offset: (u16, u16)) -> crossterm::Result<()> { 286 | let mut stdout = stdout(); 287 | match &self.node_type { 288 | NodeType::Player(p) => execute!( 289 | stdout, 290 | SetForegroundColor(if p.dead { RED } else { Color::Green }), 291 | SetBackgroundColor(Color::Green), 292 | MoveTo(self.col + offset.1, self.row + offset.0), 293 | Print("X".bold()), 294 | ), 295 | NodeType::Block(_) => execute!( 296 | stdout, 297 | SetForegroundColor(Color::Grey), 298 | SetBackgroundColor(Color::Grey), 299 | MoveTo(self.col + offset.1, self.row + offset.0), 300 | Print("B".bold()), 301 | ), 302 | NodeType::Wall(_) => execute!( 303 | stdout, 304 | SetForegroundColor(Color::White), 305 | SetBackgroundColor(Color::White), 306 | MoveTo(self.col + offset.1, self.row + offset.0), 307 | Print("I".bold()), 308 | ), 309 | NodeType::Switch(s) => execute!( 310 | stdout, 311 | SetForegroundColor(Color::Black), 312 | SetBackgroundColor(if s.on { Color::Yellow } else { Color::Red }), 313 | MoveTo(self.col + offset.1, self.row + offset.0), 314 | Print("s".bold()), 315 | ), 316 | NodeType::ToggleBlock(t) => { 317 | if t.visible { 318 | execute!( 319 | stdout, 320 | SetForegroundColor(Color::Magenta), 321 | SetBackgroundColor(Color::Magenta), 322 | MoveTo(self.col + offset.1, self.row + offset.0), 323 | Print("T".bold()) 324 | ) 325 | } else { 326 | Ok(()) 327 | } 328 | } 329 | NodeType::Button(b) => execute!( 330 | stdout, 331 | SetForegroundColor(Color::Black), 332 | SetBackgroundColor(if b.pressed { Color::Yellow } else { Color::Red }), 333 | MoveTo(self.col + offset.1, self.row + offset.0), 334 | Print("b".bold()), 335 | ), 336 | NodeType::Mirror(m) => execute!( 337 | stdout, 338 | SetForegroundColor(if self.moveable { 339 | Color::Black 340 | } else { 341 | Color::White 342 | }), 343 | SetBackgroundColor(if self.moveable { 344 | Color::White 345 | } else { 346 | Color::Reset 347 | }), 348 | MoveTo(self.col + offset.1, self.row + offset.0), 349 | Print(if matches!(m.dir, Direction::FORWARD) { 350 | "/".bold() 351 | } else { 352 | "\\".bold() 353 | }), 354 | ), 355 | NodeType::Laser(l) => execute!( 356 | stdout, 357 | SetForegroundColor(if l.on { RED } else { DIM_RED }), 358 | SetBackgroundColor(if l.on { RED } else { DIM_RED }), 359 | MoveTo(self.col + offset.1, self.row + offset.0), 360 | Print("L".bold()), 361 | ), 362 | NodeType::Statue(s) => { 363 | if s.reversed { 364 | execute!( 365 | stdout, 366 | SetForegroundColor(Color::Black), 367 | SetBackgroundColor(if s.lit { DIM_YELLOW } else { YELLOW }), 368 | MoveTo(self.col + offset.1, self.row + offset.0), 369 | Print("R".bold()), 370 | ) 371 | } else { 372 | execute!( 373 | stdout, 374 | SetForegroundColor(if s.lit { YELLOW } else { DIM_YELLOW }), 375 | SetBackgroundColor(if s.lit { YELLOW } else { DIM_YELLOW }), 376 | MoveTo(self.col + offset.1, self.row + offset.0), 377 | Print("S".bold()), 378 | ) 379 | } 380 | } 381 | NodeType::Zapper(z) => execute!( 382 | stdout, 383 | SetForegroundColor(if z.lit { Color::Black } else { Color::Yellow }), 384 | SetBackgroundColor(if z.lit { Color::Yellow } else { Color::Black }), 385 | MoveTo(self.col + offset.1, self.row + offset.0), 386 | Print("Z".bold()), 387 | ), 388 | }?; 389 | execute!(stdout, ResetColor) 390 | } 391 | 392 | pub fn would_move_to(&mut self, dir: Direction) -> (u16, u16) { 393 | if !self.moveable { 394 | return (self.row, self.col); 395 | } 396 | let mut row = self.row as i16; 397 | let mut col = self.col as i16; 398 | if self.row as i16 + dir.0 >= 0 { 399 | row = self.row as i16 + dir.0; 400 | } 401 | if self.col as i16 + dir.1 >= 0 { 402 | col = self.col as i16 + dir.1; 403 | } 404 | (row as u16, col as u16) 405 | } 406 | 407 | pub fn move_in_dir(&mut self, dir: Direction) { 408 | if !self.moveable { 409 | return; 410 | } 411 | if self.row as i16 + dir.0 >= 0 { 412 | self.row = (self.row as i16 + dir.0) as u16 413 | } 414 | if self.col as i16 + dir.1 >= 0 { 415 | self.col = (self.col as i16 + dir.1) as u16 416 | } 417 | } 418 | 419 | pub fn is_moveable(&self) -> bool { 420 | self.moveable 421 | } 422 | 423 | pub fn is_player_toggleable(&self) -> bool { 424 | matches!( 425 | &self.node_type, 426 | NodeType::Laser(_) | NodeType::Mirror(_) | NodeType::Switch(_) 427 | ) 428 | } 429 | 430 | pub fn is_laser_toggleable(&self) -> bool { 431 | matches!( 432 | &self.node_type, 433 | NodeType::Player(_) | NodeType::Laser(_) | NodeType::Statue(_) | NodeType::Zapper(_) 434 | ) 435 | } 436 | 437 | pub fn turn_on(&mut self) { 438 | match &mut self.node_type { 439 | NodeType::Player(p) => p.dead = true, 440 | NodeType::Laser(l) => l.on = true, 441 | NodeType::Statue(s) => s.lit = true, 442 | NodeType::Zapper(z) => z.lit = true, 443 | NodeType::Button(b) => b.pressed = true, 444 | NodeType::Switch(s) => s.on = true, 445 | NodeType::ToggleBlock(t) => t.visible = true, 446 | _ => (), 447 | } 448 | } 449 | 450 | pub fn turn_off(&mut self) { 451 | match &mut self.node_type { 452 | NodeType::Player(p) => p.dead = false, 453 | NodeType::Laser(l) => l.on = false, 454 | NodeType::Statue(s) => s.lit = false, 455 | NodeType::Zapper(z) => z.lit = false, 456 | NodeType::Button(b) => b.pressed = false, 457 | NodeType::Switch(s) => s.on = false, 458 | NodeType::ToggleBlock(t) => t.visible = false, 459 | _ => (), 460 | } 461 | } 462 | 463 | pub fn toggle(&mut self) { 464 | match &mut self.node_type { 465 | NodeType::Player(p) => p.dead = !p.dead, 466 | NodeType::Laser(l) => l.on = !l.on, 467 | NodeType::Statue(s) => s.lit = !s.lit, 468 | NodeType::Zapper(z) => z.lit = !z.lit, 469 | NodeType::Mirror(m) => { 470 | if matches!(m.dir, Direction::FORWARD) { 471 | m.dir = Direction::BACKWARD; 472 | } else { 473 | m.dir = Direction::FORWARD; 474 | } 475 | } 476 | NodeType::Button(b) => b.pressed = !b.pressed, 477 | NodeType::Switch(s) => s.on = !s.on, 478 | NodeType::ToggleBlock(t) => t.visible = !t.visible, 479 | _ => (), 480 | } 481 | } 482 | 483 | pub fn set_shooting_at(&mut self, shooting_at: Vec<(u16, u16, char, char)>) { 484 | if let NodeType::Laser(l) = &mut self.node_type { 485 | l.shooting_at = shooting_at 486 | } 487 | } 488 | } 489 | -------------------------------------------------------------------------------- /src/repository.rs: -------------------------------------------------------------------------------- 1 | use crate::level::{LevelInfo, LevelSource}; 2 | use serde::Deserialize; 3 | use std::error::Error; 4 | 5 | #[derive(Debug, Clone)] 6 | pub struct Repository { 7 | pub name: String, 8 | pub url: String, 9 | pub levels: Vec, 10 | } 11 | 12 | #[derive(Deserialize, Debug, Clone)] 13 | pub struct RepositoryLevelInfo { 14 | pub source: String, 15 | pub name: String, 16 | pub author: String, 17 | pub description: String, 18 | } 19 | 20 | #[derive(Deserialize, Debug, Clone)] 21 | pub struct RepositoryResponse { 22 | pub levels: Vec, 23 | } 24 | 25 | impl Repository { 26 | pub fn new(name: String, url: String) -> Repository { 27 | Repository { 28 | name, 29 | url, 30 | levels: vec![], 31 | } 32 | } 33 | 34 | pub async fn download_listing(&mut self) -> Result<(), Box> { 35 | let response = reqwest::get(self.url.to_string() + "/l1t") 36 | .await? 37 | .text() 38 | .await?; 39 | let response: RepositoryResponse = match serde_json::from_str(&response) { 40 | Ok(d) => d, 41 | Err(e) => { 42 | eprintln!("Error: {e}"); 43 | RepositoryResponse { levels: vec![] } 44 | } 45 | }; 46 | self.levels = response 47 | .levels 48 | .iter() 49 | .map(|i| LevelInfo { 50 | source: LevelSource::Url(self.url.to_string() + "/" + &i.source), 51 | name: i.name.to_string(), 52 | author: i.author.to_string(), 53 | description: i.description.to_string(), 54 | }) 55 | .collect(); 56 | Ok(()) 57 | } 58 | 59 | pub async fn download_from_url(url: String) -> Result> { 60 | let response = reqwest::get(url).await?.text().await?; 61 | Ok(response) 62 | } 63 | } 64 | -------------------------------------------------------------------------------- /src/userdata.rs: -------------------------------------------------------------------------------- 1 | use crate::{ 2 | level::{LevelInfo, LevelSource}, 3 | repository::Repository, 4 | }; 5 | use serde::{Deserialize, Serialize}; 6 | use std::time::{SystemTime, UNIX_EPOCH}; 7 | use std::{fs, path}; 8 | 9 | #[derive(Debug, Serialize, Deserialize, Clone)] 10 | pub struct CompletedRepoLevel { 11 | pub url: String, 12 | pub name: String, 13 | pub author: String, 14 | pub description: String, 15 | pub completed_at: u64, 16 | } 17 | 18 | pub struct UserData { 19 | file: String, 20 | pub completed_core_levels: Vec, 21 | pub completed_levels: Vec, 22 | pub repositories: Vec, 23 | } 24 | 25 | #[derive(Debug, Serialize, Deserialize)] 26 | pub struct SerializedUserData { 27 | file: String, 28 | pub completed_core_levels: Vec, 29 | pub completed_levels: Vec, 30 | } 31 | 32 | impl UserData { 33 | fn read_repositories(home_dir: String) -> Result, String> { 34 | let file = home_dir.to_string() + "/.l1t/repositories.l1t_conf"; 35 | if !path::Path::new(&file).exists() { 36 | fs::create_dir(home_dir + "/.l1t").ok(); 37 | if let Err(e) = fs::write(&file, "") { 38 | return Err(e.to_string()); 39 | } 40 | } 41 | let file_content = fs::read_to_string(&file).unwrap_or_default(); 42 | let mut repositories: Vec = vec![]; 43 | for line in file_content.trim().split('\n') { 44 | let parts: Vec<&str> = line.trim().split('=').collect(); 45 | if parts.len() < 2 { 46 | continue; 47 | } 48 | repositories.push(Repository::new( 49 | parts[0].trim().to_string(), 50 | parts[1].trim().to_string(), 51 | )); 52 | } 53 | Ok(repositories) 54 | } 55 | 56 | pub fn read(home_dir: String) -> Result { 57 | let file = home_dir.to_string() + "/.l1t/data.json"; 58 | if !path::Path::new(&file).exists() { 59 | fs::create_dir(home_dir.clone() + "/.l1t").ok(); 60 | let data = SerializedUserData { 61 | file: file.clone(), 62 | completed_core_levels: vec![], 63 | completed_levels: vec![], 64 | }; 65 | let content = match serde_json::to_string(&data) { 66 | Ok(c) => c, 67 | Err(e) => return Err(e.to_string()), 68 | }; 69 | if let Err(e) = fs::write(&file, content) { 70 | return Err(e.to_string()); 71 | }; 72 | } 73 | let file_content = fs::read_to_string(&file).unwrap_or_default(); 74 | let data = match serde_json::from_str::(&file_content) { 75 | Ok(d) => d, 76 | Err(e) => return Err(e.to_string()), 77 | }; 78 | let repositories = UserData::read_repositories(home_dir)?; 79 | 80 | Ok(UserData { 81 | repositories, 82 | file, 83 | completed_core_levels: data.completed_core_levels, 84 | completed_levels: data.completed_levels, 85 | }) 86 | } 87 | 88 | fn complete_core(&mut self, level: usize) -> Result<(), String> { 89 | if self.completed_core_levels.iter().any(|i| *i == level) { 90 | return Ok(()); 91 | } 92 | self.completed_core_levels.push(level); 93 | let content = match serde_json::to_string(&SerializedUserData { 94 | file: self.file.clone(), 95 | completed_core_levels: self.completed_core_levels.clone(), 96 | completed_levels: self.completed_levels.clone(), 97 | }) { 98 | Ok(c) => c, 99 | Err(e) => return Err(e.to_string()), 100 | }; 101 | if let Err(e) = fs::write(&self.file, content) { 102 | return Err(e.to_string()); 103 | }; 104 | Ok(()) 105 | } 106 | 107 | fn complete_repo(&mut self, level_info: LevelInfo) -> Result<(), String> { 108 | if let LevelSource::Url(url) = level_info.source { 109 | if self.completed_levels.iter().any(|l| { 110 | l.url == url || (l.name == level_info.name && l.author == level_info.author) 111 | }) { 112 | return Ok(()); 113 | } 114 | let completed_at = SystemTime::now() 115 | .duration_since(UNIX_EPOCH) 116 | .unwrap() 117 | .as_secs(); 118 | self.completed_levels.push(CompletedRepoLevel { 119 | url, 120 | completed_at, 121 | name: level_info.name, 122 | author: level_info.author, 123 | description: level_info.description, 124 | }); 125 | let content = match serde_json::to_string(&SerializedUserData { 126 | file: self.file.clone(), 127 | completed_core_levels: self.completed_core_levels.clone(), 128 | completed_levels: self.completed_levels.clone(), 129 | }) { 130 | Ok(c) => c, 131 | Err(e) => return Err(e.to_string()), 132 | }; 133 | if let Err(e) = fs::write(&self.file, content) { 134 | return Err(e.to_string()); 135 | }; 136 | } 137 | Ok(()) 138 | } 139 | 140 | pub fn complete(&mut self, level_info: LevelInfo) -> Result<(), String> { 141 | match level_info.source { 142 | LevelSource::Core(level) => self.complete_core(level), 143 | LevelSource::Url(_) => self.complete_repo(level_info), 144 | _ => Err("".to_string()), 145 | } 146 | } 147 | } 148 | --------------------------------------------------------------------------------