performance improvements

This commit is contained in:
Alexander Mohr 2025-04-24 22:20:59 +02:00
parent 1cf6fa5f13
commit 8673ef4d13
6 changed files with 312 additions and 183 deletions

118
Cargo.lock generated
View file

@ -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",

View file

@ -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"

View file

@ -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<String, DesktopError> {
fetch_icon_from_theme("image-missing").map_err(|_| DesktopError::MissingIcon)
}
fn fetch_icon_from_theme(icon_name: &str) -> Result<String, DesktopError> {
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<String, DesktopError> {
// fetch_icon_from_theme("image-missing").map_err(|_| DesktopError::MissingIcon)
// }
//
// fn fetch_icon_from_theme(icon_name: &str) -> Result<String, DesktopError> {
// 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<String, DesktopErr
PathBuf::from("/usr/share/pixmaps"),
];
if let Some(home) = home_dir() {
if let Some(home) = dirs::home_dir() {
paths.push(home.join(".local/share/icons"));
}
@ -124,6 +127,7 @@ fn find_file_case_insensitive(folder: &Path, file_name: &Regex) -> Option<Vec<Pa
///
/// When it cannot parse the internal regex
#[must_use]
pub fn find_desktop_files() -> Vec<DesktopFile> {
let mut paths = vec![
PathBuf::from("/usr/share/applications"),
@ -131,27 +135,28 @@ pub fn find_desktop_files() -> Vec<DesktopFile> {
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<DesktopFile> {
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<String> {
let locale = env::var("LC_ALL")

View file

@ -251,17 +251,18 @@ fn build_ui<T, P>(
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<T: Clone + 'static>(
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<Func>(
) 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<Func>(
// 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<T: Clone + 'static>(
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<T: Clone>(menu_item: &MenuItem<T>, config: &Config) -> Option<Image> {
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 {

View file

@ -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<T: Clone> {
cache: HashMap<String, i64>,
}
impl<T: Clone> DRunProvider<T> {
impl<T: Clone + std::marker::Send + std::marker::Sync> DRunProvider<T> {
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<MenuItem<T>> = 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<T> = 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<MenuItem<T>> = 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",

View file

@ -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();