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

View File

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

View File

@@ -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<Self> {
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.