diff --git a/komorebi-core/src/lib.rs b/komorebi-core/src/lib.rs index 43e0d0a9..87305e01 100644 --- a/komorebi-core/src/lib.rs +++ b/komorebi-core/src/lib.rs @@ -142,6 +142,8 @@ pub enum SocketMessage { BorderStyle(BorderStyle), BorderWidth(i32), BorderOffset(i32), + Transparency(bool), + TransparencyAlpha(u8), InvisibleBorders(Rect), StackbarMode(StackbarMode), StackbarLabel(StackbarLabel), diff --git a/komorebi/Cargo.toml b/komorebi/Cargo.toml index 251a374e..2346dcd9 100644 --- a/komorebi/Cargo.toml +++ b/komorebi/Cargo.toml @@ -18,7 +18,7 @@ clap = { version = "4", features = ["derive"] } color-eyre = { workspace = true } crossbeam-channel = "0.5" crossbeam-utils = "0.8" -ctrlc = "3" +ctrlc = { version = "3", features = ["termination"] } dirs = { workspace = true } getset = "0.1" hex_color = { version = "3", features = ["serde"] } diff --git a/komorebi/src/border_manager/mod.rs b/komorebi/src/border_manager/mod.rs index 51e40fa1..fb24c8ad 100644 --- a/komorebi/src/border_manager/mod.rs +++ b/komorebi/src/border_manager/mod.rs @@ -176,7 +176,7 @@ pub fn handle_notifications(wm: Arc>) -> color_eyre::Result borders.remove(id); } - continue 'receiver; + continue 'monitors; } // Handle the monocle container separately @@ -187,7 +187,7 @@ pub fn handle_notifications(wm: Arc>) -> color_eyre::Result if let Ok(border) = Border::create(monocle.id()) { entry.insert(border) } else { - continue 'receiver; + continue 'monitors; } } }; @@ -287,7 +287,7 @@ pub fn handle_notifications(wm: Arc>) -> color_eyre::Result if let Ok(border) = Border::create(c.id()) { entry.insert(border) } else { - continue 'receiver; + continue 'monitors; } } }; @@ -304,7 +304,7 @@ pub fn handle_notifications(wm: Arc>) -> color_eyre::Result *Z_ORDER.lock() = restore_z_order; - continue 'receiver; + continue 'monitors; } // Get the border entry for this container from the map or create one @@ -314,7 +314,7 @@ pub fn handle_notifications(wm: Arc>) -> color_eyre::Result if let Ok(border) = Border::create(c.id()) { entry.insert(border) } else { - continue 'receiver; + continue 'monitors; } } }; diff --git a/komorebi/src/lib.rs b/komorebi/src/lib.rs index 460a9602..dd91d7ae 100644 --- a/komorebi/src/lib.rs +++ b/komorebi/src/lib.rs @@ -16,6 +16,7 @@ pub mod set_window_position; pub mod stackbar_manager; pub mod static_config; pub mod styles; +pub mod transparency_manager; pub mod window; pub mod window_manager; pub mod window_manager_event; diff --git a/komorebi/src/main.rs b/komorebi/src/main.rs index f50ce36a..0cde159b 100644 --- a/komorebi/src/main.rs +++ b/komorebi/src/main.rs @@ -34,6 +34,7 @@ use komorebi::process_movement::listen_for_movements; use komorebi::reaper; use komorebi::stackbar_manager; use komorebi::static_config::StaticConfig; +use komorebi::transparency_manager; use komorebi::window_manager::WindowManager; use komorebi::windows_api::WindowsApi; use komorebi::winevent_listener; @@ -258,6 +259,7 @@ fn main() -> Result<()> { border_manager::listen_for_notifications(wm.clone()); stackbar_manager::listen_for_notifications(wm.clone()); + transparency_manager::listen_for_notifications(wm.clone()); workspace_reconciliator::listen_for_notifications(wm.clone()); monitor_reconciliator::listen_for_notifications(wm.clone())?; reaper::watch_for_orphans(wm.clone()); diff --git a/komorebi/src/process_command.rs b/komorebi/src/process_command.rs index dedbfed3..71aa5639 100644 --- a/komorebi/src/process_command.rs +++ b/komorebi/src/process_command.rs @@ -45,6 +45,7 @@ use crate::current_virtual_desktop; use crate::notify_subscribers; use crate::stackbar_manager; use crate::static_config::StaticConfig; +use crate::transparency_manager; use crate::window::RuleDebug; use crate::window::Window; use crate::window_manager; @@ -1256,6 +1257,12 @@ impl WindowManager { SocketMessage::BorderOffset(offset) => { border_manager::BORDER_OFFSET.store(offset, Ordering::SeqCst); } + SocketMessage::Transparency(enable) => { + transparency_manager::TRANSPARENCY_ENABLED.store(enable, Ordering::SeqCst); + } + SocketMessage::TransparencyAlpha(alpha) => { + transparency_manager::TRANSPARENCY_ALPHA.store(alpha, Ordering::SeqCst); + } SocketMessage::StackbarMode(mode) => { STACKBAR_MODE.store(mode); } @@ -1347,6 +1354,7 @@ impl WindowManager { notify_subscribers(&serde_json::to_string(¬ification)?)?; border_manager::event_tx().send(border_manager::Notification)?; + transparency_manager::event_tx().send(transparency_manager::Notification)?; stackbar_manager::event_tx().send(stackbar_manager::Notification)?; tracing::info!("processed"); diff --git a/komorebi/src/process_event.rs b/komorebi/src/process_event.rs index d175efe7..86ea9590 100644 --- a/komorebi/src/process_event.rs +++ b/komorebi/src/process_event.rs @@ -19,6 +19,7 @@ use crate::border_manager::BORDER_WIDTH; use crate::current_virtual_desktop; use crate::notify_subscribers; use crate::stackbar_manager; +use crate::transparency_manager; use crate::window::should_act; use crate::window::RuleDebug; use crate::window_manager::WindowManager; @@ -609,6 +610,7 @@ impl WindowManager { notify_subscribers(&serde_json::to_string(¬ification)?)?; border_manager::event_tx().send(border_manager::Notification)?; + transparency_manager::event_tx().send(transparency_manager::Notification)?; stackbar_manager::event_tx().send(stackbar_manager::Notification)?; // Too many spammy OBJECT_NAMECHANGE events from JetBrains IDEs diff --git a/komorebi/src/static_config.rs b/komorebi/src/static_config.rs index 9f834a6e..ac4816c5 100644 --- a/komorebi/src/static_config.rs +++ b/komorebi/src/static_config.rs @@ -14,6 +14,7 @@ use crate::stackbar_manager::STACKBAR_TAB_BACKGROUND_COLOUR; use crate::stackbar_manager::STACKBAR_TAB_HEIGHT; use crate::stackbar_manager::STACKBAR_TAB_WIDTH; use crate::stackbar_manager::STACKBAR_UNFOCUSED_TEXT_COLOUR; +use crate::transparency_manager; use crate::window_manager::WindowManager; use crate::window_manager_event::WindowManagerEvent; use crate::windows_api::WindowsApi; @@ -278,6 +279,12 @@ pub struct StaticConfig { /// Active window border z-order (default: System) #[serde(skip_serializing_if = "Option::is_none")] pub border_z_order: Option, + /// Add transparency to unfocused windows (default: false) + #[serde(skip_serializing_if = "Option::is_none")] + pub transparency: Option, + /// Alpha value for unfocused window transparency [[0-255]] (default: 200) + #[serde(skip_serializing_if = "Option::is_none")] + pub transparency_alpha: Option, /// Global default workspace padding (default: 10) #[serde(skip_serializing_if = "Option::is_none")] pub default_workspace_padding: Option, @@ -468,6 +475,12 @@ impl From<&WindowManager> for StaticConfig { border_offset: Option::from(border_manager::BORDER_OFFSET.load(Ordering::SeqCst)), border: Option::from(border_manager::BORDER_ENABLED.load(Ordering::SeqCst)), border_colours, + transparency: Option::from( + transparency_manager::TRANSPARENCY_ENABLED.load(Ordering::SeqCst), + ), + transparency_alpha: Option::from( + transparency_manager::TRANSPARENCY_ALPHA.load(Ordering::SeqCst), + ), border_style: Option::from(*STYLE.lock()), border_z_order: Option::from(*Z_ORDER.lock()), default_workspace_padding: Option::from( @@ -546,6 +559,11 @@ impl StaticConfig { let border_style = self.border_style.unwrap_or_default(); *STYLE.lock() = border_style; + transparency_manager::TRANSPARENCY_ENABLED + .store(self.transparency.unwrap_or(false), Ordering::SeqCst); + transparency_manager::TRANSPARENCY_ALPHA + .store(self.transparency_alpha.unwrap_or(200), Ordering::SeqCst); + let mut float_identifiers = FLOAT_IDENTIFIERS.lock(); let mut regex_identifiers = REGEX_IDENTIFIERS.lock(); let mut manage_identifiers = MANAGE_IDENTIFIERS.lock(); diff --git a/komorebi/src/transparency_manager.rs b/komorebi/src/transparency_manager.rs new file mode 100644 index 00000000..ba8f2f50 --- /dev/null +++ b/komorebi/src/transparency_manager.rs @@ -0,0 +1,155 @@ +#![deny(clippy::unwrap_used, clippy::expect_used)] + +use crossbeam_channel::Receiver; +use crossbeam_channel::Sender; +use crossbeam_utils::atomic::AtomicConsume; +use parking_lot::Mutex; +use schemars::JsonSchema; +use serde::Deserialize; +use serde::Serialize; +use std::sync::atomic::AtomicBool; +use std::sync::atomic::AtomicU8; +use std::sync::Arc; +use std::sync::OnceLock; +use windows::Win32::Foundation::HWND; + +use crate::Window; +use crate::WindowManager; +use crate::WindowsApi; + +pub static TRANSPARENCY_ENABLED: AtomicBool = AtomicBool::new(false); +pub static TRANSPARENCY_ALPHA: AtomicU8 = AtomicU8::new(200); + +static KNOWN_HWNDS: OnceLock>> = OnceLock::new(); + +pub struct Notification; + +static CHANNEL: OnceLock<(Sender, Receiver)> = OnceLock::new(); + +pub fn known_hwnds() -> Vec { + let known = KNOWN_HWNDS.get_or_init(|| Mutex::new(Vec::new())).lock(); + known.iter().copied().collect() +} + +pub fn channel() -> &'static (Sender, Receiver) { + CHANNEL.get_or_init(crossbeam_channel::unbounded) +} + +pub fn event_tx() -> Sender { + channel().0.clone() +} + +pub fn event_rx() -> Receiver { + channel().1.clone() +} + +pub fn listen_for_notifications(wm: Arc>) { + std::thread::spawn(move || loop { + match handle_notifications(wm.clone()) { + Ok(()) => { + tracing::warn!("restarting finished thread"); + } + Err(error) => { + tracing::warn!("restarting failed thread: {}", error); + } + } + }); +} + +pub fn handle_notifications(wm: Arc>) -> color_eyre::Result<()> { + tracing::info!("listening"); + + let receiver = event_rx(); + event_tx().send(Notification)?; + + 'receiver: for _ in receiver { + let known_hwnds = KNOWN_HWNDS.get_or_init(|| Mutex::new(Vec::new())); + if !TRANSPARENCY_ENABLED.load_consume() { + for hwnd in known_hwnds.lock().iter() { + Window::from(*hwnd).opaque()?; + } + + continue 'receiver; + } + + known_hwnds.lock().clear(); + + // Check the wm state every time we receive a notification + let state = wm.lock(); + + let focused_monitor_idx = state.focused_monitor_idx(); + + 'monitors: for (monitor_idx, m) in state.monitors.elements().iter().enumerate() { + let focused_workspace_idx = m.focused_workspace_idx(); + + 'workspaces: for (workspace_idx, ws) in m.workspaces().iter().enumerate() { + // Only operate on the focused workspace of each monitor + // Workspaces with tiling disabled don't have transparent windows + if !ws.tile() || workspace_idx != focused_workspace_idx { + for window in ws.visible_windows().iter().flatten() { + window.opaque()?; + } + + continue 'workspaces; + } + + // Monocle container is never transparent + if let Some(monocle) = ws.monocle_container() { + if let Some(window) = monocle.focused_window() { + window.opaque()?; + } + + continue 'monitors; + } + + let foreground_hwnd = WindowsApi::foreground_window().unwrap_or_default(); + let is_maximized = WindowsApi::is_zoomed(HWND(foreground_hwnd)); + + if is_maximized { + Window { + hwnd: foreground_hwnd, + } + .opaque()?; + continue 'monitors; + } + + for (idx, c) in ws.containers().iter().enumerate() { + // Update the transparency for all containers on this workspace + + // If the window is not focused on the current workspace, or isn't on the focused monitor + // make it transparent + if idx != ws.focused_container_idx() || monitor_idx != focused_monitor_idx { + let unfocused_window = c.focused_window().copied().unwrap_or_default(); + unfocused_window.transparent()?; + + known_hwnds.lock().push(unfocused_window.hwnd); + // Otherwise, make it opaque + } else { + c.focused_window().copied().unwrap_or_default().opaque()?; + }; + } + } + } + } + + Ok(()) +} + +#[derive(Debug, Copy, Clone, Serialize, Deserialize, JsonSchema)] +pub enum ZOrder { + Top, + NoTopMost, + Bottom, + TopMost, +} + +impl From for isize { + fn from(val: ZOrder) -> Self { + match val { + ZOrder::Top => 0, + ZOrder::NoTopMost => -2, + ZOrder::Bottom => 1, + ZOrder::TopMost => -1, + } + } +} diff --git a/komorebi/src/window.rs b/komorebi/src/window.rs index f1d66e2a..d91fcb67 100644 --- a/komorebi/src/window.rs +++ b/komorebi/src/window.rs @@ -8,6 +8,7 @@ use std::time::Duration; use color_eyre::eyre; use color_eyre::Result; +use crossbeam_utils::atomic::AtomicConsume; use komorebi_core::config_generation::IdWithIdentifier; use komorebi_core::config_generation::MatchingRule; use komorebi_core::config_generation::MatchingStrategy; @@ -25,6 +26,7 @@ use komorebi_core::Rect; use crate::styles::ExtendedWindowStyle; use crate::styles::WindowStyle; +use crate::transparency_manager; use crate::window_manager_event::WindowManagerEvent; use crate::windows_api::WindowsApi; use crate::FLOAT_IDENTIFIERS; @@ -42,6 +44,12 @@ pub struct Window { pub hwnd: isize, } +impl From for Window { + fn from(value: isize) -> Self { + Self { hwnd: value } + } +} + #[allow(clippy::module_name_repetitions)] #[derive(Debug, Clone, Serialize, JsonSchema)] pub struct WindowDetails { @@ -249,7 +257,10 @@ impl Window { let mut ex_style = self.ex_style()?; ex_style.insert(ExtendedWindowStyle::LAYERED); self.update_ex_style(&ex_style)?; - WindowsApi::set_transparent(self.hwnd()) + WindowsApi::set_transparent( + self.hwnd(), + transparency_manager::TRANSPARENCY_ALPHA.load_consume(), + ) } pub fn opaque(self) -> Result<()> { @@ -374,7 +385,7 @@ impl Window { 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(&title, &exe_name, &class, &path, style, ex_style, event, debug); + let eligible = window_is_eligible(self.hwnd, &title, &exe_name, &class, &path, style, ex_style, event, debug); debug.should_manage = eligible; return Ok(eligible); } @@ -394,6 +405,7 @@ pub struct RuleDebug { pub has_title: bool, pub is_cloaked: bool, pub allow_cloaked: bool, + pub allow_layered_transparency: bool, pub window_style: Option, pub extended_window_style: Option, pub title: Option, @@ -410,6 +422,7 @@ pub struct RuleDebug { #[allow(clippy::too_many_arguments)] fn window_is_eligible( + hwnd: isize, title: &String, exe_name: &String, class: &String, @@ -464,7 +477,7 @@ fn window_is_eligible( } let layered_whitelist = LAYERED_WHITELIST.lock(); - let allow_layered = if let Some(rule) = should_act( + let mut allow_layered = if let Some(rule) = should_act( title, exe_name, class, @@ -478,8 +491,14 @@ fn window_is_eligible( false }; - // TODO: might need this for transparency - // let allow_layered = true; + let known_layered_hwnds = transparency_manager::known_hwnds(); + + allow_layered = if known_layered_hwnds.contains(&hwnd) { + debug.allow_layered_transparency = true; + true + } else { + allow_layered + }; let allow_wsl2_gui = { let wsl2_ui_processes = WSL2_UI_PROCESSES.lock(); diff --git a/komorebi/src/window_manager.rs b/komorebi/src/window_manager.rs index f700991b..bf728360 100644 --- a/komorebi/src/window_manager.rs +++ b/komorebi/src/window_manager.rs @@ -55,6 +55,7 @@ use crate::stackbar_manager::STACKBAR_TAB_HEIGHT; use crate::stackbar_manager::STACKBAR_TAB_WIDTH; use crate::stackbar_manager::STACKBAR_UNFOCUSED_TEXT_COLOUR; use crate::static_config::StaticConfig; +use crate::transparency_manager; use crate::window::Window; use crate::window_manager_event::WindowManagerEvent; use crate::windows_api::WindowsApi; @@ -907,6 +908,7 @@ impl WindowManager { tracing::info!("restoring all hidden windows"); let no_titlebar = NO_TITLEBAR.lock(); + let known_transparent_hwnds = transparency_manager::known_hwnds(); for monitor in self.monitors_mut() { for workspace in monitor.workspaces_mut() { @@ -916,6 +918,10 @@ impl WindowManager { window.add_title_bar()?; } + if known_transparent_hwnds.contains(&window.hwnd) { + window.opaque()?; + } + window.restore(); } } diff --git a/komorebi/src/windows_api.rs b/komorebi/src/windows_api.rs index 3982e3c8..e044e6d0 100644 --- a/komorebi/src/windows_api.rs +++ b/komorebi/src/windows_api.rs @@ -979,11 +979,10 @@ impl WindowsApi { .process() } - pub fn set_transparent(hwnd: HWND) -> Result<()> { + pub fn set_transparent(hwnd: HWND, alpha: u8) -> Result<()> { unsafe { #[allow(clippy::cast_sign_loss)] - // TODO: alpha should be configurable - SetLayeredWindowAttributes(hwnd, COLORREF(-1i32 as u32), 150, LWA_ALPHA)?; + SetLayeredWindowAttributes(hwnd, COLORREF(-1i32 as u32), alpha, LWA_ALPHA)?; } Ok(()) diff --git a/komorebic/src/main.rs b/komorebic/src/main.rs index 70535e57..847e688b 100644 --- a/komorebic/src/main.rs +++ b/komorebic/src/main.rs @@ -649,6 +649,18 @@ struct Border { boolean_state: BooleanState, } +#[derive(Parser)] +struct Transparency { + #[clap(value_enum)] + boolean_state: BooleanState, +} + +#[derive(Parser)] +struct TransparencyAlpha { + /// Alpha + alpha: u8, +} + #[derive(Parser)] struct BorderColour { #[clap(value_enum, short, long, default_value = "single")] @@ -1161,6 +1173,12 @@ enum SubCommand { #[clap(arg_required_else_help = true)] #[clap(alias = "active-window-border-offset")] BorderOffset(BorderOffset), + /// Enable or disable transparency for unfocused windows + #[clap(arg_required_else_help = true)] + Transparency(Transparency), + /// Set the alpha value for unfocused window transparency + #[clap(arg_required_else_help = true)] + TransparencyAlpha(TransparencyAlpha), /// Enable or disable focus follows mouse for the operating system #[clap(arg_required_else_help = true)] FocusFollowsMouse(FocusFollowsMouse), @@ -2241,6 +2259,12 @@ Stop-Process -Name:komorebi -ErrorAction SilentlyContinue SubCommand::BorderOffset(arg) => { send_message(&SocketMessage::BorderOffset(arg.offset).as_bytes()?)?; } + SubCommand::Transparency(arg) => { + send_message(&SocketMessage::Transparency(arg.boolean_state.into()).as_bytes()?)?; + } + SubCommand::TransparencyAlpha(arg) => { + send_message(&SocketMessage::TransparencyAlpha(arg.alpha).as_bytes()?)?; + } SubCommand::ResizeDelta(arg) => { send_message(&SocketMessage::ResizeDelta(arg.pixels).as_bytes()?)?; } @@ -2383,6 +2407,11 @@ fn show_window(hwnd: HWND, command: SHOW_WINDOW_CMD) { unsafe { ShowWindow(hwnd, command) }; } +fn remove_transparency(hwnd: HWND) { + let _ = komorebi_client::Window::from(hwnd.0).opaque(); +} + fn restore_window(hwnd: HWND) { show_window(hwnd, SW_RESTORE); + remove_transparency(hwnd); }