Compare commits

...

16 Commits

Author SHA1 Message Date
LGUG2Z
f36926cdb1 feat(borders): wip 2024-05-10 22:17:51 -07:00
LGUG2Z
c47cf4718b docs(mkdocs): add ahk exe troubleshooting section 2024-05-10 14:50:41 -07:00
LGUG2Z
1accbf65ca docs(mkdocs): add shell.nix for python doc deps 2024-05-10 14:39:41 -07:00
0x3bb
7ee3c928d8 docs(mkdocs): addressing display suspension within monitor OSD 2024-05-10 14:39:41 -07:00
0x3bb
6d1903099a docs(mkdocs): add pymdownx.highlight and pymdownx.superfences extensions
Used for syntax highlighting in code blocks, and allows for indenting existing code blocks.

References:
- https://squidfunk.github.io/mkdocs-material/setup/extensions/python-markdown-extensions/#highlight
- https://squidfunk.github.io/mkdocs-material/setup/extensions/python-markdown-extensions/#superfences
2024-05-10 14:39:41 -07:00
0x3bb
d5c6f090cc docs(mkdocs): add admonition extension
Used for bringing attention to troubleshooting steps in komorebi documentation.

Reference: https://squidfunk.github.io/mkdocs-material/reference/admonitions/
2024-05-10 14:39:41 -07:00
LGUG2Z
598f9ec0aa fix(wm): avoid dupes when following links
When adding selective handling of Uncloak events, a regression was
introduced where, for example, when clicking a link from Discord on
Workspace 2, a Firefox instance on Workspace 1 would be moved to
Workspace 2 to open the link, but when moving back to Workspace 1, a
ghost tile would be left, and the Firefox instance would be duplicated
across two workspaces.

