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.
This commit is contained in:
LGUG2Z
2025-04-27 11:44:43 -07:00
parent 10424b696f
commit 17cd0308cb
3 changed files with 50 additions and 25 deletions

1
Cargo.lock generated
View File

@@ -2998,6 +2998,7 @@ dependencies = [
"sysinfo 0.34.2", "sysinfo 0.34.2",
"tracing", "tracing",
"tracing-subscriber", "tracing-subscriber",
"which",
"windows 0.61.1", "windows 0.61.1",
"windows-core 0.61.0", "windows-core 0.61.0",
"windows-icons 0.1.0 (git+https://github.com/LGUG2Z/windows-icons?rev=0c9d7ee1b807347c507d3a9862dd007b4d3f4354)", "windows-icons 0.1.0 (git+https://github.com/LGUG2Z/windows-icons?rev=0c9d7ee1b807347c507d3a9862dd007b4d3f4354)",

View File

@@ -35,6 +35,7 @@ starship-battery = "0.10"
sysinfo = { workspace = true } sysinfo = { workspace = true }
tracing = { workspace = true } tracing = { workspace = true }
tracing-subscriber = { workspace = true } tracing-subscriber = { workspace = true }
which = { workspace = true }
windows = { workspace = true } windows = { workspace = true }
windows-core = { workspace = true } windows-core = { workspace = true }
windows-icons = { git = "https://github.com/LGUG2Z/windows-icons", rev = "0c9d7ee1b807347c507d3a9862dd007b4d3f4354" } windows-icons = { git = "https://github.com/LGUG2Z/windows-icons", rev = "0c9d7ee1b807347c507d3a9862dd007b4d3f4354" }

View File

@@ -19,13 +19,16 @@ use eframe::egui::Ui;
use eframe::egui::Vec2; use eframe::egui::Vec2;
use image::DynamicImage; use image::DynamicImage;
use image::RgbaImage; use image::RgbaImage;
use komorebi_client::PathExt;
use serde::Deserialize; use serde::Deserialize;
use serde::Serialize; use serde::Serialize;
use std::path::Path; use std::path::Path;
use std::path::PathBuf;
use std::process::Command; use std::process::Command;
use std::time::Duration; use std::time::Duration;
use std::time::Instant; use std::time::Instant;
use tracing; use tracing;
use which::which;
/// Minimum interval between consecutive application launches to prevent accidental spamming. /// Minimum interval between consecutive application launches to prevent accidental spamming.
const MIN_LAUNCH_INTERVAL: Duration = Duration::from_millis(800); const MIN_LAUNCH_INTERVAL: Duration = Duration::from_millis(800);
@@ -118,28 +121,41 @@ impl From<&ApplicationsConfig> for Applications {
fn from(applications_config: &ApplicationsConfig) -> Self { fn from(applications_config: &ApplicationsConfig) -> Self {
// Allow immediate launch by initializing last_launch in the past. // Allow immediate launch by initializing last_launch in the past.
let last_launch = Instant::now() - 2 * MIN_LAUNCH_INTERVAL; let last_launch = Instant::now() - 2 * MIN_LAUNCH_INTERVAL;
let mut applications_config = applications_config.clone();
let items = applications_config let items = applications_config
.items .items
.iter() .iter_mut()
.enumerate() .enumerate()
.map(|(index, app_config)| App { .map(|(index, app_config)| {
enable: app_config.enable.unwrap_or(applications_config.enable), app_config.command = app_config
name: app_config .command
.name .replace_env()
.is_empty() .to_string_lossy()
.then(|| format!("App {}", index + 1)) .to_string();
.unwrap_or_else(|| app_config.name.clone()),
icon: Icon::try_from(app_config), if let Some(icon) = &mut app_config.icon {
command: app_config.command.clone(), *icon = icon.replace_env().to_string_lossy().to_string();
display: app_config }
.display
.or(applications_config.display) App {
.unwrap_or_default(), enable: app_config.enable.unwrap_or(applications_config.enable),
show_command_on_hover: app_config name: app_config
.show_command_on_hover .name
.or(applications_config.show_command_on_hover) .is_empty()
.unwrap_or(false), .then(|| format!("App {}", index + 1))
last_launch, .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(); .collect();
@@ -258,7 +274,7 @@ impl Icon {
pub fn try_from(config: &AppConfig) -> Option<Self> { pub fn try_from(config: &AppConfig) -> Option<Self> {
if let Some(icon) = config.icon.as_deref().map(str::trim) { if let Some(icon) = config.icon.as_deref().map(str::trim) {
if !icon.is_empty() { if !icon.is_empty() {
let path = Path::new(icon); let path = Path::new(&icon);
if path.is_file() { if path.is_file() {
match image::open(path).as_ref().map(DynamicImage::to_rgba8) { match image::open(path).as_ref().map(DynamicImage::to_rgba8) {
Ok(image) => return Some(Icon::Image(image)), Ok(image) => return Some(Icon::Image(image)),
@@ -272,12 +288,19 @@ impl Icon {
} }
} }
if Path::new(&config.command).is_file() { let binary = PathBuf::from(config.command.split(".exe").next()?);
return windows_icons::get_icon_by_path(&config.command) let path = if binary.is_file() {
.or_else(|| windows_icons_fallback::get_icon_by_path(&config.command)) Some(binary)
.map(Icon::Image); } 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. /// Renders the icon in the given `Ui` context with the specified size.