From 17cd0308cb77e40f2a7fe0df6bc902b2915ef657 Mon Sep 17 00:00:00 2001 From: LGUG2Z Date: Sun, 27 Apr 2025 11:44:43 -0700 Subject: [PATCH] feat(bar): improve path handling on apps widget This commit improves path handling for commands and icons in the new Application widget by making use of PathExt::replace_env when loading the user-specified ApplicationsConfig. Crucially for scoop users, this means that user-agnostic references to scoop apps can now be made like this: ``` $Env:USERPROFILE/scoop/apps/zed-nightly/current/zed.exe ``` When attempting to look up an icon for a command, we now split the command on ".exe", and if this is a complete path to a file, we try to use it to extract an icon, otherwise we try to resolve a complete path using "which" before doing the same. --- Cargo.lock | 1 + komorebi-bar/Cargo.toml | 1 + komorebi-bar/src/widgets/applications.rs | 73 ++++++++++++++++-------- 3 files changed, 50 insertions(+), 25 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index bb64175a..5cc30e4b 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2998,6 +2998,7 @@ dependencies = [ "sysinfo 0.34.2", "tracing", "tracing-subscriber", + "which", "windows 0.61.1", "windows-core 0.61.0", "windows-icons 0.1.0 (git+https://github.com/LGUG2Z/windows-icons?rev=0c9d7ee1b807347c507d3a9862dd007b4d3f4354)", diff --git a/komorebi-bar/Cargo.toml b/komorebi-bar/Cargo.toml index 527377ae..d71e41e5 100644 --- a/komorebi-bar/Cargo.toml +++ b/komorebi-bar/Cargo.toml @@ -35,6 +35,7 @@ starship-battery = "0.10" sysinfo = { workspace = true } tracing = { workspace = true } tracing-subscriber = { workspace = true } +which = { workspace = true } windows = { workspace = true } windows-core = { workspace = true } windows-icons = { git = "https://github.com/LGUG2Z/windows-icons", rev = "0c9d7ee1b807347c507d3a9862dd007b4d3f4354" } diff --git a/komorebi-bar/src/widgets/applications.rs b/komorebi-bar/src/widgets/applications.rs index 16ee2ade..6ed9a85f 100644 --- a/komorebi-bar/src/widgets/applications.rs +++ b/komorebi-bar/src/widgets/applications.rs @@ -19,13 +19,16 @@ use eframe::egui::Ui; use eframe::egui::Vec2; use image::DynamicImage; use image::RgbaImage; +use komorebi_client::PathExt; use serde::Deserialize; use serde::Serialize; use std::path::Path; +use std::path::PathBuf; use std::process::Command; use std::time::Duration; use std::time::Instant; use tracing; +use which::which; /// Minimum interval between consecutive application launches to prevent accidental spamming. const MIN_LAUNCH_INTERVAL: Duration = Duration::from_millis(800); @@ -118,28 +121,41 @@ impl From<&ApplicationsConfig> for Applications { fn from(applications_config: &ApplicationsConfig) -> Self { // Allow immediate launch by initializing last_launch in the past. let last_launch = Instant::now() - 2 * MIN_LAUNCH_INTERVAL; + let mut applications_config = applications_config.clone(); let items = applications_config .items - .iter() + .iter_mut() .enumerate() - .map(|(index, app_config)| App { - enable: app_config.enable.unwrap_or(applications_config.enable), - name: app_config - .name - .is_empty() - .then(|| format!("App {}", index + 1)) - .unwrap_or_else(|| app_config.name.clone()), - icon: Icon::try_from(app_config), - command: app_config.command.clone(), - display: app_config - .display - .or(applications_config.display) - .unwrap_or_default(), - show_command_on_hover: app_config - .show_command_on_hover - .or(applications_config.show_command_on_hover) - .unwrap_or(false), - last_launch, + .map(|(index, app_config)| { + app_config.command = app_config + .command + .replace_env() + .to_string_lossy() + .to_string(); + + if let Some(icon) = &mut app_config.icon { + *icon = icon.replace_env().to_string_lossy().to_string(); + } + + App { + enable: app_config.enable.unwrap_or(applications_config.enable), + name: app_config + .name + .is_empty() + .then(|| format!("App {}", index + 1)) + .unwrap_or_else(|| app_config.name.clone()), + icon: Icon::try_from(app_config), + command: app_config.command.clone(), + display: app_config + .display + .or(applications_config.display) + .unwrap_or_default(), + show_command_on_hover: app_config + .show_command_on_hover + .or(applications_config.show_command_on_hover) + .unwrap_or(false), + last_launch, + } }) .collect(); @@ -258,7 +274,7 @@ impl Icon { pub fn try_from(config: &AppConfig) -> Option { if let Some(icon) = config.icon.as_deref().map(str::trim) { if !icon.is_empty() { - let path = Path::new(icon); + let path = Path::new(&icon); if path.is_file() { match image::open(path).as_ref().map(DynamicImage::to_rgba8) { Ok(image) => return Some(Icon::Image(image)), @@ -272,12 +288,19 @@ impl Icon { } } - if Path::new(&config.command).is_file() { - return windows_icons::get_icon_by_path(&config.command) - .or_else(|| windows_icons_fallback::get_icon_by_path(&config.command)) - .map(Icon::Image); + let binary = PathBuf::from(config.command.split(".exe").next()?); + let path = if binary.is_file() { + Some(binary) + } else { + which(binary).ok() + }; + + match path { + Some(path) => windows_icons::get_icon_by_path(&path.to_string_lossy()) + .or_else(|| windows_icons_fallback::get_icon_by_path(&path.to_string_lossy())) + .map(Icon::Image), + None => None, } - None } /// Renders the icon in the given `Ui` context with the specified size.