From b2ab893e77d343dba331dc7a1fb4252626284edf Mon Sep 17 00:00:00 2001 From: LGUG2Z Date: Sun, 15 Aug 2021 18:34:27 -0700 Subject: [PATCH] feat(wm): add cmd to identify 'close to tray' apps Issue #6 highlighted a workflow that I don't personally use, but I am sure is common among other Windows users, which is to use the Close button to minimize an application to the tray. Since this is largely a configurable option in those applications (Discord etc.), I have implemented a command for the user to identify those applications themselves when configuring the window manager, instead of adding them to the previous Vec of known multi-window applications that need to be identified by default. Close/minimize to tray applications can be identified either by their class or their executable name. I figure it is pretty important to know the rules defined on the window manager instance, so I have exposed these on a new window_manager::State struct which is now what get returns from the 'komorebic.exe state' command. resolve #6 --- README.md | 13 ++++++++++- bindings/build.rs | 30 +++++++++++-------------- komorebi-core/src/lib.rs | 8 +++++++ komorebi.sample.ahk | 4 ++++ komorebi/src/main.rs | 4 +++- komorebi/src/process_command.rs | 20 ++++++++++++++++- komorebi/src/process_event.rs | 19 +++++++++++----- komorebi/src/window_manager.rs | 39 +++++++++++++++++++++++++++++---- komorebic/src/main.rs | 14 ++++++++++++ 9 files changed, 122 insertions(+), 29 deletions(-) diff --git a/README.md b/README.md index 349087cd..fff04d36 100644 --- a/README.md +++ b/README.md @@ -87,6 +87,16 @@ You can similarly stop the process by running `komorebic stop`. If you have AutoHotKey installed and a `komorebi.ahk` file in your home directory (run `$Env:UserProfile` at a PowerShell prompt to find your home directory), `komorebi` will automatically try to load it when starting. +If you are experiencing behaviour where +[closing a window leaves a blank tile, but minimizing the same window does not](https://github.com/LGUG2Z/komorebi/issues/6), +you have probably enabled a 'close/minimize to tray' option for that application. You can tell _komorebi_ to handle this +application appropriately by identifying it via the executable name or the window class: + +```powershell +komorebic.exe identify-tray-application exe Discord.exe +komorebic.exe identify-tray-application exe Telegram.exe +``` + ## Configuration As previously mentioned, this project does not handle anything related to keybindings and shortcuts directly. I @@ -115,6 +125,7 @@ sample [komorebi.ahk](komorebi.sample.ahk) AHK script that you can use as a star - [x] Floating rules based on exe name - [x] Floating rules based on window title - [x] Floating rules based on window class +- [x] Identify 'close/minimize to tray' applications - [x] Toggle floating windows - [x] Toggle monocle window - [x] Toggle focus follows mouse @@ -130,7 +141,7 @@ sample [komorebi.ahk](komorebi.sample.ahk) AHK script that you can use as a star If you would like to contribute code to this repository, there are a few requests that I have to ensure a foundation of code quality, consistency and commit hygiene: -- Flatten all `use` statements except in `bindings/build.rs` +- Flatten all `use` statements - Run `cargo +nightly clippy` and ensure that all lints and suggestions have been addressed before committing - Run `cargo +nightly fmt --all` to ensure consistent formatting before committing - Use `git cz` with diff --git a/bindings/build.rs b/bindings/build.rs index ef542e1b..80b65a0b 100644 --- a/bindings/build.rs +++ b/bindings/build.rs @@ -1,26 +1,22 @@ fn main() { windows::build!( - Windows::Win32::Foundation::{ - POINT, - RECT, - BOOL, - PWSTR, - HWND, - LPARAM, - }, + Windows::Win32::Foundation::RECT, + Windows::Win32::Foundation::POINT, + Windows::Win32::Foundation::BOOL, + Windows::Win32::Foundation::PWSTR, + Windows::Win32::Foundation::HWND, + Windows::Win32::Foundation::LPARAM, // error: `Windows.Win32.Graphics.Dwm.DWMWA_CLOAKED` not found in metadata Windows::Win32::Graphics::Dwm::*, // error: `Windows.Win32.Graphics.Gdi.MONITOR_DEFAULTTONEAREST` not found in metadata Windows::Win32::Graphics::Gdi::*, - Windows::Win32::System::Threading::{ - PROCESS_ACCESS_RIGHTS, - PROCESS_NAME_FORMAT, - OpenProcess, - QueryFullProcessImageNameW, - GetCurrentThreadId, - AttachThreadInput, - GetCurrentProcessId - }, + Windows::Win32::System::Threading::PROCESS_ACCESS_RIGHTS, + Windows::Win32::System::Threading::PROCESS_NAME_FORMAT, + Windows::Win32::System::Threading::OpenProcess, + Windows::Win32::System::Threading::QueryFullProcessImageNameW, + Windows::Win32::System::Threading::GetCurrentThreadId, + Windows::Win32::System::Threading::AttachThreadInput, + Windows::Win32::System::Threading::GetCurrentProcessId, Windows::Win32::UI::KeyboardAndMouseInput::SetFocus, Windows::Win32::UI::Accessibility::{SetWinEventHook, HWINEVENTHOOK}, // error: `Windows.Win32.UI.WindowsAndMessaging.GWL_EXSTYLE` not found in metadata diff --git a/komorebi-core/src/lib.rs b/komorebi-core/src/lib.rs index 411e4e1c..27621c6e 100644 --- a/komorebi-core/src/lib.rs +++ b/komorebi-core/src/lib.rs @@ -57,6 +57,7 @@ pub enum SocketMessage { FloatClass(String), FloatExe(String), FloatTitle(String), + IdentifyTrayApplication(ApplicationIdentifier, String), State, FocusFollowsMouse(bool), } @@ -79,6 +80,13 @@ impl FromStr for SocketMessage { } } +#[derive(Clone, Debug, Serialize, Deserialize, Display, EnumString)] +#[strum(serialize_all = "snake_case")] +pub enum ApplicationIdentifier { + Exe, + Class, +} + #[derive(Clone, Copy, Debug, Serialize, Deserialize, Display, EnumString)] #[strum(serialize_all = "snake_case")] #[derive(Clap)] diff --git a/komorebi.sample.ahk b/komorebi.sample.ahk index a1393ed4..9f4918b7 100644 --- a/komorebi.sample.ahk +++ b/komorebi.sample.ahk @@ -42,6 +42,10 @@ Run, komorebic.exe float-exe wincompose.exe, , Hide Run, komorebic.exe float-title Calculator, , Hide Run, komorebic.exe float-exe 1Password.exe, , Hide +; Identify applications that close to the tray +Run, komorebic.exe identify-tray-application exe Discord.exe, , Hide +Run, komorebic.exe identify-tray-application exe Telegram.exe, , Hide + ; Change the focused window, Alt + Vim direction keys !h:: Run, komorebic.exe focus left, , Hide diff --git a/komorebi/src/main.rs b/komorebi/src/main.rs index ab39eadf..e1c32c31 100644 --- a/komorebi/src/main.rs +++ b/komorebi/src/main.rs @@ -46,7 +46,9 @@ lazy_static! { static ref HIDDEN_HWNDS: Arc>> = Arc::new(Mutex::new(vec![])); static ref LAYERED_EXE_WHITELIST: Arc>> = Arc::new(Mutex::new(vec!["steam.exe".to_string()])); - static ref MULTI_WINDOW_EXES: Arc>> = Arc::new(Mutex::new(vec![ + static ref TRAY_AND_MULTI_WINDOW_CLASSES: Arc>> = + Arc::new(Mutex::new(vec![])); + static ref TRAY_AND_MULTI_WINDOW_EXES: Arc>> = Arc::new(Mutex::new(vec![ "explorer.exe".to_string(), "firefox.exe".to_string(), "chrome.exe".to_string(), diff --git a/komorebi/src/process_command.rs b/komorebi/src/process_command.rs index 2a8d1944..4ce3d65f 100644 --- a/komorebi/src/process_command.rs +++ b/komorebi/src/process_command.rs @@ -10,13 +10,17 @@ use color_eyre::eyre::ContextCompat; use color_eyre::Result; use uds_windows::UnixStream; +use komorebi_core::ApplicationIdentifier; use komorebi_core::SocketMessage; +use crate::window_manager; use crate::window_manager::WindowManager; use crate::windows_api::WindowsApi; use crate::FLOAT_CLASSES; use crate::FLOAT_EXES; use crate::FLOAT_TITLES; +use crate::TRAY_AND_MULTI_WINDOW_CLASSES; +use crate::TRAY_AND_MULTI_WINDOW_EXES; #[tracing::instrument] pub fn listen_for_commands(wm: Arc>) { @@ -152,7 +156,7 @@ impl WindowManager { self.set_workspace_name(monitor_idx, workspace_idx, name)?; } SocketMessage::State => { - let state = serde_json::to_string_pretty(self)?; + let state = serde_json::to_string_pretty(&window_manager::State::from(self))?; let mut socket = dirs::home_dir().context("there is no home directory")?; socket.push("komorebic.sock"); let socket = socket.as_path(); @@ -176,6 +180,20 @@ impl WindowManager { SocketMessage::WatchConfiguration(enable) => { self.watch_configuration(enable)?; } + SocketMessage::IdentifyTrayApplication(identifier, id) => match identifier { + ApplicationIdentifier::Exe => { + let mut exes = TRAY_AND_MULTI_WINDOW_EXES.lock().unwrap(); + if !exes.contains(&id) { + exes.push(id); + } + } + ApplicationIdentifier::Class => { + let mut classes = TRAY_AND_MULTI_WINDOW_CLASSES.lock().unwrap(); + if !classes.contains(&id) { + classes.push(id); + } + } + }, } tracing::info!("processed"); diff --git a/komorebi/src/process_event.rs b/komorebi/src/process_event.rs index cc5769cb..08a1be92 100644 --- a/komorebi/src/process_event.rs +++ b/komorebi/src/process_event.rs @@ -15,7 +15,8 @@ use crate::window_manager::WindowManager; use crate::window_manager_event::WindowManagerEvent; use crate::windows_api::WindowsApi; use crate::HIDDEN_HWNDS; -use crate::MULTI_WINDOW_EXES; +use crate::TRAY_AND_MULTI_WINDOW_CLASSES; +use crate::TRAY_AND_MULTI_WINDOW_EXES; #[tracing::instrument] pub fn listen_for_events(wm: Arc>) { @@ -91,11 +92,19 @@ impl WindowManager { WindowManagerEvent::Hide(_, window) => { // Some major applications unfortunately send the HIDE signal when they are being - // minimized or destroyed. Will have to keep updating this list. - let common_multi_window_exes = MULTI_WINDOW_EXES.lock().unwrap(); + // minimized or destroyed. Applications that close to the tray also do the same, + // and will have is_window() return true, as the process is still running even if + // the window is not visible. + let tray_and_multi_window_exes = TRAY_AND_MULTI_WINDOW_EXES.lock().unwrap(); + let tray_and_multi_window_classes = TRAY_AND_MULTI_WINDOW_CLASSES.lock().unwrap(); + + // We don't want to purge windows that have been deliberately hidden by us, eg. when + // they are not on the top of a container stack. let programmatically_hidden_hwnds = HIDDEN_HWNDS.lock().unwrap(); - if (!window.is_window() || common_multi_window_exes.contains(&window.exe()?)) - && !programmatically_hidden_hwnds.contains(&window.hwnd) + + if (!window.is_window() || tray_and_multi_window_exes.contains(&window.exe()?)) + || tray_and_multi_window_classes.contains(&window.class()?) + && !programmatically_hidden_hwnds.contains(&window.hwnd) { self.focused_workspace_mut()?.remove_window(window.hwnd)?; self.update_focused_workspace(false)?; diff --git a/komorebi/src/window_manager.rs b/komorebi/src/window_manager.rs index 1c9f3414..eaa9f4dc 100644 --- a/komorebi/src/window_manager.rs +++ b/komorebi/src/window_manager.rs @@ -28,19 +28,50 @@ use crate::window::Window; use crate::window_manager_event::WindowManagerEvent; use crate::windows_api::WindowsApi; use crate::workspace::Workspace; +use crate::FLOAT_CLASSES; +use crate::FLOAT_EXES; +use crate::FLOAT_TITLES; +use crate::LAYERED_EXE_WHITELIST; +use crate::TRAY_AND_MULTI_WINDOW_CLASSES; +use crate::TRAY_AND_MULTI_WINDOW_EXES; -#[derive(Debug, Serialize)] +#[derive(Debug)] pub struct WindowManager { pub monitors: Ring, - #[serde(skip_serializing)] pub incoming_events: Arc>>, - #[serde(skip_serializing)] pub command_listener: UnixListener, pub is_paused: bool, - #[serde(skip_serializing)] pub hotwatch: Hotwatch, } +#[derive(Debug, Serialize)] +pub struct State { + pub monitors: Ring, + pub is_paused: bool, + pub float_classes: Vec, + pub float_exes: Vec, + pub float_titles: Vec, + pub layered_exe_whitelist: Vec, + pub tray_and_multi_window_exes: Vec, + pub tray_and_multi_window_classes: Vec, +} + +#[allow(clippy::fallible_impl_from)] +impl From<&mut WindowManager> for State { + fn from(wm: &mut WindowManager) -> Self { + Self { + monitors: wm.monitors.clone(), + is_paused: wm.is_paused, + float_classes: FLOAT_CLASSES.lock().unwrap().clone(), + float_exes: FLOAT_EXES.lock().unwrap().clone(), + float_titles: FLOAT_TITLES.lock().unwrap().clone(), + layered_exe_whitelist: LAYERED_EXE_WHITELIST.lock().unwrap().clone(), + tray_and_multi_window_exes: TRAY_AND_MULTI_WINDOW_EXES.lock().unwrap().clone(), + tray_and_multi_window_classes: TRAY_AND_MULTI_WINDOW_CLASSES.lock().unwrap().clone(), + } + } +} + impl_ring_elements!(WindowManager, Monitor); #[tracing::instrument] diff --git a/komorebic/src/main.rs b/komorebic/src/main.rs index 3c2b2a8f..65ce1c9e 100644 --- a/komorebic/src/main.rs +++ b/komorebic/src/main.rs @@ -14,6 +14,7 @@ use bindings::Windows::Win32::Foundation::HWND; use bindings::Windows::Win32::UI::WindowsAndMessaging::ShowWindow; use bindings::Windows::Win32::UI::WindowsAndMessaging::SHOW_WINDOW_CMD; use bindings::Windows::Win32::UI::WindowsAndMessaging::SW_RESTORE; +use komorebi_core::ApplicationIdentifier; use komorebi_core::CycleDirection; use komorebi_core::Layout; use komorebi_core::LayoutFlip; @@ -62,6 +63,7 @@ enum SubCommand { FloatClass(FloatTarget), FloatExe(FloatTarget), FloatTitle(FloatTarget), + IdentifyTrayApplication(ApplicationTarget), AdjustContainerPadding(SizingAdjustment), AdjustWorkspacePadding(SizingAdjustment), FlipLayout(LayoutFlip), @@ -130,6 +132,12 @@ struct FloatTarget { id: String, } +#[derive(Clap)] +struct ApplicationTarget { + identifier: ApplicationIdentifier, + id: String, +} + #[derive(Clap)] struct Resize { edge: OperationDirection, @@ -354,6 +362,12 @@ fn main() -> Result<()> { }; send_message(&*SocketMessage::WatchConfiguration(enable).as_bytes()?)?; } + SubCommand::IdentifyTrayApplication(target) => { + send_message( + &*SocketMessage::IdentifyTrayApplication(target.identifier, target.id) + .as_bytes()?, + )?; + } } Ok(())