diff --git a/Cargo.lock b/Cargo.lock index 699f2ef..4f787b8 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -458,6 +458,21 @@ dependencies = [ "xdgkit", ] +[[package]] +name = "futures" +version = "0.3.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "65bc07b1a8bc7c85c5f2e110c476c7389b4554ba72af57d8445ea63a576b0876" +dependencies = [ + "futures-channel", + "futures-core", + "futures-executor", + "futures-io", + "futures-sink", + "futures-task", + "futures-util", +] + [[package]] name = "futures-channel" version = "0.3.31" @@ -465,6 +480,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2dff15bf788c671c1934e366d07e30c1814a8ef514e1af724a602e8a2fbe1b10" dependencies = [ "futures-core", + "futures-sink", ] [[package]] @@ -511,6 +527,12 @@ dependencies = [ "syn", ] +[[package]] +name = "futures-sink" +version = "0.3.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e575fab7d1e0dcb8d0c7bcf9a63ee213816ab51902e6d244a95819acacf1d4f7" + [[package]] name = "futures-task" version = "0.3.31" @@ -523,9 +545,13 @@ version = "0.3.31" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9fa08315bb612088cc391249efdc3bc77536f16c91f6cf495e6fbe85b20a4a81" dependencies = [ + "futures-channel", "futures-core", + "futures-io", "futures-macro", + "futures-sink", "futures-task", + "memchr", "pin-project-lite", "pin-utils", "slab", @@ -872,15 +898,6 @@ dependencies = [ "libc", ] -[[package]] -name = "home" -version = "0.5.11" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "589533453244b0995c858700322199b2becb13b627df2851f64a2775d024abcf" -dependencies = [ - "windows-sys 0.59.0", -] - [[package]] name = "hyprland" version = "0.4.0-beta.2" @@ -1004,6 +1021,16 @@ version = "0.9.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "cd945864f07fe9f5371a27ad7b52a172b4b499999f1d97574c9fa68373937e12" +[[package]] +name = "lock_api" +version = "0.4.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "07af8b9cdd281b7915f413fa73f29ebd5d55d0d3f0155584dade1ff18cea1b17" +dependencies = [ + "autocfg", + "scopeguard", +] + [[package]] name = "log" version = "0.4.27" @@ -1137,6 +1164,29 @@ dependencies = [ "system-deps", ] +[[package]] +name = "parking_lot" +version = "0.12.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f1bf18183cf54e8d6059647fc3063646a1801cf30896933ec2311622cc4b9a27" +dependencies = [ + "lock_api", + "parking_lot_core", +] + +[[package]] +name = "parking_lot_core" +version = "0.9.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e401f977ab385c9e4e3ab30627d6f26d00e2c73eef317493c4ec6d468726cf8" +dependencies = [ + "cfg-if", + "libc", + "redox_syscall", + "smallvec", + "windows-targets", +] + [[package]] name = "paste" version = "1.0.15" @@ -1280,6 +1330,35 @@ version = "0.6.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ec0be4795e2f6a28069bec0b5ff3e2ac9bafc99e6a9a7dc3547996c5c816922c" +[[package]] +name = "rayon" +version = "1.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b418a60154510ca1a002a752ca9714984e21e4241e804d32555251faf8b78ffa" +dependencies = [ + "either", + "rayon-core", +] + +[[package]] +name = "rayon-core" +version = "1.12.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1465873a3dfdaa8ae7cb14b4383657caab0b3e8a0aa9ae8e04b044854c8dfce2" +dependencies = [ + "crossbeam-deque", + "crossbeam-utils", +] + +[[package]] +name = "redox_syscall" +version = "0.5.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d2f103c6d277498fbceb16e84d317e2a400f160f46904d5f5410848c829511a3" +dependencies = [ + "bitflags 2.9.0", +] + [[package]] name = "redox_users" version = "0.5.0" @@ -1354,6 +1433,12 @@ version = "1.0.20" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "28d3b2b1366ec20994f1fd18c3c594f05c5dd4bc44d8bb0c1c632c8d6829481f" +[[package]] +name = "scopeguard" +version = "1.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "94143f37725109f92c262ed2cf5e59bce7498c01bcc1502d7b9afe439a4e9f49" + [[package]] name = "semver" version = "1.0.26" @@ -1412,6 +1497,15 @@ dependencies = [ "serde", ] +[[package]] +name = "signal-hook-registry" +version = "1.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a9e9e0b4211b72e7b8b6e85c807d36c212bdb33ea8587f7569562a84df5465b1" +dependencies = [ + "libc", +] + [[package]] name = "siphasher" version = "1.0.1" @@ -1536,7 +1630,9 @@ dependencies = [ "bytes", "libc", "mio", + "parking_lot", "pin-project-lite", + "signal-hook-registry", "socket2", "tokio-macros", "windows-sys 0.52.0", @@ -1786,19 +1882,21 @@ dependencies = [ "dirs", "env_logger", "freedesktop-file-parser", + "futures", "gdk4", "gtk4", "gtk4-layer-shell", - "home", "hyprland", "libc", "log", "meval", + "rayon", "regex", "serde", "serde_json", "strsim 0.11.1", "thiserror", + "tokio", "toml", "tree_magic_mini", "which", diff --git a/Cargo.toml b/Cargo.toml index 9fa4efd..08c3539 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -27,7 +27,6 @@ gtk4-layer-shell = "0.5.0" gdk4 = "0.9.6" anyhow = "1.0.97" env_logger = "0.11.8" -home = "0.5.11" log = "0.4.27" regex = "1.11.1" hyprland = "0.4.0-beta.2" @@ -44,3 +43,6 @@ dirs = "6.0.0" which = "7.0.3" meval = "0.2.0" tree_magic_mini = "3.1.6" +rayon = "1.10.0" +tokio = { version = "1.44.2", features = ["full"] } +futures = "0.3.31" diff --git a/src/lib/desktop.rs b/src/lib/desktop.rs index 6c81503..8530baf 100644 --- a/src/lib/desktop.rs +++ b/src/lib/desktop.rs @@ -4,10 +4,13 @@ use std::path::PathBuf; use std::{env, fs, string}; use freedesktop_file_parser::DesktopFile; +use futures::stream; use gdk4::Display; -use gtk4::prelude::*; +use gdk4::prelude::FileExt; use gtk4::{IconLookupFlags, IconTheme, TextDirection}; -use home::home_dir; +use rayon::prelude::*; + +use futures::StreamExt; use log; use regex::Regex; @@ -16,50 +19,50 @@ pub enum DesktopError { MissingIcon, ParsingError(String), } - -/// # Errors -/// -/// Will return `Err` if no icon can be found -pub fn default_icon() -> Result { - fetch_icon_from_theme("image-missing").map_err(|_| DesktopError::MissingIcon) -} - -fn fetch_icon_from_theme(icon_name: &str) -> Result { - let display = gtk4::gdk::Display::default(); - if display.is_none() { - log::error!("Failed to get display"); - } - - let display = Display::default().expect("Failed to get default display"); - let theme = IconTheme::for_display(&display); - - let icon = theme.lookup_icon( - icon_name, - &[], - 32, - 1, - TextDirection::None, - IconLookupFlags::empty(), - ); - - match icon - .file() - .and_then(|file| file.path()) - .and_then(|path| path.to_str().map(string::ToString::to_string)) - { - None => { - let path = PathBuf::from("/usr/share/icons") - .join(theme.theme_name()) - .join(format!("{icon_name}.svg")); - if path.exists() { - Ok(path.display().to_string()) - } else { - Err(DesktopError::MissingIcon) - } - } - Some(i) => Ok(i), - } -} +// +// /// # Errors +// /// +// /// Will return `Err` if no icon can be found +// pub fn default_icon() -> Result { +// fetch_icon_from_theme("image-missing").map_err(|_| DesktopError::MissingIcon) +// } +// +// fn fetch_icon_from_theme(icon_name: &str) -> Result { +// let display = Display::default(); +// if display.is_none() { +// log::error!("Failed to get display"); +// } +// +// let display = Display::default().expect("Failed to get default display"); +// let theme = IconTheme::for_display(&display); +// +// let icon = theme.lookup_icon( +// icon_name, +// &[], +// 32, +// 1, +// TextDirection::None, +// IconLookupFlags::empty(), +// ); +// +// match icon +// .file() +// .and_then(|file| file.path()) +// .and_then(|path| path.to_str().map(string::ToString::to_string)) +// { +// None => { +// let path = PathBuf::from("/usr/share/icons") +// .join(theme.theme_name()) +// .join(format!("{icon_name}.svg")); +// if path.exists() { +// Ok(path.display().to_string()) +// } else { +// Err(DesktopError::MissingIcon) +// } +// } +// Some(i) => Ok(i), +// } +// } pub fn known_image_extension_regex_pattern() -> Regex { Regex::new(&format!( @@ -80,7 +83,7 @@ pub fn fetch_icon_from_common_dirs(icon_name: &str) -> Result Option Vec { let mut paths = vec![ PathBuf::from("/usr/share/applications"), @@ -131,27 +135,28 @@ pub fn find_desktop_files() -> Vec { PathBuf::from("/var/lib/flatpak/exports/share/applications"), ]; - if let Some(home) = home_dir() { + if let Some(home) = dirs::home_dir() { paths.push(home.join(".local/share/applications")); } if let Ok(xdg_data_home) = env::var("XDG_DATA_HOME") { - // todo use dirs:: instead paths.push(PathBuf::from(xdg_data_home).join(".applications")); } - if let Ok(xdg_data_dir) = env::var("XDG_DATA_DIRS") { - paths.push(PathBuf::from(xdg_data_dir).join(".applications")); + if let Ok(xdg_data_dirs) = env::var("XDG_DATA_DIRS") { + for dir in xdg_data_dirs.split(':') { + paths.push(PathBuf::from(dir).join(".applications")); + } } let regex = &Regex::new("(?i).*\\.desktop$").unwrap(); let p: Vec<_> = paths - .into_iter() + .into_par_iter() .filter(|desktop_dir| desktop_dir.exists()) .filter_map(|icon_dir| find_file_case_insensitive(&icon_dir, regex)) .flat_map(|desktop_files| { - desktop_files.into_iter().filter_map(|desktop_file| { + desktop_files.into_par_iter().filter_map(|desktop_file| { fs::read_to_string(desktop_file) .ok() .and_then(|content| freedesktop_file_parser::parse(&content).ok()) @@ -161,6 +166,26 @@ pub fn find_desktop_files() -> Vec { p } +pub fn lookup_icon(name: &str, size: i32) -> gtk4::Image { + let img_regex = Regex::new(&format!( + r"((?i).*{})|(^/.*)", + known_image_extension_regex_pattern() + )); + let image = if img_regex.unwrap().is_match(name) { + if let Ok(img) = fetch_icon_from_common_dirs(&name) { + gtk4::Image::from_file(img) + } else { + gtk4::Image::from_icon_name(name) + } + } else { + gtk4::Image::from_icon_name(name) + }; + + image.set_pixel_size(size); + + image +} + #[must_use] pub fn get_locale_variants() -> Vec { let locale = env::var("LC_ALL") diff --git a/src/lib/gui.rs b/src/lib/gui.rs index 8ba6cce..b7a7c59 100644 --- a/src/lib/gui.rs +++ b/src/lib/gui.rs @@ -251,17 +251,18 @@ fn build_ui( window.set_child(Some(&outer_box)); + let window_show = Instant::now(); + window.show(); let window_done = Instant::now(); - window.show(); animate_window_show(config, window.clone()); let animation_done = Instant::now(); log::debug!( - "Building UI took {:?}, window creation {:?}, animation {:?}", + "Building UI took {:?}, window show {:?}, animation {:?}", start.elapsed(), - window_done - start, - animation_done - start + window_done - window_show, + animation_done - window_done ); } @@ -300,7 +301,7 @@ fn build_ui_from_menu_items( let created_ui = Instant::now(); log::debug!( - "Creating UI took {:?}, got locker after {:?}, cleared box after {:?}, created UI after {:?}", + "Creating UI took {:?}, got lock after {:?}, cleared box after {:?}, created UI after {:?}", start.elapsed(), got_lock - start, cleared_box - start, @@ -482,6 +483,7 @@ fn animate_window_show(config: &Config, window: ApplicationWindow) { 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; @@ -536,10 +538,17 @@ fn animate_window( ) where Func: Fn() + 'static, { + if animation_time == 0 { + window.set_width_request(target_width); + window.set_height_request(target_height); + on_done_func(); + return; + } + let allocation = window.allocation(); // Define animation parameters - let animation_step_length = Duration::from_millis(16); // ~60 FPS + let animation_step_length = Duration::from_millis(8); // ~120 FPS // Start positions (initial window dimensions) let start_width = allocation.width() as f32; @@ -552,32 +561,29 @@ fn animate_window( // Start the animation timer let start_time = Instant::now(); - // Start the animation loop (runs at ~60 FPS) + let mut last_t = 0.0; + timeout_add_local(animation_step_length, move || { - // Get the elapsed time in milliseconds since the animation started - let elapsed_ms = start_time.elapsed().as_millis() as f32; // Elapsed time in milliseconds + let elapsed_us = start_time.elapsed().as_micros() as f32; + let t = (elapsed_us / (animation_time * 1000) as f32).min(1.0); - // Calculate the progress (t) from 0.0 to 1.0 based on elapsed time and animation duration - let t = (elapsed_ms / animation_time as f32).min(1.0); + // Skip if there's no meaningful change in progress + if (t - last_t).abs() < 0.001 && t < 1.0 { + return ControlFlow::Continue; + } + last_t = t; - // Apply easing function for smoother transition - // could be other easing function, but this looked best - // todo make other easing types configurable? let eased_t = ease_in_out_cubic(t); - // Calculate the new width and height based on easing let current_width = start_width + delta_width * eased_t; let current_height = start_height + delta_height * eased_t; - // Round the dimensions to nearest integers let rounded_width = current_width.round() as i32; let rounded_height = current_height.round() as i32; - // Perform the resizing of the window based on the current width and height window.set_width_request(rounded_width); window.set_height_request(rounded_height); - // If the animation is complete (t >= 1.0), set final size and break the loop if t >= 1.0 { window.set_width_request(target_width); window.set_height_request(target_height); @@ -774,20 +780,23 @@ 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 - ); + // 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).*{})|(^/.*)", known_image_extension_regex_pattern())); - let image = if img_regex.unwrap().is_match(image_path) { + let img_regex = Regex::new(&format!( + r"((?i).*{})|(^/.*)", + known_image_extension_regex_pattern() + )); + let image = 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 15052f9..73aa04b 100644 --- a/src/lib/mode.rs +++ b/src/lib/mode.rs @@ -1,5 +1,10 @@ +use crate::config::{Config, expand_path}; +use crate::desktop::{find_desktop_files, get_locale_variants, lookup_name_with_locale}; +use crate::gui; +use crate::gui::{ItemProvider, MenuItem}; use anyhow::Context; use freedesktop_file_parser::EntryType; +use rayon::prelude::*; use regex::Regex; use serde::{Deserialize, Serialize}; use std::collections::HashMap; @@ -10,13 +15,6 @@ use std::process::{Command, Stdio}; use std::time::Instant; use std::{env, fmt, fs, io}; -use crate::config::{Config, expand_path}; -use crate::desktop::{ - default_icon, find_desktop_files, get_locale_variants, lookup_name_with_locale, -}; -use crate::gui; -use crate::gui::{ItemProvider, MenuItem}; - #[derive(Debug)] pub enum ModeError { UpdateCacheError(String), @@ -53,103 +51,101 @@ struct DRunProvider { cache: HashMap, } -impl DRunProvider { +impl DRunProvider { fn new(menu_item_data: T) -> Self { let locale_variants = get_locale_variants(); - let default_icon = default_icon().unwrap_or_default(); + 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> = Vec::new(); - for file in find_desktop_files().iter().filter(|f| { - f.entry.hidden.is_none_or(|hidden| !hidden) - && f.entry.no_display.is_none_or(|no_display| !no_display) - }) { - let Some(name) = lookup_name_with_locale( - &locale_variants, - &file.entry.name.variants, - &file.entry.name.default, - ) else { - log::warn!("Skipping desktop entry without name {file:?}"); - continue; - }; - let (action, working_dir) = match &file.entry.entry_type { - EntryType::Application(app) => (app.exec.clone(), app.path.clone()), - _ => (None, None), - }; - - let cmd_exists = action - .as_ref() - .and_then(|a| { - a.split(' ') - .next() - .map(|cmd| cmd.replace('"', "")) - .map(|cmd| PathBuf::from(&cmd).exists() || which::which(&cmd).is_ok()) - }) - .unwrap_or(false); - - if !cmd_exists { - log::warn!( - "Skipping desktop entry for {name:?} because action {action:?} does not exist" - ); - continue; - } - - let icon = file - .entry - .icon - .as_ref() - .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 mut entry: MenuItem = MenuItem { - label: name, - icon_path: icon.clone(), - action, - sub_elements: Vec::default(), - working_dir: working_dir.clone(), - initial_sort_score: *sort_score, - search_sort_score: 0.0, - data: Some(menu_item_data.clone()), - visible: true, - }; - - file.actions.iter().for_each(|(_, action)| { - if let Some(action_name) = lookup_name_with_locale( + let mut entries: Vec> = find_desktop_files() + .into_par_iter() + .filter_map(|file| { + let name = lookup_name_with_locale( &locale_variants, - &action.name.variants, - &action.name.default, - ) { - let action_icon = action - .icon - .as_ref() - .map(|s| s.content.clone()) - .or(icon.clone()) - .unwrap_or("application-x-executable".to_string()); + &file.entry.name.variants, + &file.entry.name.default, + )?; - log::debug!("sub, action_name={action_name:?}, action_icon={action_icon:?}"); + let (action, working_dir) = match &file.entry.entry_type { + EntryType::Application(app) => (app.exec.clone(), app.path.clone()), + _ => return None, + }; - let sub_entry = MenuItem { - label: action_name, - icon_path: Some(action_icon), - action: action.exec.clone(), - sub_elements: Vec::default(), - working_dir: working_dir.clone(), - initial_sort_score: 0, // subitems are never sorted right now. - search_sort_score: 0.0, - data: None, - visible: true, - }; - entry.sub_elements.push(sub_entry); + let cmd_exists = action + .as_ref() + .and_then(|a| { + a.split(' ') + .next() + .map(|cmd| cmd.replace('"', "")) + .map(|cmd| PathBuf::from(&cmd).exists() || which::which(&cmd).is_ok()) + }) + .unwrap_or(false); + + if !cmd_exists { + log::warn!( + "Skipping desktop entry for {name:?} because action {action:?} does not exist" + ); + return None; } - }); - entries.push(entry); - } + let icon = file + .entry + .icon + .as_ref() + .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 mut entry = MenuItem { + label: name.clone(), + icon_path: icon.clone(), + action: action.clone(), + sub_elements: Vec::new(), + working_dir: working_dir.clone(), + initial_sort_score: sort_score, + search_sort_score: 0.0, + data: Some(menu_item_data.clone()), + visible: true, + }; + + for (_, action) in &file.actions { + if let Some(action_name) = lookup_name_with_locale( + &locale_variants, + &action.name.variants, + &action.name.default, + ) { + let action_icon = action + .icon + .as_ref() + .map(|s| s.content.clone()) + .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, + icon_path: Some(action_icon), + action: action.exec.clone(), + sub_elements: Vec::new(), + working_dir: working_dir.clone(), + initial_sort_score: 0, + search_sort_score: 0.0, + data: None, + visible: true, + }); + } + } + + Some(entry) + }) + .collect(); log::info!( "parsing desktop files took {}ms", diff --git a/src/main.rs b/src/main.rs index cae7ce3..3645db9 100644 --- a/src/main.rs +++ b/src/main.rs @@ -8,7 +8,6 @@ fn main() -> anyhow::Result<()> { gtk4::init()?; env_logger::Builder::new() - // todo change to error as default .parse_filters(&env::var("RUST_LOG").unwrap_or_else(|_| "error".to_owned())) .init();