Skip to content

Commit

Permalink
feat(wm): add alt-tab heuristics to wsr
Browse files Browse the repository at this point in the history
This commit adds some rough heuristics to workspace_reconciliator which
should help with having the correct window focused after reconciliation
in the majority of, but probably not all, cases.

EnumWindows generally returns HWNDs according to z order, and a window
selected by alt-tab will almost always be put on the top of the z order.
Before sending a workspace_reconciliator::Notification, we store this
HWND along with an Instant and an AtomicBool telling us that we have a
candidate to focus after the workspace switch.
  • Loading branch information
LGUG2Z committed May 15, 2024
1 parent 87b1ab9 commit d102c00
Show file tree
Hide file tree
Showing 6 changed files with 125 additions and 10 deletions.
2 changes: 0 additions & 2 deletions justfile
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,6 @@ install:
just install-target komorebi

run:
just install-target komorebic
cargo +stable run --bin komorebi --locked

warn $RUST_LOG="warn":
Expand All @@ -44,7 +43,6 @@ trace $RUST_LOG="trace":
just run

deadlock $RUST_LOG="trace":
just install-komorebic
cargo +stable run --bin komorebi --locked --features deadlock_detection

docgen:
Expand Down
31 changes: 24 additions & 7 deletions komorebi/src/border_manager/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -18,8 +18,11 @@ use std::sync::atomic::AtomicI32;
use std::sync::atomic::AtomicU32;
use std::sync::Arc;
use std::sync::OnceLock;
use std::time::Duration;
use std::time::Instant;
use windows::Win32::Foundation::HWND;

use crate::workspace_reconciliator::ALT_TAB_HWND;
use crate::Colour;
use crate::Rect;
use crate::Rgb;
Expand Down Expand Up @@ -121,23 +124,37 @@ pub fn handle_notifications(wm: Arc<Mutex<WindowManager>>) -> color_eyre::Result
tracing::info!("listening");

let receiver = event_rx();
let mut instant: Option<Instant> = None;
event_tx().send(Notification)?;

'receiver: for _ in receiver {
if let Some(instant) = instant {
if instant.elapsed().lt(&Duration::from_millis(50)) {
continue 'receiver;
}
}

instant = Some(Instant::now());

let mut borders = BORDER_STATE.lock();
let mut borders_monitors = BORDERS_MONITORS.lock();

// Check the wm state every time we receive a notification
let state = wm.lock();

