feat(wm): initial commit

One week of blissful, in-the-zone coding, applying all of the lessons
learnt from the development of yatta.
This commit is contained in:
LGUG2Z
2021-07-29 16:18:06 -07:00
commit 61cee458a1
32 changed files with 5593 additions and 0 deletions

2
.gitignore vendored Normal file
View File

@@ -0,0 +1,2 @@
/target
.idea

1100
Cargo.lock generated Normal file

File diff suppressed because it is too large Load Diff

8
Cargo.toml Normal file
View File

@@ -0,0 +1,8 @@
[workspace]
members = [
"bindings",
"komorebi",
"komorebi-core",
"komorebic"
]

13
bindings/Cargo.toml Normal file
View File

@@ -0,0 +1,13 @@
[package]
name = "bindings"
version = "0.1.0"
authors = ["Jade Iqbal"]
edition = "2018"
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
[dependencies]
windows = "0.17.2"
[build-dependencies]
windows = "0.17.2"

29
bindings/build.rs Normal file
View File

@@ -0,0 +1,29 @@
fn main() {
windows::build!(
Windows::Win32::Foundation::{
POINT,
RECT,
BOOL,
PWSTR,
HWND,
LPARAM,
},
// error: `Windows.Win32.Graphics.Dwm.DWMWA_CLOAKED` not found in metadata
Windows::Win32::Graphics::Dwm::*,
// error: `Windows.Win32.Graphics.Gdi.MONITOR_DEFAULTTONEAREST` not found in metadata
Windows::Win32::Graphics::Gdi::*,
Windows::Win32::System::Threading::{
PROCESS_ACCESS_RIGHTS,
PROCESS_NAME_FORMAT,
OpenProcess,
QueryFullProcessImageNameW,
GetCurrentThreadId,
AttachThreadInput,
GetCurrentProcessId
},
Windows::Win32::UI::KeyboardAndMouseInput::SetFocus,
Windows::Win32::UI::Accessibility::{SetWinEventHook, HWINEVENTHOOK},
// error: `Windows.Win32.UI.WindowsAndMessaging.GWL_EXSTYLE` not found in metadata
Windows::Win32::UI::WindowsAndMessaging::*,
);
}

1
bindings/src/lib.rs Normal file
View File

@@ -0,0 +1 @@
::windows::include_bindings!();

15
komorebi-core/Cargo.toml Normal file
View File

@@ -0,0 +1,15 @@
[package]
name = "komorebi-core"
version = "0.1.0"
edition = "2018"
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
[dependencies]
bindings = { package = "bindings", path = "../bindings" }
color-eyre = "0.5.11"
clap = "3.0.0-beta.2"
serde = { version = "1.0", features = ["derive"] }
serde_json = "1.0"
strum = { version = "0.21", features = ["derive"] }

View File

@@ -0,0 +1,34 @@
use clap::Clap;
use serde::Deserialize;
use serde::Serialize;
use strum::Display;
use strum::EnumString;
#[derive(Clone, Copy, Debug, Serialize, Deserialize, Display, EnumString)]
#[strum(serialize_all = "snake_case")]
#[derive(Clap)]
pub enum CycleDirection {
Previous,
Next,
}
impl CycleDirection {
pub fn next_idx(&self, idx: usize, len: usize) -> usize {
match self {
CycleDirection::Previous => {
if idx == 0 {
len - 1
} else {
idx - 1
}
}
CycleDirection::Next => {
if idx == len - 1 {
0
} else {
idx + 1
}
}
}
}
}

193
komorebi-core/src/layout.rs Normal file
View File

@@ -0,0 +1,193 @@
use clap::Clap;
use serde::Deserialize;
use serde::Serialize;
use strum::Display;
use strum::EnumString;
use crate::Rect;
#[derive(Clone, Copy, Debug, Serialize, Deserialize, Display, EnumString)]
#[strum(serialize_all = "snake_case")]
#[derive(Clap)]
pub enum Layout {
BSP,
Columns,
Rows,
}
#[derive(Clone, Copy, Debug, Serialize, Deserialize, Display, EnumString)]
#[strum(serialize_all = "snake_case")]
#[derive(Clap)]
pub enum LayoutFlip {
Horizontal,
Vertical,
HorizontalAndVertical,
}
impl Layout {
pub fn calculate(
&self,
area: &Rect,
count: usize,
container_padding: Option<i32>,
layout_flip: Option<LayoutFlip>,
) -> Vec<Rect> {
let mut dimensions = match self {
Layout::BSP => self.fibonacci(area, count, layout_flip),
Layout::Columns => {
let right = area.right / count as i32;
let mut left = 0;
let mut layouts: Vec<Rect> = vec![];
for _ in 0..count {
layouts.push(Rect {
left: area.left + left,
top: area.top,
right,
bottom: area.bottom,
});
left += right;
}
layouts
}
Layout::Rows => {
let bottom = area.bottom / count as i32;
let mut top = 0;
let mut layouts: Vec<Rect> = vec![];
for _ in 0..count {
layouts.push(Rect {
left: area.left,
top: area.top + top,
right: area.right,
bottom,
});
top += bottom;
}
layouts
}
};
dimensions
.iter_mut()
.for_each(|l| l.add_padding(container_padding));
dimensions
}
pub fn fibonacci(
&self,
area: &Rect,
count: usize,
layout_flip: Option<LayoutFlip>,
) -> Vec<Rect> {
let mut dimensions = vec![];
for _ in 0..count {
dimensions.push(Rect::default())
}
let mut left = area.left;
let mut top = area.top;
let mut bottom = area.bottom;
let mut right = area.right;
for i in 0..count {
if i % 2 != 0 {
continue;
}
let half_width = right / 2;
let half_height = bottom / 2;
let (main_x, alt_x, new_y, alt_y);
match layout_flip {
Some(flip) => match flip {
LayoutFlip::Horizontal => {
main_x = left + half_width;
alt_x = left;
new_y = top + half_height;
alt_y = top;
}
LayoutFlip::Vertical => {
new_y = top;
alt_y = top + half_height;
main_x = left;
alt_x = left + half_width;
}
LayoutFlip::HorizontalAndVertical => {
main_x = left + half_width;
alt_x = left;
new_y = top;
alt_y = top + half_height;
}
},
None => {
main_x = left;
alt_x = left + half_width;
new_y = top + half_height;
alt_y = top;
}
}
match count - i {
1 => {
set_dimensions(&mut dimensions[i], left, top, right, bottom);
}
2 => {
set_dimensions(&mut dimensions[i], main_x, top, half_width, bottom);
set_dimensions(&mut dimensions[i + 1], alt_x, top, half_width, bottom);
}
_ => {
set_dimensions(&mut dimensions[i], main_x, top, half_width, bottom);
set_dimensions(
&mut dimensions[i + 1],
alt_x,
alt_y,
half_width,
half_height,
);
left = alt_x;
top = new_y;
right = half_width;
bottom = half_height;
}
}
}
dimensions
}
}
impl Layout {
pub fn next(&mut self) {
match self {
Layout::BSP => *self = Layout::Columns,
Layout::Columns => *self = Layout::Rows,
Layout::Rows => *self = Layout::BSP,
}
}
pub fn previous(&mut self) {
match self {
Layout::BSP => *self = Layout::Rows,
Layout::Columns => *self = Layout::BSP,
Layout::Rows => *self = Layout::Columns,
}
}
}
fn set_dimensions(rect: &mut Rect, left: i32, top: i32, right: i32, bottom: i32) {
rect.bottom = bottom;
rect.right = right;
rect.left = left;
rect.top = top;
}

105
komorebi-core/src/lib.rs Normal file
View File

@@ -0,0 +1,105 @@
use std::str::FromStr;
use clap::Clap;
use color_eyre::Result;
use serde::Deserialize;
use serde::Serialize;
use strum::Display;
use strum::EnumString;
pub use cycle_direction::CycleDirection;
pub use layout::Layout;
pub use layout::LayoutFlip;
pub use operation_direction::OperationDirection;
pub use rect::Rect;
pub mod cycle_direction;
pub mod layout;
pub mod operation_direction;
pub mod rect;
#[derive(Clone, Debug, Serialize, Deserialize, Display)]
pub enum SocketMessage {
// Window / Container Commands
FocusWindow(OperationDirection),
MoveWindow(OperationDirection),
StackWindow(OperationDirection),
UnstackWindow,
CycleStack(CycleDirection),
MoveContainerToMonitorNumber(usize),
MoveContainerToWorkspaceNumber(usize),
Promote,
ToggleFloat,
ToggleMonocle,
// Current Workspace Commands
AdjustContainerPadding(Sizing, i32),
AdjustWorkspacePadding(Sizing, i32),
ChangeLayout(Layout),
FlipLayout(LayoutFlip),
// Monitor and Workspace Commands
Stop,
TogglePause,
Retile,
FocusMonitorNumber(usize),
FocusWorkspaceNumber(usize),
ContainerPadding(usize, usize, i32),
WorkspacePadding(usize, usize, i32),
WorkspaceName(usize, usize, String),
SetLayout(usize, usize, Layout),
// Configuration
FloatClass(String),
FloatExe(String),
FloatTitle(String),
// TODO: Add some state query commands
}
impl SocketMessage {
pub fn as_bytes(&self) -> Result<Vec<u8>> {
Ok(serde_json::to_string(self)?.as_bytes().to_vec())
}
pub fn from_bytes(bytes: &[u8]) -> Result<Self> {
Ok(serde_json::from_slice(bytes)?)
}
}
impl FromStr for SocketMessage {
type Err = serde_json::Error;
fn from_str(s: &str) -> Result<Self, Self::Err> {
serde_json::from_str(s)
}
}
#[derive(Clone, Copy, Debug, Serialize, Deserialize, Display, EnumString)]
#[strum(serialize_all = "snake_case")]
#[derive(Clap)]
pub enum Sizing {
Increase,
Decrease,
}
impl Sizing {
pub fn adjust_by(&self, value: i32, adjustment: i32) -> i32 {
match self {
Sizing::Increase => value + adjustment,
Sizing::Decrease => {
if value > 0 && value - adjustment >= 0 {
value - adjustment
} else {
value
}
}
}
}
}
#[derive(Clone, Copy, Debug, Serialize, Deserialize, Display, EnumString)]
#[strum(serialize_all = "snake_case")]
#[derive(Clap)]
pub enum ResizeEdge {
Left,
Top,
Right,
Bottom,
}

View File

@@ -0,0 +1,91 @@
use clap::Clap;
use serde::Deserialize;
use serde::Serialize;
use strum::Display;
use strum::EnumString;
use crate::Layout;
#[derive(Clone, Copy, Debug, Serialize, Deserialize, Display, EnumString)]
#[strum(serialize_all = "snake_case")]
#[derive(Clap)]
pub enum OperationDirection {
Left,
Right,
Up,
Down,
}
impl OperationDirection {
pub fn can_resize(&self, layout: Layout, idx: usize, len: usize) -> bool {
match layout {
Layout::BSP => match self {
Self::Left => len != 0 && idx != 0,
Self::Up => len > 2 && idx != 0 && idx != 1,
Self::Right => len > 1 && idx % 2 == 0 && idx != len - 1,
Self::Down => len > 2 && idx != len - 1 && idx % 2 != 0,
},
_ => false,
}
}
pub fn is_valid(&self, layout: Layout, idx: usize, len: usize) -> bool {
match self {
OperationDirection::Up => match layout {
Layout::BSP => len > 2 && idx != 0 && idx != 1,
Layout::Columns => false,
Layout::Rows => idx != 0,
},
OperationDirection::Down => match layout {
Layout::BSP => len > 2 && idx != len - 1 && idx % 2 != 0,
Layout::Columns => false,
Layout::Rows => idx != len - 1,
},
OperationDirection::Left => match layout {
Layout::BSP => len > 1 && idx != 0,
Layout::Columns => idx != 0,
Layout::Rows => false,
},
OperationDirection::Right => match layout {
Layout::BSP => len > 1 && idx % 2 == 0,
Layout::Columns => idx != len - 1,
Layout::Rows => false,
},
}
}
pub fn new_idx(&self, layout: Layout, idx: usize) -> usize {
match self {
OperationDirection::Up => match layout {
Layout::BSP => {
if idx % 2 == 0 {
idx - 1
} else {
idx - 2
}
}
Layout::Columns => unreachable!(),
Layout::Rows => idx - 1,
},
OperationDirection::Down => match layout {
Layout::BSP | Layout::Rows => idx + 1,
Layout::Columns => unreachable!(),
},
OperationDirection::Left => match layout {
Layout::BSP => {
if idx % 2 == 0 {
idx - 2
} else {
idx - 1
}
}
Layout::Columns => idx - 1,
Layout::Rows => unreachable!(),
},
OperationDirection::Right => match layout {
Layout::BSP | Layout::Columns => idx + 1,
Layout::Rows => unreachable!(),
},
}
}
}

49
komorebi-core/src/rect.rs Normal file
View File

@@ -0,0 +1,49 @@
use bindings::Windows::Win32::Foundation::RECT;
#[derive(Debug, Clone)]
pub struct Rect {
pub left: i32,
pub top: i32,
pub right: i32,
pub bottom: i32,
}
impl Default for Rect {
fn default() -> Self {
Rect {
left: 0,
top: 0,
right: 0,
bottom: 0,
}
}
}
impl From<RECT> for Rect {
fn from(rect: RECT) -> Self {
Rect {
left: rect.left,
top: rect.top,
right: rect.right - rect.left,
bottom: rect.bottom - rect.top,
}
}
}
impl Rect {
pub fn add_padding(&mut self, padding: Option<i32>) {
if let Some(padding) = padding {
self.left += padding;
self.top += padding;
self.right -= padding * 2;
self.bottom -= padding * 2;
}
}
pub fn contains_point(&self, point: (i32, i32)) -> bool {
point.0 >= self.left
&& point.0 <= self.left + self.right
&& point.1 >= self.top
&& point.1 <= self.top + self.bottom
}
}

15
komorebi.iml Normal file
View File

@@ -0,0 +1,15 @@
<?xml version="1.0" encoding="UTF-8"?>
<module type="RUST_MODULE" version="4">
<component name="NewModuleRootManager" inherit-compiler-output="true">
<exclude-output />
<content url="file://$MODULE_DIR$">
<sourceFolder url="file://$MODULE_DIR$/bindings/src" isTestSource="false" />
<sourceFolder url="file://$MODULE_DIR$/komorebi-core/src" isTestSource="false" />
<sourceFolder url="file://$MODULE_DIR$/komorebi/src" isTestSource="false" />
<sourceFolder url="file://$MODULE_DIR$/komorebic/src" isTestSource="false" />
<excludeFolder url="file://$MODULE_DIR$/target" />
</content>
<orderEntry type="inheritedJdk" />
<orderEntry type="sourceFolder" forTests="false" />
</component>
</module>

555
komorebi/Cargo.lock generated Normal file
View File

