Skip to content

Commit

Permalink
feat(transparency): add transparency manager module
Browse files Browse the repository at this point in the history
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.
  • Loading branch information
LGUG2Z committed May 31, 2024
1 parent b7a987b commit cad2eb9
Show file tree
Hide file tree
Showing 13 changed files with 255 additions and 14 deletions.
2 changes: 2 additions & 0 deletions komorebi-core/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -142,6 +142,8 @@ pub enum SocketMessage {
BorderStyle(BorderStyle),
BorderWidth(i32),
BorderOffset(i32),
Transparency(bool),
TransparencyAlpha(u8),
InvisibleBorders(Rect),
StackbarMode(StackbarMode),
StackbarLabel(StackbarLabel),
Expand Down
2 changes: 1 addition & 1 deletion komorebi/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -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"] }
Expand Down
10 changes: 5 additions & 5 deletions komorebi/src/border_manager/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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;
}
}
};
Expand Down Expand Up @@ -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;
}
}
};
Expand All @@ -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
Expand All @@ -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;
}
}
};
Expand Down
1 change: 1 addition & 0 deletions komorebi/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down
2 changes: 2 additions & 0 deletions komorebi/src/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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());
Expand Down
8 changes: 8 additions & 0 deletions komorebi/src/process_command.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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);
}
Expand Down Expand Up @@ -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");
Expand Down
2 changes: 2 additions & 0 deletions komorebi/src/process_event.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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
Expand Down
18 changes: 18 additions & 0 deletions komorebi/src/static_config.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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>,
Expand Down Expand Up @@ -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(
Expand Down Expand Up @@ -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();
Expand Down
155 changes: 155 additions & 0 deletions komorebi/src/transparency_manager.rs
Original file line number Diff line number Diff line change
@@ -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,
}
}
}
Loading

0 comments on commit cad2eb9

Please sign in to comment.