├── rustfmt.toml ├── .gitignore ├── rust-toolchain.toml ├── src ├── bin │ └── lc.rs ├── plugins │ ├── mod.rs │ ├── chrome.rs │ └── leetcode.rs ├── cache │ ├── schemas.rs │ ├── sql.rs │ ├── parser.rs │ ├── mod.rs │ └── models.rs ├── flag.rs ├── pym.rs ├── cmds │ ├── mod.rs │ ├── completions.rs │ ├── exec.rs │ ├── test.rs │ ├── data.rs │ ├── stat.rs │ ├── edit.rs │ ├── pick.rs │ └── list.rs ├── config │ ├── storage.rs │ ├── cookies.rs │ ├── code.rs │ ├── mod.rs │ └── sys.rs ├── cli.rs ├── err.rs ├── lib.rs └── helper.rs ├── .github ├── workflows │ ├── release.yml │ ├── clippy.yml │ └── rust.yml └── dependabot.yml ├── SECURITY.md ├── LICENSE ├── Cargo.toml ├── flake.lock ├── CHANGELOG.md ├── flake.nix ├── tests └── de.rs └── README.md /rustfmt.toml: -------------------------------------------------------------------------------- 1 | edition = "2021" 2 | tab_spaces = 4 3 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | **/*target 2 | **/*.rs.bk 3 | .DS_Store 4 | .idea 5 | .direnv/ 6 | /result 7 | -------------------------------------------------------------------------------- /rust-toolchain.toml: -------------------------------------------------------------------------------- 1 | [toolchain] 2 | channel = "stable" 3 | components = [ 4 | "rustc", 5 | "cargo", 6 | "rustfmt", 7 | "clippy", 8 | "rust-analyzer", 9 | "rust-src" 10 | ] 11 | profile = "minimal" 12 | -------------------------------------------------------------------------------- /src/bin/lc.rs: -------------------------------------------------------------------------------- 1 | use leetcode_cli::cli; 2 | use tokio::runtime::Builder; 3 | 4 | fn main() { 5 | if let Err(err) = Builder::new_multi_thread() 6 | .enable_all() 7 | .build() 8 | .expect("Build tokio runtime failed") 9 | .block_on(cli::main()) 10 | { 11 | println!("{:?}", err); 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /src/plugins/mod.rs: -------------------------------------------------------------------------------- 1 | //! Leetcode-cil plugins 2 | //! 3 | //! + chrome cookie parser 4 | //! + leetcode API 5 | //! 6 | //! ## login to `leetcode.com` 7 | //! Leetcode-cli use chrome cookie directly, do not need to login, please make sure you have loggined in `leetcode.com` before usnig `leetcode-cli` 8 | //! 9 | 10 | // FIXME: Read cookies from local storage. (issue #122) 11 | mod chrome; 12 | mod leetcode; 13 | pub use leetcode::LeetCode; 14 | -------------------------------------------------------------------------------- /src/cache/schemas.rs: -------------------------------------------------------------------------------- 1 | //! Leetcode data schemas 2 | table! { 3 | problems(id) { 4 | category -> Text, 5 | fid -> Integer, 6 | id -> Integer, 7 | level -> Integer, 8 | locked -> Bool, 9 | name -> Text, 10 | percent -> Float, 11 | slug -> Text, 12 | starred -> Bool, 13 | status -> Text, 14 | desc -> Text, 15 | } 16 | } 17 | 18 | // Tags 19 | table! { 20 | tags(tag) { 21 | tag -> Text, 22 | refs -> Text, 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /.github/workflows/release.yml: -------------------------------------------------------------------------------- 1 | name: release 2 | 3 | on: 4 | push: 5 | tags: 6 | - "v*" 7 | jobs: 8 | publish: 9 | name: Publish 10 | # Specify OS 11 | runs-on: ubuntu-latest 12 | steps: 13 | - name: Checkout sources 14 | uses: actions/checkout@v5 15 | - name: Install stable toolchain 16 | uses: actions-rs/toolchain@v1 17 | with: 18 | toolchain: stable 19 | - uses: katyo/publish-crates@v2 20 | with: 21 | registry-token: ${{ secrets.CARGO_REGISTRY_TOKEN }} 22 | -------------------------------------------------------------------------------- /.github/dependabot.yml: -------------------------------------------------------------------------------- 1 | version: 2 2 | updates: 3 | - package-ecosystem: "cargo" 4 | directory: "/" 5 | schedule: 6 | interval: "monthly" 7 | day: "sunday" 8 | commit-message: 9 | prefix: "chore(dep): " 10 | groups: 11 | deps: 12 | patterns: 13 | - "*" 14 | 15 | - package-ecosystem: "github-actions" 16 | directory: "/" 17 | schedule: 18 | interval: "monthly" 19 | day: "sunday" 20 | commit-message: 21 | prefix: "chore(dep): " 22 | groups: 23 | deps: 24 | patterns: 25 | - "*" 26 | -------------------------------------------------------------------------------- /.github/workflows/clippy.yml: -------------------------------------------------------------------------------- 1 | name: Clippy Check 2 | 3 | on: 4 | push: 5 | branches: [ master ] 6 | pull_request: 7 | branches: [ master ] 8 | 9 | jobs: 10 | clippy: 11 | runs-on: ${{ matrix.os }} 12 | strategy: 13 | matrix: 14 | os: [macOS-latest, ubuntu-latest] 15 | steps: 16 | - uses: actions/checkout@v5 17 | - uses: actions-rs/toolchain@v1 18 | with: 19 | components: clippy 20 | toolchain: stable 21 | - uses: actions-rs/clippy-check@v1 22 | with: 23 | token: ${{ secrets.GITHUB_TOKEN }} 24 | -------------------------------------------------------------------------------- /SECURITY.md: -------------------------------------------------------------------------------- 1 | # Security Policy 2 | 3 | ## Supported Versions 4 | 5 | Use this section to tell people about which versions of your project are 6 | currently being supported with security updates. 7 | 8 | | Version | Supported | 9 | | ------- | ------------------ | 10 | | 5.1.x | :white_check_mark: | 11 | | 5.0.x | :x: | 12 | | 4.0.x | :white_check_mark: | 13 | | < 4.0 | :x: | 14 | 15 | ## Reporting a Vulnerability 16 | 17 | Use this section to tell people how to report a vulnerability. 18 | 19 | Tell them where to go, how often they can expect to get an update on a 20 | reported vulnerability, what to expect if the vulnerability is accepted or 21 | declined, etc. 22 | -------------------------------------------------------------------------------- /src/cache/sql.rs: -------------------------------------------------------------------------------- 1 | pub static CREATE_PROBLEMS_IF_NOT_EXISTS: &str = r#" 2 | CREATE TABLE IF NOT EXISTS problems ( 3 | category TEXT NOT NULL, 4 | fid INTEGER NOT NULL, 5 | id INTEGER NOT NULL PRIMARY KEY, 6 | level INTEGER NOT NULL, 7 | locked BOOLEAN NOT NULL DEFAULT 0, 8 | name TEXT NOT NULL, 9 | percent FLOAT NOT NULL, 10 | slug TEXT NOT NULL, 11 | starred BOOLEAN NOT NULL DEFAULT 0, 12 | status TEXT NOT NULL, 13 | desc TEXT NOT NULL DEFAULT "" 14 | ) 15 | "#; 16 | 17 | pub static CREATE_TAGS_IF_NOT_EXISTS: &str = r#" 18 | CREATE TABLE IF NOT EXISTS tags ( 19 | tag TEXT NOT NULL, 20 | refs TEXT NOT NULL 21 | ) 22 | "#; 23 | 24 | // pub static DROP_PROBLEMS: &'static str = r#"DROP TABLE problems"#; 25 | -------------------------------------------------------------------------------- /src/flag.rs: -------------------------------------------------------------------------------- 1 | //! Flags in leetcode-cli 2 | //! 3 | //! ```sh 4 | //! FLAGS: 5 | //! -d, --debug debug mode 6 | //! -h, --help Prints help information 7 | //! -V, --version Prints version information 8 | //! ``` 9 | use crate::err::Error; 10 | use clap::{Arg, ArgAction}; 11 | use env_logger::Env; 12 | 13 | /// Abstract flag trait 14 | pub trait Flag { 15 | fn usage() -> Arg; 16 | fn handler() -> Result<(), Error>; 17 | } 18 | 19 | /// Debug logger 20 | pub struct Debug; 21 | 22 | impl Flag for Debug { 23 | fn usage() -> Arg { 24 | Arg::new("debug") 25 | .short('d') 26 | .long("debug") 27 | .help("debug mode") 28 | .action(ArgAction::SetTrue) 29 | } 30 | 31 | fn handler() -> Result<(), Error> { 32 | env_logger::Builder::from_env(Env::default().default_filter_or("debug")).init(); 33 | 34 | Ok(()) 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /.github/workflows/rust.yml: -------------------------------------------------------------------------------- 1 | name: leetcode-cli 2 | 3 | on: 4 | push: 5 | branches: [master] 6 | pull_request: 7 | branches: [master] 8 | 9 | jobs: 10 | build: 11 | runs-on: ${{ matrix.os }} 12 | strategy: 13 | matrix: 14 | os: [macOS-latest, ubuntu-latest] 15 | steps: 16 | - name: Checkout the source code 17 | uses: actions/checkout@v5 18 | - name: Set nightly toolchain 19 | uses: actions-rs/toolchain@v1 20 | with: 21 | toolchain: stable 22 | - name: Environment 23 | run: | 24 | if [[ "$(uname)" == 'Darwin' ]]; then 25 | brew install sqlite3 26 | else 27 | sudo apt-get update -y 28 | sudo apt-get install -y libsqlite3-dev libdbus-1-dev 29 | fi 30 | - name: Build 31 | run: cargo build --release --all-features 32 | - name: Run tests 33 | run: cargo test --release --all-features 34 | -------------------------------------------------------------------------------- /src/pym.rs: -------------------------------------------------------------------------------- 1 | //! This module is for python scripts. 2 | //! 3 | //! Seems like some error exists now, welocome pr to fix this : ) 4 | use crate::{cache::Cache, helper::load_script, Result}; 5 | use pyo3::prelude::*; 6 | use std::ffi::CString; 7 | 8 | /// Exec python scripts as filter 9 | pub fn exec(module: &str) -> Result> { 10 | pyo3::prepare_freethreaded_python(); 11 | let script = load_script(&module)?; 12 | let cache = Cache::new()?; 13 | 14 | // args 15 | let sps = serde_json::to_string(&cache.get_problems()?)?; 16 | let stags = serde_json::to_string(&cache.get_tags()?)?; 17 | 18 | // pygil 19 | Python::with_gil(|py| { 20 | let script_cstr = CString::new(script.as_str())?; 21 | let filename_cstr = CString::new("plan.py")?; 22 | let module_name_cstr = CString::new("plan")?; 23 | let pym = PyModule::from_code(py, &script_cstr, &filename_cstr, &module_name_cstr)?; 24 | pym.getattr("plan")?.call1((sps, stags))?.extract() 25 | }) 26 | .map_err(Into::into) 27 | } 28 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2019 clearloop 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /src/cmds/mod.rs: -------------------------------------------------------------------------------- 1 | //! All subcommands in leetcode-cli 2 | //! 3 | //! ```sh 4 | //! SUBCOMMANDS: 5 | //! data Manage Cache [aliases: d] 6 | //! edit Edit question by id [aliases: e] 7 | //! list List problems [aliases: l] 8 | //! pick Pick a problem [aliases: p] 9 | //! stat Show simple chart about submissions [aliases: s] 10 | //! test Edit question by id [aliases: t] 11 | //! help Prints this message or the help of the given subcommand(s) 12 | //! ``` 13 | use crate::err::Error; 14 | use async_trait::async_trait; 15 | use clap::{ArgMatches, Command as ClapCommand}; 16 | 17 | /// Abstract commands' trait. 18 | #[async_trait] 19 | pub trait Command { 20 | /// Usage of the specific command 21 | fn usage() -> ClapCommand; 22 | 23 | /// The handler will deal [args, options,...] from the command-line 24 | async fn handler(m: &ArgMatches) -> Result<(), Error>; 25 | } 26 | 27 | mod completions; 28 | mod data; 29 | mod edit; 30 | mod exec; 31 | mod list; 32 | mod pick; 33 | mod stat; 34 | mod test; 35 | pub use completions::{completion_handler, CompletionCommand}; 36 | pub use data::DataCommand; 37 | pub use edit::EditCommand; 38 | pub use exec::ExecCommand; 39 | pub use list::ListCommand; 40 | pub use pick::PickCommand; 41 | pub use stat::StatCommand; 42 | pub use test::TestCommand; 43 | -------------------------------------------------------------------------------- /Cargo.toml: -------------------------------------------------------------------------------- 1 | [[bin]] 2 | name = "leetcode" 3 | path = "src/bin/lc.rs" 4 | 5 | [package] 6 | name = "leetcode-cli" 7 | version = "0.4.7" 8 | authors = ["clearloop "] 9 | edition = "2021" 10 | description = "Leetcode command-line interface in rust." 11 | repository = "https://github.com/clearloop/leetcode-cli" 12 | license = "MIT" 13 | documentation = "https://docs.rs/leetcode_cli" 14 | homepage = "https://github.com/clearloop/leetcode-cli" 15 | keywords = ["cli", "games", "leetcode"] 16 | readme = './README.md' 17 | 18 | [dependencies] 19 | async-trait = "0.1.89" 20 | tokio = { version = "1.48.0", features = ["full"] } 21 | clap = { version = "4.5.51", features = ["cargo"] } 22 | colored = "3.0.0" 23 | dirs = "6.0.0" 24 | env_logger = "0.11.6" 25 | keyring = "3.6.3" 26 | log = "0.4.28" 27 | openssl = "0.10.74" 28 | pyo3 = { version = "0.27.1", optional = true } 29 | rand = "0.9.2" 30 | serde = { version = "1.0.228", features = ["derive"] } 31 | serde_json = "1.0.145" 32 | toml = "0.9.8" 33 | regex = "1.12.2" 34 | scraper = "0.24.0" 35 | anyhow = "1.0.100" 36 | clap_complete = "4.5.60" 37 | thiserror = "2.0.17" 38 | unicode-width = "0.2" 39 | 40 | [dependencies.diesel] 41 | version = "2.2.12" 42 | features = ["sqlite"] 43 | 44 | [dependencies.reqwest] 45 | version = "0.12.24" 46 | features = ["gzip", "json"] 47 | 48 | [features] 49 | pym = ["pyo3"] 50 | 51 | [target.'cfg(target_family = "unix")'.dependencies] 52 | nix = { version = "0.30.1", features = [ "signal" ] } 53 | -------------------------------------------------------------------------------- /src/cmds/completions.rs: -------------------------------------------------------------------------------- 1 | //! Completions command 2 | 3 | use super::Command; 4 | use crate::err::Error; 5 | use async_trait::async_trait; 6 | use clap::{Arg, ArgAction, ArgMatches, Command as ClapCommand}; 7 | use clap_complete::{generate, Generator, Shell}; 8 | 9 | /// Abstract shell completions command 10 | /// 11 | /// ```sh 12 | /// Generate shell Completions 13 | 14 | /// USAGE: 15 | /// leetcode completions 16 | 17 | /// ARGUMENTS: 18 | /// [possible values: bash, elvish, fish, powershell, zsh] 19 | /// ``` 20 | pub struct CompletionCommand; 21 | 22 | #[async_trait] 23 | impl Command for CompletionCommand { 24 | /// `pick` usage 25 | fn usage() -> ClapCommand { 26 | ClapCommand::new("completions") 27 | .about("Generate shell Completions") 28 | .visible_alias("c") 29 | .arg( 30 | Arg::new("shell") 31 | .action(ArgAction::Set) 32 | .value_parser(clap::value_parser!(Shell)), 33 | ) 34 | } 35 | 36 | async fn handler(_m: &ArgMatches) -> Result<(), Error> { 37 | // defining custom handler to print the completions. Handler method signature limits taking 38 | // other params. We need &ArgMatches and &mut ClapCommand to generate completions. 39 | println!("Don't use this handler. Does not implement the functionality to print completions. Use completions_handler() below."); 40 | Ok(()) 41 | } 42 | } 43 | 44 | fn get_completions_string(gen: G, cmd: &mut ClapCommand) -> Result { 45 | let mut v: Vec = Vec::new(); 46 | let name = cmd.get_name().to_string(); 47 | generate(gen, cmd, name, &mut v); 48 | Ok(String::from_utf8(v)?) 49 | } 50 | 51 | pub fn completion_handler(m: &ArgMatches, cmd: &mut ClapCommand) -> Result<(), Error> { 52 | let shell = *m.get_one::("shell").unwrap_or( 53 | // if shell value is not provided try to get from the environment 54 | &Shell::from_env().ok_or(Error::MatchError)?, 55 | ); 56 | let completions = get_completions_string(shell, cmd)?; 57 | println!("{}", completions); 58 | Ok(()) 59 | } 60 | -------------------------------------------------------------------------------- /src/config/storage.rs: -------------------------------------------------------------------------------- 1 | //! Storage in config. 2 | use crate::{Error, Result}; 3 | use serde::{Deserialize, Serialize}; 4 | use std::{fs, path::PathBuf}; 5 | 6 | /// Locate code files 7 | /// 8 | /// + cache -> the path to cache 9 | #[derive(Clone, Debug, Deserialize, Serialize)] 10 | pub struct Storage { 11 | cache: String, 12 | code: String, 13 | root: String, 14 | scripts: Option, 15 | } 16 | 17 | impl Default for Storage { 18 | fn default() -> Self { 19 | Self { 20 | cache: "Problems".into(), 21 | code: "code".into(), 22 | scripts: Some("scripts".into()), 23 | root: "~/.leetcode".into(), 24 | } 25 | } 26 | } 27 | 28 | impl Storage { 29 | /// convert root path 30 | pub fn root(&self) -> Result { 31 | let home = dirs::home_dir() 32 | .ok_or(Error::NoneError)? 33 | .to_string_lossy() 34 | .to_string(); 35 | let path = self.root.replace('~', &home); 36 | Ok(path) 37 | } 38 | 39 | /// get cache path 40 | pub fn cache(&self) -> Result { 41 | let root = PathBuf::from(self.root()?); 42 | if !root.exists() { 43 | info!("Generate cache dir at {:?}.", &root); 44 | fs::DirBuilder::new().recursive(true).create(&root)?; 45 | } 46 | 47 | Ok(root.join("Problems").to_string_lossy().to_string()) 48 | } 49 | 50 | /// get code path 51 | pub fn code(&self) -> Result { 52 | let root = &self.root()?; 53 | let p = PathBuf::from(root).join(&self.code); 54 | if !PathBuf::from(&p).exists() { 55 | fs::create_dir(&p)? 56 | } 57 | 58 | Ok(p.to_string_lossy().to_string()) 59 | } 60 | 61 | /// get scripts path 62 | pub fn scripts(mut self) -> Result { 63 | let root = &self.root()?; 64 | if self.scripts.is_none() { 65 | self.scripts = Some("scripts".into()); 66 | } 67 | 68 | let p = PathBuf::from(root).join(self.scripts.ok_or(Error::NoneError)?); 69 | if !PathBuf::from(&p).exists() { 70 | std::fs::create_dir(&p)? 71 | } 72 | 73 | Ok(p.to_string_lossy().to_string()) 74 | } 75 | } 76 | -------------------------------------------------------------------------------- /src/cmds/exec.rs: -------------------------------------------------------------------------------- 1 | //! Exec command 2 | use super::Command; 3 | use crate::{Error, Result}; 4 | use async_trait::async_trait; 5 | use clap::{Arg, ArgAction, ArgGroup, ArgMatches, Command as ClapCommand}; 6 | 7 | /// Abstract Exec Command 8 | /// 9 | /// ```sh 10 | /// leetcode-exec 11 | /// Submit solution 12 | /// 13 | /// USAGE: 14 | /// leetcode exec 15 | /// 16 | /// FLAGS: 17 | /// -h, --help Prints help information 18 | /// -V, --version Prints version information 19 | /// 20 | /// ARGS: 21 | /// question id 22 | /// ``` 23 | pub struct ExecCommand; 24 | 25 | #[async_trait] 26 | impl Command for ExecCommand { 27 | /// `exec` usage 28 | fn usage() -> ClapCommand { 29 | ClapCommand::new("exec") 30 | .about("Submit solution") 31 | .visible_alias("x") 32 | .arg( 33 | Arg::new("id") 34 | .num_args(1) 35 | .required(true) 36 | .value_parser(clap::value_parser!(i32)) 37 | .help("question id"), 38 | ) 39 | .arg( 40 | Arg::new("daily") 41 | .short('d') 42 | .long("daily") 43 | .help("Exec today's daily challenge") 44 | .action(ArgAction::SetTrue), 45 | ) 46 | .group( 47 | ArgGroup::new("question-id") 48 | .args(["id", "daily"]) 49 | .multiple(false) 50 | .required(true), 51 | ) 52 | } 53 | 54 | /// `exec` handler 55 | async fn handler(m: &ArgMatches) -> Result<()> { 56 | use crate::cache::{Cache, Run}; 57 | 58 | let cache = Cache::new()?; 59 | 60 | let daily = m.get_one::("daily").unwrap_or(&false); 61 | let daily_id = if *daily { 62 | Some(cache.get_daily_problem_id().await?) 63 | } else { 64 | None 65 | }; 66 | 67 | let id = m 68 | .get_one::("id") 69 | .copied() 70 | .or(daily_id) 71 | .ok_or(Error::NoneError)?; 72 | 73 | let res = cache.exec_problem(id, Run::Submit, None).await?; 74 | 75 | println!("{}", res); 76 | Ok(()) 77 | } 78 | } 79 | -------------------------------------------------------------------------------- /src/cli.rs: -------------------------------------------------------------------------------- 1 | //! Clap Commanders 2 | use crate::{ 3 | cmds::{ 4 | completion_handler, Command, CompletionCommand, DataCommand, EditCommand, ExecCommand, 5 | ListCommand, PickCommand, StatCommand, TestCommand, 6 | }, 7 | err::Error, 8 | flag::{Debug, Flag}, 9 | }; 10 | use clap::crate_version; 11 | use log::LevelFilter; 12 | 13 | /// This should be called before calling any cli method or printing any output. 14 | pub fn reset_signal_pipe_handler() { 15 | #[cfg(target_family = "unix")] 16 | { 17 | use nix::sys::signal; 18 | 19 | unsafe { 20 | let _ = signal::signal(signal::Signal::SIGPIPE, signal::SigHandler::SigDfl) 21 | .map_err(|e| println!("{:?}", e)); 22 | } 23 | } 24 | } 25 | 26 | /// Get matches 27 | pub async fn main() -> Result<(), Error> { 28 | reset_signal_pipe_handler(); 29 | 30 | let mut cmd = clap::Command::new("leetcode") 31 | .version(crate_version!()) 32 | .about("May the Code be with You 👻") 33 | .subcommands(vec![ 34 | DataCommand::usage().display_order(1), 35 | EditCommand::usage().display_order(2), 36 | ExecCommand::usage().display_order(3), 37 | ListCommand::usage().display_order(4), 38 | PickCommand::usage().display_order(5), 39 | StatCommand::usage().display_order(6), 40 | TestCommand::usage().display_order(7), 41 | CompletionCommand::usage().display_order(8), 42 | ]) 43 | .arg(Debug::usage()) 44 | .arg_required_else_help(true); 45 | 46 | let m = cmd.clone().get_matches(); 47 | 48 | if m.get_flag("debug") { 49 | Debug::handler()?; 50 | } else { 51 | env_logger::Builder::new() 52 | .filter_level(LevelFilter::Info) 53 | .format_timestamp(None) 54 | .init(); 55 | } 56 | 57 | match m.subcommand() { 58 | Some(("data", sub_m)) => Ok(DataCommand::handler(sub_m).await?), 59 | Some(("edit", sub_m)) => Ok(EditCommand::handler(sub_m).await?), 60 | Some(("exec", sub_m)) => Ok(ExecCommand::handler(sub_m).await?), 61 | Some(("list", sub_m)) => Ok(ListCommand::handler(sub_m).await?), 62 | Some(("pick", sub_m)) => Ok(PickCommand::handler(sub_m).await?), 63 | Some(("stat", sub_m)) => Ok(StatCommand::handler(sub_m).await?), 64 | Some(("test", sub_m)) => Ok(TestCommand::handler(sub_m).await?), 65 | Some(("completions", sub_m)) => Ok(completion_handler(sub_m, &mut cmd)?), 66 | _ => Err(Error::MatchError), 67 | } 68 | } 69 | -------------------------------------------------------------------------------- /src/cmds/test.rs: -------------------------------------------------------------------------------- 1 | //! Test command 2 | use super::Command; 3 | use crate::{Error, Result}; 4 | use async_trait::async_trait; 5 | use clap::{Arg, ArgAction, ArgGroup, ArgMatches, Command as ClapCommand}; 6 | 7 | /// Abstract Test Command 8 | /// 9 | /// ```sh 10 | /// leetcode-test 11 | /// Edit question by id 12 | /// 13 | /// USAGE: 14 | /// leetcode test 15 | /// 16 | /// FLAGS: 17 | /// -h, --help Prints help information 18 | /// -V, --version Prints version information 19 | /// 20 | /// ARGS: 21 | /// question id 22 | /// ``` 23 | pub struct TestCommand; 24 | 25 | #[async_trait] 26 | impl Command for TestCommand { 27 | /// `test` usage 28 | fn usage() -> ClapCommand { 29 | ClapCommand::new("test") 30 | .about("Test a question") 31 | .visible_alias("t") 32 | .arg( 33 | Arg::new("id") 34 | .num_args(1) 35 | .value_parser(clap::value_parser!(i32)) 36 | .help("question id"), 37 | ) 38 | .arg( 39 | Arg::new("testcase") 40 | .num_args(1) 41 | .required(false) 42 | .help("custom testcase"), 43 | ) 44 | .arg( 45 | Arg::new("daily") 46 | .short('d') 47 | .long("daily") 48 | .help("Test today's daily challenge") 49 | .action(ArgAction::SetTrue), 50 | ) 51 | .group( 52 | ArgGroup::new("question-id") 53 | .args(["id", "daily"]) 54 | .multiple(false) 55 | .required(true), 56 | ) 57 | } 58 | 59 | /// `test` handler 60 | async fn handler(m: &ArgMatches) -> Result<()> { 61 | use crate::cache::{Cache, Run}; 62 | 63 | let cache = Cache::new()?; 64 | 65 | let daily = m.get_one::("daily").unwrap_or(&false); 66 | let daily_id = if *daily { 67 | Some(cache.get_daily_problem_id().await?) 68 | } else { 69 | None 70 | }; 71 | 72 | let id = m 73 | .get_one::("id") 74 | .copied() 75 | .or(daily_id) 76 | .ok_or(Error::NoneError)?; 77 | 78 | let testcase = m.get_one::("testcase"); 79 | let case_str: Option = match testcase { 80 | Some(case) => Option::from(case.replace("\\n", "\n")), 81 | _ => None, 82 | }; 83 | let res = cache.exec_problem(id, Run::Test, case_str).await?; 84 | 85 | println!("{}", res); 86 | Ok(()) 87 | } 88 | } 89 | -------------------------------------------------------------------------------- /src/config/cookies.rs: -------------------------------------------------------------------------------- 1 | //! Cookies in config 2 | use serde::{Deserialize, Serialize}; 3 | use std::{ 4 | fmt::{self, Display}, 5 | str::FromStr, 6 | }; 7 | 8 | #[derive(Clone, Debug, Deserialize, Serialize)] 9 | pub enum LeetcodeSite { 10 | #[serde(rename = "leetcode.com")] 11 | LeetcodeCom, 12 | #[serde(rename = "leetcode.cn")] 13 | LeetcodeCn, 14 | } 15 | 16 | impl FromStr for LeetcodeSite { 17 | type Err = String; 18 | fn from_str(s: &str) -> Result { 19 | match s { 20 | "leetcode.com" => Ok(LeetcodeSite::LeetcodeCom), 21 | "leetcode.cn" => Ok(LeetcodeSite::LeetcodeCn), 22 | _ => Err("Invalid site key".to_string()), 23 | } 24 | } 25 | } 26 | 27 | impl Display for LeetcodeSite { 28 | fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { 29 | let s = match self { 30 | LeetcodeSite::LeetcodeCom => "leetcode.com", 31 | LeetcodeSite::LeetcodeCn => "leetcode.cn", 32 | }; 33 | 34 | write!(f, "{s}") 35 | } 36 | } 37 | 38 | /// Cookies settings 39 | #[derive(Clone, Debug, Deserialize, Serialize)] 40 | pub struct Cookies { 41 | pub csrf: String, 42 | pub session: String, 43 | pub site: LeetcodeSite, 44 | } 45 | 46 | impl Default for Cookies { 47 | fn default() -> Self { 48 | Self { 49 | csrf: "".to_string(), 50 | session: "".to_string(), 51 | site: LeetcodeSite::LeetcodeCom, 52 | } 53 | } 54 | } 55 | 56 | impl Display for Cookies { 57 | fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { 58 | write!( 59 | f, 60 | "LEETCODE_SESSION={};csrftoken={};", 61 | self.session, self.csrf 62 | ) 63 | } 64 | } 65 | 66 | /// Override cookies from environment variables 67 | pub const LEETCODE_CSRF_ENV: &str = "LEETCODE_CSRF"; 68 | pub const LEETCODE_SESSION_ENV: &str = "LEETCODE_SESSION"; 69 | pub const LEETCODE_SITE_ENV: &str = "LEETCODE_SITE"; 70 | 71 | impl Cookies { 72 | /// Load cookies from environment variables, overriding any existing values 73 | /// if the environment variables are set. 74 | pub fn with_env_override(mut self) -> Self { 75 | if let Ok(csrf) = std::env::var(LEETCODE_CSRF_ENV) { 76 | self.csrf = csrf; 77 | } 78 | if let Ok(session) = std::env::var(LEETCODE_SESSION_ENV) { 79 | self.session = session; 80 | } 81 | if let Ok(site) = std::env::var(LEETCODE_SITE_ENV) { 82 | if let Ok(leetcode_site) = LeetcodeSite::from_str(&site) { 83 | self.site = leetcode_site; 84 | } 85 | } 86 | self 87 | } 88 | } 89 | -------------------------------------------------------------------------------- /src/config/code.rs: -------------------------------------------------------------------------------- 1 | //! Code in config 2 | use serde::{Deserialize, Serialize}; 3 | 4 | const PICK_DEFAULT: &str = "${fid}.${slug}"; 5 | fn default_pick() -> String { 6 | PICK_DEFAULT.into() 7 | } 8 | 9 | const SUBMISSION_DEFAULT: &str = "${fid}.${slug}.${sid}.${ac}"; 10 | fn default_submission() -> String { 11 | SUBMISSION_DEFAULT.into() 12 | } 13 | 14 | fn is_default_pick(t: &String) -> bool { 15 | t == PICK_DEFAULT 16 | } 17 | 18 | fn is_default_submission(t: &String) -> bool { 19 | t == SUBMISSION_DEFAULT 20 | } 21 | 22 | fn is_default_string(t: &String) -> bool { 23 | t.is_empty() 24 | } 25 | fn is_default_bool(t: &bool) -> bool { 26 | !t 27 | } 28 | 29 | /// Code config 30 | #[derive(Clone, Debug, Deserialize, Serialize)] 31 | pub struct Code { 32 | #[serde(default)] 33 | pub editor: String, 34 | #[serde(rename(serialize = "editor-args"), alias = "editor-args", default)] 35 | pub editor_args: Option>, 36 | #[serde(rename(serialize = "editor-envs"), alias = "editor-envs", default)] 37 | pub editor_envs: Option>, 38 | #[serde(default, skip_serializing_if = "is_default_bool")] 39 | pub edit_code_marker: bool, 40 | #[serde(default, skip_serializing_if = "is_default_string")] 41 | pub start_marker: String, 42 | #[serde(default, skip_serializing_if = "is_default_string")] 43 | pub end_marker: String, 44 | #[serde(rename(serialize = "inject_before"), alias = "inject_before", default)] 45 | pub inject_before: Option>, 46 | #[serde(rename(serialize = "inject_after"), alias = "inject_after", default)] 47 | pub inject_after: Option>, 48 | #[serde(default, skip_serializing_if = "is_default_bool")] 49 | pub comment_problem_desc: bool, 50 | #[serde(default, skip_serializing_if = "is_default_string")] 51 | pub comment_leading: String, 52 | #[serde(default, skip_serializing_if = "is_default_bool")] 53 | pub test: bool, 54 | pub lang: String, 55 | #[serde(default = "default_pick", skip_serializing_if = "is_default_pick")] 56 | pub pick: String, 57 | #[serde( 58 | default = "default_submission", 59 | skip_serializing_if = "is_default_submission" 60 | )] 61 | pub submission: String, 62 | } 63 | 64 | impl Default for Code { 65 | fn default() -> Self { 66 | Self { 67 | editor: "vim".into(), 68 | editor_args: None, 69 | editor_envs: None, 70 | edit_code_marker: false, 71 | start_marker: "".into(), 72 | end_marker: "".into(), 73 | inject_before: None, 74 | inject_after: None, 75 | comment_problem_desc: false, 76 | comment_leading: "".into(), 77 | test: true, 78 | lang: "rust".into(), 79 | pick: "${fid}.${slug}".into(), 80 | submission: "${fid}.${slug}.${sid}.${ac}".into(), 81 | } 82 | } 83 | } 84 | -------------------------------------------------------------------------------- /src/cmds/data.rs: -------------------------------------------------------------------------------- 1 | //! Cache managger 2 | use super::Command; 3 | use crate::{cache::Cache, helper::Digit, Error}; 4 | use async_trait::async_trait; 5 | use clap::{Arg, ArgAction, ArgMatches, Command as ClapCommand}; 6 | use colored::Colorize; 7 | 8 | /// Abstract `data` command 9 | /// 10 | /// ```sh 11 | /// leetcode-data 12 | /// Manage Cache 13 | /// 14 | /// USAGE: 15 | /// leetcode data [FLAGS] 16 | /// 17 | /// FLAGS: 18 | /// -d, --delete Delete cache 19 | /// -u, --update Update cache 20 | /// -h, --help Prints help information 21 | /// -V, --version Prints version information 22 | /// ``` 23 | pub struct DataCommand; 24 | 25 | #[async_trait] 26 | impl Command for DataCommand { 27 | /// `data` command usage 28 | fn usage() -> ClapCommand { 29 | ClapCommand::new("data") 30 | .about("Manage Cache") 31 | .visible_alias("d") 32 | .arg( 33 | Arg::new("delete") 34 | .display_order(1) 35 | .short('d') 36 | .long("delete") 37 | .help("Delete cache") 38 | .action(ArgAction::SetTrue), 39 | ) 40 | .arg( 41 | Arg::new("update") 42 | .display_order(2) 43 | .short('u') 44 | .long("update") 45 | .help("Update cache") 46 | .action(ArgAction::SetTrue), 47 | ) 48 | } 49 | 50 | /// `data` handler 51 | async fn handler(m: &ArgMatches) -> Result<(), Error> { 52 | use std::fs::File; 53 | use std::path::Path; 54 | 55 | let cache = Cache::new()?; 56 | let path = cache.0.conf.storage.cache()?; 57 | let f = File::open(&path)?; 58 | let len = format!("{}K", f.metadata()?.len() / 1000); 59 | 60 | let out = format!( 61 | " {}{}", 62 | Path::new(&path) 63 | .file_name() 64 | .ok_or(Error::NoneError)? 65 | .to_string_lossy() 66 | .to_string() 67 | .digit(65 - (len.len() as i32)) 68 | .bright_green(), 69 | len 70 | ); 71 | 72 | let mut title = "\n Cache".digit(63); 73 | title.push_str("Size"); 74 | title.push_str("\n "); 75 | title.push_str(&"-".repeat(65)); 76 | 77 | let mut flags = 0; 78 | if m.get_flag("delete") { 79 | flags += 1; 80 | cache.clean()?; 81 | println!("{}", "ok!".bright_green()); 82 | } 83 | 84 | if m.get_flag("update") { 85 | flags += 1; 86 | cache.update().await?; 87 | println!("{}", "ok!".bright_green()); 88 | } 89 | 90 | if flags == 0 { 91 | println!("{}", title.bright_black()); 92 | println!("{}\n", out); 93 | } 94 | 95 | Ok(()) 96 | } 97 | } 98 | -------------------------------------------------------------------------------- /src/config/mod.rs: -------------------------------------------------------------------------------- 1 | //! Soft-link with `config.toml` 2 | //! 3 | //! leetcode-cli will generate a `leetcode.toml` by default, 4 | //! if you wanna change to it, you can: 5 | //! 6 | //! + Edit leetcode.toml at `~/.leetcode/leetcode.toml` directly 7 | //! + Use `leetcode config` to update it 8 | use crate::{ 9 | config::{code::Code, cookies::Cookies, storage::Storage, sys::Sys}, 10 | Error, Result, 11 | }; 12 | use serde::{Deserialize, Serialize}; 13 | use std::{fs, path::Path}; 14 | 15 | mod code; 16 | mod cookies; 17 | mod storage; 18 | mod sys; 19 | 20 | pub use cookies::LeetcodeSite; 21 | 22 | /// Sync with `~/.leetcode/leetcode.toml` 23 | #[derive(Clone, Debug, Default, Deserialize, Serialize)] 24 | pub struct Config { 25 | #[serde(default, skip_serializing)] 26 | pub sys: Sys, 27 | pub code: Code, 28 | pub cookies: Cookies, 29 | pub storage: Storage, 30 | } 31 | 32 | impl Config { 33 | fn write_default(p: impl AsRef) -> Result<()> { 34 | fs::write(p.as_ref(), toml::ser::to_string_pretty(&Self::default())?)?; 35 | 36 | Ok(()) 37 | } 38 | 39 | /// Locate lc's config file 40 | pub fn locate() -> Result { 41 | let conf = Self::root()?.join("leetcode.toml"); 42 | 43 | if !conf.is_file() { 44 | Self::write_default(&conf)?; 45 | } 46 | 47 | let s = fs::read_to_string(&conf)?; 48 | match toml::from_str::(&s) { 49 | Ok(mut config) => { 50 | // Override config.cookies with environment variables 51 | config.cookies = config.cookies.with_env_override(); 52 | 53 | match config.cookies.site { 54 | cookies::LeetcodeSite::LeetcodeCom => Ok(config), 55 | cookies::LeetcodeSite::LeetcodeCn => { 56 | let mut config = config; 57 | config.sys.urls = sys::Urls::new_with_leetcode_cn(); 58 | Ok(config) 59 | } 60 | } 61 | } 62 | Err(e) => { 63 | let tmp = Self::root()?.join("leetcode.tmp.toml"); 64 | Self::write_default(tmp)?; 65 | Err(e.into()) 66 | } 67 | } 68 | } 69 | 70 | /// Get root path of leetcode-cli 71 | pub fn root() -> Result { 72 | let dir = dirs::home_dir().ok_or(Error::NoneError)?.join(".leetcode"); 73 | if !dir.is_dir() { 74 | info!("Generate root dir at {:?}.", &dir); 75 | fs::DirBuilder::new().recursive(true).create(&dir)?; 76 | } 77 | 78 | Ok(dir) 79 | } 80 | 81 | /// Sync new config to config.toml 82 | pub fn sync(&self) -> Result<()> { 83 | let home = dirs::home_dir().ok_or(Error::NoneError)?; 84 | let conf = home.join(".leetcode/leetcode.toml"); 85 | fs::write(conf, toml::ser::to_string_pretty(&self)?)?; 86 | 87 | Ok(()) 88 | } 89 | } 90 | -------------------------------------------------------------------------------- /flake.lock: -------------------------------------------------------------------------------- 1 | { 2 | "nodes": { 3 | "naersk": { 4 | "inputs": { 5 | "nixpkgs": [ 6 | "nixpkgs" 7 | ] 8 | }, 9 | "locked": { 10 | "lastModified": 1721727458, 11 | "narHash": "sha256-r/xppY958gmZ4oTfLiHN0ZGuQ+RSTijDblVgVLFi1mw=", 12 | "owner": "nix-community", 13 | "repo": "naersk", 14 | "rev": "3fb418eaf352498f6b6c30592e3beb63df42ef11", 15 | "type": "github" 16 | }, 17 | "original": { 18 | "owner": "nix-community", 19 | "repo": "naersk", 20 | "type": "github" 21 | } 22 | }, 23 | "nixpkgs": { 24 | "locked": { 25 | "lastModified": 1728538411, 26 | "narHash": "sha256-f0SBJz1eZ2yOuKUr5CA9BHULGXVSn6miBuUWdTyhUhU=", 27 | "owner": "NixOS", 28 | "repo": "nixpkgs", 29 | "rev": "b69de56fac8c2b6f8fd27f2eca01dcda8e0a4221", 30 | "type": "github" 31 | }, 32 | "original": { 33 | "owner": "NixOS", 34 | "ref": "nixpkgs-unstable", 35 | "repo": "nixpkgs", 36 | "type": "github" 37 | } 38 | }, 39 | "root": { 40 | "inputs": { 41 | "naersk": "naersk", 42 | "nixpkgs": "nixpkgs", 43 | "rust-overlay": "rust-overlay", 44 | "utils": "utils" 45 | } 46 | }, 47 | "rust-overlay": { 48 | "inputs": { 49 | "nixpkgs": [ 50 | "nixpkgs" 51 | ] 52 | }, 53 | "locked": { 54 | "lastModified": 1728700003, 55 | "narHash": "sha256-Ox1pvEHxLK6lAdaKQW21Zvk65SPDag+cD8YA444R/og=", 56 | "owner": "oxalica", 57 | "repo": "rust-overlay", 58 | "rev": "fc1e58ebabe0cef4442eedea07556ff0c9eafcfe", 59 | "type": "github" 60 | }, 61 | "original": { 62 | "owner": "oxalica", 63 | "repo": "rust-overlay", 64 | "type": "github" 65 | } 66 | }, 67 | "systems": { 68 | "locked": { 69 | "lastModified": 1681028828, 70 | "narHash": "sha256-Vy1rq5AaRuLzOxct8nz4T6wlgyUR7zLU309k9mBC768=", 71 | "owner": "nix-systems", 72 | "repo": "default", 73 | "rev": "da67096a3b9bf56a91d16901293e51ba5b49a27e", 74 | "type": "github" 75 | }, 76 | "original": { 77 | "owner": "nix-systems", 78 | "repo": "default", 79 | "type": "github" 80 | } 81 | }, 82 | "utils": { 83 | "inputs": { 84 | "systems": "systems" 85 | }, 86 | "locked": { 87 | "lastModified": 1726560853, 88 | "narHash": "sha256-X6rJYSESBVr3hBoH0WbKE5KvhPU5bloyZ2L4K60/fPQ=", 89 | "owner": "numtide", 90 | "repo": "flake-utils", 91 | "rev": "c1dfcf08411b08f6b8615f7d8971a2bfa81d5e8a", 92 | "type": "github" 93 | }, 94 | "original": { 95 | "owner": "numtide", 96 | "repo": "flake-utils", 97 | "type": "github" 98 | } 99 | } 100 | }, 101 | "root": "root", 102 | "version": 7 103 | } 104 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | ## v0.4.1 2 | 3 | - Search problems by name 4 | - Re-enable chrome plugin 5 | 6 | ## v0.3.3 7 | 8 | - allow more flexible categories by @frrad 9 | - change params type to `Option` @frrad 10 | 11 | ## v0.3.2 12 | 13 | - adds additional tracing by @shmuga 14 | - removes test_mode parameter by @shmuga 15 | 16 | ## v0.3.1 17 | 18 | - pipe handling by @aymanbagabas 19 | - Improve README by @xiaoxiae 20 | 21 | ## v0.3.0 22 | 23 | - Upgrade reqwest to async mode 24 | - Format code using clippy 25 | 26 | ## v0.2.23 27 | 28 | - support color display 29 | 30 | ## v0.2.22 31 | 32 | - Fixed the cache can't update with new added problems 33 | 34 | - Display user friendly errors when pick/edit new added problem. 35 | 36 | - upgrade pyo3 37 | 38 | - fix leetcode list with empty cache 39 | 40 | ## v0.2.21 41 | 42 | - Make programmable support to be an advanced feature 43 | 44 | ## v0.2.20 45 | 46 | - Support sup/sub style for numbers 47 | 48 | ## v0.2.19 49 | 50 | - Better HTML! 51 | 52 | ## v0.2.18 53 | 54 | - Display stdout for test and execute commands, fix minor spacing in results displayed 55 | 56 | - Fix panic on `pick` command without cache 57 | 58 | ## v0.2.17 59 | 60 | Fix panic on stat command with zero numbers 61 | 62 | ## v0.2.16 63 | 64 | Update versions of diesel and reqwest 65 | 66 | ## v0.2.15 67 | 68 | Allow for custom testcases with the `leetcode test` command, and some minor edits 69 | 70 | ## v0.2.14 71 | 72 | Corrects file suffixes for c\*\* and c# files 73 | 74 | ## v0.2.13 75 | 76 | fix percent length panic 77 | 78 | ## v0.2.12 79 | 80 | fix gt || ge || lt || le 81 | 82 | ## v0.2.11 83 | 84 | added code 14 and transfered `&#ge;`、`&#le` and `'`. 85 | 86 | ## v0.2.10 87 | 88 | add code 15 89 | 90 | ## v0.2.9 91 | 92 | update ac status after submit successfully 93 | 94 | ## v0.2.8 95 | 96 | show last testcases 97 | 98 | ## v0.2.7 99 | 100 | fixed float bug in result 101 | 102 | ## v0.2.6 103 | 104 | sync config while change current lang 105 | 106 | ## v0.2.5 107 | 108 | update local cache when submission status changes 109 | 110 | ## v0.2.4 111 | 112 | auto fetch question while exec `edit` directly. 113 | 114 | ## v0.2.3 115 | 116 | Programmable leetcode-cli 117 | 118 | ## v0.2.2 119 | 120 | 1. optimize logs 121 | 2. add tag filter 122 | 3. sync configs 123 | 124 | ## v0.2.1 125 | 126 | 1. fix cookies error handling 127 | 2. dismiss all `unwrap`, `expect` and `panic` 128 | 3. add cookie configs 129 | 130 | ## v0.2.0 131 | 132 | 1. Add Linux Support 133 | 134 | ## v0.1.9 135 | 136 | 1. release submit command 137 | 2. deserialize json using outter funcs 138 | 139 | ## v0.1.8 140 | 141 | 1. pack mod exports 142 | 2. add edit command 143 | 3. add test command 144 | 145 | ## v0.1.7 146 | 147 | render html in command-line, and `pick` command 148 | 149 | ## v0.1.6 150 | 151 | complete `stat` command 152 | 153 | ## v0.1.5 154 | 155 | complete `cache` command 156 | 157 | ## v0.1.3 158 | 159 | complete `list` command 160 | 161 | ## v0.1.2 162 | 163 | abstract data cache 164 | 165 | ## v0.1.1 166 | 167 | add list command 168 | 169 | ## v0.1.0 170 | 171 | chrome cookie 172 | -------------------------------------------------------------------------------- /flake.nix: -------------------------------------------------------------------------------- 1 | { 2 | description = "Leet your code in command-line."; 3 | 4 | inputs = { 5 | nixpkgs.url = "github:NixOS/nixpkgs/nixpkgs-unstable"; 6 | utils.url = "github:numtide/flake-utils"; 7 | 8 | naersk = { 9 | url = "github:nix-community/naersk"; 10 | inputs.nixpkgs.follows = "nixpkgs"; 11 | }; 12 | 13 | rust-overlay = { 14 | url = "github:oxalica/rust-overlay"; 15 | inputs.nixpkgs.follows = "nixpkgs"; 16 | }; 17 | }; 18 | 19 | outputs = { 20 | self, 21 | nixpkgs, 22 | utils, 23 | naersk, 24 | rust-overlay, 25 | ... 26 | }: 27 | utils.lib.eachDefaultSystem (system: 28 | let 29 | overlays = [ (import rust-overlay) ]; 30 | 31 | pkgs = (import nixpkgs) { 32 | inherit system overlays; 33 | }; 34 | 35 | toolchain = pkgs.rust-bin.fromRustupToolchainFile ./rust-toolchain.toml; 36 | 37 | naersk' = pkgs.callPackage naersk { 38 | cargo = toolchain; 39 | rustc = toolchain; 40 | clippy = toolchain; 41 | }; 42 | 43 | nativeBuildInputs = with pkgs; [ 44 | pkg-config 45 | ]; 46 | 47 | darwinBuildInputs = pkgs.lib.optionals pkgs.stdenv.isDarwin [ 48 | pkgs.darwin.apple_sdk.frameworks.Security 49 | pkgs.darwin.apple_sdk.frameworks.SystemConfiguration 50 | ]; 51 | 52 | buildInputs = with pkgs; [ 53 | openssl 54 | dbus 55 | sqlite 56 | ] ++ darwinBuildInputs; 57 | 58 | package = naersk'.buildPackage rec { 59 | pname = "leetcode-cli"; 60 | version = "git"; 61 | 62 | src = ./.; 63 | doCheck = true; # run `cargo test` on build 64 | 65 | inherit buildInputs nativeBuildInputs; 66 | 67 | buildNoDefaultFeatures = true; 68 | 69 | buildFeatures = "git"; 70 | 71 | meta = with pkgs.lib; { 72 | description = "Leet your code in command-line."; 73 | homepage = "https://github.com/clearloop/leetcode-cli"; 74 | licenses = licenses.mit; 75 | maintainers = with maintainers; [ congee ]; 76 | mainProgram = "leetcode"; 77 | }; 78 | 79 | # Env vars 80 | # a nightly compiler is required unless we use this cheat code. 81 | RUSTC_BOOTSTRAP = 1; 82 | 83 | # CFG_RELEASE = "${rustPlatform.rust.rustc.version}-stable"; 84 | CFG_RELEASE_CHANNEL = "stable"; 85 | }; 86 | in 87 | { 88 | defaultPackage = package; 89 | overlay = final: prev: { leetcode-cli = package; }; 90 | 91 | devShell = with pkgs; mkShell { 92 | name = "shell"; 93 | inherit nativeBuildInputs; 94 | 95 | buildInputs = buildInputs ++ [ 96 | toolchain 97 | cargo-edit 98 | cargo-bloat 99 | cargo-audit 100 | cargo-about 101 | cargo-outdated 102 | ]; 103 | 104 | PKG_CONFIG_PATH = "${pkgs.openssl.dev}/lib/pkgconfig"; 105 | RUST_BACKTRACE = "full"; 106 | LD_LIBRARY_PATH = lib.makeLibraryPath buildInputs; 107 | RUSTC_BOOTSTRAP = 1; 108 | CFG_RELEASE_CHANNEL = "stable"; 109 | }; 110 | } 111 | ); 112 | } 113 | -------------------------------------------------------------------------------- /src/err.rs: -------------------------------------------------------------------------------- 1 | //! Errors in leetcode-cli 2 | use crate::cmds::{Command, DataCommand}; 3 | use anyhow::anyhow; 4 | use colored::Colorize; 5 | 6 | #[cfg(debug_assertions)] 7 | const CONFIG: &str = "~/.leetcode/leetcode.tmp.toml"; 8 | #[cfg(not(debug_assertions))] 9 | const CONFIG: &str = "~/.leetcode/leetcode_tmp.toml"; 10 | 11 | /// Leetcode result. 12 | pub type Result = std::result::Result; 13 | 14 | /// Leetcode cli errors 15 | #[derive(thiserror::Error, Debug)] 16 | pub enum Error { 17 | #[error("Nothing matched")] 18 | MatchError, 19 | #[error("Download {0} failed, please try again")] 20 | DownloadError(String), 21 | #[error(transparent)] 22 | Reqwest(#[from] reqwest::Error), 23 | #[error(transparent)] 24 | HeaderName(#[from] reqwest::header::InvalidHeaderName), 25 | #[error(transparent)] 26 | HeaderValue(#[from] reqwest::header::InvalidHeaderValue), 27 | #[error( 28 | "Your leetcode cookies seems expired, \ 29 | {} \ 30 | Either you can handwrite your `LEETCODE_SESSION` and `csrf` into `leetcode.toml`, \ 31 | more info please checkout this: \ 32 | https://github.com/clearloop/leetcode-cli/blob/master/README.md#cookies", 33 | "please make sure you have logined in leetcode.com with chrome. ".yellow().bold() 34 | )] 35 | CookieError, 36 | #[error( 37 | "Your leetcode account lacks a premium subscription, which the given problem requires.\n \ 38 | If this looks like a mistake, please open a new issue at: {}", 39 | "https://github.com/clearloop/leetcode-cli/".underline() 40 | )] 41 | PremiumError, 42 | #[error(transparent)] 43 | Utf8(#[from] std::string::FromUtf8Error), 44 | #[error( 45 | "json from response parse failed, please open a new issue at: {}.", 46 | "https://github.com/clearloop/leetcode-cli/".underline() 47 | )] 48 | NoneError, 49 | #[error( 50 | "Parse config file failed, \ 51 | leetcode-cli has just generated a new leetcode.toml at {}, \ 52 | the current one at {} seems missing some keys, Please compare \ 53 | the new file and add the missing keys.\n", 54 | CONFIG, 55 | "~/.leetcode/leetcode.toml".yellow().bold().underline(), 56 | )] 57 | Config(#[from] toml::de::Error), 58 | #[error("Maybe you not login on the Chrome, you can login and retry")] 59 | ChromeNotLogin, 60 | #[error(transparent)] 61 | ParseInt(#[from] std::num::ParseIntError), 62 | #[error(transparent)] 63 | Json(#[from] serde_json::Error), 64 | #[error(transparent)] 65 | Toml(#[from] toml::ser::Error), 66 | #[error(transparent)] 67 | Io(#[from] std::io::Error), 68 | #[error(transparent)] 69 | Anyhow(#[from] anyhow::Error), 70 | #[error(transparent)] 71 | Keyring(#[from] keyring::Error), 72 | #[error(transparent)] 73 | OpenSSL(#[from] openssl::error::ErrorStack), 74 | #[cfg(feature = "pym")] 75 | #[error(transparent)] 76 | Pyo3(#[from] pyo3::PyErr), 77 | } 78 | 79 | impl std::convert::From for Error { 80 | fn from(err: diesel::result::Error) -> Self { 81 | match err { 82 | diesel::result::Error::NotFound => { 83 | DataCommand::usage().print_help().unwrap_or(()); 84 | Error::Anyhow(anyhow!( 85 | "NotFound, you may update cache, and try it again\r\n" 86 | )) 87 | } 88 | _ => Error::Anyhow(anyhow!("{err}")), 89 | } 90 | } 91 | } 92 | -------------------------------------------------------------------------------- /src/cmds/stat.rs: -------------------------------------------------------------------------------- 1 | //! status command 2 | use super::Command; 3 | use async_trait::async_trait; 4 | use clap::{ArgMatches, Command as ClapCommand}; 5 | use colored::Colorize; 6 | 7 | /// Abstract statues command 8 | /// 9 | /// ```sh 10 | /// leetcode-stat 11 | /// Show simple chart about submissions 12 | /// 13 | /// USAGE: 14 | /// leetcode stat 15 | /// 16 | /// FLAGS: 17 | /// -h, --help Prints help information 18 | /// -V, --version Prints version information 19 | /// ``` 20 | pub struct StatCommand; 21 | 22 | #[async_trait] 23 | impl Command for StatCommand { 24 | /// `stat` usage 25 | fn usage() -> ClapCommand { 26 | ClapCommand::new("stat") 27 | .about("Show simple chart about submissions") 28 | .visible_alias("s") 29 | } 30 | 31 | /// `stat` handler 32 | async fn handler(_m: &ArgMatches) -> Result<(), crate::err::Error> { 33 | use crate::{helper::Digit, Cache}; 34 | 35 | let cache = Cache::new()?; 36 | let res = cache.get_problems()?; 37 | 38 | let mut easy: f64 = 0.00; 39 | let mut easy_ac: f64 = 0.00; 40 | let mut medium: f64 = 0.00; 41 | let mut medium_ac: f64 = 0.00; 42 | let mut hard: f64 = 0.00; 43 | let mut hard_ac: f64 = 0.00; 44 | 45 | for i in res.into_iter() { 46 | match i.level { 47 | 1 => { 48 | easy += 1.00; 49 | if i.status == "ac" { 50 | easy_ac += 1.00; 51 | } 52 | } 53 | 2 => { 54 | medium += 1.00; 55 | if i.status == "ac" { 56 | medium_ac += 1.00; 57 | } 58 | } 59 | 3 => { 60 | hard += 1.00; 61 | if i.status == "ac" { 62 | hard_ac += 1.00; 63 | } 64 | } 65 | _ => {} 66 | } 67 | } 68 | 69 | // level: len = 8 70 | // count: len = 10 71 | // percent: len = 16 72 | // chart: len = 32 73 | // title 74 | println!( 75 | "\n{}", 76 | " Level Count Percent Chart".bright_black() 77 | ); 78 | println!( 79 | "{}", 80 | " -----------------------------------------------------------------".bright_black() 81 | ); 82 | 83 | // lines 84 | for (i, l) in [(easy, easy_ac), (medium, medium_ac), (hard, hard_ac)] 85 | .iter() 86 | .enumerate() 87 | { 88 | match i { 89 | 0 => { 90 | print!(" {}", "Easy".bright_green()); 91 | print!("{}", " ".digit(4)); 92 | } 93 | 1 => { 94 | print!(" {}", "Medium".bright_yellow()); 95 | print!("{}", " ".digit(2)); 96 | } 97 | 2 => { 98 | print!(" {}", "Hard".bright_red()); 99 | print!("{}", " ".digit(4)); 100 | } 101 | _ => continue, 102 | } 103 | 104 | let checked_div = |lhs: f64, rhs: f64| if rhs == 0. { 0. } else { lhs / rhs }; 105 | let count = format!("{}/{}", l.1, l.0); 106 | let pct = format!("( {:.2} %)", checked_div(100.0 * l.1, l.0)); 107 | let mut line = "".to_string(); 108 | line.push_str(&" ".digit(10 - (count.len() as i32))); 109 | line.push_str(&count); 110 | line.push_str(&" ".digit(12 - (pct.len() as i32))); 111 | line.push_str(&pct); 112 | print!("{}", line); 113 | print!(" "); 114 | 115 | let done = "░" 116 | .repeat(checked_div(32.00 * l.1, l.0) as usize) 117 | .bright_green(); 118 | let udone = "░" 119 | .repeat(32 - checked_div(32.00 * l.1, l.0) as usize) 120 | .red(); 121 | print!("{}", done); 122 | println!("{}", udone); 123 | } 124 | println!(); 125 | Ok(()) 126 | } 127 | } 128 | -------------------------------------------------------------------------------- /src/config/sys.rs: -------------------------------------------------------------------------------- 1 | //! System section 2 | //! 3 | //! This section is a set of constants after #88 4 | 5 | use serde::{Deserialize, Serialize}; 6 | 7 | const CATEGORIES: [&str; 4] = ["algorithms", "concurrency", "database", "shell"]; 8 | 9 | // TODO: find a better solution. 10 | fn categories() -> Vec { 11 | CATEGORIES.into_iter().map(|s| s.into()).collect() 12 | } 13 | 14 | /// Leetcode API 15 | #[derive(Clone, Debug, Serialize, Deserialize)] 16 | pub struct Urls { 17 | pub base: String, 18 | pub graphql: String, 19 | pub login: String, 20 | pub problems: String, 21 | pub problem: String, 22 | pub tag: String, 23 | pub test: String, 24 | pub session: String, 25 | pub submit: String, 26 | pub submissions: String, 27 | pub submission: String, 28 | pub verify: String, 29 | pub favorites: String, 30 | pub favorite_delete: String, 31 | } 32 | 33 | impl Default for Urls { 34 | fn default() -> Self { 35 | Self { 36 | base: "https://leetcode.com".into(), 37 | graphql: "https://leetcode.com/graphql".into(), 38 | login: "https://leetcode.com/accounts/login/".into(), 39 | problems: "https://leetcode.com/api/problems/$category/".into(), 40 | problem: "https://leetcode.com/problems/$slug/description/".into(), 41 | tag: "https://leetcode.com/tag/$slug/".into(), 42 | test: "https://leetcode.com/problems/$slug/interpret_solution/".into(), 43 | session: "https://leetcode.com/session/".into(), 44 | submit: "https://leetcode.com/problems/$slug/submit/".into(), 45 | submissions: "https://leetcode.com/submissions/detail/$id/".into(), 46 | submission: "https://leetcode.com/submissions/detail/$id/".into(), 47 | verify: "https://leetcode.com/submissions/detail/$id/check/".into(), 48 | favorites: "https://leetcode.com/list/api/questions".into(), 49 | favorite_delete: "https://leetcode.com/list/api/questions/$hash/$id".into(), 50 | } 51 | } 52 | } 53 | 54 | impl Urls { 55 | pub fn new_with_leetcode_cn() -> Self { 56 | Self { 57 | base: "https://leetcode.cn".into(), 58 | graphql: "https://leetcode.cn/graphql".into(), 59 | login: "https://leetcode.cn/accounts/login/".into(), 60 | problems: "https://leetcode.cn/api/problems/$category/".into(), 61 | problem: "https://leetcode.cn/problems/$slug/description/".into(), 62 | tag: "https://leetcode.cn/tag/$slug/".into(), 63 | test: "https://leetcode.cn/problems/$slug/interpret_solution/".into(), 64 | session: "https://leetcode.cn/session/".into(), 65 | submit: "https://leetcode.cn/problems/$slug/submit/".into(), 66 | submissions: "https://leetcode.cn/submissions/detail/$id/".into(), 67 | submission: "https://leetcode.cn/submissions/detail/$id/".into(), 68 | verify: "https://leetcode.cn/submissions/detail/$id/check/".into(), 69 | favorites: "https://leetcode.cn/list/api/questions".into(), 70 | favorite_delete: "https://leetcode.cn/list/api/questions/$hash/$id".into(), 71 | } 72 | } 73 | 74 | /// problem url with specific `$slug` 75 | pub fn problem(&self, slug: &str) -> String { 76 | self.problem.replace("$slug", slug) 77 | } 78 | 79 | /// problems url with specific `$category` 80 | pub fn problems(&self, category: &str) -> String { 81 | self.problems.replace("$category", category) 82 | } 83 | 84 | /// submit url with specific `$slug` 85 | pub fn submit(&self, slug: &str) -> String { 86 | self.submit.replace("$slug", slug) 87 | } 88 | 89 | /// tag url with specific `$slug` 90 | pub fn tag(&self, slug: &str) -> String { 91 | self.tag.replace("$slug", slug) 92 | } 93 | 94 | /// test url with specific `$slug` 95 | pub fn test(&self, slug: &str) -> String { 96 | self.test.replace("$slug", slug) 97 | } 98 | 99 | /// verify url with specific `$id` 100 | pub fn verify(&self, id: &str) -> String { 101 | self.verify.replace("$id", id) 102 | } 103 | } 104 | 105 | /// System settings, for leetcode api mainly 106 | #[derive(Clone, Debug, Serialize, Deserialize)] 107 | pub struct Sys { 108 | #[serde(default = "categories")] 109 | pub categories: Vec, 110 | #[serde(default)] 111 | pub urls: Urls, 112 | } 113 | 114 | impl Default for Sys { 115 | fn default() -> Self { 116 | Self { 117 | categories: CATEGORIES.into_iter().map(|s| s.into()).collect(), 118 | urls: Default::default(), 119 | } 120 | } 121 | } 122 | -------------------------------------------------------------------------------- /src/plugins/chrome.rs: -------------------------------------------------------------------------------- 1 | use crate::{cache, Error, Result}; 2 | use anyhow::anyhow; 3 | use diesel::prelude::*; 4 | use keyring::Entry; 5 | use openssl::{hash, pkcs5, symm}; 6 | use std::{collections::HashMap, fmt::Display}; 7 | 8 | /// LeetCode Cookies Schema 9 | mod schema { 10 | table! { 11 | cookies (host_key) { 12 | encrypted_value -> Binary, 13 | host_key -> Text, 14 | name -> Text, 15 | } 16 | } 17 | } 18 | 19 | /// Please make sure the order 20 | /// 21 | /// The order between table and struct must be same. 22 | #[derive(Queryable, Debug, Clone)] 23 | struct Cookies { 24 | pub encrypted_value: Vec, 25 | #[allow(dead_code)] 26 | pub host_key: String, 27 | pub name: String, 28 | } 29 | 30 | /// Spawn cookies to cookie format 31 | #[derive(Debug)] 32 | pub struct Ident { 33 | pub csrf: String, 34 | session: String, 35 | } 36 | 37 | impl Display for Ident { 38 | fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { 39 | write!( 40 | f, 41 | "LEETCODE_SESSION={};csrftoken={};", 42 | self.session, self.csrf 43 | ) 44 | } 45 | } 46 | 47 | /// Get cookies from chrome storage 48 | pub fn cookies() -> Result { 49 | let ccfg = crate::config::Config::locate()?.cookies; 50 | if !ccfg.csrf.is_empty() && !ccfg.session.is_empty() { 51 | return Ok(Ident { 52 | csrf: ccfg.csrf, 53 | session: ccfg.session, 54 | }); 55 | } 56 | 57 | // If doesn't config SESSION and csrftoken 58 | use self::schema::cookies::dsl::*; 59 | trace!("Derive cookies from google chrome..."); 60 | 61 | let home = dirs::home_dir().ok_or(Error::NoneError)?; 62 | let p = match std::env::consts::OS { 63 | "macos" => home.join("Library/Application Support/Google/Chrome/Default/Cookies"), 64 | "linux" => home.join(".config/google-chrome/Default/Cookies"), 65 | _ => panic!("Opps...only works on OSX or Linux now..."), 66 | }; 67 | 68 | debug!("Chrome Cookies path is {:?}", &p); 69 | let mut conn = cache::conn(p.to_string_lossy().to_string()); 70 | let res = cookies 71 | .filter(host_key.like(format!("#{}", ccfg.site))) 72 | .load::(&mut conn) 73 | .expect("Loading cookies from google chrome failed."); 74 | 75 | debug!("res {:?}", &res); 76 | if res.is_empty() { 77 | return Err(Error::CookieError); 78 | } 79 | 80 | // Get system password 81 | let ring = Entry::new("Chrome Safe Storage", "Chrome")?; 82 | let pass = ring.get_password().expect("Get Password failed"); 83 | 84 | // Decode cookies 85 | let mut m: HashMap = HashMap::new(); 86 | for c in res { 87 | if (c.name == "csrftoken") || (c.name == "LEETCODE_SESSION") { 88 | m.insert(c.name, decode_cookies(&pass, c.encrypted_value)?); 89 | } 90 | } 91 | 92 | Ok(Ident { 93 | csrf: m.get("csrftoken").ok_or(Error::ChromeNotLogin)?.to_string(), 94 | session: m 95 | .get("LEETCODE_SESSION") 96 | .ok_or(Error::ChromeNotLogin)? 97 | .to_string(), 98 | }) 99 | } 100 | 101 | /// Decode cookies from chrome 102 | fn decode_cookies(pass: &str, v: Vec) -> Result { 103 | let mut key = [0_u8; 16]; 104 | match std::env::consts::OS { 105 | "macos" => { 106 | pkcs5::pbkdf2_hmac( 107 | pass.as_bytes(), 108 | b"saltysalt", 109 | 1003, 110 | hash::MessageDigest::sha1(), 111 | &mut key, 112 | ) 113 | .expect("pbkdf2 hmac went error."); 114 | } 115 | "linux" => { 116 | pkcs5::pbkdf2_hmac( 117 | b"peanuts", 118 | b"saltysalt", 119 | 1, 120 | hash::MessageDigest::sha1(), 121 | &mut key, 122 | ) 123 | .expect("pbkdf2 hmac went error."); 124 | } 125 | _ => return Err(anyhow!("only supports OSX or Linux for now").into()), 126 | } 127 | 128 | chrome_decrypt(v, key) 129 | } 130 | 131 | /// Decrypt chrome cookie value with aes-128-cbc 132 | fn chrome_decrypt(v: Vec, key: [u8; 16]) -> Result { 133 | // : \u16 134 | let iv = vec![32_u8; 16]; 135 | let mut decrypter = symm::Crypter::new( 136 | symm::Cipher::aes_128_cbc(), 137 | symm::Mode::Decrypt, 138 | &key, 139 | Some(&iv), 140 | )?; 141 | 142 | let data_len = v.len() - 3; 143 | let block_size = symm::Cipher::aes_128_cbc().block_size(); 144 | let mut plaintext = vec![0; data_len + block_size]; 145 | 146 | decrypter.pad(false); 147 | 148 | let count = decrypter.update(&v[3..], &mut plaintext)?; 149 | decrypter.finalize(&mut plaintext[count..])?; 150 | plaintext.retain(|x| x >= &20_u8); 151 | 152 | Ok(String::from_utf8_lossy(&plaintext.to_vec()).to_string()) 153 | } 154 | -------------------------------------------------------------------------------- /src/cache/parser.rs: -------------------------------------------------------------------------------- 1 | //! Sub-Module for parsing resp data 2 | use super::models::*; 3 | use serde_json::Value; 4 | 5 | /// problem parser 6 | pub fn problem(problems: &mut Vec, v: Value) -> Option<()> { 7 | let pairs = v.get("stat_status_pairs")?.as_array()?; 8 | for p in pairs { 9 | let stat = p.get("stat")?.as_object()?; 10 | let total_acs = stat.get("total_acs")?.as_f64()? as f32; 11 | let total_submitted = stat.get("total_submitted")?.as_f64()? as f32; 12 | 13 | let fid_obj = stat.get("frontend_question_id")?; 14 | let fid = match fid_obj.as_i64() { 15 | // Handle on leetcode-com 16 | Some(s) => s as i32, 17 | // Handle on leetcode-cn 18 | None => fid_obj.as_str()?.split(' ').last()?.parse::().ok()?, 19 | }; 20 | 21 | problems.push(Problem { 22 | category: v.get("category_slug")?.as_str()?.to_string(), 23 | fid, 24 | id: stat.get("question_id")?.as_i64()? as i32, 25 | level: p.get("difficulty")?.as_object()?.get("level")?.as_i64()? as i32, 26 | locked: p.get("paid_only")?.as_bool()?, 27 | name: stat.get("question__title")?.as_str()?.to_string(), 28 | percent: total_acs / total_submitted * 100.0, 29 | slug: stat.get("question__title_slug")?.as_str()?.to_string(), 30 | starred: p.get("is_favor")?.as_bool()?, 31 | status: p.get("status")?.as_str().unwrap_or("Null").to_string(), 32 | desc: String::new(), 33 | }); 34 | } 35 | 36 | Some(()) 37 | } 38 | 39 | /// desc parser 40 | pub fn desc(q: &mut Question, v: Value) -> Option { 41 | /* None - parsing failed 42 | * Some(false) - content was null (premium?) 43 | * Some(true) - content was parsed 44 | */ 45 | let o = &v 46 | .as_object()? 47 | .get("data")? 48 | .as_object()? 49 | .get("question")? 50 | .as_object()?; 51 | 52 | if *o.get("content")? == Value::Null { 53 | return Some(false); 54 | } 55 | 56 | *q = Question { 57 | content: o.get("content")?.as_str().unwrap_or("").to_string(), 58 | stats: serde_json::from_str(o.get("stats")?.as_str()?).ok()?, 59 | defs: serde_json::from_str(o.get("codeDefinition")?.as_str()?).ok()?, 60 | case: o.get("sampleTestCase")?.as_str()?.to_string(), 61 | all_cases: o 62 | .get("exampleTestcases") 63 | .unwrap_or(o.get("sampleTestCase")?) // soft fail to the sampleTestCase 64 | .as_str()? 65 | .to_string(), 66 | metadata: serde_json::from_str(o.get("metaData")?.as_str()?).ok()?, 67 | test: o.get("enableRunCode")?.as_bool()?, 68 | t_content: o 69 | .get("translatedContent")? 70 | .as_str() 71 | .unwrap_or("") 72 | .to_string(), 73 | }; 74 | 75 | Some(true) 76 | } 77 | 78 | /// tag parser 79 | pub fn tags(v: Value) -> Option> { 80 | trace!("Parse tags..."); 81 | let tag = v.as_object()?.get("data")?.as_object()?.get("topicTag")?; 82 | 83 | if tag.is_null() { 84 | return Some(vec![]); 85 | } 86 | 87 | let arr = tag.as_object()?.get("questions")?.as_array()?; 88 | 89 | let mut res: Vec = vec![]; 90 | for q in arr.iter() { 91 | res.push(q.as_object()?.get("questionId")?.as_str()?.to_string()) 92 | } 93 | 94 | Some(res) 95 | } 96 | 97 | /// daily parser 98 | pub fn daily(v: Value) -> Option { 99 | trace!("Parse daily..."); 100 | let v_obj = v.as_object()?.get("data")?.as_object()?; 101 | match v_obj.get("activeDailyCodingChallengeQuestion") { 102 | // Handle on leetcode-com 103 | Some(v) => v, 104 | // Handle on leetcode-cn 105 | None => v_obj.get("todayRecord")?.as_array()?.first()?, 106 | } 107 | .as_object()? 108 | .get("question")? 109 | .as_object()? 110 | .get("questionFrontendId")? 111 | .as_str()? 112 | .parse() 113 | .ok() 114 | } 115 | 116 | /// user parser 117 | pub fn user(v: Value) -> Option> { 118 | // None => error while parsing 119 | // Some(None) => User not found 120 | // Some("...") => username 121 | let user = v.as_object()?.get("data")?.as_object()?.get("user")?; 122 | if *user == Value::Null { 123 | return Some(None); 124 | } 125 | let user = user.as_object()?; 126 | Some(Some(( 127 | user.get("username")?.as_str()?.to_owned(), 128 | user.get("isCurrentUserPremium")?.as_bool()?, 129 | ))) 130 | } 131 | 132 | pub use ss::ssr; 133 | /// string or squence 134 | mod ss { 135 | use serde::{de, Deserialize, Deserializer}; 136 | use std::fmt; 137 | use std::marker::PhantomData; 138 | 139 | /// de Vec from string or sequence 140 | pub fn ssr<'de, D>(deserializer: D) -> Result, D::Error> 141 | where 142 | D: Deserializer<'de>, 143 | { 144 | struct StringOrVec(PhantomData>); 145 | 146 | impl<'de> de::Visitor<'de> for StringOrVec { 147 | type Value = Vec; 148 | 149 | fn expecting(&self, formatter: &mut fmt::Formatter) -> fmt::Result { 150 | formatter.write_str("string or list of strings") 151 | } 152 | 153 | fn visit_str(self, value: &str) -> Result 154 | where 155 | E: de::Error, 156 | { 157 | Ok(vec![value.to_owned()]) 158 | } 159 | 160 | fn visit_seq(self, visitor: S) -> Result 161 | where 162 | S: de::SeqAccess<'de>, 163 | { 164 | Deserialize::deserialize(de::value::SeqAccessDeserializer::new(visitor)) 165 | } 166 | } 167 | 168 | deserializer.deserialize_any(StringOrVec(PhantomData)) 169 | } 170 | } 171 | -------------------------------------------------------------------------------- /src/lib.rs: -------------------------------------------------------------------------------- 1 | //! # leetcode-cli 2 | //! [![doc](https://img.shields.io/badge/current-docs-green.svg)](https://docs.rs/leetcode-cli/) 3 | //! [![Crates.io](https://img.shields.io/crates/v/leetcode-cli.svg)](https://crates.io/crates/leetcode-cli) 4 | //! [![Crates.io](https://img.shields.io/crates/d/leetcode-cli.svg)](https://crates.io/crates/leetcode-cli) 5 | //! [![LICENSE](https://img.shields.io/crates/l/leetcode-cli.svg)](https://choosealicense.com/licenses/mit/) 6 | //! 7 | //! ## Installing 8 | //! 9 | //! ```sh 10 | //! cargo install leetcode-cli 11 | //! ``` 12 | //! 13 | //! ## Usage 14 | //! 15 | //! **Please make sure you have logined in `leetcode.com` with `chrome`**, more info plz checkout [this](#cookies) 16 | //! 17 | //! ```sh 18 | //! leetcode 0.3.10 19 | //! May the Code be with You 👻 20 | //! 21 | //! USAGE: 22 | //! leetcode [FLAGS] [SUBCOMMAND] 23 | //! 24 | //! FLAGS: 25 | //! -d, --debug debug mode 26 | //! -h, --help Prints help information 27 | //! -V, --version Prints version information 28 | //! 29 | //! SUBCOMMANDS: 30 | //! data Manage Cache [aliases: d] 31 | //! edit Edit question by id [aliases: e] 32 | //! exec Submit solution [aliases: x] 33 | //! list List problems [aliases: l] 34 | //! pick Pick a problem [aliases: p] 35 | //! stat Show simple chart about submissions [aliases: s] 36 | //! test Edit question by id [aliases: t] 37 | //! help Prints this message or the help of the given subcommand(s) 38 | //! ``` 39 | //! 40 | //! ## Example 41 | //! 42 | //! For example, if your config is: 43 | //! 44 | //! ```toml 45 | //! [code] 46 | //! lang = "rust" 47 | //! editor = "emacs" 48 | //! ``` 49 | //! 50 | //! #### 1. pick 51 | //! 52 | //! ```sh 53 | //! leetcode pick 1 54 | //! ``` 55 | //! 56 | //! ```sh 57 | //! [1] Two Sum is on the run... 58 | //! 59 | //! 60 | //! Given an array of integers, return indices of the two numbers such that they add up to a specific target. 61 | //! 62 | //! You may assume that each input would have exactly one solution, and you may not use the same element twice. 63 | //! 64 | //! -------------------------------------------------- 65 | //! 66 | //! Example: 67 | //! 68 | //! 69 | //! Given nums = [2, 7, 11, 15], target = 9, 70 | //! 71 | //! Because nums[0] + nums[1] = 2 + 7 = 9, 72 | //! return [0, 1]. 73 | //! ``` 74 | //! 75 | //! #### 2. edit 76 | //! 77 | //! ```sh 78 | //! leetcode edit 1 79 | //! ``` 80 | //! 81 | //! ```rust 82 | //! # struct Solution; 83 | //! impl Solution { 84 | //! pub fn two_sum(nums: Vec, target: i32) -> Vec { 85 | //! use std::collections::HashMap; 86 | //! let mut m: HashMap = HashMap::new(); 87 | //! 88 | //! for (i, e) in nums.iter().enumerate() { 89 | //! if let Some(v) = m.get(&(target - e)) { 90 | //! return vec![*v, i as i32]; 91 | //! } 92 | //! 93 | //! m.insert(*e, i as i32).unwrap_or_default(); 94 | //! } 95 | //! 96 | //! return vec![]; 97 | //! } 98 | //! } 99 | //! ``` 100 | //! 101 | //! #### 3. test 102 | //! 103 | //! ```sh 104 | //! leetcode test 1 105 | //! ``` 106 | //! 107 | //! ```sh 108 | //! 109 | //! Accepted Runtime: 0 ms 110 | //! 111 | //! Your input: [2,7,11,15], 9 112 | //! Output: [0,1] 113 | //! Expected: [0,1] 114 | //! 115 | //! ``` 116 | //! 117 | //! #### 4. submit 118 | //! 119 | //! ```sh 120 | //! leetcode submit 1 121 | //! ``` 122 | //! 123 | //! ```sh 124 | //! 125 | //! Success 126 | //! 127 | //! Runtime: 0 ms, faster than 100% of Rustonline submissions for Two Sum. 128 | //! 129 | //! Memory Usage: 2.4 MB, less than 100% of Rustonline submissions for Two Sum. 130 | //! 131 | //! 132 | //! ``` 133 | //! 134 | //! ## Cookies 135 | //! 136 | //! The cookie plugin of leetcode-cil can work on OSX and [Linux][#1], **If you are on other platforms or your cookies just don't want to be catched**, you can handwrite your LeetCode Cookies to `~/.leetcode/leetcode.toml` 137 | //! 138 | //! ```toml 139 | //! # Make sure `leetcode.toml` file is placed at `~/.leetcode/leetcode.toml` 140 | //! [cookies] 141 | //! csrf = "..." 142 | //! session = "..." 143 | //! ``` 144 | //! 145 | //! For Example, if you're using chrome to login to leetcode.com. 146 | //! 147 | //! 148 | //! #### Step 1 149 | //! 150 | //! Open chrome and paste the link below to the `chrome linkbar`. 151 | //! 152 | //! ```sh 153 | //! chrome://settings/cookies/detail?site=leetcode.com 154 | //! ``` 155 | //! 156 | //! #### Step 2 157 | //! 158 | //! Copy the contents of `LEETCODE_SESSION` and `csrftoken`. 159 | //! 160 | //! #### Step 3 161 | //! 162 | //! Paste them to `session` and `csrf`. 163 | //! 164 | //! ```toml 165 | //! # Make sure `leetcode.toml` file is placed at `~/.leetcode/leetcode.toml` 166 | //! [cookies] 167 | //! csrf = "${csrftoken}" 168 | //! session = "${LEETCODE_SESSION}" 169 | //! ``` 170 | //! 171 | //! 172 | //! ## Programmable 173 | //! 174 | //! If we want to filter leetcode questions using our own python scripts, what should we do? 175 | //! 176 | //! For example, our config is: 177 | //! 178 | //! ```toml 179 | //! # Make sure `leetcode.toml` file is placed at `~/.leetcode/leetcode.toml` 180 | //! [storage] 181 | //! scripts = "scripts" 182 | //! ``` 183 | //! 184 | //! We write our python scripts: 185 | //! 186 | //! ```python 187 | //! # ~/.leetcode/scripts/plan1.py 188 | //! import json; 189 | //! 190 | //! def plan(sps, stags): 191 | //! ## 192 | //! # `print` in python is supported, 193 | //! # if you want to know the data structures of these two args, 194 | //! # just print them 195 | //! ## 196 | //! problems = json.loads(sps) 197 | //! tags = json.loads(stags) 198 | //! 199 | //! ret = [] 200 | //! tm = {} 201 | //! for tag in tags: 202 | //! tm[tag["tag"]] = tag["refs"]; 203 | //! 204 | //! for i in problems: 205 | //! if i["level"] == 1 and str(i["id"]) in tm["linked-list"]: 206 | //! ret.append(str(i["id"])) 207 | //! 208 | //! # return is `List[string]` 209 | //! return ret 210 | //! ``` 211 | //! 212 | //! Then we can run filter as what we write now: 213 | //! 214 | //! ```sh 215 | //! leetcode list -p plan1 216 | //! ``` 217 | //! 218 | //! Well done, enjoy it! 219 | //! 220 | //! 221 | //! ## PR 222 | //! 223 | //! PR is welcome, [here][pr] it is. 224 | //! 225 | //! ## LICENSE 226 | //! MIT 227 | //! 228 | //! 229 | //! [pr]: https://github.com/clearloop/leetcode-cli/pulls 230 | //! [#1]: https://github.com/clearloop/leetcode-cli/issues/1 231 | #[macro_use] 232 | extern crate log; 233 | #[macro_use] 234 | extern crate diesel; 235 | 236 | // show docs 237 | pub mod cache; 238 | pub mod cli; 239 | pub mod cmds; 240 | pub mod config; 241 | pub mod err; 242 | pub mod flag; 243 | pub mod helper; 244 | pub mod plugins; 245 | #[cfg(feature = "pym")] 246 | pub mod pym; 247 | 248 | // re-exports 249 | pub use cache::Cache; 250 | pub use config::Config; 251 | pub use err::{Error, Result}; 252 | -------------------------------------------------------------------------------- /src/helper.rs: -------------------------------------------------------------------------------- 1 | //! A set of helper traits 2 | pub use self::{ 3 | digit::Digit, 4 | file::{code_path, load_script, test_cases_path}, 5 | filter::{filter, squash}, 6 | html::HTML, 7 | }; 8 | 9 | /// Convert i32 to specific digits string. 10 | mod digit { 11 | /// Abstract Digit trait, fill the empty space to specific length. 12 | pub trait Digit { 13 | fn digit(self, d: T) -> String; 14 | } 15 | 16 | impl Digit for i32 { 17 | fn digit(self, d: i32) -> String { 18 | let mut s = self.to_string(); 19 | let space = " ".repeat((d as usize) - s.len()); 20 | s.push_str(&space); 21 | 22 | s 23 | } 24 | } 25 | 26 | impl Digit for String { 27 | fn digit(self, d: i32) -> String { 28 | let mut s = self.clone(); 29 | let space = " ".repeat((d as usize) - self.len()); 30 | s.push_str(&space); 31 | 32 | s 33 | } 34 | } 35 | 36 | impl Digit for &'static str { 37 | fn digit(self, d: i32) -> String { 38 | let mut s = self.to_string(); 39 | let space = " ".repeat((d as usize) - self.len()); 40 | s.push_str(&space); 41 | 42 | s 43 | } 44 | } 45 | } 46 | 47 | /// Question filter tool 48 | mod filter { 49 | use crate::cache::models::Problem; 50 | /// Abstract query filter 51 | /// 52 | /// ```sh 53 | /// -q, --query Filter questions by conditions: 54 | /// Uppercase means negative 55 | /// e = easy E = m+h 56 | /// m = medium M = e+h 57 | /// h = hard H = e+m 58 | /// d = done D = not done 59 | /// l = locked L = not locked 60 | /// s = starred S = not starred 61 | /// ``` 62 | pub fn filter(ps: &mut Vec, query: String) { 63 | for p in query.chars() { 64 | match p { 65 | 'l' => ps.retain(|x| x.locked), 66 | 'L' => ps.retain(|x| !x.locked), 67 | 's' => ps.retain(|x| x.starred), 68 | 'S' => ps.retain(|x| !x.starred), 69 | 'e' => ps.retain(|x| x.level == 1), 70 | 'E' => ps.retain(|x| x.level != 1), 71 | 'm' => ps.retain(|x| x.level == 2), 72 | 'M' => ps.retain(|x| x.level != 2), 73 | 'h' => ps.retain(|x| x.level == 3), 74 | 'H' => ps.retain(|x| x.level != 3), 75 | 'd' => ps.retain(|x| x.status == "ac"), 76 | 'D' => ps.retain(|x| x.status != "ac"), 77 | _ => {} 78 | } 79 | } 80 | } 81 | 82 | /// Squash questions and ids 83 | pub fn squash(ps: &mut Vec, ids: Vec) -> crate::Result<()> { 84 | use std::collections::HashMap; 85 | 86 | let mut map: HashMap = HashMap::new(); 87 | ids.iter().for_each(|x| { 88 | map.insert(x.to_string(), true).unwrap_or_default(); 89 | }); 90 | 91 | ps.retain(|x| map.contains_key(&x.id.to_string())); 92 | Ok(()) 93 | } 94 | } 95 | 96 | pub fn superscript(n: u8) -> String { 97 | match n { 98 | x if x >= 10 => format!("{}{}", superscript(n / 10), superscript(n % 10)), 99 | 0 => "⁰".to_string(), 100 | 1 => "¹".to_string(), 101 | 2 => "²".to_string(), 102 | 3 => "³".to_string(), 103 | 4 => "⁴".to_string(), 104 | 5 => "⁵".to_string(), 105 | 6 => "⁶".to_string(), 106 | 7 => "⁷".to_string(), 107 | 8 => "⁸".to_string(), 108 | 9 => "⁹".to_string(), 109 | _ => n.to_string(), 110 | } 111 | } 112 | 113 | pub fn subscript(n: u8) -> String { 114 | match n { 115 | x if x >= 10 => format!("{}{}", subscript(n / 10), subscript(n % 10)), 116 | 0 => "₀".to_string(), 117 | 1 => "₁".to_string(), 118 | 2 => "₂".to_string(), 119 | 3 => "₃".to_string(), 120 | 4 => "₄".to_string(), 121 | 5 => "₅".to_string(), 122 | 6 => "₆".to_string(), 123 | 7 => "₇".to_string(), 124 | 8 => "₈".to_string(), 125 | 9 => "₉".to_string(), 126 | _ => n.to_string(), 127 | } 128 | } 129 | 130 | /// Render html to command-line 131 | mod html { 132 | use crate::helper::{subscript, superscript}; 133 | use regex::Captures; 134 | use scraper::Html; 135 | 136 | /// Html render plugin 137 | pub trait HTML { 138 | fn render(&self) -> String; 139 | } 140 | 141 | impl HTML for String { 142 | fn render(&self) -> String { 143 | let sup_re = regex::Regex::new(r"(?P[0-9]*)").unwrap(); 144 | let sub_re = regex::Regex::new(r"(?P[0-9]*)").unwrap(); 145 | 146 | let res = sup_re.replace_all(self, |cap: &Captures| { 147 | let num: u8 = cap["num"].to_string().parse().unwrap(); 148 | superscript(num) 149 | }); 150 | 151 | let res = sub_re.replace_all(&res, |cap: &Captures| { 152 | let num: u8 = cap["num"].to_string().parse().unwrap(); 153 | subscript(num) 154 | }); 155 | 156 | let frag = Html::parse_fragment(&res); 157 | 158 | let res = frag 159 | .root_element() 160 | .text() 161 | .fold(String::new(), |acc, e| acc + e); 162 | 163 | res 164 | } 165 | } 166 | } 167 | 168 | mod file { 169 | /// Convert file suffix from language type 170 | pub fn suffix(l: &str) -> crate::Result<&'static str> { 171 | match l { 172 | "bash" => Ok("sh"), 173 | "c" => Ok("c"), 174 | "cpp" => Ok("cpp"), 175 | "csharp" => Ok("cs"), 176 | "elixir" => Ok("ex"), 177 | "golang" => Ok("go"), 178 | "java" => Ok("java"), 179 | "javascript" => Ok("js"), 180 | "kotlin" => Ok("kt"), 181 | "mysql" => Ok("sql"), 182 | "php" => Ok("php"), 183 | "python" => Ok("py"), 184 | "python3" => Ok("py"), 185 | "ruby" => Ok("rb"), 186 | "rust" => Ok("rs"), 187 | "scala" => Ok("scala"), 188 | "swift" => Ok("swift"), 189 | "typescript" => Ok("ts"), 190 | _ => Ok("c"), 191 | } 192 | } 193 | 194 | use crate::{cache::models::Problem, Error}; 195 | 196 | /// Generate test cases path by fid 197 | pub fn test_cases_path(problem: &Problem) -> crate::Result { 198 | let conf = crate::config::Config::locate()?; 199 | let mut path = format!("{}/{}.tests.dat", conf.storage.code()?, conf.code.pick); 200 | 201 | path = path.replace("${fid}", &problem.fid.to_string()); 202 | path = path.replace("${slug}", &problem.slug.to_string()); 203 | Ok(path) 204 | } 205 | 206 | /// Generate code path by fid 207 | pub fn code_path(problem: &Problem, l: Option) -> crate::Result { 208 | let conf = crate::config::Config::locate()?; 209 | let mut lang = conf.code.lang; 210 | if l.is_some() { 211 | lang = l.ok_or(Error::NoneError)?; 212 | } 213 | 214 | let mut path = format!( 215 | "{}/{}.{}", 216 | conf.storage.code()?, 217 | conf.code.pick, 218 | suffix(&lang)?, 219 | ); 220 | 221 | path = path.replace("${fid}", &problem.fid.to_string()); 222 | path = path.replace("${slug}", &problem.slug.to_string()); 223 | 224 | Ok(path) 225 | } 226 | 227 | /// Load python scripts 228 | pub fn load_script(module: &str) -> crate::Result { 229 | use std::fs::File; 230 | use std::io::Read; 231 | let conf = crate::config::Config::locate()?; 232 | let mut script = "".to_string(); 233 | File::open(format!("{}/{}.py", conf.storage.scripts()?, module))? 234 | .read_to_string(&mut script)?; 235 | 236 | Ok(script) 237 | } 238 | } 239 | -------------------------------------------------------------------------------- /src/cmds/edit.rs: -------------------------------------------------------------------------------- 1 | //! Edit command 2 | use super::Command; 3 | use crate::{Error, Result}; 4 | use anyhow::anyhow; 5 | use async_trait::async_trait; 6 | use clap::{Arg, ArgAction, ArgGroup, ArgMatches, Command as ClapCommand}; 7 | use std::collections::HashMap; 8 | 9 | /// Abstract `edit` command 10 | /// 11 | /// ```sh 12 | /// leetcode-edit 13 | /// Edit question by id 14 | /// 15 | /// USAGE: 16 | /// leetcode edit 17 | /// 18 | /// FLAGS: 19 | /// -h, --help Prints help information 20 | /// -V, --version Prints version information 21 | /// 22 | /// ARGS: 23 | /// question id 24 | /// ``` 25 | pub struct EditCommand; 26 | 27 | #[async_trait] 28 | impl Command for EditCommand { 29 | /// `edit` usage 30 | fn usage() -> ClapCommand { 31 | ClapCommand::new("edit") 32 | .about("Edit question") 33 | .visible_alias("e") 34 | .arg( 35 | Arg::new("lang") 36 | .short('l') 37 | .long("lang") 38 | .num_args(1) 39 | .help("Edit with specific language"), 40 | ) 41 | .arg( 42 | Arg::new("id") 43 | .num_args(1) 44 | .value_parser(clap::value_parser!(i32)) 45 | .help("question id"), 46 | ) 47 | .arg( 48 | Arg::new("daily") 49 | .short('d') 50 | .long("daily") 51 | .help("Edit today's daily challenge") 52 | .action(ArgAction::SetTrue), 53 | ) 54 | .group( 55 | ArgGroup::new("question-id") 56 | .args(["id", "daily"]) 57 | .multiple(false) 58 | .required(true), 59 | ) 60 | } 61 | 62 | /// `edit` handler 63 | async fn handler(m: &ArgMatches) -> Result<()> { 64 | use crate::{cache::models::Question, Cache}; 65 | use std::fs::File; 66 | use std::io::Write; 67 | use std::path::Path; 68 | 69 | let cache = Cache::new()?; 70 | 71 | let daily = m.get_one::("daily").unwrap_or(&false); 72 | let daily_id = if *daily { 73 | Some(cache.get_daily_problem_id().await?) 74 | } else { 75 | None 76 | }; 77 | 78 | let id = m 79 | .get_one::("id") 80 | .copied() 81 | .or(daily_id) 82 | .ok_or(Error::NoneError)?; 83 | 84 | let problem = cache.get_problem(id)?; 85 | let mut conf = cache.to_owned().0.conf; 86 | 87 | let test_flag = conf.code.test; 88 | 89 | let p_desc_comment = problem.desc_comment(&conf); 90 | // condition language 91 | if m.contains_id("lang") { 92 | conf.code.lang = m 93 | .get_one::("lang") 94 | .ok_or(Error::NoneError)? 95 | .to_string(); 96 | conf.sync()?; 97 | } 98 | 99 | let lang = &conf.code.lang; 100 | let path = crate::helper::code_path(&problem, Some(lang.to_owned()))?; 101 | 102 | if !Path::new(&path).exists() { 103 | let mut qr = serde_json::from_str(&problem.desc); 104 | if qr.is_err() { 105 | qr = Ok(cache.get_question(id).await?); 106 | } 107 | 108 | let question: Question = qr?; 109 | 110 | let mut file_code = File::create(&path)?; 111 | let question_desc = question.desc_comment(&conf) + "\n"; 112 | 113 | let test_path = crate::helper::test_cases_path(&problem)?; 114 | 115 | let mut flag = false; 116 | for d in question.defs.0 { 117 | if d.value == *lang { 118 | flag = true; 119 | if conf.code.comment_problem_desc { 120 | file_code.write_all(p_desc_comment.as_bytes())?; 121 | file_code.write_all(question_desc.as_bytes())?; 122 | } 123 | if let Some(inject_before) = &conf.code.inject_before { 124 | for line in inject_before { 125 | file_code.write_all((line.to_string() + "\n").as_bytes())?; 126 | } 127 | } 128 | if conf.code.edit_code_marker { 129 | file_code.write_all( 130 | (conf.code.comment_leading.clone() 131 | + " " 132 | + &conf.code.start_marker 133 | + "\n") 134 | .as_bytes(), 135 | )?; 136 | } 137 | file_code.write_all((d.code.to_string() + "\n").as_bytes())?; 138 | if conf.code.edit_code_marker { 139 | file_code.write_all( 140 | (conf.code.comment_leading.clone() 141 | + " " 142 | + &conf.code.end_marker 143 | + "\n") 144 | .as_bytes(), 145 | )?; 146 | } 147 | if let Some(inject_after) = &conf.code.inject_after { 148 | for line in inject_after { 149 | file_code.write_all((line.to_string() + "\n").as_bytes())?; 150 | } 151 | } 152 | 153 | if test_flag { 154 | let mut file_tests = File::create(&test_path)?; 155 | file_tests.write_all(question.all_cases.as_bytes())?; 156 | } 157 | } 158 | } 159 | 160 | // if language is not found in the list of supported languges clean up files 161 | if !flag { 162 | std::fs::remove_file(&path)?; 163 | 164 | if test_flag { 165 | std::fs::remove_file(&test_path)?; 166 | } 167 | 168 | return Err( 169 | anyhow!("This question doesn't support {lang}, please try another").into(), 170 | ); 171 | } 172 | } 173 | 174 | // Get arguments of the editor 175 | // 176 | // for example: 177 | // 178 | // ```toml 179 | // [code] 180 | // editor = "emacsclient" 181 | // editor_args = [ "-n", "-s", "doom" ] 182 | // ``` 183 | // 184 | // ```rust 185 | // Command::new("emacsclient").args(&[ "-n", "-s", "doom", "" ]) 186 | // ``` 187 | let mut args: Vec = Default::default(); 188 | if let Some(editor_args) = conf.code.editor_args { 189 | args.extend_from_slice(&editor_args); 190 | } 191 | 192 | // Set environment variables for editor 193 | // 194 | // for example: 195 | // 196 | // ```toml 197 | // [code] 198 | // editor = "nvim" 199 | // editor_envs = [ "XDG_DATA_HOME=...", "XDG_CONFIG_HOME=...", "XDG_STATE_HOME=..." ] 200 | // ``` 201 | // 202 | // ```rust 203 | // Command::new("nvim").envs(&[ ("XDG_DATA_HOME", "..."), ("XDG_CONFIG_HOME", "..."), ("XDG_STATE_HOME", "..."), ]); 204 | // ``` 205 | let mut envs: HashMap = Default::default(); 206 | if let Some(editor_envs) = &conf.code.editor_envs { 207 | for env in editor_envs.iter() { 208 | let parts: Vec<&str> = env.split('=').collect(); 209 | if parts.len() == 2 { 210 | let name = parts[0].trim(); 211 | let value = parts[1].trim(); 212 | envs.insert(name.to_string(), value.to_string()); 213 | } else { 214 | return Err(anyhow!("Invalid editor environment variable: {env}").into()); 215 | } 216 | } 217 | } 218 | 219 | args.push(path); 220 | std::process::Command::new(conf.code.editor) 221 | .envs(envs) 222 | .args(args) 223 | .status()?; 224 | Ok(()) 225 | } 226 | } 227 | -------------------------------------------------------------------------------- /src/cmds/pick.rs: -------------------------------------------------------------------------------- 1 | //! Pick command 2 | use super::Command; 3 | use crate::cache::models::Problem; 4 | use crate::err::Error; 5 | use async_trait::async_trait; 6 | use clap::{Arg, ArgAction, ArgMatches, Command as ClapCommand}; 7 | /// Abstract pick command 8 | /// 9 | /// ```sh 10 | /// leetcode-pick 11 | /// Pick a problem 12 | /// 13 | /// USAGE: 14 | /// leetcode pick [OPTIONS] [id] 15 | /// 16 | /// FLAGS: 17 | /// -h, --help Prints help information 18 | /// -V, --version Prints version information 19 | /// 20 | /// OPTIONS: 21 | /// -q, --query Filter questions by conditions: 22 | /// Uppercase means negative 23 | /// e = easy E = m+h 24 | /// m = medium M = e+h 25 | /// h = hard H = e+m 26 | /// d = done D = not done 27 | /// l = locked L = not locked 28 | /// s = starred S = not starred 29 | /// 30 | /// ARGS: 31 | /// Problem id 32 | /// ``` 33 | pub struct PickCommand; 34 | 35 | static QUERY_HELP: &str = r#"Filter questions by conditions: 36 | Uppercase means negative 37 | e = easy E = m+h 38 | m = medium M = e+h 39 | h = hard H = e+m 40 | d = done D = not done 41 | l = locked L = not locked 42 | s = starred S = not starred"#; 43 | 44 | #[async_trait] 45 | impl Command for PickCommand { 46 | /// `pick` usage 47 | fn usage() -> ClapCommand { 48 | ClapCommand::new("pick") 49 | .about("Pick a problem") 50 | .visible_alias("p") 51 | .arg( 52 | Arg::new("name") 53 | .short('n') 54 | .long("name") 55 | .value_parser(clap::value_parser!(String)) 56 | .help("Problem name") 57 | .num_args(1), 58 | ) 59 | .arg( 60 | Arg::new("id") 61 | .value_parser(clap::value_parser!(i32)) 62 | .help("Problem id") 63 | .num_args(1), 64 | ) 65 | .arg( 66 | Arg::new("plan") 67 | .short('p') 68 | .long("plan") 69 | .num_args(1) 70 | .help("Invoking python scripts to filter questions"), 71 | ) 72 | .arg( 73 | Arg::new("query") 74 | .short('q') 75 | .long("query") 76 | .num_args(1) 77 | .help(QUERY_HELP), 78 | ) 79 | .arg( 80 | Arg::new("tag") 81 | .short('t') 82 | .long("tag") 83 | .num_args(1) 84 | .help("Filter questions by tag"), 85 | ) 86 | .arg( 87 | Arg::new("daily") 88 | .short('d') 89 | .long("daily") 90 | .help("Pick today's daily challenge") 91 | .action(ArgAction::SetTrue), 92 | ) 93 | } 94 | 95 | /// `pick` handler 96 | async fn handler(m: &ArgMatches) -> Result<(), Error> { 97 | use crate::cache::Cache; 98 | use rand::Rng; 99 | 100 | let cache = Cache::new()?; 101 | let mut problems = cache.get_problems()?; 102 | if problems.is_empty() { 103 | cache.download_problems().await?; 104 | Self::handler(m).await?; 105 | return Ok(()); 106 | } 107 | 108 | // filtering... 109 | // pym scripts 110 | #[cfg(feature = "pym")] 111 | { 112 | if m.contains_id("plan") { 113 | let ids = crate::pym::exec(m.get_one::("plan").unwrap_or(&"".to_string()))?; 114 | crate::helper::squash(&mut problems, ids)?; 115 | } 116 | } 117 | 118 | // tag filter 119 | if m.contains_id("tag") { 120 | let ids = cache 121 | .clone() 122 | .get_tagged_questions(m.get_one::("tag").unwrap_or(&"".to_string())) 123 | .await?; 124 | crate::helper::squash(&mut problems, ids)?; 125 | } 126 | 127 | // query filter 128 | if m.contains_id("query") { 129 | let query = m.get_one::("query").ok_or(Error::NoneError)?; 130 | crate::helper::filter(&mut problems, query.to_string()); 131 | } 132 | 133 | let daily = m.get_one::("daily").unwrap_or(&false); 134 | let daily_id = if *daily { 135 | Some(cache.get_daily_problem_id().await?) 136 | } else { 137 | None 138 | }; 139 | 140 | let fid = match m.contains_id("name") { 141 | // check for name specified, or closest name 142 | true => { 143 | match m.get_one::("name") { 144 | Some(quesname) => closest_named_problem(&problems, quesname).unwrap_or(1), 145 | None => { 146 | // Pick random without specify id 147 | let problem = &problems[rand::rng().random_range(0..problems.len())]; 148 | problem.fid 149 | } 150 | } 151 | } 152 | false => { 153 | m.get_one::("id") 154 | .copied() 155 | .or(daily_id) 156 | .unwrap_or_else(|| { 157 | // Pick random without specify id 158 | let problem = &problems[rand::rng().random_range(0..problems.len())]; 159 | problem.fid 160 | }) 161 | } 162 | }; 163 | 164 | let r = cache.get_question(fid).await; 165 | 166 | match r { 167 | Ok(q) => println!("{}", q.desc()), 168 | Err(e) => { 169 | eprintln!("{:?}", e); 170 | if let Error::Reqwest(_) = e { 171 | Self::handler(m).await?; 172 | } 173 | } 174 | } 175 | 176 | Ok(()) 177 | } 178 | } 179 | 180 | // Returns the closest problem according to a scoring algorithm 181 | // taking into account both the longest common subsequence and the size 182 | // problem string (to compensate for smaller strings having smaller lcs). 183 | // Returns None if there are no problems in the problem list 184 | fn closest_named_problem(problems: &Vec, lookup_name: &str) -> Option { 185 | let max_name_size: usize = problems.iter().map(|p| p.name.len()).max()?; 186 | // Init table to the max name length of all the problems to share 187 | // the same table allocation 188 | let mut table: Vec = vec![0; (max_name_size + 1) * (lookup_name.len() + 1)]; 189 | 190 | // this is guaranteed because of the earlier max None propegation 191 | assert!(!problems.is_empty()); 192 | let mut max_score = 0; 193 | let mut current_problem = &problems[0]; 194 | for problem in problems { 195 | // In case bug becomes bugged, always return the matching string 196 | if problem.name == lookup_name { 197 | return Some(problem.fid); 198 | } 199 | 200 | let this_lcs = longest_common_subsequence(&mut table, &problem.name, lookup_name); 201 | let this_score = this_lcs * (max_name_size - problem.name.len()); 202 | 203 | if this_score > max_score { 204 | max_score = this_score; 205 | current_problem = problem; 206 | } 207 | } 208 | 209 | Some(current_problem.fid) 210 | } 211 | 212 | // Longest commong subsequence DP approach O(nm) space and time. Table must be at least 213 | // (text1.len() + 1) * (text2.len() + 1) length or greater and is mutated every call 214 | fn longest_common_subsequence(table: &mut [usize], text1: &str, text2: &str) -> usize { 215 | assert!(table.len() >= (text1.len() + 1) * (text2.len() + 1)); 216 | let height: usize = text1.len() + 1; 217 | let width: usize = text2.len() + 1; 218 | 219 | // initialize base cases to 0 220 | for i in 0..height { 221 | table[i * width + (width - 1)] = 0; 222 | } 223 | for j in 0..width { 224 | table[((height - 1) * width) + j] = 0; 225 | } 226 | 227 | let mut i: usize = height - 1; 228 | let mut j: usize; 229 | for c0 in text1.chars().rev() { 230 | i -= 1; 231 | j = width - 1; 232 | for c1 in text2.chars().rev() { 233 | j -= 1; 234 | if c0.to_lowercase().next() == c1.to_lowercase().next() { 235 | table[i * width + j] = 1 + table[(i + 1) * width + j + 1]; 236 | } else { 237 | let a = table[(i + 1) * width + j]; 238 | let b = table[i * width + j + 1]; 239 | table[i * width + j] = std::cmp::max(a, b); 240 | } 241 | } 242 | } 243 | table[0] 244 | } 245 | -------------------------------------------------------------------------------- /src/cmds/list.rs: -------------------------------------------------------------------------------- 1 | //! list subcomAmand - List leetcode problems 2 | //! 3 | //! ```sh 4 | //! leetcode-list 5 | //! List problems 6 | //! 7 | //! USAGE: 8 | //! leetcode list [FLAGS] [OPTIONS] [keyword] 9 | //! 10 | //! FLAGS: 11 | //! -h, --help Prints help information 12 | //! -s, --stat Show statistics of listed problems 13 | //! -V, --version Prints version information 14 | //! 15 | //! OPTIONS: 16 | //! -c, --category Filter problems by category name 17 | //! [algorithms, database, shell, concurrency] 18 | //! -q, --query Filter questions by conditions: 19 | //! Uppercase means negative 20 | //! e = easy E = m+h 21 | //! m = medium M = e+h 22 | //! h = hard H = e+m 23 | //! d = done D = not done 24 | //! l = locked L = not locked 25 | //! s = starred S = not starred 26 | //! 27 | //! ARGS: 28 | //! Keyword in select query 29 | //! 30 | //! EXAMPLES: 31 | //! leetcode list List all questions 32 | //! leetcode list array List questions that has "array" in name 33 | //! leetcode list -c database List questions that in database category 34 | //! leetcode list -q eD List questions that with easy level and not done 35 | //! ``` 36 | use super::Command; 37 | use crate::{cache::Cache, err::Error, helper::Digit}; 38 | use async_trait::async_trait; 39 | use clap::{Arg, ArgAction, ArgMatches, Command as ClapCommand}; 40 | /// Abstract `list` command 41 | /// 42 | /// ## handler 43 | /// + try to request cache 44 | /// + prints the list 45 | /// + if cache doesn't exist, download problems list 46 | /// + ... 47 | pub struct ListCommand; 48 | 49 | static CATEGORY_HELP: &str = r#"Filter problems by category name 50 | [algorithms, database, shell, concurrency] 51 | "#; 52 | 53 | static QUERY_HELP: &str = r#"Filter questions by conditions: 54 | Uppercase means negative 55 | e = easy E = m+h 56 | m = medium M = e+h 57 | h = hard H = e+m 58 | d = done D = not done 59 | l = locked L = not locked 60 | s = starred S = not starred"#; 61 | 62 | static LIST_AFTER_HELP: &str = r#"EXAMPLES: 63 | leetcode list List all questions 64 | leetcode list array List questions that has "array" in name, and this is letter non-sensitive 65 | leetcode list -c database List questions that in database category 66 | leetcode list -q eD List questions that with easy level and not done 67 | leetcode list -t linked-list List questions that under tag "linked-list" 68 | leetcode list -r 50 100 List questions that has id in between 50 and 100 69 | "#; 70 | 71 | /// implement Command trait for `list` 72 | #[async_trait] 73 | impl Command for ListCommand { 74 | /// `list` command usage 75 | fn usage() -> ClapCommand { 76 | ClapCommand::new("list") 77 | .about("List problems") 78 | .visible_alias("l") 79 | .arg( 80 | Arg::new("category") 81 | .short('c') 82 | .long("category") 83 | .num_args(1) 84 | .help(CATEGORY_HELP), 85 | ) 86 | .arg( 87 | Arg::new("plan") 88 | .short('p') 89 | .long("plan") 90 | .num_args(1) 91 | .help("Invoking python scripts to filter questions"), 92 | ) 93 | .arg( 94 | Arg::new("query") 95 | .short('q') 96 | .long("query") 97 | .num_args(1) 98 | .help(QUERY_HELP), 99 | ) 100 | .arg( 101 | Arg::new("range") 102 | .short('r') 103 | .long("range") 104 | .num_args(2..) 105 | .value_parser(clap::value_parser!(i32)) 106 | .help("Filter questions by id range"), 107 | ) 108 | .after_help(LIST_AFTER_HELP) 109 | .arg( 110 | Arg::new("stat") 111 | .short('s') 112 | .long("stat") 113 | .help("Show statistics of listed problems") 114 | .action(ArgAction::SetTrue), 115 | ) 116 | .arg( 117 | Arg::new("tag") 118 | .short('t') 119 | .long("tag") 120 | .num_args(1) 121 | .help("Filter questions by tag"), 122 | ) 123 | .arg( 124 | Arg::new("keyword") 125 | .num_args(1) 126 | .help("Keyword in select query"), 127 | ) 128 | } 129 | 130 | /// `list` command handler 131 | /// 132 | /// List commands contains "-c", "-q", "-s" flags. 133 | /// + matches with `-c` will override the default keyword. 134 | /// + `-qs` 135 | async fn handler(m: &ArgMatches) -> Result<(), Error> { 136 | trace!("Input list command..."); 137 | 138 | let cache = Cache::new()?; 139 | let mut ps = cache.get_problems()?; 140 | 141 | // if cache doesn't exist, request a new copy 142 | if ps.is_empty() { 143 | cache.download_problems().await?; 144 | return Self::handler(m).await; 145 | } 146 | 147 | // filtering... 148 | // pym scripts 149 | #[cfg(feature = "pym")] 150 | { 151 | if m.contains_id("plan") { 152 | let ids = crate::pym::exec(m.get_one::("plan").unwrap_or(&"".to_string()))?; 153 | crate::helper::squash(&mut ps, ids)?; 154 | } 155 | } 156 | 157 | // filter tag 158 | if m.contains_id("tag") { 159 | let ids = cache 160 | .get_tagged_questions(m.get_one::("tag").unwrap_or(&"".to_string())) 161 | .await?; 162 | crate::helper::squash(&mut ps, ids)?; 163 | } 164 | 165 | // filter category 166 | if m.contains_id("category") { 167 | ps.retain(|x| { 168 | x.category 169 | == *m 170 | .get_one::("category") 171 | .unwrap_or(&"algorithms".to_string()) 172 | }); 173 | } 174 | 175 | // filter query 176 | if m.contains_id("query") { 177 | let query = m.get_one::("query").ok_or(Error::NoneError)?; 178 | crate::helper::filter(&mut ps, query.to_string()); 179 | } 180 | 181 | // filter range 182 | if m.contains_id("range") { 183 | let num_range: Vec = m 184 | .get_many::("range") 185 | .ok_or(Error::NoneError)? 186 | .copied() 187 | .collect(); 188 | ps.retain(|x| num_range[0] <= x.fid && x.fid <= num_range[1]); 189 | } 190 | 191 | // retain if keyword exists 192 | if let Some(keyword) = m.get_one::("keyword") { 193 | let lowercase_kw = keyword.to_lowercase(); 194 | ps.retain(|x| x.name.to_lowercase().contains(&lowercase_kw)); 195 | } 196 | 197 | // output problem lines sorted by [problem number] like 198 | // [ 1 ] Two Sum 199 | // [ 2 ] Add Two Numbers 200 | ps.sort_unstable_by_key(|p| p.fid); 201 | 202 | let out: Vec = ps.iter().map(ToString::to_string).collect(); 203 | println!("{}", out.join("\n")); 204 | 205 | // one more thing, filter stat 206 | if m.contains_id("stat") { 207 | let mut listed = 0; 208 | let mut locked = 0; 209 | let mut starred = 0; 210 | let mut ac = 0; 211 | let mut notac = 0; 212 | let mut easy = 0; 213 | let mut medium = 0; 214 | let mut hard = 0; 215 | 216 | for p in ps { 217 | listed += 1; 218 | if p.starred { 219 | starred += 1; 220 | } 221 | if p.locked { 222 | locked += 1; 223 | } 224 | 225 | match p.status.as_str() { 226 | "ac" => ac += 1, 227 | "notac" => notac += 1, 228 | _ => {} 229 | } 230 | 231 | match p.level { 232 | 1 => easy += 1, 233 | 2 => medium += 1, 234 | 3 => hard += 1, 235 | _ => {} 236 | } 237 | } 238 | 239 | let remain = listed - ac - notac; 240 | println!( 241 | " 242 | Listed: {} Locked: {} Starred: {} 243 | Accept: {} Not-Ac: {} Remain: {} 244 | Easy : {} Medium: {} Hard: {}", 245 | listed.digit(4), 246 | locked.digit(4), 247 | starred.digit(4), 248 | ac.digit(4), 249 | notac.digit(4), 250 | remain.digit(4), 251 | easy.digit(4), 252 | medium.digit(4), 253 | hard.digit(4), 254 | ); 255 | } 256 | Ok(()) 257 | } 258 | } 259 | -------------------------------------------------------------------------------- /tests/de.rs: -------------------------------------------------------------------------------- 1 | use leetcode_cli::cache::models::VerifyResult; 2 | 3 | #[test] 4 | fn de_from_test_success() { 5 | let r: Result = serde_json::from_str( 6 | r#"{"status_code": 10, "lang": "rust", "run_success": true, "status_runtime": "0 ms", "memory": 2040000, "code_answer": ["[0,1]"], "code_output": [], "elapsed_time": 0, "task_finish_time": 1578201833478, "expected_status_code": 10, "expected_lang": "cpp", "expected_run_success": true, "expected_status_runtime": "0", "expected_memory": 8296000, "expected_code_answer": ["[0,1]"], "expected_code_output": [], "expected_elapsed_time": 20, "expected_task_finish_time": 1578201003754, "correct_answer": true, "total_correct": null, "total_testcases": null, "runtime_percentile": null, "status_memory": "2 MB", "memory_percentile": null, "pretty_lang": "Rust", "submission_id": "runcode_1578201829.4103167_XbDDrj9Ihb", "status_msg": "Accepted", "state": "SUCCESS"}"#, 7 | ); 8 | assert!(r.is_ok()); 9 | } 10 | 11 | #[test] 12 | fn de_from_test_wrong() { 13 | let r: Result = serde_json::from_str( 14 | r#"{"status_code": 10, "lang": "rust", "run_success": true, "status_runtime": "0 ms", "memory": 2040000, "code_answer": ["[0,1]"], "code_output": [], "elapsed_time": 0, "task_finish_time": 1578201833478, "expected_status_code": 10, "expected_lang": "cpp", "expected_run_success": true, "expected_status_runtime": "0", "expected_memory": 8296000, "expected_code_answer": ["[0,1]"], "expected_code_output": [], "expected_elapsed_time": 20, "expected_task_finish_time": 1578201003754, "correct_answer": false, "total_correct": null, "total_testcases": null, "runtime_percentile": null, "status_memory": "2 MB", "memory_percentile": null, "pretty_lang": "Rust", "submission_id": "runcode_1578201829.4103167_XbDDrj9Ihb", "status_msg": "Accepted", "state": "SUCCESS"}"#, 15 | ); 16 | assert!(r.is_ok()); 17 | } 18 | 19 | #[test] 20 | fn de_from_submit_success() { 21 | let r: Result = serde_json::from_str( 22 | r#"{"status_code": 10, "lang": "rust", "run_success": true, "status_runtime": "0 ms", "memory": 2300000, "question_id": "1", "elapsed_time": 0, "compare_result": "11111111111111111111111111111", "code_output": "", "std_output": "", "last_testcase": "", "task_finish_time": 1578193674018, "total_correct": 29, "total_testcases": 29, "runtime_percentile": 100, "status_memory": "2.3 MB", "memory_percentile": 100, "pretty_lang": "Rust", "submission_id": "291285717", "status_msg": "Accepted", "state": "SUCCESS"}"#, 23 | ); 24 | assert!(r.is_ok()); 25 | } 26 | 27 | #[test] 28 | fn de_from_float_pencentile() { 29 | let r: Result = serde_json::from_str( 30 | r#"{"status_code": 10, "lang": "rust", "run_success": true, "status_runtime": "4 ms", "memory": 2716000, "question_id": "203", "elapsed_time": 0, "compare_result": "11111111111111111111111111111111111111111111111111111111111111111", "code_output": "", "std_output": "", "last_testcase": "", "task_finish_time": 1578590021187, "total_correct": 65, "total_testcases": 65, "runtime_percentile": 76.9231, "status_memory": "2.7 MB", "memory_percentile": 100, "pretty_lang": "Rust", "submission_id": "292701790", "status_msg": "Accepted", "state": "SUCCESS"}"#, 31 | ); 32 | assert!(r.is_ok()); 33 | } 34 | 35 | #[test] 36 | fn de_from_failed_tests() { 37 | let r: Result = serde_json::from_str( 38 | r#"{"status_code": 11, "lang": "rust", "run_success": true, "status_runtime": "4 ms", "memory": 2716000, "question_id": "203", "elapsed_time": 0, "compare_result": "11111111111111111111111111111111111111111111111111111111111011111", "code_output": "", "std_output": "", "last_testcase": "[1, 2, 3]", "task_finish_time": 1578590021187, "total_correct": 65, "total_testcases": 65, "runtime_percentile": 76.9231, "status_memory": "2.7 MB", "memory_percentile": 100, "pretty_lang": "Rust", "submission_id": "292701790", "status_msg": "Failed", "state": "SUCCESS"}"#, 39 | ); 40 | assert!(r.is_ok()); 41 | } 42 | 43 | #[test] 44 | fn de_from_test_limit_exceed() { 45 | let r: Result = serde_json::from_str( 46 | r#"{"status_code": 13, "lang": "rust", "run_success": false, "status_runtime": "N/A", "memory": 2048000, "code_answer": [], "code_output": ["ever in loop.", "ever in loop.", "ever in loop.", "ever in loop.", "ever in loop.", "ever in loop.", "ever in loop.", "ever in loop.", "ever in loop.", "ever in loop.", "ever in loop.", "ever in loop.", "ever in loop.", "ever in loop.", "ever in loop.", "ever in loop.", "ever in loop.", "ever in loop.", "ever in loop.", "ever in loop.", "ever in loop.", "ever in loop.", "ever in loop.", "ever in loop.", "ever in loop.", "ever in loop.", "ever in loop.", "ever in loop.", "ever in loop.", "ever in loop.", "ever in loop.", "ever in loop.", "ever in loop.", "ever in loop.", "ever in loop.", "ever in loop.", "ever in loop.", "ever in loop.", "ever in loop.", "ever in loop.", "ever in loop.", "ever in loop.", "ever in loop.", "ever in loop.", "ever in loop.", "ever in loop.", "ever in loop.", "ever in loop.", "ever in loop.", "ever in loop.", "ever in loop.", "ever in loop.", "ever in loop.", "ever in loop.", "ever in loop.", "ever in loop.", "ever in loop.", "ever in loop.", "ever in loop.", "ever in loop.", "ever in loop.", "ever in loop.", "ever in loop.", "ever in loop.", "ever in loop.", "ever in loop.", "ever in loop.", "ever in loop.", "ever in loop.", "ever in loop.", "ever in loop.", "ever in loop.", "ever in loop.", "ever in loop.", "ever in loop.", "ever in loop.", "ever in loop.", "ever in loop.", "ever in loop.", "ever in loop.", "ever in loop.", "ever in loop.", "ever in loop.", "ever in loop.", "ever in loop.", "ever in loop.", "ever in loop.", "ever in loop.", "ever in loop.", "ever in loop.", "ever in loop.", "ever in loop.", "ever in loop.", "ever in loop.", "ever in loop.", "ever in loop.", "ever in loop.", "ever in loop.", "ever in loop.", "ever in loop.", "... 374392 more lines"], "elapsed_time": 0, "task_finish_time": 1578215847058, "total_correct": null, "total_testcases": null, "runtime_percentile": null, "status_memory": "N/A", "memory_percentile": null, "pretty_lang": "Rust", "submission_id": "runcode_1578215840.1441765_Bj7ADXgHrl", "status_msg": "Output Limit Exceeded", "state": "SUCCESS"}"#, 47 | ); 48 | assert!(r.is_ok()); 49 | } 50 | 51 | #[test] 52 | fn de_from_test_limit_exceed_2() { 53 | let r: Result = serde_json::from_str( 54 | r#"{"status_code": 13, "lang": "rust", "run_success": false, "status_runtime": "N/A", "memory": 2048000, "code_answer": [], "code_output": ["ever in loop.", "ever in loop.", "ever in loop.", "ever in loop.", "ever in loop.", "ever in loop.", "ever in loop.", "ever in loop.", "ever in loop.", "ever in loop.", "ever in loop.", "ever in loop.", "ever in loop.", "ever in loop.", "ever in loop.", "ever in loop.", "ever in loop.", "ever in loop.", "ever in loop.", "ever in loop.", "ever in loop.", "ever in loop.", "ever in loop.", "ever in loop.", "ever in loop.", "ever in loop.", "ever in loop.", "ever in loop.", "ever in loop.", "ever in loop.", "ever in loop.", "ever in loop.", "ever in loop.", "ever in loop.", "ever in loop.", "ever in loop.", "ever in loop.", "ever in loop.", "ever in loop.", "ever in loop.", "ever in loop.", "ever in loop.", "ever in loop.", "ever in loop.", "ever in loop.", "ever in loop.", "ever in loop.", "ever in loop.", "ever in loop.", "ever in loop.", "ever in loop.", "ever in loop.", "ever in loop.", "ever in loop.", "ever in loop.", "ever in loop.", "ever in loop.", "ever in loop.", "ever in loop.", "ever in loop.", "ever in loop.", "ever in loop.", "ever in loop.", "ever in loop.", "ever in loop.", "ever in loop.", "ever in loop.", "ever in loop.", "ever in loop.", "ever in loop.", "ever in loop.", "ever in loop.", "ever in loop.", "ever in loop.", "ever in loop.", "ever in loop.", "ever in loop.", "ever in loop.", "ever in loop.", "ever in loop.", "ever in loop.", "ever in loop.", "ever in loop.", "ever in loop.", "ever in loop.", "ever in loop.", "ever in loop.", "ever in loop.", "ever in loop.", "ever in loop.", "ever in loop.", "ever in loop.", "ever in loop.", "ever in loop.", "ever in loop.", "ever in loop.", "ever in loop.", "ever in loop.", "ever in loop.", "ever in loop.", "... 374392 more lines"], "elapsed_time": 0, "task_finish_time": 1578215847058, "total_correct": null, "total_testcases": null, "runtime_percentile": null, "status_memory": "N/A", "memory_percentile": null, "pretty_lang": "Rust", "submission_id": "runcode_1578215840.1441765_Bj7ADXgHrl", "status_msg": "Output Limit Exceeded", "state": "SUCCESS"}"#, 55 | ); 56 | assert!(r.is_ok()); 57 | } 58 | 59 | #[test] 60 | fn de_runtime_error() { 61 | let r: Result = serde_json::from_str( 62 | r#"{"status_code": 15, "lang": "rust", "run_success": false, "status_runtime": "N/A", "memory": 2048000, "code_answer": [], "code_output": [""], "elapsed_time": 0, "task_finish_time": 1578215847058, "total_correct": null, "total_testcases": null, "runtime_percentile": null, "status_memory": "N/A", "memory_percentile": null, "pretty_lang": "Rust", "submission_id": "runcode_1578215840.1441765_Bj7ADXgHrl", "status_msg": "Runtime Error", "state": "SUCCESS"}"#, 63 | ); 64 | assert!(r.is_ok()); 65 | } 66 | 67 | #[test] 68 | fn de_compile_error() { 69 | let r: Result = serde_json::from_str( 70 | r#"{"status_code": 20, "lang": "rust", "run_success": false, "status_runtime": "N/A", "memory": 2048000, "code_answer": [], "code_output": [""], "elapsed_time": 0, "task_finish_time": 1578215847058, "total_correct": null, "total_testcases": null, "runtime_percentile": null, "status_memory": "N/A", "memory_percentile": null, "pretty_lang": "Rust", "submission_id": "runcode_1578215840.1441765_Bj7ADXgHrl", "status_msg": "Compile Error", "full_compile_error": "I'm error", "state": "SUCCESS"}"#, 71 | ); 72 | assert!(r.is_ok()); 73 | } 74 | 75 | #[test] 76 | fn de_unknown_error() { 77 | let r: Result = serde_json::from_str( 78 | r#"{"status_code": 42, "lang": "rust", "run_success": false, "status_runtime": "N/A", "memory": 2048000, "code_answer": [], "code_output": [""], "elapsed_time": 0, "task_finish_time": 1578215847058, "total_correct": null, "total_testcases": null, "runtime_percentile": null, "status_memory": "N/A", "memory_percentile": null, "pretty_lang": "Rust", "submission_id": "runcode_1578215840.1441765_Bj7ADXgHrl", "status_msg": "Compile Error", "full_compile_error": "I'm error", "state": "SUCCESS"}"#, 79 | ); 80 | assert!(r.is_ok()); 81 | } 82 | -------------------------------------------------------------------------------- /src/plugins/leetcode.rs: -------------------------------------------------------------------------------- 1 | use self::req::{Json, Mode, Req}; 2 | use crate::{ 3 | config::{self, Config}, 4 | Result, 5 | }; 6 | use reqwest::{ 7 | header::{HeaderMap, HeaderName, HeaderValue}, 8 | Client, ClientBuilder, Response, 9 | }; 10 | use std::{collections::HashMap, str::FromStr, time::Duration}; 11 | 12 | /// LeetCode API set 13 | #[derive(Clone)] 14 | pub struct LeetCode { 15 | pub conf: Config, 16 | client: Client, 17 | default_headers: HeaderMap, 18 | } 19 | 20 | impl LeetCode { 21 | /// Parse reqwest headers 22 | fn headers(mut headers: HeaderMap, ts: Vec<(&str, &str)>) -> Result { 23 | for (k, v) in ts.into_iter() { 24 | let name = HeaderName::from_str(k)?; 25 | let value = HeaderValue::from_str(v)?; 26 | headers.insert(name, value); 27 | } 28 | 29 | Ok(headers) 30 | } 31 | 32 | /// New LeetCode client 33 | pub fn new() -> Result { 34 | let conf = config::Config::locate()?; 35 | let (cookie, csrf) = if conf.cookies.csrf.is_empty() || conf.cookies.session.is_empty() { 36 | let cookies = super::chrome::cookies()?; 37 | (cookies.to_string(), cookies.csrf) 38 | } else { 39 | (conf.cookies.clone().to_string(), conf.cookies.clone().csrf) 40 | }; 41 | let default_headers = LeetCode::headers( 42 | HeaderMap::new(), 43 | vec![ 44 | ("Cookie", &cookie), 45 | ("x-csrftoken", &csrf), 46 | ("x-requested-with", "XMLHttpRequest"), 47 | ("Origin", &conf.sys.urls.base), 48 | ], 49 | )?; 50 | 51 | let client = ClientBuilder::new() 52 | .gzip(true) 53 | .connect_timeout(Duration::from_secs(30)) 54 | .build()?; 55 | 56 | Ok(LeetCode { 57 | conf, 58 | client, 59 | default_headers, 60 | }) 61 | } 62 | 63 | /// Get category problems 64 | pub async fn get_category_problems(self, category: &str) -> Result { 65 | trace!("Requesting {} problems...", &category); 66 | let url = &self.conf.sys.urls.problems(category); 67 | 68 | Req { 69 | default_headers: self.default_headers, 70 | refer: None, 71 | info: false, 72 | json: None, 73 | mode: Mode::Get, 74 | name: "get_category_problems", 75 | url: url.to_string(), 76 | } 77 | .send(&self.client) 78 | .await 79 | } 80 | 81 | pub async fn get_question_ids_by_tag(self, slug: &str) -> Result { 82 | trace!("Requesting {} ref problems...", &slug); 83 | let url = &self.conf.sys.urls.graphql; 84 | let mut json: Json = HashMap::new(); 85 | json.insert("operationName", "getTopicTag".to_string()); 86 | json.insert("variables", r#"{"slug": "$slug"}"#.replace("$slug", slug)); 87 | json.insert( 88 | "query", 89 | ["query getTopicTag($slug: String!) {", 90 | " topicTag(slug: $slug) {", 91 | " questions {", 92 | " questionId", 93 | " }", 94 | " }", 95 | "}"] 96 | .join("\n"), 97 | ); 98 | 99 | Req { 100 | default_headers: self.default_headers, 101 | refer: Some(self.conf.sys.urls.tag(slug)), 102 | info: false, 103 | json: Some(json), 104 | mode: Mode::Post, 105 | name: "get_question_ids_by_tag", 106 | url: (*url).to_string(), 107 | } 108 | .send(&self.client) 109 | .await 110 | } 111 | 112 | pub async fn get_user_info(self) -> Result { 113 | trace!("Requesting user info..."); 114 | let url = &self.conf.sys.urls.graphql; 115 | let mut json: Json = HashMap::new(); 116 | json.insert("operationName", "a".to_string()); 117 | json.insert( 118 | "query", 119 | "query a { 120 | user { 121 | username 122 | isCurrentUserPremium 123 | } 124 | }" 125 | .to_owned(), 126 | ); 127 | 128 | Req { 129 | default_headers: self.default_headers, 130 | refer: None, 131 | info: false, 132 | json: Some(json), 133 | mode: Mode::Post, 134 | name: "get_user_info", 135 | url: (*url).to_string(), 136 | } 137 | .send(&self.client) 138 | .await 139 | } 140 | 141 | /// Get daily problem 142 | pub async fn get_question_daily(self) -> Result { 143 | trace!("Requesting daily problem..."); 144 | let url = &self.conf.sys.urls.graphql; 145 | let mut json: Json = HashMap::new(); 146 | 147 | match self.conf.cookies.site { 148 | config::LeetcodeSite::LeetcodeCom => { 149 | json.insert("operationName", "daily".to_string()); 150 | json.insert( 151 | "query", 152 | ["query daily {", 153 | " activeDailyCodingChallengeQuestion {", 154 | " question {", 155 | " questionFrontendId", 156 | " }", 157 | " }", 158 | "}"] 159 | .join("\n"), 160 | ); 161 | } 162 | config::LeetcodeSite::LeetcodeCn => { 163 | json.insert("operationName", "questionOfToday".to_string()); 164 | json.insert( 165 | "query", 166 | ["query questionOfToday {", 167 | " todayRecord {", 168 | " question {", 169 | " questionFrontendId", 170 | " }", 171 | " }", 172 | "}"] 173 | .join("\n"), 174 | ); 175 | } 176 | } 177 | 178 | Req { 179 | default_headers: self.default_headers, 180 | refer: None, 181 | info: false, 182 | json: Some(json), 183 | mode: Mode::Post, 184 | name: "get_question_daily", 185 | url: (*url).to_string(), 186 | } 187 | .send(&self.client) 188 | .await 189 | } 190 | 191 | /// Get specific problem detail 192 | pub async fn get_question_detail(self, slug: &str) -> Result { 193 | trace!("Requesting {} detail...", &slug); 194 | let refer = self.conf.sys.urls.problem(slug); 195 | let mut json: Json = HashMap::new(); 196 | json.insert( 197 | "query", 198 | ["query getQuestionDetail($titleSlug: String!) {", 199 | " question(titleSlug: $titleSlug) {", 200 | " content", 201 | " stats", 202 | " codeDefinition", 203 | " sampleTestCase", 204 | " exampleTestcases", 205 | " enableRunCode", 206 | " metaData", 207 | " translatedContent", 208 | " }", 209 | "}"] 210 | .join("\n"), 211 | ); 212 | 213 | json.insert( 214 | "variables", 215 | r#"{"titleSlug": "$titleSlug"}"#.replace("$titleSlug", slug), 216 | ); 217 | 218 | json.insert("operationName", "getQuestionDetail".to_string()); 219 | 220 | Req { 221 | default_headers: self.default_headers, 222 | refer: Some(refer), 223 | info: false, 224 | json: Some(json), 225 | mode: Mode::Post, 226 | name: "get_problem_detail", 227 | url: self.conf.sys.urls.graphql, 228 | } 229 | .send(&self.client) 230 | .await 231 | } 232 | 233 | /// Send code to judge 234 | pub async fn run_code(self, j: Json, url: String, refer: String) -> Result { 235 | info!("Sending code to judge..."); 236 | Req { 237 | default_headers: self.default_headers, 238 | refer: Some(refer), 239 | info: false, 240 | json: Some(j), 241 | mode: Mode::Post, 242 | name: "run_code", 243 | url, 244 | } 245 | .send(&self.client) 246 | .await 247 | } 248 | 249 | /// Get the result of submission / testing 250 | pub async fn verify_result(self, id: String) -> Result { 251 | trace!("Verifying result..."); 252 | let url = self.conf.sys.urls.verify(&id); 253 | 254 | Req { 255 | default_headers: self.default_headers, 256 | refer: None, 257 | info: false, 258 | json: None, 259 | mode: Mode::Get, 260 | name: "verify_result", 261 | url, 262 | } 263 | .send(&self.client) 264 | .await 265 | } 266 | } 267 | 268 | /// Sub-module for leetcode, simplify requests 269 | mod req { 270 | use super::LeetCode; 271 | use crate::err::Error; 272 | use reqwest::{header::HeaderMap, Client, Response}; 273 | use std::collections::HashMap; 274 | 275 | /// Standardize json format 276 | pub type Json = HashMap<&'static str, String>; 277 | 278 | /// Standardize request mode 279 | pub enum Mode { 280 | Get, 281 | Post, 282 | } 283 | 284 | /// LeetCode request prototype 285 | pub struct Req { 286 | pub default_headers: HeaderMap, 287 | pub refer: Option, 288 | pub json: Option, 289 | pub info: bool, 290 | pub mode: Mode, 291 | pub name: &'static str, 292 | pub url: String, 293 | } 294 | 295 | impl Req { 296 | pub async fn send(self, client: &Client) -> Result { 297 | trace!("Running leetcode::{}...", &self.name); 298 | if self.info { 299 | info!("{}", &self.name); 300 | } 301 | let url = self.url.to_owned(); 302 | let headers = LeetCode::headers( 303 | self.default_headers, 304 | vec![("Referer", &self.refer.unwrap_or(url))], 305 | )?; 306 | 307 | let req = match self.mode { 308 | Mode::Get => client.get(&self.url), 309 | Mode::Post => client.post(&self.url).json(&self.json), 310 | }; 311 | 312 | Ok(req.headers(headers).send().await?) 313 | } 314 | } 315 | } 316 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # leetcode-cli 2 | 3 | ![Rust](https://github.com/clearloop/leetcode-cli/workflows/leetcode-cli/badge.svg) 4 | [![crate](https://img.shields.io/crates/v/leetcode-cli.svg)](https://crates.io/crates/leetcode-cli) 5 | [![doc](https://img.shields.io/badge/current-docs-brightgreen.svg)](https://docs.rs/leetcode-cli/) 6 | [![downloads](https://img.shields.io/crates/d/leetcode-cli.svg)](https://crates.io/crates/leetcode-cli) 7 | [![telegram](https://img.shields.io/badge/telegram-blue?logo=telegram)](https://t.me/+U_5si6PhWykxZTI1) 8 | [![LICENSE](https://img.shields.io/crates/l/leetcode-cli.svg)](https://choosealicense.com/licenses/mit/) 9 | 10 | ## Installing 11 | 12 | ```sh 13 | # Required dependencies: 14 | # 15 | # gcc 16 | # libssl-dev 17 | # libdbus-1-dev 18 | # libsqlite3-dev 19 | 20 | cargo install leetcode-cli 21 | ``` 22 | 23 |
24 | Shell completions 25 | 26 | For Bash and Zsh (by default picks up `$SHELL` from environment) 27 | 28 | ```sh 29 | eval "$(leetcode completions)" 30 | ``` 31 | 32 | Copy the line above to `.bash_profile` or `.zshrc` 33 | 34 | You may also obtain specific shell configuration using. 35 | 36 | ```sh 37 | leetcode completions fish 38 | ``` 39 | 40 | If no argument is provided, the shell is inferred from the `SHELL` environment variable. 41 | 42 |
43 | 44 | ## Usage 45 | 46 | **Make sure you have logged in to `leetcode.com` with `Firefox`**. See [Cookies](#cookies) for why you need to do this first. 47 | 48 | ```sh 49 | leetcode 0.4.0 50 | May the Code be with You 👻 51 | 52 | USAGE: 53 | leetcode [FLAGS] [SUBCOMMAND] 54 | 55 | FLAGS: 56 | -d, --debug debug mode 57 | -h, --help Prints help information 58 | -V, --version Prints version information 59 | 60 | SUBCOMMANDS: 61 | data Manage Cache [aliases: d] 62 | edit Edit question by id [aliases: e] 63 | exec Submit solution [aliases: x] 64 | list List problems [aliases: l] 65 | pick Pick a problem [aliases: p] 66 | stat Show simple chart about submissions [aliases: s] 67 | test Test question by id [aliases: t] 68 | help Prints this message or the help of the given subcommand(s) 69 | ``` 70 | 71 | ## Example 72 | 73 | To configure leetcode-cli, create a file at `~/.leetcode/leetcode.toml`): 74 | 75 | ```toml 76 | [code] 77 | editor = 'emacs' 78 | # Optional parameter 79 | editor_args = ['-nw'] 80 | # Optional environment variables (ex. [ "XDG_DATA_HOME=...", "XDG_CONFIG_HOME=...", "XDG_STATE_HOME=..." ]) 81 | editor_envs = [] 82 | lang = 'rust' 83 | edit_code_marker = false 84 | start_marker = "" 85 | end_marker = "" 86 | # if include problem description 87 | comment_problem_desc = false 88 | # comment syntax 89 | comment_leading = "" 90 | test = true 91 | 92 | [cookies] 93 | csrf = '' 94 | session = '' 95 | # leetcode.com or leetcode.cn 96 | site = "leetcode.com" 97 | 98 | [storage] 99 | cache = 'Problems' 100 | code = 'code' 101 | root = '~/.leetcode' 102 | scripts = 'scripts' 103 | ``` 104 | 105 |
106 | Configuration Explanation 107 | 108 | ```toml 109 | [code] 110 | editor = 'emacs' 111 | # Optional parameter 112 | editor_args = ['-nw'] 113 | # Optional environment variables (ex. [ "XDG_DATA_HOME=...", "XDG_CONFIG_HOME=...", "XDG_STATE_HOME=..." ]) 114 | editor_envs = [] 115 | lang = 'rust' 116 | edit_code_marker = true 117 | start_marker = "start_marker" 118 | end_marker = "end_marker" 119 | # if include problem description 120 | comment_problem_desc = true 121 | # comment syntax 122 | comment_leading = "//" 123 | test = true 124 | 125 | [cookies] 126 | csrf = '' 127 | session = '' 128 | 129 | [storage] 130 | cache = 'Problems' 131 | code = 'code' 132 | root = '~/.leetcode' 133 | scripts = 'scripts' 134 | ``` 135 | 136 | If we change the configuration as shown previously, we will get the following code after `leetcode edit 15`. 137 | 138 | ```rust 139 | // Category: algorithms 140 | // Level: Medium 141 | // Percent: 32.90331% 142 | 143 | // 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. 144 | // 145 | // Notice that the solution set must not contain duplicate triplets. 146 | // 147 | //   148 | // Example 1: 149 | // 150 | // Input: nums = [-1,0,1,2,-1,-4] 151 | // Output: [[-1,-1,2],[-1,0,1]] 152 | // Explanation: 153 | // nums[0] + nums[1] + nums[2] = (-1) + 0 + 1 = 0. 154 | // nums[1] + nums[2] + nums[4] = 0 + 1 + (-1) = 0. 155 | // nums[0] + nums[3] + nums[4] = (-1) + 2 + (-1) = 0. 156 | // The distinct triplets are [-1,0,1] and [-1,-1,2]. 157 | // Notice that the order of the output and the order of the triplets does not matter. 158 | // 159 | // 160 | // Example 2: 161 | // 162 | // Input: nums = [0,1,1] 163 | // Output: [] 164 | // Explanation: The only possible triplet does not sum up to 0. 165 | // 166 | // 167 | // Example 3: 168 | // 169 | // Input: nums = [0,0,0] 170 | // Output: [[0,0,0]] 171 | // Explanation: The only possible triplet sums up to 0. 172 | // 173 | // 174 | //   175 | // Constraints: 176 | // 177 | // 178 | // 3 <= nums.length <= 3000 179 | // -10⁵ <= nums[i] <= 10⁵ 180 | // 181 | 182 | // start_marker 183 | impl Solution { 184 | pub fn three_sum(nums: Vec) -> Vec> { 185 | 186 | } 187 | 188 | } 189 | // end_marker 190 | 191 | ``` 192 | 193 |
194 | 195 |
196 | 197 | 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: 198 | 199 | ```toml 200 | [code] 201 | inject_before = ["#include", "using namespace std;"] 202 | inject_after = ["int main() {\n Solution solution;\n\n}"] 203 | ``` 204 | 205 | #### 1. pick 206 | 207 | ```sh 208 | leetcode pick 1 209 | ``` 210 | 211 | ```sh 212 | leetcode pick --name "Two Sum" 213 | ``` 214 | 215 | ```sh 216 | [1] Two Sum is on the run... 217 | 218 | 219 | Given an array of integers, return indices of the two numbers such that they add up to a specific target. 220 | 221 | You may assume that each input would have exactly one solution, and you may not use the same element twice. 222 | 223 | -------------------------------------------------- 224 | 225 | Example: 226 | 227 | 228 | Given nums = [2, 7, 11, 15], target = 9, 229 | 230 | Because nums[0] + nums[1] = 2 + 7 = 9, 231 | return [0, 1]. 232 | ``` 233 | 234 | #### 2. edit 235 | 236 | ```sh 237 | leetcode edit 1 238 | ``` 239 | 240 | ```rust 241 | # struct Solution; 242 | impl Solution { 243 | pub fn two_sum(nums: Vec, target: i32) -> Vec { 244 | use std::collections::HashMap; 245 | let mut m: HashMap = HashMap::new(); 246 | 247 | for (i, e) in nums.iter().enumerate() { 248 | if let Some(v) = m.get(&(target - e)) { 249 | return vec![*v, i as i32]; 250 | } 251 | 252 | m.insert(*e, i as i32).unwrap_or_default(); 253 | } 254 | 255 | return vec![]; 256 | } 257 | } 258 | ``` 259 | 260 | #### 3. test 261 | 262 | ```sh 263 | leetcode test 1 264 | ``` 265 | 266 | ```sh 267 | 268 | Accepted Runtime: 0 ms 269 | 270 | Your input: [2,7,11,15], 9 271 | Output: [0,1] 272 | Expected: [0,1] 273 | 274 | ``` 275 | 276 | #### 4. exec 277 | 278 | ```sh 279 | leetcode exec 1 280 | ``` 281 | 282 | ```sh 283 | 284 | Success 285 | 286 | Runtime: 0 ms, faster than 100% of Rustonline submissions for Two Sum. 287 | 288 | Memory Usage: 2.4 MB, less than 100% of Rustonline submissions for Two Sum. 289 | 290 | 291 | ``` 292 | 293 | ## Cookies 294 | 295 | 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**, 296 | you can manually input your LeetCode Cookies to the configuration file. 297 | 298 | ```toml 299 | [cookies] 300 | csrf = "..." 301 | session = "..." 302 | ``` 303 | 304 | For Example, using Firefox (after logging in to LeetCode): 305 | 306 | #### Step 1 307 | 308 | Open Firefox, press F12, and click `Storage` tab. 309 | 310 | #### Step 2 311 | 312 | Expand `Cookies` tab on the left and select https://leetcode.com. 313 | 314 | #### Step 2 315 | 316 | Copy `Value` from `LEETCODE_SESSION` and `csrftoken` to `session` and `csrf` in your configuration file, respectively: 317 | 318 | ```toml 319 | [cookies] 320 | csrf = '' 321 | session = '' 322 | ``` 323 | 324 | #### Environment variables 325 | 326 | 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`: 327 | 328 | ```toml 329 | [cookies] 330 | csrf = '' 331 | session = '' 332 | site = 'leetcode.com' 333 | ``` 334 | 335 | Then set the environment variables: 336 | 337 | ```bash 338 | export LEETCODE_CSRF='' 339 | export LEETCODE_SESSION='' 340 | export LEETCODE_SITE='leetcode.cn' # or 'leetcode.com' 341 | ``` 342 | 343 | 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. 344 | 345 | ## Programmable 346 | 347 | If you want to filter LeetCode questions using custom Python scripts, add the following to your the configuration file: 348 | 349 | ```toml 350 | [storage] 351 | scripts = "scripts" 352 | ``` 353 | 354 | Then write the script: 355 | 356 | ```python 357 | # ~/.leetcode/scripts/plan1.py 358 | import json; 359 | 360 | def plan(sps, stags): 361 | ## 362 | # `print` in python is supported, 363 | # if you want to know the data structures of these two args, 364 | # just print them 365 | ## 366 | problems = json.loads(sps) 367 | tags = json.loads(stags) 368 | 369 | ret = [] 370 | tm = {} 371 | for tag in tags: 372 | tm[tag["tag"]] = tag["refs"]; 373 | 374 | for i in problems: 375 | if i["level"] == 1 and str(i["id"]) in tm["linked-list"]: 376 | ret.append(str(i["id"])) 377 | 378 | # return is `List[string]` 379 | return ret 380 | ``` 381 | 382 | Then run `list` with the filter that you just wrote: 383 | 384 | ```sh 385 | leetcode list -p plan1 386 | ``` 387 | 388 | That's it! Enjoy! 389 | 390 | ## Contributions 391 | 392 | Feel free to add your names and emails in the `authors` field of `Cargo.toml` ! 393 | 394 | ## LICENSE 395 | 396 | MIT 397 | 398 | [pr]: https://github.com/clearloop/leetcode-cli/pulls 399 | [#1]: https://github.com/clearloop/leetcode-cli/issues/1 400 | -------------------------------------------------------------------------------- /src/cache/mod.rs: -------------------------------------------------------------------------------- 1 | //! Save bad network\'s ass. 2 | pub mod models; 3 | pub mod parser; 4 | pub mod schemas; 5 | mod sql; 6 | use self::models::*; 7 | use self::schemas::{problems::dsl::*, tags::dsl::*}; 8 | use self::sql::*; 9 | use crate::helper::test_cases_path; 10 | use crate::{config::Config, err::Error, plugins::LeetCode}; 11 | use anyhow::anyhow; 12 | use colored::Colorize; 13 | use diesel::prelude::*; 14 | use reqwest::Response; 15 | use serde::de::DeserializeOwned; 16 | use serde_json::Value; 17 | use std::collections::HashMap; 18 | 19 | /// sqlite connection 20 | pub fn conn(p: String) -> SqliteConnection { 21 | SqliteConnection::establish(&p).unwrap_or_else(|_| panic!("Error connecting to {:?}", p)) 22 | } 23 | 24 | /// Condition submit or test 25 | #[derive(Clone, Debug)] 26 | #[derive(Default)] 27 | pub enum Run { 28 | Test, 29 | #[default] 30 | Submit, 31 | } 32 | 33 | 34 | /// Requests if data not download 35 | #[derive(Clone)] 36 | pub struct Cache(pub LeetCode); 37 | 38 | impl Cache { 39 | /// Ref to sqlite connection 40 | fn conn(&self) -> Result { 41 | Ok(conn(self.0.conf.storage.cache()?)) 42 | } 43 | 44 | /// Clean cache 45 | pub fn clean(&self) -> Result<(), Error> { 46 | Ok(std::fs::remove_file(self.0.conf.storage.cache()?)?) 47 | } 48 | 49 | /// ref to download problems 50 | pub async fn update(self) -> Result<(), Error> { 51 | self.download_problems().await?; 52 | Ok(()) 53 | } 54 | 55 | pub fn update_after_ac(self, rid: i32) -> Result<(), Error> { 56 | let mut c = conn(self.0.conf.storage.cache()?); 57 | let target = problems.filter(id.eq(rid)); 58 | diesel::update(target) 59 | .set(status.eq("ac")) 60 | .execute(&mut c)?; 61 | Ok(()) 62 | } 63 | 64 | async fn get_user_info(&self) -> Result<(String, bool), Error> { 65 | let user = parser::user(self.clone().0.get_user_info().await?.json().await?); 66 | match user { 67 | None => Err(Error::NoneError), 68 | Some(None) => Err(Error::CookieError), 69 | Some(Some((s, b))) => Ok((s, b)), 70 | } 71 | } 72 | 73 | async fn is_session_bad(&self) -> bool { 74 | // i.e. self.get_user_info().contains_err(Error::CookieError) 75 | matches!(self.get_user_info().await, Err(Error::CookieError)) 76 | } 77 | 78 | async fn resp_to_json(&self, resp: Response) -> Result { 79 | let maybe_json: Result = resp.json().await; 80 | if maybe_json.is_err() && self.is_session_bad().await { 81 | Err(Error::CookieError) 82 | } else { 83 | Ok(maybe_json?) 84 | } 85 | } 86 | 87 | /// Download leetcode problems to db 88 | pub async fn download_problems(self) -> Result, Error> { 89 | info!("Fetching leetcode problems..."); 90 | let mut ps: Vec = vec![]; 91 | 92 | for i in &self.0.conf.sys.categories.to_owned() { 93 | let json = self 94 | .0 95 | .clone() 96 | .get_category_problems(i) 97 | .await? 98 | .json() // does not require LEETCODE_SESSION 99 | .await?; 100 | parser::problem(&mut ps, json).ok_or(Error::NoneError)?; 101 | } 102 | 103 | diesel::replace_into(problems) 104 | .values(&ps) 105 | .execute(&mut self.conn()?)?; 106 | 107 | Ok(ps) 108 | } 109 | 110 | /// Get problem 111 | pub fn get_problem(&self, rfid: i32) -> Result { 112 | let p: Problem = problems.filter(fid.eq(rfid)).first(&mut self.conn()?)?; 113 | if p.category != "algorithms" { 114 | return Err(anyhow!("No support for database and shell questions yet").into()); 115 | } 116 | 117 | Ok(p) 118 | } 119 | 120 | /// Get problem from name 121 | pub fn get_problem_id_from_name(&self, problem_name: &String) -> Result { 122 | let p: Problem = problems 123 | .filter(name.eq(problem_name)) 124 | .first(&mut self.conn()?)?; 125 | if p.category != "algorithms" { 126 | return Err(anyhow!("No support for database and shell questions yet").into()); 127 | } 128 | Ok(p.fid) 129 | } 130 | 131 | /// Get daily problem 132 | pub async fn get_daily_problem_id(&self) -> Result { 133 | parser::daily( 134 | self.clone() 135 | .0 136 | .get_question_daily() 137 | .await? 138 | .json() // does not require LEETCODE_SESSION 139 | .await?, 140 | ) 141 | .ok_or(Error::NoneError) 142 | } 143 | 144 | /// Get problems from cache 145 | pub fn get_problems(&self) -> Result, Error> { 146 | Ok(problems.load::(&mut self.conn()?)?) 147 | } 148 | 149 | /// Get question 150 | #[allow(clippy::useless_let_if_seq)] 151 | pub async fn get_question(&self, rfid: i32) -> Result { 152 | let target: Problem = problems.filter(fid.eq(rfid)).first(&mut self.conn()?)?; 153 | 154 | let ids = match target.level { 155 | 1 => target.fid.to_string().green(), 156 | 2 => target.fid.to_string().yellow(), 157 | 3 => target.fid.to_string().red(), 158 | _ => target.fid.to_string().dimmed(), 159 | }; 160 | 161 | println!( 162 | "\n[{}] {} {}\n\n", 163 | &ids, 164 | &target.name.bold().underline(), 165 | "is on the run...".dimmed() 166 | ); 167 | 168 | if target.category != "algorithms" { 169 | return Err(anyhow!("No support for database and shell questions yet").into()); 170 | } 171 | 172 | let mut rdesc = Question::default(); 173 | if !target.desc.is_empty() { 174 | rdesc = serde_json::from_str(&target.desc)?; 175 | } else { 176 | let json: Value = self 177 | .0 178 | .clone() 179 | .get_question_detail(&target.slug) 180 | .await? 181 | .json() 182 | .await?; 183 | debug!("{:#?}", &json); 184 | match parser::desc(&mut rdesc, json) { 185 | None => return Err(Error::NoneError), 186 | Some(false) => { 187 | return if self.is_session_bad().await { 188 | Err(Error::CookieError) 189 | } else { 190 | Err(Error::PremiumError) 191 | } 192 | } 193 | Some(true) => (), 194 | } 195 | 196 | // update the question 197 | let sdesc = serde_json::to_string(&rdesc)?; 198 | diesel::update(&target) 199 | .set(desc.eq(sdesc)) 200 | .execute(&mut self.conn()?)?; 201 | } 202 | 203 | Ok(rdesc) 204 | } 205 | 206 | pub async fn get_tagged_questions(self, rslug: &str) -> Result, Error> { 207 | trace!("Geting {} questions...", &rslug); 208 | let ids: Vec; 209 | let rtag = tags 210 | .filter(tag.eq(rslug.to_string())) 211 | .first::(&mut self.conn()?); 212 | if let Ok(t) = rtag { 213 | trace!("Got {} questions from local cache...", &rslug); 214 | ids = serde_json::from_str(&t.refs)?; 215 | } else { 216 | ids = parser::tags( 217 | self.clone() 218 | .0 219 | .get_question_ids_by_tag(rslug) 220 | .await? 221 | .json() 222 | .await?, 223 | ) 224 | .ok_or(Error::NoneError)?; 225 | let t = Tag { 226 | r#tag: rslug.to_string(), 227 | r#refs: serde_json::to_string(&ids)?, 228 | }; 229 | 230 | diesel::insert_into(tags) 231 | .values(&t) 232 | .execute(&mut self.conn()?)?; 233 | } 234 | 235 | Ok(ids) 236 | } 237 | 238 | pub fn get_tags(&self) -> Result, Error> { 239 | Ok(tags.load::(&mut self.conn()?)?) 240 | } 241 | 242 | /// run_code data 243 | async fn pre_run_code( 244 | &self, 245 | run: Run, 246 | rfid: i32, 247 | test_case: Option, 248 | ) -> Result<(HashMap<&'static str, String>, [String; 2]), Error> { 249 | trace!("pre-run code..."); 250 | use crate::helper::code_path; 251 | use std::fs::File; 252 | use std::io::Read; 253 | 254 | let mut p = self.get_problem(rfid)?; 255 | if p.desc.is_empty() { 256 | trace!("Problem description does not exist, pull desc and exec again..."); 257 | self.get_question(rfid).await?; 258 | p = self.get_problem(rfid)?; 259 | } 260 | 261 | let d: Question = serde_json::from_str(&p.desc)?; 262 | let conf = &self.0.conf; 263 | let mut json: HashMap<&'static str, String> = HashMap::new(); 264 | let mut code: String = "".to_string(); 265 | 266 | let maybe_file_testcases: Option = test_cases_path(&p) 267 | .map(|file_name| { 268 | let mut tests = "".to_string(); 269 | File::open(file_name) 270 | .and_then(|mut file_descriptor| file_descriptor.read_to_string(&mut tests)) 271 | .map(|_| Some(tests)) 272 | .unwrap_or(None) 273 | }) 274 | .unwrap_or(None); 275 | 276 | let maybe_all_testcases: Option = if d.all_cases.is_empty() { 277 | None 278 | } else { 279 | Some(d.all_cases.to_string()) 280 | }; 281 | 282 | // Takes test cases using following priority 283 | // 1. cli parameter 284 | // 2. if test cases file exist, use the file test cases(user can edit it) 285 | // 3. test cases from problem desc all test cases 286 | // 4. sample test case from the task 287 | let test_case = test_case 288 | .or(maybe_file_testcases) 289 | .or(maybe_all_testcases) 290 | .unwrap_or(d.case); 291 | 292 | File::open(code_path(&p, None)?)?.read_to_string(&mut code)?; 293 | 294 | let code = if conf.code.edit_code_marker { 295 | let begin = code.find(&conf.code.start_marker); 296 | let end = code.find(&conf.code.end_marker); 297 | if let (Some(l), Some(r)) = (begin, end) { 298 | code.get((l + conf.code.start_marker.len())..r) 299 | .map(|s| s.to_string()) 300 | .unwrap_or_else(|| code) 301 | } else { 302 | code 303 | } 304 | } else { 305 | code 306 | }; 307 | 308 | json.insert("lang", conf.code.lang.to_string()); 309 | json.insert("question_id", p.id.to_string()); 310 | json.insert("typed_code", code); 311 | 312 | // pass manually data 313 | json.insert("name", p.name.to_string()); 314 | json.insert("data_input", test_case); 315 | 316 | let url = match run { 317 | Run::Test => conf.sys.urls.test(&p.slug), 318 | Run::Submit => { 319 | json.insert("judge_type", "large".to_string()); 320 | conf.sys.urls.submit(&p.slug) 321 | } 322 | }; 323 | 324 | Ok((json, [url, conf.sys.urls.problem(&p.slug)])) 325 | } 326 | 327 | /// TODO: The real delay 328 | async fn recur_verify(&self, rid: String) -> Result { 329 | use std::time::Duration; 330 | 331 | trace!("Run verify recursion..."); 332 | std::thread::sleep(Duration::from_micros(3000)); 333 | 334 | let json: VerifyResult = self 335 | .resp_to_json(self.clone().0.verify_result(rid.clone()).await?) 336 | .await?; 337 | 338 | Ok(json) 339 | } 340 | 341 | /// Exec problem filter —— Test or Submit 342 | pub async fn exec_problem( 343 | &self, 344 | rfid: i32, 345 | run: Run, 346 | test_case: Option, 347 | ) -> Result { 348 | trace!("Exec problem filter —— Test or Submit"); 349 | let (json, [url, refer]) = self.pre_run_code(run.clone(), rfid, test_case).await?; 350 | trace!("Pre-run code result {:#?}, {}, {}", json, url, refer); 351 | 352 | let text = self 353 | .0 354 | .clone() 355 | .run_code(json.clone(), url.clone(), refer.clone()) 356 | .await? 357 | .text() 358 | .await?; 359 | 360 | let run_res: RunCode = serde_json::from_str(&text).map_err(|e| { 361 | anyhow!("JSON error: {e}, please double check your session and csrf config.") 362 | })?; 363 | 364 | trace!("Run code result {:#?}", run_res); 365 | 366 | // Check if leetcode accepted the Run request 367 | if match run { 368 | Run::Test => run_res.interpret_id.is_empty(), 369 | Run::Submit => run_res.submission_id == 0, 370 | } { 371 | return Err(Error::CookieError); 372 | } 373 | 374 | let mut res: VerifyResult = VerifyResult::default(); 375 | while res.state != "SUCCESS" { 376 | res = match run { 377 | Run::Test => self.recur_verify(run_res.interpret_id.clone()).await?, 378 | Run::Submit => self.recur_verify(run_res.submission_id.to_string()).await?, 379 | }; 380 | } 381 | trace!("Recur verify result {:#?}", res); 382 | 383 | res.name = json.get("name").ok_or(Error::NoneError)?.to_string(); 384 | res.data_input = json.get("data_input").ok_or(Error::NoneError)?.to_string(); 385 | res.result_type = run; 386 | Ok(res) 387 | } 388 | 389 | /// New cache 390 | pub fn new() -> Result { 391 | let conf = Config::locate()?; 392 | let mut c = conn(conf.storage.cache()?); 393 | diesel::sql_query(CREATE_PROBLEMS_IF_NOT_EXISTS).execute(&mut c)?; 394 | diesel::sql_query(CREATE_TAGS_IF_NOT_EXISTS).execute(&mut c)?; 395 | 396 | Ok(Cache(LeetCode::new()?)) 397 | } 398 | } 399 | -------------------------------------------------------------------------------- /src/cache/models.rs: -------------------------------------------------------------------------------- 1 | //! Leetcode data models 2 | use unicode_width::UnicodeWidthStr; 3 | use unicode_width::UnicodeWidthChar; 4 | use super::schemas::{problems, tags}; 5 | use crate::helper::HTML; 6 | use colored::Colorize; 7 | use serde::{Deserialize, Serialize}; 8 | use serde_json::Number; 9 | 10 | /// Tag model 11 | #[derive(Clone, Insertable, Queryable, Serialize, Debug)] 12 | #[diesel(table_name = tags)] 13 | pub struct Tag { 14 | pub tag: String, 15 | pub refs: String, 16 | } 17 | 18 | /// Problem model 19 | #[derive(AsChangeset, Clone, Identifiable, Insertable, Queryable, Serialize, Debug)] 20 | #[diesel(table_name = problems)] 21 | pub struct Problem { 22 | pub category: String, 23 | pub fid: i32, 24 | pub id: i32, 25 | pub level: i32, 26 | pub locked: bool, 27 | pub name: String, 28 | pub percent: f32, 29 | pub slug: String, 30 | pub starred: bool, 31 | pub status: String, 32 | pub desc: String, 33 | } 34 | 35 | impl Problem { 36 | fn display_level(&self) -> &str { 37 | match self.level { 38 | 1 => "Easy", 39 | 2 => "Medium", 40 | 3 => "Hard", 41 | _ => "Unknown", 42 | } 43 | } 44 | 45 | pub fn desc_comment(&self, conf: &Config) -> String { 46 | let mut res = String::new(); 47 | let comment_leading = &conf.code.comment_leading; 48 | res += format!("{} Category: {}\n", comment_leading, self.category).as_str(); 49 | res += format!("{} Level: {}\n", comment_leading, self.display_level(),).as_str(); 50 | res += format!("{} Percent: {}%\n\n", comment_leading, self.percent).as_str(); 51 | 52 | res + "\n" 53 | } 54 | } 55 | 56 | static DONE: &str = " ✔"; 57 | static ETC: &str = "..."; 58 | static LOCK: &str = "🔒"; 59 | static NDONE: &str = " ✘"; 60 | static SPACE: &str = " "; 61 | impl std::fmt::Display for Problem { 62 | fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { 63 | let space_2 = SPACE.repeat(2); 64 | let mut lock = space_2.as_str(); 65 | let mut done = space_2.normal(); 66 | let mut id = "".to_string(); 67 | let mut name = "".to_string(); 68 | let mut level = "".normal(); 69 | 70 | if self.locked { 71 | lock = LOCK 72 | }; 73 | if self.status == "ac" { 74 | done = DONE.green().bold(); 75 | } else if self.status == "notac" { 76 | done = NDONE.green().bold(); 77 | } 78 | 79 | match self.fid.to_string().len() { 80 | 1 => { 81 | id.push_str(&SPACE.repeat(2)); 82 | id.push_str(&self.fid.to_string()); 83 | id.push_str(SPACE); 84 | } 85 | 2 => { 86 | id.push_str(SPACE); 87 | id.push_str(&self.fid.to_string()); 88 | id.push_str(SPACE); 89 | } 90 | 3 => { 91 | id.push_str(SPACE); 92 | id.push_str(&self.fid.to_string()); 93 | } 94 | 4 => { 95 | id.push_str(&self.fid.to_string()); 96 | } 97 | _ => { 98 | id.push_str(&space_2); 99 | id.push_str(&space_2); 100 | } 101 | } 102 | 103 | let name_width = UnicodeWidthStr::width(self.name.as_str()); 104 | let target_width = 60; 105 | if name_width <= target_width { 106 | name.push_str(&self.name); 107 | name.push_str(&SPACE.repeat(target_width - name_width)); 108 | } else { 109 | // truncate carefully to target width - 3 (because "..." will take some width) 110 | let mut truncated = String::new(); 111 | let mut current_width = 0; 112 | for c in self.name.chars() { 113 | let char_width = UnicodeWidthChar::width(c).unwrap_or(0); 114 | if current_width + char_width > target_width - 3 { 115 | break; 116 | } 117 | truncated.push(c); 118 | current_width += char_width; 119 | } 120 | truncated.push_str(ETC); // add "..." 121 | let truncated_width = UnicodeWidthStr::width(truncated.as_str()); 122 | truncated.push_str(&SPACE.repeat(target_width - truncated_width)); 123 | name = truncated; 124 | } 125 | 126 | level = match self.level { 127 | 1 => "Easy ".bright_green(), 128 | 2 => "Medium".bright_yellow(), 129 | 3 => "Hard ".bright_red(), 130 | _ => level, 131 | }; 132 | 133 | let mut pct = self.percent.to_string(); 134 | if pct.len() < 5 { 135 | pct.push_str(&"0".repeat(5 - pct.len())); 136 | } 137 | write!( 138 | f, 139 | " {} {} [{}] {} {} ({} %)", 140 | lock, 141 | done, 142 | id, 143 | name, 144 | level, 145 | &pct[..5] 146 | ) 147 | } 148 | } 149 | 150 | /// desc model 151 | #[derive(Debug, Default, Serialize, Deserialize)] 152 | pub struct Question { 153 | pub content: String, 154 | pub stats: Stats, 155 | pub defs: CodeDefintion, 156 | pub case: String, 157 | pub all_cases: String, 158 | pub metadata: MetaData, 159 | pub test: bool, 160 | pub t_content: String, 161 | } 162 | 163 | impl Question { 164 | pub fn desc(&self) -> String { 165 | self.content.render() 166 | } 167 | 168 | pub fn desc_comment(&self, conf: &Config) -> String { 169 | let desc = self.content.render(); 170 | 171 | let mut res = desc.lines().fold("\n".to_string(), |acc, e| { 172 | acc + "" + conf.code.comment_leading.as_str() + " " + e + "\n" 173 | }); 174 | res += " \n"; 175 | 176 | res 177 | } 178 | } 179 | 180 | use question::*; 181 | /// deps of Question 182 | mod question { 183 | use serde::{Deserialize, Serialize}; 184 | 185 | /// Code samples 186 | #[derive(Debug, Default, Serialize, Deserialize)] 187 | pub struct CodeDefintion(pub Vec); 188 | 189 | /// CodeDefinition Inner struct 190 | #[derive(Debug, Default, Serialize, Deserialize)] 191 | pub struct CodeDefintionInner { 192 | pub value: String, 193 | pub text: String, 194 | #[serde(alias = "defaultCode")] 195 | pub code: String, 196 | } 197 | 198 | /// Question status 199 | #[derive(Debug, Default, Serialize, Deserialize)] 200 | pub struct Stats { 201 | #[serde(alias = "totalAccepted")] 202 | tac: String, 203 | #[serde(alias = "totalSubmission")] 204 | tsm: String, 205 | #[serde(alias = "totalAcceptedRaw")] 206 | tacr: i32, 207 | #[serde(alias = "totalSubmissionRaw")] 208 | tsmr: i32, 209 | #[serde(alias = "acRate")] 210 | rate: String, 211 | } 212 | 213 | /// Algorithm metadata 214 | #[derive(Debug, Default, Serialize, Deserialize)] 215 | pub struct MetaData { 216 | pub name: Option, 217 | pub params: Option>, 218 | pub r#return: Return, 219 | } 220 | 221 | /// MetaData nested fields 222 | #[derive(Debug, Default, Serialize, Deserialize)] 223 | pub struct Param { 224 | pub name: String, 225 | pub r#type: String, 226 | } 227 | 228 | /// MetaData nested fields 229 | #[derive(Debug, Default, Serialize, Deserialize)] 230 | pub struct Return { 231 | pub r#type: String, 232 | } 233 | } 234 | 235 | /// run_code Result 236 | #[derive(Debug, Deserialize)] 237 | pub struct RunCode { 238 | #[serde(default)] 239 | pub interpret_id: String, 240 | #[serde(default)] 241 | pub test_case: String, 242 | #[serde(default)] 243 | pub submission_id: i64, 244 | } 245 | 246 | use super::parser::ssr; 247 | use crate::cache::Run; 248 | 249 | /// verify result model 250 | #[derive(Default, Debug, Deserialize)] 251 | pub struct VerifyResult { 252 | pub state: String, 253 | #[serde(skip)] 254 | pub name: String, 255 | #[serde(skip)] 256 | pub data_input: String, 257 | #[serde(skip)] 258 | pub result_type: Run, 259 | // #[serde(default)] 260 | // lang: String, 261 | #[serde(default)] 262 | pretty_lang: String, 263 | // #[serde(default)] 264 | // submission_id: String, 265 | // #[serde(default)] 266 | // run_success: bool, 267 | #[serde(default)] 268 | correct_answer: bool, 269 | #[serde(default, deserialize_with = "ssr")] 270 | code_answer: Vec, 271 | #[serde(default, deserialize_with = "ssr")] 272 | code_output: Vec, 273 | #[serde(default, deserialize_with = "ssr")] 274 | expected_output: Vec, 275 | #[serde(default, deserialize_with = "ssr")] 276 | std_output: Vec, 277 | 278 | // flatten 279 | // #[serde(flatten, default)] 280 | // info: VerifyInfo, 281 | #[serde(flatten, default)] 282 | status: VerifyStatus, 283 | #[serde(flatten, default)] 284 | analyse: Analyse, 285 | #[serde(flatten, default)] 286 | expected: Expected, 287 | #[serde(flatten, default)] 288 | error: CompileError, 289 | #[serde(flatten, default)] 290 | submit: Submit, 291 | } 292 | 293 | impl std::fmt::Display for VerifyResult { 294 | fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { 295 | let ca = match &self.code_answer.len() { 296 | 1 => self.code_answer[0].to_string(), 297 | _ => self.code_answer.join("↩ "), 298 | }; 299 | 300 | let eca = match &self.expected.expected_code_answer.len() { 301 | 1 => self.expected.expected_code_answer[0].to_string(), 302 | _ => self.expected.expected_code_answer.join("↩ "), 303 | }; 304 | 305 | debug!("{:#?}", &self); 306 | 307 | match &self.status.status_code { 308 | 10 => { 309 | if matches!(self.result_type, Run::Test) && self.correct_answer { 310 | // Pass Tests 311 | write!( 312 | f, 313 | "\n{}{}{}\n{}{}{}{}{}{}\n", 314 | &self.status.status_msg.green().bold(), 315 | &"Runtime: ".before_spaces(7).dimmed(), 316 | &self.status.status_runtime.dimmed(), 317 | &"\nYour input:".after_spaces(4), 318 | &self.data_input.replace('\n', "↩ "), 319 | &"\nOutput:".after_spaces(8), 320 | ca, 321 | &"\nExpected:".after_spaces(6), 322 | eca, 323 | )? 324 | } else if matches!(self.result_type, Run::Submit) 325 | && !self.submit.compare_result.is_empty() 326 | { 327 | // only Submit execute this branch 328 | // Submit Successfully 329 | // TODO: result should be all 1; 330 | // Lines below are sucks... 331 | let cache = super::Cache::new().expect("cache gen failed"); 332 | cache 333 | .update_after_ac( 334 | self.submit 335 | .question_id 336 | .parse() 337 | .expect("submit successfully, parse question_id to i32 failed"), 338 | ) 339 | .expect("update ac to cache failed"); 340 | 341 | // prints 342 | let rp = if let Some(n) = &self.analyse.runtime_percentile { 343 | if n.is_f64() { 344 | n.as_f64().unwrap_or(0.0) as i64 345 | } else { 346 | n.as_i64().unwrap_or(0) 347 | } 348 | } else { 349 | 0 350 | }; 351 | 352 | let mp = if let Some(n) = &self.analyse.memory_percentile { 353 | if n.is_f64() { 354 | n.as_f64().unwrap_or(0.0) as i64 355 | } else { 356 | n.as_i64().unwrap_or(0) 357 | } 358 | } else { 359 | 0 360 | }; 361 | 362 | write!( 363 | f, 364 | "\n{}{}{}\ 365 | , faster than \ 366 | {}{}\ 367 | of \ 368 | {} \ 369 | online submissions for \ 370 | {}.\n\n\ 371 | {}{}\ 372 | , less than \ 373 | {}{}\ 374 | of \ 375 | {} {}.\n\n", 376 | "Success\n\n".green().bold(), 377 | "Runtime: ".dimmed(), 378 | &self.status.status_runtime.bold(), 379 | rp.to_string().bold(), 380 | "% ".bold(), 381 | &self.pretty_lang, 382 | &self.name, 383 | "Memory Usage: ".dimmed(), 384 | &self.status.status_memory.bold(), 385 | mp.to_string().bold(), 386 | "% ".bold(), 387 | &self.pretty_lang, 388 | &self.name, 389 | )? 390 | } else { 391 | // Wrong Answer during testing 392 | write!( 393 | f, 394 | "\n{}{}{}\n{}{}{}{}{}{}\n", 395 | "Wrong Answer".red().bold(), 396 | " Runtime: ".dimmed(), 397 | &self.status.status_runtime.dimmed(), 398 | &"\nYour input:".after_spaces(4), 399 | &self.data_input.replace('\n', "↩ "), 400 | &"\nOutput:".after_spaces(8), 401 | ca, 402 | &"\nExpected:".after_spaces(6), 403 | eca, 404 | )? 405 | } 406 | } 407 | // Failed some tests during submission 408 | 11 => write!( 409 | f, 410 | "\n{}\n\n{}{}\n{}{}\n{}{}{}{}{}{}\n", 411 | &self.status.status_msg.red().bold(), 412 | "Cases passed:".after_spaces(2).green(), 413 | &self 414 | .analyse 415 | .total_correct 416 | .as_ref() 417 | .unwrap_or(&Number::from(0)) 418 | .to_string() 419 | .green(), 420 | &"Total cases:".after_spaces(3).yellow(), 421 | &self 422 | .analyse 423 | .total_testcases 424 | .as_ref() 425 | .unwrap_or(&Number::from(0)) 426 | .to_string() 427 | .bold() 428 | .yellow(), 429 | &"Last case:".after_spaces(5).dimmed(), 430 | &self.submit.last_testcase.replace('\n', "↩ ").dimmed(), 431 | &"\nOutput:".after_spaces(8), 432 | self.code_output[0], 433 | &"\nExpected:".after_spaces(6), 434 | self.expected_output[0], 435 | )?, 436 | // Memory Exceeded 437 | 12 => write!( 438 | f, 439 | "\n{}\n\n{}{}\n", 440 | &self.status.status_msg.yellow().bold(), 441 | &"Last case:".after_spaces(5).dimmed(), 442 | &self.data_input.replace('\n', "↩ "), 443 | )?, 444 | // Output Timeout Exceeded 445 | // 446 | // TODO: 13 and 14 might have some different, 447 | // if anybody reach this, welcome to fix this! 448 | 13 | 14 => write!(f, "\n{}\n", &self.status.status_msg.yellow().bold(),)?, 449 | // Runtime error 450 | 15 => write!( 451 | f, 452 | "\n{}\n{}\n'", 453 | &self.status.status_msg.red().bold(), 454 | &self.status.runtime_error 455 | )?, 456 | // Compile Error 457 | 20 => write!( 458 | f, 459 | "\n{}:\n\n{}\n", 460 | &self.status.status_msg.red().bold(), 461 | &self.error.full_compile_error.dimmed() 462 | )?, 463 | _ => write!( 464 | f, 465 | "{}{}{}{}{}{}{}{}", 466 | "\nUnknown Error...\n".red().bold(), 467 | "\nBingo! Welcome to fix this! Pull your request at ".yellow(), 468 | "https://github.com/clearloop/leetcode-cli/pulls" 469 | .dimmed() 470 | .underline(), 471 | ", and this file is located at ".yellow(), 472 | "leetcode-cli/src/cache/models.rs".dimmed().underline(), 473 | " waiting for you! Yep, line ".yellow(), 474 | "385".dimmed().underline(), 475 | ".\n".yellow(), 476 | )?, 477 | }; 478 | 479 | match &self.result_type { 480 | Run::Test => { 481 | if !self.code_output.is_empty() { 482 | write!( 483 | f, 484 | "{}{}", 485 | &"Stdout:".after_spaces(8).purple(), 486 | &self.code_output.join(&"\n".after_spaces(15)) 487 | ) 488 | } else { 489 | write!(f, "") 490 | } 491 | } 492 | _ => { 493 | if !self.std_output.is_empty() { 494 | write!( 495 | f, 496 | "{}{}", 497 | &"Stdout:".after_spaces(8).purple(), 498 | &self.std_output[0].replace('\n', &"\n".after_spaces(15)) 499 | ) 500 | } else { 501 | write!(f, "") 502 | } 503 | } 504 | } 505 | } 506 | } 507 | 508 | use crate::Config; 509 | use verify::*; 510 | 511 | mod verify { 512 | use super::super::parser::ssr; 513 | use serde::Deserialize; 514 | use serde_json::Number; 515 | 516 | #[derive(Debug, Default, Deserialize)] 517 | pub struct Submit { 518 | #[serde(default)] 519 | pub question_id: String, 520 | #[serde(default)] 521 | pub last_testcase: String, 522 | #[serde(default)] 523 | pub compare_result: String, 524 | } 525 | 526 | // #[derive(Debug, Default, Deserialize)] 527 | // pub struct VerifyInfo { 528 | // #[serde(default)] 529 | // memory: i64, 530 | // #[serde(default)] 531 | // elapsed_time: i64, 532 | // #[serde(default)] 533 | // task_finish_time: i64, 534 | // } 535 | 536 | #[derive(Debug, Default, Deserialize)] 537 | pub struct Analyse { 538 | #[serde(default)] 539 | pub total_correct: Option, 540 | #[serde(default)] 541 | pub total_testcases: Option, 542 | #[serde(default)] 543 | pub runtime_percentile: Option, 544 | #[serde(default)] 545 | pub memory_percentile: Option, 546 | } 547 | 548 | #[derive(Debug, Default, Deserialize)] 549 | pub struct VerifyStatus { 550 | #[serde(default)] 551 | pub status_code: i32, 552 | #[serde(default)] 553 | pub status_msg: String, 554 | #[serde(default)] 555 | pub status_memory: String, 556 | #[serde(default)] 557 | pub status_runtime: String, 558 | #[serde(default)] 559 | pub runtime_error: String, 560 | } 561 | 562 | #[derive(Debug, Default, Deserialize)] 563 | pub struct CompileError { 564 | // #[serde(default)] 565 | // compile_error: String, 566 | #[serde(default)] 567 | pub full_compile_error: String, 568 | } 569 | 570 | #[derive(Debug, Default, Deserialize)] 571 | pub struct Expected { 572 | // #[serde(default)] 573 | // expected_status_code: i32, 574 | // #[serde(default)] 575 | // expected_lang: String, 576 | // #[serde(default)] 577 | // expected_run_success: bool, 578 | // #[serde(default)] 579 | // expected_status_runtime: String, 580 | // #[serde(default)] 581 | // expected_memory: i64, 582 | // #[serde(default, deserialize_with = "ssr")] 583 | // expected_code_output: Vec, 584 | // #[serde(default)] 585 | // expected_elapsed_time: i64, 586 | // #[serde(default)] 587 | // expected_task_finish_time: i64, 588 | #[serde(default, deserialize_with = "ssr")] 589 | pub expected_code_answer: Vec, 590 | } 591 | } 592 | 593 | /// Formatter for str 594 | trait Formatter { 595 | fn after_spaces(&self, spaces: usize) -> String; 596 | fn before_spaces(&self, spaces: usize) -> String; 597 | } 598 | 599 | impl Formatter for str { 600 | fn after_spaces(&self, spaces: usize) -> String { 601 | let mut r = String::new(); 602 | r.push_str(self); 603 | r.push_str(&" ".repeat(spaces)); 604 | r 605 | } 606 | 607 | fn before_spaces(&self, spaces: usize) -> String { 608 | let mut r = String::new(); 609 | r.push_str(&" ".repeat(spaces)); 610 | r.push_str(self); 611 | r 612 | } 613 | } 614 | --------------------------------------------------------------------------------