@@ -0,0 +1,555 @@
# This file is automatically @generated by Cargo.
# It is not intended for manual editing.
version = 3
[[package]]
name = "addr2line"
version = "0.16.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3e61f2b7f93d2c7d2b08263acaa4a363b3e276806c68af6134c44f523bf1aacd"
dependencies = [
"gimli",
]
[[package]]
name = "adler"
version = "1.0.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f26201604c87b1e01bd3d98f8d5d9a8fcbb815e8cedb41ffccbeb4bf593a35fe"
[[package]]
name = "ansi_term"
version = "0.12.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d52a9bb7ec0cf484c551830a7ce27bd20d67eac647e1befb56b0be4ee39a55d2"
dependencies = [
"winapi",
]
[[package]]
name = "autocfg"
version = "1.0.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "cdb031dd78e28731d87d56cc8ffef4a8f36ca26c38fe2de700543e627f8a464a"
[[package]]
name = "backtrace"
version = "0.3.61"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e7a905d892734eea339e896738c14b9afce22b5318f64b951e70bf3844419b01"
dependencies = [
"addr2line",
"cc",
"cfg-if",
"libc",
"miniz_oxide",
"object",
"rustc-demangle",
]
[[package]]
name = "bindings"
version = "0.1.0"
dependencies = [
"windows",
]
[[package]]
name = "bitflags"
version = "1.2.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "cf1de2fe8c75bc145a2f577add951f8134889b4795d47466a54a5c846d691693"
[[package]]
name = "cc"
version = "1.0.69"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e70cc2f62c6ce1868963827bd677764c62d07c3d9a3e1fb1177ee1a9ab199eb2"
[[package]]
name = "cfg-if"
version = "1.0.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd"
[[package]]
name = "chrono"
version = "0.4.19"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "670ad68c9088c2a963aaa298cb369688cf3f9465ce5e2d4ca10e6e0098a1ce73"
dependencies = [
"libc",
"num-integer",
"num-traits",
"winapi",
]
[[package]]
name = "color-eyre"
version = "0.5.11"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1f1885697ee8a177096d42f158922251a41973117f6d8a234cee94b9509157b7"
dependencies = [
"backtrace",
"color-spantrace",
"eyre",
"indenter",
"once_cell",
"owo-colors",
"tracing-error",
]
[[package]]
name = "color-spantrace"
version = "0.1.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b6eee477a4a8a72f4addd4de416eb56d54bc307b284d6601bafdee1f4ea462d1"
dependencies = [
"once_cell",
"owo-colors",
"tracing-core",
"tracing-error",
]
[[package]]
name = "const-sha1"
version = "0.2.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "fb58b6451e8c2a812ad979ed1d83378caa5e927eef2622017a45f251457c2c9d"
[[package]]
name = "crossbeam-channel"
version = "0.5.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "06ed27e177f16d65f0f0c22a213e17c696ace5dd64b14258b52f9417ccb52db4"
dependencies = [
"cfg-if",
"crossbeam-utils",
]
[[package]]
name = "crossbeam-utils"
version = "0.8.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d82cfc11ce7f2c3faef78d8a684447b40d503d9681acebed6cb728d45940c4db"
dependencies = [
"cfg-if",
"lazy_static",
]
[[package]]
name = "eyre"
version = "0.6.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "221239d1d5ea86bf5d6f91c9d6bc3646ffe471b08ff9b0f91c44f115ac969d2b"
dependencies = [
"indenter",
"once_cell",
]
[[package]]
name = "gimli"
version = "0.25.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f0a01e0497841a3b2db4f8afa483cce65f7e96a3498bd6c541734792aeac8fe7"
[[package]]
name = "heck"
version = "0.3.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6d621efb26863f0e9924c6ac577e8275e5e6b77455db64ffa6c65c904e9e132c"
dependencies = [
"unicode-segmentation",
]
[[package]]
name = "indenter"
version = "0.3.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ce23b50ad8242c51a442f3ff322d56b02f08852c77e4c0b4d3fd684abc89c683"
[[package]]
name = "itoa"
version = "0.4.7"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "dd25036021b0de88a0aff6b850051563c6516d0bf53f8638938edbb9de732736"
[[package]]
name = "komorebi"
version = "0.1.0"
dependencies = [
"bindings",
"bitflags",
"color-eyre",
"crossbeam-channel",
"crossbeam-utils",
"lazy_static",
"strum",
"tracing",
"tracing-subscriber",
]
[[package]]
name = "lazy_static"
version = "1.4.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e2abad23fbc42b3700f2f279844dc832adb2b2eb069b2df918f455c4e18cc646"
[[package]]
name = "libc"
version = "0.2.98"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "320cfe77175da3a483efed4bc0adc1968ca050b098ce4f2f1c13a56626128790"
[[package]]
name = "log"
version = "0.4.14"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "51b9bbe6c47d51fc3e1a9b945965946b4c44142ab8792c50835a980d362c2710"
dependencies = [
"cfg-if",
]
[[package]]
name = "matchers"
version = "0.0.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f099785f7595cc4b4553a174ce30dd7589ef93391ff414dbb67f62392b9e0ce1"
dependencies = [
"regex-automata",
]
[[package]]
name = "memchr"
version = "2.4.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b16bd47d9e329435e309c58469fe0791c2d0d1ba96ec0954152a5ae2b04387dc"
[[package]]
name = "miniz_oxide"
version = "0.4.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a92518e98c078586bc6c934028adcca4c92a53d6a958196de835170a01d84e4b"
dependencies = [
"adler",
"autocfg",
]
[[package]]
name = "num-integer"
version = "0.1.44"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d2cc698a63b549a70bc047073d2949cce27cd1c7b0a4a862d08a8031bc2801db"
dependencies = [
"autocfg",
"num-traits",
]
[[package]]
name = "num-traits"
version = "0.2.14"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9a64b1ec5cda2586e284722486d802acf1f7dbdc623e2bfc57e65ca1cd099290"
dependencies = [
"autocfg",
]
[[package]]
name = "object"
version = "0.26.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c55827317fb4c08822499848a14237d2874d6f139828893017237e7ab93eb386"
dependencies = [
"memchr",
]
[[package]]
name = "once_cell"
version = "1.8.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "692fcb63b64b1758029e0a96ee63e049ce8c5948587f2f7208df04625e5f6b56"
[[package]]
name = "owo-colors"
version = "1.3.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2386b4ebe91c2f7f51082d4cefa145d030e33a1842a96b12e4885cc3c01f7a55"
[[package]]
name = "pin-project-lite"
version = "0.2.7"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8d31d11c69a6b52a174b42bdc0c30e5e11670f90788b2c471c31c1d17d449443"
[[package]]
name = "proc-macro2"
version = "1.0.28"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5c7ed8b8c7b886ea3ed7dde405212185f423ab44682667c8c6dd14aa1d9f6612"
dependencies = [
"unicode-xid",
]
[[package]]
name = "quote"
version = "1.0.9"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c3d0b9745dc2debf507c8422de05d7226cc1f0644216dfdfead988f9b1ab32a7"
dependencies = [
"proc-macro2",
]
[[package]]
name = "regex"
version = "1.5.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d07a8629359eb56f1e2fb1652bb04212c072a87ba68546a04065d525673ac461"
dependencies = [
"regex-syntax",
]
[[package]]
name = "regex-automata"
version = "0.1.10"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6c230d73fb8d8c1b9c0b3135c5142a8acee3a0558fb8db5cf1cb65f8d7862132"
dependencies = [
"regex-syntax",
]
[[package]]
name = "regex-syntax"
version = "0.6.25"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f497285884f3fcff424ffc933e56d7cbca511def0c9831a7f9b5f6153e3cc89b"
[[package]]
name = "rustc-demangle"
version = "0.1.20"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "dead70b0b5e03e9c814bcb6b01e03e68f7c57a80aa48c72ec92152ab3e818d49"
[[package]]
name = "ryu"
version = "1.0.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "71d301d4193d031abdd79ff7e3dd721168a9572ef3fe51a1517aba235bd8f86e"
[[package]]
name = "serde"
version = "1.0.126"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ec7505abeacaec74ae4778d9d9328fe5a5d04253220a85c4ee022239fc996d03"
[[package]]
name = "serde_json"
version = "1.0.64"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "799e97dc9fdae36a5c8b8f2cae9ce2ee9fdce2058c57a93e6099d919fd982f79"
dependencies = [
"itoa",
"ryu",
"serde",
]
[[package]]
name = "sharded-slab"
version = "0.1.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "79c719719ee05df97490f80a45acfc99e5a30ce98a1e4fb67aee422745ae14e3"
dependencies = [
"lazy_static",
]
[[package]]
name = "smallvec"
version = "1.6.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "fe0f37c9e8f3c5a4a66ad655a93c74daac4ad00c441533bf5c6e7990bb42604e"
[[package]]
name = "strum"
version = "0.21.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "aaf86bbcfd1fa9670b7a129f64fc0c9fcbbfe4f1bc4210e9e98fe71ffc12cde2"
dependencies = [
"strum_macros",
]
[[package]]
name = "strum_macros"
version = "0.21.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d06aaeeee809dbc59eb4556183dd927df67db1540de5be8d3ec0b6636358a5ec"
dependencies = [
"heck",
"proc-macro2",
"quote",
"syn",
]
[[package]]
name = "syn"
version = "1.0.74"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1873d832550d4588c3dbc20f01361ab00bfe741048f71e3fecf145a7cc18b29c"
dependencies = [
"proc-macro2",
"quote",
"unicode-xid",
]
[[package]]
name = "thread_local"
version = "1.1.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8018d24e04c95ac8790716a5987d0fec4f8b27249ffa0f7d33f1369bdfb88cbd"
dependencies = [
"once_cell",
]
[[package]]
name = "tracing"
version = "0.1.26"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "09adeb8c97449311ccd28a427f96fb563e7fd31aabf994189879d9da2394b89d"
dependencies = [
"cfg-if",
"pin-project-lite",
"tracing-attributes",
"tracing-core",
]
[[package]]
name = "tracing-attributes"
version = "0.1.15"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c42e6fa53307c8a17e4ccd4dc81cf5ec38db9209f59b222210375b54ee40d1e2"
dependencies = [
"proc-macro2",
"quote",
"syn",
]
[[package]]
name = "tracing-core"
version = "0.1.18"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a9ff14f98b1a4b289c6248a023c1c2fa1491062964e9fed67ab29c4e4da4a052"
dependencies = [
"lazy_static",
]
[[package]]
name = "tracing-error"
version = "0.1.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b4d7c0b83d4a500748fa5879461652b361edf5c9d51ede2a2ac03875ca185e24"
dependencies = [
"tracing",
"tracing-subscriber",
]
[[package]]
name = "tracing-log"
version = "0.1.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a6923477a48e41c1951f1999ef8bb5a3023eb723ceadafe78ffb65dc366761e3"
dependencies = [
"lazy_static",
"log",
"tracing-core",
]
[[package]]
name = "tracing-serde"
version = "0.1.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "fb65ea441fbb84f9f6748fd496cf7f63ec9af5bca94dd86456978d055e8eb28b"
dependencies = [
"serde",
"tracing-core",
]
[[package]]
name = "tracing-subscriber"
version = "0.2.19"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ab69019741fca4d98be3c62d2b75254528b5432233fd8a4d2739fec20278de48"
dependencies = [
"ansi_term",
"chrono",
"lazy_static",
"matchers",
"regex",
"serde",
"serde_json",
"sharded-slab",
"smallvec",
"thread_local",
"tracing",
"tracing-core",
"tracing-log",
"tracing-serde",
]
[[package]]
name = "unicode-segmentation"
version = "1.8.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8895849a949e7845e06bd6dc1aa51731a103c42707010a5b591c0038fb73385b"
[[package]]
name = "unicode-xid"
version = "0.2.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8ccb82d61f80a663efe1f787a51b16b5a51e3314d6ac365b08639f52387b33f3"
[[package]]
name = "winapi"
version = "0.3.9"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5c839a674fcd7a98952e593242ea400abe93992746761e38641405d28b00f419"
dependencies = [
"winapi-i686-pc-windows-gnu",
"winapi-x86_64-pc-windows-gnu",
]
[[package]]
name = "winapi-i686-pc-windows-gnu"
version = "0.4.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ac3b87c63620426dd9b991e5ce0329eff545bccbbb34f3be09ff6fb6ab51b7b6"
[[package]]
name = "winapi-x86_64-pc-windows-gnu"
version = "0.4.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f"
[[package]]
name = "windows"
version = "0.17.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5f1c7f11b289e450f78d55dd9dc09ae91c6ae8faed980bbf3e3a4c8f166ac259"
dependencies = [
"const-sha1",
"windows_gen",
"windows_macros",
]
[[package]]
name = "windows_gen"
version = "0.17.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "57f5facfb04bc84b5fcd27018266d90ce272e11f8b91745dfdd47282e8e0607e"
[[package]]
name = "windows_macros"
version = "0.17.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7c32753c378262520a4fa70c2e4389f4649e751faab2a887090567cff192d299"
dependencies = [
"syn",
"windows_gen",
]

26
komorebi/Cargo.toml Normal file
View File

@@ -0,0 +1,26 @@
[package]
name = "komorebi"
version = "0.1.0"
edition = "2018"
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
[dependencies]
bindings = { package = "bindings", path = "../bindings" }
komorebi-core = { path = "../komorebi-core" }
bitflags = "1.2.1"
color-eyre = "0.5.11"
crossbeam-channel = "0.5.1"
crossbeam-utils = "0.8.5"
ctrlc = "3"
dirs = "3"
eyre = "0.6.5"
lazy_static = "1.4.0"
nanoid = "0.4.0"
strum = { version = "0.21", features = ["derive"] }
sysinfo = "0.19"
tracing = "0.1.26"
tracing-appender = "0.1.2"
tracing-subscriber = "0.2.19"
uds_windows = "1"

120
komorebi/src/container.rs Normal file
View File

@@ -0,0 +1,120 @@
use std::collections::VecDeque;
use nanoid::nanoid;
use crate::ring::Ring;
use crate::window::Window;
#[derive(Debug, Clone)]
pub struct Container {
id: String,
windows: Ring<Window>,
}
impl Default for Container {
fn default() -> Self {
Self {
id: nanoid!(),
windows: Ring::default(),
}
}
}
impl PartialEq for &Container {
fn eq(&self, other: &Self) -> bool {
self.id == other.id
}
}
impl Container {
pub fn hide(&mut self) {
for window in self.windows_mut() {
window.hide();
}
}
pub fn load_focused_window(&mut self) {
let focused_idx = self.focused_window_idx();
for (i, window) in self.windows_mut().iter_mut().enumerate() {
if i == focused_idx {
window.restore();
} else {
window.hide();
}
}
}
pub fn contains_window(&self, hwnd: isize) -> bool {
for window in self.windows() {
if window.hwnd == hwnd {
return true;
}
}
false
}
pub fn idx_for_window(&self, hwnd: isize) -> Option<usize> {
let mut idx = None;
for (i, window) in self.windows().iter().enumerate() {
if window.hwnd == hwnd {
idx = Option::from(i);
}
}
idx
}
pub fn remove_window_by_idx(&mut self, idx: usize) -> Option<Window> {
self.windows_mut().remove(idx)
}
pub fn remove_focused_window(&mut self) -> Option<Window> {
let focused_idx = self.focused_window_idx();
let window = self.remove_window_by_idx(focused_idx);
if focused_idx != 0 {
self.focus_window(focused_idx - 1);
}
window
}
pub fn add_window(&mut self, window: Window) {
self.windows_mut().push_back(window);
self.focus_window(self.windows().len() - 1);
}
pub fn focused_window(&self) -> Option<&Window> {
self.windows.focused()
}
pub const fn focused_window_idx(&self) -> usize {
self.windows.focused_idx()
}
pub fn focused_window_mut(&mut self) -> Option<&mut Window> {
self.windows.focused_mut()
}
pub fn focus_window(&mut self, idx: usize) {
tracing::info!("focusing window at index: {}", idx);
self.windows.focus(idx);
}
pub const fn windows(&self) -> &VecDeque<Window> {
self.windows.elements()
}
pub fn windows_mut(&mut self) -> &mut VecDeque<Window> {
self.windows.elements_mut()
}
pub fn visible_window_mut(&mut self) -> Option<&mut Window> {
self.focused_window_mut()
}
pub const fn id(&self) -> &String {
&self.id
}
}

125
komorebi/src/main.rs Normal file
View File

@@ -0,0 +1,125 @@
#![warn(clippy::all, clippy::nursery, clippy::pedantic)]
#![allow(clippy::missing_errors_doc)]
use std::sync::Arc;
use std::sync::Mutex;
use color_eyre::eyre::ContextCompat;
use color_eyre::Result;
use crossbeam_channel::Receiver;
use crossbeam_channel::Sender;
use lazy_static::lazy_static;
use sysinfo::SystemExt;
use tracing_appender::non_blocking::WorkerGuard;
use tracing_subscriber::layer::SubscriberExt;
use tracing_subscriber::EnvFilter;
use crate::process_command::listen_for_commands;
use crate::process_event::listen_for_events;
use crate::window_manager_event::WindowManagerEvent;
use crate::windows_api::WindowsApi;
mod container;
mod monitor;
mod process_command;
mod process_event;
mod ring;
mod styles;
mod window;
mod window_manager;
mod window_manager_event;
mod windows_api;
mod windows_callbacks;
mod winevent;
mod winevent_listener;
mod workspace;
lazy_static! {
static ref FLOAT_CLASSES: Arc<Mutex<Vec<String>>> = Arc::new(Mutex::new(vec![]));
static ref FLOAT_EXES: Arc<Mutex<Vec<String>>> = Arc::new(Mutex::new(vec![]));
static ref FLOAT_TITLES: Arc<Mutex<Vec<String>>> = Arc::new(Mutex::new(vec![]));
static ref LAYERED_EXE_WHITELIST: Arc<Mutex<Vec<String>>> =
Arc::new(Mutex::new(vec!["steam.exe".to_string()]));
}
fn setup() -> Result<WorkerGuard> {
if std::env::var("RUST_LIB_BACKTRACE").is_err() {
std::env::set_var("RUST_LIB_BACKTRACE", "1");
}
color_eyre::install()?;
if std::env::var("RUST_LOG").is_err() {
std::env::set_var("RUST_LOG", "info");
}
let home = dirs::home_dir().context("there is no home directory")?;
let appender = tracing_appender::rolling::never(home, "komorebi.log");
let (non_blocking, guard) = tracing_appender::non_blocking(appender);
tracing::subscriber::set_global_default(
tracing_subscriber::fmt::Subscriber::builder()
.with_env_filter(EnvFilter::from_default_env())
.with_max_level(tracing::Level::DEBUG)
.finish()
.with(
tracing_subscriber::fmt::Layer::default()
.with_writer(non_blocking)
.with_ansi(false),
),
)?;
Ok(guard)
}
fn main() -> Result<()> {
match std::env::args().count() {
1 => {
let mut system = sysinfo::System::new_all();
system.refresh_processes();
if system.process_by_name("komorebi.exe").len() > 1 {
tracing::error!("komorebi.exe is already running, please exit the existing process before starting a new one");
std::process::exit(1);
}
// File logging worker guard has to have an assignment in the main fn to work
let _guard = setup()?;
let process_id = WindowsApi::current_process_id();
WindowsApi::allow_set_foreground_window(process_id)?;
let (outgoing, incoming): (Sender<WindowManagerEvent>, Receiver<WindowManagerEvent>) =
crossbeam_channel::unbounded();
let winevent_listener = winevent_listener::new(Arc::new(Mutex::new(outgoing)));
winevent_listener.start();
let wm = Arc::new(Mutex::new(window_manager::new(Arc::new(Mutex::new(
incoming,
)))?));
wm.lock().unwrap().init()?;
listen_for_commands(wm.clone());
listen_for_events(wm.clone());
let (ctrlc_sender, ctrlc_receiver) = crossbeam_channel::bounded(1);
ctrlc::set_handler(move || {
ctrlc_sender
.send(())
.expect("could not send signal on ctrl-c channel");
})?;
ctrlc_receiver
.recv()
.expect("could not receive signal on ctrl-c channel");
tracing::error!(
"received ctrl-c, restoring all hidden windows and terminating process"
);
wm.lock().unwrap().restore_all_windows();
std::process::exit(130);
}
_ => Ok(()),
}
}

152
komorebi/src/monitor.rs Normal file
View File

