Files
komorebi/komorebi/src/window.rs
T
LGUG2Z cab39bedfb refactor(windows): move paddings, aspect ratio, layout defaults and display preferences to settings
Plan step 10, S3. The two default paddings, the aspect ratio of a window
toggled to floating (centre with resize), layout_defaults and
display_index_preferences are no longer statics: their writers
(apply_globals through SharedDelta::apply, the DisplayIndexPreference
command) and all readers use settings() together. The semantics are the
statics': a missing padding or display_index_preferences key keeps the
value, a missing layout_defaults clears it, a missing aspect ratio is
Standard (not the initial Widescreen), and the command inserts one entry.
Readers get an owned copy from one snapshot (accessors in settings.rs).

BEHAVIOUR only in this: apply_globals used to store these keys one at a
time and now publishes the group in one update, so a thread can no longer see
a half-applied reload of them. Value semantics are pinned by
crates/komorebi-settings tests/reload.rs and shared.rs (reload merges, a
few keys reset, a load never touches groups it does not name).

Windows: window.rs, windows_api.rs, workspace.rs, monitor.rs,
monitor_reconciliator, stackbar_manager (the gap reads the container padding
from settings), state.rs, static_config.rs, process_command.rs, lib.rs.
MONITOR_INDEX_PREFERENCES stays a static (Windows only).

Verified: Windows clippy -D warnings --locked --all-targets on
komorebi-windows (cross): clean. e2e not run: config_reload, layouts,
workspaces, stackbars (container padding gap) are needed.
2026-11-05 10:30:05 -08:00

1421 lines
47 KiB
Rust

