diff --git a/Cargo.lock b/Cargo.lock index 10672ee..a522434 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1133,7 +1133,7 @@ checksum = "09edd9e8b54e49e587e4f6295a7d29c3ea94d469cb40ab8ca70b288248a81db2" [[package]] name = "leetcode-cli" -version = "0.5.4" +version = "0.5.5" dependencies = [ "aes", "anyhow", diff --git a/Cargo.toml b/Cargo.toml index 905b8fe..b7d17dd 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -4,7 +4,7 @@ path = "src/bin/lc.rs" [package] name = "leetcode-cli" -version = "0.5.4" +version = "0.5.5" authors = ["clearloop "] edition = "2024" description = "Leetcode command-line interface in rust." diff --git a/docs/configuration.md b/docs/configuration.md index 7bfcdd2..89e7053 100644 --- a/docs/configuration.md +++ b/docs/configuration.md @@ -10,6 +10,7 @@ Controls the editor that opens and the code that gets generated. ```toml [code] +# Overridden by $VISUAL, then $EDITOR, if either is set editor = 'vim' # Extra arguments passed to the editor editor_args = ['-nw'] @@ -23,7 +24,7 @@ test = true | Key | Default | Description | | --- | --- | --- | -| `editor` | `'vim'` | Command used to open the solution file. | +| `editor` | `'vim'` | Command used to open the solution file. `$VISUAL` wins over `$EDITOR`, and both win over this key. | | `editor_args` | — | Extra arguments passed before the file path. | | `editor_envs` | — | Environment variables for the editor process, each as `"NAME=VALUE"`. | | `lang` | `'rust'` | Language of the generated file. `leetcode edit --lang ` overrides this per-call and persists it. | diff --git a/src/cache/mod.rs b/src/cache/mod.rs index 8ec75a9..fc45256 100644 --- a/src/cache/mod.rs +++ b/src/cache/mod.rs @@ -158,8 +158,8 @@ impl Cache { println!( "\n[{}] {} {}\n\n", - &ids, - &target.name.bold().underline(), + ids, + target.name.bold().underline(), "is on the run...".dimmed() ); @@ -178,7 +178,7 @@ impl Cache { .await? .json() .await?; - debug!("{:#?}", &json); + debug!("{:#?}", json); match parser::desc(&mut rdesc, json) { None => return Err(Error::NoneError), Some(false) => { @@ -202,13 +202,13 @@ impl Cache { } pub async fn get_tagged_questions(self, rslug: &str) -> Result, Error> { - trace!("Geting {} questions...", &rslug); + trace!("Geting {} questions...", rslug); let ids: Vec; let rtag = tags .filter(tag.eq(rslug.to_string())) .first::(&mut self.conn()?); if let Ok(t) = rtag { - trace!("Got {} questions from local cache...", &rslug); + trace!("Got {} questions from local cache...", rslug); ids = serde_json::from_str(&t.refs)?; } else { ids = parser::tags( diff --git a/src/cache/models.rs b/src/cache/models.rs index 79f4a70..6262417 100644 --- a/src/cache/models.rs +++ b/src/cache/models.rs @@ -302,7 +302,7 @@ impl std::fmt::Display for VerifyResult { _ => self.expected.expected_code_answer.join("↩ "), }; - debug!("{:#?}", &self); + debug!("{:#?}", self); match &self.status.status_code { 10 => { @@ -311,14 +311,14 @@ impl std::fmt::Display for VerifyResult { write!( f, "\n{}{}{}\n{}{}{}{}{}{}\n", - &self.status.status_msg.green().bold(), - &"Runtime: ".before_spaces(7).dimmed(), - &self.status.status_runtime.dimmed(), - &"\nYour input:".after_spaces(4), - &self.data_input.replace('\n', "↩ "), - &"\nOutput:".after_spaces(8), + self.status.status_msg.green().bold(), + "Runtime: ".before_spaces(7).dimmed(), + self.status.status_runtime.dimmed(), + "\nYour input:".after_spaces(4), + self.data_input.replace('\n', "↩ "), + "\nOutput:".after_spaces(8), ca, - &"\nExpected:".after_spaces(6), + "\nExpected:".after_spaces(6), eca, )? } else if matches!(self.result_type, Run::Submit) @@ -375,17 +375,17 @@ impl std::fmt::Display for VerifyResult { {} {}.\n\n", "Success\n\n".green().bold(), "Runtime: ".dimmed(), - &self.status.status_runtime.bold(), + self.status.status_runtime.bold(), rp.to_string().bold(), "% ".bold(), - &self.pretty_lang, - &self.name, + self.pretty_lang, + self.name, "Memory Usage: ".dimmed(), - &self.status.status_memory.bold(), + self.status.status_memory.bold(), mp.to_string().bold(), "% ".bold(), - &self.pretty_lang, - &self.name, + self.pretty_lang, + self.name, )? } else { // Wrong Answer during testing @@ -394,12 +394,12 @@ impl std::fmt::Display for VerifyResult { "\n{}{}{}\n{}{}{}{}{}{}\n", "Wrong Answer".red().bold(), " Runtime: ".dimmed(), - &self.status.status_runtime.dimmed(), - &"\nYour input:".after_spaces(4), - &self.data_input.replace('\n', "↩ "), - &"\nOutput:".after_spaces(8), + self.status.status_runtime.dimmed(), + "\nYour input:".after_spaces(4), + self.data_input.replace('\n', "↩ "), + "\nOutput:".after_spaces(8), ca, - &"\nExpected:".after_spaces(6), + "\nExpected:".after_spaces(6), eca, )? } @@ -408,57 +408,55 @@ impl std::fmt::Display for VerifyResult { 11 => write!( f, "\n{}\n\n{}{}\n{}{}\n{}{}{}{}{}{}\n", - &self.status.status_msg.red().bold(), + self.status.status_msg.red().bold(), "Cases passed:".after_spaces(2).green(), - &self - .analyse + self.analyse .total_correct .as_ref() .unwrap_or(&Number::from(0)) .to_string() .green(), - &"Total cases:".after_spaces(3).yellow(), - &self - .analyse + "Total cases:".after_spaces(3).yellow(), + self.analyse .total_testcases .as_ref() .unwrap_or(&Number::from(0)) .to_string() .bold() .yellow(), - &"Last case:".after_spaces(5).dimmed(), - &self.submit.last_testcase.replace('\n', "↩ ").dimmed(), - &"\nOutput:".after_spaces(8), + "Last case:".after_spaces(5).dimmed(), + self.submit.last_testcase.replace('\n', "↩ ").dimmed(), + "\nOutput:".after_spaces(8), self.code_output[0], - &"\nExpected:".after_spaces(6), + "\nExpected:".after_spaces(6), self.expected_output[0], )?, // Memory Exceeded 12 => write!( f, "\n{}\n\n{}{}\n", - &self.status.status_msg.yellow().bold(), - &"Last case:".after_spaces(5).dimmed(), - &self.data_input.replace('\n', "↩ "), + self.status.status_msg.yellow().bold(), + "Last case:".after_spaces(5).dimmed(), + self.data_input.replace('\n', "↩ "), )?, // Output Timeout Exceeded // // TODO: 13 and 14 might have some different, // if anybody reach this, welcome to fix this! - 13 | 14 => write!(f, "\n{}\n", &self.status.status_msg.yellow().bold(),)?, + 13 | 14 => write!(f, "\n{}\n", self.status.status_msg.yellow().bold(),)?, // Runtime error 15 => write!( f, "\n{}\n{}\n'", - &self.status.status_msg.red().bold(), - &self.status.runtime_error + self.status.status_msg.red().bold(), + self.status.runtime_error )?, // Compile Error 20 => write!( f, "\n{}:\n\n{}\n", - &self.status.status_msg.red().bold(), - &self.error.full_compile_error.dimmed() + self.status.status_msg.red().bold(), + self.error.full_compile_error.dimmed() )?, _ => write!( f, @@ -482,8 +480,8 @@ impl std::fmt::Display for VerifyResult { write!( f, "{}{}", - &"Stdout:".after_spaces(8).purple(), - &self.code_output.join(&"\n".after_spaces(15)) + "Stdout:".after_spaces(8).purple(), + self.code_output.join(&"\n".after_spaces(15)) ) } else { write!(f, "") @@ -494,8 +492,8 @@ impl std::fmt::Display for VerifyResult { write!( f, "{}{}", - &"Stdout:".after_spaces(8).purple(), - &self.std_output[0].replace('\n', &"\n".after_spaces(15)) + "Stdout:".after_spaces(8).purple(), + self.std_output[0].replace('\n', &"\n".after_spaces(15)) ) } else { write!(f, "") diff --git a/src/config/code.rs b/src/config/code.rs index 6e9be74..f226ef4 100644 --- a/src/config/code.rs +++ b/src/config/code.rs @@ -17,10 +17,14 @@ fn is_default_bool(t: &bool) -> bool { !t } +fn default_editor() -> String { + "vim".into() +} + /// Code config #[derive(Clone, Debug, Deserialize, Serialize)] pub struct Code { - #[serde(default)] + #[serde(default = "default_editor")] pub editor: String, #[serde(rename(serialize = "editor-args"), alias = "editor-args", default)] pub editor_args: Option>, @@ -47,10 +51,24 @@ pub struct Code { pub pick: String, } +impl Code { + /// `$VISUAL` and `$EDITOR` take precedence over the configured editor. + pub fn with_env_override(mut self) -> Self { + for key in ["EDITOR", "VISUAL"] { + if let Ok(editor) = std::env::var(key) + && !editor.is_empty() + { + self.editor = editor; + } + } + self + } +} + impl Default for Code { fn default() -> Self { Self { - editor: "vim".into(), + editor: default_editor(), editor_args: None, editor_envs: None, edit_code_marker: false, diff --git a/src/config/mod.rs b/src/config/mod.rs index de8bffc..088ee9e 100644 --- a/src/config/mod.rs +++ b/src/config/mod.rs @@ -10,7 +10,7 @@ use crate::{ config::{code::Code, cookies::Cookies, storage::Storage, sys::Sys}, }; use serde::{Deserialize, Serialize}; -use std::{fs, path::Path}; +use std::{fs, path::Path, str::FromStr}; mod code; mod cookies; @@ -44,34 +44,18 @@ impl Config { Self::write_default(&conf)?; } - let s = fs::read_to_string(&conf)?; - match toml::from_str::(&s) { - Ok(mut config) => { - // Override config.cookies with environment variables - config.cookies = config.cookies.with_env_override(); - - match config.cookies.site { - cookies::LeetcodeSite::LeetcodeCom => Ok(config), - cookies::LeetcodeSite::LeetcodeCn => { - let mut config = config; - config.sys.urls = sys::Urls::new_with_leetcode_cn(); - Ok(config) - } - } - } - Err(e) => { - let tmp = Self::root()?.join("leetcode.tmp.toml"); - Self::write_default(tmp)?; - Err(e.into()) - } - } + fs::read_to_string(&conf)? + .parse::() + .inspect_err(|_| { + let _ = Self::write_default(conf.with_file_name("leetcode.tmp.toml")); + }) } /// Get root path of leetcode-cli pub fn root() -> Result { let dir = dirs::home_dir().ok_or(Error::NoneError)?.join(".leetcode"); if !dir.is_dir() { - info!("Generate root dir at {:?}.", &dir); + info!("Generate root dir at {:?}.", dir); fs::DirBuilder::new().recursive(true).create(&dir)?; } @@ -87,3 +71,21 @@ impl Config { Ok(()) } } + +impl FromStr for Config { + type Err = Error; + + /// Parses `leetcode.toml`, applying the environment overrides on top of it. + fn from_str(s: &str) -> Result { + let mut config: Config = toml::from_str(s)?; + + config.code = config.code.with_env_override(); + config.cookies = config.cookies.with_env_override(); + + if let cookies::LeetcodeSite::LeetcodeCn = config.cookies.site { + config.sys.urls = sys::Urls::new_with_leetcode_cn(); + } + + Ok(config) + } +} diff --git a/src/config/storage.rs b/src/config/storage.rs index e3ee69f..c5f3080 100644 --- a/src/config/storage.rs +++ b/src/config/storage.rs @@ -39,7 +39,7 @@ impl Storage { pub fn cache(&self) -> Result { let root = PathBuf::from(self.root()?); if !root.exists() { - info!("Generate cache dir at {:?}.", &root); + info!("Generate cache dir at {:?}.", root); fs::DirBuilder::new().recursive(true).create(&root)?; } diff --git a/src/plugins/leetcode.rs b/src/plugins/leetcode.rs index 4e67b8c..582db2d 100644 --- a/src/plugins/leetcode.rs +++ b/src/plugins/leetcode.rs @@ -62,7 +62,7 @@ impl LeetCode { /// Get category problems pub async fn get_category_problems(self, category: &str) -> Result { - trace!("Requesting {} problems...", &category); + trace!("Requesting {} problems...", category); let url = &self.conf.sys.urls.problems(category); Req { @@ -79,7 +79,7 @@ impl LeetCode { } pub async fn get_question_ids_by_tag(self, slug: &str) -> Result { - trace!("Requesting {} ref problems...", &slug); + trace!("Requesting {} ref problems...", slug); let url = &self.conf.sys.urls.graphql; let mut json: Json = HashMap::new(); json.insert("operationName", "getTopicTag".to_string()); @@ -196,7 +196,7 @@ impl LeetCode { /// Get specific problem detail pub async fn get_question_detail(self, slug: &str) -> Result { - trace!("Requesting {} detail...", &slug); + trace!("Requesting {} detail...", slug); let refer = self.conf.sys.urls.problem(slug); let mut json: Json = HashMap::new(); json.insert( @@ -302,9 +302,9 @@ mod req { impl Req { pub async fn send(self, client: &Client) -> Result { - trace!("Running leetcode::{}...", &self.name); + trace!("Running leetcode::{}...", self.name); if self.info { - info!("{}", &self.name); + info!("{}", self.name); } let url = self.url.to_owned(); let headers = LeetCode::headers( diff --git a/tests/config_code.rs b/tests/config_code.rs new file mode 100644 index 0000000..ebada0f --- /dev/null +++ b/tests/config_code.rs @@ -0,0 +1,38 @@ +use leetcode_cli::Config; + +const CONFIG: &str = r#" +[code] +editor = 'vim' +lang = 'rust' + +[cookies] +csrf = '' +session = '' +site = 'leetcode.com' + +[storage] +code = 'code' +root = '~/.leetcode' +scripts = 'scripts' +"#; + +/// One test: `set_var` is process-wide, so splitting these would race. +#[test] +fn env_overrides_the_configured_editor() { + let editor = || CONFIG.parse::().unwrap().code.editor; + + unsafe { + std::env::remove_var("EDITOR"); + std::env::remove_var("VISUAL"); + } + assert_eq!(editor(), "vim"); + + unsafe { std::env::set_var("EDITOR", "nano") }; + assert_eq!(editor(), "nano"); + + unsafe { std::env::set_var("VISUAL", "nvim") }; + assert_eq!(editor(), "nvim"); + + unsafe { std::env::set_var("VISUAL", "") }; + assert_eq!(editor(), "nano"); +}