@@ -0,0 +1,152 @@
use std::collections::HashMap;
use std::collections::VecDeque;
use color_eyre::eyre::ContextCompat;
use color_eyre::Result;
use komorebi_core::Rect;
use crate::container::Container;
use crate::ring::Ring;
use crate::workspace::Workspace;
#[derive(Debug, Clone)]
pub struct Monitor {
id: isize,
monitor_size: Rect,
work_area_size: Rect,
workspaces: Ring<Workspace>,
workspace_names: HashMap<usize, String>,
}
pub fn new(id: isize, monitor_size: Rect, work_area_size: Rect) -> Monitor {
Monitor {
id,
monitor_size,
work_area_size,
workspaces: Ring::default(),
workspace_names: HashMap::default(),
}
}
impl Monitor {
pub fn load_focused_workspace(&mut self) {
let focused_idx = self.focused_workspace_idx();
for (i, workspace) in self.workspaces_mut().iter_mut().enumerate() {
if i == focused_idx {
workspace.restore();
} else {
workspace.hide();
}
}
}
pub fn add_container(&mut self, container: Container) -> Result<()> {
let workspace = self
.focused_workspace_mut()
.context("there is no workspace")?;
workspace.add_container(container);
Ok(())
}
pub fn move_container_to_workspace(
&mut self,
target_workspace_idx: usize,
follow: bool,
) -> Result<()> {
let container = self
.focused_workspace_mut()
.context("there is no workspace")?
.remove_focused_container()
.context("there is no container")?;
let workspaces = self.workspaces_mut();
let target_workspace = match workspaces.get_mut(target_workspace_idx) {
None => {
workspaces.resize(target_workspace_idx + 1, Workspace::default());
workspaces.get_mut(target_workspace_idx).unwrap()
}
Some(workspace) => workspace,
};
target_workspace.add_container(container);
if follow {
self.focus_workspace(target_workspace_idx)?;
}
Ok(())
}
pub fn focused_workspace(&self) -> Option<&Workspace> {
self.workspaces.focused()
}
pub const fn focused_workspace_idx(&self) -> usize {
self.workspaces.focused_idx()
}
pub fn focused_workspace_mut(&mut self) -> Option<&mut Workspace> {
self.workspaces.focused_mut()
}
pub fn focus_workspace(&mut self, idx: usize) -> Result<()> {
{
let workspaces = self.workspaces_mut();
tracing::info!("focusing workspace at index: {}", idx);
if workspaces.get(idx).is_none() {
workspaces.resize(idx + 1, Workspace::default());
}
self.workspaces.focus(idx);
}
// Always set the latest known name when creating the workspace for the first time
{
let name = { self.workspace_names.get(&idx).cloned() };
if name.is_some() {
self.workspaces_mut()
.get_mut(idx)
.context("there is no workspace")?
.set_name(name);
}
}
Ok(())
}
pub fn update_focused_workspace(&mut self) -> Result<()> {
tracing::info!("updating workspace: {}", self.focused_workspace_idx());
let work_area = self.work_area_size().clone();
self.focused_workspace_mut()
.context("there is no workspace")?
.update(&work_area)?;
Ok(())
}
pub const fn workspaces(&self) -> &VecDeque<Workspace> {
self.workspaces.elements()
}
pub fn workspaces_mut(&mut self) -> &mut VecDeque<Workspace> {
self.workspaces.elements_mut()
}
pub fn workspace_names_mut(&mut self) -> &mut HashMap<usize, String> {
&mut self.workspace_names
}
pub const fn id(&self) -> isize {
self.id
}
pub const fn work_area_size(&self) -> &Rect {
&self.work_area_size
}
}

View File

@@ -0,0 +1,147 @@
use std::io::BufRead;
use std::io::BufReader;
use std::str::FromStr;
use std::sync::{Arc, Mutex};
use std::thread;
use color_eyre::eyre::ContextCompat;
use color_eyre::Result;
use uds_windows::UnixStream;
use komorebi_core::SocketMessage;
use crate::window_manager::WindowManager;
use crate::FLOAT_CLASSES;
use crate::FLOAT_EXES;
use crate::FLOAT_TITLES;
pub fn listen_for_commands(wm: Arc<Mutex<WindowManager>>) {
let listener = wm
.lock()
.unwrap()
.command_listener
.try_clone()
.expect("could not clone unix listener");
thread::spawn(move || {
tracing::info!("listening for commands");
for client in listener.incoming() {
match client {
Ok(stream) => match wm.lock().unwrap().process_command(stream) {
Ok(()) => tracing::info!("command processed"),
Err(error) => tracing::error!("{}", error),
},
Err(error) => {
tracing::error!("{}", error);
break;
}
}
}
});
}
impl WindowManager {
pub fn process_command(&mut self, stream: UnixStream) -> Result<()> {
let stream = BufReader::new(stream);
for line in stream.lines() {
let message = SocketMessage::from_str(&line?)?;
if self.is_paused {
if let SocketMessage::TogglePause = message {
tracing::info!("resuming window management");
self.is_paused = !self.is_paused;
return Ok(());
}
tracing::info!("ignoring commands while paused");
return Ok(());
}
tracing::info!("processing command: {}", &message);
match message {
SocketMessage::Promote => self.promote_container_to_front()?,
SocketMessage::FocusWindow(direction) => {
self.focus_container_in_direction(direction)?;
}
SocketMessage::MoveWindow(direction) => {
self.move_container_in_direction(direction)?;
}
SocketMessage::StackWindow(direction) => self.add_window_to_container(direction)?,
SocketMessage::UnstackWindow => self.remove_window_from_container()?,
SocketMessage::CycleStack(direction) => {
self.cycle_container_window_in_direction(direction)?;
}
SocketMessage::ToggleFloat => self.toggle_float()?,
SocketMessage::ToggleMonocle => self.toggle_monocle()?,
SocketMessage::ContainerPadding(monitor_idx, workspace_idx, size) => {
self.set_container_padding(monitor_idx, workspace_idx, size)?;
}
SocketMessage::WorkspacePadding(monitor_idx, workspace_idx, size) => {
self.set_workspace_padding(monitor_idx, workspace_idx, size)?;
}
SocketMessage::FloatClass(target) => {
let mut float_classes = FLOAT_CLASSES.lock().unwrap();
if !float_classes.contains(&target) {
float_classes.push(target);
}
}
SocketMessage::FloatExe(target) => {
let mut float_exes = FLOAT_EXES.lock().unwrap();
if !float_exes.contains(&target) {
float_exes.push(target);
}
}
SocketMessage::FloatTitle(target) => {
let mut float_titles = FLOAT_TITLES.lock().unwrap();
if !float_titles.contains(&target) {
float_titles.push(target);
}
}
SocketMessage::AdjustContainerPadding(sizing, adjustment) => {
self.adjust_container_padding(sizing, adjustment)?;
}
SocketMessage::AdjustWorkspacePadding(sizing, adjustment) => {
self.adjust_workspace_padding(sizing, adjustment)?;
}
SocketMessage::MoveContainerToWorkspaceNumber(workspace_idx) => {
self.move_container_to_workspace(workspace_idx, true)?;
}
SocketMessage::MoveContainerToMonitorNumber(monitor_idx) => {
self.move_container_to_monitor(monitor_idx, true)?;
}
SocketMessage::TogglePause => self.is_paused = !self.is_paused,
SocketMessage::FocusMonitorNumber(monitor_idx) => {
self.focus_monitor(monitor_idx)?;
self.update_focused_workspace(true)?;
}
SocketMessage::Retile => {
for monitor in self.monitors_mut() {
let work_area = monitor.work_area_size().clone();
monitor
.focused_workspace_mut()
.context("there is no workspace")?
.update(&work_area)?;
}
}
SocketMessage::FlipLayout(layout_flip) => self.flip_layout(layout_flip)?,
SocketMessage::ChangeLayout(layout) => self.change_workspace_layout(layout)?,
SocketMessage::SetLayout(monitor_idx, workspace_idx, layout) => {
self.set_workspace_layout(monitor_idx, workspace_idx, layout)?;
}
SocketMessage::FocusWorkspaceNumber(workspace_idx) => {
self.focus_workspace(workspace_idx)?;
}
SocketMessage::Stop => {
tracing::error!("received stop command, restoring all hidden windows and terminating process");
self.restore_all_windows();
std::process::exit(0)
}
SocketMessage::WorkspaceName(monitor_idx, workspace_idx, name) => {
self.set_workspace_name(monitor_idx, workspace_idx, name)?;
}
}
}
Ok(())
}
}

View File

@@ -0,0 +1,124 @@
use std::sync::Arc;
use std::sync::Mutex;
use std::thread;
use color_eyre::eyre::ContextCompat;
use color_eyre::Result;
use crossbeam_channel::select;
use crate::window_manager::WindowManager;
use crate::window_manager_event::WindowManagerEvent;
pub fn listen_for_events(wm: Arc<Mutex<WindowManager>>) {
let receiver = wm.lock().unwrap().incoming_events.lock().unwrap().clone();
thread::spawn(move || {
tracing::info!("listening for events");
loop {
select! {
recv(receiver) -> mut maybe_event => {
if let Ok(event) = maybe_event.as_mut() {
match wm.lock().unwrap().process_event(event) {
Ok(()) => {},
Err(error) => tracing::error!("{}", error)
}
}
}
}
}
});
}
impl WindowManager {
pub fn process_event(&mut self, event: &mut WindowManagerEvent) -> Result<()> {
if self.is_paused {
tracing::info!("ignoring events while paused");
return Ok(());
}
// Make sure we have the most recently focused monitor from any event
match event {
WindowManagerEvent::FocusChange(_, window)
| WindowManagerEvent::Show(_, window)
| WindowManagerEvent::MoveResizeStart(_, window)
| WindowManagerEvent::MoveResizeEnd(_, window) => {
let monitor_idx = self
.monitor_idx_from_window(window)
.context("there is no monitor associated with this window, it may have already been destroyed")?;
self.focus_monitor(monitor_idx)?;
}
_ => {}
}
for (i, monitor) in self.monitors_mut().iter_mut().enumerate() {
let work_area = monitor.work_area_size().clone();
for (j, workspace) in monitor.workspaces_mut().iter_mut().enumerate() {
let reaped_orphans = workspace.reap_orphans()?;
if reaped_orphans.0 > 0 || reaped_orphans.1 > 0 {
workspace.update(&work_area)?;
tracing::info!(
"reaped {} orphan window(s) and {} orphaned container(s) on monitor: {}, workspace: {}",
reaped_orphans.0,
reaped_orphans.1,
i,
j
);
}
}
}
if matches!(event, WindowManagerEvent::MouseCapture(_, _)) {
tracing::trace!("only reaping orphans for mouse capture event");
return Ok(());
}
tracing::info!("processing event: {}", event);
match event {
WindowManagerEvent::Minimize(_, window) | WindowManagerEvent::Destroy(_, window) => {
self.focused_workspace_mut()?.remove_window(window.hwnd)?;
self.update_focused_workspace(false)?;
}
WindowManagerEvent::Hide(_, window) => {
// explorer.exe is always returns true even if it's running in the background,
// but we want to be able to handle the event when File Explorer windows close properly
if !window.is_window() || window.exe()? == "explorer.exe" {
self.focused_workspace_mut()?.remove_window(window.hwnd)?;
self.update_focused_workspace(false)?;
}
}
WindowManagerEvent::FocusChange(_, window) => self
.focused_workspace_mut()?
.focus_container_by_window(window.hwnd)?,
WindowManagerEvent::Show(_, window) => {
let workspace = self.focused_workspace_mut()?;
if workspace.containers().is_empty() || !workspace.contains_window(window.hwnd) {
workspace.new_container_for_window(*window);
self.update_focused_workspace(false)?;
}
}
WindowManagerEvent::MoveResizeStart(_, _window) => {
// TODO: Implement dragging resize (one day)
}
WindowManagerEvent::MoveResizeEnd(_, _window) => {
let workspace = self.focused_workspace_mut()?;
let focused_idx = workspace.focused_container_idx();
match workspace.container_idx_from_current_point() {
Some(target_idx) => {
workspace.swap_containers(focused_idx, target_idx);
self.update_focused_workspace(false)?;
}
None => self.update_focused_workspace(true)?,
}
}
WindowManagerEvent::MouseCapture(..) => {}
};
tracing::info!("finished processing event: {}", event);
Ok(())
}
}

46
komorebi/src/ring.rs Normal file
View File

@@ -0,0 +1,46 @@
use std::collections::VecDeque;
#[derive(Debug, Clone)]
pub struct Ring<T> {
elements: VecDeque<T>,
focused: usize,
}
impl<T> Default for Ring<T> {
fn default() -> Self {
Self {
elements: VecDeque::default(),
focused: 0,
}
}
}
impl<T> Ring<T> {
pub const fn elements(&self) -> &VecDeque<T> {
&self.elements
}
pub fn elements_mut(&mut self) -> &mut VecDeque<T> {
&mut self.elements
}
pub fn focus(&mut self, idx: usize) {
self.focused = idx;
}
pub fn focused(&self) -> Option<&T> {
self.elements.get(self.focused)
}
pub const fn focused_idx(&self) -> usize {
self.focused
}
pub fn focused_mut(&mut self) -> Option<&mut T> {
self.elements.get_mut(self.focused)
}
pub fn swap(&mut self, i: usize, j: usize) {
self.elements.swap(i, j);
}
}

122
komorebi/src/styles.rs Normal file
View File

@@ -0,0 +1,122 @@
use bitflags::bitflags;
use bindings::Windows::Win32::UI::WindowsAndMessaging::WS_BORDER;
use bindings::Windows::Win32::UI::WindowsAndMessaging::WS_CAPTION;
use bindings::Windows::Win32::UI::WindowsAndMessaging::WS_CHILD;
use bindings::Windows::Win32::UI::WindowsAndMessaging::WS_CHILDWINDOW;
use bindings::Windows::Win32::UI::WindowsAndMessaging::WS_CLIPCHILDREN;
use bindings::Windows::Win32::UI::WindowsAndMessaging::WS_CLIPSIBLINGS;
use bindings::Windows::Win32::UI::WindowsAndMessaging::WS_DISABLED;
use bindings::Windows::Win32::UI::WindowsAndMessaging::WS_DLGFRAME;
use bindings::Windows::Win32::UI::WindowsAndMessaging::WS_EX_ACCEPTFILES;
use bindings::Windows::Win32::UI::WindowsAndMessaging::WS_EX_APPWINDOW;
use bindings::Windows::Win32::UI::WindowsAndMessaging::WS_EX_CLIENTEDGE;
use bindings::Windows::Win32::UI::WindowsAndMessaging::WS_EX_COMPOSITED;
use bindings::Windows::Win32::UI::WindowsAndMessaging::WS_EX_CONTEXTHELP;
use bindings::Windows::Win32::UI::WindowsAndMessaging::WS_EX_CONTROLPARENT;
use bindings::Windows::Win32::UI::WindowsAndMessaging::WS_EX_DLGMODALFRAME;
use bindings::Windows::Win32::UI::WindowsAndMessaging::WS_EX_LAYERED;
use bindings::Windows::Win32::UI::WindowsAndMessaging::WS_EX_LAYOUTRTL;
use bindings::Windows::Win32::UI::WindowsAndMessaging::WS_EX_LEFT;
use bindings::Windows::Win32::UI::WindowsAndMessaging::WS_EX_LEFTSCROLLBAR;
use bindings::Windows::Win32::UI::WindowsAndMessaging::WS_EX_LTRREADING;
use bindings::Windows::Win32::UI::WindowsAndMessaging::WS_EX_MDICHILD;
use bindings::Windows::Win32::UI::WindowsAndMessaging::WS_EX_NOACTIVATE;
use bindings::Windows::Win32::UI::WindowsAndMessaging::WS_EX_NOINHERITLAYOUT;
use bindings::Windows::Win32::UI::WindowsAndMessaging::WS_EX_NOPARENTNOTIFY;
use bindings::Windows::Win32::UI::WindowsAndMessaging::WS_EX_NOREDIRECTIONBITMAP;
use bindings::Windows::Win32::UI::WindowsAndMessaging::WS_EX_OVERLAPPEDWINDOW;
use bindings::Windows::Win32::UI::WindowsAndMessaging::WS_EX_PALETTEWINDOW;
use bindings::Windows::Win32::UI::WindowsAndMessaging::WS_EX_RIGHT;
use bindings::Windows::Win32::UI::WindowsAndMessaging::WS_EX_RIGHTSCROLLBAR;
use bindings::Windows::Win32::UI::WindowsAndMessaging::WS_EX_RTLREADING;
use bindings::Windows::Win32::UI::WindowsAndMessaging::WS_EX_STATICEDGE;
use bindings::Windows::Win32::UI::WindowsAndMessaging::WS_EX_TOOLWINDOW;
use bindings::Windows::Win32::UI::WindowsAndMessaging::WS_EX_TOPMOST;
use bindings::Windows::Win32::UI::WindowsAndMessaging::WS_EX_TRANSPARENT;
use bindings::Windows::Win32::UI::WindowsAndMessaging::WS_EX_WINDOWEDGE;
use bindings::Windows::Win32::UI::WindowsAndMessaging::WS_GROUP;
use bindings::Windows::Win32::UI::WindowsAndMessaging::WS_HSCROLL;
use bindings::Windows::Win32::UI::WindowsAndMessaging::WS_ICONIC;
use bindings::Windows::Win32::UI::WindowsAndMessaging::WS_MAXIMIZE;
use bindings::Windows::Win32::UI::WindowsAndMessaging::WS_MAXIMIZEBOX;
use bindings::Windows::Win32::UI::WindowsAndMessaging::WS_MINIMIZE;
use bindings::Windows::Win32::UI::WindowsAndMessaging::WS_MINIMIZEBOX;
use bindings::Windows::Win32::UI::WindowsAndMessaging::WS_OVERLAPPED;
use bindings::Windows::Win32::UI::WindowsAndMessaging::WS_OVERLAPPEDWINDOW;
use bindings::Windows::Win32::UI::WindowsAndMessaging::WS_POPUP;
use bindings::Windows::Win32::UI::WindowsAndMessaging::WS_POPUPWINDOW;
use bindings::Windows::Win32::UI::WindowsAndMessaging::WS_SIZEBOX;
use bindings::Windows::Win32::UI::WindowsAndMessaging::WS_SYSMENU;
use bindings::Windows::Win32::UI::WindowsAndMessaging::WS_TABSTOP;
use bindings::Windows::Win32::UI::WindowsAndMessaging::WS_THICKFRAME;
use bindings::Windows::Win32::UI::WindowsAndMessaging::WS_TILED;
use bindings::Windows::Win32::UI::WindowsAndMessaging::WS_TILEDWINDOW;
use bindings::Windows::Win32::UI::WindowsAndMessaging::WS_VISIBLE;
use bindings::Windows::Win32::UI::WindowsAndMessaging::WS_VSCROLL;
bitflags! {
#[derive(Default)]
pub struct GwlStyle: u32 {
const BORDER = WS_BORDER.0;
const CAPTION = WS_CAPTION.0;
const CHILD = WS_CHILD.0;
const CHILDWINDOW = WS_CHILDWINDOW.0;
const CLIPCHILDREN = WS_CLIPCHILDREN.0;
const CLIPSIBLINGS = WS_CLIPSIBLINGS.0;
const DISABLED = WS_DISABLED.0;
const DLGFRAME = WS_DLGFRAME.0;
const GROUP = WS_GROUP.0;
const HSCROLL = WS_HSCROLL.0;
const ICONIC = WS_ICONIC.0;
const MAXIMIZE = WS_MAXIMIZE.0;
const MAXIMIZEBOX = WS_MAXIMIZEBOX.0;
const MINIMIZE = WS_MINIMIZE.0;
const MINIMIZEBOX = WS_MINIMIZEBOX.0;
const OVERLAPPED = WS_OVERLAPPED.0;
const OVERLAPPEDWINDOW = WS_OVERLAPPEDWINDOW.0;
const POPUP = WS_POPUP.0;
const POPUPWINDOW = WS_POPUPWINDOW.0;
const SIZEBOX = WS_SIZEBOX.0;
const SYSMENU = WS_SYSMENU.0;
const TABSTOP = WS_TABSTOP.0;
const THICKFRAME = WS_THICKFRAME.0;
const TILED = WS_TILED.0;
const TILEDWINDOW = WS_TILEDWINDOW.0;
const VISIBLE = WS_VISIBLE.0;
const VSCROLL = WS_VSCROLL.0;
}
}
bitflags! {
#[derive(Default)]
pub struct GwlExStyle: u32 {
const ACCEPTFILES = WS_EX_ACCEPTFILES.0;
const APPWINDOW = WS_EX_APPWINDOW.0;
const CLIENTEDGE = WS_EX_CLIENTEDGE.0;
const COMPOSITED = WS_EX_COMPOSITED.0;
const CONTEXTHELP = WS_EX_CONTEXTHELP.0;
const CONTROLPARENT = WS_EX_CONTROLPARENT.0;
const DLGMODALFRAME = WS_EX_DLGMODALFRAME.0;
const LAYERED = WS_EX_LAYERED.0;
const LAYOUTRTL = WS_EX_LAYOUTRTL.0;
const LEFT = WS_EX_LEFT.0;
const LEFTSCROLLBAR = WS_EX_LEFTSCROLLBAR.0;
const LTRREADING = WS_EX_LTRREADING.0;
const MDICHILD = WS_EX_MDICHILD.0;
const NOACTIVATE = WS_EX_NOACTIVATE.0;
const NOINHERITLAYOUT = WS_EX_NOINHERITLAYOUT.0;
const NOPARENTNOTIFY = WS_EX_NOPARENTNOTIFY.0;
const NOREDIRECTIONBITMAP = WS_EX_NOREDIRECTIONBITMAP.0;
const OVERLAPPEDWINDOW = WS_EX_OVERLAPPEDWINDOW.0;
const PALETTEWINDOW = WS_EX_PALETTEWINDOW.0;
const RIGHT = WS_EX_RIGHT.0;
const RIGHTSCROLLBAR = WS_EX_RIGHTSCROLLBAR.0;
const RTLREADING = WS_EX_RTLREADING.0;
const STATICEDGE = WS_EX_STATICEDGE.0;
const TOOLWINDOW = WS_EX_TOOLWINDOW.0;
const TOPMOST = WS_EX_TOPMOST.0;
const TRANSPARENT = WS_EX_TRANSPARENT.0;
const WINDOWEDGE = WS_EX_WINDOWEDGE.0;
}
}

