Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
23 changes: 0 additions & 23 deletions .github/workflows/clippy.yml

This file was deleted.

6 changes: 2 additions & 4 deletions .github/workflows/release.yml
Original file line number Diff line number Diff line change
Expand Up @@ -11,11 +11,9 @@ jobs:
runs-on: ubuntu-latest
steps:
- name: Checkout sources
uses: actions/checkout@v5
uses: actions/checkout@v6
- name: Install stable toolchain
uses: actions-rs/toolchain@v1
with:
toolchain: stable
run: rustup update stable && rustup default stable
- uses: katyo/publish-crates@v2
with:
registry-token: ${{ secrets.CARGO_REGISTRY_TOKEN }}
34 changes: 18 additions & 16 deletions .github/workflows/rust.yml
Original file line number Diff line number Diff line change
Expand Up @@ -14,20 +14,22 @@ jobs:
os: [macOS-latest, ubuntu-latest]
steps:
- name: Checkout the source code
uses: actions/checkout@v5
- name: Set nightly toolchain
uses: actions-rs/toolchain@v1
with:
toolchain: stable
- name: Environment
run: |
if [[ "$(uname)" == 'Darwin' ]]; then
brew install sqlite3
else
sudo apt-get update -y
sudo apt-get install -y libsqlite3-dev libdbus-1-dev
fi
- name: Build
run: cargo build --release --all-features
uses: actions/checkout@v6
- name: Install Toolchain
run: rustup update stable && rustup default stable
- uses: taiki-e/install-action@sccache
- uses: taiki-e/install-action@nextest
- name: Run tests
run: cargo test --release --all-features
run: cargo nextest run --release --all-features
check:
runs-on: ubuntu-latest
steps:
- name: Checkout the source code
uses: actions/checkout@v6
- name: Install Toolchain
run: rustup update stable && rustup default stable
- uses: taiki-e/install-action@sccache
- name: Run format check
run: cargo fmt --check
- name: Run clippy
run: cargo clippy --all-features -- -D warnings
15 changes: 14 additions & 1 deletion Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

14 changes: 7 additions & 7 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -4,9 +4,9 @@ path = "src/bin/lc.rs"

[package]
name = "leetcode-cli"
version = "0.4.8"
version = "0.5.0"
authors = ["clearloop <tianyi.gc@gmail.com>"]
edition = "2021"
edition = "2024"
description = "Leetcode command-line interface in rust."
repository = "https://github.com/clearloop/leetcode-cli"
license = "MIT"
Expand All @@ -18,22 +18,22 @@ readme = './README.md'
[dependencies]
async-trait = "0.1.89"
tokio = { version = "1.48.0", features = ["full"] }
clap = { version = "4.5.51", features = ["cargo"] }
clap = { version = "4.5", features = ["cargo", "derive"] }
colored = "3.0.0"
dirs = "6.0.0"
env_logger = "0.11.6"
keyring = "3.6.3"
log = "0.4.28"
openssl = "0.10.74"
pyo3 = { version = "0.27.1", optional = true }
openssl = "0.10"
pyo3 = { version = "0.27", optional = true }
rand = "0.9.2"
serde = { version = "1.0.228", features = ["derive"] }
serde_json = "1.0.145"
toml = "0.9.8"
regex = "1.12.2"
scraper = "0.24.0"
anyhow = "1.0.100"
clap_complete = "4.5.60"
clap_complete = "4.5"
thiserror = "2.0.17"
unicode-width = "0.2"
notify = "6.1.1"
Expand All @@ -50,4 +50,4 @@ features = ["gzip", "json"]
pym = ["pyo3"]

[target.'cfg(target_family = "unix")'.dependencies]
nix = { version = "0.30.1", features = [ "signal" ] }
nix = { version = "0.30.1", features = ["signal"] }
12 changes: 7 additions & 5 deletions src/cache/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -22,15 +22,13 @@ pub fn conn(p: String) -> SqliteConnection {
}

