Compare commits

...

7 Commits

Author SHA1 Message Date
LGUG2Z
b93cab6fd0 feat(wm): add alt-tab heuristics to wsr
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.
2024-05-13 08:34:03 -07:00
LGUG2Z
08377cbffc fix(wm): restart bm and wsr notif handlers on fail
This commit ensures that if and when either the border_manager or
workspace_reconciliator notification handlers crash in their respective
threads, they will be restarted instead of killing the whole thread.

This is acheived via a loop running in the spawned threads of both
listeners, which will report whichever error killed the notification
handler functions.

Clippy annotations to deny expect and unwrap calls have been added to
these two modules.

Calls to DestroyWindow for expired borders were sporadically failing in
unpredictable circumstances, so these have been switched out with calls
to CloseWindow which has been working well so far with the stackbar
feature.
2024-05-12 18:58:28 -07:00
LGUG2Z
9fd0c7bf39 refactor(clippy): apply lints 2024-05-12 16:03:01 -07:00
LGUG2Z
f02703876a fix(borders): reap untracked hwnds in destroy_all
This commit ensures that even border hwnds that may now be untracked as
a result of events such as monitor changes will now be reaped in the
destroy_all_borders function.
2024-05-12 13:41:58 -07:00
LGUG2Z
349508dd16 perf(wm): ignore same-workspace switch requests
This commit ensures that if a user uses index-based commands to switch
workspaces, workspace layout update code paths will not be run if the
user is already on the desired monitor and workspace indices.

resolve #647
2024-05-12 12:40:37 -07:00
LGUG2Z
f0c8143503 feat(wm): add noop cross-monitor-move-behaviour
This commit adds a new "NoOp" MoveBehaviour for users who don't want any
moves to happen across monitor boundaries. The
toggle-cross-monitor-move-behaviour will only toggle between Swap and
Insert, and will do nothing if NoOp is the selected MoveBehaviour.

resolve #667
2024-05-12 12:32:36 -07:00
LGUG2Z
e4bc74f7ea fix(wm): no directional focus for monocle + max
This commit ensures that directional focus commands are not processed
when a monocle container or maximized window is present on a workspace.

fix #819
2024-05-12 12:22:57 -07:00
12 changed files with 504 additions and 289 deletions

View File

@@ -28,7 +28,6 @@ install:
just install-target komorebi
run:
just install-target komorebic
cargo +stable run --bin komorebi --locked
warn $RUST_LOG="warn":
@@ -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:

View File