211
komorebi/src/window.rs Normal file
View File

@@ -0,0 +1,211 @@
use std::convert::TryFrom;
use std::fmt::Display;
use std::fmt::Formatter;
use color_eyre::eyre::ContextCompat;
use color_eyre::Result;
use bindings::Windows::Win32::Foundation::HWND;
use komorebi_core::Rect;
use crate::styles::GwlExStyle;
use crate::styles::GwlStyle;
use crate::window_manager_event::WindowManagerEvent;
use crate::windows_api::WindowsApi;
use crate::FLOAT_CLASSES;
use crate::FLOAT_EXES;
use crate::FLOAT_TITLES;
use crate::LAYERED_EXE_WHITELIST;
#[derive(Debug, Clone, Copy)]
pub struct Window {
pub(crate) hwnd: isize,
pub(crate) original_style: GwlStyle,
}
impl Display for Window {
fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
let mut display = format!("(hwnd: {}", self.hwnd);
if let Ok(title) = self.title() {
display.push_str(&format!(", title: {}", title));
}
if let Ok(exe) = self.exe() {
display.push_str(&format!(", exe: {}", exe));
}
if let Ok(class) = self.class() {
display.push_str(&format!(", class: {}", class));
}
display.push(')');
write!(f, "{}", display)
}
}
impl Window {
pub const fn hwnd(&self) -> HWND {
HWND(self.hwnd)
}
pub fn set_position(&mut self, layout: &Rect) -> Result<()> {
WindowsApi::set_window_pos(self.hwnd(), layout)
}
pub fn hide(&self) {
WindowsApi::hide_window(self.hwnd());
}
pub fn restore(&self) {
WindowsApi::restore_window(self.hwnd());
}
pub fn focus(&self) -> Result<()> {
// Attach komorebi thread to Window thread
let (_, window_thread_id) = WindowsApi::window_thread_process_id(self.hwnd());
let current_thread_id = WindowsApi::current_thread_id();
WindowsApi::attach_thread_input(current_thread_id, window_thread_id, true)?;
// Raise Window to foreground
WindowsApi::set_foreground_window(self.hwnd())?;
// Center cursor in Window
WindowsApi::center_cursor_in_rect(&WindowsApi::window_rect(self.hwnd())?)?;
// This isn't really needed when the above command works as expected via AHK
WindowsApi::set_focus(self.hwnd())
}
pub fn update_style(&self, style: GwlStyle) -> Result<()> {
WindowsApi::update_style(self.hwnd(), isize::try_from(style.bits())?)
}
pub fn restore_style(&self) -> Result<()> {
self.update_style(self.original_style)
}
pub fn remove_border(&self) -> Result<()> {
let mut style = self.style()?;
style.remove(GwlStyle::BORDER);
self.update_style(style)
}
pub fn add_border(&self) -> Result<()> {
let mut style = self.style()?;
style.insert(GwlStyle::BORDER);
self.update_style(style)
}
pub fn remove_padding_and_title_bar(&self) -> Result<()> {
let mut style = self.style()?;
style.remove(GwlStyle::THICKFRAME);
style.remove(GwlStyle::CAPTION);
self.update_style(style)
}
pub fn add_padding_padding_and_title_bar(&self) -> Result<()> {
let mut style = self.style()?;
style.insert(GwlStyle::THICKFRAME);
style.insert(GwlStyle::CAPTION);
self.update_style(style)
}
pub fn style(&self) -> Result<GwlStyle> {
let bits = u32::try_from(WindowsApi::gwl_style(self.hwnd())?)?;
GwlStyle::from_bits(bits).context("there is no gwl style")
}
pub fn ex_style(&self) -> Result<GwlExStyle> {
let bits = u32::try_from(WindowsApi::gwl_ex_style(self.hwnd())?)?;
GwlExStyle::from_bits(bits).context("there is no gwl style")
}
pub fn title(&self) -> Result<String> {
WindowsApi::window_text_w(self.hwnd())
}
pub fn exe(&self) -> Result<String> {
let (process_id, _) = WindowsApi::window_thread_process_id(self.hwnd());
WindowsApi::exe(WindowsApi::process_handle(process_id)?)
}
pub fn class(&self) -> Result<String> {
WindowsApi::real_window_class_w(self.hwnd())
}
pub fn is_cloaked(&self) -> Result<bool> {
WindowsApi::is_window_cloaked(self.hwnd())
}
pub fn is_window(self) -> bool {
WindowsApi::is_window(self.hwnd())
}
pub fn should_manage(&self, event: Option<WindowManagerEvent>) -> Result<bool> {
let classes = FLOAT_CLASSES.lock().unwrap();
let exes = FLOAT_EXES.lock().unwrap();
let titles = FLOAT_TITLES.lock().unwrap();
if self.title().is_err() {
return Ok(false);
}
let is_cloaked = self.is_cloaked()?;
let mut allow_cloaked = false;
if let Some(WindowManagerEvent::Hide(_, _)) = event {
allow_cloaked = true;
}
match (allow_cloaked, is_cloaked) {
// If allowing cloaked windows, we don't need to check the cloaked status
(true, _) |
// If not allowing cloaked windows, we need to ensure the window is not cloaked
(false, false) => {
if let (Ok(title), Ok(exe_name)) = (self.title(), self.exe()) {
if titles.contains(&title) {
return Ok(false);
}
if exes.contains(&exe_name) {
return Ok(false);
}
if let Ok(class) = self.class() {
if classes.contains(&class) {
return Ok(false);
}
}
let allow_layered = LAYERED_EXE_WHITELIST.lock().unwrap().contains(&exe_name);
let style = self.style()?;
let ex_style = self.ex_style()?;
if style.contains(GwlStyle::CAPTION)
&& ex_style.contains(GwlExStyle::WINDOWEDGE)
&& !ex_style.contains(GwlExStyle::DLGMODALFRAME)
// Get a lot of dupe events coming through that make the redrawing go crazy
// on FocusChange events if I don't filter out this one. But, if we are
// allowing a specific layered window on the whitelist (like Steam), it should
// pass this check
&& (allow_layered || !ex_style.contains(GwlExStyle::LAYERED))
{
Ok(true)
} else {
if let Some(event) = event {
tracing::debug!("ignoring window: {} (event: {})", self, event);
}
Ok(false)
}
} else {
Ok(false)
}
}
_ => Ok(false),
}
}
}

View File

