diff --git a/src/lib/config.rs b/src/lib/config.rs index f75f9fd..205d9b9 100644 --- a/src/lib/config.rs +++ b/src/lib/config.rs @@ -405,7 +405,7 @@ impl Default for Config { #[allow(clippy::unnecessary_wraps)] #[must_use] pub fn default_show_animation_time() -> Option { - Some(50) + Some(10) } // allowed because option is needed for serde macro diff --git a/src/lib/desktop.rs b/src/lib/desktop.rs index 8530baf..99a485f 100644 --- a/src/lib/desktop.rs +++ b/src/lib/desktop.rs @@ -1,6 +1,7 @@ use std::collections::HashMap; use std::path::Path; use std::path::PathBuf; +use std::time::Instant; use std::{env, fs, string}; use freedesktop_file_parser::DesktopFile; @@ -112,6 +113,7 @@ fn find_file_case_insensitive(folder: &Path, file_name: &Regex) -> Option Vec { PathBuf::from("/var/lib/flatpak/exports/share/applications"), ]; + let start = Instant::now(); + if let Some(home) = dirs::home_dir() { paths.push(home.join(".local/share/applications")); } @@ -163,6 +167,7 @@ pub fn find_desktop_files() -> Vec { }) }) .collect(); + log::debug!("Found {} desktop files in {:?}", p.len(), start.elapsed()); p } diff --git a/src/lib/gui.rs b/src/lib/gui.rs index b7a7c59..6e6c1f5 100644 --- a/src/lib/gui.rs +++ b/src/lib/gui.rs @@ -1,5 +1,6 @@ use std::collections::HashMap; use std::sync::{Arc, Mutex}; +use std::thread; use std::time::{Duration, Instant}; use crate::config::{Anchor, Config, MatchMethod, WrapMode}; @@ -31,7 +32,7 @@ type ArcMenuMap = Arc>>>; type ArcProvider = Arc>>; type MenuItemSender = Sender, anyhow::Error>>; -pub trait ItemProvider { +pub trait ItemProvider { fn get_elements(&mut self, search: Option<&str>) -> Vec>; fn get_sub_elements(&mut self, item: &MenuItem) -> Option>>; } @@ -102,9 +103,10 @@ impl AsRef> for MenuItem { /// Will return Err when the channel between the UI and this is broken pub fn show(config: Config, item_provider: P) -> Result, anyhow::Error> where - T: Clone + 'static, - P: ItemProvider + 'static + Clone, + T: Clone + 'static + std::marker::Send, + P: ItemProvider + 'static + Clone + std::marker::Send, { + log::debug!("Starting GUI"); if let Some(ref css) = config.style { let provider = CssProvider::new(); let css_file_path = File::for_path(css); @@ -136,19 +138,45 @@ fn build_ui( sender: &Sender, anyhow::Error>>, app: &Application, ) where - T: Clone + 'static, - P: ItemProvider + 'static, + T: Clone + 'static + std::marker::Send, + P: ItemProvider + 'static + std::marker::Send, { let start = Instant::now(); + let item_provider = Arc::new(Mutex::new(item_provider)); + let provider_clone = Arc::clone(&item_provider); + let get_items = thread::spawn(move || { + log::debug!("getting items"); + provider_clone.lock().unwrap().get_elements(None) + }); + let window = ApplicationWindow::builder() .application(app) .decorated(false) .resizable(false) - .default_width(0) - .default_height(0) + .default_width(100) + .default_height(100) .build(); + let window_show = Instant::now(); + let entry = SearchEntry::new(); + let inner_box = FlowBox::new(); + let list_items: ArcMenuMap = Arc::new(Mutex::new(HashMap::new())); + + // handle keys as soon as possible + setup_key_event_handler( + &window, + &entry, + inner_box.clone(), + app.clone(), + sender.clone(), + ArcMenuMap::clone(&list_items), + config.clone(), + item_provider, + ); + + log::debug!("keyboard ready after {:?}", start.elapsed()); + window.set_widget_name("window"); if !config.normal_window { @@ -159,6 +187,8 @@ fn build_ui( window.set_namespace(Some("worf")); } + let window_done = Instant::now(); + if let Some(location) = config.location.as_ref() { for anchor in location { window.set_anchor(anchor.into(), true); @@ -168,7 +198,6 @@ fn build_ui( let outer_box = gtk4::Box::new(config.orientation.unwrap().into(), 0); outer_box.set_widget_name("outer-box"); - let entry = SearchEntry::new(); entry.set_widget_name("input"); entry.set_css_classes(&["input"]); entry.set_placeholder_text(config.prompt.as_deref()); @@ -186,16 +215,18 @@ fn build_ui( outer_box.append(&scroll); - let inner_box = FlowBox::new(); inner_box.set_widget_name("inner-box"); inner_box.set_css_classes(&["inner-box"]); inner_box.set_hexpand(true); inner_box.set_vexpand(false); + inner_box.set_selection_mode(gtk4::SelectionMode::Browse); + inner_box.set_max_children_per_line(config.columns.unwrap()); + inner_box.set_activate_on_single_click(true); + if let Some(halign) = config.halign { inner_box.set_halign(halign.into()); } - if let Some(valign) = config.valign { inner_box.set_valign(valign.into()); } else if config.orientation.unwrap() == config::Orientation::Horizontal { @@ -204,28 +235,6 @@ fn build_ui( inner_box.set_valign(Align::Start); } - inner_box.set_selection_mode(gtk4::SelectionMode::Browse); - inner_box.set_max_children_per_line(config.columns.unwrap()); - inner_box.set_activate_on_single_click(true); - - let item_provider = Arc::new(Mutex::new(item_provider)); - let list_items: ArcMenuMap = Arc::new(Mutex::new(HashMap::new())); - let elements = item_provider.lock().unwrap().get_elements(None); - - build_ui_from_menu_items( - &elements, - &list_items, - &inner_box, - config, - sender, - app, - &window, - ); - - let items_sort = ArcMenuMap::clone(&list_items); - inner_box - .set_sort_func(move |child1, child2| sort_menu_items_by_score(child1, child2, &items_sort)); - let items_focus = ArcMenuMap::clone(&list_items); inner_box.connect_map(move |fb| { fb.grab_focus(); @@ -238,23 +247,25 @@ fn build_ui( wrapper_box.append(&inner_box); scroll.set_child(Some(&wrapper_box)); - setup_key_event_handler( - &window, - &entry, - inner_box, - app.clone(), - sender.clone(), - ArcMenuMap::clone(&list_items), - config.clone(), - item_provider, - ); - window.set_child(Some(&outer_box)); - let window_show = Instant::now(); - window.show(); - let window_done = Instant::now(); + let wait_for_items = Instant::now(); + let elements = get_items.join().unwrap(); + log::debug!("got items after {:?}", wait_for_items.elapsed()); + build_ui_from_menu_items( + &elements, + &list_items, + &inner_box, + config, + sender, + app, + &window, + ); + let items_sort = ArcMenuMap::clone(&list_items); + inner_box + .set_sort_func(move |child1, child2| sort_menu_items_by_score(child1, child2, &items_sort)); + window.show(); animate_window_show(config, window.clone()); let animation_done = Instant::now(); @@ -313,7 +324,7 @@ fn build_ui_from_menu_items( } #[allow(clippy::too_many_arguments)] // todo fix this -fn setup_key_event_handler( +fn setup_key_event_handler( window: &ApplicationWindow, entry: &SearchEntry, inner_box: FlowBox, @@ -477,13 +488,13 @@ fn sort_menu_items_by_score( } fn animate_window_show(config: &Config, window: ApplicationWindow) { - let display = window.display(); if let Some(surface) = window.surface() { + let display = window.display(); + // todo this does not work for multi monitor systems let monitor = display.monitor_at_surface(&surface); if let Some(monitor) = monitor { let geometry = monitor.geometry(); - log::debug!("monitor geometry: {:?}", geometry); let Some(target_width) = percent_or_absolute(config.width.as_ref(), geometry.width()) else { return; @@ -495,12 +506,19 @@ fn animate_window_show(config: &Config, window: ApplicationWindow) { return; }; + log::debug!( + "monitor geometry: {geometry:?}, target_height {target_height}, target_width {target_width}" + ); + + let animation_start = Instant::now(); animate_window( window.clone(), config.show_animation_time.unwrap_or(0), target_height, target_width, - move || {}, + move || { + log::debug!("animation done after {:?}", animation_start.elapsed()); + }, ); } } @@ -522,10 +540,10 @@ where } fn ease_in_out_cubic(t: f32) -> f32 { - if t < 0.5 { - 4.0 * t * t * t + if t < 0.7 { + 10.0 * t * t * t } else { - 1.0 - (-2.0 * t + 2.0).powi(3) / 2.0 + 1.0 - (-2.0 * t + 2.0).powi(3) } } @@ -548,7 +566,7 @@ fn animate_window( let allocation = window.allocation(); // Define animation parameters - let animation_step_length = Duration::from_millis(8); // ~120 FPS + let animation_step_length = Duration::from_millis(20); // Start positions (initial window dimensions) let start_width = allocation.width() as f32; @@ -558,12 +576,23 @@ fn animate_window( let delta_width = target_width as f32 - start_width; let delta_height = target_height as f32 - start_height; - // Start the animation timer - let start_time = Instant::now(); + // Animation time starts when the timeout is ran for the first time + let mut start_time: Option = None; let mut last_t = 0.0; + let before_animation = Instant::now(); timeout_add_local(animation_step_length, move || { + if !window.is_visible() { + return ControlFlow::Continue; + } + let start_time = start_time.unwrap_or_else(|| { + let now = Instant::now(); + start_time = Some(now); + log::debug!("animation started after {:?}", before_animation.elapsed()); + now + }); + let elapsed_us = start_time.elapsed().as_micros() as f32; let t = (elapsed_us / (animation_time * 1000) as f32).min(1.0); @@ -746,8 +775,6 @@ fn create_menu_row( row_box.set_halign(Align::Fill); row.set_child(Some(&row_box)); - let ui_created = Instant::now(); - if config.allow_images.is_some_and(|allow_images| allow_images) { if let Some(image) = lookup_icon(&menu_item, config) { image.set_widget_name("img"); @@ -755,8 +782,6 @@ fn create_menu_row( } } - let icon_found = Instant::now(); - let label = Label::new(Some(menu_item.label.as_str())); let wrap_mode: NaturalWrapMode = if let Some(config_wrap) = &config.line_wrap { config_wrap.into() @@ -780,23 +805,18 @@ fn create_menu_row( label.set_xalign(0.0); } - // log::debug!( - // "Creating menu took {:?}, ui created after {:?}, icon found after {:?}", - // start.elapsed(), - // ui_created - start, - // icon_found - start - // ); - row.upcast() } fn lookup_icon(menu_item: &MenuItem, config: &Config) -> Option { if let Some(image_path) = &menu_item.icon_path { let img_regex = Regex::new(&format!( - r"((?i).*{})|(^/.*)", + r"((?i).*{})", known_image_extension_regex_pattern() )); - let image = if img_regex.unwrap().is_match(image_path) { + let image = if image_path.starts_with("/") { + Image::from_file(image_path) + } else if img_regex.unwrap().is_match(image_path) { if let Ok(img) = desktop::fetch_icon_from_common_dirs(&image_path) { Image::from_file(img) } else { diff --git a/src/lib/mode.rs b/src/lib/mode.rs index 73aa04b..dc9ab4e 100644 --- a/src/lib/mode.rs +++ b/src/lib/mode.rs @@ -7,7 +7,7 @@ use freedesktop_file_parser::EntryType; use rayon::prelude::*; use regex::Regex; use serde::{Deserialize, Serialize}; -use std::collections::HashMap; +use std::collections::{HashMap, HashSet}; use std::io::Read; use std::os::unix::prelude::CommandExt; use std::path::{Path, PathBuf}; @@ -46,22 +46,34 @@ struct DRunCache { #[derive(Clone)] struct DRunProvider { - items: Vec>, + items: Option>>, cache_path: Option, cache: HashMap, + data: T, } impl DRunProvider { fn new(menu_item_data: T) -> Self { + let (cache_path, d_run_cache) = load_d_run_cache(); + DRunProvider { + items: None, + cache_path, + cache: d_run_cache, + data: menu_item_data, + } + } + + fn load(&self) -> Vec> { let locale_variants = get_locale_variants(); let default_icon = "application-x-executable".to_string(); - - let (cache_path, d_run_cache) = load_d_run_cache(); - let start = Instant::now(); - let mut entries: Vec> = find_desktop_files() + let entries: Vec> = find_desktop_files() .into_par_iter() + .filter(|file| { + !file.entry.no_display.unwrap_or(false) + && !file.entry.hidden.unwrap_or(false) + }) .filter_map(|file| { let name = lookup_name_with_locale( &locale_variants, @@ -85,9 +97,7 @@ impl DRunProvider { .unwrap_or(false); if !cmd_exists { - log::warn!( - "Skipping desktop entry for {name:?} because action {action:?} does not exist" - ); + log::warn!("Skipping desktop entry for {name:?} because action {action:?} does not exist"); return None; } @@ -98,9 +108,7 @@ impl DRunProvider { .map(|s| s.content.clone()) .or(Some(default_icon.clone())); - log::debug!("file, name={name:?}, icon={icon:?}, action={action:?}"); - - let sort_score = *d_run_cache.get(&name).unwrap_or(&0); + let sort_score = *self.cache.get(&name).unwrap_or(&0); let mut entry = MenuItem { label: name.clone(), @@ -110,7 +118,7 @@ impl DRunProvider { working_dir: working_dir.clone(), initial_sort_score: sort_score, search_sort_score: 0.0, - data: Some(menu_item_data.clone()), + data: Some(self.data.clone()), visible: true, }; @@ -127,7 +135,6 @@ impl DRunProvider { .or(icon.clone()) .unwrap_or("application-x-executable".to_string()); - log::debug!("sub, action_name={action_name:?}, action_icon={action_icon:?}"); entry.sub_elements.push(MenuItem { label: action_name, @@ -137,7 +144,7 @@ impl DRunProvider { working_dir: working_dir.clone(), initial_sort_score: 0, search_sort_score: 0.0, - data: None, + data: Some(self.data.clone()), visible: true, }); } @@ -147,24 +154,28 @@ impl DRunProvider { }) .collect(); + let mut seen_actions = HashSet::new(); + let mut entries: Vec> = entries + .into_iter() + .filter(|entry| seen_actions.insert(entry.action.clone())) + .collect(); + log::info!( "parsing desktop files took {}ms", start.elapsed().as_millis() ); gui::sort_menu_items_alphabetically_honor_initial_score(&mut entries); - - DRunProvider { - items: entries, - cache_path, - cache: d_run_cache, - } + entries } } -impl ItemProvider for DRunProvider { +impl ItemProvider for DRunProvider { fn get_elements(&mut self, _: Option<&str>) -> Vec> { - self.items.clone() + if self.items.is_none() { + self.items = Some(self.load().clone()); + } + self.items.clone().unwrap() } fn get_sub_elements(&mut self, _: &MenuItem) -> Option>> { diff --git a/src/main.rs b/src/main.rs index 3645db9..4c9d440 100644 --- a/src/main.rs +++ b/src/main.rs @@ -3,12 +3,12 @@ use std::env; use anyhow::anyhow; use worf_lib::config::Mode; use worf_lib::{config, mode}; - fn main() -> anyhow::Result<()> { gtk4::init()?; env_logger::Builder::new() .parse_filters(&env::var("RUST_LOG").unwrap_or_else(|_| "error".to_owned())) + .format_timestamp_micros() .init(); let args = config::parse_args();