Skip to content

Commit

Permalink
feat: support override app icons (#145)
Browse files Browse the repository at this point in the history
  • Loading branch information
sigoden authored Oct 25, 2024
1 parent 00d4b75 commit ec5dabd
Show file tree
Hide file tree
Showing 4 changed files with 96 additions and 33 deletions.
13 changes: 11 additions & 2 deletions src/app.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down
28 changes: 25 additions & 3 deletions src/config.rs
Original file line number Diff line number Diff line change
@@ -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,
Expand All @@ -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<String, String>,
}

impl Default for Config {
Expand All @@ -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(),
}
}
}
Expand All @@ -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() {
Expand All @@ -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;
Expand All @@ -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)
}
Expand Down Expand Up @@ -271,7 +285,11 @@ impl Hotkey {

pub fn load_config() -> Result<Config> {
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)
}
Expand Down Expand Up @@ -308,6 +326,10 @@ fn get_config_path() -> Result<PathBuf> {
Ok(config_path)
}

fn normalize_path_value(value: &str) -> String {
value.replace("\\\\", "\\")
}

#[cfg(test)]
mod tests {
use super::*;
Expand Down
81 changes: 54 additions & 27 deletions src/utils/app_icon.rs
Original file line number Diff line number Diff line change
@@ -1,12 +1,14 @@
use super::to_wstring;

use std::{
collections::HashMap,
fs::File,
io::{BufReader, Read},
mem,
path::{Path, PathBuf},
time,
};

use indexmap::IndexMap;
use windows::{
core::PCWSTR,
Win32::{
Expand All @@ -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,
},
},
},
Expand All @@ -26,26 +29,37 @@ use xml::reader::XmlEvent;
use xml::EventReader;

pub fn get_app_icon(
cached_icons: &mut HashMap<String, HICON>,
override_icons: &IndexMap<String, String>,
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<PathBuf> {
Expand Down Expand Up @@ -104,14 +118,7 @@ fn get_appx_logo_path(module_path: &str) -> Option<PathBuf> {
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);
Expand All @@ -121,10 +128,30 @@ fn get_appx_logo_path(module_path: &str) -> Option<PathBuf> {
}

pub fn load_image_as_hicon<T: AsRef<Path>>(image_path: T) -> Option<HICON> {
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 {
Expand Down
7 changes: 6 additions & 1 deletion window-switcher.ini
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand All @@ -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 =

0 comments on commit ec5dabd

Please sign in to comment.