if !BORDER_ENABLED.load_consume() || state.is_paused {
if !borders.is_empty() {
for (_, border) in borders.iter() {
border.destroy()?;
}

borders.clear();
// If borders are disabled
if !BORDER_ENABLED.load_consume()
// Or if the wm is paused
|| state.is_paused
// Or if we are handling an alt-tab across workspaces
|| ALT_TAB_HWND.load().is_some()
{
// Destroy the borders we know about
for (_, border) in borders.iter() {
border.destroy()?;
}

borders.clear();
continue 'receiver;
}

Expand Down
22 changes: 22 additions & 0 deletions komorebi/src/process_event.rs
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
use std::fs::OpenOptions;
use std::sync::atomic::Ordering;
use std::sync::Arc;
use std::time::Duration;
use std::time::Instant;

use color_eyre::eyre::anyhow;
use color_eyre::Result;
Expand All @@ -23,6 +25,8 @@ use crate::window_manager_event::WindowManagerEvent;
use crate::windows_api::WindowsApi;
use crate::winevent::WinEvent;
use crate::workspace_reconciliator;
use crate::workspace_reconciliator::ALT_TAB_HWND;
use crate::workspace_reconciliator::ALT_TAB_HWND_INSTANT;
use crate::Notification;
use crate::NotificationEvent;
use crate::DATA_DIR;
Expand Down Expand Up @@ -271,6 +275,24 @@ impl WindowManager {
for (i, monitors) in self.monitors().iter().enumerate() {
for (j, workspace) in monitors.workspaces().iter().enumerate() {
if workspace.contains_window(window.hwnd) && focused_pair != (i, j) {
// At this point we know we are going to send a notification to the workspace reconciliator
// So we get the topmost window returned by EnumWindows, which is almost always the window
// that has been selected by alt-tab
if let Ok(alt_tab_windows) = WindowsApi::alt_tab_windows() {
if let Some(first) =
alt_tab_windows.iter().find(|w| w.title().is_ok())
{
// If our record of this HWND hasn't been updated in over a minute
let mut instant = ALT_TAB_HWND_INSTANT.lock();
if instant.elapsed().gt(&Duration::from_secs(1)) {
// Update our record with the HWND we just found
ALT_TAB_HWND.store(Some(first.hwnd));
// Update the timestamp of our record
*instant = Instant::now();
}
}
}

workspace_reconciliator::event_tx().send(
workspace_reconciliator::Notification {
monitor_idx: i,
Expand Down
11 changes: 11 additions & 0 deletions komorebi/src/windows_api.rs
Original file line number Diff line number Diff line change
Expand Up @@ -139,6 +139,7 @@ use crate::monitor::Monitor;
use crate::ring::Ring;
use crate::set_window_position::SetWindowPosition;
use crate::windows_callbacks;
use crate::Window;

pub enum WindowsResult<T, E> {
Err(E),
Expand Down Expand Up @@ -493,6 +494,16 @@ impl WindowsApi {
unsafe { GetWindow(hwnd, GW_HWNDNEXT) }.process()
}

pub fn alt_tab_windows() -> Result<Vec<Window>> {
let mut hwnds = vec![];
Self::enum_windows(
Some(windows_callbacks::alt_tab_windows),
&mut hwnds as *mut Vec<Window> as isize,
)?;

Ok(hwnds)
}

#[allow(dead_code)]
pub fn top_visible_window() -> Result<isize> {
let hwnd = Self::top_window()?;
Expand Down
20 changes: 20 additions & 0 deletions komorebi/src/windows_callbacks.rs
Original file line number Diff line number Diff line change
Expand Up @@ -152,6 +152,26 @@ pub extern "system" fn enum_window(hwnd: HWND, lparam: LPARAM) -> BOOL {
true.into()
}

pub extern "system" fn alt_tab_windows(hwnd: HWND, lparam: LPARAM) -> BOOL {
let windows = unsafe { &mut *(lparam.0 as *mut Vec<Window>) };

let is_visible = WindowsApi::is_window_visible(hwnd);
let is_window = WindowsApi::is_window(hwnd);
let is_minimized = WindowsApi::is_iconic(hwnd);

if is_visible && is_window && !is_minimized {
let window = Window { hwnd: hwnd.0 };

if let Ok(should_manage) = window.should_manage(None, &mut RuleDebug::default()) {
if should_manage {
windows.push(window);
}
}
}

true.into()
}

pub extern "system" fn win_event_hook(
_h_win_event_hook: HWINEVENTHOOK,
event: u32,
Expand Down
49 changes: 48 additions & 1 deletion komorebi/src/workspace_reconciliator.rs
Original file line number Diff line number Diff line change
@@ -1,18 +1,29 @@
#![deny(clippy::unwrap_used, clippy::expect_used)]

use crate::border_manager;
use crate::WindowManager;
use crossbeam_channel::Receiver;
use crossbeam_channel::Sender;
use crossbeam_utils::atomic::AtomicCell;
use lazy_static::lazy_static;
use parking_lot::Mutex;
use std::sync::Arc;
use std::sync::OnceLock;
use std::time::Duration;
use std::time::Instant;

#[derive(Copy, Clone)]
pub struct Notification {
pub monitor_idx: usize,
pub workspace_idx: usize,
}

pub static ALT_TAB_HWND: AtomicCell<Option<isize>> = AtomicCell::new(None);

lazy_static! {
pub static ref ALT_TAB_HWND_INSTANT: Arc<Mutex<Instant>> = Arc::new(Mutex::new(Instant::now()));
}

static CHANNEL: OnceLock<(Sender<Notification>, Receiver<Notification>)> = OnceLock::new();

pub fn channel() -> &'static (Sender<Notification>, Receiver<Notification>) {
Expand All @@ -34,7 +45,11 @@ pub fn listen_for_notifications(wm: Arc<Mutex<WindowManager>>) {
tracing::warn!("restarting finished thread");
}
Err(error) => {
tracing::warn!("restarting failed thread: {}", error);
if cfg!(debug_assertions) {
tracing::error!("restarting failed thread: {:?}", error)
} else {
tracing::error!("restarting failed thread: {}", error)
}
}
}
});
Expand All @@ -43,9 +58,11 @@ pub fn handle_notifications(wm: Arc<Mutex<WindowManager>>) -> color_eyre::Result
tracing::info!("listening");

let receiver = event_rx();
let arc = wm.clone();

for notification in receiver {
tracing::info!("running reconciliation");

let mut wm = wm.lock();
let focused_monitor_idx = wm.focused_monitor_idx();
let focused_workspace_idx =
Expand All @@ -62,6 +79,36 @@ pub fn handle_notifications(wm: Arc<Mutex<WindowManager>>) -> color_eyre::Result
monitor.focus_workspace(notification.workspace_idx)?;
monitor.load_focused_workspace(mouse_follows_focus)?;
}

// Drop our lock on the window manager state here to not slow down updates
drop(wm);

// Check if there was an alt-tab across workspaces in the last second
if let Some(hwnd) = ALT_TAB_HWND.load() {
if ALT_TAB_HWND_INSTANT
.lock()
.elapsed()
.lt(&Duration::from_secs(1))
{
// Sleep for 100 millis to let other events pass
std::thread::sleep(Duration::from_millis(100));
tracing::info!("focusing alt-tabbed window");

// Take a new lock on the wm and try to focus the container with
// the recorded HWND from the alt-tab
let mut wm = arc.lock();
if let Ok(workspace) = wm.focused_workspace_mut() {
// Regardless of if this fails, we need to get past this part
// to unblock the border manager below
let _ = workspace.focus_container_by_window(hwnd);
}

// Unblock the border manager
ALT_TAB_HWND.store(None);
// Send a notification to the border manager to update the borders
border_manager::event_tx().send(border_manager::Notification)?;
}
}
}
}

Expand Down

0 comments on commit d102c00

Please sign in to comment.