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 93cf624..67e0e9c 100644
--- a/.gitignore
+++ b/.gitignore
@@ -1,6 +1,6 @@
**/*target
**/*.rs.bk
-Cargo.lock
.DS_Store
.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 5da9c82..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 = "4", 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
-
+
+
[](https://crates.io/crates/leetcode-cli)
[](https://docs.rs/leetcode-cli/)
[](https://crates.io/crates/leetcode-cli)
-[](https://gitter.im/Odditypark/leetcode-cli)
+[](https://t.me/+U_5si6PhWykxZTI1)
[](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 02323b6..8d2d0df 100644
--- a/flake.lock
+++ b/flake.lock
@@ -1,12 +1,32 @@
{
"nodes": {
+ "naersk": {
+ "inputs": {
+ "nixpkgs": [
+ "nixpkgs"
+ ]
+ },
+ "locked": {
+ "lastModified": 1721727458,
+ "narHash": "sha256-r/xppY958gmZ4oTfLiHN0ZGuQ+RSTijDblVgVLFi1mw=",
+ "owner": "nix-community",
+ "repo": "naersk",
+ "rev": "3fb418eaf352498f6b6c30592e3beb63df42ef11",
+ "type": "github"
+ },
+ "original": {
+ "owner": "nix-community",
+ "repo": "naersk",
+ "type": "github"
+ }
+ },
"nixpkgs": {
"locked": {
- "lastModified": 1665634984,
- "narHash": "sha256-zwXeMc96BD9iFxSB/SLr3dI8iYpqM+seX9qy6bGV+cw=",
+ "lastModified": 1728538411,
+ "narHash": "sha256-f0SBJz1eZ2yOuKUr5CA9BHULGXVSn6miBuUWdTyhUhU=",
"owner": "NixOS",
"repo": "nixpkgs",
- "rev": "cfea568da97a2668ef3cb3fc42eaacfb0e706807",
+ "rev": "b69de56fac8c2b6f8fd27f2eca01dcda8e0a4221",
"type": "github"
},
"original": {
@@ -18,17 +38,57 @@
},
"root": {
"inputs": {
+ "naersk": "naersk",
"nixpkgs": "nixpkgs",
+ "rust-overlay": "rust-overlay",
"utils": "utils"
}
},
+ "rust-overlay": {
+ "inputs": {
+ "nixpkgs": [
+ "nixpkgs"
+ ]
+ },
+ "locked": {
+ "lastModified": 1728700003,
+ "narHash": "sha256-Ox1pvEHxLK6lAdaKQW21Zvk65SPDag+cD8YA444R/og=",
+ "owner": "oxalica",
+ "repo": "rust-overlay",
+ "rev": "fc1e58ebabe0cef4442eedea07556ff0c9eafcfe",
+ "type": "github"
+ },
+ "original": {
+ "owner": "oxalica",
+ "repo": "rust-overlay",
+ "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": 1659877975,
- "narHash": "sha256-zllb8aq3YO3h8B/U0/J1WBgAL8EX5yWf5pMj3G0NAmc=",
+ "lastModified": 1726560853,
+ "narHash": "sha256-X6rJYSESBVr3hBoH0WbKE5KvhPU5bloyZ2L4K60/fPQ=",
"owner": "numtide",
"repo": "flake-utils",
- "rev": "c0e246b9b83f637f4681389ecabcb2681b4f3af0",
+ "rev": "c1dfcf08411b08f6b8615f7d8971a2bfa81d5e8a",
"type": "github"
},
"original": {
diff --git a/flake.nix b/flake.nix
index d9ce06a..936e777 100644
--- a/flake.nix
+++ b/flake.nix
@@ -1,41 +1,72 @@
{
description = "Leet your code in command-line.";
- inputs.nixpkgs.url = "github:NixOS/nixpkgs/nixpkgs-unstable";
- inputs.utils.url = "github:numtide/flake-utils";
+ inputs = {
+ nixpkgs.url = "github:NixOS/nixpkgs/nixpkgs-unstable";
+ utils.url = "github:numtide/flake-utils";
- outputs = { self, nixpkgs, 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 = [ (import rust-overlay) ];
+
+ pkgs = (import nixpkgs) {
+ inherit system overlays;
+ };
+
+ toolchain = pkgs.rust-bin.fromRustupToolchainFile ./rust-toolchain.toml;
+
+ naersk' = pkgs.callPackage naersk {
+ cargo = toolchain;
+ rustc = toolchain;
+ clippy = toolchain;
+ };
nativeBuildInputs = with pkgs; [
pkg-config
];
+ darwinBuildInputs = pkgs.lib.optionals pkgs.stdenv.isDarwin [
+ pkgs.darwin.apple_sdk.frameworks.Security
+ pkgs.darwin.apple_sdk.frameworks.SystemConfiguration
+ ];
+
buildInputs = with pkgs; [
openssl
dbus
sqlite
- ] ++ lib.optionals stdenv.isDarwin [ darwin.apple_sdk.frameworks.Security ];
+ ] ++ darwinBuildInputs;
-
- package = with pkgs; rustPlatform.buildRustPackage rec {
+ package = naersk'.buildPackage rec {
pname = "leetcode-cli";
- version = "0.3.11";
- src = fetchCrate {
- inherit pname version;
- sha256 = "sha256-DHtIhiRPRGuO6Rf1d9f8r0bMOHqAaJleUvYNyPiX6mc=";
- };
- cargoSha256 = "sha256-Suk/nQ+JcoD9HO9x1lYp+p4qx0DZ9dt0p5jPz0ZQB+k=";
+ version = "git";
+
+ src = ./.;
+ doCheck = true; # run `cargo test` on build
inherit buildInputs nativeBuildInputs;
- # a nightly compiler is required unless we use this cheat code.
- RUSTC_BOOTSTRAP = 0;
+ buildNoDefaultFeatures = true;
- # CFG_RELEASE = "${rustPlatform.rust.rustc.version}-stable";
- CFG_RELEASE_CHANNEL = "stable";
+ buildFeatures = "git";
meta = with pkgs.lib; {
description = "Leet your code in command-line.";
@@ -44,9 +75,16 @@
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; };
@@ -55,11 +93,7 @@
inherit nativeBuildInputs;
buildInputs = buildInputs ++ [
- rustc
- cargo
- rustfmt
- clippy
- rust-analyzer
+ toolchain
cargo-edit
cargo-bloat
cargo-audit
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 6ec86cb..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()?);
+ 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 f74cb28..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";
@@ -323,22 +339,26 @@ impl std::fmt::Display for VerifyResult {
.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 90d1756..63107a2 100644
--- a/src/cli.rs
+++ b/src/cli.rs
@@ -1,13 +1,13 @@
//! 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.
@@ -26,7 +26,8 @@ pub fn reset_signal_pipe_handler() {
/// 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![
@@ -37,10 +38,12 @@ 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.get_flag("debug") {
Debug::handler()?;
@@ -59,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/edit.rs b/src/cmds/edit.rs
index f7e490b..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
///
@@ -27,7 +29,7 @@ impl Command for EditCommand {
/// `edit` usage
fn usage() -> ClapCommand {
ClapCommand::new("edit")
- .about("Edit question by id")
+ .about("Edit question")
.visible_alias("e")
.arg(
Arg::new("lang")
@@ -39,21 +41,46 @@ impl Command for EditCommand {
.arg(
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("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 = *m.get_one::("id").ok_or(Error::NoneError)?;
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;
@@ -93,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()
@@ -112,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)?;
@@ -123,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(),
+ );
}
}
@@ -140,7 +178,7 @@ impl Command for EditCommand {
// ```toml
// [code]
// editor = "emacsclient"
- // editor-args = [ "-n", "-s", "doom" ]
+ // editor_args = [ "-n", "-s", "doom" ]
// ```
//
// ```rust
@@ -151,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 f25f9b4..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
///
@@ -36,14 +36,40 @@ impl Command for ExecCommand {
.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.get_one::("id").ok_or(Error::NoneError)?;
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 f18a953..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
@@ -42,15 +42,15 @@ use clap::{Arg, ArgAction, ArgMatches, Command as ClapCommand};
/// ## 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"
@@ -183,8 +183,7 @@ impl Command for ListCommand {
let num_range: Vec = m
.get_many::("range")
.ok_or(Error::NoneError)?
- .map(|id| *id)
- .into_iter()
+ .copied()
.collect();
ps.retain(|x| num_range[0] <= x.fid && x.fid <= num_range[1]);
}
diff --git a/src/cmds/mod.rs b/src/cmds/mod.rs
index 7bf957f..1124532 100644
--- a/src/cmds/mod.rs
+++ b/src/cmds/mod.rs
@@ -24,6 +24,7 @@ pub trait Command {
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 9fc136f..aba8166 100644
--- a/src/cmds/pick.rs
+++ b/src/cmds/pick.rs
@@ -1,5 +1,6 @@
//! Pick command
use super::Command;
+use crate::cache::models::Problem;
use crate::err::Error;
use async_trait::async_trait;
use clap::{Arg, ArgAction, ArgMatches, Command as ClapCommand};
@@ -17,7 +18,7 @@ use clap::{Arg, ArgAction, 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, ArgAction, 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
@@ -47,6 +48,14 @@ impl Command for PickCommand {
ClapCommand::new("pick")
.about("Pick a problem")
.visible_alias("p")
+ .arg(
+ 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))
@@ -121,21 +130,36 @@ impl Command for PickCommand {
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
- .get_one::("id")
- .map(|id| *id)
- .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;
@@ -143,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?;
}
}
@@ -152,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 913064a..4b463da 100644
--- a/src/cmds/stat.rs
+++ b/src/cmds/stat.rs
@@ -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 af72014..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
///
@@ -27,12 +27,11 @@ impl Command for TestCommand {
/// `test` usage
fn usage() -> ClapCommand {
ClapCommand::new("test")
- .about("Test question by id")
+ .about("Test a question")
.visible_alias("t")
.arg(
Arg::new("id")
.num_args(1)
- .required(true)
.value_parser(clap::value_parser!(i32))
.help("question id"),
)
@@ -42,18 +41,45 @@ impl Command for TestCommand {
.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.get_one::("id").ok_or(Error::NoneError)?;
+
+ 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/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 141c765..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)
}