use crate::AnimationStyle;
use crate::FLOATING_APPLICATIONS;
use crate::HIDDEN_HWNDS;
use crate::HIDING_BEHAVIOUR;
use crate::IGNORE_IDENTIFIERS;
use crate::LAYERED_WHITELIST;
use crate::MANAGE_IDENTIFIERS;
use crate::NO_TITLEBAR;
use crate::PERMAIGNORE_CLASSES;
use crate::REGEX_IDENTIFIERS;
use crate::SLOW_APPLICATION_COMPENSATION_TIME;
use crate::SLOW_APPLICATION_IDENTIFIERS;
use crate::WSL2_UI_PROCESSES;
use crate::animation::ANIMATION_DURATION_GLOBAL;
use crate::animation::ANIMATION_DURATION_PER_ANIMATION;
use crate::animation::ANIMATION_ENABLED_GLOBAL;
use crate::animation::ANIMATION_ENABLED_PER_ANIMATION;
use crate::animation::ANIMATION_MANAGER;
use crate::animation::ANIMATION_STYLE_GLOBAL;
use crate::animation::ANIMATION_STYLE_PER_ANIMATION;
use crate::animation::AnimationEngine;
use crate::animation::RenderDispatcher;
use crate::animation::ghost_dispatcher::GhostDispatcher;
use crate::animation::lerp::Lerp;
use crate::animation::prefix::AnimationPrefix;
use crate::animation::prefix::new_animation_key;
use crate::animation::transition;
use crate::border_manager;
use crate::cloaked;
use crate::com::SetCloak;
use crate::core::ApplicationIdentifier;
use crate::core::HidingBehaviour;
use crate::core::Rect;
use crate::core::config_generation::IdWithIdentifier;
use crate::core::config_generation::MatchingRule;
use crate::core::config_generation::MatchingStrategy;
use crate::live_resize;
use crate::styles::ExtendedWindowStyle;
use crate::styles::WindowStyle;
use crate::transparency_manager;
use crate::window_manager_event::WindowManagerEvent;
use crate::windows_api;
use crate::windows_api::WindowsApi;
use crate::windows_backend::backend;
use color_eyre::eyre;
use crossbeam_utils::atomic::AtomicConsume;
use komorebi_backend::WindowId;
pub use komorebi_config::aspect_ratio::AspectRatio;
pub use komorebi_config::aspect_ratio::PredefinedAspectRatio;
use regex::Regex;
use serde::Deserialize;
use serde::Serialize;
use serde::Serializer;
use serde::ser::SerializeStruct;
use std::collections::HashMap;
use std::convert::TryFrom;
use std::fmt::Display;
use std::fmt::Formatter;
use std::fmt::Write as _;
use std::sync::atomic::AtomicI32;
use std::sync::atomic::Ordering;
use std::thread;
use std::time::Duration;
use windows::Win32::Foundation::HWND;
pub static MINIMUM_WIDTH: AtomicI32 = AtomicI32::new(0);
pub static MINIMUM_HEIGHT: AtomicI32 = AtomicI32::new(0);
#[derive(Debug, Default, Clone, Copy, Deserialize, PartialEq)]
#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
pub struct Window {
pub hwnd: isize,
}
/// What names a [`Window`]: the type the shared container and workspace code compares ids as
pub type PlatformWindowId = <Window as komorebi_core::ContainerWindow>::Id;
/// The handle of the window that has the focus, if any window does
pub fn foreground_window_id() -> Option<PlatformWindowId> {
WindowsApi::foreground_window().ok()
}
/// Where the pointer is, if the system says
pub fn cursor_position() -> Option<(i32, i32)> {
WindowsApi::cursor_pos()
.ok()
.map(|point| (point.x, point.y))
}
impl From<isize> for Window {
fn from(value: isize) -> Self {
Self { hwnd: value }
}
}
impl From<HWND> for Window {
fn from(value: HWND) -> Self {
Self {
hwnd: value.0 as isize,
}
}
}
#[allow(clippy::module_name_repetitions)]
#[derive(Debug, Clone, Serialize)]
#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
pub struct WindowDetails {
pub title: String,
pub exe: String,
pub class: String,
}
impl From<Window> for WindowDetails {
fn from(value: Window) -> Self {
Self {
title: value.title().unwrap_or_default(),
exe: value.exe().unwrap_or_default(),
class: value.class().unwrap_or_default(),
}
}
}
impl Display for Window {
fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
let mut display = format!("(hwnd: {}", self.hwnd);
if let Some(title) = self.title() {
write!(display, ", title: {title}")?;
}
if let Some(exe) = self.exe() {
write!(display, ", exe: {exe}")?;
}
if let Some(class) = self.class() {
write!(display, ", class: {class}")?;
}
write!(display, ")")?;
write!(f, "{display}")
}
}
impl Serialize for Window {
fn serialize<S>(&self, serializer: S) -> eyre::Result<S::Ok, S::Error>
where
S: Serializer,
{
let mut state = serializer.serialize_struct("Window", 5)?;
state.serialize_field("hwnd", &self.hwnd)?;
state.serialize_field(
"title",
&self
.title()
.unwrap_or_else(|| String::from("could not get window title")),
)?;
state.serialize_field(
"exe",
&self
.exe()
.unwrap_or_else(|| String::from("could not get window exe")),
)?;
state.serialize_field(
"class",
&self
.class()
.unwrap_or_else(|| String::from("could not get window class")),
)?;
state.serialize_field(
"rect",
&WindowsApi::window_rect(self.hwnd).unwrap_or_default(),
)?;
state.end()
}
}
struct TransparencyRenderDispatcher {
hwnd: isize,
start_opacity: u8,
target_opacity: u8,
style: AnimationStyle,
is_opaque: bool,
}
impl TransparencyRenderDispatcher {
const PREFIX: AnimationPrefix = AnimationPrefix::Transparency;
pub fn new(
hwnd: isize,
is_opaque: bool,
start_opacity: u8,
target_opacity: u8,
style: AnimationStyle,
) -> Self {
Self {
hwnd,
start_opacity,
target_opacity,
style,
is_opaque,
}
}
}
impl RenderDispatcher for TransparencyRenderDispatcher {
fn get_animation_key(&self) -> String {
new_animation_key(TransparencyRenderDispatcher::PREFIX, self.hwnd.to_string())
}
fn pre_render(&self) -> eyre::Result<()> {
//transparent
if !self.is_opaque {
let window = Window::from(self.hwnd);
let mut ex_style = window.ex_style()?;
ex_style.insert(ExtendedWindowStyle::LAYERED);
window.update_ex_style(&ex_style)?;
}
Ok(())
}
fn render(&self, progress: f64) -> eyre::Result<()> {
WindowsApi::set_transparent(
self.hwnd,
self.start_opacity
.lerp(self.target_opacity, progress, self.style),
)
}
fn post_render(&self) -> eyre::Result<()> {
//opaque
if self.is_opaque {
let window = Window::from(self.hwnd);
let mut ex_style = window.ex_style()?;
ex_style.remove(ExtendedWindowStyle::LAYERED);
window.update_ex_style(&ex_style)?;
}
Ok(())
}
}
impl Window {
pub const fn hwnd(self) -> HWND {
HWND(windows_api::as_ptr!(self.hwnd))
}
pub fn move_to_area(&mut self, current_area: &Rect, target_area: &Rect) -> eyre::Result<()> {
let current_rect = WindowsApi::window_rect(self.hwnd)?;
let x_diff = target_area.left - current_area.left;
let y_diff = target_area.top - current_area.top;
let x_ratio = f32::abs((target_area.right as f32) / (current_area.right as f32));
let y_ratio = f32::abs((target_area.bottom as f32) / (current_area.bottom as f32));
let window_relative_x = current_rect.left - current_area.left;
let window_relative_y = current_rect.top - current_area.top;
let corrected_relative_x = (window_relative_x as f32 * x_ratio) as i32;
let corrected_relative_y = (window_relative_y as f32 * y_ratio) as i32;
let window_x = current_area.left + corrected_relative_x;
let window_y = current_area.top + corrected_relative_y;
let left = x_diff + window_x;
let top = y_diff + window_y;
let corrected_width = (current_rect.right as f32 * x_ratio) as i32;
let corrected_height = (current_rect.bottom as f32 * y_ratio) as i32;
let new_rect = Rect {
left,
top,
right: corrected_width,
bottom: corrected_height,
};
let is_maximized = &new_rect == target_area;
if is_maximized {
windows_api::WindowsApi::unmaximize_window(self.hwnd);
let animation_enabled = ANIMATION_ENABLED_PER_ANIMATION.lock();
let move_enabled = animation_enabled
.get(&GhostDispatcher::PREFIX)
.is_some_and(|v| *v);
drop(animation_enabled);
if move_enabled || ANIMATION_ENABLED_GLOBAL.load(Ordering::SeqCst) {
let anim_count = ANIMATION_MANAGER
.lock()
.count_in_progress(GhostDispatcher::PREFIX);
self.set_position(&new_rect, true)?;
let hwnd = self.hwnd;
// Wait for the animation to finish before maximizing the window again, otherwise
// we would be maximizing the window on the current monitor anyway
thread::spawn(move || {
let mut new_anim_count = ANIMATION_MANAGER
.lock()
.count_in_progress(GhostDispatcher::PREFIX);
let mut max_wait = 2000; // Max waiting time. No one will be using an animation longer than 2s, right? RIGHT??? WHY?
while new_anim_count > anim_count && max_wait > 0 {
thread::sleep(Duration::from_millis(10));
new_anim_count = ANIMATION_MANAGER
.lock()
.count_in_progress(GhostDispatcher::PREFIX);
max_wait -= 1;
}
windows_api::WindowsApi::maximize_window(hwnd);
});
} else {
self.set_position(&new_rect, true)?;
windows_api::WindowsApi::maximize_window(self.hwnd);
}
} else {
self.set_position(&new_rect, true)?;
}
Ok(())
}
pub fn center(&mut self, work_area: &Rect, resize: bool) -> eyre::Result<()> {
let (target_width, target_height) = if resize {
let (aspect_ratio_width, aspect_ratio_height) =
crate::settings::floating_window_aspect_ratio().width_and_height();
let target_height = work_area.bottom / 2;
let target_width = (target_height * aspect_ratio_width) / aspect_ratio_height;
(target_width, target_height)
} else {
let current_rect = WindowsApi::window_rect(self.hwnd)?;
(current_rect.right, current_rect.bottom)
};
let x = work_area.left + ((work_area.right - target_width) / 2);
let y = work_area.top + ((work_area.bottom - target_height) / 2);
self.set_position(
&Rect {
left: x,
top: y,
right: target_width,
bottom: target_height,
},
true,
)
}
pub fn set_position(&self, layout: &Rect, top: bool) -> eyre::Result<()> {
// a window of the workspace that has been switched to slides in from the
// side of the monitor, which is so even if it is where the layout puts it
// already, as it was when it was hidden
if let Some(slid) = transition::slide_in(*self, layout, top) {
return slid;
}
// and it is put where it goes by that animation, which a layout that is
// done while it is on its way is not to cancel
if transition::is_sliding_in(self.hwnd) {
return Ok(());
}
// a window that the user has hold of by an edge is where they put it, until
// they let go
if live_resize::is_dragging(self.hwnd) {
return Ok(());
}
let window_rect = WindowsApi::window_rect(self.hwnd)?;
if window_rect.eq(layout) {
if live_resize::is_stepping() {
tracing::debug!(
"live resize: hwnd {} is where it is to be, {layout:?}",
self.hwnd
);
live_resize::follow(self.hwnd, &window_rect, layout);
}
return Ok(());
}
// the neighbours of a window being dragged follow it as it goes, which is
// not to be seen catching up
if live_resize::is_stepping() {
// what a window that is resized is seen to do is covered: the cover is where the
// window is to be, which is the pointer's, and the window is sent there when
// the drag is over
if live_resize::follow(self.hwnd, &window_rect, layout) {
return Ok(());
}
if live_resize::is_catching_up(self.hwnd, layout) {
tracing::debug!(
"live resize: hwnd {} is at {window_rect:?}, not yet where it was sent, left alone for {layout:?}",
self.hwnd
);
return Ok(());
}
tracing::debug!(
"live resize: hwnd {} sent to {layout:?} from {window_rect:?} ({})",
self.hwnd,
if live_resize::is_pacing() {
"step"
} else {
"end"
}
);
live_resize::sent(self.hwnd, layout);
return WindowsApi::position_window(self.hwnd, layout, top, true);
}
// a command that resizes windows puts them where they go at once: one that is on its
// way somewhere is stopped where it is, first
if live_resize::is_in_place() {
let key = new_animation_key(GhostDispatcher::PREFIX, self.hwnd.to_string());
if ANIMATION_MANAGER.lock().in_progress(&key) {
AnimationEngine::cancel(&key);
}
return WindowsApi::position_window(self.hwnd, layout, top, true);
}
let animation_enabled = ANIMATION_ENABLED_PER_ANIMATION.lock();
let move_enabled = animation_enabled.get(&GhostDispatcher::PREFIX);
if move_enabled.is_some_and(|enabled| *enabled)
|| ANIMATION_ENABLED_GLOBAL.load(Ordering::SeqCst)
{
let duration = Duration::from_millis(
*ANIMATION_DURATION_PER_ANIMATION
.lock()
.get(&GhostDispatcher::PREFIX)
.unwrap_or(&ANIMATION_DURATION_GLOBAL.load(Ordering::SeqCst)),
);
let style = *ANIMATION_STYLE_PER_ANIMATION
.lock()
.get(&GhostDispatcher::PREFIX)
.unwrap_or(&ANIMATION_STYLE_GLOBAL.lock());
let render_dispatcher =
GhostDispatcher::new(self.hwnd, window_rect, *layout, top, style);
AnimationEngine::animate(render_dispatcher, duration)
} else {
WindowsApi::position_window(self.hwnd, layout, top, true)
}
}
pub fn is_maximized(self) -> bool {
WindowsApi::is_zoomed(self.hwnd)
}
pub fn is_miminized(self) -> bool {
WindowsApi::is_iconic(self.hwnd)
}
pub fn is_visible(self) -> bool {
WindowsApi::is_window_visible(self.hwnd)
}
pub fn hide_with_border(self, hide_border: bool) {
// a workspace that is sliding out hides its windows itself, when their
// ghosts are up
if transition::is_exiting(self.hwnd) {
return;
}
let mut programmatically_hidden_hwnds = HIDDEN_HWNDS.lock();
if !programmatically_hidden_hwnds.contains(&self.hwnd) {
programmatically_hidden_hwnds.push(self.hwnd);
}
let hiding_behaviour = HIDING_BEHAVIOUR.lock();
#[allow(deprecated)]
match *hiding_behaviour {
HidingBehaviour::Hide => WindowsApi::hide_window(self.hwnd),
HidingBehaviour::Minimize => WindowsApi::minimize_window(self.hwnd),
HidingBehaviour::Cloak => {
SetCloak(self.hwnd(), 1, 2);
cloaked::record(&programmatically_hidden_hwnds);
}
}
if hide_border {
border_manager::hide_border(self.hwnd);
}
}
pub fn hide(self) {
self.hide_with_border(true);
}
pub fn restore_with_border(self, restore_border: bool) {
// a workspace that is sliding in shows its windows itself, when they have
// got there
if transition::is_entering(self.hwnd) {
return;
}
let mut programmatically_hidden_hwnds = HIDDEN_HWNDS.lock();
if let Some(idx) = programmatically_hidden_hwnds
.iter()
.position(|&hwnd| hwnd == self.hwnd)
{
programmatically_hidden_hwnds.remove(idx);
}
let hiding_behaviour = HIDING_BEHAVIOUR.lock();
#[allow(deprecated)]
match *hiding_behaviour {
HidingBehaviour::Hide | HidingBehaviour::Minimize => {
WindowsApi::restore_window(self.hwnd);
}
HidingBehaviour::Cloak => {
SetCloak(self.hwnd(), 1, 0);
cloaked::record(&programmatically_hidden_hwnds);
}
}
if restore_border {
border_manager::show_border(self.hwnd);
}
}
pub fn restore(self) {
self.restore_with_border(true);
}
pub fn minimize(self) {
let exe = self.exe().unwrap_or_default();
if !exe.contains("komorebi-bar") {
WindowsApi::minimize_window(self.hwnd);
}
}
pub fn close(self) -> eyre::Result<()> {
WindowsApi::close_window(self.hwnd)
}
pub fn maximize_native(self) {
let mut programmatically_hidden_hwnds = HIDDEN_HWNDS.lock();
if let Some(idx) = programmatically_hidden_hwnds
.iter()
.position(|&hwnd| hwnd == self.hwnd)
{
programmatically_hidden_hwnds.remove(idx);
}
WindowsApi::maximize_window(self.hwnd);
}
pub fn unmaximize_native(self) {
let mut programmatically_hidden_hwnds = HIDDEN_HWNDS.lock();
if let Some(idx) = programmatically_hidden_hwnds
.iter()
.position(|&hwnd| hwnd == self.hwnd)
{
programmatically_hidden_hwnds.remove(idx);
}
WindowsApi::unmaximize_window(self.hwnd);
}
// The operations the shared code calls on a window go through the backend, as on KDE
// (`Window::focus`, `maximize` and `unmaximize` there call `backend()` too). The Windows
// primitives they end in are `focus_native`, `maximize_native` and `unmaximize_native`, which
// `WindowsBackend` calls; they stay public for the animation code that is inside the focus.
/// Gives the window the focus (and the pointer, if `mouse_follows_focus`).
pub fn focus(self, mouse_follows_focus: bool) -> eyre::Result<()> {
Ok(backend().focus(WindowId::from_hwnd(self.hwnd), mouse_follows_focus)?)
}
/// Maximizes the window; a window that is gone is left alone.
pub fn maximize(self) {
let _ = backend().maximize(WindowId::from_hwnd(self.hwnd), true);
}
/// Undoes `maximize`; a window that is gone is left alone.
pub fn unmaximize(self) {
let _ = backend().maximize(WindowId::from_hwnd(self.hwnd), false);
}
pub fn focus_native(self, mouse_follows_focus: bool) -> eyre::Result<()> {
// Focusing a window that is hidden shows it, which is not for it to be
// when it is yet to slide in: it is focused when its animation shows it
if transition::is_entering(self.hwnd) {
transition::focus_when_shown(self.hwnd, mouse_follows_focus);
return Ok(());
}
// If the target window is already focused, do nothing.
if let Ok(ihwnd) = WindowsApi::foreground_window()
&& ihwnd == self.hwnd
{
// Center cursor in Window
if mouse_follows_focus {
WindowsApi::center_cursor_in_rect(&WindowsApi::window_rect(self.hwnd)?)?;
}
return Ok(());
}
WindowsApi::raise_and_focus_window(self.hwnd)?;
// Center cursor in Window
if mouse_follows_focus {
WindowsApi::center_cursor_in_rect(&WindowsApi::window_rect(self.hwnd)?)?;
}
Ok(())
}
pub fn is_focused(self) -> bool {
WindowsApi::foreground_window().unwrap_or_default() == self.hwnd
}
pub fn transparent(self) -> eyre::Result<()> {
let animation_enabled = ANIMATION_ENABLED_PER_ANIMATION.lock();
let transparent_enabled = animation_enabled.get(&TransparencyRenderDispatcher::PREFIX);
if transparent_enabled.is_some_and(|enabled| *enabled)
|| ANIMATION_ENABLED_GLOBAL.load(Ordering::SeqCst)
{
let duration = Duration::from_millis(
*ANIMATION_DURATION_PER_ANIMATION
.lock()
.get(&TransparencyRenderDispatcher::PREFIX)
.unwrap_or(&ANIMATION_DURATION_GLOBAL.load(Ordering::SeqCst)),
);
let style = *ANIMATION_STYLE_PER_ANIMATION
.lock()
.get(&TransparencyRenderDispatcher::PREFIX)
.unwrap_or(&ANIMATION_STYLE_GLOBAL.lock());
let render_dispatcher = TransparencyRenderDispatcher::new(
self.hwnd,
false,
WindowsApi::get_transparent(self.hwnd).unwrap_or(255),
transparency_manager::TRANSPARENCY_ALPHA.load_consume(),
style,
);
AnimationEngine::animate(render_dispatcher, duration)
} else {
let mut ex_style = self.ex_style()?;
ex_style.insert(ExtendedWindowStyle::LAYERED);
self.update_ex_style(&ex_style)?;
WindowsApi::set_transparent(
self.hwnd,
transparency_manager::TRANSPARENCY_ALPHA.load_consume(),
)
}
}
pub fn opaque(self) -> eyre::Result<()> {
let animation_enabled = ANIMATION_ENABLED_PER_ANIMATION.lock();
let transparent_enabled = animation_enabled.get(&TransparencyRenderDispatcher::PREFIX);
if transparent_enabled.is_some_and(|enabled| *enabled)
|| ANIMATION_ENABLED_GLOBAL.load(Ordering::SeqCst)
{
let duration = Duration::from_millis(
*ANIMATION_DURATION_PER_ANIMATION
.lock()
.get(&TransparencyRenderDispatcher::PREFIX)
.unwrap_or(&ANIMATION_DURATION_GLOBAL.load(Ordering::SeqCst)),
);
let style = *ANIMATION_STYLE_PER_ANIMATION
.lock()
.get(&TransparencyRenderDispatcher::PREFIX)
.unwrap_or(&ANIMATION_STYLE_GLOBAL.lock());
let render_dispatcher = TransparencyRenderDispatcher::new(
self.hwnd,
true,
WindowsApi::get_transparent(self.hwnd)
.unwrap_or(transparency_manager::TRANSPARENCY_ALPHA.load_consume()),
255,
style,
);
AnimationEngine::animate(render_dispatcher, duration)
} else {
let mut ex_style = self.ex_style()?;
ex_style.remove(ExtendedWindowStyle::LAYERED);
self.update_ex_style(&ex_style)
}
}
pub fn set_accent(self, colour: u32) -> eyre::Result<()> {
WindowsApi::set_window_accent(self.hwnd, Some(colour))
}
pub fn remove_accent(self) -> eyre::Result<()> {
WindowsApi::set_window_accent(self.hwnd, None)
}
#[cfg(target_pointer_width = "64")]
pub fn update_style(self, style: &WindowStyle) -> eyre::Result<()> {
WindowsApi::update_style(self.hwnd, isize::try_from(style.bits())?)
}
#[cfg(target_pointer_width = "32")]
pub fn update_style(self, style: &WindowStyle) -> eyre::Result<()> {
WindowsApi::update_style(self.hwnd, i32::try_from(style.bits())?)
}
#[cfg(target_pointer_width = "64")]
pub fn update_ex_style(self, style: &ExtendedWindowStyle) -> eyre::Result<()> {
WindowsApi::update_ex_style(self.hwnd, isize::try_from(style.bits())?)
}
#[cfg(target_pointer_width = "32")]
pub fn update_ex_style(self, style: &ExtendedWindowStyle) -> eyre::Result<()> {
WindowsApi::update_ex_style(self.hwnd, i32::try_from(style.bits())?)
}
pub fn style(self) -> eyre::Result<WindowStyle> {
let bits = u32::try_from(WindowsApi::gwl_style(self.hwnd)?)?;
Ok(WindowStyle::from_bits_truncate(bits))
}
pub fn ex_style(self) -> eyre::Result<ExtendedWindowStyle> {
let bits = u32::try_from(WindowsApi::gwl_ex_style(self.hwnd)?)?;
Ok(ExtendedWindowStyle::from_bits_truncate(bits))
}
pub fn title(self) -> Option<String> {
WindowsApi::window_text_w(self.hwnd).ok()
}
pub fn path(self) -> Option<String> {
let lookup = || -> eyre::Result<String> {
let (process_id, _) = WindowsApi::window_thread_process_id(self.hwnd);
let handle = WindowsApi::process_handle(process_id)?;
let path = WindowsApi::exe_path(handle);
WindowsApi::close_process(handle)?;
path
};
lookup().ok()
}
pub fn exe(self) -> Option<String> {
let lookup = || -> eyre::Result<String> {
let (process_id, _) = WindowsApi::window_thread_process_id(self.hwnd);
let handle = WindowsApi::process_handle(process_id)?;
let exe = WindowsApi::exe(handle);
WindowsApi::close_process(handle)?;
exe
};
lookup().ok()
}
pub fn process_id(self) -> u32 {
let (process_id, _) = WindowsApi::window_thread_process_id(self.hwnd);
process_id
}
pub fn class(self) -> Option<String> {
WindowsApi::real_window_class_w(self.hwnd).ok()
}
pub fn is_cloaked(self) -> eyre::Result<bool> {
WindowsApi::is_window_cloaked(self.hwnd)
}
pub fn is_window(self) -> bool {
WindowsApi::is_window(self.hwnd)
}
// The operation names of the shared `komorebi_backend::Backend` (docs/dev/seam.md, section 2).
// `set_rect`, `alive`, `window_rect` and `unminimize` only delegate; `focus`, `raise`, `hide`,
// `restore`, `minimize` and `close` above already carry the shared name.
/// Set frame: `set_position` without raising the window to the front.
pub fn set_rect(self, rect: &Rect) -> eyre::Result<()> {
self.set_position(rect, false)
}
/// Alive check: Windows still has the window (`IsWindow`).
pub fn alive(self) -> bool {
self.is_window()
}
/// Frame query: the visible bounds (without the shadow), see `WindowsApi::window_rect`.
pub fn window_rect(self) -> eyre::Result<Rect> {
WindowsApi::window_rect(self.hwnd)
}
/// Windows has no unminimize of its own: `SW_SHOWNOACTIVATE`, as `restore` ends in for the
/// `Hide` and `Minimize` hiding behaviours, without the `HIDDEN_HWNDS` and border bookkeeping.
pub fn unminimize(self) {
WindowsApi::restore_window(self.hwnd);
}
pub fn remove_title_bar(self) -> eyre::Result<()> {
let mut style = self.style()?;
style.remove(WindowStyle::CAPTION);
style.remove(WindowStyle::THICKFRAME);
self.update_style(&style)
}
pub fn add_title_bar(self) -> eyre::Result<()> {
let mut style = self.style()?;
style.insert(WindowStyle::CAPTION);
style.insert(WindowStyle::THICKFRAME);
self.update_style(&style)
}
/// Raise the window to the top of the Z order, but do not activate or focus
/// it. Use raise_and_focus_window to activate and focus a window.
/// It also checks if there is a border attached to this window and if it is
/// it raises it as well.
pub fn raise(self) -> eyre::Result<()> {
WindowsApi::raise_window(self.hwnd)?;
if let Some(border_info) = crate::border_manager::window_border(self.hwnd) {
WindowsApi::raise_window(border_info.border_hwnd)?;
}
Ok(())
}
/// Lower the window to the bottom of the Z order, but do not activate or focus
/// it.
/// It also checks if there is a border attached to this window and if it is
/// it lowers it as well.
pub fn lower(self) -> eyre::Result<()> {
WindowsApi::lower_window(self.hwnd)?;
if let Some(border_info) = crate::border_manager::window_border(self.hwnd) {
WindowsApi::lower_window(border_info.border_hwnd)?;
}
Ok(())
}
#[tracing::instrument(fields(exe, title), skip(debug))]
pub fn should_manage(
self,
event: Option<WindowManagerEvent>,
debug: &mut RuleDebug,
) -> eyre::Result<bool> {
if !self.is_window() {
return Ok(false);
}
debug.is_window = true;
let rect = WindowsApi::window_rect(self.hwnd).unwrap_or_default();
if rect.right < MINIMUM_WIDTH.load(Ordering::SeqCst) {
return Ok(false);
}
debug.has_minimum_width = true;
if rect.bottom < MINIMUM_HEIGHT.load(Ordering::SeqCst) {
return Ok(false);
}
debug.has_minimum_height = true;
if self.title().is_none() {
return Ok(false);
}
debug.has_title = true;
let is_cloaked = self.is_cloaked().unwrap_or_default();
debug.is_cloaked = is_cloaked;
let mut allow_cloaked = false;
if let Some(event) = event
&& matches!(
event,
WindowManagerEvent::Hide(_, _) | WindowManagerEvent::Cloak(_, _)
)
{
allow_cloaked = true;
}
debug.allow_cloaked = allow_cloaked;
match (allow_cloaked, is_cloaked) {
// If allowing cloaked windows, we don't need to check the cloaked status
(true, _) |
// If not allowing cloaked windows, we need to ensure the window is not cloaked
(false, false) => {
if let (Some(title), Some(exe_name), Some(class), Some(path)) = (self.title(), self.exe(), self.class(), self.path()) {
debug.title = Some(title.clone());
debug.exe_name = Some(exe_name.clone());
debug.class = Some(class.clone());
debug.path = Some(path.clone());
// calls for styles can fail quite often for events with windows that aren't really "windows"
// since we have moved up calls of should_manage to the beginning of the process_event handler,
// we should handle failures here gracefully to be able to continue the execution of process_event
if let (Ok(style), Ok(ex_style)) = (&self.style(), &self.ex_style()) {
debug.window_style = Some(*style);
debug.extended_window_style = Some(*ex_style);
let eligible = window_is_eligible(self.hwnd, &title, &exe_name, &class, &path, style, ex_style, event, debug);
debug.should_manage = eligible;
return Ok(eligible);
}
}
}
_ => {}
}
Ok(false)
}
}
#[derive(Debug, Default, Serialize, Deserialize)]
pub struct RuleDebug {
pub should_manage: bool,
pub is_window: bool,
pub has_minimum_width: bool,
pub has_minimum_height: bool,
pub has_title: bool,
pub is_cloaked: bool,
pub allow_cloaked: bool,
pub allow_layered_transparency: bool,
pub window_style: Option<WindowStyle>,
pub extended_window_style: Option<ExtendedWindowStyle>,
pub title: Option<String>,
pub exe_name: Option<String>,
pub class: Option<String>,
pub path: Option<String>,
pub matches_permaignore_class: Option<String>,
pub matches_ignore_identifier: Option<MatchingRule>,
pub matches_managed_override: Option<MatchingRule>,
pub matches_layered_whitelist: Option<MatchingRule>,
pub matches_floating_applications: Option<MatchingRule>,
pub matches_wsl2_gui: Option<String>,
pub matches_no_titlebar: Option<MatchingRule>,
}
#[allow(clippy::too_many_arguments)]
fn window_is_eligible(
hwnd: isize,
title: &String,
exe_name: &String,
class: &String,
path: &str,
style: &WindowStyle,
ex_style: &ExtendedWindowStyle,
event: Option<WindowManagerEvent>,
debug: &mut RuleDebug,
) -> bool {
{
let permaignore_classes = PERMAIGNORE_CLASSES.lock();
if permaignore_classes.contains(class) {
debug.matches_permaignore_class = Some(class.clone());
return false;
}
}
// Every place that holds two of the rule lists takes them in the order documented at the
// statics in lib.rs, so all the lists this check reads are taken up front, in that order.
let floating_identifiers = FLOATING_APPLICATIONS.lock();
let ignore_identifiers = IGNORE_IDENTIFIERS.lock();
let manage_identifiers = MANAGE_IDENTIFIERS.lock();
let layered_whitelist = LAYERED_WHITELIST.lock();
let slow_application_identifiers = SLOW_APPLICATION_IDENTIFIERS.lock();
let titlebars_removed = NO_TITLEBAR.lock();
let regex_identifiers = REGEX_IDENTIFIERS.lock();
let should_ignore = if let Some(rule) = should_act(
title,
exe_name,
class,
path,
&ignore_identifiers,
&regex_identifiers,
) {
debug.matches_ignore_identifier = Some(rule);
true
} else {
false
};
let managed_override = if let Some(rule) = should_act(
title,
exe_name,
class,
path,
&manage_identifiers,
&regex_identifiers,
) {
debug.matches_managed_override = Some(rule);
true
} else {
false
};
if let Some(rule) = should_act(
title,
exe_name,
class,
path,
&floating_identifiers,
&regex_identifiers,
) {
debug.matches_floating_applications = Some(rule);
}
if should_ignore && !managed_override {
return false;
}
let mut allow_layered = if let Some(rule) = should_act(
title,
exe_name,
class,
path,
&layered_whitelist,
&regex_identifiers,
) {
debug.matches_layered_whitelist = Some(rule);
true
} else {
false
};
let known_layered_hwnds = transparency_manager::known_hwnds();
allow_layered = if known_layered_hwnds.contains(&hwnd)
// we always want to process hide events for windows with transparency, even on other
// monitors, because we don't want to be left with ghost tiles
|| matches!(event, Some(WindowManagerEvent::Hide(_, _)))
{
debug.allow_layered_transparency = true;
true
} else {
allow_layered
};
let allow_wsl2_gui = {
let wsl2_ui_processes = WSL2_UI_PROCESSES.lock();
let allow = wsl2_ui_processes.contains(exe_name);
if allow {
debug.matches_wsl2_gui = Some(exe_name.clone())
}
allow
};
let allow_titlebar_removed = if let Some(rule) = should_act(
title,
exe_name,
class,
path,
&titlebars_removed,
&regex_identifiers,
) {
debug.matches_no_titlebar = Some(rule);
true
} else {
false
};
let should_sleep = should_act(
title,
exe_name,
class,
path,
&slow_application_identifiers,
&regex_identifiers,
)
.is_some();
// Nothing below reads a rule list: release them all before sleeping, so a config load or a
// rule command is never kept waiting for the slow-application compensation time.
drop(floating_identifiers);
drop(ignore_identifiers);
drop(manage_identifiers);
drop(layered_whitelist);
drop(slow_application_identifiers);
drop(titlebars_removed);
drop(regex_identifiers);
if should_sleep {
std::thread::sleep(Duration::from_millis(
SLOW_APPLICATION_COMPENSATION_TIME.load(Ordering::SeqCst),
));
}
if (allow_wsl2_gui || allow_titlebar_removed || style.contains(WindowStyle::CAPTION) && ex_style.contains(ExtendedWindowStyle::WINDOWEDGE))
&& !ex_style.contains(ExtendedWindowStyle::DLGMODALFRAME)
// Get a lot of dupe events coming through that make the redrawing go crazy
// on FocusChange events if I don't filter out this one. But, if we are
// allowing a specific layered window on the whitelist (like Steam), it should
// pass this check
&& (allow_layered || !ex_style.contains(ExtendedWindowStyle::LAYERED))
|| managed_override
{
return true;
} else if let Some(event) = event {
tracing::debug!(
"ignoring (exe: {}, title: {}, event: {})",
exe_name,
title,
event
);
}
false
}
#[allow(clippy::cognitive_complexity, clippy::too_many_lines)]
/// [`should_act`] for a window, which has one class here
pub fn should_act_window(
title: &str,
exe_name: &str,
class: &str,
path: &str,
identifiers: &[MatchingRule],
regex_identifiers: &HashMap<String, Regex>,
) -> Option<MatchingRule> {
should_act(title, exe_name, class, path, identifiers, regex_identifiers)
}
/// [`should_act_individual`] for a window, which has one class here
pub fn should_act_window_individual(
title: &str,
exe_name: &str,
class: &str,
path: &str,
identifier: &IdWithIdentifier,
regex_identifiers: &HashMap<String, Regex>,
) -> bool {
should_act_individual(title, exe_name, class, path, identifier, regex_identifiers)
}
pub fn should_act(
title: &str,
exe_name: &str,
class: &str,
path: &str,
identifiers: &[MatchingRule],
regex_identifiers: &HashMap<String, Regex>,
) -> Option<MatchingRule> {
let mut matching_rule = None;
for rule in identifiers {
match rule {
MatchingRule::Simple(identifier) => {
if should_act_individual(
title,
exe_name,
class,
path,
identifier,
regex_identifiers,
) {
matching_rule = Some(rule.clone());
};
}
MatchingRule::Composite(identifiers) => {
let mut composite_results = vec![];
for identifier in identifiers {
composite_results.push(should_act_individual(
title,
exe_name,
class,
path,
identifier,
regex_identifiers,
));
}
if composite_results.iter().all(|&x| x) {
matching_rule = Some(rule.clone());
}
}
}
}
matching_rule
}
pub fn should_act_individual(
title: &str,
exe_name: &str,
class: &str,
path: &str,
identifier: &IdWithIdentifier,
regex_identifiers: &HashMap<String, Regex>,
) -> bool {
let mut should_act = false;
match identifier.matching_strategy {
None | Some(MatchingStrategy::Legacy) => match identifier.kind {
ApplicationIdentifier::Title => {
if title.starts_with(&identifier.id) || title.ends_with(&identifier.id) {
should_act = true;
}
}
ApplicationIdentifier::Class => {
if class.starts_with(&identifier.id) || class.ends_with(&identifier.id) {
should_act = true;
}
}
ApplicationIdentifier::Exe => {
if exe_name.eq(&identifier.id) {
should_act = true;
}
}
ApplicationIdentifier::Path => {
if path.eq(&identifier.id) {
should_act = true;
}
}
},
Some(MatchingStrategy::Equals) => match identifier.kind {
ApplicationIdentifier::Title => {
if title.eq(&identifier.id) {
should_act = true;
}
}
ApplicationIdentifier::Class => {
if class.eq(&identifier.id) {
should_act = true;
}
}
ApplicationIdentifier::Exe => {
if exe_name.eq(&identifier.id) {
should_act = true;
}
}
ApplicationIdentifier::Path => {
if path.eq(&identifier.id) {
should_act = true;
}
}
},
Some(MatchingStrategy::DoesNotEqual) => match identifier.kind {
ApplicationIdentifier::Title => {
if !title.eq(&identifier.id) {
should_act = true;
}
}
ApplicationIdentifier::Class => {
if !class.eq(&identifier.id) {
should_act = true;
}
}
ApplicationIdentifier::Exe => {
if !exe_name.eq(&identifier.id) {
should_act = true;
}
}
ApplicationIdentifier::Path => {
if !path.eq(&identifier.id) {
should_act = true;
}
}
},
Some(MatchingStrategy::StartsWith) => match identifier.kind {
ApplicationIdentifier::Title => {
if title.starts_with(&identifier.id) {
should_act = true;
}
}
ApplicationIdentifier::Class => {
if class.starts_with(&identifier.id) {
should_act = true;
}
}
ApplicationIdentifier::Exe => {
if exe_name.starts_with(&identifier.id) {
should_act = true;
}
}
ApplicationIdentifier::Path => {
if path.starts_with(&identifier.id) {
should_act = true;
}
}
},
Some(MatchingStrategy::DoesNotStartWith) => match identifier.kind {
ApplicationIdentifier::Title => {
if !title.starts_with(&identifier.id) {
should_act = true;
}
}
ApplicationIdentifier::Class => {
if !class.starts_with(&identifier.id) {
should_act = true;
}
}
ApplicationIdentifier::Exe => {
if !exe_name.starts_with(&identifier.id) {
should_act = true;
}
}
ApplicationIdentifier::Path => {
if !path.starts_with(&identifier.id) {
should_act = true;
}
}
},
Some(MatchingStrategy::EndsWith) => match identifier.kind {
ApplicationIdentifier::Title => {
if title.ends_with(&identifier.id) {
should_act = true;
}
}
ApplicationIdentifier::Class => {
if class.ends_with(&identifier.id) {
should_act = true;
}
}
ApplicationIdentifier::Exe => {
if exe_name.ends_with(&identifier.id) {
should_act = true;
}
}
ApplicationIdentifier::Path => {
if path.ends_with(&identifier.id) {
should_act = true;
}
}
},
Some(MatchingStrategy::DoesNotEndWith) => match identifier.kind {
ApplicationIdentifier::Title => {
if !title.ends_with(&identifier.id) {
should_act = true;
}
}
ApplicationIdentifier::Class => {
if !class.ends_with(&identifier.id) {
should_act = true;
}
}
ApplicationIdentifier::Exe => {
if !exe_name.ends_with(&identifier.id) {
should_act = true;
}
}
ApplicationIdentifier::Path => {
if !path.ends_with(&identifier.id) {
should_act = true;
}
}
},
Some(MatchingStrategy::Contains) => match identifier.kind {
ApplicationIdentifier::Title => {
if title.contains(&identifier.id) {
should_act = true;
}
}
ApplicationIdentifier::Class => {
if class.contains(&identifier.id) {
should_act = true;
}
}
ApplicationIdentifier::Exe => {
if exe_name.contains(&identifier.id) {
should_act = true;
}
}
ApplicationIdentifier::Path => {
if path.contains(&identifier.id) {
should_act = true;
}
}
},
Some(MatchingStrategy::DoesNotContain) => match identifier.kind {
ApplicationIdentifier::Title => {
if !title.contains(&identifier.id) {
should_act = true;
}
}
ApplicationIdentifier::Class => {
if !class.contains(&identifier.id) {
should_act = true;
}
}
ApplicationIdentifier::Exe => {
if !exe_name.contains(&identifier.id) {
should_act = true;
}
}
ApplicationIdentifier::Path => {
if !path.contains(&identifier.id) {
should_act = true;
}
}
},
Some(MatchingStrategy::Regex) => match identifier.kind {
ApplicationIdentifier::Title => {
if let Some(re) = regex_identifiers.get(&identifier.id)
&& re.is_match(title)
{
should_act = true;
}
}
ApplicationIdentifier::Class => {
if let Some(re) = regex_identifiers.get(&identifier.id)
&& re.is_match(class)
{
should_act = true;
}
}
ApplicationIdentifier::Exe => {
if let Some(re) = regex_identifiers.get(&identifier.id)
&& re.is_match(exe_name)
{
should_act = true;
}
}
ApplicationIdentifier::Path => {
if let Some(re) = regex_identifiers.get(&identifier.id)
&& re.is_match(path)
{
should_act = true;
}
}
},
}
should_act
}