From 66a708b429757ae3d1d3e1005f4b8151d4e1494a Mon Sep 17 00:00:00 2001 From: Alexander Mohr Date: Fri, 2 May 2025 01:57:33 +0200 Subject: [PATCH] improve hide/show performance --- src/lib/desktop.rs | 12 +++ src/lib/gui.rs | 80 ++++++++++------- src/lib/mode.rs | 216 +++++++++++++++++++++++++++++++++++++-------- src/main.rs | 2 +- 4 files changed, 241 insertions(+), 69 deletions(-) diff --git a/src/lib/desktop.rs b/src/lib/desktop.rs index 80f5e04..c86f87b 100644 --- a/src/lib/desktop.rs +++ b/src/lib/desktop.rs @@ -10,6 +10,7 @@ use std::path::PathBuf; use std::process::{Command, Stdio}; use std::time::Instant; use std::{env, fs, io}; +use std::os::unix::fs::PermissionsExt; /// Returns a regex with supported image extensions /// # Panics @@ -279,3 +280,14 @@ pub fn create_file_if_not_exists(path: &PathBuf) -> Result<(), Error> { Err(e) => Err(Error::Io(e.to_string())), } } + + +/// Check if the given dir entry is an executable +pub fn is_executable(entry: &PathBuf) -> bool { + if let Ok(metadata) = entry.metadata() { + let permissions = metadata.permissions(); + metadata.is_file() && (permissions.mode() & 0o111 != 0) + } else { + false + } +} diff --git a/src/lib/gui.rs b/src/lib/gui.rs index aa0f3ed..12d598e 100644 --- a/src/lib/gui.rs +++ b/src/lib/gui.rs @@ -1,4 +1,5 @@ use std::collections::HashMap; +use std::ops::{Deref, DerefMut}; use std::rc::Rc; use std::sync::{Arc, Mutex}; use std::thread; @@ -34,8 +35,8 @@ type ArcProvider = Arc + Send>>; type MenuItemSender = Sender, anyhow::Error>>; pub trait ItemProvider { - fn get_elements(&mut self, search: Option<&str>) -> Vec>; - fn get_sub_elements(&mut self, item: &MenuItem) -> Option>>; + fn get_elements(&mut self, search: Option<&str>) -> (bool, Vec>); + fn get_sub_elements(&mut self, item: &MenuItem) -> (bool, Option>>); } impl From<&Anchor> for Edge { @@ -290,9 +291,12 @@ fn build_ui( scroll.set_child(Some(&wrapper_box)); let wait_for_items = Instant::now(); - let provider_elements = get_provider_elements.join().unwrap(); + let (_changed, provider_elements) = get_provider_elements.join().unwrap(); log::debug!("got items after {:?}", wait_for_items.elapsed()); - build_ui_from_menu_items(&ui_elements, &meta, &provider_elements); + { + let mut lock = ui_elements.menu_rows.lock().unwrap(); + build_ui_from_menu_items(&ui_elements, &meta, &provider_elements, lock.deref_mut()); + } let items_sort = ArcMenuMap::clone(&ui_elements.menu_rows); ui_elements @@ -338,7 +342,8 @@ fn build_main_box(config: &Config, ui_elements: &Rc( ui: &Rc>, meta: &Rc>, items: &Vec>, + map: &mut HashMap>, ) { let start = Instant::now(); { - let mut arc_lock = ui.menu_rows.lock().unwrap(); let got_lock = Instant::now(); ui.main_box.unset_sort_func(); @@ -370,13 +375,13 @@ fn build_ui_from_menu_items( ui.main_box.remove(&b); drop(b); } - arc_lock.clear(); + map.clear(); let cleared_box = Instant::now(); for entry in items { if entry.visible { - arc_lock.insert(add_menu_item(ui, meta, entry), (*entry).clone()); + map.insert(add_menu_item(ui, meta, entry), (*entry).clone()); } } @@ -389,10 +394,6 @@ fn build_ui_from_menu_items( created_ui - start ); } - - let lic = ArcMenuMap::clone(&ui.menu_rows); - ui.main_box - .set_sort_func(move |child2, child1| sort_menu_items_by_score(child1, child2, &lic)); } fn setup_key_event_handler( @@ -416,15 +417,26 @@ fn handle_key_press( meta: &Rc>, keyboard_key: Key, ) -> Propagation { - let update_view = |query: &String, items: &mut Vec>| { - set_menu_visibility_for_search(query, items, &meta.config); - build_ui_from_menu_items(ui, meta, items); - select_first_visible_child(ui); + let update_view = |query: &String| { + let mut lock = ui.menu_rows.lock().unwrap(); + let mut menus = lock.iter_mut().map(|(s, v)| v).collect::>(); + set_menu_visibility_for_search(query, menus.as_mut_slice(), &meta.config); + for (fb, item) in lock.iter() { + fb.set_visible(item.visible); + } + + select_first_visible_child(&*lock, &ui.main_box); }; let update_view_from_provider = |query: &String| { - let mut filtered_list = meta.item_provider.lock().unwrap().get_elements(Some(query)); - update_view(query, &mut filtered_list); + let (changed, filtered_list) = meta.item_provider.lock().unwrap().get_elements(Some(query)); + if changed { + + let mut lock = ui.menu_rows.lock().unwrap(); + build_ui_from_menu_items(&ui, &meta, &filtered_list, lock.deref_mut()); + } + + update_view(query); }; match keyboard_key { @@ -461,18 +473,22 @@ fn handle_key_press( let lock = ui.menu_rows.lock().unwrap(); let menu_item = lock.get(fb); if let Some(menu_item) = menu_item { - if let Some(mut new_items) = meta + let (changed, items) = meta .item_provider .lock() .unwrap() - .get_sub_elements(menu_item) - { - let query = menu_item.label.clone(); - drop(lock); + .get_sub_elements(menu_item); - ui.search.set_text(&query); - update_view(&query, &mut new_items); + let items = items.unwrap_or_default(); + if changed { + let mut lock = ui.menu_rows.lock().unwrap(); + build_ui_from_menu_items(ui, meta, &items, &mut *lock); } + + let query = menu_item.label.clone(); + + ui.search.set_text(&query); + update_view(&query); } } } @@ -847,7 +863,7 @@ fn lookup_icon(menu_item: &MenuItem, config: &Config) -> Option( query: &str, - items: &mut [MenuItem], + items: &mut [&mut MenuItem], config: &Config, ) { { @@ -906,14 +922,16 @@ fn set_menu_visibility_for_search( } } -fn select_first_visible_child(ui: &UiElements) { - let items = ui.menu_rows.lock().unwrap(); +fn select_first_visible_child( + items: &HashMap>, + flow_box: &FlowBox, +) { for i in 0..items.len() { let i_32 = i.try_into().unwrap_or(i32::MAX); - if let Some(child) = ui.main_box.child_at_index(i_32) { + if let Some(child) = flow_box.child_at_index(i_32) { if child.is_visible() { - ui.main_box.select_child(&child); - break; + flow_box.select_child(&child); + return; } } } diff --git a/src/lib/mode.rs b/src/lib/mode.rs index bc9e739..0a10c0a 100644 --- a/src/lib/mode.rs +++ b/src/lib/mode.rs @@ -1,8 +1,5 @@ use crate::config::{Config, expand_path}; -use crate::desktop::{ - create_file_if_not_exists, find_desktop_files, get_locale_variants, load_cache_file, - lookup_name_with_locale, save_cache_file, spawn_fork, -}; +use crate::desktop::{create_file_if_not_exists, find_desktop_files, get_locale_variants, is_executable, load_cache_file, lookup_name_with_locale, save_cache_file, spawn_fork}; use crate::gui::{ItemProvider, MenuItem}; use crate::{Error, gui}; use freedesktop_file_parser::EntryType; @@ -13,7 +10,7 @@ use std::collections::{HashMap, HashSet}; use std::io::Read; use std::path::{Path, PathBuf}; use std::time::Instant; -use std::{fs, io}; +use std::{env, fs, io}; #[derive(Debug, Deserialize, Serialize, Clone)] struct DRunCache { @@ -145,19 +142,112 @@ impl DRunProvider { } } -impl ItemProvider for DRunProvider { - fn get_elements(&mut self, _: Option<&str>) -> Vec> { +impl ItemProvider for RunProvider { + fn get_elements(&mut self, _: Option<&str>) -> (bool, Vec>) { if self.items.is_none() { self.items = Some(self.load().clone()); } - self.items.clone().unwrap() + (false, self.items.clone().unwrap()) } - fn get_sub_elements(&mut self, _: &MenuItem) -> Option>> { - None + fn get_sub_elements(&mut self, _: &MenuItem) -> (bool, std::option::Option>>) { + (false, None) } } +#[derive(Clone)] +struct RunProvider { + items: Option>>, + cache_path: Option, + cache: HashMap, + data: T, +} + +impl RunProvider { + fn new(menu_item_data: T) -> Self { + let (cache_path, d_run_cache) = load_run_cache(); + RunProvider { + items: None, + cache_path, + cache: d_run_cache, + data: menu_item_data, + } + } + + #[allow(clippy::cast_possible_truncation)] + #[allow(clippy::cast_precision_loss)] + fn load(&self) -> Vec> { + let path_var = env::var("PATH").unwrap_or_default(); + let paths = env::split_paths(&path_var); + + let entries : Vec<_>= paths + .filter(|dir| dir.is_dir()) + .flat_map(|dir| { + fs::read_dir(dir) + .into_iter() + .flatten() + .filter_map(Result::ok) + .filter_map(|entry| { + let path = entry.path(); + if is_executable(&path) { + let label = path + .file_name() + .and_then(|s| s.to_str()) + .map(String::from)?; + let sort_score = *self.cache.get(&label).unwrap_or(&0) as f64; + Some(MenuItem::new( + label, + None, + path.to_str().map(std::string::ToString::to_string), + vec![], + None, + sort_score, + None, + )) + } else { + None + } + }) + }) + .collect(); + + + let mut seen_actions = HashSet::new(); + let mut entries: Vec> = entries + .into_iter() + .filter(|entry| { + if let Some(action) = &entry.action { + if let Some(cmd) = action.split('/').last() { + seen_actions.insert(cmd.to_string()) + } else { + false + } + } else { + false + } + }) + .collect(); + + + gui::sort_menu_items_alphabetically_honor_initial_score(&mut entries); + entries + } +} + +impl ItemProvider for DRunProvider { + fn get_elements(&mut self, _: Option<&str>) -> (bool, Vec>) { + if self.items.is_none() { + self.items = Some(self.load().clone()); + } + (false, self.items.clone().unwrap()) + } + + fn get_sub_elements(&mut self, _: &MenuItem) -> (bool, std::option::Option>>) { + (false, None) + } +} + + #[derive(Clone)] struct FileItemProvider { last_result: Option>>, @@ -213,7 +303,7 @@ impl FileItemProvider { } impl ItemProvider for FileItemProvider { - fn get_elements(&mut self, search: Option<&str>) -> Vec> { + fn get_elements(&mut self, search: Option<&str>) -> (bool, Vec>) { let default_path = if let Some(home) = dirs::home_dir() { home.display().to_string() } else { @@ -230,10 +320,10 @@ impl ItemProvider for FileItemProvider { if !path.exists() { if let Some(last) = &self.last_result { - return last.clone(); + return (false, last.clone()); } - return vec![]; + return (true, vec![]); } if path.is_dir() { @@ -295,11 +385,11 @@ impl ItemProvider for FileItemProvider { gui::sort_menu_items_alphabetically_honor_initial_score(&mut items); self.last_result = Some(items.clone()); - items + (true, items) } - fn get_sub_elements(&mut self, _: &MenuItem) -> Option>> { - self.last_result.clone() + fn get_sub_elements(&mut self, _: &MenuItem) -> (bool, std::option::Option>>) { + (false, self.last_result.clone()) } } @@ -344,12 +434,12 @@ impl SshProvider { } impl ItemProvider for SshProvider { - fn get_elements(&mut self, _: Option<&str>) -> Vec> { - self.elements.clone() + fn get_elements(&mut self, _: Option<&str>) -> (bool, Vec>) { + (false, self.elements.clone()) } - fn get_sub_elements(&mut self, _: &MenuItem) -> Option>> { - None + fn get_sub_elements(&mut self, _: &MenuItem) -> (bool, std::option::Option>>) { + (false, None) } } @@ -386,7 +476,7 @@ impl MathProvider { } impl ItemProvider for MathProvider { - fn get_elements(&mut self, search: Option<&str>) -> Vec> { + fn get_elements(&mut self, search: Option<&str>) -> (bool, Vec>) { if let Some(search_text) = search { let result = match meval::eval_str(search_text) { Ok(result) => result.to_string(), @@ -404,14 +494,14 @@ impl ItemProvider for MathProvider { ); let mut result = vec![item]; result.append(&mut self.elements.clone()); - result + (true, result) } else { - self.elements.clone() + (false, self.elements.clone()) } } - fn get_sub_elements(&mut self, _: &MenuItem) -> Option>> { - None + fn get_sub_elements(&mut self, _: &MenuItem) -> (bool, std::option::Option>>) { + (false, None) } } @@ -438,12 +528,12 @@ impl DMenuProvider { } impl ItemProvider for DMenuProvider { - fn get_elements(&mut self, _: Option<&str>) -> Vec> { - self.items.clone() + fn get_elements(&mut self, _: Option<&str>) -> (bool, Vec>) { + (false, self.items.clone()) } - fn get_sub_elements(&mut self, _: &MenuItem) -> Option>> { - None + fn get_sub_elements(&mut self, _: &MenuItem) -> (bool, std::option::Option>>) { + (false, None) } } @@ -478,7 +568,7 @@ impl AutoItemProvider { } impl ItemProvider for AutoItemProvider { - fn get_elements(&mut self, search_opt: Option<&str>) -> Vec> { + fn get_elements(&mut self, search_opt: Option<&str>) -> (bool, Vec>) { if let Some(search) = search_opt { let trimmed_search = search.trim(); if trimmed_search.is_empty() { @@ -496,9 +586,9 @@ impl ItemProvider for AutoItemProvider { self.ssh.get_elements(search_opt) } else { // return ssh and drun items - let mut drun = self.drun.get_elements(search_opt); - drun.append(&mut self.ssh.get_elements(search_opt)); - drun + let (changed, mut drun) = self.drun.get_elements(search_opt); + drun.append(&mut self.ssh.get_elements(search_opt).1); + (changed, drun) } } else { self.drun.get_elements(search_opt) @@ -508,8 +598,9 @@ impl ItemProvider for AutoItemProvider { fn get_sub_elements( &mut self, item: &MenuItem, - ) -> Option>> { - Some(self.get_elements(Some(item.label.as_ref()))) + ) -> (bool, std::option::Option>>) { + let (changed, items) = self.get_elements(Some(item.label.as_ref())); + (changed, Some(items)) } } @@ -534,6 +625,27 @@ pub fn d_run(config: &Config) -> Result<(), Error> { Ok(()) } +/// Shows the run mode +/// # Errors +/// +/// Will return `Err` if it was not able to spawn the process +pub fn run(config: &Config) -> Result<(), Error> { + let provider = RunProvider::new(String::new()); + let cache_path = provider.cache_path.clone(); + let mut cache = provider.cache.clone(); + + let selection_result = gui::show(config.clone(), provider, false); + match selection_result { + Ok(s) => update_run_cache_and_run(cache_path, &mut cache, s)?, + Err(_) => { + log::error!("No item selected"); + } + } + + Ok(()) +} + + /// Shows the auto mode /// # Errors /// @@ -696,9 +808,39 @@ fn update_drun_cache_and_run( } } + +fn update_run_cache_and_run( + cache_path: Option, + cache: &mut HashMap, + selection_result: MenuItem, +) -> Result<(), Error> { + if let Some(cache_path) = cache_path { + *cache.entry(selection_result.label).or_insert(0) += 1; + if let Err(e) = save_cache_file(&cache_path, cache) { + log::warn!("cannot save run cache {e:?}"); + } + } + + if let Some(action) = selection_result.action { + spawn_fork(&action, selection_result.working_dir.as_ref()) + } else { + Err(Error::MissingAction) + } +} + fn load_d_run_cache() -> (Option, HashMap) { let cache_path = dirs::cache_dir().map(|x| x.join("worf-drun")); - let d_run_cache = { + load_cache(cache_path) +} + +fn load_run_cache() -> (Option, HashMap) { + let cache_path = dirs::cache_dir().map(|x| x.join("worf-run")); + load_cache(cache_path) +} + + +fn load_cache(cache_path: Option) -> (Option, HashMap) { + let cache = { if let Some(ref cache_path) = cache_path { if let Err(e) = create_file_if_not_exists(cache_path) { log::warn!("No drun cache file and cannot create: {e:?}"); @@ -707,5 +849,5 @@ fn load_d_run_cache() -> (Option, HashMap) { load_cache_file(cache_path.as_ref()).unwrap_or_default() }; - (cache_path, d_run_cache) + (cache_path, cache) } diff --git a/src/main.rs b/src/main.rs index a70c8f1..3879f7f 100644 --- a/src/main.rs +++ b/src/main.rs @@ -15,7 +15,7 @@ fn main() -> anyhow::Result<()> { if let Some(show) = &config.show() { match show { Mode::Run => { - todo!("run not implemented") + mode::run(&config).map_err(|e| anyhow!(e))?; } Mode::Drun => { mode::d_run(&config).map_err(|e| anyhow!(e))?;