From ec5dabd1e0d8e1a8d2829f504f3178ed36aa5554 Mon Sep 17 00:00:00 2001 From: sigoden Date: Fri, 25 Oct 2024 21:42:05 +0800 Subject: [PATCH] feat: support override app icons (#145) --- src/app.rs | 13 +++++-- src/config.rs | 28 +++++++++++++-- src/utils/app_icon.rs | 81 ++++++++++++++++++++++++++++--------------- window-switcher.ini | 7 +++- 4 files changed, 96 insertions(+), 33 deletions(-) diff --git a/src/app.rs b/src/app.rs index d003100..03a4d18 100644 --- a/src/app.rs +++ b/src/app.rs @@ -400,8 +400,17 @@ impl App { } else { hwnds[0].0 }; - let module_hicon = get_app_icon(&mut self.cached_icons, module_path, module_hwnd); - apps.push((module_hicon, module_hwnd)); + let module_hicon = self + .cached_icons + .entry(module_path.clone()) + .or_insert_with(|| { + get_app_icon( + &self.config.switch_apps_override_icons, + module_path, + module_hwnd, + ) + }); + apps.push((*module_hicon, module_hwnd)); } let num_apps = apps.len() as i32; if num_apps == 0 { diff --git a/src/config.rs b/src/config.rs index 56648c5..7646b68 100644 --- a/src/config.rs +++ b/src/config.rs @@ -1,7 +1,8 @@ use std::{collections::HashSet, fs, path::PathBuf, process::Command}; use anyhow::{anyhow, Result}; -use ini::Ini; +use indexmap::IndexMap; +use ini::{Ini, ParseOption}; use log::LevelFilter; use windows::Win32::UI::Input::KeyboardAndMouse::{ VIRTUAL_KEY, VK_LCONTROL, VK_LMENU, VK_LWIN, VK_RCONTROL, VK_RMENU, VK_RWIN, @@ -25,6 +26,7 @@ pub struct Config { pub switch_apps_enable: bool, pub switch_apps_hotkey: Hotkey, pub switch_apps_ignore_minimal: bool, + pub switch_apps_override_icons: IndexMap, } impl Default for Config { @@ -45,6 +47,7 @@ impl Default for Config { switch_apps_hotkey: Hotkey::create(SWITCH_APPS_HOTKEY_ID, "switch apps", "alt + tab") .unwrap(), switch_apps_ignore_minimal: false, + switch_apps_override_icons: Default::default(), } } } @@ -62,7 +65,7 @@ impl Config { if let Some(level) = section.get("level").and_then(|v| v.parse().ok()) { conf.log_level = level; } - if let Some(path) = section.get("path") { + if let Some(path) = section.get("path").map(normalize_path_value) { if !path.trim().is_empty() { let mut path = PathBuf::from(path); if !path.is_absolute() { @@ -84,6 +87,7 @@ impl Config { if let Some(v) = section .get("blacklist") + .map(normalize_path_value) .map(|v| v.split(',').map(|v| v.trim().to_string()).collect()) { conf.switch_windows_blacklist = v; @@ -105,6 +109,16 @@ impl Config { if let Some(v) = section.get("ignore_minimal").and_then(Config::to_bool) { conf.switch_apps_ignore_minimal = v; } + if let Some(v) = section.get("override_icons").map(normalize_path_value) { + conf.switch_apps_override_icons = v + .split([',', ';']) + .filter_map(|v| { + v.trim() + .split_once("=") + .map(|(k, v)| (k.to_lowercase(), v.to_string())) + }) + .collect(); + } } Ok(conf) } @@ -271,7 +285,11 @@ impl Hotkey { pub fn load_config() -> Result { let filepath = get_config_path()?; - let conf = Ini::load_from_file(&filepath) + let opt = ParseOption { + enabled_escape: false, + ..Default::default() + }; + let conf = Ini::load_from_file_opt(&filepath, opt) .map_err(|err| anyhow!("Failed to load config file '{}', {err}", filepath.display()))?; Config::load(&conf) } @@ -308,6 +326,10 @@ fn get_config_path() -> Result { Ok(config_path) } +fn normalize_path_value(value: &str) -> String { + value.replace("\\\\", "\\") +} + #[cfg(test)] mod tests { use super::*; diff --git a/src/utils/app_icon.rs b/src/utils/app_icon.rs index 59488db..6117dd1 100644 --- a/src/utils/app_icon.rs +++ b/src/utils/app_icon.rs @@ -1,5 +1,6 @@ +use super::to_wstring; + use std::{ - collections::HashMap, fs::File, io::{BufReader, Read}, mem, @@ -7,6 +8,7 @@ use std::{ time, }; +use indexmap::IndexMap; use windows::{ core::PCWSTR, Win32::{ @@ -16,8 +18,9 @@ use windows::{ Controls::IImageList, Shell::{SHGetFileInfoW, SHGetImageList, SHFILEINFOW, SHGFI_SYSICONINDEX}, WindowsAndMessaging::{ - CopyIcon, CreateIconFromResourceEx, LoadIconW, SendMessageW, GCL_HICON, HICON, - ICON_BIG, IDI_APPLICATION, LR_DEFAULTCOLOR, WM_GETICON, + CopyIcon, CreateIconFromResourceEx, LoadIconW, LoadImageW, SendMessageW, GCL_HICON, + HICON, ICON_BIG, IDI_APPLICATION, IMAGE_ICON, LR_DEFAULTCOLOR, LR_DEFAULTSIZE, + LR_LOADFROMFILE, WM_GETICON, }, }, }, @@ -26,26 +29,37 @@ use xml::reader::XmlEvent; use xml::EventReader; pub fn get_app_icon( - cached_icons: &mut HashMap, + override_icons: &IndexMap, module_path: &str, hwnd: HWND, ) -> HICON { - if let Some(icon) = cached_icons.get(module_path) { - return *icon; + let module_path_lc = module_path.to_lowercase(); + if let Some((_, v)) = override_icons + .iter() + .find(|(k, _)| module_path_lc.contains(*k)) + { + let mut override_path = PathBuf::from(v); + if !override_path.is_absolute() { + if let Some(module_dir) = Path::new(module_path).parent() { + override_path = module_dir.join(override_path); + } + } + if let Some(icon) = load_image_as_hicon(override_path) { + return icon; + } } if module_path.starts_with("C:\\Program Files\\WindowsApps") { - let icon = get_appx_logo_path(module_path) - .and_then(|image_path| load_image_as_hicon(&image_path)) - .unwrap_or_else(fallback_icon); - cached_icons.insert(module_path.to_string(), icon); - return icon; + if let Some(icon) = + get_appx_logo_path(module_path).and_then(|image_path| load_image_as_hicon(&image_path)) + { + return icon; + } } - let icon = get_exe_icon(module_path) + + get_exe_icon(module_path) .or_else(|| get_window_icon(hwnd)) - .unwrap_or_else(fallback_icon); - cached_icons.insert(module_path.to_string(), icon); - icon + .unwrap_or_else(fallback_icon) } fn get_appx_logo_path(module_path: &str) -> Option { @@ -104,14 +118,7 @@ fn get_appx_logo_path(module_path: &str) -> Option { let extension = format!(".{}", logo_path.extension()?.to_string_lossy()); let logo_path = logo_path.display().to_string(); let prefix = &logo_path[0..(logo_path.len() - extension.len())]; - for size in [ - "targetsize-256", - "targetsize-128", - "targetsize-72", - "targetsize-36", - "scale-200", - "scale-100", - ] { + for size in ["targetsize-256", "targetsize-128", "scale-200", "scale-100"] { let logo_path = PathBuf::from(format!("{prefix}.{size}{extension}")); if logo_path.exists() { return Some(logo_path); @@ -121,10 +128,30 @@ fn get_appx_logo_path(module_path: &str) -> Option { } pub fn load_image_as_hicon>(image_path: T) -> Option { - let mut logo_file = File::open(image_path.as_ref()).ok()?; - let mut buffer = vec![]; - logo_file.read_to_end(&mut buffer).ok()?; - unsafe { CreateIconFromResourceEx(&buffer, TRUE, 0x30000, 100, 100, LR_DEFAULTCOLOR) }.ok() + let image_path = image_path.as_ref(); + if !image_path.exists() { + return None; + } + if let Some("ico") = image_path.extension().and_then(|v| v.to_str()) { + let icon_path = to_wstring(image_path.to_string_lossy().as_ref()); + unsafe { + LoadImageW( + None, + PCWSTR(icon_path.as_ptr()), + IMAGE_ICON, + 256, + 256, + LR_LOADFROMFILE | LR_DEFAULTSIZE, + ) + } + .ok() + .map(|v| HICON(v.0)) + } else { + let mut logo_file = File::open(image_path).ok()?; + let mut buffer = vec![]; + logo_file.read_to_end(&mut buffer).ok()?; + unsafe { CreateIconFromResourceEx(&buffer, TRUE, 0x30000, 100, 100, LR_DEFAULTCOLOR) }.ok() + } } fn fallback_icon() -> HICON { diff --git a/window-switcher.ini b/window-switcher.ini index 5eafc64..626a57e 100644 --- a/window-switcher.ini +++ b/window-switcher.ini @@ -24,6 +24,11 @@ hotkey = alt+tab # Ignore minimal windows ignore_minimal = no +# List of override icons, syntax: app1.exe=icon1.ico,app2.exe=icon2.png. +# The icon path can be a full path or a relative path to the app's directory. +# The icon format can be ico or png. +override_icons = + [log] # Log level can be one of off,error,warn,info,debug,trace. @@ -32,5 +37,5 @@ level = info # Log file path. # e.g. # window-switcher.log (located in the same directory as window-switcher.exe) -# C:\\bin\\window-switcher.log (full path) +# C:\Users\sigod\AppData\Local\Temp\window-switcher.log (or used the full path) path = \ No newline at end of file