@@ -0,0 +1,546 @@
use std::collections::VecDeque;
use std::io::ErrorKind;
use std::sync::Arc;
use std::sync::Mutex;
use color_eyre::eyre::ContextCompat;
use color_eyre::Result;
use crossbeam_channel::Receiver;
use uds_windows::UnixListener;
use komorebi_core::CycleDirection;
use komorebi_core::Layout;
use komorebi_core::LayoutFlip;
use komorebi_core::OperationDirection;
use komorebi_core::Rect;
use komorebi_core::Sizing;
use crate::container::Container;
use crate::monitor::Monitor;
use crate::ring::Ring;
use crate::window::Window;
use crate::window_manager_event::WindowManagerEvent;
use crate::windows_api::WindowsApi;
use crate::workspace::Workspace;
#[derive(Debug)]
pub struct WindowManager {
pub monitors: Ring<Monitor>,
pub incoming_events: Arc<Mutex<Receiver<WindowManagerEvent>>>,
pub command_listener: UnixListener,
pub is_paused: bool,
}
pub fn new(incoming: Arc<Mutex<Receiver<WindowManagerEvent>>>) -> Result<WindowManager> {
let home = dirs::home_dir().context("there is no home directory")?;
let mut socket = home;
socket.push("komorebi.sock");
let socket = socket.as_path();
match std::fs::remove_file(&socket) {
Ok(_) => {}
Err(error) => match error.kind() {
// Doing this because ::exists() doesn't work reliably on Windows via IntelliJ
ErrorKind::NotFound => {}
_ => {
return Err(error.into());
}
},
};
let listener = UnixListener::bind(&socket)?;
Ok(WindowManager {
monitors: Ring::default(),
incoming_events: incoming,
command_listener: listener,
is_paused: false,
})
}
impl WindowManager {
pub fn init(&mut self) -> Result<()> {
tracing::info!("initialising");
WindowsApi::load_monitor_information(&mut self.monitors)?;
WindowsApi::load_workspace_information(&mut self.monitors)?;
self.update_focused_workspace(false)
}
pub fn update_focused_workspace(&mut self, mouse_follows_focus: bool) -> Result<()> {
tracing::info!("updating monitor: {}", self.focused_monitor_idx());
self.focused_monitor_mut()
.context("there is no monitor")?
.update_focused_workspace()?;
if mouse_follows_focus {
self.focused_window_mut()?.focus()?;
}
Ok(())
}
pub fn restore_all_windows(&mut self) {
for monitor in self.monitors_mut() {
for workspace in monitor.workspaces_mut() {
for containers in workspace.containers_mut() {
for window in containers.windows_mut() {
window.restore();
}
}
}
}
}
pub fn move_container_to_monitor(&mut self, idx: usize, follow: bool) -> Result<()> {
let monitor = self.focused_monitor_mut().context("there is no monitor")?;
let container = monitor
.focused_workspace_mut()
.context("there is no workspace")?
.remove_focused_container()
.context("there is no container")?;
let target_monitor = self
.monitors_mut()
.get_mut(idx)
.context("there is no monitor")?;
target_monitor.add_container(container)?;
target_monitor.load_focused_workspace();
if follow {
self.focus_monitor(idx)?;
}
self.update_focused_workspace(true)
}
pub fn move_container_to_workspace(&mut self, idx: usize, follow: bool) -> Result<()> {
let monitor = self.focused_monitor_mut().context("there is no monitor")?;
monitor.move_container_to_workspace(idx, follow)?;
monitor.load_focused_workspace();
self.update_focused_workspace(true)
}
pub fn focus_container_in_direction(&mut self, direction: OperationDirection) -> Result<()> {
let workspace = self.focused_workspace_mut()?;
let new_idx = workspace
.new_idx_for_direction(direction)
.context("this is not a valid direction from the current position")?;
workspace.focus_container(new_idx);
self.focused_window_mut()?.focus()?;
Ok(())
}
pub fn move_container_in_direction(&mut self, direction: OperationDirection) -> Result<()> {
let workspace = self.focused_workspace_mut()?;
let current_idx = workspace.focused_container_idx();
let new_idx = workspace
.new_idx_for_direction(direction)
.context("this is not a valid direction from the current position")?;
workspace.swap_containers(current_idx, new_idx);
workspace.focus_container(new_idx);
self.update_focused_workspace(true)
}
pub fn cycle_container_window_in_direction(&mut self, direction: CycleDirection) -> Result<()> {
let container = self.focused_container_mut()?;
if container.windows().len() == 1 {
return Err(eyre::anyhow!("there is only one window in this container"));
}
let current_idx = container.focused_window_idx();
let next_idx = direction.next_idx(current_idx, container.windows().len());
container.focus_window(next_idx);
container.load_focused_window();
self.update_focused_workspace(true)
}
pub fn add_window_to_container(&mut self, direction: OperationDirection) -> Result<()> {
let workspace = self.focused_workspace_mut()?;
let current_container_idx = workspace.focused_container_idx();
let is_valid = direction.is_valid(
workspace.layout(),
workspace.focused_container_idx(),
workspace.containers_mut().len(),
);
if is_valid {
let new_idx = workspace
.new_idx_for_direction(direction)
.context("this is not a valid direction from the current position")?;
let adjusted_new_index = if new_idx > current_container_idx {
new_idx - 1
} else {
new_idx
};
workspace.move_window_to_container(adjusted_new_index)?;
self.update_focused_workspace(true)?;
}
Ok(())
}
pub fn promote_container_to_front(&mut self) -> Result<()> {
let workspace = self.focused_workspace_mut()?;
workspace.promote_container()?;
self.update_focused_workspace(true)
}
pub fn remove_window_from_container(&mut self) -> Result<()> {
if self.focused_container()?.windows().len() == 1 {
return Err(eyre::anyhow!("a container must have at least one window"));
}
let workspace = self.focused_workspace_mut()?;
workspace.new_container_for_focused_window()?;
self.update_focused_workspace(true)
}
pub fn toggle_float(&mut self) -> Result<()> {
let hwnd = WindowsApi::foreground_window()?;
let workspace = self.focused_workspace_mut()?;
let mut is_floating_window = false;
for window in workspace.floating_windows() {
if window.hwnd == hwnd {
is_floating_window = true;
}
}
if is_floating_window {
self.unfloat_window()?;
} else {
self.float_window()?;
}
self.update_focused_workspace(true)
}
pub fn float_window(&mut self) -> Result<()> {
let work_area = self.focused_monitor_work_area()?;
let workspace = self.focused_workspace_mut()?;
workspace.new_floating_window()?;
let window = workspace
.floating_windows_mut()
.last_mut()
.context("there is no floating window")?;
let half_width = work_area.right / 2;
let half_weight = work_area.bottom / 2;
let center = Rect {
left: work_area.left + ((work_area.right - half_width) / 2),
top: work_area.top + ((work_area.bottom - half_weight) / 2),
right: half_width,
bottom: half_weight,
};
window.set_position(&center)?;
window.focus()?;
Ok(())
}
pub fn unfloat_window(&mut self) -> Result<()> {
let workspace = self.focused_workspace_mut()?;
workspace.new_container_for_floating_window()
}
pub fn toggle_monocle(&mut self) -> Result<()> {
let workspace = self.focused_workspace_mut()?;
match workspace.monocle_container() {
None => self.monocle_on()?,
Some(_) => self.monocle_off()?,
}
self.update_focused_workspace(true)
}
pub fn monocle_on(&mut self) -> Result<()> {
let workspace = self.focused_workspace_mut()?;
workspace.new_monocle_container()
}
pub fn monocle_off(&mut self) -> Result<()> {
let workspace = self.focused_workspace_mut()?;
workspace.reintegrate_monocle_container()
}
pub fn flip_layout(&mut self, layout_flip: LayoutFlip) -> Result<()> {
let workspace = self.focused_workspace_mut()?;
#[allow(clippy::match_same_arms)]
match workspace.layout_flip() {
None => workspace.set_layout_flip(Option::from(layout_flip)),
Some(current_layout_flip) => {
match current_layout_flip {
LayoutFlip::Horizontal => match layout_flip {
LayoutFlip::Horizontal => workspace.set_layout_flip(None),
LayoutFlip::Vertical => workspace
.set_layout_flip(Option::from(LayoutFlip::HorizontalAndVertical)),
LayoutFlip::HorizontalAndVertical => workspace
.set_layout_flip(Option::from(LayoutFlip::HorizontalAndVertical)),
},
LayoutFlip::Vertical => match layout_flip {
LayoutFlip::Horizontal => workspace
.set_layout_flip(Option::from(LayoutFlip::HorizontalAndVertical)),
LayoutFlip::Vertical => workspace.set_layout_flip(None),
LayoutFlip::HorizontalAndVertical => workspace
.set_layout_flip(Option::from(LayoutFlip::HorizontalAndVertical)),
},
LayoutFlip::HorizontalAndVertical => {
match layout_flip {
LayoutFlip::Horizontal => {
workspace.set_layout_flip(Option::from(LayoutFlip::Vertical));
}
LayoutFlip::Vertical => {
workspace.set_layout_flip(Option::from(LayoutFlip::Horizontal));
}
LayoutFlip::HorizontalAndVertical => workspace.set_layout_flip(None),
};
}
}
}
}
self.update_focused_workspace(false)
}
pub fn change_workspace_layout(&mut self, layout: Layout) -> Result<()> {
let workspace = self.focused_workspace_mut()?;
workspace.set_layout(layout);
self.update_focused_workspace(false)
}
pub fn adjust_workspace_padding(&mut self, sizing: Sizing, adjustment: i32) -> Result<()> {
let workspace = self.focused_workspace_mut()?;
let padding = workspace
.workspace_padding()
.context("there is no workspace padding")?;
workspace.set_workspace_padding(Option::from(sizing.adjust_by(padding, adjustment)));
self.update_focused_workspace(false)
}
pub fn adjust_container_padding(&mut self, sizing: Sizing, adjustment: i32) -> Result<()> {
let workspace = self.focused_workspace_mut()?;
let padding = workspace
.container_padding()
.context("there is no container padding")?;
workspace.set_container_padding(Option::from(sizing.adjust_by(padding, adjustment)));
self.update_focused_workspace(false)
}
pub fn set_workspace_layout(
&mut self,
monitor_idx: usize,
workspace_idx: usize,
layout: Layout,
) -> Result<()> {
let focused_monitor_idx = self.focused_monitor_idx();
let monitor = self
.monitors_mut()
.get_mut(monitor_idx)
.context("there is no monitor")?;
let work_area = monitor.work_area_size().clone();
let focused_workspace_idx = monitor.focused_workspace_idx();
let workspace = monitor
.workspaces_mut()
.get_mut(workspace_idx)
.context("there is no monitor")?;
workspace.set_layout(layout);
// If this is the focused workspace on a non-focused screen, let's update it
if focused_monitor_idx != monitor_idx && focused_workspace_idx == workspace_idx {
workspace.update(&work_area)?;
Ok(())
} else {
Ok(self.update_focused_workspace(false)?)
}
}
pub fn set_workspace_padding(
&mut self,
monitor_idx: usize,
workspace_idx: usize,
size: i32,
) -> Result<()> {
let monitor = self
.monitors_mut()
.get_mut(monitor_idx)
.context("there is no monitor")?;
let workspace = monitor
.workspaces_mut()
.get_mut(workspace_idx)
.context("there is no monitor")?;
workspace.set_workspace_padding(Option::from(size));
self.update_focused_workspace(false)
}
pub fn set_workspace_name(
&mut self,
monitor_idx: usize,
workspace_idx: usize,
name: String,
) -> Result<()> {
let monitor = self
.monitors_mut()
.get_mut(monitor_idx)
.context("there is no monitor")?;
let workspace = monitor
.workspaces_mut()
.get_mut(workspace_idx)
.context("there is no monitor")?;
workspace.set_name(Option::from(name.clone()));
monitor.workspace_names_mut().insert(workspace_idx, name);
Ok(())
}
pub fn set_container_padding(
&mut self,
monitor_idx: usize,
workspace_idx: usize,
size: i32,
) -> Result<()> {
let monitor = self
.monitors_mut()
.get_mut(monitor_idx)
.context("there is no monitor")?;
let workspace = monitor
.workspaces_mut()
.get_mut(workspace_idx)
.context("there is no monitor")?;
workspace.set_container_padding(Option::from(size));
self.update_focused_workspace(false)
}
}
impl WindowManager {
pub const fn monitors(&self) -> &VecDeque<Monitor> {
self.monitors.elements()
}
pub fn monitors_mut(&mut self) -> &mut VecDeque<Monitor> {
self.monitors.elements_mut()
}
pub fn focused_monitor(&self) -> Option<&Monitor> {
self.monitors.focused()
}
pub const fn focused_monitor_idx(&self) -> usize {
self.monitors.focused_idx()
}
pub fn focused_monitor_mut(&mut self) -> Option<&mut Monitor> {
self.monitors.focused_mut()
}
pub fn focused_monitor_work_area(&self) -> Result<Rect> {
Ok(self
.focused_monitor()
.context("there is no monitor")?
.work_area_size()
.clone())
}
pub fn focus_monitor(&mut self, idx: usize) -> Result<()> {
if self.monitors().get(idx).is_some() {
self.monitors.focus(idx);
} else {
return Err(eyre::anyhow!("this is not a valid monitor index"));
}
Ok(())
}
pub fn monitor_idx_from_window(&mut self, window: &Window) -> Option<usize> {
let hmonitor = WindowsApi::monitor_from_window(window.hwnd());
for (i, monitor) in self.monitors().iter().enumerate() {
if monitor.id() == hmonitor {
return Option::from(i);
}
}
None
}
pub fn focused_workspace(&self) -> Result<&Workspace> {
self.focused_monitor()
.context("there is no monitor")?
.focused_workspace()
.context("there is no workspace")
}
pub fn focused_workspace_mut(&mut self) -> Result<&mut Workspace> {
self.focused_monitor_mut()
.context("there is no monitor")?
.focused_workspace_mut()
.context("there is no workspace")
}
pub fn focus_workspace(&mut self, idx: usize) -> Result<()> {
let monitor = self
.focused_monitor_mut()
.context("there is no workspace")?;
monitor.focus_workspace(idx)?;
monitor.load_focused_workspace();
self.update_focused_workspace(true)
}
pub fn focused_container(&self) -> Result<&Container> {
self.focused_workspace()?
.focused_container()
.context("there is no container")
}
pub fn focused_container_mut(&mut self) -> Result<&mut Container> {
self.focused_workspace_mut()?
.focused_container_mut()
.context("there is no container")
}
fn focused_window_mut(&mut self) -> Result<&mut Window> {
self.focused_container_mut()?
.focused_window_mut()
.context("there is no window")
}
}

View File

@@ -0,0 +1,90 @@
use std::fmt::Display;
use std::fmt::Formatter;
use crate::window::Window;
use crate::winevent::WinEvent;
#[derive(Debug, Copy, Clone)]
pub enum WindowManagerEvent {
Destroy(WinEvent, Window),
FocusChange(WinEvent, Window),
Hide(WinEvent, Window),
Minimize(WinEvent, Window),
Show(WinEvent, Window),
MoveResizeStart(WinEvent, Window),
MoveResizeEnd(WinEvent, Window),
MouseCapture(WinEvent, Window),
}
impl Display for WindowManagerEvent {
fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
match self {
WindowManagerEvent::Destroy(winevent, window) => {
write!(f, "Destroy (WinEvent: {}, Window: {})", winevent, window)
}
WindowManagerEvent::FocusChange(winevent, window) => {
write!(
f,
"FocusChange (WinEvent: {}, Window: {})",
winevent, window
)
}
WindowManagerEvent::Hide(winevent, window) => {
write!(f, "Hide (WinEvent: {}, Window: {})", winevent, window)
}
WindowManagerEvent::Minimize(winevent, window) => {
write!(f, "Minimize (WinEvent: {}, Window: {})", winevent, window)
}
WindowManagerEvent::Show(winevent, window) => {
write!(f, "Show (WinEvent: {}, Window: {})", winevent, window)
}
WindowManagerEvent::MoveResizeStart(winevent, window) => {
write!(
f,
"MoveResizeStart (WinEvent: {}, Window: {})",
winevent, window
)
}
WindowManagerEvent::MoveResizeEnd(winevent, window) => {
write!(
f,
"MoveResizeEnd (WinEvent: {}, Window: {})",
winevent, window
)
}
WindowManagerEvent::MouseCapture(winevent, window) => {
write!(
f,
"MouseCapture (WinEvent: {}, Window: {})",
winevent, window
)
}
}
}
}
impl WindowManagerEvent {
pub const fn from_win_event(winevent: WinEvent, window: Window) -> Option<Self> {
match winevent {
WinEvent::ObjectDestroy => Some(Self::Destroy(winevent, window)),
WinEvent::ObjectCloaked | WinEvent::ObjectHide => Some(Self::Hide(winevent, window)),
WinEvent::SystemMinimizeStart => Some(Self::Minimize(winevent, window)),
WinEvent::ObjectShow | WinEvent::ObjectUncloaked | WinEvent::SystemMinimizeEnd => {
Some(Self::Show(winevent, window))
}
WinEvent::ObjectFocus | WinEvent::SystemForeground => {
Some(Self::FocusChange(winevent, window))
}
WinEvent::SystemMoveSizeStart => Some(Self::MoveResizeStart(winevent, window)),
WinEvent::SystemMoveSizeEnd => Some(Self::MoveResizeEnd(winevent, window)),
WinEvent::SystemCaptureStart | WinEvent::SystemCaptureEnd => {
Some(Self::MouseCapture(winevent, window))
}
_ => None,
}
}
}

470
komorebi/src/windows_api.rs Normal file
View File

