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
16 changes: 15 additions & 1 deletion .githooks/pre-push
Original file line number Diff line number Diff line change
Expand Up @@ -2,9 +2,23 @@

echo "Running pre-push checks..."

# Check code formatting
echo "Checking code formatting..."
if ! cargo +nightly fmt --check; then
echo "❌ Code formatting check failed. Please run 'cargo +nightly fmt' and commit the changes."
exit 1
fi

# Lint code
echo "Running clippy..."
if ! cargo clippy --all-features -- -D warnings; then
echo "❌ Clippy linting failed. Please fix the warnings and commit the changes."
exit 1
fi

# Run tests
echo "Running cargo test..."
if ! cargo test; then
if ! cargo test -- --test-threads=1; then
echo "❌ Tests failed. Push aborted."
exit 1
fi
Expand Down
41 changes: 41 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -62,6 +62,7 @@ The configuration file is located at:
```sh
~/.config/leetcode-cli/config.toml
```

and should look like this:

```toml
Expand All @@ -84,6 +85,8 @@ Login to LeetCode and obtain the csrftoken from the cookie value.

## Usage

### Global Commands

For more details on available commands, run:

```sh
Expand All @@ -96,6 +99,44 @@ For a specific command, run:
leetcode_cli <command> --help
```

### Local Configuration

When you start a problem using `leetcode_cli start --id <problem_id>`, the tool automatically creates a `.leetcode-cli` file in the problem directory. This file contains:

```toml
problem_id = 42
problem_name = "trapping_rain_water"
language = "Rust"
```

### Working with Problems

Once you're in a problem directory (one that contains a `.leetcode-cli` file), you can run commands without specifying the problem ID:

```sh
# Start a problem (creates the local config)
leetcode_cli start --id 42 --lang rust

# Navigate to the problem directory
cd ~/leetcode/42_trapping_rain_water

# Test your solution (automatically detects problem ID and main file)
leetcode_cli test

# Submit your solution (automatically detects problem ID and main file)
leetcode_cli submit

