From a8353cf5557b5414b4e60530c164202423da38e1 Mon Sep 17 00:00:00 2001 From: Wojciech Pietrzak Date: Sun, 9 Aug 2026 08:59:21 +0200 Subject: [PATCH 01/11] feat(env): respect environment variables for editor --- docs/configuration.md | 3 +- src/config/code.rs | 16 ++++++++-- tests/config_code.rs | 69 +++++++++++++++++++++++++++++++++++++++++++ 3 files changed, 85 insertions(+), 3 deletions(-) create mode 100644 tests/config_code.rs diff --git a/docs/configuration.md b/docs/configuration.md index 7bfcdd2..667bf1f 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] +# Falls back to $VISUAL, then $EDITOR, then 'vim' 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` | `$VISUAL` / `$EDITOR` / `'vim'` | Command used to open the solution file. | | `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/config/code.rs b/src/config/code.rs index 6e9be74..c6de4d8 100644 --- a/src/config/code.rs +++ b/src/config/code.rs @@ -17,10 +17,22 @@ fn is_default_bool(t: &bool) -> bool { !t } +fn default_editor() -> String { + std::env::var_os("VISUAL") + .and_then(|value| value.into_string().ok()) + .filter(|value| !value.is_empty()) + .or_else(|| { + std::env::var_os("EDITOR") + .and_then(|value| value.into_string().ok()) + .filter(|value| !value.is_empty()) + }) + .unwrap_or_else(|| "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>, @@ -50,7 +62,7 @@ pub struct Code { 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/tests/config_code.rs b/tests/config_code.rs new file mode 100644 index 0000000..46a7d25 --- /dev/null +++ b/tests/config_code.rs @@ -0,0 +1,69 @@ +use leetcode_cli::Config; +use std::{ + env, + sync::{Mutex, OnceLock}, +}; + +fn env_lock() -> &'static Mutex<()> { + static LOCK: OnceLock> = OnceLock::new(); + LOCK.get_or_init(|| Mutex::new(())) +} + +fn with_env(editor: Option<&str>, visual: Option<&str>, f: impl FnOnce()) { + let _guard = env_lock().lock().unwrap(); + + let old_editor = env::var_os("EDITOR"); + let old_visual = env::var_os("VISUAL"); + + unsafe { + match editor { + Some(value) => env::set_var("EDITOR", value), + None => env::remove_var("EDITOR"), + } + match visual { + Some(value) => env::set_var("VISUAL", value), + None => env::remove_var("VISUAL"), + } + } + + f(); + + unsafe { + match old_editor { + Some(value) => env::set_var("EDITOR", value), + None => env::remove_var("EDITOR"), + } + match old_visual { + Some(value) => env::set_var("VISUAL", value), + None => env::remove_var("VISUAL"), + } + } +} + +#[test] +fn config_uses_editor_from_visual_env_var_when_set() { + with_env(None, Some("nvim"), || { + assert_eq!(Config::default().code.editor, "nvim"); + }); +} + +#[test] +fn config_uses_editor_from_editor_env_var_when_set() { + with_env(Some("nvim"), None, || { + assert_eq!(Config::default().code.editor, "nvim"); + }); +} + +#[test] +fn config_falls_back_to_vim_on_empty_env_vars() { + with_env(None, None, || { + assert_eq!(Config::default().code.editor, "vim"); + }); +} + +#[test] +fn config_prefers_visual_over_editor() { + with_env(Some("nvim"), Some("vim"), || { + assert_eq!(Config::default().code.editor, "vim"); + }); +} From d97931bff715d09c25bc3c404fca1f0330a22192 Mon Sep 17 00:00:00 2001 From: Wojciech Pietrzak Date: Sun, 9 Aug 2026 09:28:00 +0200 Subject: [PATCH 02/11] fix(tests): clippy works again --- src/cache/mod.rs | 10 +++--- src/cache/models.rs | 80 ++++++++++++++++++++--------------------- src/config/mod.rs | 2 +- src/config/storage.rs | 2 +- src/plugins/leetcode.rs | 10 +++--- 5 files changed, 51 insertions(+), 53 deletions(-) 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/mod.rs b/src/config/mod.rs index de8bffc..7a3c490 100644 --- a/src/config/mod.rs +++ b/src/config/mod.rs @@ -71,7 +71,7 @@ impl Config { 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)?; } 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( From fb23e278dfa49508e21d0860e5630b69c99936f4 Mon Sep 17 00:00:00 2001 From: Wojciech Pietrzak Date: Mon, 10 Aug 2026 09:18:02 +0200 Subject: [PATCH 03/11] Apply suggestion from @clearloop Co-authored-by: clearloop --- docs/configuration.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/configuration.md b/docs/configuration.md index 667bf1f..82ea674 100644 --- a/docs/configuration.md +++ b/docs/configuration.md @@ -10,7 +10,7 @@ Controls the editor that opens and the code that gets generated. ```toml [code] -# Falls back to $VISUAL, then $EDITOR, then 'vim' +# Overridden by $VISUAL, then $EDITOR, if either is set editor = 'vim' # Extra arguments passed to the editor editor_args = ['-nw'] From 3edc9585950f8de4ac782accd4fc02516c21484f Mon Sep 17 00:00:00 2001 From: Wojciech Pietrzak Date: Mon, 10 Aug 2026 09:18:24 +0200 Subject: [PATCH 04/11] Apply suggestion from @clearloop Co-authored-by: clearloop --- docs/configuration.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/configuration.md b/docs/configuration.md index 82ea674..89e7053 100644 --- a/docs/configuration.md +++ b/docs/configuration.md @@ -24,7 +24,7 @@ test = true | Key | Default | Description | | --- | --- | --- | -| `editor` | `$VISUAL` / `$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. | From b0c1a33c516a518fcf7479a921099b255f9b89cf Mon Sep 17 00:00:00 2001 From: Wojciech Pietrzak Date: Mon, 10 Aug 2026 09:19:53 +0200 Subject: [PATCH 05/11] Apply suggestion from @clearloop Co-authored-by: clearloop --- src/config/code.rs | 10 +--------- 1 file changed, 1 insertion(+), 9 deletions(-) diff --git a/src/config/code.rs b/src/config/code.rs index c6de4d8..3c0ee19 100644 --- a/src/config/code.rs +++ b/src/config/code.rs @@ -18,15 +18,7 @@ fn is_default_bool(t: &bool) -> bool { } fn default_editor() -> String { - std::env::var_os("VISUAL") - .and_then(|value| value.into_string().ok()) - .filter(|value| !value.is_empty()) - .or_else(|| { - std::env::var_os("EDITOR") - .and_then(|value| value.into_string().ok()) - .filter(|value| !value.is_empty()) - }) - .unwrap_or_else(|| "vim".into()) + "vim".into() } /// Code config From 366b41db13e7cbe3a1a1a1d6fe07a2343d6cb9bc Mon Sep 17 00:00:00 2001 From: Wojciech Pietrzak Date: Mon, 10 Aug 2026 09:20:27 +0200 Subject: [PATCH 06/11] Apply suggestion from @clearloop Co-authored-by: clearloop --- tests/config_code.rs | 86 +++++++++++++++++++++++++++----------------- 1 file changed, 53 insertions(+), 33 deletions(-) diff --git a/tests/config_code.rs b/tests/config_code.rs index 46a7d25..95e65ae 100644 --- a/tests/config_code.rs +++ b/tests/config_code.rs @@ -4,66 +4,86 @@ use std::{ sync::{Mutex, OnceLock}, }; +const CONFIG: &str = r#" +[code] +editor = 'vim' +lang = 'rust' + +[cookies] +csrf = '' +session = '' +site = 'leetcode.com' + +[storage] +code = 'code' +root = '~/.leetcode' +scripts = 'scripts' +"#; + fn env_lock() -> &'static Mutex<()> { static LOCK: OnceLock> = OnceLock::new(); LOCK.get_or_init(|| Mutex::new(())) } -fn with_env(editor: Option<&str>, visual: Option<&str>, f: impl FnOnce()) { +/// Parse `toml` the way `Config::locate` does, then apply the env override. +fn editor_of(toml: &str, editor: Option<&str>, visual: Option<&str>) -> String { let _guard = env_lock().lock().unwrap(); let old_editor = env::var_os("EDITOR"); let old_visual = env::var_os("VISUAL"); unsafe { - match editor { - Some(value) => env::set_var("EDITOR", value), - None => env::remove_var("EDITOR"), - } - match visual { - Some(value) => env::set_var("VISUAL", value), - None => env::remove_var("VISUAL"), - } + set("EDITOR", editor); + set("VISUAL", visual); } - f(); + let config: Config = toml::from_str(toml).unwrap(); + let editor = config.code.with_env_override().editor; unsafe { - match old_editor { - Some(value) => env::set_var("EDITOR", value), - None => env::remove_var("EDITOR"), - } - match old_visual { - Some(value) => env::set_var("VISUAL", value), - None => env::remove_var("VISUAL"), + set("EDITOR", old_editor.as_deref().and_then(|v| v.to_str())); + set("VISUAL", old_visual.as_deref().and_then(|v| v.to_str())); + } + + editor +} + +unsafe fn set(key: &str, value: Option<&str>) { + unsafe { + match value { + Some(value) => env::set_var(key, value), + None => env::remove_var(key), } } } #[test] -fn config_uses_editor_from_visual_env_var_when_set() { - with_env(None, Some("nvim"), || { - assert_eq!(Config::default().code.editor, "nvim"); - }); +fn visual_overrides_the_configured_editor() { + assert_eq!(editor_of(CONFIG, None, Some("nvim")), "nvim"); +} + +#[test] +fn editor_overrides_the_configured_editor() { + assert_eq!(editor_of(CONFIG, Some("nvim"), None), "nvim"); +} + +#[test] +fn visual_takes_precedence_over_editor() { + assert_eq!(editor_of(CONFIG, Some("nano"), Some("nvim")), "nvim"); } #[test] -fn config_uses_editor_from_editor_env_var_when_set() { - with_env(Some("nvim"), None, || { - assert_eq!(Config::default().code.editor, "nvim"); - }); +fn the_configured_editor_is_kept_without_env() { + assert_eq!(editor_of(CONFIG, None, None), "vim"); } #[test] -fn config_falls_back_to_vim_on_empty_env_vars() { - with_env(None, None, || { - assert_eq!(Config::default().code.editor, "vim"); - }); +fn empty_env_vars_are_ignored() { + assert_eq!(editor_of(CONFIG, Some(""), Some("")), "vim"); } #[test] -fn config_prefers_visual_over_editor() { - with_env(Some("nvim"), Some("vim"), || { - assert_eq!(Config::default().code.editor, "vim"); - }); +fn vim_is_the_fallback_when_nothing_is_set() { + let bare = CONFIG.replace("editor = 'vim'\n", ""); + assert_eq!(editor_of(&bare, None, None), "vim"); } From a6a77735870d1419bcd7ea43ed79d2f6432eb9a7 Mon Sep 17 00:00:00 2001 From: Wojciech Pietrzak Date: Mon, 10 Aug 2026 09:21:49 +0200 Subject: [PATCH 07/11] Apply suggestion from @clearloop Co-authored-by: clearloop --- src/config/code.rs | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/src/config/code.rs b/src/config/code.rs index 3c0ee19..f226ef4 100644 --- a/src/config/code.rs +++ b/src/config/code.rs @@ -51,6 +51,20 @@ 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 { From 5facb993bbdd65d66695831b1abed908bfd7a9c0 Mon Sep 17 00:00:00 2001 From: clearloop Date: Mon, 10 Aug 2026 19:06:19 +0800 Subject: [PATCH 08/11] fix(config): run the editor env override on the real load path --- src/config/mod.rs | 36 ++++++++++----------- tests/config_code.rs | 77 ++++++++------------------------------------ 2 files changed, 31 insertions(+), 82 deletions(-) diff --git a/src/config/mod.rs b/src/config/mod.rs index 7a3c490..9c1f316 100644 --- a/src/config/mod.rs +++ b/src/config/mod.rs @@ -36,6 +36,20 @@ impl Config { Ok(()) } + /// Parse a config, applying the environment overrides on top of it + pub fn parse(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) + } + /// Locate lc's config file pub fn locate() -> Result { let conf = Self::root()?.join("leetcode.toml"); @@ -44,25 +58,11 @@ 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) - } - } - } + match Self::parse(&fs::read_to_string(&conf)?) { + Ok(config) => Ok(config), Err(e) => { - let tmp = Self::root()?.join("leetcode.tmp.toml"); - Self::write_default(tmp)?; - Err(e.into()) + Self::write_default(Self::root()?.join("leetcode.tmp.toml"))?; + Err(e) } } } diff --git a/tests/config_code.rs b/tests/config_code.rs index 95e65ae..92319c2 100644 --- a/tests/config_code.rs +++ b/tests/config_code.rs @@ -1,8 +1,4 @@ use leetcode_cli::Config; -use std::{ - env, - sync::{Mutex, OnceLock}, -}; const CONFIG: &str = r#" [code] @@ -20,70 +16,23 @@ root = '~/.leetcode' scripts = 'scripts' "#; -fn env_lock() -> &'static Mutex<()> { - static LOCK: OnceLock> = OnceLock::new(); - LOCK.get_or_init(|| Mutex::new(())) -} - -/// Parse `toml` the way `Config::locate` does, then apply the env override. -fn editor_of(toml: &str, editor: Option<&str>, visual: Option<&str>) -> String { - let _guard = env_lock().lock().unwrap(); - - let old_editor = env::var_os("EDITOR"); - let old_visual = env::var_os("VISUAL"); - - unsafe { - set("EDITOR", editor); - set("VISUAL", visual); - } - - let config: Config = toml::from_str(toml).unwrap(); - let editor = config.code.with_env_override().editor; - - unsafe { - set("EDITOR", old_editor.as_deref().and_then(|v| v.to_str())); - set("VISUAL", old_visual.as_deref().and_then(|v| v.to_str())); - } - - editor -} +/// One test: `set_var` is process-wide, so splitting these would race. +#[test] +fn env_overrides_the_configured_editor() { + let editor = || Config::parse(CONFIG).unwrap().code.editor; -unsafe fn set(key: &str, value: Option<&str>) { unsafe { - match value { - Some(value) => env::set_var(key, value), - None => env::remove_var(key), - } + std::env::remove_var("EDITOR"); + std::env::remove_var("VISUAL"); } -} + assert_eq!(editor(), "vim"); -#[test] -fn visual_overrides_the_configured_editor() { - assert_eq!(editor_of(CONFIG, None, Some("nvim")), "nvim"); -} - -#[test] -fn editor_overrides_the_configured_editor() { - assert_eq!(editor_of(CONFIG, Some("nvim"), None), "nvim"); -} - -#[test] -fn visual_takes_precedence_over_editor() { - assert_eq!(editor_of(CONFIG, Some("nano"), Some("nvim")), "nvim"); -} + unsafe { std::env::set_var("EDITOR", "nano") }; + assert_eq!(editor(), "nano"); -#[test] -fn the_configured_editor_is_kept_without_env() { - assert_eq!(editor_of(CONFIG, None, None), "vim"); -} + unsafe { std::env::set_var("VISUAL", "nvim") }; + assert_eq!(editor(), "nvim"); -#[test] -fn empty_env_vars_are_ignored() { - assert_eq!(editor_of(CONFIG, Some(""), Some("")), "vim"); -} - -#[test] -fn vim_is_the_fallback_when_nothing_is_set() { - let bare = CONFIG.replace("editor = 'vim'\n", ""); - assert_eq!(editor_of(&bare, None, None), "vim"); + unsafe { std::env::set_var("VISUAL", "") }; + assert_eq!(editor(), "nano"); } From 32567ced954f4f22b81ebf53f38e8eae72a108f9 Mon Sep 17 00:00:00 2001 From: clearloop Date: Mon, 10 Aug 2026 19:22:27 +0800 Subject: [PATCH 09/11] refactor(config): parse the config through FromStr --- src/config/mod.rs | 34 ++++++++++++++++++---------------- tests/config_code.rs | 2 +- 2 files changed, 19 insertions(+), 17 deletions(-) diff --git a/src/config/mod.rs b/src/config/mod.rs index 9c1f316..b49495e 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; @@ -29,15 +29,11 @@ pub struct Config { pub storage: Storage, } -impl Config { - fn write_default(p: impl AsRef) -> Result<()> { - fs::write(p.as_ref(), toml::ser::to_string_pretty(&Self::default())?)?; - - Ok(()) - } +impl FromStr for Config { + type Err = Error; - /// Parse a config, applying the environment overrides on top of it - pub fn parse(s: &str) -> Result { + /// 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(); @@ -49,6 +45,14 @@ impl Config { Ok(config) } +} + +impl Config { + fn write_default(p: impl AsRef) -> Result<()> { + fs::write(p.as_ref(), toml::ser::to_string_pretty(&Self::default())?)?; + + Ok(()) + } /// Locate lc's config file pub fn locate() -> Result { @@ -58,13 +62,11 @@ impl Config { Self::write_default(&conf)?; } - match Self::parse(&fs::read_to_string(&conf)?) { - Ok(config) => Ok(config), - Err(e) => { - Self::write_default(Self::root()?.join("leetcode.tmp.toml"))?; - Err(e) - } - } + 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 diff --git a/tests/config_code.rs b/tests/config_code.rs index 92319c2..ebada0f 100644 --- a/tests/config_code.rs +++ b/tests/config_code.rs @@ -19,7 +19,7 @@ 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(CONFIG).unwrap().code.editor; + let editor = || CONFIG.parse::().unwrap().code.editor; unsafe { std::env::remove_var("EDITOR"); From 2412651d7704d8cb48d43a1af29827efbe576dd6 Mon Sep 17 00:00:00 2001 From: clearloop Date: Mon, 10 Aug 2026 19:26:25 +0800 Subject: [PATCH 10/11] refactor(config): move the FromStr impl below the inherent impl --- src/config/mod.rs | 36 ++++++++++++++++++------------------ 1 file changed, 18 insertions(+), 18 deletions(-) diff --git a/src/config/mod.rs b/src/config/mod.rs index b49495e..088ee9e 100644 --- a/src/config/mod.rs +++ b/src/config/mod.rs @@ -29,24 +29,6 @@ pub struct Config { pub storage: Storage, } -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) - } -} - impl Config { fn write_default(p: impl AsRef) -> Result<()> { fs::write(p.as_ref(), toml::ser::to_string_pretty(&Self::default())?)?; @@ -89,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) + } +} From f9f3f013fd3a39161ab1c3f14e60de03721c2b1d Mon Sep 17 00:00:00 2001 From: clearloop Date: Mon, 10 Aug 2026 19:26:25 +0800 Subject: [PATCH 11/11] build: bump the version to 0.5.5 --- Cargo.lock | 2 +- Cargo.toml | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) 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."