feat(transparency): add transparency manager module

This commit adds the transparency manager module, which, when enabled,
will make unfocused windows transparent using a user-configurable alpha
value between 0-255.

The corresponding komorebic commands (transparency, transparency-alpha)
have been added, as well as the corresponding static configuration
values (transparency, transparency_alpha).

This feature is off-by-default and must be explicitly enabled by the user.

If the process is not shut down cleanly via the 'komorebic stop'
command, it is possible that the user will be left with transparent
windows which will not be managed by komorebi the next time it launches.

This is because the WS_EX_LAYERED style is required for transparency,
but is ignored by default in komorebi's window eligibility heuristics.
For this reason, a separate state tracker of windows that have had this
style added by the window manager is kept in the transparency manager
module.

For this edge case of shutdowns where the cleanup logic cannot be run,
the 'komorebic restore-windows' command has been updated to remove
transparency from all windows that were known to the window manager
during the last session before it was killed.

This must be run _before_ restarting komorebi, so that the previous
session's known window data is not overwritten.

In the worst case scenario that the previous session's data is
overwritten, the user will have to either kill and restart the
applications, or compile komorebi from source and explicitly set
"allow_layered" to "true" in the window_is_eligible function, before
setting the transparency alpha to 255 (fully opaque), and then resetting
to the desired value.
This commit is contained in:
LGUG2Z
2024-05-31 09:32:56 -07:00
parent b7a987be8f
commit cad2eb9a63
13 changed files with 255 additions and 14 deletions

View File

@@ -142,6 +142,8 @@ pub enum SocketMessage {
BorderStyle(BorderStyle),
BorderWidth(i32),
BorderOffset(i32),
Transparency(bool),
TransparencyAlpha(u8),
InvisibleBorders(Rect),
StackbarMode(StackbarMode),
StackbarLabel(StackbarLabel),

View File

@@ -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"] }

View File

@@ -176,7 +176,7 @@ pub fn handle_notifications(wm: Arc<Mutex<WindowManager>>) -> 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<Mutex<WindowManager>>) -> 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<Mutex<WindowManager>>) -> 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<Mutex<WindowManager>>) -> 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<Mutex<WindowManager>>) -> color_eyre::Result
if let Ok(border) = Border::create(c.id()) {
entry.insert(border)
} else {
continue 'receiver;
continue 'monitors;
}
}
};

View File

@@ -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;

View File

@@ -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());

View File

@@ -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(&notification)?)?;
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");

View File

@@ -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(&notification)?)?;
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

View File

@@ -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<ZOrder>,
/// Add transparency to unfocused windows (default: false)
#[serde(skip_serializing_if = "Option::is_none")]
pub transparency: Option<bool>,
/// Alpha value for unfocused window transparency [[0-255]] (default: 200)
#[serde(skip_serializing_if = "Option::is_none")]
pub transparency_alpha: Option<u8>,
/// Global default workspace padding (default: 10)
#[serde(skip_serializing_if = "Option::is_none")]
pub default_workspace_padding: Option<i32>,
@@ -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();

View File

@@ -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<Mutex<Vec<isize>>> = OnceLock::new();
pub struct Notification;
static CHANNEL: OnceLock<(Sender<Notification>, Receiver<Notification>)> = OnceLock::new();
pub fn known_hwnds() -> Vec<isize> {
let known = KNOWN_HWNDS.get_or_init(|| Mutex::new(Vec::new())).lock();
known.iter().copied().collect()
}
pub fn channel() -> &'static (Sender<Notification>, Receiver<Notification>) {
CHANNEL.get_or_init(crossbeam_channel::unbounded)
}
pub fn event_tx() -> Sender<Notification> {
channel().0.clone()
}
pub fn event_rx() -> Receiver<Notification> {
channel().1.clone()
}
pub fn listen_for_notifications(wm: Arc<Mutex<WindowManager>>) {
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<Mutex<WindowManager>>) -> 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<ZOrder> for isize {
fn from(val: ZOrder) -> Self {
match val {
ZOrder::Top => 0,
ZOrder::NoTopMost => -2,
ZOrder::Bottom => 1,
ZOrder::TopMost => -1,
}
}
}

View File

@@ -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<isize> 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<WindowStyle>,
pub extended_window_style: Option<ExtendedWindowStyle>,
pub title: Option<String>,
@@ -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();

View File

@@ -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();
}
}

View File

@@ -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(())

View File

@@ -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);
}