@@ -26,7 +26,7 @@ pub trait Arrangement {
}
impl Arrangement for DefaultLayout {
#[allow(clippy::too_many_lines)]
#[allow(clippy::too_many_lines, clippy::cognitive_complexity)]
fn calculate(
&self,
area: &Rect,
@@ -58,12 +58,13 @@ impl Arrangement for DefaultLayout {
layout.right += adjustment.right;
});
match layout_flip {
Some(Axis::Horizontal | Axis::HorizontalAndVertical) => match len {
2.. => columns_reverse(&mut layouts),
_ => {}
},
_ => {}
if matches!(
layout_flip,
Some(Axis::Horizontal | Axis::HorizontalAndVertical)
) {
if let 2.. = len {
columns_reverse(&mut layouts);
}
}
layouts
@@ -82,12 +83,13 @@ impl Arrangement for DefaultLayout {
layout.right += adjustment.right;
});
match layout_flip {
Some(Axis::Vertical | Axis::HorizontalAndVertical) => match len {
2.. => rows_reverse(&mut layouts),
_ => {}
},
_ => {}
if matches!(
layout_flip,
Some(Axis::Vertical | Axis::HorizontalAndVertical)
) {
if let 2.. = len {
rows_reverse(&mut layouts);
}
}
layouts
@@ -135,28 +137,28 @@ impl Arrangement for DefaultLayout {
layout.right += adjustment.right;
});
match layout_flip {
Some(Axis::Horizontal | Axis::HorizontalAndVertical) => match len {
2.. => {
let (primary, rest) = layouts.split_at_mut(1);
let primary = &mut primary[0];
if matches!(
layout_flip,
Some(Axis::Horizontal | Axis::HorizontalAndVertical)
) {
if let 2.. = len {
let (primary, rest) = layouts.split_at_mut(1);
let primary = &mut primary[0];
for rect in rest.iter_mut() {
rect.left = primary.left;
}
primary.left = rest[0].left + rest[0].right;
for rect in rest.iter_mut() {
rect.left = primary.left;
}
_ => {}
},
_ => {}
primary.left = rest[0].left + rest[0].right;
}
}
match layout_flip {
Some(Axis::Vertical | Axis::HorizontalAndVertical) => match len {
3.. => rows_reverse(&mut layouts[1..]),
_ => {}
},
_ => {}
if matches!(
layout_flip,
Some(Axis::Vertical | Axis::HorizontalAndVertical)
) {
if let 3.. = len {
rows_reverse(&mut layouts[1..]);
}
}
layouts
@@ -207,28 +209,28 @@ impl Arrangement for DefaultLayout {
layout.right += adjustment.right;
});
match layout_flip {
Some(Axis::Horizontal | Axis::HorizontalAndVertical) => match len {
2.. => {
let (primary, rest) = layouts.split_at_mut(1);
let primary = &mut primary[0];
if matches!(
layout_flip,
Some(Axis::Horizontal | Axis::HorizontalAndVertical)
) {
if let 2.. = len {
let (primary, rest) = layouts.split_at_mut(1);
let primary = &mut primary[0];
primary.left = rest[0].left;
for rect in rest.iter_mut() {
rect.left = primary.left + primary.right;
}
primary.left = rest[0].left;
for rect in rest.iter_mut() {
rect.left = primary.left + primary.right;
}
_ => {}
},
_ => {}
}
}
match layout_flip {
Some(Axis::Vertical | Axis::HorizontalAndVertical) => match len {
3.. => rows_reverse(&mut layouts[1..]),
_ => {}
},
_ => {}
if matches!(
layout_flip,
Some(Axis::Vertical | Axis::HorizontalAndVertical)
) {
if let 3.. = len {
rows_reverse(&mut layouts[1..]);
}
}
layouts
@@ -276,28 +278,28 @@ impl Arrangement for DefaultLayout {
layout.right += adjustment.right;
});
match layout_flip {
Some(Axis::Vertical | Axis::HorizontalAndVertical) => match len {
2.. => {
let (primary, rest) = layouts.split_at_mut(1);
let primary = &mut primary[0];
if matches!(
layout_flip,
Some(Axis::Vertical | Axis::HorizontalAndVertical)
) {
if let 2.. = len {
let (primary, rest) = layouts.split_at_mut(1);
let primary = &mut primary[0];
for rect in rest.iter_mut() {
rect.top = primary.top;
}
primary.top = rest[0].top + rest[0].bottom;
for rect in rest.iter_mut() {
rect.top = primary.top;
}
_ => {}
},
_ => {}
primary.top = rest[0].top + rest[0].bottom;
}
}
match layout_flip {
Some(Axis::Horizontal | Axis::HorizontalAndVertical) => match len {
3.. => columns_reverse(&mut layouts[1..]),
_ => {}
},
_ => {}
if matches!(
layout_flip,
Some(Axis::Horizontal | Axis::HorizontalAndVertical)
) {
if let 3.. = len {
columns_reverse(&mut layouts[1..]);
}
}
layouts
@@ -374,8 +376,11 @@ impl Arrangement for DefaultLayout {
layout.right += adjustment.right;
});
match layout_flip {
Some(Axis::Horizontal | Axis::HorizontalAndVertical) => match len {
if matches!(
layout_flip,
Some(Axis::Horizontal | Axis::HorizontalAndVertical)
) {
match len {
2 => {
let (primary, secondary) = layouts.split_at_mut(1);
let primary = &mut primary[0];
@@ -397,20 +402,20 @@ impl Arrangement for DefaultLayout {
secondary.left = primary.left + primary.right;
}
_ => {}
},
_ => {}
}
}
match layout_flip {
Some(Axis::Vertical | Axis::HorizontalAndVertical) => match len {
4.. => rows_reverse(&mut layouts[2..]),
_ => {}
},
_ => {}
if matches!(
layout_flip,
Some(Axis::Vertical | Axis::HorizontalAndVertical)
) {
if let 4.. = len {
rows_reverse(&mut layouts[2..]);
}
}
layouts
},
}
#[allow(
clippy::cast_precision_loss,
clippy::cast_possible_truncation,

View File

@@ -283,6 +283,8 @@ pub enum MoveBehaviour {
Swap,
/// Insert the window container into the focused workspace on the adjacent monitor
Insert,
/// Do nothing if trying to move a window container in the direction of an adjacent monitor
NoOp,
}
#[derive(

View File

@@ -19,6 +19,7 @@ use std::sync::atomic::Ordering;
use std::sync::mpsc;
use std::time::Duration;
use windows::core::PCWSTR;
use windows::Win32::Foundation::BOOL;
use windows::Win32::Foundation::COLORREF;
use windows::Win32::Foundation::HWND;
use windows::Win32::Foundation::LPARAM;
@@ -47,10 +48,29 @@ use windows::Win32::UI::WindowsAndMessaging::WM_DESTROY;
use windows::Win32::UI::WindowsAndMessaging::WM_PAINT;
use windows::Win32::UI::WindowsAndMessaging::WNDCLASSW;
pub extern "system" fn border_hwnds(hwnd: HWND, lparam: LPARAM) -> BOOL {
let hwnds = unsafe { &mut *(lparam.0 as *mut Vec<isize>) };
if let Ok(class) = WindowsApi::real_window_class_w(hwnd) {
if class.starts_with("komoborder") {
hwnds.push(hwnd.0);
}
}
true.into()
}
#[derive(Debug)]
pub struct Border {
pub hwnd: isize,
}
impl From<isize> for Border {
fn from(value: isize) -> Self {
Self { hwnd: value }
}
}
impl Border {
pub const fn hwnd(&self) -> HWND {
HWND(self.hwnd)
@@ -97,7 +117,7 @@ impl Border {
}
pub fn destroy(&self) -> color_eyre::Result<()> {
WindowsApi::destroy_window(self.hwnd())
WindowsApi::close_window(self.hwnd())
}
pub fn update(&self, rect: &Rect) -> color_eyre::Result<()> {

View File

@@ -1,3 +1,5 @@
#![deny(clippy::unwrap_used, clippy::expect_used)]
mod border;
use crossbeam_channel::Receiver;
@@ -9,19 +11,24 @@ use parking_lot::Mutex;
use schemars::JsonSchema;
use serde::Deserialize;
use serde::Serialize;
use std::collections::hash_map::Entry;
use std::collections::HashMap;
use std::sync::atomic::AtomicBool;
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;
use crate::WindowManager;
use crate::WindowsApi;
use border::border_hwnds;
use border::Border;
use komorebi_core::WindowKind;
@@ -68,6 +75,11 @@ pub fn event_rx() -> Receiver<Notification> {
pub fn destroy_all_borders() -> color_eyre::Result<()> {
let mut borders = BORDER_STATE.lock();
tracing::info!(
"purging known borders: {:?}",
borders.iter().map(|b| b.1.hwnd).collect::<Vec<_>>()
);
for (_, border) in borders.iter() {
border.destroy()?;
}
@@ -77,128 +89,87 @@ pub fn destroy_all_borders() -> color_eyre::Result<()> {
BORDERS_MONITORS.lock().clear();
FOCUS_STATE.lock().clear();
let mut remaining_hwnds = vec![];
WindowsApi::enum_windows(
Some(border_hwnds),
&mut remaining_hwnds as *mut Vec<isize> as isize,
)?;
if !remaining_hwnds.is_empty() {
tracing::info!("purging unknown borders: {:?}", remaining_hwnds);
for hwnd in remaining_hwnds {
Border::from(hwnd).destroy()?;
}
}
Ok(())
}
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();
let mut instant: Option<Instant> = None;
event_tx().send(Notification)?;
std::thread::spawn(move || -> color_eyre::Result<()> {
'receiver: for _ in receiver {
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();
}
'receiver: for _ in receiver {
if let Some(instant) = instant {
if instant.elapsed().lt(&Duration::from_millis(50)) {
continue 'receiver;
}
}
let focused_monitor_idx = state.focused_monitor_idx();
instant = Some(Instant::now());
for (monitor_idx, m) in state.monitors.elements().iter().enumerate() {
// Only operate on the focused workspace of each monitor
if let Some(ws) = m.focused_workspace() {
// Workspaces with tiling disabled don't have borders
if !ws.tile() {
let mut to_remove = vec![];
for (id, border) in borders.iter() {
if borders_monitors.get(id).copied().unwrap_or_default() == monitor_idx
{
border.destroy()?;
to_remove.push(id.clone());
}
}
let mut borders = BORDER_STATE.lock();
let mut borders_monitors = BORDERS_MONITORS.lock();
for id in &to_remove {
borders.remove(id);
}
// Check the wm state every time we receive a notification
let state = wm.lock();
continue 'receiver;
}
// 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()?;
}
// Handle the monocle container separately
if let Some(monocle) = ws.monocle_container() {
let mut to_remove = vec![];
for (id, border) in borders.iter() {
if borders_monitors.get(id).copied().unwrap_or_default() == monitor_idx
{
border.destroy()?;
to_remove.push(id.clone());
}
}
borders.clear();
continue 'receiver;
}
for id in &to_remove {
borders.remove(id);
}
let border = borders.entry(monocle.id().clone()).or_insert_with(|| {
Border::create(monocle.id()).expect("border creation failed")
});
borders_monitors.insert(monocle.id().clone(), monitor_idx);
{
let mut focus_state = FOCUS_STATE.lock();
focus_state.insert(border.hwnd, WindowKind::Monocle);
}
let rect = WindowsApi::window_rect(
monocle
.focused_window()
.expect("monocle container has no focused window")
.hwnd(),
)?;
border.update(&rect)?;
continue 'receiver;
}
let is_maximized = WindowsApi::is_zoomed(HWND(
WindowsApi::foreground_window().unwrap_or_default(),
));
if is_maximized {
let mut to_remove = vec![];
for (id, border) in borders.iter() {
if borders_monitors.get(id).copied().unwrap_or_default() == monitor_idx
{
border.destroy()?;
to_remove.push(id.clone());
}
}
for id in &to_remove {
borders.remove(id);
}
continue 'receiver;
}
// Destroy any borders not associated with the focused workspace
let container_ids = ws
.containers()
.iter()
.map(|c| c.id().clone())
.collect::<Vec<_>>();
let focused_monitor_idx = state.focused_monitor_idx();
for (monitor_idx, m) in state.monitors.elements().iter().enumerate() {
// Only operate on the focused workspace of each monitor
if let Some(ws) = m.focused_workspace() {
// Workspaces with tiling disabled don't have borders
if !ws.tile() {
let mut to_remove = vec![];
for (id, border) in borders.iter() {
if borders_monitors.get(id).copied().unwrap_or_default() == monitor_idx {
if !container_ids.contains(id) {
border.destroy()?;
to_remove.push(id.clone());
}
border.destroy()?;
to_remove.push(id.clone());
}
}
@@ -206,80 +177,169 @@ pub fn listen_for_notifications(wm: Arc<Mutex<WindowManager>>) {
borders.remove(id);
}
for (idx, c) in ws.containers().iter().enumerate() {
// Update border when moving or resizing with mouse
if state.pending_move_op.is_some() && idx == ws.focused_container_idx() {
let restore_z_order = *Z_ORDER.lock();
*Z_ORDER.lock() = ZOrder::TopMost;
continue 'receiver;
}
let mut rect = WindowsApi::window_rect(
c.focused_window()
.expect("container has no focused window")
.hwnd(),
)?;
// Handle the monocle container separately
if let Some(monocle) = ws.monocle_container() {
let mut to_remove = vec![];
for (id, border) in borders.iter() {
if borders_monitors.get(id).copied().unwrap_or_default() == monitor_idx {
border.destroy()?;
to_remove.push(id.clone());
}
}
while WindowsApi::lbutton_is_pressed() {
let border = borders.entry(c.id().clone()).or_insert_with(|| {
Border::create(c.id()).expect("border creation failed")
});
for id in &to_remove {
borders.remove(id);
}
let new_rect = WindowsApi::window_rect(
c.focused_window()
.expect("container has no focused window")
.hwnd(),
)?;
if rect != new_rect {
rect = new_rect;
border.update(&rect)?;
}
let border = match borders.entry(monocle.id().clone()) {
Entry::Occupied(entry) => entry.into_mut(),
Entry::Vacant(entry) => {
if let Ok(border) = Border::create(monocle.id()) {
entry.insert(border)
} else {
continue 'receiver;
}
*Z_ORDER.lock() = restore_z_order;
continue 'receiver;
}
};
// Get the border entry for this container from the map or create one
let border = borders.entry(c.id().clone()).or_insert_with(|| {
Border::create(c.id()).expect("border creation failed")
});
borders_monitors.insert(monocle.id().clone(), monitor_idx);
borders_monitors.insert(c.id().clone(), monitor_idx);
{
let mut focus_state = FOCUS_STATE.lock();
focus_state.insert(border.hwnd, WindowKind::Monocle);
}
// Update the focused state for all containers on this workspace
{
let mut focus_state = FOCUS_STATE.lock();
focus_state.insert(
border.hwnd,
if idx != ws.focused_container_idx()
|| monitor_idx != focused_monitor_idx
{
WindowKind::Unfocused
} else {
if c.windows().len() > 1 {
WindowKind::Stack
} else {
WindowKind::Single
}
},
);
let rect = WindowsApi::window_rect(
monocle.focused_window().copied().unwrap_or_default().hwnd(),
)?;
border.update(&rect)?;
continue 'receiver;
}
let is_maximized = WindowsApi::is_zoomed(HWND(
WindowsApi::foreground_window().unwrap_or_default(),
));
if is_maximized {
let mut to_remove = vec![];
for (id, border) in borders.iter() {
if borders_monitors.get(id).copied().unwrap_or_default() == monitor_idx {
border.destroy()?;
to_remove.push(id.clone());
}
}
let rect = WindowsApi::window_rect(
c.focused_window()
.expect("container has no focused window")
.hwnd(),
for id in &to_remove {
borders.remove(id);
}
continue 'receiver;
}
// Destroy any borders not associated with the focused workspace
let container_ids = ws
.containers()
.iter()
.map(|c| c.id().clone())
.collect::<Vec<_>>();
let mut to_remove = vec![];
for (id, border) in borders.iter() {
if borders_monitors.get(id).copied().unwrap_or_default() == monitor_idx
&& !container_ids.contains(id)
{
border.destroy()?;
to_remove.push(id.clone());
}
}
for id in &to_remove {
borders.remove(id);
}
for (idx, c) in ws.containers().iter().enumerate() {
// Update border when moving or resizing with mouse
if state.pending_move_op.is_some() && idx == ws.focused_container_idx() {
let restore_z_order = *Z_ORDER.lock();
*Z_ORDER.lock() = ZOrder::TopMost;
let mut rect = WindowsApi::window_rect(
c.focused_window().copied().unwrap_or_default().hwnd(),
)?;
border.update(&rect)?;
while WindowsApi::lbutton_is_pressed() {
let border = match borders.entry(c.id().clone()) {
Entry::Occupied(entry) => entry.into_mut(),
Entry::Vacant(entry) => {
if let Ok(border) = Border::create(c.id()) {
entry.insert(border)
} else {
continue 'receiver;
}
}
};
let new_rect = WindowsApi::window_rect(
c.focused_window().copied().unwrap_or_default().hwnd(),
)?;
if rect != new_rect {
rect = new_rect;
border.update(&rect)?;
}
}
*Z_ORDER.lock() = restore_z_order;
continue 'receiver;
}
// Get the border entry for this container from the map or create one
let border = match borders.entry(c.id().clone()) {
Entry::Occupied(entry) => entry.into_mut(),
Entry::Vacant(entry) => {
if let Ok(border) = Border::create(c.id()) {
entry.insert(border)
} else {
continue 'receiver;
}
}
};
borders_monitors.insert(c.id().clone(), monitor_idx);
// Update the focused state for all containers on this workspace
{
let mut focus_state = FOCUS_STATE.lock();
focus_state.insert(
border.hwnd,
if idx != ws.focused_container_idx()
|| monitor_idx != focused_monitor_idx
{
WindowKind::Unfocused
} else if c.windows().len() > 1 {
WindowKind::Stack
} else {
WindowKind::Single
},
);
}
let rect = WindowsApi::window_rect(
c.focused_window().copied().unwrap_or_default().hwnd(),
)?;
border.update(&rect)?;
}
}
}
}
Ok(())
});
Ok(())
}
#[derive(Debug, Copy, Clone, Serialize, Deserialize, JsonSchema)]
@@ -290,9 +350,9 @@ pub enum ZOrder {
TopMost,
}
impl Into<isize> for ZOrder {
fn into(self) -> isize {
match self {
impl From<ZOrder> for isize {
fn from(val: ZOrder) -> Self {
match val {
ZOrder::Top => 0,
ZOrder::NoTopMost => -2,
ZOrder::Bottom => 1,

View File

@@ -650,7 +650,9 @@ impl WindowManager {
self.focus_monitor(monitor_idx)?;
}
self.focus_workspace(workspace_idx)?;
if self.focused_workspace_idx().unwrap_or_default() != workspace_idx {
self.focus_workspace(workspace_idx)?;
}
}
SocketMessage::FocusWorkspaceNumbers(workspace_idx) => {
// This is to ensure that even on an empty workspace on a secondary monitor, the
@@ -672,8 +674,15 @@ impl WindowManager {
self.focus_workspace(workspace_idx)?;
}
SocketMessage::FocusMonitorWorkspaceNumber(monitor_idx, workspace_idx) => {
self.focus_monitor(monitor_idx)?;
self.focus_workspace(workspace_idx)?;
let focused_monitor_idx = self.focused_monitor_idx();
let focused_workspace_idx = self.focused_workspace_idx().unwrap_or_default();
let focused_pair = (focused_monitor_idx, focused_workspace_idx);
if focused_pair != (monitor_idx, workspace_idx) {
self.focus_monitor(monitor_idx)?;
self.focus_workspace(workspace_idx)?;
}
}
SocketMessage::FocusNamedWorkspace(ref name) => {
if let Some((monitor_idx, workspace_idx)) =
@@ -1187,6 +1196,7 @@ impl WindowManager {
MoveBehaviour::Insert => {
self.cross_monitor_move_behaviour = MoveBehaviour::Swap;
}
_ => {}
}
}
SocketMessage::CrossMonitorMoveBehaviour(behaviour) => {

View File

@@ -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;
@@ -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;
@@ -270,17 +274,33 @@ impl WindowManager {
for (i, monitors) in self.monitors().iter().enumerate() {
for (j, workspace) in monitors.workspaces().iter().enumerate() {
if workspace.contains_window(window.hwnd) {
if focused_pair != (i, j) {
workspace_reconciliator::event_tx().send(
workspace_reconciliator::Notification {
monitor_idx: i,
workspace_idx: j,
},
)?;
needs_reconciliation = true;
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,
workspace_idx: j,
},
)?;
needs_reconciliation = true;
}
}
}

View File

@@ -38,7 +38,7 @@ use crate::PERMAIGNORE_CLASSES;
use crate::REGEX_IDENTIFIERS;
use crate::WSL2_UI_PROCESSES;
#[derive(Debug, Clone, Copy, Deserialize, JsonSchema)]
#[derive(Debug, Default, Clone, Copy, Deserialize, JsonSchema)]
pub struct Window {
pub hwnd: isize,
}

View File

@@ -1273,6 +1273,11 @@ impl WindowManager {
let workspace = self.focused_workspace()?;
// Do not proceed if we have a monocle container or maximized window
if workspace.monocle_container().is_some() || workspace.maximized_window().is_some() {
return Ok(());
}
tracing::info!("focusing container");
let new_idx = workspace.new_idx_for_direction(direction);
@@ -1333,6 +1338,11 @@ impl WindowManager {
// If there is nowhere to move on the current workspace, try to move it onto the monitor
// in that direction if there is one
None => {
// Don't do anything if the user has set the MoveBehaviour to NoOp
if matches!(self.cross_monitor_move_behaviour, MoveBehaviour::NoOp) {
return Ok(());
}
let target_monitor_idx = self
.monitor_idx_in_direction(direction)
.ok_or_else(|| anyhow!("there is no container or monitor in this direction"))?;
@@ -2307,6 +2317,13 @@ impl WindowManager {
None
}
pub fn focused_workspace_idx(&self) -> Result<usize> {
Ok(self
.focused_monitor()
.ok_or_else(|| anyhow!("there is no monitor"))?
.focused_workspace_idx())
}
pub fn focused_workspace(&self) -> Result<&Workspace> {
self.focused_monitor()
.ok_or_else(|| anyhow!("there is no monitor"))?

View File

@@ -121,7 +121,6 @@ use windows::Win32::UI::WindowsAndMessaging::SYSTEM_PARAMETERS_INFO_ACTION;
use windows::Win32::UI::WindowsAndMessaging::SYSTEM_PARAMETERS_INFO_UPDATE_FLAGS;
use windows::Win32::UI::WindowsAndMessaging::WINDOW_LONG_PTR_INDEX;
use windows::Win32::UI::WindowsAndMessaging::WM_CLOSE;
use windows::Win32::UI::WindowsAndMessaging::WM_DESTROY;
use windows::Win32::UI::WindowsAndMessaging::WNDCLASSW;
use windows::Win32::UI::WindowsAndMessaging::WNDENUMPROC;
use windows::Win32::UI::WindowsAndMessaging::WS_DISABLED;
@@ -140,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),
@@ -433,13 +433,6 @@ impl WindowsApi {
}
}
pub fn destroy_window(hwnd: HWND) -> Result<()> {
match Self::post_message(hwnd, WM_DESTROY, WPARAM(0), LPARAM(0)) {
Ok(()) => Ok(()),
Err(_) => Err(anyhow!("could not close window")),
}
}
pub fn hide_window(hwnd: HWND) {
Self::show_window(hwnd, SW_HIDE);
}
@@ -501,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()?;

View File

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

View File

@@ -1,9 +1,16 @@
#![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 {
@@ -11,6 +18,12 @@ pub struct Notification {
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>) {
@@ -26,31 +39,78 @@ pub fn event_rx() -> Receiver<Notification> {
}
pub fn listen_for_notifications(wm: Arc<Mutex<WindowManager>>) {
tracing::info!("listening");
let receiver = event_rx();
std::thread::spawn(move || -> color_eyre::Result<()> {
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 =
wm.focused_workspace_idx_for_monitor_idx(focused_monitor_idx)?;
let focused_pair = (focused_monitor_idx, focused_workspace_idx);
let updated_pair = (notification.monitor_idx, notification.workspace_idx);
if focused_pair != updated_pair {
wm.focus_monitor(notification.monitor_idx)?;
let mouse_follows_focus = wm.mouse_follows_focus;
if let Some(monitor) = wm.focused_monitor_mut() {
monitor.focus_workspace(notification.workspace_idx)?;
monitor.load_focused_workspace(mouse_follows_focus)?;
std::thread::spawn(move || loop {
match handle_notifications(wm.clone()) {
Ok(()) => {
tracing::warn!("restarting finished thread");
}
Err(error) => {
if cfg!(debug_assertions) {
tracing::error!("restarting failed thread: {:?}", error)
} else {
tracing::error!("restarting failed thread: {}", error)
}
}
}
Ok(())
});
}
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 =
wm.focused_workspace_idx_for_monitor_idx(focused_monitor_idx)?;
let focused_pair = (focused_monitor_idx, focused_workspace_idx);
let updated_pair = (notification.monitor_idx, notification.workspace_idx);
if focused_pair != updated_pair {
wm.focus_monitor(notification.monitor_idx)?;
let mouse_follows_focus = wm.mouse_follows_focus;
if let Some(monitor) = wm.focused_monitor_mut() {
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)?;
}
}
}
}
Ok(())
}