From 112a849770aabccf62f1246c6c53d554f4446d26 Mon Sep 17 00:00:00 2001 From: ll-nick <68419636+ll-nick@users.noreply.github.com> Date: Fri, 21 Nov 2025 20:48:13 +0100 Subject: [PATCH 01/11] Use locked builds in test pipeline --- .github/workflows/run_tests.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/run_tests.yml b/.github/workflows/run_tests.yml index 891a835..bc8cdc4 100644 --- a/.github/workflows/run_tests.yml +++ b/.github/workflows/run_tests.yml @@ -9,6 +9,6 @@ jobs: steps: - uses: actions/checkout@v4 - name: Build - run: cargo build --verbose + run: cargo build --locked --verbose - name: Run tests - run: cargo test --verbose + run: cargo test --locked --verbose From f99611fcc7d9d2dbf8704317d738d8e9f0e75c32 Mon Sep 17 00:00:00 2001 From: ll-nick <68419636+ll-nick@users.noreply.github.com> Date: Fri, 21 Nov 2025 20:48:44 +0100 Subject: [PATCH 02/11] Add a formatting check to the CI workflow --- .github/workflows/run_tests.yml | 2 ++ 1 file changed, 2 insertions(+) diff --git a/.github/workflows/run_tests.yml b/.github/workflows/run_tests.yml index bc8cdc4..8ba478d 100644 --- a/.github/workflows/run_tests.yml +++ b/.github/workflows/run_tests.yml @@ -8,6 +8,8 @@ jobs: runs-on: ubuntu-latest steps: - uses: actions/checkout@v4 + - name: Check formatting + run: cargo fmt --all -- --check - name: Build run: cargo build --locked --verbose - name: Run tests From 64bc71cc9950dd335ae267000d0acf01f88b2ccf Mon Sep 17 00:00:00 2001 From: ll-nick <68419636+ll-nick@users.noreply.github.com> Date: Fri, 21 Nov 2025 20:49:18 +0100 Subject: [PATCH 03/11] Add a linter to the CI workflow --- .github/workflows/run_tests.yml | 2 ++ 1 file changed, 2 insertions(+) diff --git a/.github/workflows/run_tests.yml b/.github/workflows/run_tests.yml index 8ba478d..cd905c4 100644 --- a/.github/workflows/run_tests.yml +++ b/.github/workflows/run_tests.yml @@ -10,6 +10,8 @@ jobs: - uses: actions/checkout@v4 - name: Check formatting run: cargo fmt --all -- --check + - name: Check linting + run: cargo clippy --all-targets --all-features -- -D warnings - name: Build run: cargo build --locked --verbose - name: Run tests From 851e109abf1b2c1704970aee8a638b16954df3b7 Mon Sep 17 00:00:00 2001 From: ll-nick <68419636+ll-nick@users.noreply.github.com> Date: Fri, 21 Nov 2025 20:52:01 +0100 Subject: [PATCH 04/11] Rename workflow since it now does more than running the tests --- .github/workflows/{run_tests.yml => run_checks.yml} | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) rename .github/workflows/{run_tests.yml => run_checks.yml} (92%) diff --git a/.github/workflows/run_tests.yml b/.github/workflows/run_checks.yml similarity index 92% rename from .github/workflows/run_tests.yml rename to .github/workflows/run_checks.yml index cd905c4..ef0c57b 100644 --- a/.github/workflows/run_tests.yml +++ b/.github/workflows/run_checks.yml @@ -1,18 +1,22 @@ -name: Run tests +name: Run checks on: push: env: CARGO_TERM_COLOR: always jobs: - build: + run_checks: runs-on: ubuntu-latest steps: - uses: actions/checkout@v4 + - name: Check formatting run: cargo fmt --all -- --check + - name: Check linting run: cargo clippy --all-targets --all-features -- -D warnings + - name: Build run: cargo build --locked --verbose + - name: Run tests run: cargo test --locked --verbose From 10488f2aa80b712d6b322baefed93619b88c54ea Mon Sep 17 00:00:00 2001 From: ll-nick <68419636+ll-nick@users.noreply.github.com> Date: Fri, 21 Nov 2025 21:06:55 +0100 Subject: [PATCH 05/11] Remove unnecessary returns --- src/keybinding/fish.rs | 2 +- src/keybinding/shell_binding.rs | 10 +++++----- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/src/keybinding/fish.rs b/src/keybinding/fish.rs index 27e7228..99fcaae 100644 --- a/src/keybinding/fish.rs +++ b/src/keybinding/fish.rs @@ -46,7 +46,7 @@ pub fn fish_keyevent_to_shell_seq(ev: KeyEvent) -> String { _ => "".into(), }; - return format!("{}{}", modifier, keycode); + format!("{}{}", modifier, keycode) } #[cfg(test)] diff --git a/src/keybinding/shell_binding.rs b/src/keybinding/shell_binding.rs index cb70665..8d87ac8 100644 --- a/src/keybinding/shell_binding.rs +++ b/src/keybinding/shell_binding.rs @@ -32,7 +32,7 @@ pub fn keyevents_to_shell_binding( .map(|ev| keyevent_to_shell_seq(*ev)) .collect::(); - return Ok(format!( + Ok(format!( "\nbind -m emacs -x '\"{}\":{}'\n\ bind -m vi-insert -x '\"{}\":{}'\n\ # In vi-command mode, switch to insert mode, invoke leadr using the binding defined above, then return to command mode\n\ @@ -43,7 +43,7 @@ pub fn keyevents_to_shell_binding( function_name, key_code_string, function_name, - )); + )) } Shell::Fish => { let key_code_string = events @@ -53,7 +53,7 @@ pub fn keyevents_to_shell_binding( let key_code_string = key_code_string.trim_end_matches(","); - return Ok(format!("\nbind {} {}\n", key_code_string, function_name)); + Ok(format!("\nbind {} {}\n", key_code_string, function_name)) } Shell::Nushell => { ensure!( @@ -86,11 +86,11 @@ pub fn keyevents_to_shell_binding( .map(|ev| keyevent_to_shell_seq(*ev)) .collect::(); - return Ok(format!( + Ok(format!( "zle -N {}\n\ bindkey '{}' {}", function_name, key_code_string, function_name - )); + )) } } } From cc952218aa5a4147ecf2a6f0a3f9d0481adbde31 Mon Sep 17 00:00:00 2001 From: ll-nick <68419636+ll-nick@users.noreply.github.com> Date: Fri, 21 Nov 2025 21:09:46 +0100 Subject: [PATCH 06/11] Fix clone_on_copy warnings --- src/keybinding/bash_zsh.rs | 4 ++-- src/keybinding/fish.rs | 8 +++----- src/keybinding/nushell.rs | 6 +++--- 3 files changed, 8 insertions(+), 10 deletions(-) diff --git a/src/keybinding/bash_zsh.rs b/src/keybinding/bash_zsh.rs index 7ee1486..f2df524 100644 --- a/src/keybinding/bash_zsh.rs +++ b/src/keybinding/bash_zsh.rs @@ -56,13 +56,13 @@ mod tests { #[test] fn test_keyevent_to_shell_seq_ctrl() { - let seq = keyevent_to_shell_seq(parse_keysequence("").unwrap()[0].clone()); + let seq = keyevent_to_shell_seq(parse_keysequence("").unwrap()[0]); assert_eq!(seq, "\x07"); // Ctrl-G } #[test] fn test_keyevent_to_shell_seq_alt() { - let seq = keyevent_to_shell_seq(parse_keysequence("").unwrap()[0].clone()); + let seq = keyevent_to_shell_seq(parse_keysequence("").unwrap()[0]); assert_eq!(seq, "\x1Bx"); // ESC + 'x' } } diff --git a/src/keybinding/fish.rs b/src/keybinding/fish.rs index 99fcaae..a9860a9 100644 --- a/src/keybinding/fish.rs +++ b/src/keybinding/fish.rs @@ -56,21 +56,19 @@ mod tests { #[test] fn test_fish_fields_simple() { - let seq = fish_keyevent_to_shell_seq(parse_keysequence("").unwrap()[0].clone()); + let seq = fish_keyevent_to_shell_seq(parse_keysequence("").unwrap()[0]); assert_eq!(seq, "ctrl-g"); } #[test] fn test_fish_fields_combo() { - let seq = fish_keyevent_to_shell_seq(parse_keysequence("").unwrap()[0].clone()); + let seq = fish_keyevent_to_shell_seq(parse_keysequence("").unwrap()[0]); assert_eq!(seq, "ctrl-alt-shift-x"); } #[test] fn test_fish_fields_non_char() { - let seq = fish_keyevent_to_shell_seq(parse_keysequence("").unwrap()[0].clone()); + let seq = fish_keyevent_to_shell_seq(parse_keysequence("").unwrap()[0]); assert_eq!(seq, "f5"); } - - } diff --git a/src/keybinding/nushell.rs b/src/keybinding/nushell.rs index 0e5fc3d..c121abc 100644 --- a/src/keybinding/nushell.rs +++ b/src/keybinding/nushell.rs @@ -65,7 +65,7 @@ mod tests { #[test] fn test_nushell_fields_simple() { let ev = parse_keysequence("").unwrap(); - let fields = nushell_keyevent_to_fields(ev.first().unwrap().clone()).unwrap(); + let fields = nushell_keyevent_to_fields(*ev.first().unwrap()).unwrap(); assert_eq!(fields.modifier, "Control"); assert_eq!(fields.keycode, "Char_g"); } @@ -73,7 +73,7 @@ mod tests { #[test] fn test_nushell_fields_combo() { let ev = parse_keysequence("").unwrap(); - let fields = nushell_keyevent_to_fields(ev.first().unwrap().clone()).unwrap(); + let fields = nushell_keyevent_to_fields(*ev.first().unwrap()).unwrap(); assert_eq!(fields.modifier, "Control_Alt_Shift"); assert_eq!(fields.keycode, "Char_x"); } @@ -81,7 +81,7 @@ mod tests { #[test] fn test_nushell_fields_non_char() { let ev = parse_keysequence("").unwrap(); - let fields = nushell_keyevent_to_fields(ev.first().unwrap().clone()).unwrap(); + let fields = nushell_keyevent_to_fields(*ev.first().unwrap()).unwrap(); assert_eq!(fields.keycode, "F5"); assert_eq!(fields.modifier, "None"); } From 8861a412c005e466a7185e5e1c2709eb8060f577 Mon Sep 17 00:00:00 2001 From: ll-nick <68419636+ll-nick@users.noreply.github.com> Date: Fri, 21 Nov 2025 21:16:31 +0100 Subject: [PATCH 07/11] Improve readability of for loop --- src/mappings.rs | 11 ++++++----- 1 file changed, 6 insertions(+), 5 deletions(-) diff --git a/src/mappings.rs b/src/mappings.rs index f3bd3ae..2affcda 100644 --- a/src/mappings.rs +++ b/src/mappings.rs @@ -227,11 +227,12 @@ impl Mappings { pub fn next_possible_keys(&self, sequence: &str) -> BTreeSet { let mut next_keys = BTreeSet::new(); - for (key, _) in &self.mappings { - if key.starts_with(sequence) { - if let Some((_, ch)) = key[sequence.len()..].char_indices().next() { - next_keys.insert(ch.to_string()); - } + for key in self.mappings.keys() { + if let Some(next_char) = key + .strip_prefix(sequence) + .and_then(|rest| rest.chars().next()) + { + next_keys.insert(next_char.to_string()); } } From 714be6b0a012dd3513d8af07cbece833c80aea95 Mon Sep 17 00:00:00 2001 From: ll-nick <68419636+ll-nick@users.noreply.github.com> Date: Fri, 21 Nov 2025 21:21:53 +0100 Subject: [PATCH 08/11] Collapse nested if-statements --- src/main.rs | 8 +++---- src/session.rs | 65 +++++++++++++++++++++++++------------------------- 2 files changed, 35 insertions(+), 38 deletions(-) diff --git a/src/main.rs b/src/main.rs index de1a60b..8a75585 100644 --- a/src/main.rs +++ b/src/main.rs @@ -99,11 +99,9 @@ fn main() -> Result<()> { fn get_config_dir() -> Result { if let Ok(custom_path) = std::env::var("LEADR_CONFIG_DIR") { Ok(PathBuf::from(custom_path)) + } else if let Some(path) = ProjectDirs::from("com", "leadr", "leadr") { + Ok(path.config_dir().to_path_buf()) } else { - if let Some(path) = ProjectDirs::from("com", "leadr", "leadr") { - Ok(path.config_dir().to_path_buf()) - } else { - Err(eyre!("Could not determine configuration directory.")) - } + Err(eyre!("Could not determine configuration directory.")) } } diff --git a/src/session.rs b/src/session.rs index 7e7cff1..fd6e2d0 100644 --- a/src/session.rs +++ b/src/session.rs @@ -38,10 +38,10 @@ impl LeadrSession { // Cosmetically fix the prompt line disappearing while leadr is active. let mut prompt_guard = prompt::PromptGuard::try_new(); - if let Ok(ref mut guard) = prompt_guard { - if self.config.redraw_prompt_line { - guard.redraw()?; - } + if let Ok(ref mut guard) = prompt_guard + && self.config.redraw_prompt_line + { + guard.redraw()?; } loop { @@ -60,41 +60,40 @@ impl LeadrSession { } } - if poll(Duration::from_millis(50))? { - if let Event::Key(KeyEvent { + if poll(Duration::from_millis(50))? + && let Event::Key(KeyEvent { code, modifiers, .. }) = read()? - { - if modifiers == crossterm::event::KeyModifiers::CONTROL { - if code == KeyCode::Char('c') { - return Ok(SessionResult::Cancelled); - } - continue; + { + if modifiers == crossterm::event::KeyModifiers::CONTROL { + if code == KeyCode::Char('c') { + return Ok(SessionResult::Cancelled); } - match code { - KeyCode::Char(c) => { - self.sequence.push(c); - if let Some(mapping) = self.mappings.match_sequence(&self.sequence) { - return Ok(SessionResult::Command(mapping.format_command())); - } - - if !self.mappings.has_partial_match(&self.sequence) { - return Ok(SessionResult::NoMatch); - } - } - KeyCode::Backspace => { - self.sequence.pop(); + continue; + } + match code { + KeyCode::Char(c) => { + self.sequence.push(c); + if let Some(mapping) = self.mappings.match_sequence(&self.sequence) { + return Ok(SessionResult::Command(mapping.format_command())); } - KeyCode::Esc => { - return Ok(SessionResult::Cancelled); + + if !self.mappings.has_partial_match(&self.sequence) { + return Ok(SessionResult::NoMatch); } - _ => {} } - if let Some(panel) = panel.as_mut() { - if let Err(e) = panel.draw(&self.sequence, &self.mappings) { - if !self.config.panel.fail_silently { - return Err(e); - } + KeyCode::Backspace => { + self.sequence.pop(); + } + KeyCode::Esc => { + return Ok(SessionResult::Cancelled); + } + _ => {} + } + if let Some(panel) = panel.as_mut() { + if let Err(e) = panel.draw(&self.sequence, &self.mappings) { + if !self.config.panel.fail_silently { + return Err(e); } } } From 643114630b6e26bad10a1f9d644dc79899c808a6 Mon Sep 17 00:00:00 2001 From: ll-nick <68419636+ll-nick@users.noreply.github.com> Date: Sat, 22 Nov 2025 14:47:00 +0100 Subject: [PATCH 09/11] Remove unecessary borrows --- src/ui/panel.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/ui/panel.rs b/src/ui/panel.rs index b986ad2..40ebd29 100644 --- a/src/ui/panel.rs +++ b/src/ui/panel.rs @@ -171,7 +171,7 @@ impl Panel { .take(column.height as usize) .cloned() .collect::>(); - self.draw_entries(&mut tty, column, &mappings, &sequence, &column_keys)?; + self.draw_entries(&mut tty, column, mappings, sequence, &column_keys)?; } let footer_area = Area { From 3a2a7e1a0dcc1063d89be3c0fe98fcb06bf59155 Mon Sep 17 00:00:00 2001 From: ll-nick <68419636+ll-nick@users.noreply.github.com> Date: Sat, 22 Nov 2025 15:01:18 +0100 Subject: [PATCH 10/11] Refactor Session::run function my splitting panel creation and drawing into separate functions --- src/session.rs | 43 ++++++++++++++++++++++++++----------------- 1 file changed, 26 insertions(+), 17 deletions(-) diff --git a/src/session.rs b/src/session.rs index fd6e2d0..9147fc6 100644 --- a/src/session.rs +++ b/src/session.rs @@ -47,17 +47,7 @@ impl LeadrSession { loop { let timeout_reached = start_time.elapsed() >= self.config.panel.delay; if self.config.panel.enabled && panel.is_none() && timeout_reached { - let result = (|| { - let p = Panel::try_new(self.config.panel.clone(), self.theme.clone())?; - p.draw(&self.sequence, &self.mappings)?; - Ok(p) - })(); - - match result { - Ok(p) => panel = Some(p), - Err(_e) if self.config.panel.fail_silently => {} - Err(e) => return Err(e), - } + panel = self.try_new_panel()?; } if poll(Duration::from_millis(50))? @@ -90,14 +80,33 @@ impl LeadrSession { } _ => {} } - if let Some(panel) = panel.as_mut() { - if let Err(e) = panel.draw(&self.sequence, &self.mappings) { - if !self.config.panel.fail_silently { - return Err(e); - } - } + + if let Some(ref mut p) = panel { + self.try_draw_panel(p)?; } } } } + + /// Try creating a new panel and draw upon success. + /// Will return Ok(None) if panel creation fails but fail_silently is set. + fn try_new_panel(&self) -> Result> { + match Panel::try_new(self.config.panel.clone(), self.theme.clone()) { + Ok(mut p) => { + self.try_draw_panel(&mut p)?; + Ok(Some(p)) + } + Err(_) if self.config.panel.fail_silently => Ok(None), + Err(e) => Err(e), + } + } + + /// Try drawing the panel, respecting the fail_silently setting. + fn try_draw_panel(&self, panel: &mut Panel) -> Result<()> { + match panel.draw(&self.sequence, &self.mappings) { + Ok(()) => Ok(()), + Err(_) if self.config.panel.fail_silently => Ok(()), + Err(e) => Err(e), + } + } } From 1fecedbba0c8641c748297749d7227d0c03afcfc Mon Sep 17 00:00:00 2001 From: ll-nick <68419636+ll-nick@users.noreply.github.com> Date: Sat, 22 Nov 2025 15:06:00 +0100 Subject: [PATCH 11/11] Slightly simplify the way fish keybinds are concatinated --- src/keybinding/shell_binding.rs | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/src/keybinding/shell_binding.rs b/src/keybinding/shell_binding.rs index 8d87ac8..977b76c 100644 --- a/src/keybinding/shell_binding.rs +++ b/src/keybinding/shell_binding.rs @@ -48,10 +48,9 @@ pub fn keyevents_to_shell_binding( Shell::Fish => { let key_code_string = events .iter() - .map(|ev| fish_keyevent_to_shell_seq(*ev) + ",") - .collect::(); - - let key_code_string = key_code_string.trim_end_matches(","); + .map(|ev| fish_keyevent_to_shell_seq(*ev)) + .collect::>() + .join(","); Ok(format!("\nbind {} {}\n", key_code_string, function_name)) }