/// Condition submit or test
#[derive(Clone, Debug)]
#[derive(Default)]
#[derive(Clone, Debug, Default)]
pub enum Run {
Test,
#[default]
Submit,
}


/// Requests if data not download
#[derive(Clone)]
pub struct Cache(pub LeetCode);
Expand Down Expand Up @@ -188,7 +186,7 @@ impl Cache {
Err(Error::CookieError)
} else {
Err(Error::PremiumError)
}
};
}
Some(true) => (),
}
Expand Down Expand Up @@ -358,7 +356,11 @@ impl Cache {
.await?;

let run_res: RunCode = serde_json::from_str(&text).map_err(|e| {
anyhow!("JSON error: {e}, please double check your session and csrf config.")
anyhow!(
"Failed to decode run code result, could be caused by cookie expiration, \
csrf token mismatch, or network issue:\n {e}, raw response:\n {text}\n, \
please report this issue at https://github.com/clearloop/leetcode-cli/issues/new"
)
})?;

trace!("Run code result {:#?}", run_res);
Expand Down
4 changes: 2 additions & 2 deletions src/cache/models.rs
Original file line number Diff line number Diff line change
@@ -1,11 +1,11 @@
//! Leetcode data models
use unicode_width::UnicodeWidthStr;
use unicode_width::UnicodeWidthChar;
use super::schemas::{problems, tags};
use crate::helper::HTML;
use colored::Colorize;
use serde::{Deserialize, Serialize};
use serde_json::Number;
use unicode_width::UnicodeWidthChar;
use unicode_width::UnicodeWidthStr;

/// Tag model
#[derive(Clone, Insertable, Queryable, Serialize, Debug)]
Expand Down
9 changes: 7 additions & 2 deletions src/cache/parser.rs
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,12 @@ pub fn problem(problems: &mut Vec<Problem>, v: Value) -> Option<()> {
// Handle on leetcode-com
Some(s) => s as i32,
// Handle on leetcode-cn
None => fid_obj.as_str()?.split(' ').last()?.parse::<i32>().ok()?,
None => fid_obj
.as_str()?
.split(' ')
.next_back()?
.parse::<i32>()
.ok()?,
};

problems.push(Problem {
Expand Down Expand Up @@ -132,7 +137,7 @@ pub fn user(v: Value) -> Option<Option<(String, bool)>> {
pub use ss::ssr;
/// string or squence
mod ss {
use serde::{de, Deserialize, Deserializer};
use serde::{Deserialize, Deserializer, de};
use std::fmt;
use std::marker::PhantomData;

Expand Down
104 changes: 68 additions & 36 deletions src/cli.rs
Original file line number Diff line number Diff line change
@@ -1,13 +1,10 @@
//! Clap Commanders
use crate::{
cmds::{
completion_handler, Command, CompletionCommand, DataCommand, EditCommand, ExecCommand,
ListCommand, PickCommand, StatCommand, TestCommand,
},
cmd::{CompletionsArgs, DataArgs, EditArgs, ExecArgs, ListArgs, PickArgs, StatArgs, TestArgs},
err::Error,
flag::{Debug, Flag},
};
use clap::crate_version;
use clap::{CommandFactory, Parser, Subcommand};
use env_logger::Env;
use log::LevelFilter;

/// This should be called before calling any cli method or printing any output.
Expand All @@ -23,46 +20,81 @@ pub fn reset_signal_pipe_handler() {
}
}

/// May the Code be with You
#[derive(Parser)]
#[command(name = "leetcode", version, about = "May the Code be with You 👻")]
#[command(arg_required_else_help = true)]
pub struct Cli {
/// Debug mode
#[arg(short, long)]
pub debug: bool,

#[command(subcommand)]
pub command: Option<Commands>,
}

#[derive(Subcommand)]
pub enum Commands {
/// Manage Cache
#[command(visible_alias = "d", display_order = 1)]
Data(DataArgs),

/// Edit question
#[command(visible_alias = "e", display_order = 2)]
Edit(EditArgs),

/// Submit solution
#[command(visible_alias = "x", display_order = 3)]
Exec(ExecArgs),

/// List problems
#[command(visible_alias = "l", display_order = 4)]
List(ListArgs),

/// Pick a problem
#[command(visible_alias = "p", display_order = 5)]
Pick(PickArgs),

/// Show simple chart about submissions
#[command(visible_alias = "s", display_order = 6)]
Stat(StatArgs),

/// Test a question
#[command(visible_alias = "t", display_order = 7)]
Test(TestArgs),

/// Generate shell Completions
#[command(visible_alias = "c", display_order = 8)]
Completions(CompletionsArgs),
}

/// Get matches
pub async fn main() -> Result<(), Error> {
reset_signal_pipe_handler();

let mut cmd = clap::Command::new("leetcode")
.version(crate_version!())
.about("May the Code be with You 👻")
.subcommands(vec![
DataCommand::usage().display_order(1),
EditCommand::usage().display_order(2),
ExecCommand::usage().display_order(3),
ListCommand::usage().display_order(4),
PickCommand::usage().display_order(5),
StatCommand::usage().display_order(6),
TestCommand::usage().display_order(7),
CompletionCommand::usage().display_order(8),
])
.arg(Debug::usage())
.arg_required_else_help(true);

let m = cmd.clone().get_matches();

if m.get_flag("debug") {
Debug::handler()?;
let cli = Cli::parse();

if cli.debug {
env_logger::Builder::from_env(Env::default().default_filter_or("debug")).init();
} else {
env_logger::Builder::new()
.filter_level(LevelFilter::Info)
.format_timestamp(None)
.init();
}

match m.subcommand() {
Some(("data", sub_m)) => Ok(DataCommand::handler(sub_m).await?),
Some(("edit", sub_m)) => Ok(EditCommand::handler(sub_m).await?),
Some(("exec", sub_m)) => Ok(ExecCommand::handler(sub_m).await?),
Some(("list", sub_m)) => Ok(ListCommand::handler(sub_m).await?),
Some(("pick", sub_m)) => Ok(PickCommand::handler(sub_m).await?),
Some(("stat", sub_m)) => Ok(StatCommand::handler(sub_m).await?),
Some(("test", sub_m)) => Ok(TestCommand::handler(sub_m).await?),
Some(("completions", sub_m)) => Ok(completion_handler(sub_m, &mut cmd)?),
_ => Err(Error::MatchError),
match cli.command {
Some(Commands::Data(args)) => args.run().await,
Some(Commands::Edit(args)) => args.run().await,
Some(Commands::Exec(args)) => args.run().await,
Some(Commands::List(args)) => args.run().await,
Some(Commands::Pick(args)) => args.run().await,
Some(Commands::Stat(args)) => args.run().await,
Some(Commands::Test(args)) => args.run().await,
Some(Commands::Completions(args)) => {
let mut cmd = Cli::command();
args.run(&mut cmd)
}
None => Err(Error::MatchError),
}
}
35 changes: 35 additions & 0 deletions src/cmd/completions.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
//! Completions command
use crate::err::Error;
use clap::{Args, Command as ClapCommand};
use clap_complete::{Generator, Shell, generate};

/// Completions command arguments
#[derive(Args)]
pub struct CompletionsArgs {
/// Shell type [possible values: bash, elvish, fish, powershell, zsh]
#[arg(value_parser = clap::value_parser!(Shell))]
pub shell: Option<Shell>,
}

fn get_completions_string<G: Generator>(
generator: G,
cmd: &mut ClapCommand,
) -> Result<String, Error> {
let mut v: Vec<u8> = Vec::new();
let name = cmd.get_name().to_string();
generate(generator, cmd, name, &mut v);
Ok(String::from_utf8(v)?)
}

impl CompletionsArgs {
/// Generate and print shell completions
pub fn run(&self, cmd: &mut ClapCommand) -> Result<(), Error> {
let shell = self
.shell
.or_else(Shell::from_env)
.ok_or(Error::MatchError)?;
let completions = get_completions_string(shell, cmd)?;
println!("{}", completions);
Ok(())
}
}
Loading