@@ -0,0 +1,470 @@
use std::collections::VecDeque;
use std::convert::TryFrom;
use std::convert::TryInto;
use color_eyre::eyre::ContextCompat;
use color_eyre::Result;
use eyre::Error;
use bindings::Windows::Win32::Foundation::BOOL;
use bindings::Windows::Win32::Foundation::HANDLE;
use bindings::Windows::Win32::Foundation::HWND;
use bindings::Windows::Win32::Foundation::LPARAM;
use bindings::Windows::Win32::Foundation::POINT;
use bindings::Windows::Win32::Foundation::PWSTR;
use bindings::Windows::Win32::Graphics::Dwm::DwmGetWindowAttribute;
use bindings::Windows::Win32::Graphics::Dwm::DWMWA_CLOAKED;
use bindings::Windows::Win32::Graphics::Dwm::DWM_CLOAKED_APP;
use bindings::Windows::Win32::Graphics::Dwm::DWM_CLOAKED_INHERITED;
use bindings::Windows::Win32::Graphics::Dwm::DWM_CLOAKED_SHELL;
use bindings::Windows::Win32::Graphics::Gdi::EnumDisplayMonitors;
use bindings::Windows::Win32::Graphics::Gdi::MonitorFromWindow;
use bindings::Windows::Win32::Graphics::Gdi::HDC;
use bindings::Windows::Win32::Graphics::Gdi::MONITORENUMPROC;
use bindings::Windows::Win32::Graphics::Gdi::MONITOR_DEFAULTTONEAREST;
use bindings::Windows::Win32::Graphics::Gdi::{GetMonitorInfoW, HMONITOR, MONITORINFO};
use bindings::Windows::Win32::System::Threading::AttachThreadInput;
use bindings::Windows::Win32::System::Threading::GetCurrentProcessId;
use bindings::Windows::Win32::System::Threading::GetCurrentThreadId;
use bindings::Windows::Win32::System::Threading::OpenProcess;
use bindings::Windows::Win32::System::Threading::QueryFullProcessImageNameW;
use bindings::Windows::Win32::System::Threading::PROCESS_ACCESS_RIGHTS;
use bindings::Windows::Win32::System::Threading::PROCESS_NAME_FORMAT;
use bindings::Windows::Win32::System::Threading::PROCESS_QUERY_INFORMATION;
use bindings::Windows::Win32::UI::KeyboardAndMouseInput::SetFocus;
use bindings::Windows::Win32::UI::WindowsAndMessaging::AllowSetForegroundWindow;
use bindings::Windows::Win32::UI::WindowsAndMessaging::EnumWindows;
use bindings::Windows::Win32::UI::WindowsAndMessaging::GetCursorPos;
use bindings::Windows::Win32::UI::WindowsAndMessaging::GetForegroundWindow;
use bindings::Windows::Win32::UI::WindowsAndMessaging::GetWindowLongPtrW;
use bindings::Windows::Win32::UI::WindowsAndMessaging::GetWindowRect;
use bindings::Windows::Win32::UI::WindowsAndMessaging::GetWindowTextW;
use bindings::Windows::Win32::UI::WindowsAndMessaging::GetWindowThreadProcessId;
use bindings::Windows::Win32::UI::WindowsAndMessaging::IsIconic;
use bindings::Windows::Win32::UI::WindowsAndMessaging::IsWindow;
use bindings::Windows::Win32::UI::WindowsAndMessaging::IsWindowVisible;
use bindings::Windows::Win32::UI::WindowsAndMessaging::RealGetWindowClassW;
use bindings::Windows::Win32::UI::WindowsAndMessaging::SetCursorPos;
use bindings::Windows::Win32::UI::WindowsAndMessaging::SetForegroundWindow;
use bindings::Windows::Win32::UI::WindowsAndMessaging::SetWindowLongPtrW;
use bindings::Windows::Win32::UI::WindowsAndMessaging::SetWindowPos;
use bindings::Windows::Win32::UI::WindowsAndMessaging::ShowWindow;
use bindings::Windows::Win32::UI::WindowsAndMessaging::GWL_EXSTYLE;
use bindings::Windows::Win32::UI::WindowsAndMessaging::GWL_STYLE;
use bindings::Windows::Win32::UI::WindowsAndMessaging::HWND_NOTOPMOST;
use bindings::Windows::Win32::UI::WindowsAndMessaging::SHOW_WINDOW_CMD;
use bindings::Windows::Win32::UI::WindowsAndMessaging::SWP_NOACTIVATE;
use bindings::Windows::Win32::UI::WindowsAndMessaging::SW_HIDE;
use bindings::Windows::Win32::UI::WindowsAndMessaging::SW_RESTORE;
use bindings::Windows::Win32::UI::WindowsAndMessaging::WINDOW_LONG_PTR_INDEX;
use bindings::Windows::Win32::UI::WindowsAndMessaging::WNDENUMPROC;
use komorebi_core::Rect;
use crate::container::Container;
use crate::monitor::Monitor;
use crate::ring::Ring;
use crate::workspace::Workspace;
use crate::{monitor, windows_callbacks};
pub enum WindowsResult<T, E> {
Err(E),
Ok(T),
}
impl From<BOOL> for WindowsResult<(), Error> {
fn from(return_value: BOOL) -> Self {
if return_value.as_bool() {
Self::Ok(())
} else {
Self::Err(std::io::Error::last_os_error().into())
}
}
}
impl From<HWND> for WindowsResult<isize, Error> {
fn from(return_value: HWND) -> Self {
if return_value.is_null() {
Self::Err(std::io::Error::last_os_error().into())
} else {
Self::Ok(return_value.0)
}
}
}
impl From<HANDLE> for WindowsResult<HANDLE, Error> {
fn from(return_value: HANDLE) -> Self {
if return_value.is_null() {
Self::Err(std::io::Error::last_os_error().into())
} else {
Self::Ok(return_value)
}
}
}
impl From<isize> for WindowsResult<isize, Error> {
fn from(return_value: isize) -> Self {
match return_value {
0 => Self::Err(std::io::Error::last_os_error().into()),
_ => Self::Ok(return_value),
}
}
}
impl From<u32> for WindowsResult<u32, Error> {
fn from(return_value: u32) -> Self {
match return_value {
0 => Self::Err(std::io::Error::last_os_error().into()),
_ => Self::Ok(return_value),
}
}
}
impl From<i32> for WindowsResult<i32, Error> {
fn from(return_value: i32) -> Self {
match return_value {
0 => Self::Err(std::io::Error::last_os_error().into()),
_ => Self::Ok(return_value),
}
}
}
impl<T, E> From<WindowsResult<T, E>> for Result<T, E> {
fn from(result: WindowsResult<T, E>) -> Self {
match result {
WindowsResult::Err(error) => Self::Err(error),
WindowsResult::Ok(ok) => Self::Ok(ok),
}
}
}
pub struct WindowsApi;
impl WindowsApi {
pub fn enum_display_monitors(
callback: MONITORENUMPROC,
callback_data_address: isize,
) -> Result<()> {
Result::from(WindowsResult::from(unsafe {
EnumDisplayMonitors(
HDC(0),
std::ptr::null_mut(),
Option::from(callback),
LPARAM(callback_data_address),
)
}))
}
pub fn load_monitor_information(monitors: &mut Ring<Monitor>) -> Result<()> {
Self::enum_display_monitors(
windows_callbacks::enum_display_monitor,
monitors as *mut Ring<Monitor> as isize,
)
}
pub fn enum_windows(callback: WNDENUMPROC, callback_data_address: isize) -> Result<()> {
Result::from(WindowsResult::from(unsafe {
EnumWindows(Option::from(callback), LPARAM(callback_data_address))
}))
}
pub fn load_workspace_information(monitors: &mut Ring<Monitor>) -> Result<()> {
for monitor in monitors.elements_mut() {
if monitor.workspaces().is_empty() {
let mut workspace = Workspace::default();
// EnumWindows will enumerate through windows on all monitors
Self::enum_windows(
windows_callbacks::enum_window,
workspace.containers_mut() as *mut VecDeque<Container> as isize,
)?;
// So we have to prune each monitor's primary workspace of undesired windows here
let mut windows_on_other_monitors = vec![];
for container in workspace.containers_mut() {
for window in container.windows() {
if Self::monitor_from_window(window.hwnd()) != monitor.id() {
windows_on_other_monitors.push(window.hwnd().0);
}
}
}
for hwnd in windows_on_other_monitors {
workspace.remove_window(hwnd)?;
}
monitor.workspaces_mut().push_back(workspace);
}
}
Ok(())
}
pub fn allow_set_foreground_window(process_id: u32) -> Result<()> {
Result::from(WindowsResult::from(unsafe {
AllowSetForegroundWindow(process_id)
}))
}
pub fn monitor_from_window(hwnd: HWND) -> isize {
// MONITOR_DEFAULTTONEAREST ensures that the return value will never be NULL
// https://docs.microsoft.com/en-us/windows/win32/api/winuser/nf-winuser-monitorfromwindow
unsafe { MonitorFromWindow(hwnd, MONITOR_DEFAULTTONEAREST) }.0
}
pub fn set_window_pos(hwnd: HWND, layout: &Rect) -> Result<()> {
Result::from(WindowsResult::from(unsafe {
SetWindowPos(
hwnd,
HWND_NOTOPMOST,
layout.left,
layout.top,
layout.right,
layout.bottom,
SWP_NOACTIVATE,
)
}))
}
fn show_window(hwnd: HWND, command: SHOW_WINDOW_CMD) {
// BOOL is returned but does not signify whether or not the operation was succesful
// https://docs.microsoft.com/en-us/windows/win32/api/winuser/nf-winuser-showwindow
unsafe { ShowWindow(hwnd, command) };
}
pub fn hide_window(hwnd: HWND) {
Self::show_window(hwnd, SW_HIDE);
}
pub fn restore_window(hwnd: HWND) {
Self::show_window(hwnd, SW_RESTORE);
}
pub fn set_foreground_window(hwnd: HWND) -> Result<()> {
match WindowsResult::from(unsafe { SetForegroundWindow(hwnd) }) {
WindowsResult::Ok(_) => Ok(()),
WindowsResult::Err(error) => {
// TODO: Figure out the odd behaviour here, docs state that a zero value means
// TODO: that the window was not brought to the foreground, but this contradicts
// TODO: the behaviour that I have observed which resulted in this check
if error.to_string() == "The operation completed successfully. (os error 0)" {
Ok(())
} else {
Err(error)
}
}
}
}
pub fn foreground_window() -> Result<isize> {
Result::from(WindowsResult::from(unsafe { GetForegroundWindow().0 }))
}
pub fn window_rect(hwnd: HWND) -> Result<Rect> {
let mut rect = unsafe { std::mem::zeroed() };
Result::from(WindowsResult::from(unsafe {
GetWindowRect(hwnd, &mut rect)
}))?;
Ok(Rect::from(rect))
}
fn set_cursor_pos(x: i32, y: i32) -> Result<()> {
Result::from(WindowsResult::from(unsafe { SetCursorPos(x, y) }))
}
pub fn cursor_pos() -> Result<POINT> {
let mut cursor_pos: POINT = unsafe { std::mem::zeroed() };
Result::from(WindowsResult::from(unsafe {
GetCursorPos(&mut cursor_pos)
}))?;
Ok(cursor_pos)
}
pub fn center_cursor_in_rect(rect: &Rect) -> Result<()> {
Self::set_cursor_pos(rect.left + (rect.right / 2), rect.top + (rect.bottom / 2))
}
pub fn window_thread_process_id(hwnd: HWND) -> (u32, u32) {
let mut process_id: u32 = 0;
// Behaviour is undefined if an invalid HWND is given
// https://docs.microsoft.com/en-us/windows/win32/api/winuser/nf-winuser-getwindowthreadprocessid
let thread_id = unsafe { GetWindowThreadProcessId(hwnd, &mut process_id) };
(process_id, thread_id)
}
pub fn current_thread_id() -> u32 {
unsafe { GetCurrentThreadId() }
}
pub fn current_process_id() -> u32 {
unsafe { GetCurrentProcessId() }
}
pub fn attach_thread_input(thread_id: u32, target_thread_id: u32, attach: bool) -> Result<()> {
Result::from(WindowsResult::from(unsafe {
AttachThreadInput(thread_id, target_thread_id, attach)
}))
}
pub fn set_focus(hwnd: HWND) -> Result<()> {
match WindowsResult::from(unsafe { SetFocus(hwnd) }) {
WindowsResult::Ok(_) => Ok(()),
WindowsResult::Err(error) => {
// If the window is not attached to the calling thread's message queue, the return value is NULL
// https://docs.microsoft.com/en-us/windows/win32/api/winuser/nf-winuser-setfocus
if error.to_string() == "The operation completed successfully. (os error 0)" {
Ok(())
} else {
Err(error)
}
}
}
}
fn set_window_long_ptr_w(
hwnd: HWND,
index: WINDOW_LONG_PTR_INDEX,
new_value: isize,
) -> Result<()> {
Result::from(WindowsResult::from(unsafe {
SetWindowLongPtrW(hwnd, index, new_value)
}))
.map(|_| {})
}
pub fn gwl_style(hwnd: HWND) -> Result<isize> {
Self::window_long_ptr_w(hwnd, GWL_STYLE)
}
pub fn gwl_ex_style(hwnd: HWND) -> Result<isize> {
Self::window_long_ptr_w(hwnd, GWL_EXSTYLE)
}
fn window_long_ptr_w(hwnd: HWND, index: WINDOW_LONG_PTR_INDEX) -> Result<isize> {
Result::from(WindowsResult::from(unsafe {
GetWindowLongPtrW(hwnd, index)
}))
}
pub fn update_style(hwnd: HWND, new_value: isize) -> Result<()> {
Self::set_window_long_ptr_w(hwnd, GWL_STYLE, new_value)
}
pub fn window_text_w(hwnd: HWND) -> Result<String> {
let mut text: [u16; 512] = [0; 512];
match WindowsResult::from(unsafe {
GetWindowTextW(hwnd, PWSTR(text.as_mut_ptr()), text.len().try_into()?)
}) {
WindowsResult::Ok(len) => {
let length = usize::try_from(len)?;
Ok(String::from_utf16(&text[..length])?)
}
WindowsResult::Err(error) => Err(error),
}
}
fn open_process(
access_rights: PROCESS_ACCESS_RIGHTS,
inherit_handle: bool,
process_id: u32,
) -> Result<HANDLE> {
Result::from(WindowsResult::from(unsafe {
OpenProcess(access_rights, inherit_handle, process_id)
}))
}
pub fn process_handle(process_id: u32) -> Result<HANDLE> {
Self::open_process(PROCESS_QUERY_INFORMATION, false, process_id)
}
pub fn exe_path(handle: HANDLE) -> Result<String> {
let mut len = 260_u32;
let mut path: Vec<u16> = vec![0; len as usize];
let text_ptr = path.as_mut_ptr();
Result::from(WindowsResult::from(unsafe {
QueryFullProcessImageNameW(
handle,
PROCESS_NAME_FORMAT(0),
PWSTR(text_ptr),
&mut len as *mut u32,
)
}))?;
Ok(String::from_utf16(&path[..len as usize])?)
}
pub fn exe(handle: HANDLE) -> Result<String> {
Ok(Self::exe_path(handle)?
.split('\\')
.last()
.context("there is no last element")?
.to_string())
}
pub fn real_window_class_w(hwnd: HWND) -> Result<String> {
const BUF_SIZE: usize = 512;
let mut class: [u16; BUF_SIZE] = [0; BUF_SIZE];
let len = Result::from(WindowsResult::from(unsafe {
RealGetWindowClassW(hwnd, PWSTR(class.as_mut_ptr()), u32::try_from(BUF_SIZE)?)
}))?;
Ok(String::from_utf16(&class[0..len as usize])?)
}
pub fn is_window_cloaked(hwnd: HWND) -> Result<bool> {
let mut cloaked: u32 = 0;
unsafe {
DwmGetWindowAttribute(
hwnd,
std::mem::transmute::<_, u32>(DWMWA_CLOAKED),
(&mut cloaked as *mut u32).cast(),
u32::try_from(std::mem::size_of::<u32>())?,
)?;
}
Ok(matches!(
cloaked,
DWM_CLOAKED_APP | DWM_CLOAKED_SHELL | DWM_CLOAKED_INHERITED
))
}
pub fn is_window(hwnd: HWND) -> bool {
unsafe { IsWindow(hwnd) }.into()
}
pub fn is_window_visible(hwnd: HWND) -> bool {
unsafe { IsWindowVisible(hwnd) }.into()
}
pub fn is_iconic(hwnd: HWND) -> bool {
unsafe { IsIconic(hwnd) }.into()
}
pub fn monitor_info_w(hmonitor: HMONITOR) -> Result<MONITORINFO> {
let mut monitor_info: MONITORINFO = unsafe { std::mem::zeroed() };
monitor_info.cbSize = u32::try_from(std::mem::size_of::<MONITORINFO>())?;
Result::from(WindowsResult::from(unsafe {
GetMonitorInfoW(hmonitor, (&mut monitor_info as *mut MONITORINFO).cast())
}))?;
Ok(monitor_info)
}
pub fn monitor(hmonitor: HMONITOR) -> Result<Monitor> {
let monitor_info = Self::monitor_info_w(hmonitor)?;
Ok(monitor::new(
hmonitor.0,
monitor_info.rcMonitor.into(),
monitor_info.rcWork.into(),
))
}
}

View File

@@ -0,0 +1,125 @@
use std::collections::VecDeque;
use bindings::Windows::Win32::Foundation::BOOL;
use bindings::Windows::Win32::Foundation::HWND;
use bindings::Windows::Win32::Foundation::LPARAM;
use bindings::Windows::Win32::Foundation::RECT;
use bindings::Windows::Win32::Graphics::Gdi::HDC;
use bindings::Windows::Win32::Graphics::Gdi::HMONITOR;
use bindings::Windows::Win32::UI::Accessibility::HWINEVENTHOOK;
use crate::container::Container;
use crate::monitor::Monitor;
use crate::ring::Ring;
use crate::styles::GwlStyle;
use crate::window::Window;
use crate::window_manager_event::WindowManagerEvent;
use crate::windows_api::WindowsApi;
use crate::winevent::WinEvent;
use crate::winevent_listener::WINEVENT_CALLBACK_CHANNEL;
pub extern "system" fn enum_display_monitor(
hmonitor: HMONITOR,
_: HDC,
_: *mut RECT,
lparam: LPARAM,
) -> BOOL {
let monitors = unsafe { &mut *(lparam.0 as *mut Ring<Monitor>) };
if let Ok(m) = WindowsApi::monitor(hmonitor) {
monitors.elements_mut().push_back(m);
}
true.into()
}
pub extern "system" fn enum_window(hwnd: HWND, lparam: LPARAM) -> BOOL {
let containers = unsafe { &mut *(lparam.0 as *mut VecDeque<Container>) };
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 mut window = Window {
hwnd: hwnd.0,
original_style: GwlStyle::empty(),
};
if let Ok(style) = window.style() {
window.original_style = style;
}
if let Ok(should_manage) = window.should_manage(None) {
if should_manage {
let mut container = Container::default();
container.windows_mut().push_back(window);
containers.push_back(container);
}
}
}
true.into()
}
pub extern "system" fn win_event_hook(
_h_win_event_hook: HWINEVENTHOOK,
event: u32,
hwnd: HWND,
id_object: i32,
_id_child: i32,
_id_event_thread: u32,
_dwms_event_time: u32,
) {
// OBJID_WINDOW
if id_object != 0 {
return;
}
let mut window = Window {
hwnd: hwnd.0,
original_style: GwlStyle::empty(),
};
if let Ok(style) = window.style() {
window.original_style = style;
}
let winevent = unsafe { ::std::mem::transmute(event) };
let event_type = if let Some(event) = WindowManagerEvent::from_win_event(winevent, window) {
event
} else {
// Some apps like Firefox don't send ObjectCreate or ObjectShow on launch
// This spams the message queue, but I don't know what else to do. On launch
// it only sends the following WinEvents :/
//
// [yatta\src\windows_event.rs:110] event = 32780 ObjectNameChange
// [yatta\src\windows_event.rs:110] event = 32779 ObjectLocationChange
let object_name_change_on_launch =
vec!["firefox.exe".to_string(), "idea64.exe".to_string()];
if let Ok(exe) = window.exe() {
if winevent == WinEvent::ObjectNameChange {
if object_name_change_on_launch.contains(&exe) {
WindowManagerEvent::Show(winevent, window)
} else {
return;
}
} else {
return;
}
} else {
return;
}
};
if let Ok(should_manage) = window.should_manage(Option::from(event_type)) {
if should_manage {
WINEVENT_CALLBACK_CHANNEL
.lock()
.unwrap()
.0
.send(event_type)
.expect("could not send message on WINEVENT_CALLBACK_CHANNEL");
}
}
}

173
komorebi/src/winevent.rs Normal file
View File

