diff --git a/.cargo-husky/hooks/pre-commit b/.cargo-husky/hooks/pre-commit deleted file mode 100755 index f735ef4..0000000 --- a/.cargo-husky/hooks/pre-commit +++ /dev/null @@ -1,11 +0,0 @@ -#!/bin/sh -# -# An example hook script to verify what is about to be committed. -# Called by "git commit" with no arguments. The hook should -# exit with non-zero status after issuing an appropriate message if -# it wants to stop the commit. -# -# To enable this hook, rename this file to "pre-commit". - -cargo build -cargo test diff --git a/.github/dependabot.yml b/.github/dependabot.yml new file mode 100644 index 0000000..d4b3cf1 --- /dev/null +++ b/.github/dependabot.yml @@ -0,0 +1,25 @@ +version: 2 +updates: + - package-ecosystem: "cargo" + directory: "/" + schedule: + interval: "monthly" + day: "sunday" + commit-message: + prefix: "chore(dep): " + groups: + deps: + patterns: + - "*" + + - package-ecosystem: "github-actions" + directory: "/" + schedule: + interval: "monthly" + day: "sunday" + commit-message: + prefix: "chore(dep): " + groups: + deps: + patterns: + - "*" diff --git a/.github/workflows/clippy.yml b/.github/workflows/clippy.yml index 004dc18..563233f 100644 --- a/.github/workflows/clippy.yml +++ b/.github/workflows/clippy.yml @@ -13,7 +13,7 @@ jobs: matrix: os: [macOS-latest, ubuntu-latest] steps: - - uses: actions/checkout@v1 + - uses: actions/checkout@v4 - uses: actions-rs/toolchain@v1 with: components: clippy diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml new file mode 100644 index 0000000..1b86e39 --- /dev/null +++ b/.github/workflows/release.yml @@ -0,0 +1,21 @@ +name: release + +on: + push: + tags: + - "v*" +jobs: + publish: + name: Publish + # Specify OS + runs-on: ubuntu-latest + steps: + - name: Checkout sources + uses: actions/checkout@v4 + - name: Install stable toolchain + uses: actions-rs/toolchain@v1 + with: + toolchain: stable + - uses: katyo/publish-crates@v2 + with: + registry-token: ${{ secrets.CARGO_REGISTRY_TOKEN }} diff --git a/.github/workflows/rust.yml b/.github/workflows/rust.yml index 3f22989..ee47a77 100644 --- a/.github/workflows/rust.yml +++ b/.github/workflows/rust.yml @@ -2,9 +2,9 @@ name: leetcode-cli on: push: - branches: [ master ] + branches: [master] pull_request: - branches: [ master ] + branches: [master] jobs: build: @@ -14,7 +14,7 @@ jobs: os: [macOS-latest, ubuntu-latest] steps: - name: Checkout the source code - uses: actions/checkout@v1 + uses: actions/checkout@v4 - name: Set nightly toolchain uses: actions-rs/toolchain@v1 with: @@ -22,13 +22,12 @@ jobs: - name: Environment run: | if [[ "$(uname)" == 'Darwin' ]]; then - brew update brew install sqlite3 else sudo apt-get update -y sudo apt-get install -y libsqlite3-dev libdbus-1-dev fi - name: Build - run: cargo build + run: cargo build --release --all-features - name: Run tests - run: cargo test + run: cargo test --release --all-features diff --git a/.gitignore b/.gitignore index 80865c6..67e0e9c 100644 --- a/.gitignore +++ b/.gitignore @@ -1,5 +1,6 @@ **/*target **/*.rs.bk -Cargo.lock .DS_Store -.idea \ No newline at end of file +.idea +.direnv/ +/result diff --git a/CHANGELOG.md b/CHANGELOG.md index 4b1a223..cb532f8 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,136 +1,171 @@ +## v0.4.1 + +- Search problems by name +- Re-enable chrome plugin + ## v0.3.3 -* allow more flexible categories by @frrad -* change params type to `Option` @frrad +- allow more flexible categories by @frrad +- change params type to `Option` @frrad ## v0.3.2 -* adds additional tracing by @shmuga -* removes test_mode parameter by @shmuga +- adds additional tracing by @shmuga +- removes test_mode parameter by @shmuga ## v0.3.1 -* pipe handling by @aymanbagabas -* Improve README by @xiaoxiae +- pipe handling by @aymanbagabas +- Improve README by @xiaoxiae ## v0.3.0 -* Upgrade reqwest to async mode -* Format code using clippy +- Upgrade reqwest to async mode +- Format code using clippy ## v0.2.23 -* support color display +- support color display ## v0.2.22 -* Fixed the cache can't update with new added problems -* Display user friendly errors when pick/edit new added problem. +- Fixed the cache can't update with new added problems -* upgrade pyo3 +- Display user friendly errors when pick/edit new added problem. -* fix leetcode list with empty cache +- upgrade pyo3 + +- fix leetcode list with empty cache ## v0.2.21 -* Make programmable support to be an advanced feature +- Make programmable support to be an advanced feature ## v0.2.20 -* Support sup/sub style for numbers +- Support sup/sub style for numbers ## v0.2.19 -* Better HTML! + +- Better HTML! ## v0.2.18 -* Display stdout for test and execute commands, fix minor spacing in results displayed -* Fix panic on `pick` command without cache +- Display stdout for test and execute commands, fix minor spacing in results displayed + +- Fix panic on `pick` command without cache ## v0.2.17 -Fix panic on stat command with zero numbers + +Fix panic on stat command with zero numbers ## v0.2.16 + Update versions of diesel and reqwest ## v0.2.15 + Allow for custom testcases with the `leetcode test` command, and some minor edits ## v0.2.14 -Corrects file suffixes for c** and c# files + +Corrects file suffixes for c\*\* and c# files ## v0.2.13 + fix percent length panic ## v0.2.12 + fix gt || ge || lt || le ## v0.2.11 + added code 14 and transfered `&#ge;`、`&#le` and `'`. ## v0.2.10 + add code 15 ## v0.2.9 + update ac status after submit successfully ## v0.2.8 + show last testcases ## v0.2.7 + fixed float bug in result ## v0.2.6 + sync config while change current lang ## v0.2.5 + update local cache when submission status changes ## v0.2.4 + auto fetch question while exec `edit` directly. ## v0.2.3 + Programmable leetcode-cli ## v0.2.2 + 1. optimize logs 2. add tag filter 3. sync configs ## v0.2.1 + 1. fix cookies error handling 2. dismiss all `unwrap`, `expect` and `panic` 3. add cookie configs ## v0.2.0 + 1. Add Linux Support ## v0.1.9 + 1. release submit command 2. deserialize json using outter funcs ## v0.1.8 + 1. pack mod exports 2. add edit command 3. add test command ## v0.1.7 + render html in command-line, and `pick` command ## v0.1.6 + complete `stat` command ## v0.1.5 + complete `cache` command ## v0.1.3 + complete `list` command ## v0.1.2 + abstract data cache ## v0.1.1 + add list command ## v0.1.0 + chrome cookie diff --git a/Cargo.lock b/Cargo.lock new file mode 100644 index 0000000..1f006e7 --- /dev/null +++ b/Cargo.lock @@ -0,0 +1,2559 @@ +# This file is automatically @generated by Cargo. +# It is not intended for manual editing. +version = 4 + +[[package]] +name = "addr2line" +version = "0.22.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6e4503c46a5c0c7844e948c9a4d6acd9f50cccb4de1c48eb9e291ea17470c678" +dependencies = [ + "gimli", +] + +[[package]] +name = "adler" +version = "1.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f26201604c87b1e01bd3d98f8d5d9a8fcbb815e8cedb41ffccbeb4bf593a35fe" + +[[package]] +name = "adler2" +version = "2.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "512761e0bb2578dd7380c6baaa0f4ce03e84f95e960231d1dec8bf4d7d6e2627" + +[[package]] +name = "aho-corasick" +version = "1.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8e60d3430d3a69478ad0993f19238d2df97c507009a52b3c10addcd7f6bcb916" +dependencies = [ + "memchr", +] + +[[package]] +name = "anstream" +version = "0.6.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "64e15c1ab1f89faffbf04a634d5e1962e9074f2741eef6d97f3c4e322426d526" +dependencies = [ + "anstyle", + "anstyle-parse", + "anstyle-query", + "anstyle-wincon", + "colorchoice", + "is_terminal_polyfill", + "utf8parse", +] + +[[package]] +name = "anstyle" +version = "1.0.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1bec1de6f59aedf83baf9ff929c98f2ad654b97c9510f4e70cf6f661d49fd5b1" + +[[package]] +name = "anstyle-parse" +version = "0.2.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "eb47de1e80c2b463c735db5b217a0ddc39d612e7ac9e2e96a5aed1f57616c1cb" +dependencies = [ + "utf8parse", +] + +[[package]] +name = "anstyle-query" +version = "1.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6d36fc52c7f6c869915e99412912f22093507da8d9e942ceaf66fe4b7c14422a" +dependencies = [ + "windows-sys 0.52.0", +] + +[[package]] +name = "anstyle-wincon" +version = "3.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5bf74e1b6e971609db8ca7a9ce79fd5768ab6ae46441c572e46cf596f59e57f8" +dependencies = [ + "anstyle", + "windows-sys 0.52.0", +] + +[[package]] +name = "anyhow" +version = "1.0.98" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e16d2d3311acee920a9eb8d33b8cbc1787ce4a264e85f964c2404b969bdcd487" + +[[package]] +name = "async-compression" +version = "0.4.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fec134f64e2bc57411226dfc4e52dec859ddfc7e711fc5e07b612584f000e4aa" +dependencies = [ + "flate2", + "futures-core", + "memchr", + "pin-project-lite", + "tokio", +] + +[[package]] +name = "async-trait" +version = "0.1.88" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e539d3fca749fcee5236ab05e93a52867dd549cc157c8cb7f99595f3cedffdb5" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "atomic-waker" +version = "1.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1505bd5d3d116872e7271a6d4e16d81d0c8570876c8de68093a09ac269d8aac0" + +[[package]] +name = "autocfg" +version = "1.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0c4b4d0bd25bd0b74681c0ad21497610ce1b7c91b1022cd21c80c6fbdd9476b0" + +[[package]] +name = "backtrace" +version = "0.3.73" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5cc23269a4f8976d0a4d2e7109211a419fe30e8d88d677cd60b6bc79c5732e0a" +dependencies = [ + "addr2line", + "cc", + "cfg-if", + "libc", + "miniz_oxide 0.7.4", + "object", + "rustc-demangle", +] + +[[package]] +name = "base64" +version = "0.22.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "72b3254f16251a8381aa12e40e3c4d2f0199f8c6508fbecb9d91f575e0fbb8c6" + +[[package]] +name = "bitflags" +version = "2.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b048fb63fd8b5923fc5aa7b340d8e156aec7ec02f0c78fa8a6ddc2613f6f71de" + +[[package]] +name = "bumpalo" +version = "3.16.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "79296716171880943b8470b5f8d03aa55eb2e645a4874bdbb28adb49162e012c" + +[[package]] +name = "byteorder" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1fd0f2584146f6f2ef48085050886acf353beff7305ebd1ae69500e27c67f64b" + +[[package]] +name = "bytes" +version = "1.7.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8318a53db07bb3f8dca91a600466bdb3f2eaadeedfdbcf02e1accbad9271ba50" + +[[package]] +name = "cc" +version = "1.1.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "50d2eb3cd3d1bf4529e31c215ee6f93ec5a3d536d9f578f93d9d33ee19562932" +dependencies = [ + "shlex", +] + +[[package]] +name = "cfg-if" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd" + +[[package]] +name = "cfg_aliases" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "613afe47fcd5fac7ccf1db93babcb082c5994d996f20b8b159f2ad1658eb5724" + +[[package]] +name = "clap" +version = "4.5.40" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "40b6887a1d8685cebccf115538db5c0efe625ccac9696ad45c409d96566e910f" +dependencies = [ + "clap_builder", +] + +[[package]] +name = "clap_builder" +version = "4.5.40" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e0c66c08ce9f0c698cbce5c0279d0bb6ac936d8674174fe48f736533b964f59e" +dependencies = [ + "anstream", + "anstyle", + "clap_lex", + "strsim", +] + +[[package]] +name = "clap_complete" +version = "4.5.54" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "aad5b1b4de04fead402672b48897030eec1f3bfe1550776322f59f6d6e6a5677" +dependencies = [ + "clap", +] + +[[package]] +name = "clap_lex" +version = "0.7.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f46ad14479a25103f283c0f10005961cf086d8dc42205bb44c46ac563475dca6" + +[[package]] +name = "colorchoice" +version = "1.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d3fd119d74b830634cea2a0f58bbd0d54540518a14397557951e79340abc28c0" + +[[package]] +name = "colored" +version = "3.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fde0e0ec90c9dfb3b4b1a0891a7dcd0e2bffde2f7efed5fe7c9bb00e5bfb915e" +dependencies = [ + "windows-sys 0.59.0", +] + +[[package]] +name = "core-foundation" +version = "0.9.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "91e195e091a93c46f7102ec7818a2aa394e1e1771c3ab4825963fa03e45afb8f" +dependencies = [ + "core-foundation-sys", + "libc", +] + +[[package]] +name = "core-foundation-sys" +version = "0.8.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "773648b94d0e5d620f64f280777445740e61fe701025087ec8b57f45c791888b" + +[[package]] +name = "crc32fast" +version = "1.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a97769d94ddab943e4510d138150169a2758b5ef3eb191a9ee688de3e23ef7b3" +dependencies = [ + "cfg-if", +] + +[[package]] +name = "cssparser" +version = "0.34.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b7c66d1cd8ed61bf80b38432613a7a2f09401ab8d0501110655f8b341484a3e3" +dependencies = [ + "cssparser-macros", + "dtoa-short", + "itoa", + "phf", + "smallvec", +] + +[[package]] +name = "cssparser-macros" +version = "0.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "13b588ba4ac1a99f7f2964d24b3d896ddc6bf847ee3855dbd4366f058cfcd331" +dependencies = [ + "quote", + "syn", +] + +[[package]] +name = "darling" +version = "0.20.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6f63b86c8a8826a49b8c21f08a2d07338eec8d900540f8630dc76284be802989" +dependencies = [ + "darling_core", + "darling_macro", +] + +[[package]] +name = "darling_core" +version = "0.20.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "95133861a8032aaea082871032f5815eb9e98cef03fa916ab4500513994df9e5" +dependencies = [ + "fnv", + "ident_case", + "proc-macro2", + "quote", + "strsim", + "syn", +] + +[[package]] +name = "darling_macro" +version = "0.20.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d336a2a514f6ccccaa3e09b02d41d35330c07ddf03a62165fcec10bb561c7806" +dependencies = [ + "darling_core", + "quote", + "syn", +] + +[[package]] +name = "deranged" +version = "0.3.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b42b6fa04a440b495c8b04d0e71b707c585f83cb9cb28cf8cd0d976c315e31b4" +dependencies = [ + "powerfmt", +] + +[[package]] +name = "derive_more" +version = "0.99.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5f33878137e4dafd7fa914ad4e259e18a4e8e532b9617a2d0150262bf53abfce" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "diesel" +version = "2.2.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a917a9209950404d5be011c81d081a2692a822f73c3d6af586f0cab5ff50f614" +dependencies = [ + "diesel_derives", + "libsqlite3-sys", + "time", +] + +[[package]] +name = "diesel_derives" +version = "2.2.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e7f2c3de51e2ba6bf2a648285696137aaf0f5f487bcbea93972fe8a364e131a4" +dependencies = [ + "diesel_table_macro_syntax", + "dsl_auto_type", + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "diesel_table_macro_syntax" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "209c735641a413bc68c4923a9d6ad4bcb3ca306b794edaa7eb0b3228a99ffb25" +dependencies = [ + "syn", +] + +[[package]] +name = "dirs" +version = "6.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c3e8aa94d75141228480295a7d0e7feb620b1a5ad9f12bc40be62411e38cce4e" +dependencies = [ + "dirs-sys", +] + +[[package]] +name = "dirs-sys" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e01a3366d27ee9890022452ee61b2b63a67e6f13f58900b651ff5665f0bb1fab" +dependencies = [ + "libc", + "option-ext", + "redox_users", + "windows-sys 0.59.0", +] + +[[package]] +name = "dsl_auto_type" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c5d9abe6314103864cc2d8901b7ae224e0ab1a103a0a416661b4097b0779b607" +dependencies = [ + "darling", + "either", + "heck", + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "dtoa" +version = "1.0.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dcbb2bf8e87535c23f7a8a321e364ce21462d0ff10cb6407820e8e96dfff6653" + +[[package]] +name = "dtoa-short" +version = "0.3.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cd1511a7b6a56299bd043a9c167a6d2bfb37bf84a6dfceaba651168adfb43c87" +dependencies = [ + "dtoa", +] + +[[package]] +name = "ego-tree" +version = "0.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b2972feb8dffe7bc8c5463b1dacda1b0dfbed3710e50f977d965429692d74cd8" + +[[package]] +name = "either" +version = "1.13.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "60b1af1c220855b6ceac025d3f6ecdd2b7c4894bfe9cd9bda4fbb4bc7c0d4cf0" + +[[package]] +name = "encoding_rs" +version = "0.8.34" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b45de904aa0b010bce2ab45264d0631681847fa7b6f2eaa7dab7619943bc4f59" +dependencies = [ + "cfg-if", +] + +[[package]] +name = "env_filter" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4f2c92ceda6ceec50f43169f9ee8424fe2db276791afde7b2cd8bc084cb376ab" +dependencies = [ + "log", + "regex", +] + +[[package]] +name = "env_logger" +version = "0.11.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dcaee3d8e3cfc3fd92428d477bc97fc29ec8716d180c0d74c643bb26166660e0" +dependencies = [ + "anstream", + "anstyle", + "env_filter", + "humantime", + "log", +] + +[[package]] +name = "equivalent" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5443807d6dff69373d433ab9ef5378ad8df50ca6298caf15de6e52e24aaf54d5" + +[[package]] +name = "errno" +version = "0.3.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "534c5cf6194dfab3db3242765c03bbe257cf92f22b38f6bc0c58d59108a820ba" +dependencies = [ + "libc", + "windows-sys 0.52.0", +] + +[[package]] +name = "fastrand" +version = "2.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e8c02a5121d4ea3eb16a80748c74f5549a5665e4c21333c6098f283870fbdea6" + +[[package]] +name = "flate2" +version = "1.0.33" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "324a1be68054ef05ad64b861cc9eaf1d623d2d8cb25b4bf2cb9cdd902b4bf253" +dependencies = [ + "crc32fast", + "miniz_oxide 0.8.0", +] + +[[package]] +name = "fnv" +version = "1.0.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3f9eec918d3f24069decb9af1554cad7c880e2da24a9afd88aca000531ab82c1" + +[[package]] +name = "foreign-types" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f6f339eb8adc052cd2ca78910fda869aefa38d22d5cb648e6485e4d3fc06f3b1" +dependencies = [ + "foreign-types-shared", +] + +[[package]] +name = "foreign-types-shared" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "00b0228411908ca8685dba7fc2cdd70ec9990a6e753e89b6ac91a84c40fbaf4b" + +[[package]] +name = "form_urlencoded" +version = "1.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e13624c2627564efccf4934284bdd98cbaa14e79b0b5a141218e507b3a823456" +dependencies = [ + "percent-encoding", +] + +[[package]] +name = "futf" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "df420e2e84819663797d1ec6544b13c5be84629e7bb00dc960d6917db2987843" +dependencies = [ + "mac", + "new_debug_unreachable", +] + +[[package]] +name = "futures-channel" +version = "0.3.30" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "eac8f7d7865dcb88bd4373ab671c8cf4508703796caa2b1985a9ca867b3fcb78" +dependencies = [ + "futures-core", +] + +[[package]] +name = "futures-core" +version = "0.3.30" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dfc6580bb841c5a68e9ef15c77ccc837b40a7504914d52e47b8b0e9bbda25a1d" + +[[package]] +name = "futures-sink" +version = "0.3.30" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9fb8e00e87438d937621c1c6269e53f536c14d3fbd6a042bb24879e57d474fb5" + +[[package]] +name = "futures-task" +version = "0.3.30" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "38d84fa142264698cdce1a9f9172cf383a0c82de1bddcf3092901442c4097004" + +[[package]] +name = "futures-util" +version = "0.3.30" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3d6401deb83407ab3da39eba7e33987a73c3df0c82b4bb5813ee871c19c41d48" +dependencies = [ + "futures-core", + "futures-task", + "pin-project-lite", + "pin-utils", +] + +[[package]] +name = "fxhash" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c31b6d751ae2c7f11320402d34e41349dd1016f8d5d45e48c4312bc8625af50c" +dependencies = [ + "byteorder", +] + +[[package]] +name = "getopts" +version = "0.2.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "14dbbfd5c71d70241ecf9e6f13737f7b5ce823821063188d7e46c41d371eebd5" +dependencies = [ + "unicode-width 0.1.14", +] + +[[package]] +name = "getrandom" +version = "0.2.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c4567c8db10ae91089c99af84c68c38da3ec2f087c3f82960bcdbf3656b6f4d7" +dependencies = [ + "cfg-if", + "libc", + "wasi 0.11.0+wasi-snapshot-preview1", +] + +[[package]] +name = "getrandom" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "43a49c392881ce6d5c3b8cb70f98717b7c07aabbdff06687b9030dbfbe2725f8" +dependencies = [ + "cfg-if", + "libc", + "wasi 0.13.3+wasi-0.2.2", + "windows-targets 0.52.6", +] + +[[package]] +name = "gimli" +version = "0.29.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "40ecd4077b5ae9fd2e9e169b102c6c330d0605168eb0e8bf79952b256dbefffd" + +[[package]] +name = "h2" +version = "0.4.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "524e8ac6999421f49a846c2d4411f337e53497d8ec55d67753beffa43c5d9205" +dependencies = [ + "atomic-waker", + "bytes", + "fnv", + "futures-core", + "futures-sink", + "http", + "indexmap", + "slab", + "tokio", + "tokio-util", + "tracing", +] + +[[package]] +name = "hashbrown" +version = "0.14.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e5274423e17b7c9fc20b6e7e208532f9b19825d82dfd615708b70edd83df41f1" + +[[package]] +name = "heck" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea" + +[[package]] +name = "hermit-abi" +version = "0.3.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d231dfb89cfffdbc30e7fc41579ed6066ad03abda9e567ccafae602b97ec5024" + +[[package]] +name = "html5ever" +version = "0.29.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2e15626aaf9c351bc696217cbe29cb9b5e86c43f8a46b5e2f5c6c5cf7cb904ce" +dependencies = [ + "log", + "mac", + "markup5ever", + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "http" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "21b9ddb458710bc376481b842f5da65cdf31522de232c1ca8146abce2a358258" +dependencies = [ + "bytes", + "fnv", + "itoa", +] + +[[package]] +name = "http-body" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1efedce1fb8e6913f23e0c92de8e62cd5b772a67e7b3946df930a62566c93184" +dependencies = [ + "bytes", + "http", +] + +[[package]] +name = "http-body-util" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "793429d76616a256bcb62c2a2ec2bed781c8307e797e2598c50010f2bee2544f" +dependencies = [ + "bytes", + "futures-util", + "http", + "http-body", + "pin-project-lite", +] + +[[package]] +name = "httparse" +version = "1.9.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0fcc0b4a115bf80b728eb8ea024ad5bd707b615bfed49e0665b6e0f86fd082d9" + +[[package]] +name = "humantime" +version = "2.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9a3a5bfb195931eeb336b2a7b4d761daec841b97f947d34394601737a7bba5e4" + +[[package]] +name = "hyper" +version = "1.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cc2b571658e38e0c01b1fdca3bbbe93c00d3d71693ff2770043f8c29bc7d6f80" +dependencies = [ + "bytes", + "futures-channel", + "futures-util", + "h2", + "http", + "http-body", + "httparse", + "itoa", + "pin-project-lite", + "smallvec", + "tokio", + "want", +] + +[[package]] +name = "hyper-rustls" +version = "0.27.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5ee4be2c948921a1a5320b629c4193916ed787a7f7f293fd3f7f5a6c9de74155" +dependencies = [ + "futures-util", + "http", + "hyper", + "hyper-util", + "rustls", + "rustls-pki-types", + "tokio", + "tokio-rustls", + "tower-service", +] + +[[package]] +name = "hyper-tls" +version = "0.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "70206fc6890eaca9fde8a0bf71caa2ddfc9fe045ac9e5c70df101a7dbde866e0" +dependencies = [ + "bytes", + "http-body-util", + "hyper", + "hyper-util", + "native-tls", + "tokio", + "tokio-native-tls", + "tower-service", +] + +[[package]] +name = "hyper-util" +version = "0.1.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b1c293b6b3d21eca78250dc7dbebd6b9210ec5530e038cbfe0661b5c47ab06e8" +dependencies = [ + "base64", + "bytes", + "futures-channel", + "futures-core", + "futures-util", + "http", + "http-body", + "hyper", + "ipnet", + "libc", + "percent-encoding", + "pin-project-lite", + "socket2", + "system-configuration", + "tokio", + "tower-service", + "tracing", + "windows-registry", +] + +[[package]] +name = "ident_case" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b9e0384b61958566e926dc50660321d12159025e767c18e043daf26b70104c39" + +[[package]] +name = "idna" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "634d9b1461af396cad843f47fdba5597a4f9e6ddd4bfb6ff5d85028c25cb12f6" +dependencies = [ + "unicode-bidi", + "unicode-normalization", +] + +[[package]] +name = "indexmap" +version = "2.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "93ead53efc7ea8ed3cfb0c79fc8023fbb782a5432b52830b6518941cebe6505c" +dependencies = [ + "equivalent", + "hashbrown", +] + +[[package]] +name = "indoc" +version = "2.0.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b248f5224d1d606005e02c97f5aa4e88eeb230488bcc03bc9ca4d7991399f2b5" + +[[package]] +name = "ipnet" +version = "2.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8f518f335dce6725a761382244631d86cf0ccb2863413590b31338feb467f9c3" + +[[package]] +name = "iri-string" +version = "0.7.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dbc5ebe9c3a1a7a5127f920a418f7585e9e758e911d0466ed004f393b0e380b2" +dependencies = [ + "memchr", + "serde", +] + +[[package]] +name = "is_terminal_polyfill" +version = "1.70.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7943c866cc5cd64cbc25b2e01621d07fa8eb2a1a23160ee81ce38704e97b8ecf" + +[[package]] +name = "itoa" +version = "1.0.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "49f1f14873335454500d59611f1cf4a4b0f786f9ac11f4312a78e4cf2566695b" + +[[package]] +name = "js-sys" +version = "0.3.77" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1cfaf33c695fc6e08064efbc1f72ec937429614f25eef83af942d0e227c3a28f" +dependencies = [ + "once_cell", + "wasm-bindgen", +] + +[[package]] +name = "keyring" +version = "3.6.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1961983669d57bdfe6c0f3ef8e4c229b5ef751afcc7d87e4271d2f71f6ccfa8b" +dependencies = [ + "log", +] + +[[package]] +name = "leetcode-cli" +version = "0.4.7" +dependencies = [ + "anyhow", + "async-trait", + "clap", + "clap_complete", + "colored", + "diesel", + "dirs", + "env_logger", + "keyring", + "log", + "nix", + "openssl", + "pyo3", + "rand 0.9.1", + "regex", + "reqwest", + "scraper", + "serde", + "serde_json", + "thiserror", + "tokio", + "toml", + "unicode-width 0.2.1", +] + +[[package]] +name = "libc" +version = "0.2.172" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d750af042f7ef4f724306de029d18836c26c1765a54a6a3f094cbd23a7267ffa" + +[[package]] +name = "libredox" +version = "0.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c0ff37bd590ca25063e35af745c343cb7a0271906fb7b37e4813e8f79f00268d" +dependencies = [ + "bitflags", + "libc", +] + +[[package]] +name = "libsqlite3-sys" +version = "0.30.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2e99fb7a497b1e3339bc746195567ed8d3e24945ecd636e3619d20b9de9e9149" +dependencies = [ + "pkg-config", + "vcpkg", +] + +[[package]] +name = "linux-raw-sys" +version = "0.4.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "78b3ae25bc7c8c38cec158d1f2757ee79e9b3740fbc7ccf0e59e4b08d793fa89" + +[[package]] +name = "lock_api" +version = "0.4.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "07af8b9cdd281b7915f413fa73f29ebd5d55d0d3f0155584dade1ff18cea1b17" +dependencies = [ + "autocfg", + "scopeguard", +] + +[[package]] +name = "log" +version = "0.4.27" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "13dc2df351e3202783a1fe0d44375f7295ffb4049267b0f3018346dc122a1d94" + +[[package]] +name = "mac" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c41e0c4fef86961ac6d6f8a82609f55f31b05e4fce149ac5710e439df7619ba4" + +[[package]] +name = "markup5ever" +version = "0.14.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "82c88c6129bd24319e62a0359cb6b958fa7e8be6e19bb1663bc396b90883aca5" +dependencies = [ + "log", + "phf", + "phf_codegen", + "string_cache", + "string_cache_codegen", + "tendril", +] + +[[package]] +name = "memchr" +version = "2.7.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "78ca9ab1a0babb1e7d5695e3530886289c18cf2f87ec19a575a0abdce112e3a3" + +[[package]] +name = "memoffset" +version = "0.9.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "488016bfae457b036d996092f6cb448677611ce4449e970ceaf42695203f218a" +dependencies = [ + "autocfg", +] + +[[package]] +name = "mime" +version = "0.3.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6877bb514081ee2a7ff5ef9de3281f14a4dd4bceac4c09388074a6b5df8a139a" + +[[package]] +name = "miniz_oxide" +version = "0.7.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b8a240ddb74feaf34a79a7add65a741f3167852fba007066dcac1ca548d89c08" +dependencies = [ + "adler", +] + +[[package]] +name = "miniz_oxide" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e2d80299ef12ff69b16a84bb182e3b9df68b5a91574d3d4fa6e41b65deec4df1" +dependencies = [ + "adler2", +] + +[[package]] +name = "mio" +version = "1.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "80e04d1dcff3aae0704555fe5fee3bcfaf3d1fdf8a7e521d5b9d2b42acb52cec" +dependencies = [ + "hermit-abi", + "libc", + "wasi 0.11.0+wasi-snapshot-preview1", + "windows-sys 0.52.0", +] + +[[package]] +name = "native-tls" +version = "0.2.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a8614eb2c83d59d1c8cc974dd3f920198647674a0a035e1af1fa58707e317466" +dependencies = [ + "libc", + "log", + "openssl", + "openssl-probe", + "openssl-sys", + "schannel", + "security-framework", + "security-framework-sys", + "tempfile", +] + +[[package]] +name = "new_debug_unreachable" +version = "1.0.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "650eef8c711430f1a879fdd01d4745a7deea475becfb90269c06775983bbf086" + +[[package]] +name = "nix" +version = "0.30.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "74523f3a35e05aba87a1d978330aef40f67b0304ac79c1c00b294c9830543db6" +dependencies = [ + "bitflags", + "cfg-if", + "cfg_aliases", + "libc", +] + +[[package]] +name = "num-conv" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "51d515d32fb182ee37cda2ccdcb92950d6a3c2893aa280e540671c2cd0f3b1d9" + +[[package]] +name = "object" +version = "0.36.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "27b64972346851a39438c60b341ebc01bba47464ae329e55cf343eb93964efd9" +dependencies = [ + "memchr", +] + +[[package]] +name = "once_cell" +version = "1.19.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3fdb12b2476b595f9358c5161aa467c2438859caa136dec86c26fdd2efe17b92" + +[[package]] +name = "openssl" +version = "0.10.73" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8505734d46c8ab1e19a1dce3aef597ad87dcb4c37e7188231769bd6bd51cebf8" +dependencies = [ + "bitflags", + "cfg-if", + "foreign-types", + "libc", + "once_cell", + "openssl-macros", + "openssl-sys", +] + +[[package]] +name = "openssl-macros" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a948666b637a0f465e8564c73e89d4dde00d72d4d473cc972f390fc3dcee7d9c" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "openssl-probe" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ff011a302c396a5197692431fc1948019154afc178baf7d8e37367442a4601cf" + +[[package]] +name = "openssl-sys" +version = "0.9.109" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "90096e2e47630d78b7d1c20952dc621f957103f8bc2c8359ec81290d75238571" +dependencies = [ + "cc", + "libc", + "pkg-config", + "vcpkg", +] + +[[package]] +name = "option-ext" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "04744f49eae99ab78e0d5c0b603ab218f515ea8cfe5a456d7629ad883a3b6e7d" + +[[package]] +name = "parking_lot" +version = "0.12.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f1bf18183cf54e8d6059647fc3063646a1801cf30896933ec2311622cc4b9a27" +dependencies = [ + "lock_api", + "parking_lot_core", +] + +[[package]] +name = "parking_lot_core" +version = "0.9.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e401f977ab385c9e4e3ab30627d6f26d00e2c73eef317493c4ec6d468726cf8" +dependencies = [ + "cfg-if", + "libc", + "redox_syscall", + "smallvec", + "windows-targets 0.52.6", +] + +[[package]] +name = "percent-encoding" +version = "2.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e3148f5046208a5d56bcfc03053e3ca6334e51da8dfb19b6cdc8b306fae3283e" + +[[package]] +name = "phf" +version = "0.11.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ade2d8b8f33c7333b51bcf0428d37e217e9f32192ae4772156f65063b8ce03dc" +dependencies = [ + "phf_macros", + "phf_shared 0.11.2", +] + +[[package]] +name = "phf_codegen" +version = "0.11.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e8d39688d359e6b34654d328e262234662d16cc0f60ec8dcbe5e718709342a5a" +dependencies = [ + "phf_generator 0.11.2", + "phf_shared 0.11.2", +] + +[[package]] +name = "phf_generator" +version = "0.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5d5285893bb5eb82e6aaf5d59ee909a06a16737a8970984dd7746ba9283498d6" +dependencies = [ + "phf_shared 0.10.0", + "rand 0.8.5", +] + +[[package]] +name = "phf_generator" +version = "0.11.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "48e4cc64c2ad9ebe670cb8fd69dd50ae301650392e81c05f9bfcb2d5bdbc24b0" +dependencies = [ + "phf_shared 0.11.2", + "rand 0.8.5", +] + +[[package]] +name = "phf_macros" +version = "0.11.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3444646e286606587e49f3bcf1679b8cef1dc2c5ecc29ddacaffc305180d464b" +dependencies = [ + "phf_generator 0.11.2", + "phf_shared 0.11.2", + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "phf_shared" +version = "0.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b6796ad771acdc0123d2a88dc428b5e38ef24456743ddb1744ed628f9815c096" +dependencies = [ + "siphasher", +] + +[[package]] +name = "phf_shared" +version = "0.11.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "90fcb95eef784c2ac79119d1dd819e162b5da872ce6f3c3abe1e8ca1c082f72b" +dependencies = [ + "siphasher", +] + +[[package]] +name = "pin-project-lite" +version = "0.2.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bda66fc9667c18cb2758a2ac84d1167245054bcf85d5d1aaa6923f45801bdd02" + +[[package]] +name = "pin-utils" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8b870d8c151b6f2fb93e84a13146138f05d02ed11c7e7c54f8826aaaf7c9f184" + +[[package]] +name = "pkg-config" +version = "0.3.30" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d231b230927b5e4ad203db57bbcbee2802f6bce620b1e4a9024a07d94e2907ec" + +[[package]] +name = "portable-atomic" +version = "1.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "da544ee218f0d287a911e9c99a39a8c9bc8fcad3cb8db5959940044ecfc67265" + +[[package]] +name = "powerfmt" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "439ee305def115ba05938db6eb1644ff94165c5ab5e9420d1c1bcedbba909391" + +[[package]] +name = "ppv-lite86" +version = "0.2.20" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "77957b295656769bb8ad2b6a6b09d897d94f05c41b069aede1fcdaa675eaea04" +dependencies = [ + "zerocopy 0.7.35", +] + +[[package]] +name = "precomputed-hash" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "925383efa346730478fb4838dbe9137d2a47675ad789c546d150a6e1dd4ab31c" + +[[package]] +name = "proc-macro2" +version = "1.0.92" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "37d3544b3f2748c54e147655edb5025752e2303145b5aefb3c3ea2c78b973bb0" +dependencies = [ + "unicode-ident", +] + +[[package]] +name = "pyo3" +version = "0.25.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8970a78afe0628a3e3430376fc5fd76b6b45c4d43360ffd6cdd40bdde72b682a" +dependencies = [ + "indoc", + "libc", + "memoffset", + "once_cell", + "portable-atomic", + "pyo3-build-config", + "pyo3-ffi", + "pyo3-macros", + "unindent", +] + +[[package]] +name = "pyo3-build-config" +version = "0.25.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "458eb0c55e7ece017adeba38f2248ff3ac615e53660d7c71a238d7d2a01c7598" +dependencies = [ + "once_cell", + "target-lexicon", +] + +[[package]] +name = "pyo3-ffi" +version = "0.25.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7114fe5457c61b276ab77c5055f206295b812608083644a5c5b2640c3102565c" +dependencies = [ + "libc", + "pyo3-build-config", +] + +[[package]] +name = "pyo3-macros" +version = "0.25.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a8725c0a622b374d6cb051d11a0983786448f7785336139c3c94f5aa6bef7e50" +dependencies = [ + "proc-macro2", + "pyo3-macros-backend", + "quote", + "syn", +] + +[[package]] +name = "pyo3-macros-backend" +version = "0.25.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4109984c22491085343c05b0dbc54ddc405c3cf7b4374fc533f5c3313a572ccc" +dependencies = [ + "heck", + "proc-macro2", + "pyo3-build-config", + "quote", + "syn", +] + +[[package]] +name = "quote" +version = "1.0.37" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b5b9d34b8991d19d98081b46eacdd8eb58c6f2b201139f7c5f643cc155a633af" +dependencies = [ + "proc-macro2", +] + +[[package]] +name = "rand" +version = "0.8.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "34af8d1a0e25924bc5b7c43c079c942339d8f0a8b57c39049bef581b46327404" +dependencies = [ + "libc", + "rand_chacha 0.3.1", + "rand_core 0.6.4", +] + +[[package]] +name = "rand" +version = "0.9.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9fbfd9d094a40bf3ae768db9361049ace4c0e04a4fd6b359518bd7b73a73dd97" +dependencies = [ + "rand_chacha 0.9.0", + "rand_core 0.9.0", +] + +[[package]] +name = "rand_chacha" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e6c10a63a0fa32252be49d21e7709d4d4baf8d231c2dbce1eaa8141b9b127d88" +dependencies = [ + "ppv-lite86", + "rand_core 0.6.4", +] + +[[package]] +name = "rand_chacha" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d3022b5f1df60f26e1ffddd6c66e8aa15de382ae63b3a0c1bfc0e4d3e3f325cb" +dependencies = [ + "ppv-lite86", + "rand_core 0.9.0", +] + +[[package]] +name = "rand_core" +version = "0.6.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ec0be4795e2f6a28069bec0b5ff3e2ac9bafc99e6a9a7dc3547996c5c816922c" +dependencies = [ + "getrandom 0.2.15", +] + +[[package]] +name = "rand_core" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b08f3c9802962f7e1b25113931d94f43ed9725bebc59db9d0c3e9a23b67e15ff" +dependencies = [ + "getrandom 0.3.1", + "zerocopy 0.8.14", +] + +[[package]] +name = "redox_syscall" +version = "0.5.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2a908a6e00f1fdd0dfd9c0eb08ce85126f6d8bbda50017e74bc4a4b7d4a926a4" +dependencies = [ + "bitflags", +] + +[[package]] +name = "redox_users" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dd6f9d3d47bdd2ad6945c5015a226ec6155d0bcdfd8f7cd29f86b71f8de99d2b" +dependencies = [ + "getrandom 0.2.15", + "libredox", + "thiserror", +] + +[[package]] +name = "regex" +version = "1.11.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b544ef1b4eac5dc2db33ea63606ae9ffcfac26c1416a2806ae0bf5f56b201191" +dependencies = [ + "aho-corasick", + "memchr", + "regex-automata", + "regex-syntax", +] + +[[package]] +name = "regex-automata" +version = "0.4.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "368758f23274712b504848e9d5a6f010445cc8b87a7cdb4d7cbee666c1288da3" +dependencies = [ + "aho-corasick", + "memchr", + "regex-syntax", +] + +[[package]] +name = "regex-syntax" +version = "0.8.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2b15c43186be67a4fd63bee50d0303afffcef381492ebe2c5d87f324e1b8815c" + +[[package]] +name = "reqwest" +version = "0.12.22" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cbc931937e6ca3a06e3b6c0aa7841849b160a90351d6ab467a8b9b9959767531" +dependencies = [ + "async-compression", + "base64", + "bytes", + "encoding_rs", + "futures-core", + "futures-util", + "h2", + "http", + "http-body", + "http-body-util", + "hyper", + "hyper-rustls", + "hyper-tls", + "hyper-util", + "js-sys", + "log", + "mime", + "native-tls", + "percent-encoding", + "pin-project-lite", + "rustls-pki-types", + "serde", + "serde_json", + "serde_urlencoded", + "sync_wrapper", + "tokio", + "tokio-native-tls", + "tokio-util", + "tower", + "tower-http", + "tower-service", + "url", + "wasm-bindgen", + "wasm-bindgen-futures", + "web-sys", +] + +[[package]] +name = "ring" +version = "0.17.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c17fa4cb658e3583423e915b9f3acc01cceaee1860e33d59ebae66adc3a2dc0d" +dependencies = [ + "cc", + "cfg-if", + "getrandom 0.2.15", + "libc", + "spin", + "untrusted", + "windows-sys 0.52.0", +] + +[[package]] +name = "rustc-demangle" +version = "0.1.24" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "719b953e2095829ee67db738b3bfa9fa368c94900df327b3f07fe6e794d2fe1f" + +[[package]] +name = "rustix" +version = "0.38.34" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "70dc5ec042f7a43c4a73241207cecc9873a06d45debb38b329f8541d85c2730f" +dependencies = [ + "bitflags", + "errno", + "libc", + "linux-raw-sys", + "windows-sys 0.52.0", +] + +[[package]] +name = "rustls" +version = "0.23.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c58f8c84392efc0a126acce10fa59ff7b3d2ac06ab451a33f2741989b806b044" +dependencies = [ + "once_cell", + "rustls-pki-types", + "rustls-webpki", + "subtle", + "zeroize", +] + +[[package]] +name = "rustls-pki-types" +version = "1.12.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "229a4a4c221013e7e1f1a043678c5cc39fe5171437c88fb47151a21e6f5b5c79" +dependencies = [ + "zeroize", +] + +[[package]] +name = "rustls-webpki" +version = "0.102.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8e6b52d4fda176fd835fdc55a835d4a89b8499cad995885a21149d5ad62f852e" +dependencies = [ + "ring", + "rustls-pki-types", + "untrusted", +] + +[[package]] +name = "rustversion" +version = "1.0.20" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "eded382c5f5f786b989652c49544c4877d9f015cc22e145a5ea8ea66c2921cd2" + +[[package]] +name = "ryu" +version = "1.0.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f3cb5ba0dc43242ce17de99c180e96db90b235b8a9fdc9543c96d2209116bd9f" + +[[package]] +name = "schannel" +version = "0.1.23" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fbc91545643bcf3a0bbb6569265615222618bdf33ce4ffbbd13c4bbd4c093534" +dependencies = [ + "windows-sys 0.52.0", +] + +[[package]] +name = "scopeguard" +version = "1.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "94143f37725109f92c262ed2cf5e59bce7498c01bcc1502d7b9afe439a4e9f49" + +[[package]] +name = "scraper" +version = "0.23.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "527e65d9d888567588db4c12da1087598d0f6f8b346cc2c5abc91f05fc2dffe2" +dependencies = [ + "cssparser", + "ego-tree", + "getopts", + "html5ever", + "precomputed-hash", + "selectors", + "tendril", +] + +[[package]] +name = "security-framework" +version = "2.11.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "897b2245f0b511c87893af39b033e5ca9cce68824c4d7e7630b5a1d339658d02" +dependencies = [ + "bitflags", + "core-foundation", + "core-foundation-sys", + "libc", + "security-framework-sys", +] + +[[package]] +name = "security-framework-sys" +version = "2.11.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "75da29fe9b9b08fe9d6b22b5b4bcbc75d8db3aa31e639aa56bb62e9d46bfceaf" +dependencies = [ + "core-foundation-sys", + "libc", +] + +[[package]] +name = "selectors" +version = "0.26.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fd568a4c9bb598e291a08244a5c1f5a8a6650bee243b5b0f8dbb3d9cc1d87fe8" +dependencies = [ + "bitflags", + "cssparser", + "derive_more", + "fxhash", + "log", + "new_debug_unreachable", + "phf", + "phf_codegen", + "precomputed-hash", + "servo_arc", + "smallvec", +] + +[[package]] +name = "serde" +version = "1.0.219" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5f0e2c6ed6606019b4e29e69dbaba95b11854410e5347d525002456dbbb786b6" +dependencies = [ + "serde_derive", +] + +[[package]] +name = "serde_derive" +version = "1.0.219" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5b0276cf7f2c73365f7157c8123c21cd9a50fbbd844757af28ca1f5925fc2a00" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "serde_json" +version = "1.0.140" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "20068b6e96dc6c9bd23e01df8827e6c7e1f2fddd43c21810382803c136b99373" +dependencies = [ + "itoa", + "memchr", + "ryu", + "serde", +] + +[[package]] +name = "serde_spanned" +version = "0.6.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bf41e0cfaf7226dca15e8197172c295a782857fcb97fad1808a166870dee75a3" +dependencies = [ + "serde", +] + +[[package]] +name = "serde_urlencoded" +version = "0.7.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d3491c14715ca2294c4d6a88f15e84739788c1d030eed8c110436aafdaa2f3fd" +dependencies = [ + "form_urlencoded", + "itoa", + "ryu", + "serde", +] + +[[package]] +name = "servo_arc" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ae65c4249478a2647db249fb43e23cec56a2c8974a427e7bd8cb5a1d0964921a" +dependencies = [ + "stable_deref_trait", +] + +[[package]] +name = "shlex" +version = "1.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0fda2ff0d084019ba4d7c6f371c95d8fd75ce3524c3cb8fb653a3023f6323e64" + +[[package]] +name = "signal-hook-registry" +version = "1.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a9e9e0b4211b72e7b8b6e85c807d36c212bdb33ea8587f7569562a84df5465b1" +dependencies = [ + "libc", +] + +[[package]] +name = "siphasher" +version = "0.3.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "38b58827f4464d87d377d175e90bf58eb00fd8716ff0a62f80356b5e61555d0d" + +[[package]] +name = "slab" +version = "0.4.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8f92a496fb766b417c996b9c5e57daf2f7ad3b0bebe1ccfca4856390e3d3bb67" +dependencies = [ + "autocfg", +] + +[[package]] +name = "smallvec" +version = "1.13.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3c5e1a9a646d36c3599cd173a41282daf47c44583ad367b8e6837255952e5c67" + +[[package]] +name = "socket2" +version = "0.5.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e22376abed350d73dd1cd119b57ffccad95b4e585a7cda43e286245ce23c0678" +dependencies = [ + "libc", + "windows-sys 0.52.0", +] + +[[package]] +name = "spin" +version = "0.9.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6980e8d7511241f8acf4aebddbb1ff938df5eebe98691418c4468d0b72a96a67" + +[[package]] +name = "stable_deref_trait" +version = "1.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a8f112729512f8e442d81f95a8a7ddf2b7c6b8a1a6f509a95864142b30cab2d3" + +[[package]] +name = "string_cache" +version = "0.8.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f91138e76242f575eb1d3b38b4f1362f10d3a43f47d182a5b359af488a02293b" +dependencies = [ + "new_debug_unreachable", + "once_cell", + "parking_lot", + "phf_shared 0.10.0", + "precomputed-hash", + "serde", +] + +[[package]] +name = "string_cache_codegen" +version = "0.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6bb30289b722be4ff74a408c3cc27edeaad656e06cb1fe8fa9231fa59c728988" +dependencies = [ + "phf_generator 0.10.0", + "phf_shared 0.10.0", + "proc-macro2", + "quote", +] + +[[package]] +name = "strsim" +version = "0.11.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7da8b5736845d9f2fcb837ea5d9e2628564b3b043a70948a3f0b778838c5fb4f" + +[[package]] +name = "subtle" +version = "2.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "13c2bddecc57b384dee18652358fb23172facb8a2c51ccc10d74c157bdea3292" + +[[package]] +name = "syn" +version = "2.0.90" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "919d3b74a5dd0ccd15aeb8f93e7006bd9e14c295087c9896a110f490752bcf31" +dependencies = [ + "proc-macro2", + "quote", + "unicode-ident", +] + +[[package]] +name = "sync_wrapper" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a7065abeca94b6a8a577f9bd45aa0867a2238b74e8eb67cf10d492bc39351394" +dependencies = [ + "futures-core", +] + +[[package]] +name = "system-configuration" +version = "0.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3c879d448e9d986b661742763247d3693ed13609438cf3d006f51f5368a5ba6b" +dependencies = [ + "bitflags", + "core-foundation", + "system-configuration-sys", +] + +[[package]] +name = "system-configuration-sys" +version = "0.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8e1d1b10ced5ca923a1fcb8d03e96b8d3268065d724548c0211415ff6ac6bac4" +dependencies = [ + "core-foundation-sys", + "libc", +] + +[[package]] +name = "target-lexicon" +version = "0.13.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e502f78cdbb8ba4718f566c418c52bc729126ffd16baee5baa718cf25dd5a69a" + +[[package]] +name = "tempfile" +version = "3.12.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "04cbcdd0c794ebb0d4cf35e88edd2f7d2c4c3e9a5a6dab322839b321c6a87a64" +dependencies = [ + "cfg-if", + "fastrand", + "once_cell", + "rustix", + "windows-sys 0.59.0", +] + +[[package]] +name = "tendril" +version = "0.4.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d24a120c5fc464a3458240ee02c299ebcb9d67b5249c8848b09d639dca8d7bb0" +dependencies = [ + "futf", + "mac", + "utf-8", +] + +[[package]] +name = "thiserror" +version = "2.0.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "567b8a2dae586314f7be2a752ec7474332959c6460e02bde30d702a66d488708" +dependencies = [ + "thiserror-impl", +] + +[[package]] +name = "thiserror-impl" +version = "2.0.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7f7cf42b4507d8ea322120659672cf1b9dbb93f8f2d4ecfd6e51350ff5b17a1d" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "time" +version = "0.3.36" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5dfd88e563464686c916c7e46e623e520ddc6d79fa6641390f2e3fa86e83e885" +dependencies = [ + "deranged", + "itoa", + "num-conv", + "powerfmt", + "serde", + "time-core", + "time-macros", +] + +[[package]] +name = "time-core" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ef927ca75afb808a4d64dd374f00a2adf8d0fcff8e7b184af886c3c87ec4a3f3" + +[[package]] +name = "time-macros" +version = "0.2.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3f252a68540fde3a3877aeea552b832b40ab9a69e318efd078774a01ddee1ccf" +dependencies = [ + "num-conv", + "time-core", +] + +[[package]] +name = "tinyvec" +version = "1.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "445e881f4f6d382d5f27c034e25eb92edd7c784ceab92a0937db7f2e9471b938" +dependencies = [ + "tinyvec_macros", +] + +[[package]] +name = "tinyvec_macros" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1f3ccbac311fea05f86f61904b462b55fb3df8837a366dfc601a0161d0532f20" + +[[package]] +name = "tokio" +version = "1.45.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "75ef51a33ef1da925cea3e4eb122833cb377c61439ca401b770f54902b806779" +dependencies = [ + "backtrace", + "bytes", + "libc", + "mio", + "parking_lot", + "pin-project-lite", + "signal-hook-registry", + "socket2", + "tokio-macros", + "windows-sys 0.52.0", +] + +[[package]] +name = "tokio-macros" +version = "2.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6e06d43f1345a3bcd39f6a56dbb7dcab2ba47e68e8ac134855e7e2bdbaf8cab8" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "tokio-native-tls" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bbae76ab933c85776efabc971569dd6119c580d8f5d448769dec1764bf796ef2" +dependencies = [ + "native-tls", + "tokio", +] + +[[package]] +name = "tokio-rustls" +version = "0.26.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0c7bc40d0e5a97695bb96e27995cd3a08538541b0a846f65bba7a359f36700d4" +dependencies = [ + "rustls", + "rustls-pki-types", + "tokio", +] + +[[package]] +name = "tokio-util" +version = "0.7.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9cf6b47b3771c49ac75ad09a6162f53ad4b8088b76ac60e8ec1455b31a189fe1" +dependencies = [ + "bytes", + "futures-core", + "futures-sink", + "pin-project-lite", + "tokio", +] + +[[package]] +name = "toml" +version = "0.8.23" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dc1beb996b9d83529a9e75c17a1686767d148d70663143c7854d8b4a09ced362" +dependencies = [ + "serde", + "serde_spanned", + "toml_datetime", + "toml_edit", +] + +[[package]] +name = "toml_datetime" +version = "0.6.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "22cddaf88f4fbc13c51aebbf5f8eceb5c7c5a9da2ac40a13519eb5b0a0e8f11c" +dependencies = [ + "serde", +] + +[[package]] +name = "toml_edit" +version = "0.22.27" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "41fe8c660ae4257887cf66394862d21dbca4a6ddd26f04a3560410406a2f819a" +dependencies = [ + "indexmap", + "serde", + "serde_spanned", + "toml_datetime", + "toml_write", + "winnow", +] + +[[package]] +name = "toml_write" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5d99f8c9a7727884afe522e9bd5edbfc91a3312b36a77b5fb8926e4c31a41801" + +[[package]] +name = "tower" +version = "0.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d039ad9159c98b70ecfd540b2573b97f7f52c3e8d9f8ad57a24b916a536975f9" +dependencies = [ + "futures-core", + "futures-util", + "pin-project-lite", + "sync_wrapper", + "tokio", + "tower-layer", + "tower-service", +] + +[[package]] +name = "tower-http" +version = "0.6.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "adc82fd73de2a9722ac5da747f12383d2bfdb93591ee6c58486e0097890f05f2" +dependencies = [ + "bitflags", + "bytes", + "futures-util", + "http", + "http-body", + "iri-string", + "pin-project-lite", + "tower", + "tower-layer", + "tower-service", +] + +[[package]] +name = "tower-layer" +version = "0.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "121c2a6cda46980bb0fcd1647ffaf6cd3fc79a013de288782836f6df9c48780e" + +[[package]] +name = "tower-service" +version = "0.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8df9b6e13f2d32c91b9bd719c00d1958837bc7dec474d94952798cc8e69eeec3" + +[[package]] +name = "tracing" +version = "0.1.40" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c3523ab5a71916ccf420eebdf5521fcef02141234bbc0b8a49f2fdc4544364ef" +dependencies = [ + "pin-project-lite", + "tracing-core", +] + +[[package]] +name = "tracing-core" +version = "0.1.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c06d3da6113f116aaee68e4d601191614c9053067f9ab7f6edbcb161237daa54" +dependencies = [ + "once_cell", +] + +[[package]] +name = "try-lock" +version = "0.2.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e421abadd41a4225275504ea4d6566923418b7f05506fbc9c0fe86ba7396114b" + +[[package]] +name = "unicode-bidi" +version = "0.3.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "08f95100a766bf4f8f28f90d77e0a5461bbdb219042e7679bebe79004fed8d75" + +[[package]] +name = "unicode-ident" +version = "1.0.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3354b9ac3fae1ff6755cb6db53683adb661634f67557942dea4facebec0fee4b" + +[[package]] +name = "unicode-normalization" +version = "0.1.23" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a56d1686db2308d901306f92a263857ef59ea39678a5458e7cb17f01415101f5" +dependencies = [ + "tinyvec", +] + +[[package]] +name = "unicode-width" +version = "0.1.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7dd6e30e90baa6f72411720665d41d89b9a3d039dc45b8faea1ddd07f617f6af" + +[[package]] +name = "unicode-width" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4a1a07cc7db3810833284e8d372ccdc6da29741639ecc70c9ec107df0fa6154c" + +[[package]] +name = "unindent" +version = "0.2.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c7de7d73e1754487cb58364ee906a499937a0dfabd86bcb980fa99ec8c8fa2ce" + +[[package]] +name = "untrusted" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8ecb6da28b8a351d773b68d5825ac39017e680750f980f3a1a85cd8dd28a47c1" + +[[package]] +name = "url" +version = "2.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "22784dbdf76fdde8af1aeda5622b546b422b6fc585325248a2bf9f5e41e94d6c" +dependencies = [ + "form_urlencoded", + "idna", + "percent-encoding", +] + +[[package]] +name = "utf-8" +version = "0.7.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "09cc8ee72d2a9becf2f2febe0205bbed8fc6615b7cb429ad062dc7b7ddd036a9" + +[[package]] +name = "utf8parse" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "06abde3611657adf66d383f00b093d7faecc7fa57071cce2578660c9f1010821" + +[[package]] +name = "vcpkg" +version = "0.2.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "accd4ea62f7bb7a82fe23066fb0957d48ef677f6eeb8215f372f52e48bb32426" + +[[package]] +name = "want" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bfa7760aed19e106de2c7c0b581b509f2f25d3dacaf737cb82ac61bc6d760b0e" +dependencies = [ + "try-lock", +] + +[[package]] +name = "wasi" +version = "0.11.0+wasi-snapshot-preview1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9c8d87e72b64a3b4db28d11ce29237c246188f4f51057d65a7eab63b7987e423" + +[[package]] +name = "wasi" +version = "0.13.3+wasi-0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "26816d2e1a4a36a2940b96c5296ce403917633dff8f3440e9b236ed6f6bacad2" +dependencies = [ + "wit-bindgen-rt", +] + +[[package]] +name = "wasm-bindgen" +version = "0.2.100" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1edc8929d7499fc4e8f0be2262a241556cfc54a0bea223790e71446f2aab1ef5" +dependencies = [ + "cfg-if", + "once_cell", + "rustversion", + "wasm-bindgen-macro", +] + +[[package]] +name = "wasm-bindgen-backend" +version = "0.2.100" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2f0a0651a5c2bc21487bde11ee802ccaf4c51935d0d3d42a6101f98161700bc6" +dependencies = [ + "bumpalo", + "log", + "proc-macro2", + "quote", + "syn", + "wasm-bindgen-shared", +] + +[[package]] +name = "wasm-bindgen-futures" +version = "0.4.43" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "61e9300f63a621e96ed275155c108eb6f843b6a26d053f122ab69724559dc8ed" +dependencies = [ + "cfg-if", + "js-sys", + "wasm-bindgen", + "web-sys", +] + +[[package]] +name = "wasm-bindgen-macro" +version = "0.2.100" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7fe63fc6d09ed3792bd0897b314f53de8e16568c2b3f7982f468c0bf9bd0b407" +dependencies = [ + "quote", + "wasm-bindgen-macro-support", +] + +[[package]] +name = "wasm-bindgen-macro-support" +version = "0.2.100" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8ae87ea40c9f689fc23f209965b6fb8a99ad69aeeb0231408be24920604395de" +dependencies = [ + "proc-macro2", + "quote", + "syn", + "wasm-bindgen-backend", + "wasm-bindgen-shared", +] + +[[package]] +name = "wasm-bindgen-shared" +version = "0.2.100" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1a05d73b933a847d6cccdda8f838a22ff101ad9bf93e33684f39c1f5f0eece3d" +dependencies = [ + "unicode-ident", +] + +[[package]] +name = "web-sys" +version = "0.3.70" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "26fdeaafd9bd129f65e7c031593c24d62186301e0c72c8978fa1678be7d532c0" +dependencies = [ + "js-sys", + "wasm-bindgen", +] + +[[package]] +name = "windows-link" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "76840935b766e1b0a05c0066835fb9ec80071d4c09a16f6bd5f7e655e3c14c38" + +[[package]] +name = "windows-registry" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4286ad90ddb45071efd1a66dfa43eb02dd0dfbae1545ad6cc3c51cf34d7e8ba3" +dependencies = [ + "windows-result", + "windows-strings", + "windows-targets 0.53.0", +] + +[[package]] +name = "windows-result" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c64fd11a4fd95df68efcfee5f44a294fe71b8bc6a91993e2791938abcc712252" +dependencies = [ + "windows-link", +] + +[[package]] +name = "windows-strings" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "87fa48cc5d406560701792be122a10132491cff9d0aeb23583cc2dcafc847319" +dependencies = [ + "windows-link", +] + +[[package]] +name = "windows-sys" +version = "0.52.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "282be5f36a8ce781fad8c8ae18fa3f9beff57ec1b52cb3de0789201425d9a33d" +dependencies = [ + "windows-targets 0.52.6", +] + +[[package]] +name = "windows-sys" +version = "0.59.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e38bc4d79ed67fd075bcc251a1c39b32a1776bbe92e5bef1f0bf1f8c531853b" +dependencies = [ + "windows-targets 0.52.6", +] + +[[package]] +name = "windows-targets" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9b724f72796e036ab90c1021d4780d4d3d648aca59e491e6b98e725b84e99973" +dependencies = [ + "windows_aarch64_gnullvm 0.52.6", + "windows_aarch64_msvc 0.52.6", + "windows_i686_gnu 0.52.6", + "windows_i686_gnullvm 0.52.6", + "windows_i686_msvc 0.52.6", + "windows_x86_64_gnu 0.52.6", + "windows_x86_64_gnullvm 0.52.6", + "windows_x86_64_msvc 0.52.6", +] + +[[package]] +name = "windows-targets" +version = "0.53.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b1e4c7e8ceaaf9cb7d7507c974735728ab453b67ef8f18febdd7c11fe59dca8b" +dependencies = [ + "windows_aarch64_gnullvm 0.53.0", + "windows_aarch64_msvc 0.53.0", + "windows_i686_gnu 0.53.0", + "windows_i686_gnullvm 0.53.0", + "windows_i686_msvc 0.53.0", + "windows_x86_64_gnu 0.53.0", + "windows_x86_64_gnullvm 0.53.0", + "windows_x86_64_msvc 0.53.0", +] + +[[package]] +name = "windows_aarch64_gnullvm" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "32a4622180e7a0ec044bb555404c800bc9fd9ec262ec147edd5989ccd0c02cd3" + +[[package]] +name = "windows_aarch64_gnullvm" +version = "0.53.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "86b8d5f90ddd19cb4a147a5fa63ca848db3df085e25fee3cc10b39b6eebae764" + +[[package]] +name = "windows_aarch64_msvc" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "09ec2a7bb152e2252b53fa7803150007879548bc709c039df7627cabbd05d469" + +[[package]] +name = "windows_aarch64_msvc" +version = "0.53.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c7651a1f62a11b8cbd5e0d42526e55f2c99886c77e007179efff86c2b137e66c" + +[[package]] +name = "windows_i686_gnu" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8e9b5ad5ab802e97eb8e295ac6720e509ee4c243f69d781394014ebfe8bbfa0b" + +[[package]] +name = "windows_i686_gnu" +version = "0.53.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c1dc67659d35f387f5f6c479dc4e28f1d4bb90ddd1a5d3da2e5d97b42d6272c3" + +[[package]] +name = "windows_i686_gnullvm" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0eee52d38c090b3caa76c563b86c3a4bd71ef1a819287c19d586d7334ae8ed66" + +[[package]] +name = "windows_i686_gnullvm" +version = "0.53.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9ce6ccbdedbf6d6354471319e781c0dfef054c81fbc7cf83f338a4296c0cae11" + +[[package]] +name = "windows_i686_msvc" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "240948bc05c5e7c6dabba28bf89d89ffce3e303022809e73deaefe4f6ec56c66" + +[[package]] +name = "windows_i686_msvc" +version = "0.53.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "581fee95406bb13382d2f65cd4a908ca7b1e4c2f1917f143ba16efe98a589b5d" + +[[package]] +name = "windows_x86_64_gnu" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "147a5c80aabfbf0c7d901cb5895d1de30ef2907eb21fbbab29ca94c5b08b1a78" + +[[package]] +name = "windows_x86_64_gnu" +version = "0.53.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2e55b5ac9ea33f2fc1716d1742db15574fd6fc8dadc51caab1c16a3d3b4190ba" + +[[package]] +name = "windows_x86_64_gnullvm" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "24d5b23dc417412679681396f2b49f3de8c1473deb516bd34410872eff51ed0d" + +[[package]] +name = "windows_x86_64_gnullvm" +version = "0.53.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0a6e035dd0599267ce1ee132e51c27dd29437f63325753051e71dd9e42406c57" + +[[package]] +name = "windows_x86_64_msvc" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "589f6da84c646204747d1270a2a5661ea66ed1cced2631d546fdfb155959f9ec" + +[[package]] +name = "windows_x86_64_msvc" +version = "0.53.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "271414315aff87387382ec3d271b52d7ae78726f5d44ac98b4f4030c91880486" + +[[package]] +name = "winnow" +version = "0.7.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "74c7b26e3480b707944fc872477815d29a8e429d2f93a1ce000f5fa84a15cbcd" +dependencies = [ + "memchr", +] + +[[package]] +name = "wit-bindgen-rt" +version = "0.33.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3268f3d866458b787f390cf61f4bbb563b922d091359f9608842999eaee3943c" +dependencies = [ + "bitflags", +] + +[[package]] +name = "zerocopy" +version = "0.7.35" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1b9b4fd18abc82b8136838da5d50bae7bdea537c574d8dc1a34ed098d6c166f0" +dependencies = [ + "byteorder", + "zerocopy-derive 0.7.35", +] + +[[package]] +name = "zerocopy" +version = "0.8.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a367f292d93d4eab890745e75a778da40909cab4d6ff8173693812f79c4a2468" +dependencies = [ + "zerocopy-derive 0.8.14", +] + +[[package]] +name = "zerocopy-derive" +version = "0.7.35" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fa4f8080344d4671fb4e831a13ad1e68092748387dfc4f55e356242fae12ce3e" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "zerocopy-derive" +version = "0.8.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d3931cb58c62c13adec22e38686b559c86a30565e16ad6e8510a337cedc611e1" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "zeroize" +version = "1.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ced3678a2879b30306d323f4542626697a464a97c0a07c9aebf7ebca65cd4dde" diff --git a/Cargo.toml b/Cargo.toml index 3ab4163..a29aa42 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -4,10 +4,10 @@ path = "src/bin/lc.rs" [package] name = "leetcode-cli" -version = "0.3.12" -authors = ["clearloop "] +version = "0.4.7" +authors = ["clearloop "] edition = "2021" -description = "Leet your code in command-line." +description = "Leetcode command-line interface in rust." repository = "https://github.com/clearloop/leetcode-cli" license = "MIT" documentation = "https://docs.rs/leetcode_cli" @@ -16,38 +16,37 @@ keywords = ["cli", "games", "leetcode"] readme = './README.md' [dependencies] -async-trait = "0.1.56" -tokio = { version = "1.19.2", features = ["full"] } -clap = { version = "3.2.10", features = ["cargo"] } -colored = "2.0.0" -dirs = "4.0.0" -env_logger = "0.9.0" -keyring = "1.2.0" -log = "0.4.17" -openssl = "0.10.41" -pyo3 = { version = "0.16.5", optional = true } -rand = "0.8.5" -serde = { version = "1.0.139", features = ["derive"] } -serde_json = "1.0.82" -toml = "0.5.9" -regex = "1.6.0" -scraper = "0.13.0" +async-trait = "0.1.88" +tokio = { version = "1.45.1", features = ["full"] } +clap = { version = "4.5.40", features = ["cargo"] } +colored = "3.0.0" +dirs = "6.0.0" +env_logger = "0.11.6" +keyring = "3.6.2" +log = "0.4.27" +openssl = "0.10.73" +pyo3 = { version = "0.25.1", optional = true } +rand = "0.9.1" +serde = { version = "1.0.219", features = ["derive"] } +serde_json = "1.0.140" +toml = "0.8.23" +regex = "1.11.1" +scraper = "0.23.1" +anyhow = "1.0.98" +clap_complete = "4.5.54" +thiserror = "2.0.12" +unicode-width = "0.2" [dependencies.diesel] -version = "1.4.8" +version = "2.2.11" features = ["sqlite"] [dependencies.reqwest] -version = "0.11.11" +version = "0.12.22" features = ["gzip", "json"] -[dev-dependencies.cargo-husky] -version = "1.5.0" -default-features = false -features = ["precommit-hook", "user-hooks"] - [features] pym = ["pyo3"] [target.'cfg(target_family = "unix")'.dependencies] -nix = "0.24.1" +nix = { version = "0.30.1", features = [ "signal" ] } diff --git a/README.md b/README.md index d7a3517..315587a 100644 --- a/README.md +++ b/README.md @@ -1,16 +1,17 @@ # leetcode-cli -![Rust](https://github.com/clearloop/leetcode-cli/workflows/Rust/badge.svg) + +![Rust](https://github.com/clearloop/leetcode-cli/workflows/leetcode-cli/badge.svg) [![crate](https://img.shields.io/crates/v/leetcode-cli.svg)](https://crates.io/crates/leetcode-cli) [![doc](https://img.shields.io/badge/current-docs-brightgreen.svg)](https://docs.rs/leetcode-cli/) [![downloads](https://img.shields.io/crates/d/leetcode-cli.svg)](https://crates.io/crates/leetcode-cli) -[![gitter](https://img.shields.io/gitter/room/odditypark/leetcode-cli)](https://gitter.im/Odditypark/leetcode-cli) +[![telegram](https://img.shields.io/badge/telegram-blue?logo=telegram)](https://t.me/+U_5si6PhWykxZTI1) [![LICENSE](https://img.shields.io/crates/l/leetcode-cli.svg)](https://choosealicense.com/licenses/mit/) ## Installing ```sh # Required dependencies: -# +# # gcc # libssl-dev # libdbus-1-dev @@ -19,12 +20,33 @@ cargo install leetcode-cli ``` +
+Shell completions + +For Bash and Zsh (by default picks up `$SHELL` from environment) + +```sh +eval "$(leetcode completions)" +``` + +Copy the line above to `.bash_profile` or `.zshrc` + +You may also obtain specific shell configuration using. + +```sh +leetcode completions fish +``` + +If no argument is provided, the shell is inferred from the `SHELL` environment variable. + +
+ ## Usage -**Make sure you have logged in to `leetcode.com` with `Chrome`**. See [Cookies](#cookies) for why you need to do this first. +**Make sure you have logged in to `leetcode.com` with `Firefox`**. See [Cookies](#cookies) for why you need to do this first. ```sh -leetcode 0.3.10 +leetcode 0.4.0 May the Code be with You 👻 USAGE: @@ -42,18 +64,142 @@ SUBCOMMANDS: list List problems [aliases: l] pick Pick a problem [aliases: p] stat Show simple chart about submissions [aliases: s] - test Edit question by id [aliases: t] + test Test question by id [aliases: t] help Prints this message or the help of the given subcommand(s) ``` ## Example -For example, given this config (can be found in `~/.leetcode/leetcode.toml`, it can be generated automatically with command: `leetcode list` if you are a new user): +To configure leetcode-cli, create a file at `~/.leetcode/leetcode.toml`): ```toml [code] -lang = "rust" -editor = "emacs" +editor = 'emacs' +# Optional parameter +editor_args = ['-nw'] +# Optional environment variables (ex. [ "XDG_DATA_HOME=...", "XDG_CONFIG_HOME=...", "XDG_STATE_HOME=..." ]) +editor_envs = [] +lang = 'rust' +edit_code_marker = false +start_marker = "" +end_marker = "" +# if include problem description +comment_problem_desc = false +# comment syntax +comment_leading = "" +test = true + +[cookies] +csrf = '' +session = '' +# leetcode.com or leetcode.cn +site = "leetcode.com" + +[storage] +cache = 'Problems' +code = 'code' +root = '~/.leetcode' +scripts = 'scripts' +``` + +
+ Configuration Explanation + +```toml +[code] +editor = 'emacs' +# Optional parameter +editor_args = ['-nw'] +# Optional environment variables (ex. [ "XDG_DATA_HOME=...", "XDG_CONFIG_HOME=...", "XDG_STATE_HOME=..." ]) +editor_envs = [] +lang = 'rust' +edit_code_marker = true +start_marker = "start_marker" +end_marker = "end_marker" +# if include problem description +comment_problem_desc = true +# comment syntax +comment_leading = "//" +test = true + +[cookies] +csrf = '' +session = '' + +[storage] +cache = 'Problems' +code = 'code' +root = '~/.leetcode' +scripts = 'scripts' +``` + +If we change the configuration as shown previously, we will get the following code after `leetcode edit 15`. + +```rust +// Category: algorithms +// Level: Medium +// Percent: 32.90331% + +// Given an integer array nums, return all the triplets [nums[i], nums[j], nums[k]] such that i != j, i != k, and j != k, and nums[i] + nums[j] + nums[k] == 0. +// +// Notice that the solution set must not contain duplicate triplets. +// +//   +// Example 1: +// +// Input: nums = [-1,0,1,2,-1,-4] +// Output: [[-1,-1,2],[-1,0,1]] +// Explanation: +// nums[0] + nums[1] + nums[2] = (-1) + 0 + 1 = 0. +// nums[1] + nums[2] + nums[4] = 0 + 1 + (-1) = 0. +// nums[0] + nums[3] + nums[4] = (-1) + 2 + (-1) = 0. +// The distinct triplets are [-1,0,1] and [-1,-1,2]. +// Notice that the order of the output and the order of the triplets does not matter. +// +// +// Example 2: +// +// Input: nums = [0,1,1] +// Output: [] +// Explanation: The only possible triplet does not sum up to 0. +// +// +// Example 3: +// +// Input: nums = [0,0,0] +// Output: [[0,0,0]] +// Explanation: The only possible triplet sums up to 0. +// +// +//   +// Constraints: +// +// +// 3 <= nums.length <= 3000 +// -10⁵ <= nums[i] <= 10⁵ +// + +// start_marker +impl Solution { +pub fn three_sum(nums: Vec) -> Vec> { + + } + +} +// end_marker + +``` + +
+ +
+ +Some linting tools/lsps will throw errors unless the necessary libraries are imported. leetcode-cli can generate this boilerplate automatically if the `inject_before` key is set. Similarly, if you want to test out your code locally, you can automate that with `inject_after`. For c++ this might look something like: + +```toml +[code] +inject_before = ["#include", "using namespace std;"] +inject_after = ["int main() {\n Solution solution;\n\n}"] ``` #### 1. pick @@ -62,6 +208,10 @@ editor = "emacs" leetcode pick 1 ``` +```sh +leetcode pick --name "Two Sum" +``` + ```sh [1] Two Sum is on the run... @@ -142,7 +292,8 @@ leetcode exec 1 ## Cookies -The cookie plugin of leetcode-cli can work on OSX and [Linux][#1]. **If you are on a different platform, there are problems with caching the cookies**, you can manually input your LeetCode Cookies to the configuration file. +The cookie plugin of leetcode-cli can work on OSX and [Linux][#1]. **If you are on a different platform, there are problems with caching the cookies**, +you can manually input your LeetCode Cookies to the configuration file. ```toml [cookies] @@ -150,26 +301,47 @@ csrf = "..." session = "..." ``` -For Example, using Chrome (after logging in to LeetCode): - +For Example, using Firefox (after logging in to LeetCode): #### Step 1 -Open Chrome and navigate to the link below: +Open Firefox, press F12, and click `Storage` tab. -```sh -chrome://settings/cookies/detail?site=leetcode.com -``` +#### Step 2 + +Expand `Cookies` tab on the left and select https://leetcode.com. #### Step 2 -Copy `Content` from `LEETCODE_SESSION` and `csrftoken` to `session` and `csrf` in your configuration file, respectively: +Copy `Value` from `LEETCODE_SESSION` and `csrftoken` to `session` and `csrf` in your configuration file, respectively: + +```toml +[cookies] +csrf = '' +session = '' +``` + +#### Environment variables + +The cookies can also be overridden by environment variables, which might be useful to exclude the sensitive information from the configuration file `leetcode.toml`. To do this, you can leave the `csrf` and `session` fields empty in the configuration file and override cookies settings via the environment variables `LEETCODE_CSRF`, `LEETCODE_SESSION`, and `LEETCODE_SITE`: + ```toml [cookies] -csrf = "${csrftoken}" -session = "${LEETCODE_SESSION}" +csrf = '' +session = '' +site = 'leetcode.com' +``` + +Then set the environment variables: + +```bash +export LEETCODE_CSRF='' +export LEETCODE_SESSION='' +export LEETCODE_SITE='leetcode.cn' # or 'leetcode.com' ``` +Note that `cookies.site` in still required in the `leetcode.toml` to avoid exception during configuration file parsing, but can be overridden using environment variables. + ## Programmable If you want to filter LeetCode questions using custom Python scripts, add the following to your the configuration file: @@ -187,13 +359,13 @@ import json; def plan(sps, stags): ## - # `print` in python is supported, - # if you want to know the data structures of these two args, + # `print` in python is supported, + # if you want to know the data structures of these two args, # just print them ## problems = json.loads(sps) tags = json.loads(stags) - + ret = [] tm = {} for tag in tags: @@ -213,17 +385,15 @@ Then run `list` with the filter that you just wrote: leetcode list -p plan1 ``` -And that's it! Enjoy! +That's it! Enjoy! +## Contributions -## PR - -[PRs][pr] are more than welcome! +Feel free to add your names and emails in the `authors` field of `Cargo.toml` ! ## LICENSE MIT - [pr]: https://github.com/clearloop/leetcode-cli/pulls [#1]: https://github.com/clearloop/leetcode-cli/issues/1 diff --git a/SECURITY.md b/SECURITY.md new file mode 100644 index 0000000..034e848 --- /dev/null +++ b/SECURITY.md @@ -0,0 +1,21 @@ +# Security Policy + +## Supported Versions + +Use this section to tell people about which versions of your project are +currently being supported with security updates. + +| Version | Supported | +| ------- | ------------------ | +| 5.1.x | :white_check_mark: | +| 5.0.x | :x: | +| 4.0.x | :white_check_mark: | +| < 4.0 | :x: | + +## Reporting a Vulnerability + +Use this section to tell people how to report a vulnerability. + +Tell them where to go, how often they can expect to get an update on a +reported vulnerability, what to expect if the vulnerability is accepted or +declined, etc. diff --git a/flake.lock b/flake.lock index a6f5fbf..8d2d0df 100644 --- a/flake.lock +++ b/flake.lock @@ -1,43 +1,32 @@ { "nodes": { - "flake-utils": { - "locked": { - "lastModified": 1637014545, - "narHash": "sha256-26IZAc5yzlD9FlDT54io1oqG/bBoyka+FJk5guaX4x4=", - "owner": "numtide", - "repo": "flake-utils", - "rev": "bba5dcc8e0b20ab664967ad83d24d64cb64ec4f4", - "type": "github" + "naersk": { + "inputs": { + "nixpkgs": [ + "nixpkgs" + ] }, - "original": { - "owner": "numtide", - "repo": "flake-utils", - "type": "github" - } - }, - "nixpkgs": { "locked": { - "lastModified": 1644151317, - "narHash": "sha256-TpXGBYCFKvEN7Q+To45rn4kqTbLPY4f56rF6ymUGGRE=", - "owner": "NixOS", - "repo": "nixpkgs", - "rev": "942b0817e898262cc6e3f0a5f706ce09d8f749f1", + "lastModified": 1721727458, + "narHash": "sha256-r/xppY958gmZ4oTfLiHN0ZGuQ+RSTijDblVgVLFi1mw=", + "owner": "nix-community", + "repo": "naersk", + "rev": "3fb418eaf352498f6b6c30592e3beb63df42ef11", "type": "github" }, "original": { - "owner": "NixOS", - "ref": "nixpkgs-unstable", - "repo": "nixpkgs", + "owner": "nix-community", + "repo": "naersk", "type": "github" } }, - "nixpkgs_2": { + "nixpkgs": { "locked": { - "lastModified": 1637453606, - "narHash": "sha256-Gy6cwUswft9xqsjWxFYEnx/63/qzaFUwatcbV5GF/GQ=", + "lastModified": 1728538411, + "narHash": "sha256-f0SBJz1eZ2yOuKUr5CA9BHULGXVSn6miBuUWdTyhUhU=", "owner": "NixOS", "repo": "nixpkgs", - "rev": "8afc4e543663ca0a6a4f496262cd05233737e732", + "rev": "b69de56fac8c2b6f8fd27f2eca01dcda8e0a4221", "type": "github" }, "original": { @@ -49,6 +38,7 @@ }, "root": { "inputs": { + "naersk": "naersk", "nixpkgs": "nixpkgs", "rust-overlay": "rust-overlay", "utils": "utils" @@ -56,15 +46,16 @@ }, "rust-overlay": { "inputs": { - "flake-utils": "flake-utils", - "nixpkgs": "nixpkgs_2" + "nixpkgs": [ + "nixpkgs" + ] }, "locked": { - "lastModified": 1644287662, - "narHash": "sha256-KkdR8VSpsOXb2ZlOeBrt5sF5a9dfAPW5KH0zrInoVl0=", + "lastModified": 1728700003, + "narHash": "sha256-Ox1pvEHxLK6lAdaKQW21Zvk65SPDag+cD8YA444R/og=", "owner": "oxalica", "repo": "rust-overlay", - "rev": "2eae19e246433530998cbf239d5505b7b87bc854", + "rev": "fc1e58ebabe0cef4442eedea07556ff0c9eafcfe", "type": "github" }, "original": { @@ -73,13 +64,31 @@ "type": "github" } }, + "systems": { + "locked": { + "lastModified": 1681028828, + "narHash": "sha256-Vy1rq5AaRuLzOxct8nz4T6wlgyUR7zLU309k9mBC768=", + "owner": "nix-systems", + "repo": "default", + "rev": "da67096a3b9bf56a91d16901293e51ba5b49a27e", + "type": "github" + }, + "original": { + "owner": "nix-systems", + "repo": "default", + "type": "github" + } + }, "utils": { + "inputs": { + "systems": "systems" + }, "locked": { - "lastModified": 1644229661, - "narHash": "sha256-1YdnJAsNy69bpcjuoKdOYQX0YxZBiCYZo4Twxerqv7k=", + "lastModified": 1726560853, + "narHash": "sha256-X6rJYSESBVr3hBoH0WbKE5KvhPU5bloyZ2L4K60/fPQ=", "owner": "numtide", "repo": "flake-utils", - "rev": "3cecb5b042f7f209c56ffd8371b2711a290ec797", + "rev": "c1dfcf08411b08f6b8615f7d8971a2bfa81d5e8a", "type": "github" }, "original": { diff --git a/flake.nix b/flake.nix index 1da4c16..936e777 100644 --- a/flake.nix +++ b/flake.nix @@ -1,46 +1,72 @@ { description = "Leet your code in command-line."; - inputs.nixpkgs.url = "github:NixOS/nixpkgs/nixpkgs-unstable"; - inputs.rust-overlay.url = "github:oxalica/rust-overlay"; - inputs.utils.url = "github:numtide/flake-utils"; + inputs = { + nixpkgs.url = "github:NixOS/nixpkgs/nixpkgs-unstable"; + utils.url = "github:numtide/flake-utils"; - outputs = { self, nixpkgs, rust-overlay, utils, ... }: + naersk = { + url = "github:nix-community/naersk"; + inputs.nixpkgs.follows = "nixpkgs"; + }; + + rust-overlay = { + url = "github:oxalica/rust-overlay"; + inputs.nixpkgs.follows = "nixpkgs"; + }; + }; + + outputs = { + self, + nixpkgs, + utils, + naersk, + rust-overlay, + ... + }: utils.lib.eachDefaultSystem (system: let - pkgs = import nixpkgs { inherit system; overlays = [ rust-overlay.overlay ]; }; + overlays = [ (import rust-overlay) ]; - platform = with pkgs; makeRustPlatform { - rustc = rust-bin.nightly.latest.minimal; - cargo = rust-bin.nightly.latest.minimal; + pkgs = (import nixpkgs) { + inherit system overlays; }; - package = with pkgs; platform.buildRustPackage rec { - pname = "leetcode-cli"; - version = "0.3.10"; - src = fetchCrate { - inherit pname version; - sha256 = "SkJLA49AXNTpiWZByII2saYLyN3bAAJTlCvhamlOEXA="; - }; + toolchain = pkgs.rust-bin.fromRustupToolchainFile ./rust-toolchain.toml; - cargoSha256 = "xhKF4qYOTdt8iCSPY5yT8tH3l54HdkOAIS2SBGzqsdo="; + naersk' = pkgs.callPackage naersk { + cargo = toolchain; + rustc = toolchain; + clippy = toolchain; + }; - # a nightly compiler is required unless we use this cheat code. - RUSTC_BOOTSTRAP = 0; + nativeBuildInputs = with pkgs; [ + pkg-config + ]; - # CFG_RELEASE = "${rustPlatform.rust.rustc.version}-nightly"; - CFG_RELEASE_CHANNEL = "ngihtly"; + darwinBuildInputs = pkgs.lib.optionals pkgs.stdenv.isDarwin [ + pkgs.darwin.apple_sdk.frameworks.Security + pkgs.darwin.apple_sdk.frameworks.SystemConfiguration + ]; - nativeBuildInputs = [ - pkg-config - rust-bin.stable.latest.default - ]; + buildInputs = with pkgs; [ + openssl + dbus + sqlite + ] ++ darwinBuildInputs; + + package = naersk'.buildPackage rec { + pname = "leetcode-cli"; + version = "git"; + + src = ./.; + doCheck = true; # run `cargo test` on build - buildInputs = [ - openssl - dbus - sqlite - ] ++ lib.optionals stdenv.isDarwin [ darwin.apple_sdk.frameworks.Security ]; + inherit buildInputs nativeBuildInputs; + + buildNoDefaultFeatures = true; + + buildFeatures = "git"; meta = with pkgs.lib; { description = "Leet your code in command-line."; @@ -49,11 +75,36 @@ maintainers = with maintainers; [ congee ]; mainProgram = "leetcode"; }; + + # Env vars + # a nightly compiler is required unless we use this cheat code. + RUSTC_BOOTSTRAP = 0; + + # CFG_RELEASE = "${rustPlatform.rust.rustc.version}-stable"; + CFG_RELEASE_CHANNEL = "stable"; }; in - { + { defaultPackage = package; overlay = final: prev: { leetcode-cli = package; }; + + devShell = with pkgs; mkShell { + name = "shell"; + inherit nativeBuildInputs; + + buildInputs = buildInputs ++ [ + toolchain + cargo-edit + cargo-bloat + cargo-audit + cargo-about + cargo-outdated + ]; + + PKG_CONFIG_PATH = "${pkgs.openssl.dev}/lib/pkgconfig"; + RUST_BACKTRACE = "full"; + LD_LIBRARY_PATH = lib.makeLibraryPath buildInputs; + }; } ); } diff --git a/rust-toolchain.toml b/rust-toolchain.toml new file mode 100644 index 0000000..2a19081 --- /dev/null +++ b/rust-toolchain.toml @@ -0,0 +1,10 @@ +[toolchain] +channel = "stable" +components = [ + "rustc", + "cargo", + "rustfmt", + "clippy", + "rust-analyzer", +] +profile = "minimal" diff --git a/rustfmt.toml b/rustfmt.toml index dcbebe6..79f8a99 100644 --- a/rustfmt.toml +++ b/rustfmt.toml @@ -1 +1,2 @@ -tab_spaces = 4 \ No newline at end of file +edition = "2021" +tab_spaces = 4 diff --git a/src/cache/mod.rs b/src/cache/mod.rs index 345b3ea..3250bf6 100644 --- a/src/cache/mod.rs +++ b/src/cache/mod.rs @@ -7,7 +7,8 @@ use self::models::*; use self::schemas::{problems::dsl::*, tags::dsl::*}; use self::sql::*; use crate::helper::test_cases_path; -use crate::{cfg, err::Error, plugins::LeetCode}; +use crate::{config::Config, err::Error, plugins::LeetCode}; +use anyhow::anyhow; use colored::Colorize; use diesel::prelude::*; use reqwest::Response; @@ -22,16 +23,13 @@ pub fn conn(p: String) -> SqliteConnection { /// Condition submit or test #[derive(Clone, Debug)] +#[derive(Default)] pub enum Run { Test, + #[default] Submit, } -impl Default for Run { - fn default() -> Self { - Run::Submit - } -} /// Requests if data not download #[derive(Clone)] @@ -45,19 +43,21 @@ impl Cache { /// Clean cache pub fn clean(&self) -> Result<(), Error> { - Ok(std::fs::remove_file(&self.0.conf.storage.cache()?)?) + Ok(std::fs::remove_file(self.0.conf.storage.cache()?)?) } - /// ref to download probems + /// ref to download problems pub async fn update(self) -> Result<(), Error> { self.download_problems().await?; Ok(()) } pub fn update_after_ac(self, rid: i32) -> Result<(), Error> { - let c = conn((&self.0.conf.storage.cache()?).to_owned()); + let mut c = conn(self.0.conf.storage.cache()?); let target = problems.filter(id.eq(rid)); - diesel::update(target).set(status.eq("ac")).execute(&c)?; + diesel::update(target) + .set(status.eq("ac")) + .execute(&mut c)?; Ok(()) } @@ -102,23 +102,32 @@ impl Cache { diesel::replace_into(problems) .values(&ps) - .execute(&self.conn()?)?; + .execute(&mut self.conn()?)?; Ok(ps) } /// Get problem pub fn get_problem(&self, rfid: i32) -> Result { - let p: Problem = problems.filter(fid.eq(rfid)).first(&self.conn()?)?; + let p: Problem = problems.filter(fid.eq(rfid)).first(&mut self.conn()?)?; if p.category != "algorithms" { - return Err(Error::FeatureError( - "Not support database and shell questions for now".to_string(), - )); + return Err(anyhow!("No support for database and shell questions yet").into()); } Ok(p) } + /// Get problem from name + pub fn get_problem_id_from_name(&self, problem_name: &String) -> Result { + let p: Problem = problems + .filter(name.eq(problem_name)) + .first(&mut self.conn()?)?; + if p.category != "algorithms" { + return Err(anyhow!("No support for database and shell questions yet").into()); + } + Ok(p.fid) + } + /// Get daily problem pub async fn get_daily_problem_id(&self) -> Result { parser::daily( @@ -134,13 +143,13 @@ impl Cache { /// Get problems from cache pub fn get_problems(&self) -> Result, Error> { - Ok(problems.load::(&self.conn()?)?) + Ok(problems.load::(&mut self.conn()?)?) } /// Get question #[allow(clippy::useless_let_if_seq)] pub async fn get_question(&self, rfid: i32) -> Result { - let target: Problem = problems.filter(fid.eq(rfid)).first(&self.conn()?)?; + let target: Problem = problems.filter(fid.eq(rfid)).first(&mut self.conn()?)?; let ids = match target.level { 1 => target.fid.to_string().green(), @@ -157,9 +166,7 @@ impl Cache { ); if target.category != "algorithms" { - return Err(Error::FeatureError( - "Not support database and shell questions for now".to_string(), - )); + return Err(anyhow!("No support for database and shell questions yet").into()); } let mut rdesc = Question::default(); @@ -190,7 +197,7 @@ impl Cache { let sdesc = serde_json::to_string(&rdesc)?; diesel::update(&target) .set(desc.eq(sdesc)) - .execute(&self.conn()?)?; + .execute(&mut self.conn()?)?; } Ok(rdesc) @@ -201,7 +208,7 @@ impl Cache { let ids: Vec; let rtag = tags .filter(tag.eq(rslug.to_string())) - .first::(&self.conn()?); + .first::(&mut self.conn()?); if let Ok(t) = rtag { trace!("Got {} questions from local cache...", &rslug); ids = serde_json::from_str(&t.refs)?; @@ -222,14 +229,14 @@ impl Cache { diesel::insert_into(tags) .values(&t) - .execute(&self.conn()?)?; + .execute(&mut self.conn()?)?; } Ok(ids) } pub fn get_tags(&self) -> Result, Error> { - Ok(tags.load::(&self.conn()?)?) + Ok(tags.load::(&mut self.conn()?)?) } /// run_code data @@ -239,7 +246,7 @@ impl Cache { rfid: i32, test_case: Option, ) -> Result<(HashMap<&'static str, String>, [String; 2]), Error> { - trace!("pre run code..."); + trace!("pre-run code..."); use crate::helper::code_path; use std::fs::File; use std::io::Read; @@ -307,33 +314,14 @@ impl Cache { json.insert("data_input", test_case); let url = match run { - Run::Test => conf - .sys - .urls - .get("test") - .ok_or(Error::NoneError)? - .replace("$slug", &p.slug), + Run::Test => conf.sys.urls.test(&p.slug), Run::Submit => { json.insert("judge_type", "large".to_string()); - conf.sys - .urls - .get("submit") - .ok_or(Error::NoneError)? - .replace("$slug", &p.slug) + conf.sys.urls.submit(&p.slug) } }; - Ok(( - json, - [ - url, - conf.sys - .urls - .get("problems") - .ok_or(Error::NoneError)? - .replace("$slug", &p.slug), - ], - )) + Ok((json, [url, conf.sys.urls.problem(&p.slug)])) } /// TODO: The real delay @@ -359,15 +347,20 @@ impl Cache { ) -> Result { trace!("Exec problem filter —— Test or Submit"); let (json, [url, refer]) = self.pre_run_code(run.clone(), rfid, test_case).await?; - trace!("Pre run code result {:?}, {:?}, {:?}", json, url, refer); + trace!("Pre-run code result {:#?}, {}, {}", json, url, refer); - let run_res: RunCode = self + let text = self .0 .clone() .run_code(json.clone(), url.clone(), refer.clone()) .await? - .json() // does not require LEETCODE_SESSION (very oddly) + .text() .await?; + + let run_res: RunCode = serde_json::from_str(&text).map_err(|e| { + anyhow!("JSON error: {e}, please double check your session and csrf config.") + })?; + trace!("Run code result {:#?}", run_res); // Check if leetcode accepted the Run request @@ -395,10 +388,10 @@ impl Cache { /// New cache pub fn new() -> Result { - let conf = cfg::locate()?; - let c = conn(conf.storage.cache()?); - diesel::sql_query(CREATE_PROBLEMS_IF_NOT_EXISTS).execute(&c)?; - diesel::sql_query(CREATE_TAGS_IF_NOT_EXISTS).execute(&c)?; + let conf = Config::locate()?; + let mut c = conn(conf.storage.cache()?); + diesel::sql_query(CREATE_PROBLEMS_IF_NOT_EXISTS).execute(&mut c)?; + diesel::sql_query(CREATE_TAGS_IF_NOT_EXISTS).execute(&mut c)?; Ok(Cache(LeetCode::new()?)) } diff --git a/src/cache/models.rs b/src/cache/models.rs index 0a9a6ab..9b28482 100644 --- a/src/cache/models.rs +++ b/src/cache/models.rs @@ -1,4 +1,6 @@ //! Leetcode data models +use unicode_width::UnicodeWidthStr; +use unicode_width::UnicodeWidthChar; use super::schemas::{problems, tags}; use crate::helper::HTML; use colored::Colorize; @@ -7,7 +9,7 @@ use serde_json::Number; /// Tag model #[derive(Clone, Insertable, Queryable, Serialize, Debug)] -#[table_name = "tags"] +#[diesel(table_name = tags)] pub struct Tag { pub tag: String, pub refs: String, @@ -15,7 +17,7 @@ pub struct Tag { /// Problem model #[derive(AsChangeset, Clone, Identifiable, Insertable, Queryable, Serialize, Debug)] -#[table_name = "problems"] +#[diesel(table_name = problems)] pub struct Problem { pub category: String, pub fid: i32, @@ -39,6 +41,7 @@ impl Problem { _ => "Unknown", } } + pub fn desc_comment(&self, conf: &Config) -> String { let mut res = String::new(); let comment_leading = &conf.code.comment_leading; @@ -53,7 +56,7 @@ impl Problem { static DONE: &str = " ✔"; static ETC: &str = "..."; static LOCK: &str = "🔒"; -static NDONE: &str = "✘"; +static NDONE: &str = " ✘"; static SPACE: &str = " "; impl std::fmt::Display for Problem { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { @@ -97,14 +100,27 @@ impl std::fmt::Display for Problem { } } - if self.name.len() < 60_usize { + let name_width = UnicodeWidthStr::width(self.name.as_str()); + let target_width = 60; + if name_width <= target_width { name.push_str(&self.name); - name.push_str(&SPACE.repeat(60 - &self.name.len())); + name.push_str(&SPACE.repeat(target_width - name_width)); } else { - name.push_str(&self.name[..49]); - name = name.trim_end().to_string(); - name.push_str(ETC); - name.push_str(&SPACE.repeat(60 - name.len())); + // truncate carefully to target width - 3 (because "..." will take some width) + let mut truncated = String::new(); + let mut current_width = 0; + for c in self.name.chars() { + let char_width = UnicodeWidthChar::width(c).unwrap_or(0); + if current_width + char_width > target_width - 3 { + break; + } + truncated.push(c); + current_width += char_width; + } + truncated.push_str(ETC); // add "..." + let truncated_width = UnicodeWidthStr::width(truncated.as_str()); + truncated.push_str(&SPACE.repeat(target_width - truncated_width)); + name = truncated; } level = match self.level { @@ -153,7 +169,7 @@ impl Question { let desc = self.content.render(); let mut res = desc.lines().fold("\n".to_string(), |acc, e| { - acc + " " + conf.code.comment_leading.as_str() + " " + e + "\n" + acc + "" + conf.code.comment_leading.as_str() + " " + e + "\n" }); res += " \n"; @@ -290,7 +306,7 @@ impl std::fmt::Display for VerifyResult { match &self.status.status_code { 10 => { - if self.correct_answer { + if matches!(self.result_type, Run::Test) && self.correct_answer { // Pass Tests write!( f, @@ -305,9 +321,12 @@ impl std::fmt::Display for VerifyResult { &"\nExpected:".after_spaces(6), eca, )? - } else if !self.submit.compare_result.is_empty() { + } else if matches!(self.result_type, Run::Submit) + && !self.submit.compare_result.is_empty() + { + // only Submit execute this branch // Submit Successfully - // TODO: result shoule be all 1; + // TODO: result should be all 1; // Lines below are sucks... let cache = super::Cache::new().expect("cache gen failed"); cache @@ -315,27 +334,31 @@ impl std::fmt::Display for VerifyResult { self.submit .question_id .parse() - .expect("submit succcessfully, parse question_id to i32 failed"), + .expect("submit successfully, parse question_id to i32 failed"), ) .expect("update ac to cache failed"); // prints - let (mut rp, mut mp) = (0, 0); - if let Some(n) = &self.analyse.runtime_percentile { + let rp = if let Some(n) = &self.analyse.runtime_percentile { if n.is_f64() { - rp = n.as_f64().unwrap_or(0.0) as i64; + n.as_f64().unwrap_or(0.0) as i64 } else { - rp = n.as_i64().unwrap_or(0); + n.as_i64().unwrap_or(0) } - } + } else { + 0 + }; - if let Some(n) = &self.analyse.memory_percentile { + let mp = if let Some(n) = &self.analyse.memory_percentile { if n.is_f64() { - mp = n.as_f64().unwrap_or(0.0) as i64; + n.as_f64().unwrap_or(0.0) as i64 } else { - mp = n.as_i64().unwrap_or(0); + n.as_i64().unwrap_or(0) } - } + } else { + 0 + }; + write!( f, "\n{}{}{}\ diff --git a/src/cache/parser.rs b/src/cache/parser.rs index dbbc87e..329455d 100644 --- a/src/cache/parser.rs +++ b/src/cache/parser.rs @@ -10,9 +10,17 @@ pub fn problem(problems: &mut Vec, v: Value) -> Option<()> { let total_acs = stat.get("total_acs")?.as_f64()? as f32; let total_submitted = stat.get("total_submitted")?.as_f64()? as f32; + let fid_obj = stat.get("frontend_question_id")?; + let fid = match fid_obj.as_i64() { + // Handle on leetcode-com + Some(s) => s as i32, + // Handle on leetcode-cn + None => fid_obj.as_str()?.split(' ').last()?.parse::().ok()?, + }; + problems.push(Problem { category: v.get("category_slug")?.as_str()?.to_string(), - fid: stat.get("frontend_question_id")?.as_i64()? as i32, + fid, id: stat.get("question_id")?.as_i64()? as i32, level: p.get("difficulty")?.as_object()?.get("level")?.as_i64()? as i32, locked: p.get("paid_only")?.as_bool()?, @@ -89,17 +97,20 @@ pub fn tags(v: Value) -> Option> { /// daily parser pub fn daily(v: Value) -> Option { trace!("Parse daily..."); - v.as_object()? - .get("data")? - .as_object()? - .get("activeDailyCodingChallengeQuestion")? - .as_object()? - .get("question")? - .as_object()? - .get("questionFrontendId")? - .as_str()? - .parse() - .ok() + let v_obj = v.as_object()?.get("data")?.as_object()?; + match v_obj.get("activeDailyCodingChallengeQuestion") { + // Handle on leetcode-com + Some(v) => v, + // Handle on leetcode-cn + None => v_obj.get("todayRecord")?.as_array()?.first()?, + } + .as_object()? + .get("question")? + .as_object()? + .get("questionFrontendId")? + .as_str()? + .parse() + .ok() } /// user parser diff --git a/src/cfg.rs b/src/cfg.rs deleted file mode 100644 index 184e3e0..0000000 --- a/src/cfg.rs +++ /dev/null @@ -1,239 +0,0 @@ -//! Soft-link with `config.toml` -//! -//! leetcode-cli will generate a `leetcode.toml` by default, -//! if you wanna change to it, you can: -//! -//! + Edit leetcode.toml at `~/.leetcode/leetcode.toml` directly -//! + Use `leetcode config` to update it -use crate::Error; -use serde::{Deserialize, Serialize}; -use std::{collections::HashMap, fs, path::PathBuf}; - -pub const DEFAULT_CONFIG: &str = r##" -# usually you don't wanna change those -[sys] -categories = [ - "algorithms", - "concurrency", - "database", - "shell" -] - -langs = [ - "bash", - "c", - "cpp", - "csharp", - "golang", - "java", - "javascript", - "kotlin", - "mysql", - "php", - "python", - "python3", - "ruby", - "rust", - "scala", - "swift" -] - -[sys.urls] -base = "https://leetcode.com" -graphql = "https://leetcode.com/graphql" -login = "https://leetcode.com/accounts/login/" -problems = "https://leetcode.com/api/problems/$category/" -problem = "https://leetcode.com/problems/$slug/description/" -tag = "https://leetcode.com/tag/$slug/" -test = "https://leetcode.com/problems/$slug/interpret_solution/" -session = "https://leetcode.com/session/" -submit = "https://leetcode.com/problems/$slug/submit/" -submissions = "https://leetcode.com/api/submissions/$slug" -submission = "https://leetcode.com/submissions/detail/$id/" -verify = "https://leetcode.com/submissions/detail/$id/check/" -favorites = "https://leetcode.com/list/api/questions" -favorite_delete = "https://leetcode.com/list/api/questions/$hash/$id" - -[code] -editor = "vim" -lang = "rust" -edit_code_marker = false -comment_problem_desc = false -comment_leading = "///" -start_marker = "@lc code=start" -end_marker = "@lc code=start" -test = true -pick = "${fid}.${slug}" -submission = "${fid}.${slug}.${sid}.${ac}" - -[cookies] -csrf = "" -session = "" - -[storage] -root = "~/.leetcode" -scripts = "scripts" -code = "code" -# absolutely path for the cache, other use root as parent dir -cache = "~/.cache/leetcode" -"##; - -/// Sync with `~/.leetcode/config.toml` -#[derive(Clone, Debug, Deserialize, Serialize)] -pub struct Config { - pub sys: Sys, - pub code: Code, - pub cookies: Cookies, - pub storage: Storage, -} - -impl Config { - /// Sync new config to config.toml - pub fn sync(&self) -> Result<(), Error> { - let home = dirs::home_dir().ok_or(Error::NoneError)?; - let conf = home.join(".leetcode/leetcode.toml"); - fs::write(conf, toml::ser::to_string_pretty(&self)?)?; - - Ok(()) - } -} - -/// Cookie settings -#[derive(Clone, Debug, Deserialize, Serialize)] -pub struct Cookies { - pub csrf: String, - pub session: String, -} - -/// System settings, for leetcode api mainly -#[derive(Clone, Debug, Deserialize, Serialize)] -pub struct Sys { - pub categories: Vec, - pub langs: Vec, - pub urls: HashMap, -} - -/// Leetcode API -#[derive(Clone, Debug, Deserialize, Serialize)] -pub struct Urls { - pub base: String, - pub graphql: String, - pub login: String, - pub problems: String, - pub problem: String, - pub test: String, - pub session: String, - pub submit: String, - pub submissions: String, - pub submission: String, - pub verify: String, - pub favorites: String, - pub favorite_delete: String, -} - -/// default editor and langs -/// -/// + support editor: [emacs, vim] -/// + support langs: all in config -#[derive(Clone, Debug, Deserialize, Serialize)] -pub struct Code { - pub editor: String, - #[serde(rename(serialize = "editor-args", deserialize = "editor-args"))] - pub editor_args: Option>, - pub edit_code_marker: bool, - pub start_marker: String, - pub end_marker: String, - pub comment_problem_desc: bool, - pub comment_leading: String, - pub test: bool, - pub lang: String, - pub pick: String, - pub submission: String, -} - -/// Locate code files -/// -/// + cache -> the path to cache -#[derive(Clone, Debug, Deserialize, Serialize)] -pub struct Storage { - cache: String, - code: String, - root: String, - scripts: Option, -} - -impl Storage { - /// convert root path - pub fn root(&self) -> Result { - let home = dirs::home_dir() - .ok_or(Error::NoneError)? - .to_string_lossy() - .to_string(); - let path = self.root.replace('~', &home); - Ok(path) - } - - /// get cache path - pub fn cache(&self) -> Result { - let home = dirs::home_dir() - .ok_or(Error::NoneError)? - .to_string_lossy() - .to_string(); - let path = PathBuf::from(self.cache.replace('~', &home)); - if !path.is_dir() { - info!("Generate cache dir at {:?}.", &path); - fs::DirBuilder::new().recursive(true).create(&path)?; - } - - Ok(path.join("Problems").to_string_lossy().to_string()) - } - - /// get code path - pub fn code(&self) -> Result { - let root = &self.root()?; - let p = PathBuf::from(root).join(&self.code); - if !PathBuf::from(&p).exists() { - fs::create_dir(&p)? - } - - Ok(p.to_string_lossy().to_string()) - } - - /// get scripts path - pub fn scripts(mut self) -> Result { - let root = &self.root()?; - if self.scripts.is_none() { - let tmp = toml::from_str::(DEFAULT_CONFIG)?; - self.scripts = Some(tmp.storage.scripts.ok_or(Error::NoneError)?); - } - - let p = PathBuf::from(root).join(&self.scripts.ok_or(Error::NoneError)?); - if !PathBuf::from(&p).exists() { - std::fs::create_dir(&p)? - } - - Ok(p.to_string_lossy().to_string()) - } -} - -/// Locate lc's config file -pub fn locate() -> Result { - let conf = root()?.join("leetcode.toml"); - if !conf.is_file() { - fs::write(&conf, &DEFAULT_CONFIG[1..])?; - } - - let s = fs::read_to_string(&conf)?; - Ok(toml::from_str::(&s)?) -} - -/// Get root path of leetcode-cli -pub fn root() -> Result { - let dir = dirs::home_dir().ok_or(Error::NoneError)?.join(".leetcode"); - if !dir.is_dir() { - info!("Generate root dir at {:?}.", &dir); - fs::DirBuilder::new().recursive(true).create(&dir)?; - } - - Ok(dir) -} diff --git a/src/cli.rs b/src/cli.rs index 3ec98bf..63107a2 100644 --- a/src/cli.rs +++ b/src/cli.rs @@ -1,13 +1,14 @@ //! Clap Commanders use crate::{ cmds::{ - Command, DataCommand, EditCommand, ExecCommand, ListCommand, PickCommand, StatCommand, - TestCommand, + completion_handler, Command, CompletionCommand, DataCommand, EditCommand, ExecCommand, + ListCommand, PickCommand, StatCommand, TestCommand, }, err::Error, flag::{Debug, Flag}, }; -use clap::{crate_name, crate_version}; +use clap::crate_version; +use log::LevelFilter; /// This should be called before calling any cli method or printing any output. pub fn reset_signal_pipe_handler() { @@ -22,10 +23,11 @@ pub fn reset_signal_pipe_handler() { } } -/// Get maches +/// Get matches pub async fn main() -> Result<(), Error> { reset_signal_pipe_handler(); - let m = clap::Command::new(crate_name!()) + + let mut cmd = clap::Command::new("leetcode") .version(crate_version!()) .about("May the Code be with You 👻") .subcommands(vec![ @@ -36,15 +38,18 @@ pub async fn main() -> Result<(), Error> { PickCommand::usage().display_order(5), StatCommand::usage().display_order(6), TestCommand::usage().display_order(7), + CompletionCommand::usage().display_order(8), ]) .arg(Debug::usage()) - .arg_required_else_help(true) - .get_matches(); + .arg_required_else_help(true); + + let m = cmd.clone().get_matches(); - if m.contains_id("debug") { + if m.get_flag("debug") { Debug::handler()?; } else { - env_logger::Builder::from_env(env_logger::Env::new().default_filter_or("info")) + env_logger::Builder::new() + .filter_level(LevelFilter::Info) .format_timestamp(None) .init(); } @@ -57,6 +62,7 @@ pub async fn main() -> Result<(), Error> { Some(("pick", sub_m)) => Ok(PickCommand::handler(sub_m).await?), Some(("stat", sub_m)) => Ok(StatCommand::handler(sub_m).await?), Some(("test", sub_m)) => Ok(TestCommand::handler(sub_m).await?), + Some(("completions", sub_m)) => Ok(completion_handler(sub_m, &mut cmd)?), _ => Err(Error::MatchError), } } diff --git a/src/cmds/completions.rs b/src/cmds/completions.rs new file mode 100644 index 0000000..bc675de --- /dev/null +++ b/src/cmds/completions.rs @@ -0,0 +1,59 @@ +//! Completions command + +use super::Command; +use crate::err::Error; +use async_trait::async_trait; +use clap::{Arg, ArgAction, ArgMatches, Command as ClapCommand}; +use clap_complete::{generate, Generator, Shell}; + +/// Abstract shell completions command +/// +/// ```sh +/// Generate shell Completions + +/// USAGE: +/// leetcode completions + +/// ARGUMENTS: +/// [possible values: bash, elvish, fish, powershell, zsh] +/// ``` +pub struct CompletionCommand; + +#[async_trait] +impl Command for CompletionCommand { + /// `pick` usage + fn usage() -> ClapCommand { + ClapCommand::new("completions") + .about("Generate shell Completions") + .visible_alias("c") + .arg( + Arg::new("shell") + .action(ArgAction::Set) + .value_parser(clap::value_parser!(Shell)), + ) + } + + async fn handler(_m: &ArgMatches) -> Result<(), Error> { + // defining custom handler to print the completions. Handler method signature limits taking + // other params. We need &ArgMatches and &mut ClapCommand to generate completions. + println!("Don't use this handler. Does not implement the functionality to print completions. Use completions_handler() below."); + Ok(()) + } +} + +fn get_completions_string(gen: G, cmd: &mut ClapCommand) -> Result { + let mut v: Vec = Vec::new(); + let name = cmd.get_name().to_string(); + generate(gen, cmd, name, &mut v); + Ok(String::from_utf8(v)?) +} + +pub fn completion_handler(m: &ArgMatches, cmd: &mut ClapCommand) -> Result<(), Error> { + let shell = *m.get_one::("shell").unwrap_or( + // if shell value is not provided try to get from the environment + &Shell::from_env().ok_or(Error::MatchError)?, + ); + let completions = get_completions_string(shell, cmd)?; + println!("{}", completions); + Ok(()) +} diff --git a/src/cmds/data.rs b/src/cmds/data.rs index c590458..cb66e39 100644 --- a/src/cmds/data.rs +++ b/src/cmds/data.rs @@ -2,7 +2,7 @@ use super::Command; use crate::{cache::Cache, helper::Digit, Error}; use async_trait::async_trait; -use clap::{Arg, ArgMatches, Command as ClapCommand}; +use clap::{Arg, ArgAction, ArgMatches, Command as ClapCommand}; use colored::Colorize; /// Abstract `data` command @@ -25,23 +25,25 @@ pub struct DataCommand; #[async_trait] impl Command for DataCommand { /// `data` command usage - fn usage<'a>() -> ClapCommand<'a> { + fn usage() -> ClapCommand { ClapCommand::new("data") .about("Manage Cache") .visible_alias("d") .arg( - Arg::with_name("delete") + Arg::new("delete") .display_order(1) .short('d') .long("delete") - .help("Delete cache"), + .help("Delete cache") + .action(ArgAction::SetTrue), ) .arg( - Arg::with_name("update") + Arg::new("update") .display_order(2) .short('u') .long("update") - .help("Update cache"), + .help("Update cache") + .action(ArgAction::SetTrue), ) } @@ -73,13 +75,13 @@ impl Command for DataCommand { title.push_str(&"-".repeat(65)); let mut flags = 0; - if m.contains_id("delete") { + if m.get_flag("delete") { flags += 1; cache.clean()?; println!("{}", "ok!".bright_green()); } - if m.contains_id("update") { + if m.get_flag("update") { flags += 1; cache.update().await?; println!("{}", "ok!".bright_green()); diff --git a/src/cmds/edit.rs b/src/cmds/edit.rs index a6ec981..f611d6c 100644 --- a/src/cmds/edit.rs +++ b/src/cmds/edit.rs @@ -1,8 +1,10 @@ //! Edit command use super::Command; -use crate::Error; +use crate::{Error, Result}; +use anyhow::anyhow; use async_trait::async_trait; -use clap::{Arg, ArgMatches, Command as ClapCommand}; +use clap::{Arg, ArgAction, ArgGroup, ArgMatches, Command as ClapCommand}; +use std::collections::HashMap; /// Abstract `edit` command /// @@ -25,34 +27,60 @@ pub struct EditCommand; #[async_trait] impl Command for EditCommand { /// `edit` usage - fn usage<'a>() -> ClapCommand<'a> { + fn usage() -> ClapCommand { ClapCommand::new("edit") - .about("Edit question by id") + .about("Edit question") .visible_alias("e") .arg( - Arg::with_name("lang") + Arg::new("lang") .short('l') .long("lang") - .takes_value(true) + .num_args(1) .help("Edit with specific language"), ) .arg( - Arg::with_name("id") - .takes_value(true) - .required(true) + Arg::new("id") + .num_args(1) + .value_parser(clap::value_parser!(i32)) .help("question id"), ) + .arg( + Arg::new("daily") + .short('d') + .long("daily") + .help("Edit today's daily challenge") + .action(ArgAction::SetTrue), + ) + .group( + ArgGroup::new("question-id") + .args(["id", "daily"]) + .multiple(false) + .required(true), + ) } /// `edit` handler - async fn handler(m: &ArgMatches) -> Result<(), crate::Error> { + async fn handler(m: &ArgMatches) -> Result<()> { use crate::{cache::models::Question, Cache}; use std::fs::File; use std::io::Write; use std::path::Path; - let id: i32 = m.value_of("id").ok_or(Error::NoneError)?.parse()?; let cache = Cache::new()?; + + let daily = m.get_one::("daily").unwrap_or(&false); + let daily_id = if *daily { + Some(cache.get_daily_problem_id().await?) + } else { + None + }; + + let id = m + .get_one::("id") + .copied() + .or(daily_id) + .ok_or(Error::NoneError)?; + let problem = cache.get_problem(id)?; let mut conf = cache.to_owned().0.conf; @@ -61,7 +89,10 @@ impl Command for EditCommand { let p_desc_comment = problem.desc_comment(&conf); // condition language if m.contains_id("lang") { - conf.code.lang = m.value_of("lang").ok_or(Error::NoneError)?.to_string(); + conf.code.lang = m + .get_one::("lang") + .ok_or(Error::NoneError)? + .to_string(); conf.sync()?; } @@ -89,6 +120,11 @@ impl Command for EditCommand { file_code.write_all(p_desc_comment.as_bytes())?; file_code.write_all(question_desc.as_bytes())?; } + if let Some(inject_before) = &conf.code.inject_before { + for line in inject_before { + file_code.write_all((line.to_string() + "\n").as_bytes())?; + } + } if conf.code.edit_code_marker { file_code.write_all( (conf.code.comment_leading.clone() @@ -108,6 +144,11 @@ impl Command for EditCommand { .as_bytes(), )?; } + if let Some(inject_after) = &conf.code.inject_after { + for line in inject_after { + file_code.write_all((line.to_string() + "\n").as_bytes())?; + } + } if test_flag { let mut file_tests = File::create(&test_path)?; @@ -119,13 +160,14 @@ impl Command for EditCommand { // if language is not found in the list of supported languges clean up files if !flag { std::fs::remove_file(&path)?; + if test_flag { std::fs::remove_file(&test_path)?; } - return Err(crate::Error::FeatureError(format!( - "This question doesn't support {}, please try another", - &lang - ))); + + return Err( + anyhow!("This question doesn't support {lang}, please try another").into(), + ); } } @@ -136,7 +178,7 @@ impl Command for EditCommand { // ```toml // [code] // editor = "emacsclient" - // editor-args = [ "-n", "-s", "doom" ] + // editor_args = [ "-n", "-s", "doom" ] // ``` // // ```rust @@ -147,8 +189,36 @@ impl Command for EditCommand { args.extend_from_slice(&editor_args); } + // Set environment variables for editor + // + // for example: + // + // ```toml + // [code] + // editor = "nvim" + // editor_envs = [ "XDG_DATA_HOME=...", "XDG_CONFIG_HOME=...", "XDG_STATE_HOME=..." ] + // ``` + // + // ```rust + // Command::new("nvim").envs(&[ ("XDG_DATA_HOME", "..."), ("XDG_CONFIG_HOME", "..."), ("XDG_STATE_HOME", "..."), ]); + // ``` + let mut envs: HashMap = Default::default(); + if let Some(editor_envs) = &conf.code.editor_envs { + for env in editor_envs.iter() { + let parts: Vec<&str> = env.split('=').collect(); + if parts.len() == 2 { + let name = parts[0].trim(); + let value = parts[1].trim(); + envs.insert(name.to_string(), value.to_string()); + } else { + return Err(anyhow!("Invalid editor environment variable: {env}").into()); + } + } + } + args.push(path); std::process::Command::new(conf.code.editor) + .envs(envs) .args(args) .status()?; Ok(()) diff --git a/src/cmds/exec.rs b/src/cmds/exec.rs index 85b6d1a..d6538f8 100644 --- a/src/cmds/exec.rs +++ b/src/cmds/exec.rs @@ -1,8 +1,8 @@ //! Exec command use super::Command; -use crate::Error; +use crate::{Error, Result}; use async_trait::async_trait; -use clap::{Arg, ArgMatches, Command as ClapCommand}; +use clap::{Arg, ArgAction, ArgGroup, ArgMatches, Command as ClapCommand}; /// Abstract Exec Command /// @@ -25,24 +25,51 @@ pub struct ExecCommand; #[async_trait] impl Command for ExecCommand { /// `exec` usage - fn usage<'a>() -> ClapCommand<'a> { + fn usage() -> ClapCommand { ClapCommand::new("exec") .about("Submit solution") .visible_alias("x") .arg( - Arg::with_name("id") - .takes_value(true) + Arg::new("id") + .num_args(1) .required(true) + .value_parser(clap::value_parser!(i32)) .help("question id"), ) + .arg( + Arg::new("daily") + .short('d') + .long("daily") + .help("Exec today's daily challenge") + .action(ArgAction::SetTrue), + ) + .group( + ArgGroup::new("question-id") + .args(["id", "daily"]) + .multiple(false) + .required(true), + ) } /// `exec` handler - async fn handler(m: &ArgMatches) -> Result<(), crate::Error> { + async fn handler(m: &ArgMatches) -> Result<()> { use crate::cache::{Cache, Run}; - let id: i32 = m.value_of("id").ok_or(Error::NoneError)?.parse()?; let cache = Cache::new()?; + + let daily = m.get_one::("daily").unwrap_or(&false); + let daily_id = if *daily { + Some(cache.get_daily_problem_id().await?) + } else { + None + }; + + let id = m + .get_one::("id") + .copied() + .or(daily_id) + .ok_or(Error::NoneError)?; + let res = cache.exec_problem(id, Run::Submit, None).await?; println!("{}", res); diff --git a/src/cmds/list.rs b/src/cmds/list.rs index 0f39329..ae6df47 100644 --- a/src/cmds/list.rs +++ b/src/cmds/list.rs @@ -13,9 +13,9 @@ //! -V, --version Prints version information //! //! OPTIONS: -//! -c, --category Fliter problems by category name +//! -c, --category Filter problems by category name //! [algorithms, database, shell, concurrency] -//! -q, --query Fliter questions by conditions: +//! -q, --query Filter questions by conditions: //! Uppercase means negative //! e = easy E = m+h //! m = medium M = e+h @@ -36,21 +36,21 @@ use super::Command; use crate::{cache::Cache, err::Error, helper::Digit}; use async_trait::async_trait; -use clap::{Arg, ArgMatches, Command as ClapCommand}; +use clap::{Arg, ArgAction, ArgMatches, Command as ClapCommand}; /// Abstract `list` command /// /// ## handler /// + try to request cache /// + prints the list -/// + if chache doesn't exist, download problems list +/// + if cache doesn't exist, download problems list /// + ... pub struct ListCommand; -static CATEGORY_HELP: &str = r#"Fliter problems by category name +static CATEGORY_HELP: &str = r#"Filter problems by category name [algorithms, database, shell, concurrency] "#; -static QUERY_HELP: &str = r#"Fliter questions by conditions: +static QUERY_HELP: &str = r#"Filter questions by conditions: Uppercase means negative e = easy E = m+h m = medium M = e+h @@ -61,7 +61,7 @@ s = starred S = not starred"#; static LIST_AFTER_HELP: &str = r#"EXAMPLES: leetcode list List all questions - leetcode list array List questions that has "array" in name + leetcode list array List questions that has "array" in name, and this is letter non-sensitive leetcode list -c database List questions that in database category leetcode list -q eD List questions that with easy level and not done leetcode list -t linked-list List questions that under tag "linked-list" @@ -72,56 +72,57 @@ static LIST_AFTER_HELP: &str = r#"EXAMPLES: #[async_trait] impl Command for ListCommand { /// `list` command usage - fn usage<'a>() -> ClapCommand<'a> { + fn usage() -> ClapCommand { ClapCommand::new("list") .about("List problems") .visible_alias("l") .arg( - Arg::with_name("category") + Arg::new("category") .short('c') .long("category") - .takes_value(true) + .num_args(1) .help(CATEGORY_HELP), ) .arg( - Arg::with_name("plan") + Arg::new("plan") .short('p') .long("plan") - .takes_value(true) + .num_args(1) .help("Invoking python scripts to filter questions"), ) .arg( - Arg::with_name("query") + Arg::new("query") .short('q') .long("query") - .takes_value(true) + .num_args(1) .help(QUERY_HELP), ) .arg( - Arg::with_name("range") + Arg::new("range") .short('r') .long("range") - .takes_value(true) - .min_values(2) + .num_args(2..) + .value_parser(clap::value_parser!(i32)) .help("Filter questions by id range"), ) .after_help(LIST_AFTER_HELP) .arg( - Arg::with_name("stat") + Arg::new("stat") .short('s') .long("stat") - .help("Show statistics of listed problems"), + .help("Show statistics of listed problems") + .action(ArgAction::SetTrue), ) .arg( - Arg::with_name("tag") + Arg::new("tag") .short('t') .long("tag") - .takes_value(true) + .num_args(1) .help("Filter questions by tag"), ) .arg( - Arg::with_name("keyword") - .takes_value(true) + Arg::new("keyword") + .num_args(1) .help("Keyword in select query"), ) } @@ -148,7 +149,7 @@ impl Command for ListCommand { #[cfg(feature = "pym")] { if m.contains_id("plan") { - let ids = crate::pym::exec(m.value_of("plan").unwrap_or(""))?; + let ids = crate::pym::exec(m.get_one::("plan").unwrap_or(&"".to_string()))?; crate::helper::squash(&mut ps, ids)?; } } @@ -156,39 +157,48 @@ impl Command for ListCommand { // filter tag if m.contains_id("tag") { let ids = cache - .get_tagged_questions(m.value_of("tag").unwrap_or("")) + .get_tagged_questions(m.get_one::("tag").unwrap_or(&"".to_string())) .await?; crate::helper::squash(&mut ps, ids)?; } // filter category if m.contains_id("category") { - ps.retain(|x| x.category == m.value_of("category").unwrap_or("algorithms")); + ps.retain(|x| { + x.category + == *m + .get_one::("category") + .unwrap_or(&"algorithms".to_string()) + }); } // filter query if m.contains_id("query") { - let query = m.value_of("query").ok_or(Error::NoneError)?; + let query = m.get_one::("query").ok_or(Error::NoneError)?; crate::helper::filter(&mut ps, query.to_string()); } // filter range if m.contains_id("range") { let num_range: Vec = m - .values_of("range") + .get_many::("range") .ok_or(Error::NoneError)? - .into_iter() - .map(|x| x.parse::().unwrap_or(0)) + .copied() .collect(); ps.retain(|x| num_range[0] <= x.fid && x.fid <= num_range[1]); } // retain if keyword exists - if let Some(keyword) = m.value_of("keyword") { + if let Some(keyword) = m.get_one::("keyword") { let lowercase_kw = keyword.to_lowercase(); ps.retain(|x| x.name.to_lowercase().contains(&lowercase_kw)); } + // output problem lines sorted by [problem number] like + // [ 1 ] Two Sum + // [ 2 ] Add Two Numbers + ps.sort_unstable_by_key(|p| p.fid); + let out: Vec = ps.iter().map(ToString::to_string).collect(); println!("{}", out.join("\n")); diff --git a/src/cmds/mod.rs b/src/cmds/mod.rs index 58a9dc3..1124532 100644 --- a/src/cmds/mod.rs +++ b/src/cmds/mod.rs @@ -18,12 +18,13 @@ use clap::{ArgMatches, Command as ClapCommand}; #[async_trait] pub trait Command { /// Usage of the specific command - fn usage<'a>() -> ClapCommand<'a>; + fn usage() -> ClapCommand; /// The handler will deal [args, options,...] from the command-line async fn handler(m: &ArgMatches) -> Result<(), Error>; } +mod completions; mod data; mod edit; mod exec; @@ -31,6 +32,7 @@ mod list; mod pick; mod stat; mod test; +pub use completions::{completion_handler, CompletionCommand}; pub use data::DataCommand; pub use edit::EditCommand; pub use exec::ExecCommand; diff --git a/src/cmds/pick.rs b/src/cmds/pick.rs index cba7e5d..aba8166 100644 --- a/src/cmds/pick.rs +++ b/src/cmds/pick.rs @@ -1,8 +1,9 @@ //! Pick command use super::Command; +use crate::cache::models::Problem; use crate::err::Error; use async_trait::async_trait; -use clap::{Arg, ArgMatches, Command as ClapCommand}; +use clap::{Arg, ArgAction, ArgMatches, Command as ClapCommand}; /// Abstract pick command /// /// ```sh @@ -17,7 +18,7 @@ use clap::{Arg, ArgMatches, Command as ClapCommand}; /// -V, --version Prints version information /// /// OPTIONS: -/// -q, --query Fliter questions by conditions: +/// -q, --query Filter questions by conditions: /// Uppercase means negative /// e = easy E = m+h /// m = medium M = e+h @@ -31,7 +32,7 @@ use clap::{Arg, ArgMatches, Command as ClapCommand}; /// ``` pub struct PickCommand; -static QUERY_HELP: &str = r#"Fliter questions by conditions: +static QUERY_HELP: &str = r#"Filter questions by conditions: Uppercase means negative e = easy E = m+h m = medium M = e+h @@ -43,38 +44,51 @@ s = starred S = not starred"#; #[async_trait] impl Command for PickCommand { /// `pick` usage - fn usage<'a>() -> ClapCommand<'a> { + fn usage() -> ClapCommand { ClapCommand::new("pick") .about("Pick a problem") .visible_alias("p") - .arg(Arg::with_name("id").help("Problem id").takes_value(true)) .arg( - Arg::with_name("plan") + Arg::new("name") + .short('n') + .long("name") + .value_parser(clap::value_parser!(String)) + .help("Problem name") + .num_args(1), + ) + .arg( + Arg::new("id") + .value_parser(clap::value_parser!(i32)) + .help("Problem id") + .num_args(1), + ) + .arg( + Arg::new("plan") .short('p') .long("plan") - .takes_value(true) + .num_args(1) .help("Invoking python scripts to filter questions"), ) .arg( - Arg::with_name("query") + Arg::new("query") .short('q') .long("query") - .takes_value(true) + .num_args(1) .help(QUERY_HELP), ) .arg( - Arg::with_name("tag") + Arg::new("tag") .short('t') .long("tag") - .takes_value(true) + .num_args(1) .help("Filter questions by tag"), ) .arg( - Arg::with_name("daily") + Arg::new("daily") .short('d') .long("daily") - .takes_value(false) - .help("Pick today's daily challenge"), + .help("Pick today's daily challenge") + .action(ArgAction::SetTrue), ) } @@ -96,7 +110,7 @@ impl Command for PickCommand { #[cfg(feature = "pym")] { if m.contains_id("plan") { - let ids = crate::pym::exec(m.value_of("plan").unwrap_or(""))?; + let ids = crate::pym::exec(m.get_one::("plan").unwrap_or(&"".to_string()))?; crate::helper::squash(&mut problems, ids)?; } } @@ -105,32 +119,47 @@ impl Command for PickCommand { if m.contains_id("tag") { let ids = cache .clone() - .get_tagged_questions(m.value_of("tag").unwrap_or("")) + .get_tagged_questions(m.get_one::("tag").unwrap_or(&"".to_string())) .await?; crate::helper::squash(&mut problems, ids)?; } // query filter if m.contains_id("query") { - let query = m.value_of("query").ok_or(Error::NoneError)?; + let query = m.get_one::("query").ok_or(Error::NoneError)?; crate::helper::filter(&mut problems, query.to_string()); } - let daily_id = if m.contains_id("daily") { + let daily = m.get_one::("daily").unwrap_or(&false); + let daily_id = if *daily { Some(cache.get_daily_problem_id().await?) } else { None }; - let fid = m - .value_of("id") - .and_then(|id| id.parse::().ok()) - .or(daily_id) - .unwrap_or_else(|| { - // Pick random without specify id - let problem = &problems[rand::thread_rng().gen_range(0..problems.len())]; - problem.fid - }); + let fid = match m.contains_id("name") { + // check for name specified, or closest name + true => { + match m.get_one::("name") { + Some(quesname) => closest_named_problem(&problems, quesname).unwrap_or(1), + None => { + // Pick random without specify id + let problem = &problems[rand::rng().random_range(0..problems.len())]; + problem.fid + } + } + } + false => { + m.get_one::("id") + .copied() + .or(daily_id) + .unwrap_or_else(|| { + // Pick random without specify id + let problem = &problems[rand::rng().random_range(0..problems.len())]; + problem.fid + }) + } + }; let r = cache.get_question(fid).await; @@ -138,7 +167,7 @@ impl Command for PickCommand { Ok(q) => println!("{}", q.desc()), Err(e) => { eprintln!("{:?}", e); - if let Error::NetworkError(_) = e { + if let Error::Reqwest(_) = e { Self::handler(m).await?; } } @@ -147,3 +176,69 @@ impl Command for PickCommand { Ok(()) } } + +// Returns the closest problem according to a scoring algorithm +// taking into account both the longest common subsequence and the size +// problem string (to compensate for smaller strings having smaller lcs). +// Returns None if there are no problems in the problem list +fn closest_named_problem(problems: &Vec, lookup_name: &str) -> Option { + let max_name_size: usize = problems.iter().map(|p| p.name.len()).max()?; + // Init table to the max name length of all the problems to share + // the same table allocation + let mut table: Vec = vec![0; (max_name_size + 1) * (lookup_name.len() + 1)]; + + // this is guaranteed because of the earlier max None propegation + assert!(!problems.is_empty()); + let mut max_score = 0; + let mut current_problem = &problems[0]; + for problem in problems { + // In case bug becomes bugged, always return the matching string + if problem.name == lookup_name { + return Some(problem.fid); + } + + let this_lcs = longest_common_subsequence(&mut table, &problem.name, lookup_name); + let this_score = this_lcs * (max_name_size - problem.name.len()); + + if this_score > max_score { + max_score = this_score; + current_problem = problem; + } + } + + Some(current_problem.fid) +} + +// Longest commong subsequence DP approach O(nm) space and time. Table must be at least +// (text1.len() + 1) * (text2.len() + 1) length or greater and is mutated every call +fn longest_common_subsequence(table: &mut [usize], text1: &str, text2: &str) -> usize { + assert!(table.len() >= (text1.len() + 1) * (text2.len() + 1)); + let height: usize = text1.len() + 1; + let width: usize = text2.len() + 1; + + // initialize base cases to 0 + for i in 0..height { + table[i * width + (width - 1)] = 0; + } + for j in 0..width { + table[((height - 1) * width) + j] = 0; + } + + let mut i: usize = height - 1; + let mut j: usize; + for c0 in text1.chars().rev() { + i -= 1; + j = width - 1; + for c1 in text2.chars().rev() { + j -= 1; + if c0.to_lowercase().next() == c1.to_lowercase().next() { + table[i * width + j] = 1 + table[(i + 1) * width + j + 1]; + } else { + let a = table[(i + 1) * width + j]; + let b = table[i * width + j + 1]; + table[i * width + j] = std::cmp::max(a, b); + } + } + } + table[0] +} diff --git a/src/cmds/stat.rs b/src/cmds/stat.rs index 147392b..4b463da 100644 --- a/src/cmds/stat.rs +++ b/src/cmds/stat.rs @@ -22,7 +22,7 @@ pub struct StatCommand; #[async_trait] impl Command for StatCommand { /// `stat` usage - fn usage<'a>() -> ClapCommand<'a> { + fn usage() -> ClapCommand { ClapCommand::new("stat") .about("Show simple chart about submissions") .visible_alias("s") @@ -81,7 +81,7 @@ impl Command for StatCommand { ); // lines - for (i, l) in vec![(easy, easy_ac), (medium, medium_ac), (hard, hard_ac)] + for (i, l) in [(easy, easy_ac), (medium, medium_ac), (hard, hard_ac)] .iter() .enumerate() { diff --git a/src/cmds/test.rs b/src/cmds/test.rs index 62b4fc4..6d36920 100644 --- a/src/cmds/test.rs +++ b/src/cmds/test.rs @@ -1,8 +1,8 @@ //! Test command use super::Command; -use crate::Error; +use crate::{Error, Result}; use async_trait::async_trait; -use clap::{Arg, ArgMatches, Command as ClapCommand}; +use clap::{Arg, ArgAction, ArgGroup, ArgMatches, Command as ClapCommand}; /// Abstract Test Command /// @@ -25,34 +25,61 @@ pub struct TestCommand; #[async_trait] impl Command for TestCommand { /// `test` usage - fn usage<'a>() -> ClapCommand<'a> { + fn usage() -> ClapCommand { ClapCommand::new("test") - .about("Test question by id") + .about("Test a question") .visible_alias("t") .arg( - Arg::with_name("id") - .takes_value(true) - .required(true) + Arg::new("id") + .num_args(1) + .value_parser(clap::value_parser!(i32)) .help("question id"), ) .arg( - Arg::with_name("testcase") - .takes_value(true) + Arg::new("testcase") + .num_args(1) .required(false) .help("custom testcase"), ) + .arg( + Arg::new("daily") + .short('d') + .long("daily") + .help("Test today's daily challenge") + .action(ArgAction::SetTrue), + ) + .group( + ArgGroup::new("question-id") + .args(["id", "daily"]) + .multiple(false) + .required(true), + ) } /// `test` handler - async fn handler(m: &ArgMatches) -> Result<(), Error> { + async fn handler(m: &ArgMatches) -> Result<()> { use crate::cache::{Cache, Run}; - let id: i32 = m.value_of("id").ok_or(Error::NoneError)?.parse()?; - let testcase = m.value_of("testcase"); + + let cache = Cache::new()?; + + let daily = m.get_one::("daily").unwrap_or(&false); + let daily_id = if *daily { + Some(cache.get_daily_problem_id().await?) + } else { + None + }; + + let id = m + .get_one::("id") + .copied() + .or(daily_id) + .ok_or(Error::NoneError)?; + + let testcase = m.get_one::("testcase"); let case_str: Option = match testcase { Some(case) => Option::from(case.replace("\\n", "\n")), _ => None, }; - let cache = Cache::new()?; let res = cache.exec_problem(id, Run::Test, case_str).await?; println!("{}", res); diff --git a/src/config/code.rs b/src/config/code.rs new file mode 100644 index 0000000..b842657 --- /dev/null +++ b/src/config/code.rs @@ -0,0 +1,63 @@ +//! Code in config +use serde::{Deserialize, Serialize}; + +fn default_pick() -> String { + "${fid}.${slug}".into() +} + +fn default_submission() -> String { + "${fid}.${slug}.${sid}.${ac}".into() +} + +/// Code config +#[derive(Clone, Debug, Deserialize, Serialize)] +pub struct Code { + #[serde(default)] + pub editor: String, + #[serde(rename(serialize = "editor-args"), alias = "editor-args", default)] + pub editor_args: Option>, + #[serde(rename(serialize = "editor-envs"), alias = "editor-envs", default)] + pub editor_envs: Option>, + #[serde(default, skip_serializing)] + pub edit_code_marker: bool, + #[serde(default, skip_serializing)] + pub start_marker: String, + #[serde(default, skip_serializing)] + pub end_marker: String, + #[serde(rename(serialize = "inject_before"), alias = "inject_before", default)] + pub inject_before: Option>, + #[serde(rename(serialize = "inject_after"), alias = "inject_after", default)] + pub inject_after: Option>, + #[serde(default, skip_serializing)] + pub comment_problem_desc: bool, + #[serde(default, skip_serializing)] + pub comment_leading: String, + #[serde(default, skip_serializing)] + pub test: bool, + pub lang: String, + #[serde(default = "default_pick", skip_serializing)] + pub pick: String, + #[serde(default = "default_submission", skip_serializing)] + pub submission: String, +} + +impl Default for Code { + fn default() -> Self { + Self { + editor: "vim".into(), + editor_args: None, + editor_envs: None, + edit_code_marker: false, + start_marker: "".into(), + end_marker: "".into(), + inject_before: None, + inject_after: None, + comment_problem_desc: false, + comment_leading: "".into(), + test: true, + lang: "rust".into(), + pick: "${fid}.${slug}".into(), + submission: "${fid}.${slug}.${sid}.${ac}".into(), + } + } +} diff --git a/src/config/cookies.rs b/src/config/cookies.rs new file mode 100644 index 0000000..8492780 --- /dev/null +++ b/src/config/cookies.rs @@ -0,0 +1,88 @@ +//! Cookies in config +use serde::{Deserialize, Serialize}; +use std::{ + fmt::{self, Display}, + str::FromStr, +}; + +#[derive(Clone, Debug, Deserialize, Serialize)] +pub enum LeetcodeSite { + #[serde(rename = "leetcode.com")] + LeetcodeCom, + #[serde(rename = "leetcode.cn")] + LeetcodeCn, +} + +impl FromStr for LeetcodeSite { + type Err = String; + fn from_str(s: &str) -> Result { + match s { + "leetcode.com" => Ok(LeetcodeSite::LeetcodeCom), + "leetcode.cn" => Ok(LeetcodeSite::LeetcodeCn), + _ => Err("Invalid site key".to_string()), + } + } +} + +impl Display for LeetcodeSite { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + let s = match self { + LeetcodeSite::LeetcodeCom => "leetcode.com", + LeetcodeSite::LeetcodeCn => "leetcode.cn", + }; + + write!(f, "{s}") + } +} + +/// Cookies settings +#[derive(Clone, Debug, Deserialize, Serialize)] +pub struct Cookies { + pub csrf: String, + pub session: String, + pub site: LeetcodeSite, +} + +impl Default for Cookies { + fn default() -> Self { + Self { + csrf: "".to_string(), + session: "".to_string(), + site: LeetcodeSite::LeetcodeCom, + } + } +} + +impl Display for Cookies { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + write!( + f, + "LEETCODE_SESSION={};csrftoken={};", + self.session, self.csrf + ) + } +} + +/// Override cookies from environment variables +pub const LEETCODE_CSRF_ENV: &str = "LEETCODE_CSRF"; +pub const LEETCODE_SESSION_ENV: &str = "LEETCODE_SESSION"; +pub const LEETCODE_SITE_ENV: &str = "LEETCODE_SITE"; + +impl Cookies { + /// Load cookies from environment variables, overriding any existing values + /// if the environment variables are set. + pub fn with_env_override(mut self) -> Self { + if let Ok(csrf) = std::env::var(LEETCODE_CSRF_ENV) { + self.csrf = csrf; + } + if let Ok(session) = std::env::var(LEETCODE_SESSION_ENV) { + self.session = session; + } + if let Ok(site) = std::env::var(LEETCODE_SITE_ENV) { + if let Ok(leetcode_site) = LeetcodeSite::from_str(&site) { + self.site = leetcode_site; + } + } + self + } +} diff --git a/src/config/mod.rs b/src/config/mod.rs new file mode 100644 index 0000000..f3f5e40 --- /dev/null +++ b/src/config/mod.rs @@ -0,0 +1,89 @@ +//! Soft-link with `config.toml` +//! +//! leetcode-cli will generate a `leetcode.toml` by default, +//! if you wanna change to it, you can: +//! +//! + Edit leetcode.toml at `~/.leetcode/leetcode.toml` directly +//! + Use `leetcode config` to update it +use crate::{ + config::{code::Code, cookies::Cookies, storage::Storage, sys::Sys}, + Error, Result, +}; +use serde::{Deserialize, Serialize}; +use std::{fs, path::Path}; + +mod code; +mod cookies; +mod storage; +mod sys; + +pub use cookies::LeetcodeSite; + +/// Sync with `~/.leetcode/leetcode.toml` +#[derive(Clone, Debug, Default, Deserialize, Serialize)] +pub struct Config { + #[serde(default, skip_serializing)] + pub sys: Sys, + pub code: Code, + pub cookies: Cookies, + pub storage: Storage, +} + +impl Config { + fn write_default(p: impl AsRef) -> Result<()> { + fs::write(p.as_ref(), toml::ser::to_string_pretty(&Self::default())?)?; + + Ok(()) + } + + /// Locate lc's config file + pub fn locate() -> Result { + let conf = Self::root()?.join("leetcode.toml"); + + if !conf.is_file() { + Self::write_default(&conf)?; + } + + let s = fs::read_to_string(&conf)?; + match toml::from_str::(&s) { + Ok(mut config) => { + // Override config.cookies with environment variables + config.cookies = config.cookies.with_env_override(); + + match config.cookies.site { + cookies::LeetcodeSite::LeetcodeCom => Ok(config), + cookies::LeetcodeSite::LeetcodeCn => { + let mut config = config; + config.sys.urls = sys::Urls::new_with_leetcode_cn(); + Ok(config) + } + } + } + Err(e) => { + let tmp = Self::root()?.join("leetcode.tmp.toml"); + Self::write_default(tmp)?; + Err(e.into()) + } + } + } + + /// Get root path of leetcode-cli + pub fn root() -> Result { + let dir = dirs::home_dir().ok_or(Error::NoneError)?.join(".leetcode"); + if !dir.is_dir() { + info!("Generate root dir at {:?}.", &dir); + fs::DirBuilder::new().recursive(true).create(&dir)?; + } + + Ok(dir) + } + + /// Sync new config to config.toml + pub fn sync(&self) -> Result<()> { + let home = dirs::home_dir().ok_or(Error::NoneError)?; + let conf = home.join(".leetcode/leetcode.toml"); + fs::write(conf, toml::ser::to_string_pretty(&self)?)?; + + Ok(()) + } +} diff --git a/src/config/storage.rs b/src/config/storage.rs new file mode 100644 index 0000000..0395b1f --- /dev/null +++ b/src/config/storage.rs @@ -0,0 +1,75 @@ +//! Storage in config. +use crate::{Error, Result}; +use serde::{Deserialize, Serialize}; +use std::{fs, path::PathBuf}; + +/// Locate code files +/// +/// + cache -> the path to cache +#[derive(Clone, Debug, Deserialize, Serialize)] +pub struct Storage { + cache: String, + code: String, + root: String, + scripts: Option, +} + +impl Default for Storage { + fn default() -> Self { + Self { + cache: "Problems".into(), + code: "code".into(), + scripts: Some("scripts".into()), + root: "~/.leetcode".into(), + } + } +} + +impl Storage { + /// convert root path + pub fn root(&self) -> Result { + let home = dirs::home_dir() + .ok_or(Error::NoneError)? + .to_string_lossy() + .to_string(); + let path = self.root.replace('~', &home); + Ok(path) + } + + /// get cache path + pub fn cache(&self) -> Result { + let root = PathBuf::from(self.root()?); + if !root.exists() { + info!("Generate cache dir at {:?}.", &root); + fs::DirBuilder::new().recursive(true).create(&root)?; + } + + Ok(root.join("Problems").to_string_lossy().to_string()) + } + + /// get code path + pub fn code(&self) -> Result { + let root = &self.root()?; + let p = PathBuf::from(root).join(&self.code); + if !PathBuf::from(&p).exists() { + fs::create_dir(&p)? + } + + Ok(p.to_string_lossy().to_string()) + } + + /// get scripts path + pub fn scripts(mut self) -> Result { + let root = &self.root()?; + if self.scripts.is_none() { + self.scripts = Some("scripts".into()); + } + + let p = PathBuf::from(root).join(self.scripts.ok_or(Error::NoneError)?); + if !PathBuf::from(&p).exists() { + std::fs::create_dir(&p)? + } + + Ok(p.to_string_lossy().to_string()) + } +} diff --git a/src/config/sys.rs b/src/config/sys.rs new file mode 100644 index 0000000..02a8458 --- /dev/null +++ b/src/config/sys.rs @@ -0,0 +1,121 @@ +//! System section +//! +//! This section is a set of constants after #88 + +use serde::{Deserialize, Serialize}; + +const CATEGORIES: [&str; 4] = ["algorithms", "concurrency", "database", "shell"]; + +// TODO: find a better solution. +fn categories() -> Vec { + CATEGORIES.into_iter().map(|s| s.into()).collect() +} + +/// Leetcode API +#[derive(Clone, Debug, Serialize, Deserialize)] +pub struct Urls { + pub base: String, + pub graphql: String, + pub login: String, + pub problems: String, + pub problem: String, + pub tag: String, + pub test: String, + pub session: String, + pub submit: String, + pub submissions: String, + pub submission: String, + pub verify: String, + pub favorites: String, + pub favorite_delete: String, +} + +impl Default for Urls { + fn default() -> Self { + Self { + base: "https://leetcode.com".into(), + graphql: "https://leetcode.com/graphql".into(), + login: "https://leetcode.com/accounts/login/".into(), + problems: "https://leetcode.com/api/problems/$category/".into(), + problem: "https://leetcode.com/problems/$slug/description/".into(), + tag: "https://leetcode.com/tag/$slug/".into(), + test: "https://leetcode.com/problems/$slug/interpret_solution/".into(), + session: "https://leetcode.com/session/".into(), + submit: "https://leetcode.com/problems/$slug/submit/".into(), + submissions: "https://leetcode.com/submissions/detail/$id/".into(), + submission: "https://leetcode.com/submissions/detail/$id/".into(), + verify: "https://leetcode.com/submissions/detail/$id/check/".into(), + favorites: "https://leetcode.com/list/api/questions".into(), + favorite_delete: "https://leetcode.com/list/api/questions/$hash/$id".into(), + } + } +} + +impl Urls { + pub fn new_with_leetcode_cn() -> Self { + Self { + base: "https://leetcode.cn".into(), + graphql: "https://leetcode.cn/graphql".into(), + login: "https://leetcode.cn/accounts/login/".into(), + problems: "https://leetcode.cn/api/problems/$category/".into(), + problem: "https://leetcode.cn/problems/$slug/description/".into(), + tag: "https://leetcode.cn/tag/$slug/".into(), + test: "https://leetcode.cn/problems/$slug/interpret_solution/".into(), + session: "https://leetcode.cn/session/".into(), + submit: "https://leetcode.cn/problems/$slug/submit/".into(), + submissions: "https://leetcode.cn/submissions/detail/$id/".into(), + submission: "https://leetcode.cn/submissions/detail/$id/".into(), + verify: "https://leetcode.cn/submissions/detail/$id/check/".into(), + favorites: "https://leetcode.cn/list/api/questions".into(), + favorite_delete: "https://leetcode.cn/list/api/questions/$hash/$id".into(), + } + } + + /// problem url with specific `$slug` + pub fn problem(&self, slug: &str) -> String { + self.problem.replace("$slug", slug) + } + + /// problems url with specific `$category` + pub fn problems(&self, category: &str) -> String { + self.problems.replace("$category", category) + } + + /// submit url with specific `$slug` + pub fn submit(&self, slug: &str) -> String { + self.submit.replace("$slug", slug) + } + + /// tag url with specific `$slug` + pub fn tag(&self, slug: &str) -> String { + self.tag.replace("$slug", slug) + } + + /// test url with specific `$slug` + pub fn test(&self, slug: &str) -> String { + self.test.replace("$slug", slug) + } + + /// verify url with specific `$id` + pub fn verify(&self, id: &str) -> String { + self.verify.replace("$id", id) + } +} + +/// System settings, for leetcode api mainly +#[derive(Clone, Debug, Serialize, Deserialize)] +pub struct Sys { + #[serde(default = "categories")] + pub categories: Vec, + #[serde(default)] + pub urls: Urls, +} + +impl Default for Sys { + fn default() -> Self { + Self { + categories: CATEGORIES.into_iter().map(|s| s.into()).collect(), + urls: Default::default(), + } + } +} diff --git a/src/err.rs b/src/err.rs index 344cf1b..cf256cf 100644 --- a/src/err.rs +++ b/src/err.rs @@ -1,158 +1,91 @@ //! Errors in leetcode-cli -use crate::cfg::{root, DEFAULT_CONFIG}; use crate::cmds::{Command, DataCommand}; +use anyhow::anyhow; use colored::Colorize; -use std::fmt; -/// Error enum -#[derive(Clone)] +#[cfg(debug_assertions)] +const CONFIG: &str = "~/.leetcode/leetcode.tmp.toml"; +#[cfg(not(debug_assertions))] +const CONFIG: &str = "~/.leetcode/leetcode_tmp.toml"; + +/// Leetcode result. +pub type Result = std::result::Result; + +/// Leetcode cli errors +#[derive(thiserror::Error, Debug)] pub enum Error { + #[error("Nothing matched")] MatchError, + #[error("Download {0} failed, please try again")] DownloadError(String), - NetworkError(String), - ParseError(String), - CacheError(String), - FeatureError(String), - ScriptError(String), + #[error(transparent)] + Reqwest(#[from] reqwest::Error), + #[error(transparent)] + HeaderName(#[from] reqwest::header::InvalidHeaderName), + #[error(transparent)] + HeaderValue(#[from] reqwest::header::InvalidHeaderValue), + #[error( + "Your leetcode cookies seems expired, \ + {} \ + Either you can handwrite your `LEETCODE_SESSION` and `csrf` into `leetcode.toml`, \ + more info please checkout this: \ + https://github.com/clearloop/leetcode-cli/blob/master/README.md#cookies", + "please make sure you have logined in leetcode.com with chrome. ".yellow().bold() + )] CookieError, + #[error( + "Your leetcode account lacks a premium subscription, which the given problem requires.\n \ + If this looks like a mistake, please open a new issue at: {}", + "https://github.com/clearloop/leetcode-cli/".underline() + )] PremiumError, - DecryptError, - SilentError, + #[error(transparent)] + Utf8(#[from] std::string::FromUtf8Error), + #[error( + "json from response parse failed, please open a new issue at: {}.", + "https://github.com/clearloop/leetcode-cli/".underline() + )] NoneError, + #[error( + "Parse config file failed, \ + leetcode-cli has just generated a new leetcode.toml at {}, \ + the current one at {} seems missing some keys, Please compare \ + the new file and add the missing keys.\n", + CONFIG, + "~/.leetcode/leetcode.toml".yellow().bold().underline(), + )] + Config(#[from] toml::de::Error), + #[error("Maybe you not login on the Chrome, you can login and retry")] ChromeNotLogin, + #[error(transparent)] + ParseInt(#[from] std::num::ParseIntError), + #[error(transparent)] + Json(#[from] serde_json::Error), + #[error(transparent)] + Toml(#[from] toml::ser::Error), + #[error(transparent)] + Io(#[from] std::io::Error), + #[error(transparent)] + Anyhow(#[from] anyhow::Error), + #[error(transparent)] + Keyring(#[from] keyring::Error), + #[error(transparent)] + OpenSSL(#[from] openssl::error::ErrorStack), + #[cfg(feature = "pym")] + #[error(transparent)] + Pyo3(#[from] pyo3::PyErr), } -impl std::fmt::Debug for Error { - fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - let e = "error:".bold().red(); - match self { - Error::CacheError(s) => write!(f, "{} {}, please try again", e, s), - Error::CookieError => write!( - f, - "{} \ - Your leetcode cookies seems expired, \ - {} \ - Either you can handwrite your `LEETCODE_SESSION` and `csrf` into `leetcode.toml`, \ - more info please checkout this: \ - https://github.com/clearloop/leetcode-cli/blob/master/README.md#cookies", - e, - "please make sure you have logined in leetcode.com with chrome. " - .yellow() - .bold(), - ), - Error::PremiumError => write!( - f, - "{} \ - Your leetcode account lacks a premium subscription, which the given problem requires.\n \ - If this looks like a mistake, please open a new issue at: {}", - e, - "https://github.com/clearloop/leetcode-cli/".underline()), - Error::DownloadError(s) => write!(f, "{} Download {} failed, please try again", e, s), - Error::NetworkError(s) => write!(f, "{} {}, please try again", e, s), - Error::ParseError(s) => write!(f, "{} {}", e, s), - Error::FeatureError(s) => write!(f, "{} {}", e, s), - Error::MatchError => write!(f, "{} Nothing matches", e), - Error::DecryptError => write!(f, "{} openssl decrypt failed", e), - Error::ScriptError(s) => write!(f, "{} {}", e, s), - Error::SilentError => write!(f, ""), - Error::NoneError => write!(f, - "json from response parse failed, please open a new issue at: {}.", - "https://github.com/clearloop/leetcode-cli/".underline(), - ), - Error::ChromeNotLogin => write!(f, "maybe you not login on the Chrome, you can login and retry.") - } - } -} - -// network -impl std::convert::From for Error { - fn from(err: reqwest::Error) -> Self { - Error::NetworkError(err.to_string()) - } -} - -// nums -impl std::convert::From for Error { - fn from(err: std::num::ParseIntError) -> Self { - Error::ParseError(err.to_string()) - } -} - -// sql impl std::convert::From for Error { fn from(err: diesel::result::Error) -> Self { match err { diesel::result::Error::NotFound => { - println!("NotFound, you may update cache, and try it again\r\n"); DataCommand::usage().print_help().unwrap_or(()); - Error::SilentError + Error::Anyhow(anyhow!( + "NotFound, you may update cache, and try it again\r\n" + )) } - _ => Error::CacheError(err.to_string()), + _ => Error::Anyhow(anyhow!("{err}")), } } } - -// serde -impl std::convert::From for Error { - fn from(err: serde_json::Error) -> Self { - Error::ParseError(err.to_string()) - } -} - -// toml -impl std::convert::From for Error { - fn from(_err: toml::de::Error) -> Self { - let conf = root().unwrap().join("leetcode_tmp.toml"); - std::fs::write(&conf, &DEFAULT_CONFIG[1..]).unwrap(); - #[cfg(debug_assertions)] - let err_msg = format!( - "{}, {}{}{}{}{}{}", - _err, - "Parse config file failed, ", - "leetcode-cli has just generated a new leetcode.toml at ", - "~/.leetcode/leetcode_tmp.toml,".green().bold().underline(), - " the current one at ", - "~/.leetcode/leetcode.toml".yellow().bold().underline(), - " seems missing some keys, Please compare the new file and add the missing keys.\n", - ); - #[cfg(not(debug_assertions))] - let err_msg = format!( - "{}{}{}{}{}{}", - "Parse config file failed, ", - "leetcode-cli has just generated a new leetcode.toml at ", - "~/.leetcode/leetcode_tmp.toml,".green().bold().underline(), - " the current one at ", - "~/.leetcode/leetcode.toml".yellow().bold().underline(), - " seems missing some keys, Please compare the new file and add the missing keys.\n", - ); - Error::ParseError(err_msg) - } -} - -impl std::convert::From for Error { - fn from(err: toml::ser::Error) -> Self { - Error::ParseError(err.to_string()) - } -} - -// io -impl std::convert::From for Error { - fn from(err: std::io::Error) -> Self { - Error::CacheError(err.to_string()) - } -} - -// openssl -impl std::convert::From for Error { - fn from(_: openssl::error::ErrorStack) -> Self { - Error::DecryptError - } -} - -// pyo3 -#[cfg(feature = "pym")] -impl std::convert::From for Error { - fn from(_: pyo3::PyErr) -> Self { - Error::ScriptError("Python script went Error".to_string()) - } -} diff --git a/src/flag.rs b/src/flag.rs index 6706a06..5517153 100644 --- a/src/flag.rs +++ b/src/flag.rs @@ -7,12 +7,12 @@ //! -V, --version Prints version information //! ``` use crate::err::Error; -use clap::Arg; +use clap::{Arg, ArgAction}; use env_logger::Env; /// Abstract flag trait pub trait Flag { - fn usage<'a>() -> Arg<'a>; + fn usage() -> Arg; fn handler() -> Result<(), Error>; } @@ -20,15 +20,16 @@ pub trait Flag { pub struct Debug; impl Flag for Debug { - fn usage<'a>() -> Arg<'a> { - Arg::with_name("debug") + fn usage() -> Arg { + Arg::new("debug") .short('d') .long("debug") .help("debug mode") + .action(ArgAction::SetTrue) } fn handler() -> Result<(), Error> { - env_logger::Builder::from_env(Env::default().default_filter_or("leetcode")).init(); + env_logger::Builder::from_env(Env::default().default_filter_or("debug")).init(); Ok(()) } diff --git a/src/helper.rs b/src/helper.rs index 60efcd8..321828c 100644 --- a/src/helper.rs +++ b/src/helper.rs @@ -1,8 +1,10 @@ //! A set of helper traits -pub use self::digit::Digit; -pub use self::file::{code_path, load_script, test_cases_path}; -pub use self::filter::{filter, squash}; -pub use self::html::HTML; +pub use self::{ + digit::Digit, + file::{code_path, load_script, test_cases_path}, + filter::{filter, squash}, + html::HTML, +}; /// Convert i32 to specific digits string. mod digit { @@ -48,7 +50,7 @@ mod filter { /// Abstract query filter /// /// ```sh - /// -q, --query Fliter questions by conditions: + /// -q, --query Filter questions by conditions: /// Uppercase means negative /// e = easy E = m+h /// m = medium M = e+h @@ -78,14 +80,15 @@ mod filter { } /// Squash questions and ids - pub fn squash(ps: &mut Vec, ids: Vec) -> Result<(), crate::Error> { + pub fn squash(ps: &mut Vec, ids: Vec) -> crate::Result<()> { use std::collections::HashMap; let mut map: HashMap = HashMap::new(); ids.iter().for_each(|x| { map.insert(x.to_string(), true).unwrap_or_default(); }); - ps.retain(|x| map.get(&x.id.to_string()).is_some()); + + ps.retain(|x| map.contains_key(&x.id.to_string())); Ok(()) } } @@ -164,12 +167,13 @@ mod html { mod file { /// Convert file suffix from language type - pub fn suffix(l: &str) -> Result<&'static str, crate::Error> { + pub fn suffix(l: &str) -> crate::Result<&'static str> { match l { "bash" => Ok("sh"), "c" => Ok("c"), "cpp" => Ok("cpp"), "csharp" => Ok("cs"), + "elixir" => Ok("ex"), "golang" => Ok("go"), "java" => Ok("java"), "javascript" => Ok("js"), @@ -182,6 +186,7 @@ mod file { "rust" => Ok("rs"), "scala" => Ok("scala"), "swift" => Ok("swift"), + "typescript" => Ok("ts"), _ => Ok("c"), } } @@ -189,8 +194,8 @@ mod file { use crate::{cache::models::Problem, Error}; /// Generate test cases path by fid - pub fn test_cases_path(problem: &Problem) -> Result { - let conf = crate::cfg::locate()?; + pub fn test_cases_path(problem: &Problem) -> crate::Result { + let conf = crate::config::Config::locate()?; let mut path = format!("{}/{}.tests.dat", conf.storage.code()?, conf.code.pick); path = path.replace("${fid}", &problem.fid.to_string()); @@ -199,8 +204,8 @@ mod file { } /// Generate code path by fid - pub fn code_path(problem: &Problem, l: Option) -> Result { - let conf = crate::cfg::locate()?; + pub fn code_path(problem: &Problem, l: Option) -> crate::Result { + let conf = crate::config::Config::locate()?; let mut lang = conf.code.lang; if l.is_some() { lang = l.ok_or(Error::NoneError)?; @@ -220,10 +225,10 @@ mod file { } /// Load python scripts - pub fn load_script(module: &str) -> Result { + pub fn load_script(module: &str) -> crate::Result { use std::fs::File; use std::io::Read; - let conf = crate::cfg::locate()?; + let conf = crate::config::Config::locate()?; let mut script = "".to_string(); File::open(format!("{}/{}.py", conf.storage.scripts()?, module))? .read_to_string(&mut script)?; diff --git a/src/lib.rs b/src/lib.rs index e97fb6e..39a89e6 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -235,9 +235,9 @@ extern crate diesel; // show docs pub mod cache; -pub mod cfg; pub mod cli; pub mod cmds; +pub mod config; pub mod err; pub mod flag; pub mod helper; @@ -247,5 +247,5 @@ pub mod pym; // re-exports pub use cache::Cache; -pub use cfg::Config; -pub use err::Error; +pub use config::Config; +pub use err::{Error, Result}; diff --git a/src/plugins/chrome.rs b/src/plugins/chrome.rs index 8893999..0825300 100644 --- a/src/plugins/chrome.rs +++ b/src/plugins/chrome.rs @@ -1,8 +1,9 @@ -use crate::{cache, Error}; +use crate::{cache, Error, Result}; +use anyhow::anyhow; use diesel::prelude::*; use keyring::Entry; use openssl::{hash, pkcs5, symm}; -use std::collections::HashMap; +use std::{collections::HashMap, fmt::Display}; /// LeetCode Cookies Schema mod schema { @@ -33,15 +34,19 @@ pub struct Ident { session: String, } -impl std::string::ToString for Ident { - fn to_string(&self) -> String { - format!("LEETCODE_SESSION={};csrftoken={};", self.session, self.csrf) +impl Display for Ident { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + write!( + f, + "LEETCODE_SESSION={};csrftoken={};", + self.session, self.csrf + ) } } /// Get cookies from chrome storage -pub fn cookies() -> Result { - let ccfg = crate::cfg::locate()?.cookies; +pub fn cookies() -> Result { + let ccfg = crate::config::Config::locate()?.cookies; if !ccfg.csrf.is_empty() && !ccfg.session.is_empty() { return Ok(Ident { csrf: ccfg.csrf, @@ -61,19 +66,19 @@ pub fn cookies() -> Result { }; debug!("Chrome Cookies path is {:?}", &p); - let conn = cache::conn(p.to_string_lossy().to_string()); + let mut conn = cache::conn(p.to_string_lossy().to_string()); let res = cookies - .filter(host_key.like("%leetcode.com")) - .load::(&conn) + .filter(host_key.like(format!("#{}", ccfg.site))) + .load::(&mut conn) .expect("Loading cookies from google chrome failed."); debug!("res {:?}", &res); if res.is_empty() { - return Err(crate::Error::CookieError); + return Err(Error::CookieError); } // Get system password - let ring = Entry::new("Chrome Safe Storage", "Chrome"); + let ring = Entry::new("Chrome Safe Storage", "Chrome")?; let pass = ring.get_password().expect("Get Password failed"); // Decode cookies @@ -94,7 +99,7 @@ pub fn cookies() -> Result { } /// Decode cookies from chrome -fn decode_cookies(pass: &str, v: Vec) -> Result { +fn decode_cookies(pass: &str, v: Vec) -> Result { let mut key = [0_u8; 16]; match std::env::consts::OS { "macos" => { @@ -117,18 +122,14 @@ fn decode_cookies(pass: &str, v: Vec) -> Result { ) .expect("pbkdf2 hmac went error."); } - _ => { - return Err(crate::Error::FeatureError( - "only supports OSX or Linux for now".to_string(), - )) - } + _ => return Err(anyhow!("only supports OSX or Linux for now").into()), } chrome_decrypt(v, key) } /// Decrypt chrome cookie value with aes-128-cbc -fn chrome_decrypt(v: Vec, key: [u8; 16]) -> Result { +fn chrome_decrypt(v: Vec, key: [u8; 16]) -> Result { // : \u16 let iv = vec![32_u8; 16]; let mut decrypter = symm::Crypter::new( diff --git a/src/plugins/leetcode.rs b/src/plugins/leetcode.rs index e06627f..ec8a924 100644 --- a/src/plugins/leetcode.rs +++ b/src/plugins/leetcode.rs @@ -1,8 +1,7 @@ use self::req::{Json, Mode, Req}; use crate::{ - cfg::{self, Config}, - err::Error, - plugins::chrome, + config::{self, Config}, + Result, }; use reqwest::{ header::{HeaderMap, HeaderName, HeaderValue}, @@ -20,31 +19,32 @@ pub struct LeetCode { impl LeetCode { /// Parse reqwest headers - fn headers(mut headers: HeaderMap, ts: Vec<(&str, &str)>) -> Result { + fn headers(mut headers: HeaderMap, ts: Vec<(&str, &str)>) -> Result { for (k, v) in ts.into_iter() { - let name = HeaderName::from_str(k); - let value = HeaderValue::from_str(v); - if name.is_err() || value.is_err() { - return Err(Error::ParseError("http header parse failed".to_string())); - } - - headers.insert(name.unwrap(), value.unwrap()); + let name = HeaderName::from_str(k)?; + let value = HeaderValue::from_str(v)?; + headers.insert(name, value); } Ok(headers) } /// New LeetCode client - pub fn new() -> Result { - let conf = cfg::locate()?; - let cookies = chrome::cookies()?; + pub fn new() -> Result { + let conf = config::Config::locate()?; + let (cookie, csrf) = if conf.cookies.csrf.is_empty() || conf.cookies.session.is_empty() { + let cookies = super::chrome::cookies()?; + (cookies.to_string(), cookies.csrf) + } else { + (conf.cookies.clone().to_string(), conf.cookies.clone().csrf) + }; let default_headers = LeetCode::headers( HeaderMap::new(), vec![ - ("Cookie", cookies.to_string().as_str()), - ("x-csrftoken", &cookies.csrf), + ("Cookie", &cookie), + ("x-csrftoken", &csrf), ("x-requested-with", "XMLHttpRequest"), - ("Origin", &conf.sys.urls["base"]), + ("Origin", &conf.sys.urls.base), ], )?; @@ -53,11 +53,6 @@ impl LeetCode { .connect_timeout(Duration::from_secs(30)) .build()?; - // Sync conf - if conf.cookies.csrf != cookies.csrf { - conf.sync()?; - } - Ok(LeetCode { conf, client, @@ -66,15 +61,9 @@ impl LeetCode { } /// Get category problems - pub async fn get_category_problems(self, category: &str) -> Result { + pub async fn get_category_problems(self, category: &str) -> Result { trace!("Requesting {} problems...", &category); - let url = &self - .conf - .sys - .urls - .get("problems") - .ok_or(Error::NoneError)? - .replace("$category", category); + let url = &self.conf.sys.urls.problems(category); Req { default_headers: self.default_headers, @@ -89,31 +78,27 @@ impl LeetCode { .await } - pub async fn get_question_ids_by_tag(self, slug: &str) -> Result { + pub async fn get_question_ids_by_tag(self, slug: &str) -> Result { trace!("Requesting {} ref problems...", &slug); - let url = &self.conf.sys.urls.get("graphql").ok_or(Error::NoneError)?; + let url = &self.conf.sys.urls.graphql; let mut json: Json = HashMap::new(); json.insert("operationName", "getTopicTag".to_string()); json.insert("variables", r#"{"slug": "$slug"}"#.replace("$slug", slug)); json.insert( "query", - vec![ - "query getTopicTag($slug: String!) {", + ["query getTopicTag($slug: String!) {", " topicTag(slug: $slug) {", " questions {", " questionId", " }", " }", - "}", - ] + "}"] .join("\n"), ); Req { default_headers: self.default_headers, - refer: Some( - (self.conf.sys.urls.get("tag").ok_or(Error::NoneError)?).replace("$slug", slug), - ), + refer: Some(self.conf.sys.urls.tag(slug)), info: false, json: Some(json), mode: Mode::Post, @@ -124,9 +109,9 @@ impl LeetCode { .await } - pub async fn get_user_info(self) -> Result { + pub async fn get_user_info(self) -> Result { trace!("Requesting user info..."); - let url = &self.conf.sys.urls.get("graphql").ok_or(Error::NoneError)?; + let url = &self.conf.sys.urls.graphql; let mut json: Json = HashMap::new(); json.insert("operationName", "a".to_string()); json.insert( @@ -154,24 +139,41 @@ impl LeetCode { } /// Get daily problem - pub async fn get_question_daily(self) -> Result { + pub async fn get_question_daily(self) -> Result { trace!("Requesting daily problem..."); - let url = &self.conf.sys.urls.get("graphql").ok_or(Error::NoneError)?; + let url = &self.conf.sys.urls.graphql; let mut json: Json = HashMap::new(); - json.insert("operationName", "daily".to_string()); - json.insert( - "query", - vec![ - "query daily {", - " activeDailyCodingChallengeQuestion {", - " question {", - " questionFrontendId", - " }", - " }", - "}", - ] - .join("\n"), - ); + + match self.conf.cookies.site { + config::LeetcodeSite::LeetcodeCom => { + json.insert("operationName", "daily".to_string()); + json.insert( + "query", + ["query daily {", + " activeDailyCodingChallengeQuestion {", + " question {", + " questionFrontendId", + " }", + " }", + "}"] + .join("\n"), + ); + } + config::LeetcodeSite::LeetcodeCn => { + json.insert("operationName", "questionOfToday".to_string()); + json.insert( + "query", + ["query questionOfToday {", + " todayRecord {", + " question {", + " questionFrontendId", + " }", + " }", + "}"] + .join("\n"), + ); + } + } Req { default_headers: self.default_headers, @@ -187,20 +189,13 @@ impl LeetCode { } /// Get specific problem detail - pub async fn get_question_detail(self, slug: &str) -> Result { + pub async fn get_question_detail(self, slug: &str) -> Result { trace!("Requesting {} detail...", &slug); - let refer = self - .conf - .sys - .urls - .get("problems") - .ok_or(Error::NoneError)? - .replace("$slug", slug); + let refer = self.conf.sys.urls.problem(slug); let mut json: Json = HashMap::new(); json.insert( "query", - vec![ - "query getQuestionDetail($titleSlug: String!) {", + ["query getQuestionDetail($titleSlug: String!) {", " question(titleSlug: $titleSlug) {", " content", " stats", @@ -211,8 +206,7 @@ impl LeetCode { " metaData", " translatedContent", " }", - "}", - ] + "}"] .join("\n"), ); @@ -230,14 +224,14 @@ impl LeetCode { json: Some(json), mode: Mode::Post, name: "get_problem_detail", - url: (&self.conf.sys.urls["graphql"]).to_string(), + url: self.conf.sys.urls.graphql, } .send(&self.client) .await } /// Send code to judge - pub async fn run_code(self, j: Json, url: String, refer: String) -> Result { + pub async fn run_code(self, j: Json, url: String, refer: String) -> Result { info!("Sending code to judge..."); Req { default_headers: self.default_headers, @@ -253,15 +247,10 @@ impl LeetCode { } /// Get the result of submission / testing - pub async fn verify_result(self, id: String) -> Result { + pub async fn verify_result(self, id: String) -> Result { trace!("Verifying result..."); - let url = self - .conf - .sys - .urls - .get("verify") - .ok_or(Error::NoneError)? - .replace("$id", &id); + let url = self.conf.sys.urls.verify(&id); + Req { default_headers: self.default_headers, refer: None, diff --git a/src/plugins/mod.rs b/src/plugins/mod.rs index 5a84870..0102ca0 100644 --- a/src/plugins/mod.rs +++ b/src/plugins/mod.rs @@ -6,6 +6,8 @@ //! ## login to `leetcode.com` //! Leetcode-cli use chrome cookie directly, do not need to login, please make sure you have loggined in `leetcode.com` before usnig `leetcode-cli` //! + +// FIXME: Read cookies from local storage. (issue #122) mod chrome; mod leetcode; pub use leetcode::LeetCode; diff --git a/src/pym.rs b/src/pym.rs index dfb409b..43dd81b 100644 --- a/src/pym.rs +++ b/src/pym.rs @@ -1,26 +1,27 @@ //! This module is for python scripts. //! //! Seems like some error exists now, welocome pr to fix this : ) -use crate::cache::Cache; -use crate::helper::load_script; +use crate::{cache::Cache, helper::load_script, Result}; use pyo3::prelude::*; +use std::ffi::CString; /// Exec python scripts as filter -pub fn exec(module: &str) -> Result, crate::Error> { +pub fn exec(module: &str) -> Result> { + pyo3::prepare_freethreaded_python(); let script = load_script(&module)?; let cache = Cache::new()?; - // pygil - let gil = Python::acquire_gil(); - let py = gil.python(); - let pym = PyModule::from_code(py, &script, "plan.py", "plan")?; - // args let sps = serde_json::to_string(&cache.get_problems()?)?; let stags = serde_json::to_string(&cache.get_tags()?)?; - // ret - let res: Vec = pym.call1("plan", (sps, stags))?.extract()?; - - Ok(res) + // pygil + Python::with_gil(|py| { + let script_cstr = CString::new(script.as_str())?; + let filename_cstr = CString::new("plan.py")?; + let module_name_cstr = CString::new("plan")?; + let pym = PyModule::from_code(py, &script_cstr, &filename_cstr, &module_name_cstr)?; + pym.getattr("plan")?.call1((sps, stags))?.extract() + }) + .map_err(Into::into) } pFad - Phonifier reborn

Pfad - The Proxy pFad of © 2024 Garber Painting. All rights reserved.

Note: This service is not intended for secure transactions such as banking, social media, email, or purchasing. Use at your own risk. We assume no liability whatsoever for broken pages.


Alternative Proxies:

Alternative Proxy

pFad Proxy

pFad v3 Proxy

pFad v4 Proxy