Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: support override app icons #145

Merged
merged 3 commits into from
Oct 25, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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 =