@@ -0,0 +1,173 @@
use bindings::Windows::Win32::UI::WindowsAndMessaging::EVENT_AIA_END;
use bindings::Windows::Win32::UI::WindowsAndMessaging::EVENT_AIA_START;
use bindings::Windows::Win32::UI::WindowsAndMessaging::EVENT_CONSOLE_CARET;
use bindings::Windows::Win32::UI::WindowsAndMessaging::EVENT_CONSOLE_END;
use bindings::Windows::Win32::UI::WindowsAndMessaging::EVENT_CONSOLE_END_APPLICATION;
use bindings::Windows::Win32::UI::WindowsAndMessaging::EVENT_CONSOLE_LAYOUT;
use bindings::Windows::Win32::UI::WindowsAndMessaging::EVENT_CONSOLE_START_APPLICATION;
use bindings::Windows::Win32::UI::WindowsAndMessaging::EVENT_CONSOLE_UPDATE_REGION;
use bindings::Windows::Win32::UI::WindowsAndMessaging::EVENT_CONSOLE_UPDATE_SCROLL;
use bindings::Windows::Win32::UI::WindowsAndMessaging::EVENT_CONSOLE_UPDATE_SIMPLE;
use bindings::Windows::Win32::UI::WindowsAndMessaging::EVENT_OBJECT_ACCELERATORCHANGE;
use bindings::Windows::Win32::UI::WindowsAndMessaging::EVENT_OBJECT_CLOAKED;
use bindings::Windows::Win32::UI::WindowsAndMessaging::EVENT_OBJECT_CONTENTSCROLLED;
use bindings::Windows::Win32::UI::WindowsAndMessaging::EVENT_OBJECT_CREATE;
use bindings::Windows::Win32::UI::WindowsAndMessaging::EVENT_OBJECT_DEFACTIONCHANGE;
use bindings::Windows::Win32::UI::WindowsAndMessaging::EVENT_OBJECT_DESCRIPTIONCHANGE;
use bindings::Windows::Win32::UI::WindowsAndMessaging::EVENT_OBJECT_DESTROY;
use bindings::Windows::Win32::UI::WindowsAndMessaging::EVENT_OBJECT_DRAGCANCEL;
use bindings::Windows::Win32::UI::WindowsAndMessaging::EVENT_OBJECT_DRAGCOMPLETE;
use bindings::Windows::Win32::UI::WindowsAndMessaging::EVENT_OBJECT_DRAGDROPPED;
use bindings::Windows::Win32::UI::WindowsAndMessaging::EVENT_OBJECT_DRAGENTER;
use bindings::Windows::Win32::UI::WindowsAndMessaging::EVENT_OBJECT_DRAGLEAVE;
use bindings::Windows::Win32::UI::WindowsAndMessaging::EVENT_OBJECT_DRAGSTART;
use bindings::Windows::Win32::UI::WindowsAndMessaging::EVENT_OBJECT_END;
use bindings::Windows::Win32::UI::WindowsAndMessaging::EVENT_OBJECT_FOCUS;
use bindings::Windows::Win32::UI::WindowsAndMessaging::EVENT_OBJECT_HELPCHANGE;
use bindings::Windows::Win32::UI::WindowsAndMessaging::EVENT_OBJECT_HIDE;
use bindings::Windows::Win32::UI::WindowsAndMessaging::EVENT_OBJECT_HOSTEDOBJECTSINVALIDATED;
use bindings::Windows::Win32::UI::WindowsAndMessaging::EVENT_OBJECT_IME_CHANGE;
use bindings::Windows::Win32::UI::WindowsAndMessaging::EVENT_OBJECT_IME_HIDE;
use bindings::Windows::Win32::UI::WindowsAndMessaging::EVENT_OBJECT_IME_SHOW;
use bindings::Windows::Win32::UI::WindowsAndMessaging::EVENT_OBJECT_INVOKED;
use bindings::Windows::Win32::UI::WindowsAndMessaging::EVENT_OBJECT_LIVEREGIONCHANGED;
use bindings::Windows::Win32::UI::WindowsAndMessaging::EVENT_OBJECT_LOCATIONCHANGE;
use bindings::Windows::Win32::UI::WindowsAndMessaging::EVENT_OBJECT_NAMECHANGE;
use bindings::Windows::Win32::UI::WindowsAndMessaging::EVENT_OBJECT_PARENTCHANGE;
use bindings::Windows::Win32::UI::WindowsAndMessaging::EVENT_OBJECT_REORDER;
use bindings::Windows::Win32::UI::WindowsAndMessaging::EVENT_OBJECT_SELECTION;
use bindings::Windows::Win32::UI::WindowsAndMessaging::EVENT_OBJECT_SELECTIONADD;
use bindings::Windows::Win32::UI::WindowsAndMessaging::EVENT_OBJECT_SELECTIONREMOVE;
use bindings::Windows::Win32::UI::WindowsAndMessaging::EVENT_OBJECT_SELECTIONWITHIN;
use bindings::Windows::Win32::UI::WindowsAndMessaging::EVENT_OBJECT_SHOW;
use bindings::Windows::Win32::UI::WindowsAndMessaging::EVENT_OBJECT_STATECHANGE;
use bindings::Windows::Win32::UI::WindowsAndMessaging::EVENT_OBJECT_TEXTEDIT_CONVERSIONTARGETCHANGED;
use bindings::Windows::Win32::UI::WindowsAndMessaging::EVENT_OBJECT_TEXTSELECTIONCHANGED;
use bindings::Windows::Win32::UI::WindowsAndMessaging::EVENT_OBJECT_UNCLOAKED;
use bindings::Windows::Win32::UI::WindowsAndMessaging::EVENT_OBJECT_VALUECHANGE;
use bindings::Windows::Win32::UI::WindowsAndMessaging::EVENT_OEM_DEFINED_END;
use bindings::Windows::Win32::UI::WindowsAndMessaging::EVENT_OEM_DEFINED_START;
use bindings::Windows::Win32::UI::WindowsAndMessaging::EVENT_SYSTEM_ALERT;
use bindings::Windows::Win32::UI::WindowsAndMessaging::EVENT_SYSTEM_ARRANGMENTPREVIEW;
use bindings::Windows::Win32::UI::WindowsAndMessaging::EVENT_SYSTEM_CAPTUREEND;
use bindings::Windows::Win32::UI::WindowsAndMessaging::EVENT_SYSTEM_CAPTURESTART;
use bindings::Windows::Win32::UI::WindowsAndMessaging::EVENT_SYSTEM_CONTEXTHELPEND;
use bindings::Windows::Win32::UI::WindowsAndMessaging::EVENT_SYSTEM_CONTEXTHELPSTART;
use bindings::Windows::Win32::UI::WindowsAndMessaging::EVENT_SYSTEM_DESKTOPSWITCH;
use bindings::Windows::Win32::UI::WindowsAndMessaging::EVENT_SYSTEM_DIALOGEND;
use bindings::Windows::Win32::UI::WindowsAndMessaging::EVENT_SYSTEM_DIALOGSTART;
use bindings::Windows::Win32::UI::WindowsAndMessaging::EVENT_SYSTEM_DRAGDROPEND;
use bindings::Windows::Win32::UI::WindowsAndMessaging::EVENT_SYSTEM_DRAGDROPSTART;
use bindings::Windows::Win32::UI::WindowsAndMessaging::EVENT_SYSTEM_END;
use bindings::Windows::Win32::UI::WindowsAndMessaging::EVENT_SYSTEM_FOREGROUND;
use bindings::Windows::Win32::UI::WindowsAndMessaging::EVENT_SYSTEM_IME_KEY_NOTIFICATION;
use bindings::Windows::Win32::UI::WindowsAndMessaging::EVENT_SYSTEM_MENUEND;
use bindings::Windows::Win32::UI::WindowsAndMessaging::EVENT_SYSTEM_MENUPOPUPEND;
use bindings::Windows::Win32::UI::WindowsAndMessaging::EVENT_SYSTEM_MENUPOPUPSTART;
use bindings::Windows::Win32::UI::WindowsAndMessaging::EVENT_SYSTEM_MENUSTART;
use bindings::Windows::Win32::UI::WindowsAndMessaging::EVENT_SYSTEM_MINIMIZEEND;
use bindings::Windows::Win32::UI::WindowsAndMessaging::EVENT_SYSTEM_MINIMIZESTART;
use bindings::Windows::Win32::UI::WindowsAndMessaging::EVENT_SYSTEM_MOVESIZEEND;
use bindings::Windows::Win32::UI::WindowsAndMessaging::EVENT_SYSTEM_MOVESIZESTART;
use bindings::Windows::Win32::UI::WindowsAndMessaging::EVENT_SYSTEM_SCROLLINGEND;
use bindings::Windows::Win32::UI::WindowsAndMessaging::EVENT_SYSTEM_SCROLLINGSTART;
use bindings::Windows::Win32::UI::WindowsAndMessaging::EVENT_SYSTEM_SOUND;
use bindings::Windows::Win32::UI::WindowsAndMessaging::EVENT_SYSTEM_SWITCHEND;
use bindings::Windows::Win32::UI::WindowsAndMessaging::EVENT_SYSTEM_SWITCHER_APPDROPPED;
use bindings::Windows::Win32::UI::WindowsAndMessaging::EVENT_SYSTEM_SWITCHER_APPGRABBED;
use bindings::Windows::Win32::UI::WindowsAndMessaging::EVENT_SYSTEM_SWITCHER_APPOVERTARGET;
use bindings::Windows::Win32::UI::WindowsAndMessaging::EVENT_SYSTEM_SWITCHER_CANCELLED;
use bindings::Windows::Win32::UI::WindowsAndMessaging::EVENT_SYSTEM_SWITCHSTART;
use bindings::Windows::Win32::UI::WindowsAndMessaging::EVENT_UIA_EVENTID_END;
use bindings::Windows::Win32::UI::WindowsAndMessaging::EVENT_UIA_EVENTID_START;
use bindings::Windows::Win32::UI::WindowsAndMessaging::EVENT_UIA_PROPID_END;
use bindings::Windows::Win32::UI::WindowsAndMessaging::EVENT_UIA_PROPID_START;
#[derive(Clone, Copy, PartialEq, Debug, strum::Display)]
#[repr(u32)]
pub enum WinEvent {
AiaEnd = EVENT_AIA_END,
AiaStart = EVENT_AIA_START,
ConsoleCaret = EVENT_CONSOLE_CARET,
ConsoleEnd = EVENT_CONSOLE_END,
ConsoleEndApplication = EVENT_CONSOLE_END_APPLICATION,
ConsoleLayout = EVENT_CONSOLE_LAYOUT,
ConsoleStartApplication = EVENT_CONSOLE_START_APPLICATION,
ConsoleUpdateRegion = EVENT_CONSOLE_UPDATE_REGION,
ConsoleUpdateScroll = EVENT_CONSOLE_UPDATE_SCROLL,
ConsoleUpdateSimple = EVENT_CONSOLE_UPDATE_SIMPLE,
ObjectAcceleratorChange = EVENT_OBJECT_ACCELERATORCHANGE,
ObjectCloaked = EVENT_OBJECT_CLOAKED,
ObjectContentScrolled = EVENT_OBJECT_CONTENTSCROLLED,
ObjectCreate = EVENT_OBJECT_CREATE,
ObjectDefActionChange = EVENT_OBJECT_DEFACTIONCHANGE,
ObjectDescriptionChange = EVENT_OBJECT_DESCRIPTIONCHANGE,
ObjectDestroy = EVENT_OBJECT_DESTROY,
ObjectDragCancel = EVENT_OBJECT_DRAGCANCEL,
ObjectDragComplete = EVENT_OBJECT_DRAGCOMPLETE,
ObjectDragDropped = EVENT_OBJECT_DRAGDROPPED,
ObjectDragEnter = EVENT_OBJECT_DRAGENTER,
ObjectDragLeave = EVENT_OBJECT_DRAGLEAVE,
ObjectDragStart = EVENT_OBJECT_DRAGSTART,
ObjectEnd = EVENT_OBJECT_END,
ObjectFocus = EVENT_OBJECT_FOCUS,
ObjectHelpChange = EVENT_OBJECT_HELPCHANGE,
ObjectHide = EVENT_OBJECT_HIDE,
ObjectHostedObjectsInvalidated = EVENT_OBJECT_HOSTEDOBJECTSINVALIDATED,
ObjectImeChange = EVENT_OBJECT_IME_CHANGE,
ObjectImeHide = EVENT_OBJECT_IME_HIDE,
ObjectImeShow = EVENT_OBJECT_IME_SHOW,
ObjectInvoked = EVENT_OBJECT_INVOKED,
ObjectLiveRegionChanged = EVENT_OBJECT_LIVEREGIONCHANGED,
ObjectLocationChange = EVENT_OBJECT_LOCATIONCHANGE,
ObjectNameChange = EVENT_OBJECT_NAMECHANGE,
ObjectParentChange = EVENT_OBJECT_PARENTCHANGE,
ObjectReorder = EVENT_OBJECT_REORDER,
ObjectSelection = EVENT_OBJECT_SELECTION,
ObjectSelectionAdd = EVENT_OBJECT_SELECTIONADD,
ObjectSelectionRemove = EVENT_OBJECT_SELECTIONREMOVE,
ObjectSelectionWithin = EVENT_OBJECT_SELECTIONWITHIN,
ObjectShow = EVENT_OBJECT_SHOW,
ObjectStateChange = EVENT_OBJECT_STATECHANGE,
ObjectTextEditConversionTargetChanged = EVENT_OBJECT_TEXTEDIT_CONVERSIONTARGETCHANGED,
ObjectTextSelectionChanged = EVENT_OBJECT_TEXTSELECTIONCHANGED,
ObjectUncloaked = EVENT_OBJECT_UNCLOAKED,
ObjectValueChange = EVENT_OBJECT_VALUECHANGE,
OemDefinedEnd = EVENT_OEM_DEFINED_END,
OemDefinedStart = EVENT_OEM_DEFINED_START,
SystemAlert = EVENT_SYSTEM_ALERT,
SystemArrangementPreview = EVENT_SYSTEM_ARRANGMENTPREVIEW,
SystemCaptureEnd = EVENT_SYSTEM_CAPTUREEND,
SystemCaptureStart = EVENT_SYSTEM_CAPTURESTART,
SystemContextHelpEnd = EVENT_SYSTEM_CONTEXTHELPEND,
SystemContextHelpStart = EVENT_SYSTEM_CONTEXTHELPSTART,
SystemDesktopSwitch = EVENT_SYSTEM_DESKTOPSWITCH,
SystemDialogEnd = EVENT_SYSTEM_DIALOGEND,
SystemDialogStart = EVENT_SYSTEM_DIALOGSTART,
SystemDragDropEnd = EVENT_SYSTEM_DRAGDROPEND,
SystemDragDropStart = EVENT_SYSTEM_DRAGDROPSTART,
SystemEnd = EVENT_SYSTEM_END,
SystemForeground = EVENT_SYSTEM_FOREGROUND,
SystemImeKeyNotification = EVENT_SYSTEM_IME_KEY_NOTIFICATION,
SystemMenuEnd = EVENT_SYSTEM_MENUEND,
SystemMenuPopupEnd = EVENT_SYSTEM_MENUPOPUPEND,
SystemMenuPopupStart = EVENT_SYSTEM_MENUPOPUPSTART,
SystemMenuStart = EVENT_SYSTEM_MENUSTART,
SystemMinimizeEnd = EVENT_SYSTEM_MINIMIZEEND,
SystemMinimizeStart = EVENT_SYSTEM_MINIMIZESTART,
SystemMoveSizeEnd = EVENT_SYSTEM_MOVESIZEEND,
SystemMoveSizeStart = EVENT_SYSTEM_MOVESIZESTART,
SystemScrollingEnd = EVENT_SYSTEM_SCROLLINGEND,
SystemScrollingStart = EVENT_SYSTEM_SCROLLINGSTART,
SystemSound = EVENT_SYSTEM_SOUND,
SystemSwitchEnd = EVENT_SYSTEM_SWITCHEND,
SystemSwitchStart = EVENT_SYSTEM_SWITCHSTART,
SystemSwitcherAppDropped = EVENT_SYSTEM_SWITCHER_APPDROPPED,
SystemSwitcherAppGrabbed = EVENT_SYSTEM_SWITCHER_APPGRABBED,
SystemSwitcherAppOverTarget = EVENT_SYSTEM_SWITCHER_APPOVERTARGET,
SystemSwitcherCancelled = EVENT_SYSTEM_SWITCHER_CANCELLED,
UiaEventIdSEnd = EVENT_UIA_EVENTID_END,
UiaEventIdStart = EVENT_UIA_EVENTID_START,
UiaPropIdSEnd = EVENT_UIA_PROPID_END,
UiaPropIdStart = EVENT_UIA_PROPID_START,
}

View File

@@ -0,0 +1,107 @@
use std::sync::atomic::AtomicIsize;
use std::sync::atomic::Ordering;
use std::sync::Arc;
use std::sync::Mutex;
use std::thread;
use std::time::Duration;
use crossbeam_channel::Receiver;
use crossbeam_channel::Sender;
use lazy_static::lazy_static;
use bindings::Windows::Win32::Foundation::HWND;
use bindings::Windows::Win32::UI::Accessibility::SetWinEventHook;
use bindings::Windows::Win32::UI::WindowsAndMessaging::DispatchMessageW;
use bindings::Windows::Win32::UI::WindowsAndMessaging::PeekMessageW;
use bindings::Windows::Win32::UI::WindowsAndMessaging::TranslateMessage;
use bindings::Windows::Win32::UI::WindowsAndMessaging::EVENT_MAX;
use bindings::Windows::Win32::UI::WindowsAndMessaging::EVENT_MIN;
use bindings::Windows::Win32::UI::WindowsAndMessaging::MSG;
use bindings::Windows::Win32::UI::WindowsAndMessaging::PM_REMOVE;
use crate::window_manager_event::WindowManagerEvent;
use crate::windows_callbacks;
lazy_static! {
pub static ref WINEVENT_CALLBACK_CHANNEL: Arc<Mutex<(Sender<WindowManagerEvent>, Receiver<WindowManagerEvent>)>> =
Arc::new(Mutex::new(crossbeam_channel::unbounded()));
}
#[derive(Debug, Clone)]
pub struct WinEventListener {
hook: Arc<AtomicIsize>,
outgoing_events: Arc<Mutex<Sender<WindowManagerEvent>>>,
}
pub fn new(outgoing: Arc<Mutex<Sender<WindowManagerEvent>>>) -> WinEventListener {
WinEventListener {
hook: Arc::new(AtomicIsize::new(0)),
outgoing_events: outgoing,
}
}
impl WinEventListener {
pub fn start(self) {
let hook = self.hook.clone();
let outgoing = self.outgoing_events.lock().unwrap().clone();
thread::spawn(move || unsafe {
let hook_ref = SetWinEventHook(
EVENT_MIN as u32,
EVENT_MAX as u32,
None,
Some(windows_callbacks::win_event_hook),
0,
0,
0,
);
hook.store(hook_ref.0, Ordering::SeqCst);
// The code in the callback doesn't work in its own loop, needs to be within
// the MessageLoop callback for the winevent callback to even fire
MessageLoop::start(10, |_msg| {
if let Ok(event) = WINEVENT_CALLBACK_CHANNEL.lock().unwrap().1.try_recv() {
match outgoing.send(event) {
Ok(_) => {}
Err(error) => {
tracing::error!("{}", error);
}
}
}
true
});
});
}
}
#[derive(Debug, Copy, Clone)]
pub struct MessageLoop;
impl MessageLoop {
pub fn start(sleep: u64, cb: impl Fn(Option<MSG>) -> bool) {
Self::start_with_sleep(sleep, cb);
}
fn start_with_sleep(sleep: u64, cb: impl Fn(Option<MSG>) -> bool) {
let mut msg: MSG = MSG::default();
loop {
let mut value: Option<MSG> = None;
unsafe {
if !bool::from(!PeekMessageW(&mut msg, HWND(0), 0, 0, PM_REMOVE)) {
TranslateMessage(&msg);
DispatchMessageW(&msg);
value = Some(msg);
}
}
thread::sleep(Duration::from_millis(sleep));
if !cb(value) {
break;
}
}
}
}

537
komorebi/src/workspace.rs Normal file
View File