This commit fixes this regression and makes the handler a bit easier to
reason about while also removing unnecessary early return statements
which prevent notifcations from getting sent to subscribers.
2024-05-09 20:10:38 -07:00
LGUG2Z
11acff5236 refactor(wm): reduce noise in info and error logs 2024-05-09 19:49:43 -07:00
LGUG2Z
4802b55452 feat(wm): selectively handle uncloak events
This commit adds selective handling for WindowManagerEvent::Uncloak
alongside Show and Manage, avoiding the workspace-switching logic that
is known to cause infinite workspace loops.
2024-05-08 08:58:41 -07:00
dependabot[bot]
482a7b1d7f chore(deps): bump schemars from 0.8.17 to 0.8.18
Bumps [schemars](https://github.com/GREsau/schemars) from 0.8.17 to 0.8.18.
- [Release notes](https://github.com/GREsau/schemars/releases)
- [Changelog](https://github.com/GREsau/schemars/blob/master/CHANGELOG.md)
- [Commits](https://github.com/GREsau/schemars/compare/v0.8.17...v0.8.18)

---
updated-dependencies:
- dependency-name: schemars
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
2024-05-06 20:32:36 -07:00
dependabot[bot]
d00ee82a9d chore(deps): bump sysinfo from 0.30.11 to 0.30.12
Bumps [sysinfo](https://github.com/GuillaumeGomez/sysinfo) from 0.30.11 to 0.30.12.
- [Changelog](https://github.com/GuillaumeGomez/sysinfo/blob/master/CHANGELOG.md)
- [Commits](https://github.com/GuillaumeGomez/sysinfo/commits)

---
updated-dependencies:
- dependency-name: sysinfo
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
2024-05-06 20:32:19 -07:00
dependabot[bot]
9f01d8fa0f chore(deps): bump serde from 1.0.199 to 1.0.200
Bumps [serde](https://github.com/serde-rs/serde) from 1.0.199 to 1.0.200.
- [Release notes](https://github.com/serde-rs/serde/releases)
- [Commits](https://github.com/serde-rs/serde/compare/v1.0.199...v1.0.200)

---
updated-dependencies:
- dependency-name: serde
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
2024-05-06 20:31:51 -07:00
LGUG2Z
627088c9b9 fix(wm): focus workspaces on cross-monitor cmds
This commit ensures that if a window is moved to a workspace on another
monitor which is not the currently focused workspace on that monitor,
the target workspace will be focused as part of the operation.
2024-05-06 18:31:01 -07:00
LGUG2Z
22cf7b5017 docs(readme): add comparison with fancy zones 2024-05-02 15:52:19 -07:00
LGUG2Z
3e984d886c docs(mkdocs): fix header level typo 2024-05-01 17:42:27 -07:00
LGUG2Z
185cb4d4a8 chore(dev): begin v0.1.26-dev 2024-04-30 15:02:48 -07:00
24 changed files with 889 additions and 76 deletions

58
Cargo.lock generated
View File

@@ -796,9 +796,36 @@ dependencies = [
"wasm-bindgen",
]
[[package]]
name = "komoborders"
version = "0.1.0"
dependencies = [
"color-eyre",
"dirs",
"komoborders-client",
"komorebi",
"komorebi-client",
"lazy_static",
"parking_lot",
"serde",
"serde_json",
"uds_windows",
"windows 0.54.0",
]
[[package]]
name = "komoborders-client"
version = "0.1.0"
dependencies = [
"dirs",
"serde",
"serde_json_lenient",
"uds_windows",
]
[[package]]
name = "komorebi"
version = "0.1.25"
version = "0.1.26-dev.0"
dependencies = [
"bitflags 2.5.0",
"clap",
@@ -810,6 +837,7 @@ dependencies = [
"getset",
"hex_color",
"hotwatch",
"komoborders-client",
"komorebi-core",
"lazy_static",
"miow",
@@ -839,7 +867,7 @@ dependencies = [
[[package]]
name = "komorebi-client"
version = "0.1.25"
version = "0.1.26-dev.0"
dependencies = [
"komorebi",
"komorebi-core",
@@ -849,7 +877,7 @@ dependencies = [
[[package]]
name = "komorebi-core"
version = "0.1.25"
version = "0.1.26-dev.0"
dependencies = [
"clap",
"color-eyre",
@@ -865,7 +893,7 @@ dependencies = [
[[package]]
name = "komorebic"
version = "0.1.25"
version = "0.1.26-dev.0"
dependencies = [
"clap",
"color-eyre",
@@ -893,7 +921,7 @@ dependencies = [
[[package]]
name = "komorebic-no-console"
version = "0.1.25"
version = "0.1.26-dev.0"
[[package]]
name = "kqueue"
@@ -1640,9 +1668,9 @@ dependencies = [
[[package]]
name = "schemars"
version = "0.8.17"
version = "0.8.18"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7f55c82c700538496bdc329bb4918a81f87cc8888811bd123cf325a0f2f8d309"
checksum = "0afe01b987fac84253ce8acd5c05af9941975e4dee5b4f2d826b6947be8ec2c7"
dependencies = [
"dyn-clone",
"schemars_derive",
@@ -1652,9 +1680,9 @@ dependencies = [
[[package]]
name = "schemars_derive"
version = "0.8.17"
version = "0.8.18"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "83263746fe5e32097f06356968a077f96089739c927a61450efa069905eec108"
checksum = "d253e72f060451e9e5615a1686f3cb4ff87c4e70504c79bdab8fb3b010cd4e97"
dependencies = [
"proc-macro2",
"quote",
@@ -1693,18 +1721,18 @@ dependencies = [
[[package]]
name = "serde"
version = "1.0.199"
version = "1.0.200"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0c9f6e76df036c77cd94996771fb40db98187f096dd0b9af39c6c6e452ba966a"
checksum = "ddc6f9cc94d67c0e21aaf7eda3a010fd3af78ebf6e096aa6e2e13c79749cce4f"
dependencies = [
"serde_derive",
]
[[package]]
name = "serde_derive"
version = "1.0.199"
version = "1.0.200"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "11bd257a6541e141e42ca6d24ae26f7714887b47e89aa739099104c7e4d3b7fc"
checksum = "856f046b9400cee3c8c94ed572ecdb752444c24528c035cd35882aad6f492bcb"
dependencies = [
"proc-macro2",
"quote",
@@ -1888,9 +1916,9 @@ checksum = "2047c6ded9c721764247e62cd3b03c09ffc529b2ba5b10ec482ae507a4a70160"
[[package]]
name = "sysinfo"
version = "0.30.11"
version = "0.30.12"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "87341a165d73787554941cd5ef55ad728011566fe714e987d1b976c15dbc3a83"
checksum = "732ffa00f53e6b2af46208fba5718d9662a421049204e156328b66791ffa15ae"
dependencies = [
"cfg-if 1.0.0",
"core-foundation-sys",

View File

@@ -2,7 +2,7 @@
resolver = "2"
members = [
"derive-ahk",
"derive-ahk", "komoborders", "komoborders-client",
"komorebi",
"komorebi-client",
"komorebi-core",
@@ -18,6 +18,8 @@ dirs = "5"
color-eyre = "0.6"
serde_json = { package = "serde_json_lenient", version = "0.1" }
sysinfo = "0.30"
serde = { version = "1", features = ["derive"] }
uds_windows = "1"
[workspace.dependencies.windows]
version = "0.54"

View File

@@ -84,6 +84,19 @@ using `scoop`, `winget` or building from source.
[![Watch the quickstart walkthrough video](https://img.youtube.com/vi/H9-_c1egQ4g/hqdefault.jpg)](https://www.youtube.com/watch?v=H9-_c1egQ4g)
# Comparison With Fancy Zones
Community member [Olge](https://www.youtube.com/@polle5555) has created an
excellent video which compares the default window management features of
Windows 11, Fancy Zones and komorebi.
If you are not familiar with tiling window managers or if you are looking at
komorebi and wondering "how is this different from Fancy Zones? 🤔", this short
video will answer the majority of your questions.
[![Watch the comparison video](https://img.youtube.com/vi/0LCbS_gm0RA/hqdefault.jpg)](https://www.youtube.com/watch?v=0LCbS_gm0RA)
# Demonstrations
[@haxibami](https://github.com/haxibami) showing _komorebi_ running on Windows

View File

@@ -101,7 +101,7 @@ monocle.
+-------+-----+
```
### RightMainVerticalStack
#### RightMainVerticalStack
```
+-----+-------+

124
docs/troubleshooting.md Normal file
View File

@@ -0,0 +1,124 @@
# Troubleshooting
## AutoHotKey executable not found
If you try to start komorebi with AHK using `komorebic start --ahk`, and you have
not installed AHK using `scoop`, you'll probably receive an error:
```text
Error: could not find autohotkey, please make sure it is installed before using the --ahk flag
```
Depending on how AHK is installed the executable on your system may have a
different name. In order to account for this, you may set the `KOMOREBI_AHK_EXE`
environment variable in your
[PowerShell profile](https://learn.microsoft.com/en-us/powershell/module/microsoft.powershell.core/about/about_profiles?view=powershell-7.4)
to match the name of the executable as it is found on your system.
After setting `KOMOREBI_AHK_EXE` make sure to either reload your PowerShell
profile or open a new terminal tab.
## Komorebi is unresponsive when the display wakes from sleep
This can happen in rare cases when your monitor state is not preserved after it
wakes from sleep.
### Problem
Your hotkeys in _whkd_ work, but it feels as if _komorebi_ knows nothing about
the previous state (you can't control previous windows, although newly launched ones
can be manipulated as normal).
### Solution
Some monitors, such as the Samsung G8/G9 (LED, Neo, OLED) have an _adaptive
sync_ or _variable refresh rate_ setting within the actual monitor OSD that can
disrupt how the device is persisted in the _komorebi_ state following suspension.
To fix this, please try to disable _Adaptive Sync_ or any other _VRR_ branded
alias by referring to the manufacturer's documentation.
!!! warning
Disabling VRR within Windows (e.g. _Nvidia Control Panel_) may work and can indeed
change the configuration you see within your monitor's OSD, but some monitors
will re-enable the setting regardless following suspension.
### Reproducing
Ensure _komorebi_ is in an operational state by executing `komorebic start` as
normal.
If _komorebi_ is already unresponsive, then please restart _komorebi_ first by
running `komorebic stop` and `komorebic start`.
1. **`komorebic state`**
```json
{
"monitors": {
"elements": [
{
"id": 65537,
"name": "DISPLAY1",
"device": "SAM71AA",
"device_id": "SAM71AA-5&a1a3e88&0&UID24834",
"size": {
"left": 0,
"top": 0,
"right": 5120,
"bottom": 1440
}
}
]
}
}
```
This appears to be fine -- _komorebi_ is aware of the device and associated
window handles.
2. **Let your display go to sleep.**
Simply turning the monitor off is not enough to reproduce the problem; you must
let Windows turn off the display itself.
To avoid waiting an eternity:
- _Control Panel_ -> _Hardware and Sound_ -> _Power Options_ -> _Edit Plan
Settings_
_Turn off the display: 1 minute_
Allow a minute for the display to reset, then once it actually shuts off
allow for any additional time as prompted by your monitor for the cycle to
complete.
3. **Wake your display again** by pressing any key.
_komorebi_ should now be unresponsive.
4. **`komorebic state`**
Don't stop _komorebi_ just yet.
Since it's unresponsive, you can open another shell instead to execute the above command.
```json
{
"monitors": {
"elements": [
{
"id": 65537,
"name": "DISPLAY1",
"device": null,
"device_id": null
}
]
}
}
```
We can see the _komorebi_ state is no longer associated with the previous
device: `null`, suggesting an issue when the display resumes from a suspended
state.

View File

@@ -0,0 +1,12 @@
[package]
name = "komoborders-client"
version = "0.1.0"
edition = "2021"
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
[dependencies]
uds_windows = { workspace = true }
serde_json = { workspace = true }
serde = { workspace = true }
dirs = { workspace = true }

View File

@@ -0,0 +1,74 @@
use serde::Deserialize;
use serde::Serialize;
use std::io::Write;
use std::str::FromStr;
use uds_windows::UnixStream;
const KOMOBORDERS: &str = "komoborders.sock";
pub fn send_message(message: &SocketMessage) -> std::io::Result<()> {
let socket = dirs::data_local_dir()
.expect("there is no local data directory")
.join("komorebi")
.join(KOMOBORDERS);
let mut connected = false;
while !connected {
if let Ok(mut stream) = UnixStream::connect(&socket) {
connected = true;
stream.write_all(serde_json::to_string(message)?.as_bytes())?;
}
}
Ok(())
}
#[derive(Copy, Clone, Serialize, Deserialize)]
pub enum ZOrder {
Top,
NoTopMost,
Bottom,
TopMost,
}
// impl From<isize> for ZOrder {
// fn from(value: isize) -> Self {
// match value {
// -2 => Self::NoTopMost,
// -1 => Self::TopMost,
// 0 => Self::Top,
// 1 => Self::Bottom,
// _ => unimplemented!(),
// }
// }
// }
impl Into<isize> for ZOrder {
fn into(self) -> isize {
match self {
ZOrder::Top => 0,
ZOrder::NoTopMost => -2,
ZOrder::Bottom => 1,
ZOrder::TopMost => -1,
}
}
}
#[derive(Serialize, Deserialize)]
pub enum SocketMessage {
FocusedColour(u32, u32, u32),
UnfocusedColour(u32, u32, u32),
MonocleColour(u32, u32, u32),
StackColour(u32, u32, u32),
Width(i32),
Offset(i32),
ZOrder(ZOrder),
}
impl FromStr for SocketMessage {
type Err = serde_json::Error;
fn from_str(s: &str) -> Result<Self, Self::Err> {
serde_json::from_str(s)
}
}

19
komoborders/Cargo.toml Normal file
View File

@@ -0,0 +1,19 @@
[package]
name = "komoborders"
version = "0.1.0"
edition = "2021"
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
[dependencies]
komorebi-client = { path = "../komorebi-client" }
komoborders-client = { path = "../komoborders-client" }
komorebi = { path = "../komorebi" }
serde_json = "1"
color-eyre = "0.6"
windows = { workspace = true }
lazy_static = "1"
parking_lot = "0.12"
uds_windows = { workspace = true }
serde = { workspace = true }
dirs = { workspace = true }

199
komoborders/src/border.rs Normal file
View File

@@ -0,0 +1,199 @@
use komoborders_client::ZOrder;
use lazy_static::lazy_static;
use parking_lot::Mutex;
use std::sync::atomic::AtomicI32;
use std::sync::atomic::AtomicU32;
use std::sync::atomic::Ordering;
use std::sync::mpsc;
use std::sync::Arc;
use std::time::Duration;
use windows::core::PCWSTR;
use windows::Win32::Foundation::COLORREF;
use windows::Win32::Foundation::HWND;
use windows::Win32::Foundation::LPARAM;
use windows::Win32::Foundation::LRESULT;
use windows::Win32::Foundation::WPARAM;
use windows::Win32::Graphics::Gdi::BeginPaint;
use windows::Win32::Graphics::Gdi::CreatePen;
use windows::Win32::Graphics::Gdi::EndPaint;
use windows::Win32::Graphics::Gdi::InvalidateRect;
use windows::Win32::Graphics::Gdi::Rectangle;
use windows::Win32::Graphics::Gdi::SelectObject;
use windows::Win32::Graphics::Gdi::ValidateRect;
use windows::Win32::Graphics::Gdi::PAINTSTRUCT;
use windows::Win32::Graphics::Gdi::PS_INSIDEFRAME;
use windows::Win32::Graphics::Gdi::PS_SOLID;
use windows::Win32::UI::WindowsAndMessaging::DefWindowProcW;
use windows::Win32::UI::WindowsAndMessaging::DispatchMessageW;
use windows::Win32::UI::WindowsAndMessaging::GetMessageW;
use windows::Win32::UI::WindowsAndMessaging::PostQuitMessage;
use windows::Win32::UI::WindowsAndMessaging::TranslateMessage;
use windows::Win32::UI::WindowsAndMessaging::CS_HREDRAW;
use windows::Win32::UI::WindowsAndMessaging::CS_VREDRAW;
use windows::Win32::UI::WindowsAndMessaging::MSG;
use windows::Win32::UI::WindowsAndMessaging::WM_DESTROY;
use windows::Win32::UI::WindowsAndMessaging::WM_PAINT;
use windows::Win32::UI::WindowsAndMessaging::WNDCLASSW;
use crate::FocusKind;
use crate::FOCUSED_STATE;
use crate::RECT_STATE;
use komorebi::Rgb;
use komorebi::WindowsApi;
use komorebi_client::Rect;
pub static TRANSPARENCY: u32 = 0;
pub static BORDER_WIDTH: AtomicI32 = AtomicI32::new(8);
pub static BORDER_OFFSET: AtomicI32 = AtomicI32::new(-1);
lazy_static! {
pub static ref Z_ORDER: Arc<Mutex<ZOrder>> = Arc::new(Mutex::new(ZOrder::Bottom));
pub static ref FOCUSED: AtomicU32 = AtomicU32::new(u32::from(komorebi_client::Colour::Rgb(
Rgb::new(66, 165, 245)
)));
pub static ref UNFOCUSED: AtomicU32 = AtomicU32::new(u32::from(komorebi_client::Colour::Rgb(
Rgb::new(128, 128, 128)
)));
pub static ref MONOCLE: AtomicU32 = AtomicU32::new(u32::from(komorebi_client::Colour::Rgb(
Rgb::new(255, 51, 153)
)));
pub static ref STACK: AtomicU32 = AtomicU32::new(u32::from(komorebi_client::Colour::Rgb(
Rgb::new(0, 165, 66)
)));
}
pub struct Border {
pub hwnd: isize,
}
impl Border {
pub const fn hwnd(&self) -> HWND {
HWND(self.hwnd)
}
pub fn create(id: &str) -> color_eyre::Result<Self> {
let name: Vec<u16> = format!("komoborder-{id}\0").encode_utf16().collect();
let class_name = PCWSTR(name.as_ptr());
let h_module = WindowsApi::module_handle_w()?;
let window_class = WNDCLASSW {
hInstance: h_module.into(),
lpszClassName: class_name,
style: CS_HREDRAW | CS_VREDRAW,
lpfnWndProc: Some(Self::callback),
hbrBackground: WindowsApi::create_solid_brush(TRANSPARENCY),
..Default::default()
};
let _ = WindowsApi::register_class_w(&window_class);
let (hwnd_sender, hwnd_receiver) = mpsc::channel();
std::thread::spawn(move || -> color_eyre::Result<()> {
let hwnd = WindowsApi::create_border_window(PCWSTR(name.as_ptr()), h_module)?;
hwnd_sender.send(hwnd)?;
let mut message = MSG::default();
unsafe {
while GetMessageW(&mut message, HWND(hwnd), 0, 0).into() {
TranslateMessage(&message);
DispatchMessageW(&message);
std::thread::sleep(Duration::from_millis(10));
}
}
Ok(())
});
Ok(Self {
hwnd: hwnd_receiver.recv()?,
})
}
pub fn destroy(&self) -> color_eyre::Result<()> {
WindowsApi::destroy_window(self.hwnd())
}
pub fn update(&self, rect: &Rect) -> color_eyre::Result<()> {
// Make adjustments to the border
let mut rect = *rect;
rect.add_margin(BORDER_WIDTH.load(Ordering::SeqCst));
rect.add_padding(-BORDER_OFFSET.load(Ordering::SeqCst));
// Store the border rect so that it can be used by the callback
{
let mut rects = RECT_STATE.lock();
rects.insert(self.hwnd, rect);
}
// Update the position of the border
WindowsApi::set_border_pos(self.hwnd(), &rect, HWND((*Z_ORDER.lock()).into()))?;
// Invalidate the rect to trigger the callback to update colours etc.
self.invalidate();
Ok(())
}
pub fn invalidate(&self) {
let _ = unsafe { InvalidateRect(self.hwnd(), None, false) };
}
pub extern "system" fn callback(
window: HWND,
message: u32,
wparam: WPARAM,
lparam: LPARAM,
) -> LRESULT {
unsafe {
match message {
WM_PAINT => {
let rects = RECT_STATE.lock();
// With the rect that we stored in Self::update
if let Some(rect) = rects.get(&window.0).copied() {
// Grab the focus kind for this border
let focus_kind = {
FOCUSED_STATE
.lock()
.get(&window.0)
.copied()
.unwrap_or(FocusKind::Unfocused)
};
// Set up the brush to draw the border
let mut ps = PAINTSTRUCT::default();
let hdc = BeginPaint(window, &mut ps);
let hpen = CreatePen(
PS_SOLID | PS_INSIDEFRAME,
BORDER_WIDTH.load(Ordering::SeqCst),
COLORREF(match focus_kind {
FocusKind::Unfocused => UNFOCUSED.load(Ordering::SeqCst),
FocusKind::Single => FOCUSED.load(Ordering::SeqCst),
FocusKind::Stack => STACK.load(Ordering::SeqCst),
FocusKind::Monocle => MONOCLE.load(Ordering::SeqCst),
}),
);
let hbrush = WindowsApi::create_solid_brush(TRANSPARENCY);
// Draw the border
SelectObject(hdc, hpen);
SelectObject(hdc, hbrush);
Rectangle(hdc, 0, 0, rect.right, rect.bottom);
EndPaint(window, &ps);
ValidateRect(window, None);
}
LRESULT(0)
}
WM_DESTROY => {
PostQuitMessage(0);
LRESULT(0)
}
_ => DefWindowProcW(window, message, wparam, lparam),
}
}
}
}

259
komoborders/src/main.rs Normal file
View File

@@ -0,0 +1,259 @@
#![warn(clippy::all, clippy::nursery, clippy::pedantic)]
#![allow(
clippy::missing_errors_doc,
clippy::redundant_pub_crate,
clippy::significant_drop_tightening,
clippy::significant_drop_in_scrutinee
)]
mod border;
use komorebi_client::Rect;
use komorebi_client::UnixListener;
use lazy_static::lazy_static;
use parking_lot::Mutex;
use std::collections::HashMap;
use std::io::BufRead;
use std::io::BufReader;
use std::io::ErrorKind;
use std::str::FromStr;
use std::sync::atomic::Ordering;
use std::time::Duration;
use uds_windows::UnixStream;
use windows::Win32::Foundation::HWND;
use crate::border::Border;
use crate::border::BORDER_WIDTH;
use crate::border::FOCUSED;
use crate::border::MONOCLE;
use crate::border::STACK;
use crate::border::UNFOCUSED;
use crate::border::Z_ORDER;
use komorebi::WindowsApi;
use komorebi_client::Rgb;
lazy_static! {
static ref BORDER_STATE: Mutex<HashMap<String, Border>> = Mutex::new(HashMap::new());
static ref RECT_STATE: Mutex<HashMap<isize, Rect>> = Mutex::new(HashMap::new());
static ref FOCUSED_STATE: Mutex<HashMap<isize, FocusKind>> = Mutex::new(HashMap::new());
}
#[derive(Copy, Clone)]
enum FocusKind {
Unfocused,
Single,
Stack,
Monocle,
}
pub fn read_commands_uds(stream: UnixStream) -> color_eyre::Result<()> {
let reader = BufReader::new(stream.try_clone()?);
for line in reader.lines() {
let message = komoborders_client::SocketMessage::from_str(&line?)?;
match message {
komoborders_client::SocketMessage::FocusedColour(r, g, b) => FOCUSED.store(
komorebi::Colour::Rgb(Rgb::new(r, g, b)).into(),
Ordering::SeqCst,
),
komoborders_client::SocketMessage::UnfocusedColour(r, g, b) => UNFOCUSED.store(
komorebi::Colour::Rgb(Rgb::new(r, g, b)).into(),
Ordering::SeqCst,
),
komoborders_client::SocketMessage::MonocleColour(r, g, b) => MONOCLE.store(
komorebi::Colour::Rgb(Rgb::new(r, g, b)).into(),
Ordering::SeqCst,
),
komoborders_client::SocketMessage::StackColour(r, g, b) => STACK.store(
komorebi::Colour::Rgb(Rgb::new(r, g, b)).into(),
Ordering::SeqCst,
),
komoborders_client::SocketMessage::Width(width) => {
BORDER_WIDTH.store(width, Ordering::SeqCst)
}
komoborders_client::SocketMessage::Offset(offset) => {
BORDER_WIDTH.store(offset, Ordering::SeqCst)
}
komoborders_client::SocketMessage::ZOrder(z_order) => {
let mut z = Z_ORDER.lock();
*z = z_order;
}
}
let borders = BORDER_STATE.lock();
for (_, border) in borders.iter() {
border.invalidate();
}
}
Ok(())
}
fn main() -> color_eyre::Result<()> {
WindowsApi::set_process_dpi_awareness_context()?;
let socket = dirs::data_local_dir()
.expect("there is no local data directory")
.join("komorebi")
.join("komoborders.sock");
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)?;
std::thread::spawn(move || {
for client in listener.incoming() {
match client {
Ok(stream) => match read_commands_uds(stream) {
Ok(()) => {
println!("processed message");
}
Err(error) => {
println!("{error}");
}
},
Err(error) => {
println!("{error}");
break;
}
}
}
});
let komorebi = komorebi_client::subscribe("komoborders")?;
for client in komorebi.incoming() {
match client {
Ok(subscription) => {
let reader = BufReader::new(subscription);
#[allow(clippy::lines_filter_map_ok)]
for line in reader.lines().flatten() {
if let Ok(notification) =
serde_json::from_str::<komorebi_client::Notification>(&line)
{
let mut borders = BORDER_STATE.lock();
// Check the state every time we receive a notification
let state = notification.state;
for m in state.monitors.elements() {
// Only operate on the focused workspace of each monitor
if let Some(ws) = m.focused_workspace() {
let mut should_proceed = true;
// Handle the monocle container separately
if let Some(monocle) = ws.monocle_container() {
for (_, border) in borders.iter() {
border.destroy()?;
}
borders.clear();
let border = borders
.entry(monocle.id().clone())
.or_insert_with(|| Border::create(monocle.id()).unwrap());
{
let mut focused = FOCUSED_STATE.lock();
focused.insert(border.hwnd, FocusKind::Monocle);
}
let rect = WindowsApi::window_rect(
monocle.focused_window().unwrap().hwnd(),
)?;
border.update(&rect)?;
should_proceed = false;
}
if should_proceed {
let is_maximized = WindowsApi::is_zoomed(HWND(
WindowsApi::foreground_window().unwrap_or_default(),
));
if is_maximized {
for (_, border) in borders.iter() {
border.destroy()?;
}
borders.clear();
should_proceed = false;
}
}
if should_proceed {
// Destroy any borders not associated with the focused workspace
let container_ids = ws
.containers()
.iter()
.map(|c| c.id().clone())
.collect::<Vec<_>>();
for (id, border) in borders.iter() {
if !container_ids.contains(id) {
border.destroy()?;
}
}
// Remove them from the border map
borders.retain(|k, _| container_ids.contains(k));
for (idx, c) in ws.containers().iter().enumerate() {
// Get the border entry for this container from the map or create one
let border = borders
.entry(c.id().clone())
.or_insert_with(|| Border::create(c.id()).unwrap());
// Update the focused state for all containers on this workspace
{
let mut focused = FOCUSED_STATE.lock();
focused.insert(
border.hwnd,
if idx != ws.focused_container_idx() {
FocusKind::Unfocused
} else {
if c.windows().len() > 1 {
FocusKind::Stack
} else {
FocusKind::Single
}
},
);
}
let rect = WindowsApi::window_rect(
c.focused_window().unwrap().hwnd(),
)?;
border.update(&rect)?;
}
}
}
}
}
}
}
Err(error) => {
if error.raw_os_error().expect("could not get raw os error") == 109 {
while komorebi_client::send_message(
&komorebi_client::SocketMessage::AddSubscriberSocket(String::from(
"komoborders",
)),
)
.is_err()
{
std::thread::sleep(Duration::from_secs(5));
}
}
}
}
}
Ok(())
}

View File

@@ -1,6 +1,6 @@
[package]
name = "komorebi-client"
version = "0.1.25"
version = "0.1.26-dev.0"
edition = "2021"
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html

View File

@@ -1,6 +1,6 @@
[package]
name = "komorebi-core"
version = "0.1.25"
version = "0.1.26-dev.0"
edition = "2021"
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html

View File

@@ -1,6 +1,6 @@
[package]
name = "komorebi"
version = "0.1.25"
version = "0.1.26-dev.0"
authors = ["Jade Iqbal <jadeiqbal@fastmail.com>"]
description = "A tiling window manager for Windows"
categories = ["tiling-window-manager", "windows"]
@@ -12,6 +12,7 @@ edition = "2021"
[dependencies]
komorebi-core = { path = "../komorebi-core" }
komoborders-client = { path = "../komoborders-client" }
bitflags = { version = "2", features = ["serde"] }
clap = { version = "4", features = ["derive"] }

View File

@@ -65,7 +65,7 @@ pub struct Rgb {
}
impl Rgb {
pub fn new(r: u32, g: u32, b: u32) -> Self {
pub const fn new(r: u32, g: u32, b: u32) -> Self {
Self { r, g, b }
}
}

View File

@@ -1255,12 +1255,21 @@ impl WindowManager {
WindowKind::Single => {
BORDER_COLOUR_SINGLE.store(Rgb::new(r, g, b).into(), Ordering::SeqCst);
BORDER_COLOUR_CURRENT.store(Rgb::new(r, g, b).into(), Ordering::SeqCst);
komoborders_client::send_message(
&komoborders_client::SocketMessage::FocusedColour(r, g, b),
)?;
}
WindowKind::Stack => {
BORDER_COLOUR_STACK.store(Rgb::new(r, g, b).into(), Ordering::SeqCst);
komoborders_client::send_message(
&komoborders_client::SocketMessage::StackColour(r, g, b),
)?;
}
WindowKind::Monocle => {
BORDER_COLOUR_MONOCLE.store(Rgb::new(r, g, b).into(), Ordering::SeqCst);
komoborders_client::send_message(
&komoborders_client::SocketMessage::MonocleColour(r, g, b),
)?;
}
}
@@ -1275,10 +1284,14 @@ impl WindowManager {
SocketMessage::BorderWidth(width) => {
BORDER_WIDTH.store(width, Ordering::SeqCst);
WindowsApi::invalidate_border_rect()?;
komoborders_client::send_message(&komoborders_client::SocketMessage::Width(width))?;
}
SocketMessage::BorderOffset(offset) => {
BORDER_OFFSET.store(offset, Ordering::SeqCst);
WindowsApi::invalidate_border_rect()?;
komoborders_client::send_message(&komoborders_client::SocketMessage::Offset(
offset,
))?;
}
SocketMessage::StackbarMode(mode) => {
let mut stackbar_mode = STACKBAR_MODE.lock();

View File

@@ -284,7 +284,9 @@ impl WindowManager {
}
}
}
WindowManagerEvent::Show(_, window) | WindowManagerEvent::Manage(window) => {
WindowManagerEvent::Show(_, window)
| WindowManagerEvent::Manage(window)
| WindowManagerEvent::Uncloak(_, window) => {
let mut switch_to = None;
for (i, monitors) in self.monitors().iter().enumerate() {
for (j, workspace) in monitors.workspaces().iter().enumerate() {
@@ -294,56 +296,66 @@ impl WindowManager {
}
}
if let Some((known_monitor_idx, known_workspace_idx)) = switch_to {
if self.focused_monitor_idx() != known_monitor_idx
|| self
.focused_monitor()
.ok_or_else(|| anyhow!("there is no monitor"))?
.focused_workspace_idx()
!= known_workspace_idx
{
self.focus_monitor(known_monitor_idx)?;
self.focus_workspace(known_workspace_idx)?;
return Ok(());
match switch_to {
Some((known_monitor_idx, known_workspace_idx)) => {
if !matches!(event, WindowManagerEvent::Uncloak(_, _)) {
if self.focused_monitor_idx() != known_monitor_idx
|| self
.focused_monitor()
.ok_or_else(|| anyhow!("there is no monitor"))?
.focused_workspace_idx()
!= known_workspace_idx
{
self.focus_monitor(known_monitor_idx)?;
self.focus_workspace(known_workspace_idx)?;
}
}
}
}
None => {
// There are some applications such as Firefox where, if they are focused when a
// workspace switch takes place, it will fire an additional Show event, which will
// result in them being associated with both the original workspace and the workspace
// being switched to. This loop is to try to ensure that we don't end up with
// duplicates across multiple workspaces, as it results in ghost layout tiles.
let mut proceed = true;
// There are some applications such as Firefox where, if they are focused when a
// workspace switch takes place, it will fire an additional Show event, which will
// result in them being associated with both the original workspace and the workspace
// being switched to. This loop is to try to ensure that we don't end up with
// duplicates across multiple workspaces, as it results in ghost layout tiles.
for (i, monitor) in self.monitors().iter().enumerate() {
for (j, workspace) in monitor.workspaces().iter().enumerate() {
if workspace.container_for_window(window.hwnd).is_some()
&& i != self.focused_monitor_idx()
&& j != monitor.focused_workspace_idx()
{
tracing::debug!(
for (i, monitor) in self.monitors().iter().enumerate() {
for (j, workspace) in monitor.workspaces().iter().enumerate() {
if workspace.container_for_window(window.hwnd).is_some()
&& i != self.focused_monitor_idx()
&& j != monitor.focused_workspace_idx()
{
tracing::debug!(
"ignoring show event for window already associated with another workspace"
);
window.hide();
return Ok(());
window.hide();
proceed = false;
}
}
}
}
}
let behaviour = self.window_container_behaviour;
let workspace = self.focused_workspace_mut()?;
if proceed {
let behaviour = self.window_container_behaviour;
let workspace = self.focused_workspace_mut()?;
if !workspace.contains_window(window.hwnd) {
match behaviour {
WindowContainerBehaviour::Create => {
workspace.new_container_for_window(window);
self.update_focused_workspace(false, false)?;
}
WindowContainerBehaviour::Append => {
workspace
.focused_container_mut()
.ok_or_else(|| anyhow!("there is no focused container"))?
.add_window(window);
self.update_focused_workspace(true, false)?;
if !workspace.contains_window(window.hwnd) && switch_to.is_none() {
match behaviour {
WindowContainerBehaviour::Create => {
workspace.new_container_for_window(window);
self.update_focused_workspace(false, false)?;
}
WindowContainerBehaviour::Append => {
workspace
.focused_container_mut()
.ok_or_else(|| {
anyhow!("there is no focused container")
})?
.add_window(window);
self.update_focused_workspace(true, false)?;
}
}
}
}
}
}
@@ -583,8 +595,7 @@ impl WindowManager {
}
WindowManagerEvent::DisplayChange(..)
| WindowManagerEvent::MouseCapture(..)
| WindowManagerEvent::Cloak(..)
| WindowManagerEvent::Uncloak(..) => {}
| WindowManagerEvent::Cloak(..) => {}
};
if !self.focused_workspace()?.tile() {

View File

@@ -7,6 +7,7 @@ use crate::window_manager::WindowManager;
use crate::window_manager_event::WindowManagerEvent;
use crate::windows_api::WindowsApi;
use crate::workspace::Workspace;
use crate::Rgb;
use crate::ACTIVE_WINDOW_BORDER_STYLE;
use crate::BORDER_COLOUR_CURRENT;
use crate::BORDER_COLOUR_MONOCLE;
@@ -475,13 +476,36 @@ impl StaticConfig {
},
);
komoborders_client::send_message(&komoborders_client::SocketMessage::Width(
self.border_width.unwrap_or(8),
))?;
BORDER_OFFSET.store(self.border_offset.unwrap_or(-1), Ordering::SeqCst);
komoborders_client::send_message(&komoborders_client::SocketMessage::Width(
self.border_offset.unwrap_or(-1),
))?;
if let Some(colours) = &self.active_window_border_colours {
BORDER_COLOUR_SINGLE.store(u32::from(colours.single), Ordering::SeqCst);
BORDER_COLOUR_CURRENT.store(u32::from(colours.single), Ordering::SeqCst);
BORDER_COLOUR_STACK.store(u32::from(colours.stack), Ordering::SeqCst);
BORDER_COLOUR_MONOCLE.store(u32::from(colours.monocle), Ordering::SeqCst);
let single: Rgb = u32::from(colours.single).into();
komoborders_client::send_message(&komoborders_client::SocketMessage::FocusedColour(
single.r, single.g, single.b,
))?;
let stack: Rgb = u32::from(colours.stack).into();
komoborders_client::send_message(&komoborders_client::SocketMessage::StackColour(
stack.r, stack.g, stack.b,
))?;
let monocle: Rgb = u32::from(colours.monocle).into();
komoborders_client::send_message(&komoborders_client::SocketMessage::MonocleColour(
monocle.r, monocle.g, monocle.b,
))?;
}
let active_window_border_style = self.active_window_border_style.unwrap_or_default();

View File

@@ -14,7 +14,6 @@ use komorebi_core::config_generation::MatchingRule;
use komorebi_core::config_generation::MatchingStrategy;
use regex::Regex;
use schemars::JsonSchema;
use serde::ser::Error;
use serde::ser::SerializeStruct;
use serde::Deserialize;
use serde::Serialize;
@@ -97,24 +96,23 @@ impl Serialize for Window {
"title",
&self
.title()
.map_err(|_| S::Error::custom("could not get window title"))?,
.unwrap_or_else(|_| String::from("could not get window title")),
)?;
state.serialize_field(
"exe",
&self
.exe()
.map_err(|_| S::Error::custom("could not get window exe"))?,
.unwrap_or_else(|_| String::from("could not get window exe")),
)?;
state.serialize_field(
"class",
&self
.class()
.map_err(|_| S::Error::custom("could not get window class"))?,
.unwrap_or_else(|_| String::from("could not get window class")),
)?;
state.serialize_field(
"rect",
&WindowsApi::window_rect(self.hwnd())
.map_err(|_| S::Error::custom("could not get window rect"))?,
&WindowsApi::window_rect(self.hwnd()).unwrap_or_default(),
)?;
state.end()
}

View File

@@ -566,7 +566,7 @@ impl WindowManager {
target_workspace_idx: usize,
to_move: &mut Vec<EnforceWorkspaceRuleOp>,
) -> () {
tracing::info!(
tracing::trace!(
"{} should be on monitor {}, workspace {}",
window_title,
target_monitor_idx,
@@ -1205,6 +1205,7 @@ impl WindowManager {
let monitor = self
.focused_monitor_mut()
.ok_or_else(|| anyhow!("there is no monitor"))?;
let workspace = monitor
.focused_workspace_mut()
.ok_or_else(|| anyhow!("there is no workspace"))?;
@@ -1225,6 +1226,11 @@ impl WindowManager {
.ok_or_else(|| anyhow!("there is no monitor"))?;
target_monitor.add_container(container, workspace_idx)?;
if let Some(workspace_idx) = workspace_idx {
target_monitor.focus_workspace(workspace_idx)?;
}
target_monitor.load_focused_workspace(mouse_follows_focus)?;
target_monitor.update_focused_workspace(offset)?;

View File

@@ -124,6 +124,7 @@ use windows::Win32::UI::WindowsAndMessaging::SYSTEM_PARAMETERS_INFO_ACTION;
use windows::Win32::UI::WindowsAndMessaging::SYSTEM_PARAMETERS_INFO_UPDATE_FLAGS;
use windows::Win32::UI::WindowsAndMessaging::WINDOW_LONG_PTR_INDEX;
use windows::Win32::UI::WindowsAndMessaging::WM_CLOSE;
use windows::Win32::UI::WindowsAndMessaging::WM_DESTROY;
use windows::Win32::UI::WindowsAndMessaging::WNDCLASSW;
use windows::Win32::UI::WindowsAndMessaging::WNDENUMPROC;
use windows::Win32::UI::WindowsAndMessaging::WS_DISABLED;
@@ -414,7 +415,8 @@ impl WindowsApi {
// top of other pop-up dialogs such as a file picker dialog from
// Firefox. When adjusting this in the future, it's important to check
// those dialog cases.
Self::set_window_pos(hwnd, layout, HWND_TOP, flags.bits())
// TODO: Make the HWND_X flag configurable
Self::set_window_pos(hwnd, layout, HWND_BOTTOM, flags.bits())
}
pub fn hide_border_window(hwnd: HWND) -> Result<()> {
@@ -424,6 +426,11 @@ impl WindowsApi {
Self::set_window_pos(hwnd, &Rect::default(), position, flags.bits())
}
pub fn set_border_pos(hwnd: HWND, layout: &Rect, position: HWND) -> Result<()> {
let flags = { SetWindowPosition::SHOW_WINDOW | SetWindowPosition::NO_ACTIVATE };
Self::set_window_pos(hwnd, layout, position, flags.bits())
}
/// set_window_pos calls SetWindowPos without any accounting for Window decorations.
fn set_window_pos(hwnd: HWND, layout: &Rect, position: HWND, flags: u32) -> Result<()> {
unsafe {
@@ -461,6 +468,13 @@ impl WindowsApi {
}
}
pub fn destroy_window(hwnd: HWND) -> Result<()> {
match Self::post_message(hwnd, WM_DESTROY, WPARAM(0), LPARAM(0)) {
Ok(()) => Ok(()),
Err(_) => Err(anyhow!("could not close window")),
}
}
pub fn hide_window(hwnd: HWND) {
Self::show_window(hwnd, SW_HIDE);
}

View File

@@ -1,6 +1,6 @@
[package]
name = "komorebic-no-console"
version = "0.1.25"
version = "0.1.26-dev.0"
authors = ["Jade Iqbal <jadeiqbal@fastmail.com>"]
description = "The command-line interface (without a console) for Komorebi, a tiling window manager for Windows"
categories = ["cli", "tiling-window-manager", "windows"]

View File

@@ -1,6 +1,6 @@
[package]
name = "komorebic"
version = "0.1.25"
version = "0.1.26-dev.0"
authors = ["Jade Iqbal <jadeiqbal@fastmail.com>"]
description = "The command-line interface for Komorebi, a tiling window manager for Windows"
categories = ["cli", "tiling-window-manager", "windows"]

View File

@@ -39,6 +39,10 @@ theme:
- search.share
- search.suggest
- toc.follow
markdown_extensions:
- admonition
- pymdownx.highlight
- pymdownx.superfences
plugins:
- macros
- search
@@ -50,6 +54,7 @@ nav:
- Getting started:
- Installation: installation.md
- Example configurations: example-configurations.md
- Troubleshooting: troubleshooting.md
- Common workflows:
- common-workflows/komorebi-config-home.md
- common-workflows/active-window-border.md

11
shell.nix Normal file
View File

@@ -0,0 +1,11 @@
{pkgs ? import <nixpkgs> {}}:
with pkgs;
mkShell {
name = "komorebi";
buildInputs = [
python311Packages.mkdocs-material
python311Packages.mkdocs-macros
python311Packages.setuptools
];
}