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
This commit is contained in:
LGUG2Z
2021-08-15 18:34:27 -07:00
parent b6ff862705
commit b2ab893e77
9 changed files with 122 additions and 29 deletions

View File

@@ -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 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. 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 ## Configuration
As previously mentioned, this project does not handle anything related to keybindings and shortcuts directly. I 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 exe name
- [x] Floating rules based on window title - [x] Floating rules based on window title
- [x] Floating rules based on window class - [x] Floating rules based on window class
- [x] Identify 'close/minimize to tray' applications
- [x] Toggle floating windows - [x] Toggle floating windows
- [x] Toggle monocle window - [x] Toggle monocle window
- [x] Toggle focus follows mouse - [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 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: 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 clippy` and ensure that all lints and suggestions have been addressed before committing
- Run `cargo +nightly fmt --all` to ensure consistent formatting before committing - Run `cargo +nightly fmt --all` to ensure consistent formatting before committing
- Use `git cz` with - Use `git cz` with

View File

@@ -1,26 +1,22 @@
fn main() { fn main() {
windows::build!( windows::build!(
Windows::Win32::Foundation::{ Windows::Win32::Foundation::RECT,
POINT, Windows::Win32::Foundation::POINT,
RECT, Windows::Win32::Foundation::BOOL,
BOOL, Windows::Win32::Foundation::PWSTR,
PWSTR, Windows::Win32::Foundation::HWND,
HWND, Windows::Win32::Foundation::LPARAM,
LPARAM,
},
// error: `Windows.Win32.Graphics.Dwm.DWMWA_CLOAKED` not found in metadata // error: `Windows.Win32.Graphics.Dwm.DWMWA_CLOAKED` not found in metadata
Windows::Win32::Graphics::Dwm::*, Windows::Win32::Graphics::Dwm::*,
// error: `Windows.Win32.Graphics.Gdi.MONITOR_DEFAULTTONEAREST` not found in metadata // error: `Windows.Win32.Graphics.Gdi.MONITOR_DEFAULTTONEAREST` not found in metadata
Windows::Win32::Graphics::Gdi::*, Windows::Win32::Graphics::Gdi::*,
Windows::Win32::System::Threading::{ Windows::Win32::System::Threading::PROCESS_ACCESS_RIGHTS,
PROCESS_ACCESS_RIGHTS, Windows::Win32::System::Threading::PROCESS_NAME_FORMAT,
PROCESS_NAME_FORMAT, Windows::Win32::System::Threading::OpenProcess,
OpenProcess, Windows::Win32::System::Threading::QueryFullProcessImageNameW,
QueryFullProcessImageNameW, Windows::Win32::System::Threading::GetCurrentThreadId,
GetCurrentThreadId, Windows::Win32::System::Threading::AttachThreadInput,
AttachThreadInput, Windows::Win32::System::Threading::GetCurrentProcessId,
GetCurrentProcessId
},
Windows::Win32::UI::KeyboardAndMouseInput::SetFocus, Windows::Win32::UI::KeyboardAndMouseInput::SetFocus,
Windows::Win32::UI::Accessibility::{SetWinEventHook, HWINEVENTHOOK}, Windows::Win32::UI::Accessibility::{SetWinEventHook, HWINEVENTHOOK},
// error: `Windows.Win32.UI.WindowsAndMessaging.GWL_EXSTYLE` not found in metadata // error: `Windows.Win32.UI.WindowsAndMessaging.GWL_EXSTYLE` not found in metadata

View File

@@ -57,6 +57,7 @@ pub enum SocketMessage {
FloatClass(String), FloatClass(String),
FloatExe(String), FloatExe(String),
FloatTitle(String), FloatTitle(String),
IdentifyTrayApplication(ApplicationIdentifier, String),
State, State,
FocusFollowsMouse(bool), 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)] #[derive(Clone, Copy, Debug, Serialize, Deserialize, Display, EnumString)]
#[strum(serialize_all = "snake_case")] #[strum(serialize_all = "snake_case")]
#[derive(Clap)] #[derive(Clap)]

View File

@@ -42,6 +42,10 @@ Run, komorebic.exe float-exe wincompose.exe, , Hide
Run, komorebic.exe float-title Calculator, , Hide Run, komorebic.exe float-title Calculator, , Hide
Run, komorebic.exe float-exe 1Password.exe, , 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 ; Change the focused window, Alt + Vim direction keys
!h:: !h::
Run, komorebic.exe focus left, , Hide Run, komorebic.exe focus left, , Hide

View File

@@ -46,7 +46,9 @@ lazy_static! {
static ref HIDDEN_HWNDS: Arc<Mutex<Vec<isize>>> = Arc::new(Mutex::new(vec![])); static ref HIDDEN_HWNDS: Arc<Mutex<Vec<isize>>> = Arc::new(Mutex::new(vec![]));
static ref LAYERED_EXE_WHITELIST: Arc<Mutex<Vec<String>>> = static ref LAYERED_EXE_WHITELIST: Arc<Mutex<Vec<String>>> =
Arc::new(Mutex::new(vec!["steam.exe".to_string()])); Arc::new(Mutex::new(vec!["steam.exe".to_string()]));
static ref MULTI_WINDOW_EXES: Arc<Mutex<Vec<String>>> = Arc::new(Mutex::new(vec![ static ref TRAY_AND_MULTI_WINDOW_CLASSES: Arc<Mutex<Vec<String>>> =
Arc::new(Mutex::new(vec![]));
static ref TRAY_AND_MULTI_WINDOW_EXES: Arc<Mutex<Vec<String>>> = Arc::new(Mutex::new(vec![
"explorer.exe".to_string(), "explorer.exe".to_string(),
"firefox.exe".to_string(), "firefox.exe".to_string(),
"chrome.exe".to_string(), "chrome.exe".to_string(),

View File

@@ -10,13 +10,17 @@ use color_eyre::eyre::ContextCompat;
use color_eyre::Result; use color_eyre::Result;
use uds_windows::UnixStream; use uds_windows::UnixStream;
use komorebi_core::ApplicationIdentifier;
use komorebi_core::SocketMessage; use komorebi_core::SocketMessage;
use crate::window_manager;
use crate::window_manager::WindowManager; use crate::window_manager::WindowManager;
use crate::windows_api::WindowsApi; use crate::windows_api::WindowsApi;
use crate::FLOAT_CLASSES; use crate::FLOAT_CLASSES;
use crate::FLOAT_EXES; use crate::FLOAT_EXES;
use crate::FLOAT_TITLES; use crate::FLOAT_TITLES;
use crate::TRAY_AND_MULTI_WINDOW_CLASSES;
use crate::TRAY_AND_MULTI_WINDOW_EXES;
#[tracing::instrument] #[tracing::instrument]
pub fn listen_for_commands(wm: Arc<Mutex<WindowManager>>) { pub fn listen_for_commands(wm: Arc<Mutex<WindowManager>>) {
@@ -152,7 +156,7 @@ impl WindowManager {
self.set_workspace_name(monitor_idx, workspace_idx, name)?; self.set_workspace_name(monitor_idx, workspace_idx, name)?;
} }
SocketMessage::State => { 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")?; let mut socket = dirs::home_dir().context("there is no home directory")?;
socket.push("komorebic.sock"); socket.push("komorebic.sock");
let socket = socket.as_path(); let socket = socket.as_path();
@@ -176,6 +180,20 @@ impl WindowManager {
SocketMessage::WatchConfiguration(enable) => { SocketMessage::WatchConfiguration(enable) => {
self.watch_configuration(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"); tracing::info!("processed");

View File

@@ -15,7 +15,8 @@ use crate::window_manager::WindowManager;
use crate::window_manager_event::WindowManagerEvent; use crate::window_manager_event::WindowManagerEvent;
use crate::windows_api::WindowsApi; use crate::windows_api::WindowsApi;
use crate::HIDDEN_HWNDS; 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] #[tracing::instrument]
pub fn listen_for_events(wm: Arc<Mutex<WindowManager>>) { pub fn listen_for_events(wm: Arc<Mutex<WindowManager>>) {
@@ -91,11 +92,19 @@ impl WindowManager {
WindowManagerEvent::Hide(_, window) => { WindowManagerEvent::Hide(_, window) => {
// Some major applications unfortunately send the HIDE signal when they are being // Some major applications unfortunately send the HIDE signal when they are being
// minimized or destroyed. Will have to keep updating this list. // minimized or destroyed. Applications that close to the tray also do the same,
let common_multi_window_exes = MULTI_WINDOW_EXES.lock().unwrap(); // 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(); 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.focused_workspace_mut()?.remove_window(window.hwnd)?;
self.update_focused_workspace(false)?; self.update_focused_workspace(false)?;

View File

@@ -28,19 +28,50 @@ use crate::window::Window;
use crate::window_manager_event::WindowManagerEvent; use crate::window_manager_event::WindowManagerEvent;
use crate::windows_api::WindowsApi; use crate::windows_api::WindowsApi;
use crate::workspace::Workspace; 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 struct WindowManager {
pub monitors: Ring<Monitor>, pub monitors: Ring<Monitor>,
#[serde(skip_serializing)]
pub incoming_events: Arc<Mutex<Receiver<WindowManagerEvent>>>, pub incoming_events: Arc<Mutex<Receiver<WindowManagerEvent>>>,
#[serde(skip_serializing)]
pub command_listener: UnixListener, pub command_listener: UnixListener,
pub is_paused: bool, pub is_paused: bool,
#[serde(skip_serializing)]
pub hotwatch: Hotwatch, pub hotwatch: Hotwatch,
} }
#[derive(Debug, Serialize)]
pub struct State {
pub monitors: Ring<Monitor>,
pub is_paused: bool,
pub float_classes: Vec<String>,
pub float_exes: Vec<String>,
pub float_titles: Vec<String>,
pub layered_exe_whitelist: Vec<String>,
pub tray_and_multi_window_exes: Vec<String>,
pub tray_and_multi_window_classes: Vec<String>,
}
#[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); impl_ring_elements!(WindowManager, Monitor);
#[tracing::instrument] #[tracing::instrument]

View File

@@ -14,6 +14,7 @@ use bindings::Windows::Win32::Foundation::HWND;
use bindings::Windows::Win32::UI::WindowsAndMessaging::ShowWindow; use bindings::Windows::Win32::UI::WindowsAndMessaging::ShowWindow;
use bindings::Windows::Win32::UI::WindowsAndMessaging::SHOW_WINDOW_CMD; use bindings::Windows::Win32::UI::WindowsAndMessaging::SHOW_WINDOW_CMD;
use bindings::Windows::Win32::UI::WindowsAndMessaging::SW_RESTORE; use bindings::Windows::Win32::UI::WindowsAndMessaging::SW_RESTORE;
use komorebi_core::ApplicationIdentifier;
use komorebi_core::CycleDirection; use komorebi_core::CycleDirection;
use komorebi_core::Layout; use komorebi_core::Layout;
use komorebi_core::LayoutFlip; use komorebi_core::LayoutFlip;
@@ -62,6 +63,7 @@ enum SubCommand {
FloatClass(FloatTarget), FloatClass(FloatTarget),
FloatExe(FloatTarget), FloatExe(FloatTarget),
FloatTitle(FloatTarget), FloatTitle(FloatTarget),
IdentifyTrayApplication(ApplicationTarget),
AdjustContainerPadding(SizingAdjustment), AdjustContainerPadding(SizingAdjustment),
AdjustWorkspacePadding(SizingAdjustment), AdjustWorkspacePadding(SizingAdjustment),
FlipLayout(LayoutFlip), FlipLayout(LayoutFlip),
@@ -130,6 +132,12 @@ struct FloatTarget {
id: String, id: String,
} }
#[derive(Clap)]
struct ApplicationTarget {
identifier: ApplicationIdentifier,
id: String,
}
#[derive(Clap)] #[derive(Clap)]
struct Resize { struct Resize {
edge: OperationDirection, edge: OperationDirection,
@@ -354,6 +362,12 @@ fn main() -> Result<()> {
}; };
send_message(&*SocketMessage::WatchConfiguration(enable).as_bytes()?)?; send_message(&*SocketMessage::WatchConfiguration(enable).as_bytes()?)?;
} }
SubCommand::IdentifyTrayApplication(target) => {
send_message(
&*SocketMessage::IdentifyTrayApplication(target.identifier, target.id)
.as_bytes()?,
)?;
}
} }
Ok(()) Ok(())