@@ -0,0 +1,537 @@
use std::collections::VecDeque;
use color_eyre::eyre::ContextCompat;
use color_eyre::Result;
use komorebi_core::Layout;
use komorebi_core::LayoutFlip;
use komorebi_core::OperationDirection;
use komorebi_core::Rect;
use crate::container::Container;
use crate::ring::Ring;
use crate::window::Window;
use crate::windows_api::WindowsApi;
#[derive(Debug, Clone)]
pub struct Workspace {
name: Option<String>,
containers: Ring<Container>,
monocle_container: Option<Container>,
monocle_restore_idx: Option<usize>,
floating_windows: Vec<Window>,
layout: Layout,
layout_flip: Option<LayoutFlip>,
workspace_padding: Option<i32>,
container_padding: Option<i32>,
latest_layout: Vec<Rect>,
}
impl Default for Workspace {
fn default() -> Self {
Self {
name: None,
containers: Ring::default(),
monocle_container: None,
monocle_restore_idx: None,
floating_windows: Vec::default(),
layout: Layout::BSP,
layout_flip: None,
workspace_padding: Option::from(20),
container_padding: Option::from(5),
latest_layout: vec![],
}
}
}
impl Workspace {
pub fn hide(&mut self) {
for container in self.containers_mut() {
for window in container.windows_mut() {
window.hide();
}
}
}
pub fn restore(&mut self) {
for container in self.containers_mut() {
if let Some(window) = container.visible_window_mut() {
window.restore();
}
}
}
pub fn update(&mut self, work_area: &Rect) -> Result<()> {
let mut adjusted_work_area = work_area.clone();
adjusted_work_area.add_padding(self.workspace_padding());
if let Some(container) = self.monocle_container_mut() {
if let Some(window) = container.focused_window_mut() {
window.set_position(&adjusted_work_area)?;
window.focus()?;
}
} else {
let layouts = self.layout().calculate(
&adjusted_work_area,
self.containers().len(),
self.container_padding(),
self.layout_flip(),
);
let windows = self.visible_windows_mut();
for (i, window) in windows.into_iter().enumerate() {
if let (Some(window), Some(layout)) = (window, layouts.get(i)) {
window.set_position(layout)?;
}
}
self.set_latest_layout(layouts);
}
Ok(())
}
pub fn reap_orphans(&mut self) -> Result<(usize, usize)> {
let mut hwnds = vec![];
for window in self.visible_windows_mut().into_iter().flatten() {
if !window.is_window() {
hwnds.push(window.hwnd);
}
}
for hwnd in &hwnds {
tracing::debug!("reaping hwnd: {}", hwnd);
self.remove_window(*hwnd)?;
}
let mut container_ids = vec![];
for container in self.containers() {
if container.windows().is_empty() {
container_ids.push(container.id().clone());
}
}
self.containers_mut()
.retain(|c| !container_ids.contains(c.id()));
Ok((hwnds.len(), container_ids.len()))
}
pub fn focus_container_by_window(&mut self, hwnd: isize) -> Result<()> {
let container_idx = self
.container_idx_for_window(hwnd)
.context("there is no container/window")?;
let container = self
.containers_mut()
.get_mut(container_idx)
.context("there is no container")?;
let window_idx = container
.idx_for_window(hwnd)
.context("there is no window")?;
container.focus_window(window_idx);
self.focus_container(container_idx);
Ok(())
}
pub fn container_idx_from_current_point(&self) -> Option<usize> {
let mut idx = None;
let point = WindowsApi::cursor_pos().ok()?;
for (i, _container) in self.containers().iter().enumerate() {
if let Some(rect) = self.latest_layout().get(i) {
if rect.contains_point((point.x, point.y)) {
idx = Option::from(i);
}
}
}
idx
}
pub fn contains_window(&self, hwnd: isize) -> bool {
for x in self.containers() {
if x.contains_window(hwnd) {
return true;
}
}
false
}
pub fn promote_container(&mut self) -> Result<()> {
let container = self
.remove_focused_container()
.context("there is no container")?;
self.containers_mut().push_front(container);
self.focus_container(0);
Ok(())
}
pub fn add_container(&mut self, container: Container) {
self.containers_mut().push_back(container);
self.focus_container(self.containers().len() - 1);
}
fn remove_container_by_idx(&mut self, idx: usize) -> Option<Container> {
self.containers_mut().remove(idx)
}
fn container_idx_for_window(&mut self, hwnd: isize) -> Option<usize> {
let mut idx = None;
for (i, x) in self.containers().iter().enumerate() {
if x.contains_window(hwnd) {
idx = Option::from(i);
}
}
idx
}
pub fn remove_window(&mut self, hwnd: isize) -> Result<()> {
let container_idx = self
.container_idx_for_window(hwnd)
.context("there is no window")?;
let container = self
.containers_mut()
.get_mut(container_idx)
.context("there is no container")?;
let window_idx = container
.windows()
.iter()
.position(|window| window.hwnd == hwnd)
.context("there is no window")?;
container
.remove_window_by_idx(window_idx)
.context("there is no window")?;
if container.windows().is_empty() {
self.containers_mut()
.remove(container_idx)
.context("there is no container")?;
}
if container_idx != 0 {
self.focus_container(container_idx - 1);
}
Ok(())
}
pub fn remove_focused_container(&mut self) -> Option<Container> {
let focused_idx = self.focused_container_idx();
let container = self.remove_container_by_idx(focused_idx);
if focused_idx != 0 {
self.focus_container(focused_idx - 1);
}
container
}
pub fn new_idx_for_direction(&self, direction: OperationDirection) -> Option<usize> {
if direction.is_valid(
self.layout,
self.focused_container_idx(),
self.containers().len(),
) {
Option::from(direction.new_idx(self.layout, self.containers.focused_idx()))
} else {
None
}
}
pub fn move_window_to_container(&mut self, target_container_idx: usize) -> Result<()> {
let focused_idx = self.focused_container_idx();
let container = self
.focused_container_mut()
.context("there is no container")?;
let window = container
.remove_focused_window()
.context("there is no window")?;
// This is a little messy
let adjusted_target_container_index = if container.windows().is_empty() {
self.containers_mut().remove(focused_idx);
if focused_idx < target_container_idx {
target_container_idx - 1
} else {
target_container_idx
}
} else {
container.load_focused_window();
target_container_idx
};
let target_container = self
.containers_mut()
.get_mut(adjusted_target_container_index)
.context("there is no container")?;
target_container.add_window(window);
self.focus_container(adjusted_target_container_index);
self.focused_container_mut()
.context("there is no container")?
.load_focused_window();
Ok(())
}
pub fn new_container_for_focused_window(&mut self) -> Result<()> {
let focused_container_idx = self.focused_container_idx();
let container = self
.focused_container_mut()
.context("there is no container")?;
let window = container
.remove_focused_window()
.context("there is no window")?;
if container.windows().is_empty() {
self.containers_mut().remove(focused_container_idx);
} else {
container.load_focused_window();
}
self.new_container_for_window(window);
let mut container = Container::default();
container.add_window(window);
Ok(())
}
pub fn new_container_for_floating_window(&mut self) -> Result<()> {
let focused_idx = self.focused_container_idx();
let window = self
.remove_focused_floating_window()
.context("there is no floating window")?;
let mut container = Container::default();
container.add_window(window);
self.containers_mut().insert(focused_idx, container);
Ok(())
}
pub fn new_container_for_window(&mut self, window: Window) {
let focused_idx = self.focused_container_idx();
let len = self.containers().len();
let mut container = Container::default();
container.add_window(window);
if focused_idx == len - 1 {
self.containers_mut().resize(len, Container::default());
}
self.containers_mut().insert(focused_idx + 1, container);
self.focus_container(focused_idx + 1);
}
pub fn new_floating_window(&mut self) -> Result<()> {
let focused_idx = self.focused_container_idx();
let container = self
.focused_container_mut()
.context("there is no container")?;
let window = container
.remove_focused_window()
.context("there is no window")?;
if container.windows().is_empty() {
self.containers_mut().remove(focused_idx);
} else {
container.load_focused_window();
}
self.floating_windows_mut().push(window);
Ok(())
}
pub fn new_monocle_container(&mut self) -> Result<()> {
let focused_idx = self.focused_container_idx();
let container = self
.containers_mut()
.remove(focused_idx)
.context("there is not container")?;
self.monocle_container = Option::from(container);
self.monocle_restore_idx = Option::from(focused_idx);
if focused_idx != 0 {
self.focus_container(focused_idx - 1);
}
self.monocle_container_mut()
.context("there is no monocle container")?
.load_focused_window();
Ok(())
}
pub fn reintegrate_monocle_container(&mut self) -> Result<()> {
let restore_idx = self
.monocle_restore_idx()
.context("there is no monocle restore index")?;
let container = self
.monocle_container_mut()
.context("there is no monocle container")?;
let container = container.clone();
if restore_idx > self.containers().len() - 1 {
self.containers_mut()
.resize(restore_idx, Container::default());
}
self.containers_mut().insert(restore_idx, container);
self.focus_container(restore_idx);
self.focused_container_mut()
.context("there is no container")?
.load_focused_window();
self.monocle_container = None;
Ok(())
}
pub const fn monocle_container(&self) -> Option<&Container> {
self.monocle_container.as_ref()
}
pub fn monocle_container_mut(&mut self) -> Option<&mut Container> {
self.monocle_container.as_mut()
}
pub const fn monocle_restore_idx(&self) -> Option<usize> {
self.monocle_restore_idx
}
pub fn focused_container(&self) -> Option<&Container> {
self.containers.focused()
}
pub const fn focused_container_idx(&self) -> usize {
self.containers.focused_idx()
}
pub fn focused_container_mut(&mut self) -> Option<&mut Container> {
self.containers.focused_mut()
}
pub fn focus_container(&mut self, idx: usize) {
tracing::info!("focusing container at index: {}", idx);
self.containers.focus(idx);
}
pub const fn containers(&self) -> &VecDeque<Container> {
self.containers.elements()
}
pub fn containers_mut(&mut self) -> &mut VecDeque<Container> {
self.containers.elements_mut()
}
pub fn swap_containers(&mut self, i: usize, j: usize) {
tracing::info!("swapping containers: {}, {}", i, j);
self.containers.swap(i, j);
self.focus_container(j);
}
pub fn remove_focused_floating_window(&mut self) -> Option<Window> {
let hwnd = WindowsApi::foreground_window().ok()?;
let mut idx = None;
for (i, window) in self.floating_windows.iter().enumerate() {
if hwnd == window.hwnd {
idx = Option::from(i);
}
}
match idx {
None => None,
Some(idx) => {
if self.floating_windows.get(idx).is_some() {
Option::from(self.floating_windows_mut().remove(idx))
} else {
None
}
}
}
}
pub const fn floating_windows(&self) -> &Vec<Window> {
&self.floating_windows
}
pub fn floating_windows_mut(&mut self) -> &mut Vec<Window> {
self.floating_windows.as_mut()
}
pub fn visible_windows_mut(&mut self) -> Vec<Option<&mut Window>> {
let mut vec = vec![];
for container in self.containers_mut() {
vec.push(container.visible_window_mut());
}
vec
}
pub const fn layout(&self) -> Layout {
self.layout
}
pub const fn layout_flip(&self) -> Option<LayoutFlip> {
self.layout_flip
}
pub const fn workspace_padding(&self) -> Option<i32> {
self.workspace_padding
}
pub const fn container_padding(&self) -> Option<i32> {
self.container_padding
}
pub const fn latest_layout(&self) -> &Vec<Rect> {
&self.latest_layout
}
pub fn set_name(&mut self, name: Option<String>) {
self.name = name;
}
pub fn set_layout(&mut self, layout: Layout) {
self.layout = layout;
}
pub fn set_layout_flip(&mut self, layout_flip: Option<LayoutFlip>) {
self.layout_flip = layout_flip;
}
pub fn set_workspace_padding(&mut self, padding: Option<i32>) {
self.workspace_padding = padding;
}
pub fn set_container_padding(&mut self, padding: Option<i32>) {
self.container_padding = padding;
}
pub fn set_latest_layout(&mut self, layout: Vec<Rect>) {
self.latest_layout = layout;
}
}

15
komorebic/Cargo.toml Normal file
View File

@@ -0,0 +1,15 @@
[package]
name = "komorebic"
version = "0.1.0"
edition = "2018"
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
[dependencies]
komorebi-core = { path = "../komorebi-core" }
clap = "3.0.0-beta.2"
dirs = "3"
powershell_script = "0.1.5"
uds_windows = "1"
color-eyre = "0.5.11"

247
komorebic/src/main.rs Normal file
View File

@@ -0,0 +1,247 @@
use std::io::Write;
use clap::Clap;
use color_eyre::Result;
use uds_windows::UnixStream;
use komorebi_core::CycleDirection;
use komorebi_core::Layout;
use komorebi_core::LayoutFlip;
use komorebi_core::OperationDirection;
use komorebi_core::Sizing;
use komorebi_core::SocketMessage;
#[derive(Clap)]
#[clap(version = "1.0", author = "Jade Iqbal <jadeiqbal@fastmail.com>")]
struct Opts {
#[clap(subcommand)]
subcmd: SubCommand,
}
#[derive(Clap)]
enum SubCommand {
Focus(OperationDirection),
Move(OperationDirection),
Stack(OperationDirection),
Unstack,
CycleStack(CycleDirection),
MoveToMonitor(Target),
MoveToWorkspace(Target),
FocusMonitor(Target),
FocusWorkspace(Target),
Promote,
Retile,
ContainerPadding(SizeForMonitorWorkspace),
WorkspacePadding(SizeForMonitorWorkspace),
WorkspaceName(NameForMonitorWorkspace),
ToggleFloat,
TogglePause,
ToggleMonocle,
Start,
Stop,
FloatClass(FloatTarget),
FloatExe(FloatTarget),
FloatTitle(FloatTarget),
AdjustContainerPadding(SizingAdjustment),
AdjustWorkspacePadding(SizingAdjustment),
FlipLayout(LayoutFlip),
Layout(LayoutForMonitorWorkspace),
}
#[derive(Clap)]
struct SizeForMonitorWorkspace {
monitor: usize,
workspace: usize,
size: i32,
}
#[derive(Clap)]
struct NameForMonitorWorkspace {
monitor: usize,
workspace: usize,
value: String,
}
#[derive(Clap)]
struct LayoutForMonitorWorkspace {
monitor: usize,
workspace: usize,
layout: Layout,
}
#[derive(Clap)]
struct Target {
number: usize,
}
#[derive(Clap)]
struct SizingAdjustment {
sizing: Sizing,
adjustment: i32,
}
#[derive(Clap)]
struct FloatTarget {
id: String,
}
pub fn send_message(bytes: &[u8]) {
let mut socket = dirs::home_dir().unwrap();
socket.push("komorebi.sock");
let socket = socket.as_path();
let mut stream = match UnixStream::connect(&socket) {
Err(_) => panic!("server is not running"),
Ok(stream) => stream,
};
if stream.write_all(&*bytes).is_err() {
panic!("couldn't send message")
}
}
fn main() -> Result<()> {
let opts: Opts = Opts::parse();
match opts.subcmd {
SubCommand::Focus(direction) => {
let bytes = SocketMessage::FocusWindow(direction).as_bytes()?;
send_message(&*bytes);
}
SubCommand::Promote => {
let bytes = SocketMessage::Promote.as_bytes()?;
send_message(&*bytes);
}
SubCommand::TogglePause => {
let bytes = SocketMessage::TogglePause.as_bytes()?;
send_message(&*bytes);
}
SubCommand::Retile => {
let bytes = SocketMessage::Retile.as_bytes()?;
send_message(&*bytes);
}
SubCommand::Move(direction) => {
let bytes = SocketMessage::MoveWindow(direction).as_bytes()?;
send_message(&*bytes);
}
SubCommand::MoveToMonitor(display) => {
let bytes = SocketMessage::MoveContainerToMonitorNumber(display.number)
.as_bytes()
.unwrap();
send_message(&*bytes);
}
SubCommand::MoveToWorkspace(workspace) => {
let bytes = SocketMessage::MoveContainerToWorkspaceNumber(workspace.number)
.as_bytes()
.unwrap();
send_message(&*bytes);
}
SubCommand::ContainerPadding(gap) => {
let bytes = SocketMessage::ContainerPadding(gap.monitor, gap.workspace, gap.size)
.as_bytes()
.unwrap();
send_message(&*bytes);
}
SubCommand::WorkspacePadding(gap) => {
let bytes = SocketMessage::WorkspacePadding(gap.monitor, gap.workspace, gap.size)
.as_bytes()
.unwrap();
send_message(&*bytes);
}
SubCommand::AdjustWorkspacePadding(sizing_adjustment) => {
let bytes = SocketMessage::AdjustWorkspacePadding(
sizing_adjustment.sizing,
sizing_adjustment.adjustment,
)
.as_bytes()
.unwrap();
send_message(&*bytes);
}
SubCommand::AdjustContainerPadding(sizing_adjustment) => {
let bytes = SocketMessage::AdjustContainerPadding(
sizing_adjustment.sizing,
sizing_adjustment.adjustment,
)
.as_bytes()
.unwrap();
send_message(&*bytes);
}
SubCommand::ToggleFloat => {
let bytes = SocketMessage::ToggleFloat.as_bytes()?;
send_message(&*bytes);
}
SubCommand::ToggleMonocle => {
let bytes = SocketMessage::ToggleMonocle.as_bytes()?;
send_message(&*bytes);
}
SubCommand::Layout(layout) => {
let bytes = SocketMessage::SetLayout(layout.monitor, layout.workspace, layout.layout)
.as_bytes()
.unwrap();
send_message(&*bytes);
}
SubCommand::Start => {
let script = r#"Start-Process komorebi -WindowStyle hidden"#;
match powershell_script::run(script, true) {
Ok(output) => {
println!("{}", output);
}
Err(e) => {
println!("Error: {}", e);
}
}
}
SubCommand::Stop => {
let bytes = SocketMessage::Stop.as_bytes()?;
send_message(&*bytes);
}
SubCommand::FloatClass(target) => {
let bytes = SocketMessage::FloatClass(target.id).as_bytes()?;
send_message(&*bytes);
}
SubCommand::FloatExe(target) => {
let bytes = SocketMessage::FloatExe(target.id).as_bytes()?;
send_message(&*bytes);
}
SubCommand::FloatTitle(target) => {
let bytes = SocketMessage::FloatTitle(target.id).as_bytes()?;
send_message(&*bytes);
}
SubCommand::Stack(direction) => {
let bytes = SocketMessage::StackWindow(direction).as_bytes()?;
send_message(&*bytes);
}
SubCommand::Unstack => {
let bytes = SocketMessage::UnstackWindow.as_bytes()?;
send_message(&*bytes);
}
SubCommand::CycleStack(direction) => {
let bytes = SocketMessage::CycleStack(direction).as_bytes()?;
send_message(&*bytes);
}
SubCommand::FlipLayout(flip) => {
let bytes = SocketMessage::FlipLayout(flip).as_bytes()?;
send_message(&*bytes);
}
SubCommand::FocusMonitor(target) => {
let bytes = SocketMessage::FocusMonitorNumber(target.number)
.as_bytes()
.unwrap();
send_message(&*bytes);
}
SubCommand::FocusWorkspace(target) => {
let bytes = SocketMessage::FocusWorkspaceNumber(target.number)
.as_bytes()
.unwrap();
send_message(&*bytes);
}
SubCommand::WorkspaceName(name) => {
let bytes = SocketMessage::WorkspaceName(name.monitor, name.workspace, name.value)
.as_bytes()
.unwrap();
send_message(&*bytes);
}
}
Ok(())
}