# You can still override the defaults if needed
leetcode_cli test --id 42 --file src/custom_solution.rs
```

#### Supported Commands

- `info --id <id>`: Get problem information (ID required)
- `start --id <id> [--lang <language>]`: Start working on a problem (creates local config)
- `test [--id <id>] [--file <path>]`: Test your solution (uses local config if available)
- `submit [--id <id>] [--file <path>]`: Submit your solution (uses local config if available)

## Contributing

Contributions are welcome! Feel free to open issues or submit pull requests to improve the tool.
8 changes: 4 additions & 4 deletions src/cli.rs
Original file line number Diff line number Diff line change
Expand Up @@ -24,15 +24,15 @@ pub enum Commands {
},
Test {
#[arg(short = 'i', long)]
id: u32,
id: Option<u32>,
#[arg(short = 'p', long = "file")]
path_to_file: String,
path_to_file: Option<String>,
},
Submit {
#[arg(short = 'i', long)]
id: u32,
id: Option<u32>,

#[arg(short = 'p', long = "file")]
path_to_file: String,
path_to_file: Option<String>,
},
}
11 changes: 8 additions & 3 deletions src/leetcode_api_runner.rs
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ use nanohtml2text::html2text;

use crate::{
config::RuntimeConfigSetup,
local_config::LocalConfig,
readme_parser::LeetcodeReadmeParser,
test_generator::TestGenerator,
utils::*,
Expand Down Expand Up @@ -99,6 +100,11 @@ impl LeetcodeApiRunner {
write_readme(&pb_dir, id, &pb_name, &md_desc)?;
write_to_file(&src_dir, &get_file_name(&lang), &file_content)?;

// Create local config file
let local_config =
LocalConfig::new(id, pb_name.clone(), language_to_string(&lang));
local_config.write_to_dir(&pb_dir)?;

let success_message = format!(
"{}: {} created at \n{}\nin {}.",
id,
Expand Down Expand Up @@ -154,15 +160,14 @@ impl LeetcodeApiRunner {

pub async fn test_response(
&self, id: u32, path_to_file: &String,
) -> io::Result<()> {
) -> io::Result<String> {
let problem_info = self.api.set_problem_by_id(id).await?;
let file_content = std::fs::read_to_string(path_to_file)
.expect("Unable to read the file");
let language = get_language_from_extension(path_to_file);

let test_res = problem_info.send_test(language, &file_content).await?;
println!("Test response for problem {id}: {test_res:?}");
Ok(())
Ok(format!("Test response for problem {id}: \n{test_res:#?}"))
}

pub async fn submit_response(
Expand Down
2 changes: 2 additions & 0 deletions src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ pub mod cli;
pub mod code_signature;
pub mod config;
pub mod leetcode_api_runner;
pub mod local_config;
pub mod readme_parser;
pub mod test_generator;
pub mod utils;
Expand All @@ -12,3 +13,4 @@ pub use cli::{
};
pub use config::RuntimeConfigSetup;
pub use leetcode_api_runner::LeetcodeApiRunner;
pub use local_config::LocalConfig;
168 changes: 168 additions & 0 deletions src/local_config.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,168 @@
use std::{
fs,
io,
path::Path,
};

use serde::{
Deserialize,
Serialize,
};

#[derive(Deserialize, Serialize, Debug, Clone)]
pub struct LocalConfig {
pub problem_id: u32,
pub problem_name: String,
pub language: String,
}

impl LocalConfig {
pub fn new(
problem_id: u32, problem_name: String, language: String,
) -> Self {
Self { problem_id, problem_name, language }
}

/// Find and read local config from current directory or parent directories
pub fn find_and_read() -> io::Result<Option<Self>> {
let mut current_dir = std::env::current_dir()?;

loop {
let config_path = current_dir.join(".leetcode-cli");
if config_path.exists() {
let content = fs::read_to_string(&config_path)?;
let config: LocalConfig =
toml::from_str(&content).map_err(|e| {
io::Error::new(io::ErrorKind::InvalidData, e)
})?;
return Ok(Some(config));
}

if !current_dir.pop() {
break;
}
}

Ok(None)
}

/// Write local config to specified directory
pub fn write_to_dir(&self, dir: &Path) -> io::Result<()> {
let config_path = dir.join(".leetcode-cli");
let content = toml::to_string(self)
.map_err(|e| io::Error::new(io::ErrorKind::InvalidData, e))?;
fs::write(config_path, content)
}

/// Read local config from specified file path
pub fn read_from_path(path: &Path) -> io::Result<Self> {
let content = fs::read_to_string(path)?;
toml::from_str(&content)
.map_err(|e| io::Error::new(io::ErrorKind::InvalidData, e))
}

/// Get the main source file name based on language
pub fn get_main_file(&self) -> String {
match self.language.to_lowercase().as_str() {
"rust" => "main.rs".to_string(),
"python" | "python3" => "main.py".to_string(),
"javascript" => "main.js".to_string(),
"typescript" => "main.ts".to_string(),
"go" => "main.go".to_string(),
"java" => "Main.java".to_string(),
"c++" => "main.cpp".to_string(),
"c" => "main.c".to_string(),
_ => "main.txt".to_string(),
}
}

/// Resolve problem ID and file path from CLI args or local config
pub fn resolve_problem_params(
id: Option<u32>, path_to_file: Option<String>,
) -> io::Result<(u32, String)> {
match (id, &path_to_file) {
(Some(id), Some(path)) => Ok((id, path.clone())),
_ => {
// Try to find local config
match Self::find_and_read()? {
Some(config) => {
let problem_id = id.unwrap_or(config.problem_id);
let file_path = path_to_file.unwrap_or_else(|| {
format!("src/{}", config.get_main_file())
});
Ok((problem_id, file_path))
},
None => {
if id.is_none() {
return Err(io::Error::new(
io::ErrorKind::NotFound,
"No problem ID provided and no .leetcode-cli \
config found. Either provide --id or run \
from a problem directory",
));
}
if path_to_file.is_none() {
return Err(io::Error::new(
io::ErrorKind::NotFound,
"No file path provided",
));
}
Copy link

Copilot AI Aug 3, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This condition will never be reached because the outer match pattern _ only matches when path_to_file is None, making this check redundant and creating unreachable code.

Suggested change
}
// The check for path_to_file.is_none() is unreachable and has been removed.

Copilot uses AI. Check for mistakes.
// If we get here, both id and path_to_file must be Some
match (id, path_to_file) {
(Some(id), Some(path)) => Ok((id, path)),
_ => Err(io::Error::other(
"Unexpected error: id or path_to_file missing \
after checks",
)),
}
},
}
},
}
}
}

#[cfg(test)]
mod tests {
use tempfile::TempDir;

use super::*;

#[test]
fn test_local_config_creation() {
let config =
LocalConfig::new(1, "two_sum".to_string(), "Rust".to_string());

assert_eq!(config.problem_id, 1);
assert_eq!(config.problem_name, "two_sum");
assert_eq!(config.language, "Rust");
}

#[test]
fn test_write_and_read_config() {
let temp_dir = TempDir::new().unwrap();
let config =
LocalConfig::new(1, "two_sum".to_string(), "Rust".to_string());

config.write_to_dir(temp_dir.path()).unwrap();

let config_path = temp_dir.path().join(".leetcode-cli");
assert!(config_path.exists());

let read_config = LocalConfig::read_from_path(&config_path).unwrap();
assert_eq!(read_config.problem_id, 1);
assert_eq!(read_config.problem_name, "two_sum");
assert_eq!(read_config.language, "Rust");
}

#[test]
fn test_get_main_file() {
let config =
LocalConfig::new(1, "two_sum".to_string(), "Rust".to_string());
assert_eq!(config.get_main_file(), "main.rs");

let config =
LocalConfig::new(1, "two_sum".to_string(), "Python".to_string());
assert_eq!(config.get_main_file(), "main.py");
}
}
22 changes: 17 additions & 5 deletions src/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ use leetcode_cli::{
Cli,
Commands,
LeetcodeApiRunner,
LocalConfig,
RuntimeConfigSetup,
};

Expand Down Expand Up @@ -57,30 +58,41 @@ async fn main() -> Result<(), Box<dyn std::error::Error>> {
let start_problem = api_runner.start_problem(*id, lang).await;
stop_and_clear_spinner(spin);
match start_problem {
Ok((success_message, _, warning)) => {
Ok((success_message, pb_dir, warning)) => {
if let Some(warning) = warning {
eprintln!("{warning}");
}
println!("{success_message}");
println!("\nHappy coding :)");
println!(
"\n(ps: to use local config feature, you should \ncd \
{}\n;)",
pb_dir.display()
);
},
Err(e) => eprintln!("Error starting problem: {e}"),
}
},
Commands::Test { id, path_to_file } => {
let (problem_id, file_path) =
LocalConfig::resolve_problem_params(*id, path_to_file.clone())?;

let spin = spin_the_spinner("Running tests...");
let test_result =
api_runner.test_response(*id, &path_to_file.clone()).await;
api_runner.test_response(problem_id, &file_path).await;
stop_and_clear_spinner(spin);
match test_result {
Ok(_) => println!("Test result"),
Err(e) => eprintln!("Error running tests: {e}"),
Ok(message) => println!("{message}"),
Err(e) => eprintln!("Error running tests:\n{e}"),
}
},
Commands::Submit { id, path_to_file } => {
let (problem_id, file_path) =
LocalConfig::resolve_problem_params(*id, path_to_file.clone())?;

let spin = spin_the_spinner("Submitting solution...");
let submit_result =
api_runner.submit_response(*id, &path_to_file.clone()).await;
api_runner.submit_response(problem_id, &file_path).await;
stop_and_clear_spinner(spin);
match submit_result {
Ok(_) => println!("Submit result"),
Expand Down
Loading