From f87d667396052ddcfff74244fbded34c1a848542 Mon Sep 17 00:00:00 2001 From: esf Date: Sun, 15 Feb 2026 01:10:28 +0100 Subject: [PATCH 1/8] add shell plugin history --- Cargo.lock | 61 ++++++++++++++++++++++ plugins/shell/Cargo.toml | 11 ++-- plugins/shell/README.md | 7 ++- plugins/shell/src/history.rs | 99 ++++++++++++++++++++++++++++++++++++ plugins/shell/src/lib.rs | 98 +++++++++++++++++++++++++++-------- 5 files changed, 249 insertions(+), 27 deletions(-) create mode 100644 plugins/shell/src/history.rs diff --git a/Cargo.lock b/Cargo.lock index b2251114..01ef5d16 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -584,6 +584,27 @@ dependencies = [ "serde", ] +[[package]] +name = "dirs" +version = "6.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c3e8aa94d75141228480295a7d0e7feb620b1a5ad9f12bc40be62411e38cce4e" +dependencies = [ + "dirs-sys", +] + +[[package]] +name = "dirs-sys" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e01a3366d27ee9890022452ee61b2b63a67e6f13f58900b651ff5665f0bb1fab" +dependencies = [ + "libc", + "option-ext", + "redox_users", + "windows-sys 0.61.2", +] + [[package]] name = "displaydoc" version = "0.2.5" @@ -1620,6 +1641,16 @@ dependencies = [ "winapi", ] +[[package]] +name = "libredox" +version = "0.1.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3d0b95e02c851351f877147b7deea7b1afb1df71b63aa5f8270716e0c5720616" +dependencies = [ + "bitflags 2.10.0", + "libc", +] + [[package]] name = "litemap" version = "0.8.1" @@ -1755,6 +1786,16 @@ dependencies = [ "memchr", ] +[[package]] +name = "nucleo-matcher" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bf33f538733d1a5a3494b836ba913207f14d9d4a1d3cd67030c5061bdd2cac85" +dependencies = [ + "memchr", + "unicode-segmentation", +] + [[package]] name = "num" version = "0.4.3" @@ -1882,6 +1923,12 @@ dependencies = [ "windows-sys 0.42.0", ] +[[package]] +name = "option-ext" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "04744f49eae99ab78e0d5c0b603ab218f515ea8cfe5a456d7629ad883a3b6e7d" + [[package]] name = "pango" version = "0.21.5" @@ -2147,6 +2194,17 @@ dependencies = [ "bitflags 2.10.0", ] +[[package]] +name = "redox_users" +version = "0.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a4e608c6638b9c18977b00b475ac1f28d14e84b27d8d42f70e0bf1e3dec127ac" +dependencies = [ + "getrandom 0.2.16", + "libredox", + "thiserror", +] + [[package]] name = "regex" version = "1.12.2" @@ -2561,6 +2619,9 @@ version = "25.12.0" dependencies = [ "abi_stable", "anyrun-plugin", + "dirs", + "indexmap", + "nucleo-matcher", "ron 0.8.1", "serde", ] diff --git a/plugins/shell/Cargo.toml b/plugins/shell/Cargo.toml index 09a9c71b..51a354bb 100644 --- a/plugins/shell/Cargo.toml +++ b/plugins/shell/Cargo.toml @@ -9,7 +9,10 @@ crate-type = [ "cdylib" ] # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html [dependencies] -abi_stable = "0.11.1" -anyrun-plugin = { path = "../../anyrun-plugin" } -ron = "0.8.0" -serde = { features = [ "derive" ], version = "1.0.228" } +abi_stable = "0.11.1" +anyrun-plugin = { path = "../../anyrun-plugin" } +dirs = "6.0.0" +indexmap = "2.12.1" +nucleo-matcher = "0.3.1" +ron = "0.8.0" +serde = { features = [ "derive" ], version = "1.0.228" } diff --git a/plugins/shell/README.md b/plugins/shell/README.md index 9c5a45cb..d2f7db36 100644 --- a/plugins/shell/README.md +++ b/plugins/shell/README.md @@ -14,5 +14,10 @@ Config( prefix: ":sh", // Override the shell used to launch the command shell: None, + // None to disable history (default) + // note the double parens + history: Some(( + capacity: 100, + )), ) -``` \ No newline at end of file +``` diff --git a/plugins/shell/src/history.rs b/plugins/shell/src/history.rs new file mode 100644 index 00000000..90b04b4e --- /dev/null +++ b/plugins/shell/src/history.rs @@ -0,0 +1,99 @@ +use std::fs::File; +use std::io::{BufRead, BufReader, Seek, SeekFrom, Write}; + +use indexmap::IndexSet; + +use crate::HistoryConfig; + +pub enum HistoryBackingStore { + File(File), + Memory, +} + +pub struct History { + pub backing_store: HistoryBackingStore, + pub elements: IndexSet, + pub cap: usize, +} + +impl History { + pub fn new(history_config: &HistoryConfig) -> History { + let maybe_history_file = + dirs::state_dir().map(|s| s.join("anyrun").join("shell_history.txt")); + + let backing_store = if let Some(history_file) = maybe_history_file { + let file = (|| { + if let Some(dir) = history_file.parent() { + std::fs::create_dir_all(dir)?; + } + + File::options() + .read(true) + .write(true) + .create(true) + .truncate(false) + .open(&history_file) + })(); + + match file { + Ok(f) => HistoryBackingStore::File(f), + Err(ref err) => { + eprintln!("[shell] Failed to create file {} to persist shell plugin history, falling back to in-memory: {}", &history_file.to_string_lossy(), err.kind()); + HistoryBackingStore::Memory + } + } + } else { + HistoryBackingStore::Memory + }; + + match backing_store { + HistoryBackingStore::File(file) => History::from_file(history_config.capacity, file) + .unwrap_or_else(|err| { + eprintln!("[shell] Failed to initialize history from file: {:?}", err); + History::from_mem(history_config.capacity) + }), + HistoryBackingStore::Memory => History::from_mem(history_config.capacity), + } + } + + pub fn push(&mut self, value: String) -> Result<(), std::io::Error> { + // insert_before ensures new usages of existing commands bubble up to the top of the history, simple `insert` does not + self.elements.insert_before(self.elements.len(), value); + + if self.elements.len() > self.cap { + let remove_count = self.elements.len().saturating_sub(self.cap); + self.elements.drain(0..remove_count); + } + + if let HistoryBackingStore::File(file) = &mut self.backing_store { + file.seek(SeekFrom::Start(0))?; + file.set_len(0)?; + for line in &self.elements { + writeln!(file, "{}", line)?; + } + file.flush()?; + } + + Ok(()) + } + + fn from_mem(cap: usize) -> History { + History { + backing_store: HistoryBackingStore::Memory, + elements: IndexSet::new(), + cap, + } + } + + fn from_file(cap: usize, file: File) -> Result { + let elements: IndexSet = BufReader::new(&file) + .lines() + .collect::>()?; + + Ok(History { + backing_store: HistoryBackingStore::File(file), + elements, + cap, + }) + } +} diff --git a/plugins/shell/src/lib.rs b/plugins/shell/src/lib.rs index 3620b77f..744522c9 100644 --- a/plugins/shell/src/lib.rs +++ b/plugins/shell/src/lib.rs @@ -2,12 +2,24 @@ use std::{env, fs, process::Command}; use abi_stable::std_types::{ROption, RString, RVec}; use anyrun_plugin::*; +use nucleo_matcher::pattern::{Atom, AtomKind, CaseMatching, Normalization}; +use nucleo_matcher::Matcher; use serde::Deserialize; +use self::history::History; + +mod history; + +#[derive(Deserialize)] +struct HistoryConfig { + capacity: usize, +} + #[derive(Deserialize)] struct Config { prefix: String, shell: Option, + history: Option, } impl Default for Config { @@ -15,15 +27,28 @@ impl Default for Config { Config { prefix: ":sh".to_string(), shell: None, + history: None, } } } +#[derive(Default)] +struct State { + config: Config, + history: Option, +} + #[init] -fn init(config_dir: RString) -> Config { +fn init(config_dir: RString) -> State { match fs::read_to_string(format!("{}/shell.ron", config_dir)) { - Ok(content) => ron::from_str(&content).unwrap_or_default(), - Err(_) => Config::default(), + Ok(content) => { + let config: Config = ron::from_str(&content).unwrap_or_default(); + + let history = config.history.as_ref().map(History::new); + + State { config, history } + } + Err(_) => State::default(), } } @@ -36,28 +61,51 @@ fn info() -> PluginInfo { } #[get_matches] -fn get_matches(input: RString, config: &Config) -> RVec { +fn get_matches(input: RString, state: &State) -> RVec { + let config = &state.config; if input.starts_with(&config.prefix) { let (_, command) = input.split_once(&config.prefix).unwrap(); + let command = command.trim(); if !command.is_empty() { - vec![Match { - title: command.trim().into(), - description: ROption::RSome( - config - .shell - .clone() - .unwrap_or_else(|| { - env::var("SHELL").unwrap_or_else(|_| { - "The shell could not be determined!".to_string() + let matches = if let Some(history) = &state.history { + let mut matcher = Matcher::new(nucleo_matcher::Config::DEFAULT); + let matches = Atom::new( + command, + CaseMatching::Ignore, + Normalization::Smart, + AtomKind::Fuzzy, + false, + ) + .match_list(&history.elements, &mut matcher) + .into_iter() + .map(|(s, _)| s.as_str()) + .collect(); + matches + } else { + vec![command] + }; + + std::iter::once(command) + .chain(matches.into_iter()) + .map(|cmd| Match { + title: cmd.into(), + description: ROption::RSome( + config + .shell + .clone() + .unwrap_or_else(|| { + env::var("SHELL").unwrap_or_else(|_| { + "The shell could not be determined!".to_string() + }) }) - }) - .into(), - ), - use_pango: false, - icon: ROption::RNone, - id: ROption::RNone, - }] - .into() + .into(), + ), + use_pango: false, + icon: ROption::RNone, + id: ROption::RNone, + }) + .collect::>() + .into() } else { RVec::new() } @@ -67,7 +115,13 @@ fn get_matches(input: RString, config: &Config) -> RVec { } #[handler] -fn handler(selection: Match) -> HandleResult { +fn handler(selection: Match, state: &mut State) -> HandleResult { + if let Some(history) = state.history.as_mut() { + if let Err(err) = history.push(selection.title.clone().into_string()) { + eprintln!("[shell] Failed to push command to history: {:?}", err); + } + } + if let Err(why) = Command::new(selection.description.unwrap().as_str()) .arg("-c") .arg(selection.title.as_str()) From 00983df1c3f644d50a14605da51f674ee2327076 Mon Sep 17 00:00:00 2001 From: esf Date: Thu, 19 Feb 2026 14:46:08 +0100 Subject: [PATCH 2/8] fix history file path --- plugins/shell/src/history.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/plugins/shell/src/history.rs b/plugins/shell/src/history.rs index 90b04b4e..a27bac2c 100644 --- a/plugins/shell/src/history.rs +++ b/plugins/shell/src/history.rs @@ -19,7 +19,7 @@ pub struct History { impl History { pub fn new(history_config: &HistoryConfig) -> History { let maybe_history_file = - dirs::state_dir().map(|s| s.join("anyrun").join("shell_history.txt")); + dirs::state_dir().map(|s| s.join("anyrun").join("shell").join("shell_history.txt")); let backing_store = if let Some(history_file) = maybe_history_file { let file = (|| { From 5c80e4f80795b79a7e60184f0422e2810bbcef1a Mon Sep 17 00:00:00 2001 From: esf Date: Thu, 19 Feb 2026 16:12:57 +0100 Subject: [PATCH 3/8] remove in-memory history --- plugins/shell/src/history.rs | 77 +++++++++++------------------------- plugins/shell/src/lib.rs | 8 +++- 2 files changed, 31 insertions(+), 54 deletions(-) diff --git a/plugins/shell/src/history.rs b/plugins/shell/src/history.rs index a27bac2c..18eb3cf3 100644 --- a/plugins/shell/src/history.rs +++ b/plugins/shell/src/history.rs @@ -5,54 +5,35 @@ use indexmap::IndexSet; use crate::HistoryConfig; -pub enum HistoryBackingStore { - File(File), - Memory, -} - pub struct History { - pub backing_store: HistoryBackingStore, + pub store: File, pub elements: IndexSet, pub cap: usize, } impl History { - pub fn new(history_config: &HistoryConfig) -> History { - let maybe_history_file = + pub fn new(history_config: &HistoryConfig) -> Result { + let maybe_history_path = dirs::state_dir().map(|s| s.join("anyrun").join("shell").join("shell_history.txt")); - let backing_store = if let Some(history_file) = maybe_history_file { - let file = (|| { - if let Some(dir) = history_file.parent() { - std::fs::create_dir_all(dir)?; - } + if let Some(history_path) = maybe_history_path { + if let Some(dir) = history_path.parent() { + std::fs::create_dir_all(dir)?; + } - File::options() - .read(true) - .write(true) - .create(true) - .truncate(false) - .open(&history_file) - })(); + let file = File::options() + .read(true) + .write(true) + .create(true) + .truncate(false) + .open(&history_path)?; - match file { - Ok(f) => HistoryBackingStore::File(f), - Err(ref err) => { - eprintln!("[shell] Failed to create file {} to persist shell plugin history, falling back to in-memory: {}", &history_file.to_string_lossy(), err.kind()); - HistoryBackingStore::Memory - } - } + History::from_file(history_config.capacity, file) } else { - HistoryBackingStore::Memory - }; - - match backing_store { - HistoryBackingStore::File(file) => History::from_file(history_config.capacity, file) - .unwrap_or_else(|err| { - eprintln!("[shell] Failed to initialize history from file: {:?}", err); - History::from_mem(history_config.capacity) - }), - HistoryBackingStore::Memory => History::from_mem(history_config.capacity), + Err(std::io::Error::new( + std::io::ErrorKind::Unsupported, + "failed to get the user state directory", + )) } } @@ -65,33 +46,23 @@ impl History { self.elements.drain(0..remove_count); } - if let HistoryBackingStore::File(file) = &mut self.backing_store { - file.seek(SeekFrom::Start(0))?; - file.set_len(0)?; - for line in &self.elements { - writeln!(file, "{}", line)?; - } - file.flush()?; + self.store.seek(SeekFrom::Start(0))?; + self.store.set_len(0)?; + for line in &self.elements { + writeln!(self.store, "{}", line)?; } + self.store.flush()?; Ok(()) } - fn from_mem(cap: usize) -> History { - History { - backing_store: HistoryBackingStore::Memory, - elements: IndexSet::new(), - cap, - } - } - fn from_file(cap: usize, file: File) -> Result { let elements: IndexSet = BufReader::new(&file) .lines() .collect::>()?; Ok(History { - backing_store: HistoryBackingStore::File(file), + store: file, elements, cap, }) diff --git a/plugins/shell/src/lib.rs b/plugins/shell/src/lib.rs index 744522c9..b9231138 100644 --- a/plugins/shell/src/lib.rs +++ b/plugins/shell/src/lib.rs @@ -44,7 +44,13 @@ fn init(config_dir: RString) -> State { Ok(content) => { let config: Config = ron::from_str(&content).unwrap_or_default(); - let history = config.history.as_ref().map(History::new); + let history = config.history.as_ref().and_then(|h| match History::new(h) { + Ok(history) => Some(history), + Err(err) => { + eprintln!("[shell] Failed to initialize history: {}", err); + None + } + }); State { config, history } } From c0f295e72b29dd1d873de96a90ffe3ae49d4979b Mon Sep 17 00:00:00 2001 From: esf Date: Thu, 19 Feb 2026 18:20:10 +0100 Subject: [PATCH 4/8] swap fuzzy matcher dep, save history as json --- Cargo.lock | 27 +++++++++--------- plugins/shell/Cargo.toml | 15 +++++----- plugins/shell/src/history.rs | 54 +++++++++++++++++++++-------------- plugins/shell/src/lib.rs | 55 ++++++++++++++++++++---------------- 4 files changed, 84 insertions(+), 67 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 01ef5d16..7ff8ecb0 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1550,6 +1550,8 @@ checksum = "0ad4bb2b565bca0645f4d68c5c9af97fba094e9791da685bf83cb5f3ce74acf2" dependencies = [ "equivalent", "hashbrown 0.16.1", + "serde", + "serde_core", ] [[package]] @@ -1786,16 +1788,6 @@ dependencies = [ "memchr", ] -[[package]] -name = "nucleo-matcher" -version = "0.3.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bf33f538733d1a5a3494b836ba913207f14d9d4a1d3cd67030c5061bdd2cac85" -dependencies = [ - "memchr", - "unicode-segmentation", -] - [[package]] name = "num" version = "0.4.3" @@ -2570,15 +2562,15 @@ dependencies = [ [[package]] name = "serde_json" -version = "1.0.145" +version = "1.0.149" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "402a6f66d8c709116cf22f558eab210f5a50187f702eb4d7e5ef38d9a7f1c79c" +checksum = "83fc039473c5595ace860d8c4fafa220ff474b3fc6bfdb4293327f1a37e94d86" dependencies = [ "itoa", "memchr", - "ryu", "serde", "serde_core", + "zmij", ] [[package]] @@ -2620,10 +2612,11 @@ dependencies = [ "abi_stable", "anyrun-plugin", "dirs", + "fuzzy-matcher", "indexmap", - "nucleo-matcher", "ron 0.8.1", "serde", + "serde_json", ] [[package]] @@ -3838,3 +3831,9 @@ dependencies = [ "quote", "syn 2.0.111", ] + +[[package]] +name = "zmij" +version = "1.0.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b8848ee67ecc8aedbaf3e4122217aff892639231befc6a1b58d29fff4c2cabaa" diff --git a/plugins/shell/Cargo.toml b/plugins/shell/Cargo.toml index 51a354bb..72f8cb65 100644 --- a/plugins/shell/Cargo.toml +++ b/plugins/shell/Cargo.toml @@ -9,10 +9,11 @@ crate-type = [ "cdylib" ] # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html [dependencies] -abi_stable = "0.11.1" -anyrun-plugin = { path = "../../anyrun-plugin" } -dirs = "6.0.0" -indexmap = "2.12.1" -nucleo-matcher = "0.3.1" -ron = "0.8.0" -serde = { features = [ "derive" ], version = "1.0.228" } +abi_stable = "0.11.1" +anyrun-plugin = { path = "../../anyrun-plugin" } +dirs = "6.0.0" +fuzzy-matcher = "0.3.7" +indexmap = { features = [ "serde" ], version = "2.12.1" } +ron = "0.8.0" +serde = { features = [ "derive" ], version = "1.0.228" } +serde_json = "1.0.149" diff --git a/plugins/shell/src/history.rs b/plugins/shell/src/history.rs index 18eb3cf3..a4910054 100644 --- a/plugins/shell/src/history.rs +++ b/plugins/shell/src/history.rs @@ -1,13 +1,25 @@ use std::fs::File; -use std::io::{BufRead, BufReader, Seek, SeekFrom, Write}; +use std::io::{BufReader, BufWriter, Seek, SeekFrom, Write}; use indexmap::IndexSet; +use serde::{Deserialize, Serialize}; use crate::HistoryConfig; +#[derive(Serialize, Deserialize, PartialEq, Eq, Hash)] +pub struct HistoryItem { + pub command: String, +} + +impl HistoryItem { + pub fn new(command: String) -> Self { + Self { command } + } +} + pub struct History { pub store: File, - pub elements: IndexSet, + pub elements: IndexSet, pub cap: usize, } @@ -28,7 +40,18 @@ impl History { .truncate(false) .open(&history_path)?; - History::from_file(history_config.capacity, file) + let reader = BufReader::new(&file); + let elements: Option> = match serde_json::from_reader(reader) { + Ok(val) => val, + Err(e) if e.is_eof() => None, + Err(e) => return Err(e.into()), + }; + + Ok(History { + store: file, + elements: elements.unwrap_or_default(), + cap: history_config.capacity, + }) } else { Err(std::io::Error::new( std::io::ErrorKind::Unsupported, @@ -39,32 +62,21 @@ impl History { pub fn push(&mut self, value: String) -> Result<(), std::io::Error> { // insert_before ensures new usages of existing commands bubble up to the top of the history, simple `insert` does not - self.elements.insert_before(self.elements.len(), value); + self.elements + .insert_before(self.elements.len(), HistoryItem::new(value)); if self.elements.len() > self.cap { let remove_count = self.elements.len().saturating_sub(self.cap); self.elements.drain(0..remove_count); } - self.store.seek(SeekFrom::Start(0))?; self.store.set_len(0)?; - for line in &self.elements { - writeln!(self.store, "{}", line)?; - } - self.store.flush()?; - - Ok(()) - } + self.store.seek(SeekFrom::Start(0))?; - fn from_file(cap: usize, file: File) -> Result { - let elements: IndexSet = BufReader::new(&file) - .lines() - .collect::>()?; + let mut writer = BufWriter::new(&self.store); + serde_json::to_writer(&mut writer, &self.elements)?; + writer.flush()?; - Ok(History { - store: file, - elements, - cap, - }) + Ok(()) } } diff --git a/plugins/shell/src/lib.rs b/plugins/shell/src/lib.rs index b9231138..cac4f5b8 100644 --- a/plugins/shell/src/lib.rs +++ b/plugins/shell/src/lib.rs @@ -2,8 +2,7 @@ use std::{env, fs, process::Command}; use abi_stable::std_types::{ROption, RString, RVec}; use anyrun_plugin::*; -use nucleo_matcher::pattern::{Atom, AtomKind, CaseMatching, Normalization}; -use nucleo_matcher::Matcher; +use fuzzy_matcher::FuzzyMatcher; use serde::Deserialize; use self::history::History; @@ -70,29 +69,35 @@ fn info() -> PluginInfo { fn get_matches(input: RString, state: &State) -> RVec { let config = &state.config; if input.starts_with(&config.prefix) { - let (_, command) = input.split_once(&config.prefix).unwrap(); - let command = command.trim(); - if !command.is_empty() { - let matches = if let Some(history) = &state.history { - let mut matcher = Matcher::new(nucleo_matcher::Config::DEFAULT); - let matches = Atom::new( - command, - CaseMatching::Ignore, - Normalization::Smart, - AtomKind::Fuzzy, - false, - ) - .match_list(&history.elements, &mut matcher) - .into_iter() - .map(|(s, _)| s.as_str()) - .collect(); - matches - } else { - vec![command] - }; - - std::iter::once(command) - .chain(matches.into_iter()) + let (_, input) = input.split_once(&config.prefix).unwrap(); + let input = input.trim(); + if !input.is_empty() { + let history_matches = state + .history + .as_ref() + .map(|history| { + let matcher = fuzzy_matcher::skim::SkimMatcherV2::default().ignore_case(); + let mut matches = history + .elements + .iter() + .filter_map(|s| { + matcher + .fuzzy_match(&s.command, input) + .map(|score| (s, score)) + }) + .collect::>(); + + matches.sort_by(|(_, score_a), (_, score_b)| score_b.cmp(score_a)); + + matches + .iter() + .map(|(hist_item, _)| hist_item.command.as_str()) + .collect::>() + }) + .unwrap_or_default(); + + std::iter::once(input) + .chain(history_matches.into_iter()) .map(|cmd| Match { title: cmd.into(), description: ROption::RSome( From 043858a0c25443e7ea117a062804254b427a511c Mon Sep 17 00:00:00 2001 From: esf Date: Fri, 20 Feb 2026 00:53:24 +0100 Subject: [PATCH 5/8] add plugin max_entries --- plugins/shell/README.md | 11 ++++++----- plugins/shell/src/lib.rs | 8 ++++++++ 2 files changed, 14 insertions(+), 5 deletions(-) diff --git a/plugins/shell/README.md b/plugins/shell/README.md index d2f7db36..c489f359 100644 --- a/plugins/shell/README.md +++ b/plugins/shell/README.md @@ -14,10 +14,11 @@ Config( prefix: ":sh", // Override the shell used to launch the command shell: None, - // None to disable history (default) - // note the double parens - history: Some(( - capacity: 100, - )), + max_entries: 10, + // to enable history: + // Some(( + // capacity: 100, + // )), + history: None, ) ``` diff --git a/plugins/shell/src/lib.rs b/plugins/shell/src/lib.rs index cac4f5b8..6820cbf3 100644 --- a/plugins/shell/src/lib.rs +++ b/plugins/shell/src/lib.rs @@ -18,14 +18,21 @@ struct HistoryConfig { struct Config { prefix: String, shell: Option, + #[serde(default = "default_max_entries")] + max_entries: usize, history: Option, } +fn default_max_entries() -> usize { + 10 +} + impl Default for Config { fn default() -> Self { Config { prefix: ":sh".to_string(), shell: None, + max_entries: default_max_entries(), history: None, } } @@ -98,6 +105,7 @@ fn get_matches(input: RString, state: &State) -> RVec { std::iter::once(input) .chain(history_matches.into_iter()) + .take(config.max_entries) .map(|cmd| Match { title: cmd.into(), description: ROption::RSome( From 3cbef1571571de7f807435ba0c2589c5714290ce Mon Sep 17 00:00:00 2001 From: esf Date: Sat, 21 Feb 2026 00:13:11 +0100 Subject: [PATCH 6/8] wrap persisted history --- plugins/shell/src/history.rs | 32 ++++++++++++++++++++++++-------- plugins/shell/src/lib.rs | 3 +-- 2 files changed, 25 insertions(+), 10 deletions(-) diff --git a/plugins/shell/src/history.rs b/plugins/shell/src/history.rs index a4910054..09615340 100644 --- a/plugins/shell/src/history.rs +++ b/plugins/shell/src/history.rs @@ -6,27 +6,34 @@ use serde::{Deserialize, Serialize}; use crate::HistoryConfig; +#[derive(Serialize, Deserialize, Default)] +struct PersistedHistory { + elements: T, +} +type PersistedHistoryOwned = PersistedHistory>; +type PersistedHistoryBorrowed<'a> = PersistedHistory<&'a IndexSet>; + #[derive(Serialize, Deserialize, PartialEq, Eq, Hash)] pub struct HistoryItem { pub command: String, } impl HistoryItem { - pub fn new(command: String) -> Self { + fn new(command: String) -> Self { Self { command } } } pub struct History { - pub store: File, - pub elements: IndexSet, + store: File, + elements: IndexSet, pub cap: usize, } impl History { pub fn new(history_config: &HistoryConfig) -> Result { let maybe_history_path = - dirs::state_dir().map(|s| s.join("anyrun").join("shell").join("shell_history.txt")); + dirs::state_dir().map(|s| s.join("anyrun").join("shell").join("history.json")); if let Some(history_path) = maybe_history_path { if let Some(dir) = history_path.parent() { @@ -41,15 +48,15 @@ impl History { .open(&history_path)?; let reader = BufReader::new(&file); - let elements: Option> = match serde_json::from_reader(reader) { + let persisted_history: PersistedHistoryOwned = match serde_json::from_reader(reader) { Ok(val) => val, - Err(e) if e.is_eof() => None, + Err(e) if e.is_eof() => PersistedHistory::default(), Err(e) => return Err(e.into()), }; Ok(History { store: file, - elements: elements.unwrap_or_default(), + elements: persisted_history.elements, cap: history_config.capacity, }) } else { @@ -74,9 +81,18 @@ impl History { self.store.seek(SeekFrom::Start(0))?; let mut writer = BufWriter::new(&self.store); - serde_json::to_writer(&mut writer, &self.elements)?; + serde_json::to_writer( + &mut writer, + &PersistedHistoryBorrowed { + elements: &self.elements, + }, + )?; writer.flush()?; Ok(()) } + + pub fn elements(&self) -> impl Iterator { + self.elements.iter() + } } diff --git a/plugins/shell/src/lib.rs b/plugins/shell/src/lib.rs index 6820cbf3..ecf9eed3 100644 --- a/plugins/shell/src/lib.rs +++ b/plugins/shell/src/lib.rs @@ -85,8 +85,7 @@ fn get_matches(input: RString, state: &State) -> RVec { .map(|history| { let matcher = fuzzy_matcher::skim::SkimMatcherV2::default().ignore_case(); let mut matches = history - .elements - .iter() + .elements() .filter_map(|s| { matcher .fuzzy_match(&s.command, input) From 84ae1df2b1d0e200f961fdbb0b3acdf634e35650 Mon Sep 17 00:00:00 2001 From: esf Date: Sun, 22 Feb 2026 03:18:28 +0100 Subject: [PATCH 7/8] move max_entries default --- plugins/shell/src/lib.rs | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/plugins/shell/src/lib.rs b/plugins/shell/src/lib.rs index ecf9eed3..64e8d770 100644 --- a/plugins/shell/src/lib.rs +++ b/plugins/shell/src/lib.rs @@ -18,13 +18,15 @@ struct HistoryConfig { struct Config { prefix: String, shell: Option, - #[serde(default = "default_max_entries")] + #[serde(default = "Config::default_max_entries")] max_entries: usize, history: Option, } -fn default_max_entries() -> usize { - 10 +impl Config { + fn default_max_entries() -> usize { + 10 + } } impl Default for Config { @@ -32,7 +34,7 @@ impl Default for Config { Config { prefix: ":sh".to_string(), shell: None, - max_entries: default_max_entries(), + max_entries: Config::default_max_entries(), history: None, } } From 4cc4ea5a6c1d2ad2945ea3ef32adcdc6ad35c9b6 Mon Sep 17 00:00:00 2001 From: esf Date: Sun, 22 Feb 2026 15:50:25 +0100 Subject: [PATCH 8/8] log config load errors --- plugins/shell/src/history.rs | 3 ++- plugins/shell/src/lib.rs | 26 ++++++++++++++++++++------ 2 files changed, 22 insertions(+), 7 deletions(-) diff --git a/plugins/shell/src/history.rs b/plugins/shell/src/history.rs index 09615340..15275709 100644 --- a/plugins/shell/src/history.rs +++ b/plugins/shell/src/history.rs @@ -13,7 +13,7 @@ struct PersistedHistory { type PersistedHistoryOwned = PersistedHistory>; type PersistedHistoryBorrowed<'a> = PersistedHistory<&'a IndexSet>; -#[derive(Serialize, Deserialize, PartialEq, Eq, Hash)] +#[derive(Serialize, Deserialize, PartialEq, Eq, Hash, Debug)] pub struct HistoryItem { pub command: String, } @@ -24,6 +24,7 @@ impl HistoryItem { } } +#[derive(Debug)] pub struct History { store: File, elements: IndexSet, diff --git a/plugins/shell/src/lib.rs b/plugins/shell/src/lib.rs index 64e8d770..6a9640e3 100644 --- a/plugins/shell/src/lib.rs +++ b/plugins/shell/src/lib.rs @@ -9,12 +9,12 @@ use self::history::History; mod history; -#[derive(Deserialize)] +#[derive(Deserialize, Debug)] struct HistoryConfig { capacity: usize, } -#[derive(Deserialize)] +#[derive(Deserialize, Debug)] struct Config { prefix: String, shell: Option, @@ -40,7 +40,7 @@ impl Default for Config { } } -#[derive(Default)] +#[derive(Default, Debug)] struct State { config: Config, history: Option, @@ -48,9 +48,15 @@ struct State { #[init] fn init(config_dir: RString) -> State { - match fs::read_to_string(format!("{}/shell.ron", config_dir)) { + let config_dir = format!("{}/shell.ron", config_dir); + match fs::read_to_string(&config_dir) { Ok(content) => { - let config: Config = ron::from_str(&content).unwrap_or_default(); + let config: Config = ron::from_str(&content).unwrap_or_else(|err| { + let def = Config::default(); + eprintln!("[shell] Failed to parse configuration: {:?}", err); + eprintln!("[shell] Proceeding with fallback configuration: {:?}", def); + def + }); let history = config.history.as_ref().and_then(|h| match History::new(h) { Ok(history) => Some(history), @@ -62,7 +68,15 @@ fn init(config_dir: RString) -> State { State { config, history } } - Err(_) => State::default(), + Err(err) => { + let def = State::default(); + eprintln!( + "[shell] Failed to read configuration from '{}': {}", + &config_dir, err + ); + eprintln!("[shell] Proceeding with fallback configuration: {:?})", def); + def + } } }