Compare commits

..

25 Commits

Author SHA1 Message Date
LGUG2Z
8ceed09c20 fix(wm): dynamically reserve monitor ring space
This commit makes a small change to dynamically keep reserving space in
the VecDeque that backs Ring<Monitor> until an index preference can be
contained within the current length.
2024-05-23 16:58:54 -07:00
LGUG2Z
1712042dda chore(dev): begin v0.1.27-dev 2024-05-23 16:47:26 -07:00
LGUG2Z
3556f38469 chore(release): v0.1.26 2024-05-22 15:36:20 -07:00
LGUG2Z
62770033f2 fix(wm): make close + min op on foreground hwnd
This commit changes the handlers for the Close and Minimize
SocketMessages to operate on the output of
WindowsApi::foreground_window, without checking the window manager state
as it was doing previously.

This will allow the commands to operate on any kind of managed or
unmanaged window, and the appropriate WinEvent will be emitted by the
closed window for the window manager state to be updated when the
WinEvent goes through process_event.

fix #839
2024-05-22 15:33:03 -07:00
LGUG2Z
e294dbbe93 feat(cli): rename and deprecation feedback
This commit adds feedback about renamed and deprecated configuration
options in a user's komorebi.json file when the 'start' command is run.
2024-05-22 12:26:11 -07:00
LGUG2Z
47f0ab1ef3 feat(wm): handle OBJECT_NAMECHANGE for all apps
This commit ensures that EVENT_OBJECT_NAMECHANGE is handled for all
windows.

Previously this was mapped to WindowManagerEvent::Show, as this is the
event that apps like Firefox and JetBrains IDEs sent on launch instead
of EVENT_OBJECT_SHOW like normal apps.

Now that we are using EVENT_OBJECT_NAMECHANGE to update titles on
stackbar tabs, when a window which is not in the whitelist of
object_name_change_applications sends this event, it will be handled by
the new WindowManagerEvent::TitleUpdate variant.

This ensures that a stackbar_manager::Notification is sent at the end of
process_event to update stackbar tabs when application titles are
changing.

resolve #842
2024-05-22 11:37:49 -07:00
LGUG2Z
c4d62fc4f6 feat(wm): run orphan reaper in a dedicated thread
Until now the orphan window/container reaper has always run on every
WinEvent. Unfortunately Windows Terminal does not sent a WinEvent when
it is closed.

This is a problem for the new border_manager module which draws and
destroys borders based on notifications sent to it after WinEvents and
SocketMessages have been processed.

Since Windows Terminal is not sending a WinEvent on close, this means
that user interaction is required to remove the ghost border that gets
left behind.

This commit starts a separate thread for the reaper where it runs once
every second in a loop.

This is quite wasteful and definitely not something I wanted to
implement, but a temporary solution is needed given the popularity of
the buggy application in question.

An issue on the Windows Terminal tracker has been opened here:
https://github.com/microsoft/terminal/issues/17298
2024-05-21 19:27:58 -07:00
LGUG2Z
69680b4238 fix(stackbar): destroy stackbars on ws change
This commit ensures that whenever we receive a
stackbar_manager::Notification any stackbars not associated with the
current workspace on each monitor are destroyed.

fix #838
2024-05-21 08:23:05 -07:00
LGUG2Z
0dc17e9cb3 fix(wm): restore containers when closing monocle
This commit fixes a regression introduced by hiding other containers
when monocle is enabled. When the monocle container is closed, other
containers on the workspace will now be restored.

re #834
2024-05-20 08:24:55 -07:00
LGUG2Z
0f44efaa82 docs(wm): add komorebi-gui binary, update mkdocs 2024-05-19 16:37:35 -07:00
LGUG2Z
05af7ce16a feat(gui): add the komorebi-gui debug tool
This commit adds the komorebi-gui debug tool build with egui and eframe.

This tool was built from scratch in a YouTube mini-series which can be
found here: https://www.youtube.com/watch?v=zZKjBMt4kZ4

The most interesting part of this tool right now is the ability to view
debug information about each window as it goes through the rules engine.

While it's possible to change runtime configuration options with this
tool, it is not yet possible to write those changes out to the
configuration file.
2024-05-19 14:27:18 -07:00
LGUG2Z
92447723d2 fix(wm): respect horizontal focus from monocle
This commit ensures that horizontal focus moves onto other monitors from
a monocle container are respected (ie. we don't try moving left/right
within the workspace on the focused monitor).

Additionally, if the user tries to alt-tab a window to the foreground on
a workspace where a monocle container exists, the window will flash
before being hidden behind the monocle container as a visual cue that
monocle mode needs to be disabled to access that window.

This is in contrast to the current behaviour where that window floats on
top of the monocle container in a somewhat broken state.

re #834
2024-05-19 12:45:35 -07:00
LGUG2Z
2a45f981e6 feat(stackbar): add stackbar manager module
This commit removes all stackbar-related code from Container, Workspace,
process_command, process_event etc. and centralizes it in the new
stackbar_manager module.

Instead of trying to figure out where in process_event and
process_command we should make stackbar-related changes, a notification
gets sent to a channel that stackbar_manager listens to whenever an
event or command has finished processing.

The stackbar_manager listener, upon receiving a notification, acquires a
lock on the WindowManager instance and updates stackbars for the focused
workspace on every monitor; this allows us to centralize all edge case
handling within the stackbar_manager listener's loop.

Global state related to stackbars has also been moved into the
stackbar_manager module, which also tracks the state of stackbar objects
(STACKBAR_STATE), mappings between stackbars and containers
(STACKBARS_CONTAINERS) and the mappings between stackbars and monitors
(STACKBARS_MONITORS).

A number of edge cases around stackbar behaviour have been addressed in
this commit (re #832), and stackbars now respect the "border_style"
configuration option.
2024-05-19 10:55:40 -07:00
LGUG2Z
a29ab4cfb3 feat(wm): add monitor reconciliator module
This commit adds the monitor_reconciliator module which uses a tightly
bounded channel (cap: 1) to handle monitor connection and disconnection
events, as well as resolution and work area change events.

Before, all this logic lived in a the WindowManager.reconcile_monitors
function, which ran on pretty much every process_event iteration, and
sometimes led to undesirable behaviour, but now the logic is split up to
only run when the appropriate notifications are dispatched from the
hidden window which listens for monitor and display-related events.

The monitor cache has been moved out of WindowManager and into the
monitor_reconciliator module, and in addition to the previous behaviour
of attempting to cache monitors which had been identified as
disconnected, now when the static configuration file is loaded, if the
user has set display_index_preferences, the device IDs will be used to
pre-populate the cache for the event where a known monitor is connected
later in a session.

The monitor cache itself now uses the unique device ID as a key rather
than the hmonitor which is known to be inconsistent.

This commit also delegates all display monitor-related Win32 calls to
the "win32-display-data" crate, which was extracted from the larger
"brightness" crate for its use in komorebi.

As a result of these changes, "device" and "device_id" on Monitor have
been changed from Option<String> to String types, as failures in
retrieving these values with directly attached monitors has not been
possible to reproduce. However, it remains to be seen if this will
adversely impact users who use display docks which may prevent display
monitor device IDs from being read and stored by the operating system.

WindowManagerEvent::DisplayChange has been removed in favour of
the monitor_reconciliator::Notification enum, as these events are no
longer being handled in process_events.

Attempts are now made to eagerly update hmonitors both within the
monitor_reconciliator loop on DisplayConnectionChange notifications and
when failing to find a matching hmonitor in functions like
monitor_idx_from_current_pos and monitor_idx_from_window.
2024-05-19 06:26:45 -07:00
LGUG2Z
27cd1736aa fix(wm): smooth transitions to monocle workspaces
This commit adds hiding and restoring of other containers on a workspace
with monocle on/off, and exits early when a monocle container is found
on workspace restores to avoid flashing of other containers before the
workspace focus operation completes.

Focus is also restored when focusing a monocle container on another
monitor as part of a cross-monitor focus operation.

The border rendering for monocle containers has also been tightened up.

re #819
2024-05-18 09:53:10 -07:00
LGUG2Z
d2470b1f08 fix(cli): have log command lookup file w/ timestamp 2024-05-17 16:18:21 -07:00
LGUG2Z
835472d739 fix(wm): address monocle mode visual artifacts
This commit addresses two visual artifacts with monocle mode:

* Flashing of background windows when switching to a monocle container
  on another monitor is now gone
* Stackbars are automatically disabled whenever a container enters
  monocle mode

re #819
2024-05-17 15:54:40 -07:00
LGUG2Z
bceb28de37 fix(wm): various monocle container regressions
This commit fixes a number of monocle container-related regressions.

* Monocle container on one monitor preventing border updates on another
* Cross-monitor focus changes towards a monitor w/ a monocle container
* Cross-monitor move towards a monitor w/ a monocle container

re #819
2024-05-17 08:42:56 -07:00
LGUG2Z
fff7b5c147 feat(stackbar): update title labels in realtime
This commit handles the EVENT_OBJECT_NAMECHANGE WinEvent which is
emitted when window titles change to update Stackbar labels in real-time
when StackbarLabel::Title is used.

re #826
2024-05-16 16:23:09 -07:00
LGUG2Z
1e63947ae3 fix(wm): uw-vertical-stack stacking regression
This commit fixes a small regression that was introduced with the
addition of the Grid layout, where stacking right from index 0 on the
UltrawideVerticalStack layout would actually end up stacking to the
left.
2024-05-16 14:32:03 -07:00
LGUG2Z
d5f4f916be feat(stackbar): make label configurable
This commit introduces a new stackbar label configuration option backed
by the StackbarLabel enum, which now has two variants, Process and
Title.

The state tracker for this option is kept in an AtomicCell, and the
state tracker for StackbarMode has also been changed from an
Arc<Mutex<T>> to an AtomicCell to match.

resolve #826
2024-05-16 14:17:54 -07:00
LGUG2Z
77fc3973b6 perf(wm): switch to daily rotating log files 2024-05-15 17:54:34 -07:00
r3st
29b1794409 feat(cli): add promote-window cmd
This commit adds the promote-window command, which allows the user to
promote the window in the specified OperationDirection from the
currently focused window to the largest tile on the workspace layout.
2024-05-15 14:56:31 -07:00
LGUG2Z
1420334c94 feat(cli): add cycle-move-workspace-to-monitor cmd
This commit adds a new cli command, cycle-move-workspace-to-monitor.

After the introduction of the monitor reconciliator module in
combination with display_index_preferences, this command should never
really be necessary, however it is worth having as a backup.

resolve #718
2024-05-15 14:06:35 -07:00
LGUG2Z
82aa2edf8f docs(wm): border renames and aliases, ahk example
This commit renames a number of border-related code refs, removing the
ActiveWindow prefix since these borders are no longer just for the
active window.

Aliases have been added to preserve backwards compat for existing
configs.

An example AHK configuration file has been added to the Common Workflows
section of the docs site.

A link to the docs site has been added to the output of komorebic start.

A note has been added recommending that users disable system animations
for the best experience in the Getting Started guide.
2024-05-15 11:52:13 -07:00
58 changed files with 6005 additions and 1421 deletions

View File

@@ -93,9 +93,10 @@ jobs:
target/${{ matrix.target }}/release/komorebi.exe
target/${{ matrix.target }}/release/komorebic.exe
target/${{ matrix.target }}/release/komorebic-no-console.exe
target/${{ matrix.target }}/release/komorebi-gui.exe
target/${{ matrix.target }}/release/komorebi.pdb
target/${{ matrix.target }}/release/komorebic.pdb
target/${{ matrix.target }}/release/komorebic-no-console.pdb
target/${{ matrix.target }}/release/komorebi-gui.pdb
target/wix/komorebi-*.msi
retention-days: 7
- name: Check GoReleaser

View File

@@ -35,6 +35,15 @@ builds:
post:
- mkdir -p dist/windows_amd64
- cp ".\target\x86_64-pc-windows-msvc\release\komorebic-no-console.exe" ".\dist\komorebic-no-console_windows_amd64_v1\komorebic-no-console.exe"
- id: komorebi-gui
main: dummy.go
goos: [ "windows" ]
goarch: [ "amd64" ]
binary: komorebi-gui
hooks:
post:
- mkdir -p dist/windows_amd64
- cp ".\target\x86_64-pc-windows-msvc\release\komorebi-gui.exe" ".\dist\komorebi-gui_windows_amd64_v1\komorebi-gui.exe"
archives:
- name_template: "{{ .ProjectName }}-{{ .Version }}-x86_64-pc-windows-msvc"

3174
Cargo.lock generated

File diff suppressed because it is too large Load Diff

View File

@@ -6,6 +6,7 @@ members = [
"komorebi",
"komorebi-client",
"komorebi-core",
"komorebi-gui",
"komorebic",
"komorebic-no-console",
]

View File

@@ -1,26 +0,0 @@
# active-window-border-colour
```
Set the colour for the active window border
Usage: komorebic.exe active-window-border-colour [OPTIONS] <R> <G> <B>
Arguments:
<R>
Red
<G>
Green
<B>
Blue
Options:
-w, --window-kind <WINDOW_KIND>
[default: single]
[possible values: single, stack, monocle]
-h, --help
Print help
```

View File

@@ -1,16 +0,0 @@
# active-window-border-offset
```
Set the offset for the active window border
Usage: komorebic.exe active-window-border-offset <OFFSET>
Arguments:
<OFFSET>
Desired offset of the active window border
Options:
-h, --help
Print help
```

View File

@@ -1,16 +0,0 @@
# active-window-border-width
```
Set the width for the active window border
Usage: komorebic.exe active-window-border-width <WIDTH>
Arguments:
<WIDTH>
Desired width of the active window border
Options:
-h, --help
Print help
```

26
docs/cli/border-colour.md Normal file
View File

@@ -0,0 +1,26 @@
# border-colour
```
Set the colour for a window border kind
Usage: komorebic.exe border-colour [OPTIONS] <R> <G> <B>
Arguments:
<R>
Red
<G>
Green
<B>
Blue
Options:
-w, --window-kind <WINDOW_KIND>
[default: single]
[possible values: single, stack, monocle, unfocused]
-h, --help
Print help
```

16
docs/cli/border-offset.md Normal file
View File

@@ -0,0 +1,16 @@
# border-offset
```
Set the border offset
Usage: komorebic.exe border-offset <OFFSET>
Arguments:
<OFFSET>
Desired offset of the window border
Options:
-h, --help
Print help
```

16
docs/cli/border-width.md Normal file
View File

@@ -0,0 +1,16 @@
# border-width
```
Set the border width
Usage: komorebic.exe border-width <WIDTH>
Arguments:
<WIDTH>
Desired width of the window border
Options:
-h, --help
Print help
```

View File

@@ -1,9 +1,9 @@
# active-window-border
# border
```
Enable or disable the active window border
Enable or disable borders
Usage: komorebic.exe active-window-border <BOOLEAN_STATE>
Usage: komorebic.exe border <BOOLEAN_STATE>
Arguments:
<BOOLEAN_STATE>

View File

@@ -10,6 +10,7 @@ Arguments:
Possible values:
- swap: Swap the window container with the window container at the edge of the adjacent monitor
- insert: Insert the window container into the focused workspace on the adjacent monitor
- no-op: Do nothing if trying to move a window container in the direction of an adjacent monitor
Options:
-h, --help

View File

@@ -0,0 +1,16 @@
# cycle-move-workspace-to-monitor
```
Move the focused workspace monitor in the given cycle direction
Usage: komorebic.exe cycle-move-workspace-to-monitor <CYCLE_DIRECTION>
Arguments:
<CYCLE_DIRECTION>
[possible values: previous, next]
Options:
-h, --help
Print help
```

12
docs/cli/gui.md Normal file
View File

@@ -0,0 +1,12 @@
# gui
```
Launch the komorebi-gui debugging tool
Usage: komorebic.exe gui
Options:
-h, --help
Print help
```

View File

@@ -0,0 +1,16 @@
# promote-window
```
Promote the window in the specified direction
Usage: komorebic.exe promote-window <OPERATION_DIRECTION>
Arguments:
<OPERATION_DIRECTION>
[possible values: left, right, up, down]
Options:
-h, --help
Print help
```

View File

@@ -1,39 +0,0 @@
# Active Window Border
If you would like to add a visual border around the currently focused window,
ensure the following options are defined in the `komorebi.json` configuration
file.
```json
{
"active_window_border": true,
"active_window_border_style": "Rounded",
"active_window_border_colours": {
"single": {
"r": 66,
"g": 165,
"b": 245
},
"stack": {
"r": 256,
"g": 165,
"b": 66
},
"monocle": {
"r": 255,
"g": 51,
"b": 153
}
},
}
```
It is important to note that the active window border will only apply to
windows managed by `komorebi`.
This feature is not considered stable, and you may encounter visual artifacts
from time to time.
[![Watch the tutorial
video](https://img.youtube.com/vi/7_9D22t7KK4/hqdefault.jpg)](https://www.youtube.com/watch?v=7_9D22t7KK4)

View File

@@ -1,7 +1,5 @@
# AutoHotKey
<!-- TODO: Update this completely -->
If you would like to use Autohotkey, please make sure you have AutoHotKey v2
installed.
@@ -9,19 +7,16 @@ Generally, users who opt for AHK will have specific needs that can only be
addressed by the advanced functionality of AHK, and so they are assumed to be
able to craft their own configuration files.
If you would like to try out AHK, a simple sample configuration powered by
`komorebic.lib.ahk` is provided as a starting point. This sample configuration
does not take into account the use of a static configuration file; if you
choose to use a static configuration file alongside AHK, you can remove all the
configuration options from your `komorebi.ahk` and use it solely to handle
hotkey bindings.
If you would like to try out AHK, here is a simple sample configuration which
largely matches the `whkdrc` sample configuration.
```powershell
# save the latest generated komorebic library to ~/komorebic.lib.ahk
iwr https://raw.githubusercontent.com/LGUG2Z/komorebi/v0.1.20/komorebic.lib.ahk -OutFile $Env:USERPROFILE\komorebic.lib.ahk
# save the sample komorebi configuration file to ~/komorebi.ahk
iwr https://raw.githubusercontent.com/LGUG2Z/komorebi/v0.1.20/komorebi.sample.ahk -OutFile $Env:USERPROFILE\komorebi.ahk
```
{% include "../komorebi.ahk" %}
```
By default, the `komorebi.ahk` file should be located in the `$Env:USERPROFILE`
directory, however, if `$Env:KOMOREBI_CONFIG_HOME` is set, it should be located
there.
Once the file is in place, you can stop komorebi and whkd by running `komorebic stop --whkd`,
and then start komorebi with Autohotkey by running `komorebic start --ahk`.

View File

@@ -0,0 +1,28 @@
# Borders
If you would like to add a visual border around both the currently focused window
and unfocused windows ensure the following options are defined in the `komorebi.json`
configuration file.
```json
{
"border": true,
"border_width": 8,
"border_offset": -1,
"border_style": "System",
"border_colours": {
"single": "#42a5f5",
"stack": "#00a542",
"monocle": "#ff3399",
"unfocused": "#808080"
}
}
```
It is important to note that borders will only apply to windows managed by `komorebi`.
This feature is not considered stable, and you may encounter visual artifacts
from time to time.
[![Watch the tutorial
video](https://img.youtube.com/vi/7_9D22t7KK4/hqdefault.jpg)](https://www.youtube.com/watch?v=7_9D22t7KK4)

View File

@@ -30,10 +30,10 @@ to manipulate the window manager, you use
[WinGet](https://winget.run/pkg/LGUG2Z/komorebi), and you may also built
it from [source](https://github.com/LGUG2Z/komorebi) if you would prefer.
- [Scoop](#scoop)
- [WinGet](#winget)
- [Building from source](#building-from-source)
- [Offline](#offline)
- [Scoop](#scoop)
- [WinGet](#winget)
- [Building from source](#building-from-source)
- [Offline](#offline)
## Long path support
@@ -45,6 +45,12 @@ running the following command in an Administrator Terminal before installing
Set-ItemProperty 'HKLM:\SYSTEM\CurrentControlSet\Control\FileSystem' -Name 'LongPathsEnabled' -Value 1
```
## Disabling Unnecessary System Animations
It is highly recommended that you enable the "Turn off all unnecessary animations (when possible)" option in
"Control Panel > Ease of Access > Ease of Access Centre / Make the computer easier to see" for the best performance with
komorebi.
## Scoop
Make sure you have installed [`scoop`](https://scoop.sh) and verified that
@@ -108,6 +114,7 @@ Clone the git repository, enter the directory, and build the following binaries:
cargo +stable install --path komorebi --locked
cargo +stable install --path komorebic --locked
cargo +stable install --path komorebic-no-console --locked
cargo +stable install --path komorebi-gui --locked
```
If the binaries have been built and added to your `$PATH` correctly, you should

View File

@@ -5,17 +5,18 @@
"cross_monitor_move_behaviour": "Insert",
"default_workspace_padding": 20,
"default_container_padding": 20,
"border": true,
"border_width": 8,
"border_offset": -1,
"active_window_border": false,
"active_window_border_colours": {
"border_colours": {
"single": "#42a5f5",
"stack": "#00a542",
"monocle": "#ff3399"
"monocle": "#ff3399",
"unfocused": "#808080"
},
"stackbar": {
"height": 40,
"mode": "Never",
"mode": "OnStack",
"tabs": {
"width": 300,
"focused_text": "#00a542",

View File

@@ -10,6 +10,9 @@ alt + shift + o : komorebic reload-configuration
# alt + f : if ($wshell.AppActivate('Firefox') -eq $False) { start firefox }
# alt + b : if ($wshell.AppActivate('Chrome') -eq $False) { start chrome }
alt + q : komorebic close
alt + m : komorebic minimize
# Focus windows
alt + h : komorebic focus left
alt + j : komorebic focus down
@@ -56,8 +59,18 @@ alt + y : komorebic flip-layout vertical
alt + 1 : komorebic focus-workspace 0
alt + 2 : komorebic focus-workspace 1
alt + 3 : komorebic focus-workspace 2
alt + 4 : komorebic focus-workspace 3
alt + 5 : komorebic focus-workspace 4
alt + 6 : komorebic focus-workspace 5
alt + 7 : komorebic focus-workspace 6
alt + 8 : komorebic focus-workspace 7
# Move windows across workspaces
alt + shift + 1 : komorebic move-to-workspace 0
alt + shift + 2 : komorebic move-to-workspace 1
alt + shift + 3 : komorebic move-to-workspace 2
alt + shift + 4 : komorebic move-to-workspace 3
alt + shift + 5 : komorebic move-to-workspace 4
alt + shift + 6 : komorebic move-to-workspace 5
alt + shift + 7 : komorebic move-to-workspace 6
alt + shift + 8 : komorebic move-to-workspace 7

View File

@@ -16,12 +16,6 @@ fmt:
install-target target:
cargo +stable install --path {{ target }} --locked
prepare:
komorebic ahk-asc '~/.config/komorebi/applications.yaml'
komorebic pwsh-asc '~/.config/komorebi/applications.yaml'
cat '~/.config/komorebi/komorebi.generated.ps1' >komorebi.generated.ps1
cat '~/.config/komorebi/komorebi.generated.ahk' >komorebi.generated.ahk
install:
just install-target komorebic
just install-target komorebic-no-console

View File

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

View File

@@ -9,7 +9,7 @@ pub use komorebi::ring::Ring;
pub use komorebi::window::Window;
pub use komorebi::window_manager_event::WindowManagerEvent;
pub use komorebi::workspace::Workspace;
pub use komorebi::ActiveWindowBorderColours;
pub use komorebi::BorderColours;
pub use komorebi::GlobalState;
pub use komorebi::Notification;
pub use komorebi::NotificationEvent;
@@ -18,9 +18,9 @@ pub use komorebi::StackbarConfig;
pub use komorebi::State;
pub use komorebi::StaticConfig;
pub use komorebi::TabsConfig;
pub use komorebi_core::ActiveWindowBorderStyle;
pub use komorebi_core::Arrangement;
pub use komorebi_core::Axis;
pub use komorebi_core::BorderStyle;
pub use komorebi_core::CustomLayout;
pub use komorebi_core::CycleDirection;
pub use komorebi_core::DefaultLayout;
@@ -29,6 +29,7 @@ pub use komorebi_core::Layout;
pub use komorebi_core::OperationDirection;
pub use komorebi_core::Rect;
pub use komorebi_core::SocketMessage;
pub use komorebi_core::StackbarLabel;
pub use komorebi_core::StackbarMode;
pub use komorebi_core::WindowKind;

View File

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

View File

@@ -59,6 +59,7 @@ pub enum SocketMessage {
SendContainerToMonitorWorkspaceNumber(usize, usize),
MoveContainerToMonitorWorkspaceNumber(usize, usize),
SendContainerToNamedWorkspace(String),
CycleMoveWorkspaceToMonitor(CycleDirection),
MoveWorkspaceToMonitorNumber(usize),
SwapWorkspacesToMonitorNumber(usize),
ForceFocus,
@@ -66,6 +67,7 @@ pub enum SocketMessage {
Minimize,
Promote,
PromoteFocus,
PromoteWindow(OperationDirection),
ToggleFloat,
ToggleMonocle,
ToggleMaximize,
@@ -130,13 +132,17 @@ pub enum SocketMessage {
WatchConfiguration(bool),
CompleteConfiguration,
AltFocusHack(bool),
ActiveWindowBorder(bool),
ActiveWindowBorderColour(WindowKind, u32, u32, u32),
ActiveWindowBorderStyle(ActiveWindowBorderStyle),
#[serde(alias = "ActiveWindowBorder")]
Border(bool),
#[serde(alias = "ActiveWindowBorderColour")]
BorderColour(WindowKind, u32, u32, u32),
#[serde(alias = "ActiveWindowBorderStyle")]
BorderStyle(BorderStyle),
BorderWidth(i32),
BorderOffset(i32),
InvisibleBorders(Rect),
StackbarMode(StackbarMode),
StackbarLabel(StackbarLabel),
StackbarFocusedTextColour(u32, u32, u32),
StackbarUnfocusedTextColour(u32, u32, u32),
StackbarBackgroundColour(u32, u32, u32),
@@ -198,10 +204,19 @@ pub enum StackbarMode {
OnStack,
}
#[derive(
Debug, Copy, Default, Clone, Eq, PartialEq, Display, Serialize, Deserialize, JsonSchema,
)]
pub enum StackbarLabel {
#[default]
Process,
Title,
}
#[derive(
Default, Copy, Clone, Debug, Eq, PartialEq, Display, Serialize, Deserialize, JsonSchema,
)]
pub enum ActiveWindowBorderStyle {
pub enum BorderStyle {
#[default]
/// Use the system border style
System,

14
komorebi-gui/Cargo.toml Normal file
View File

@@ -0,0 +1,14 @@
[package]
name = "komorebi-gui"
version = "0.1.27-dev.0"
edition = "2021"
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
[dependencies]
egui_extras = { version = "0.27" }
eframe = "0.27"
komorebi-client = { path = "../komorebi-client" }
serde_json = "1"
random_word = { version = "0.4.3", features = ["en"] }
windows = { workspace = true }

805
komorebi-gui/src/main.rs Normal file
View File

@@ -0,0 +1,805 @@
use eframe::egui;
use eframe::egui::color_picker::Alpha;
use eframe::egui::Color32;
use eframe::egui::ViewportBuilder;
use komorebi_client::BorderStyle;
use komorebi_client::Colour;
use komorebi_client::DefaultLayout;
use komorebi_client::GlobalState;
use komorebi_client::Layout;
use komorebi_client::Rect;
use komorebi_client::Rgb;
use komorebi_client::RuleDebug;
use komorebi_client::SocketMessage;
use komorebi_client::StackbarLabel;
use komorebi_client::StackbarMode;
use komorebi_client::State;
use komorebi_client::Window;
use komorebi_client::WindowKind;
use std::collections::HashMap;
use std::time::Duration;
use windows::Win32::UI::WindowsAndMessaging::EnumWindows;
fn main() {
let native_options = eframe::NativeOptions {
viewport: ViewportBuilder::default()
.with_always_on_top()
.with_inner_size([320.0, 500.0]),
follow_system_theme: true,
..Default::default()
};
let _ = eframe::run_native(
"komorebi-gui",
native_options,
Box::new(|cc| Box::new(KomorebiGui::new(cc))),
);
}
struct BorderColours {
single: Color32,
stack: Color32,
monocle: Color32,
unfocused: Color32,
}
struct BorderConfig {
border_enabled: bool,
border_colours: BorderColours,
border_style: BorderStyle,
border_offset: i32,
border_width: i32,
}
struct StackbarConfig {
mode: StackbarMode,
label: StackbarLabel,
height: i32,
width: i32,
focused_text_colour: Color32,
unfocused_text_colour: Color32,
background_colour: Color32,
}
struct MonitorConfig {
size: Rect,
work_area_offset: Rect,
workspaces: Vec<WorkspaceConfig>,
}
impl From<&komorebi_client::Monitor> for MonitorConfig {
fn from(value: &komorebi_client::Monitor) -> Self {
let mut workspaces = vec![];
for ws in value.workspaces() {
workspaces.push(WorkspaceConfig::from(ws));
}
Self {
size: *value.size(),
work_area_offset: value.work_area_offset().unwrap_or_default(),
workspaces,
}
}
}
struct WorkspaceConfig {
name: String,
tile: bool,
layout: DefaultLayout,
container_padding: i32,
workspace_padding: i32,
}
impl From<&komorebi_client::Workspace> for WorkspaceConfig {
fn from(value: &komorebi_client::Workspace) -> Self {
let layout = match value.layout() {
Layout::Default(layout) => *layout,
Layout::Custom(_) => DefaultLayout::BSP,
};
let name = value
.name()
.to_owned()
.unwrap_or_else(|| random_word::gen(random_word::Lang::En).to_string());
Self {
layout,
name,
tile: *value.tile(),
workspace_padding: value.workspace_padding().unwrap_or(20),
container_padding: value.container_padding().unwrap_or(20),
}
}
}
struct KomorebiGui {
border_config: BorderConfig,
stackbar_config: StackbarConfig,
mouse_follows_focus: bool,
monitors: Vec<MonitorConfig>,
workspace_names: HashMap<usize, Vec<String>>,
debug_hwnd: isize,
debug_windows: Vec<Window>,
debug_rule: Option<RuleDebug>,
}
fn colour32(colour: Option<Colour>) -> Color32 {
match colour {
Some(Colour::Rgb(rgb)) => Color32::from_rgb(rgb.r as u8, rgb.g as u8, rgb.b as u8),
Some(Colour::Hex(hex)) => {
let rgb = Rgb::from(hex);
Color32::from_rgb(rgb.r as u8, rgb.g as u8, rgb.b as u8)
}
None => Color32::from_rgb(0, 0, 0),
}
}
impl KomorebiGui {
fn new(_cc: &eframe::CreationContext<'_>) -> Self {
// Customize egui here with cc.egui_ctx.set_fonts and cc.egui_ctx.set_visuals.
// Restore app state using cc.storage (requires the "persistence" feature).
// Use the cc.gl (a glow::Context) to create graphics shaders and buffers that you can use
// for e.g. egui::PaintCallback.
let global_state: GlobalState = serde_json::from_str(
&komorebi_client::send_query(&SocketMessage::GlobalState).unwrap(),
)
.unwrap();
let state: State =
serde_json::from_str(&komorebi_client::send_query(&SocketMessage::State).unwrap())
.unwrap();
let border_colours = BorderColours {
single: colour32(global_state.border_colours.single),
stack: colour32(global_state.border_colours.stack),
monocle: colour32(global_state.border_colours.monocle),
unfocused: colour32(global_state.border_colours.unfocused),
};
let border_config = BorderConfig {
border_enabled: global_state.border_enabled,
border_colours,
border_style: global_state.border_style,
border_offset: global_state.border_offset,
border_width: global_state.border_width,
};
let mut monitors = vec![];
for m in state.monitors.elements() {
monitors.push(MonitorConfig::from(m));
}
let mut workspace_names = HashMap::new();
for (monitor_idx, m) in monitors.iter().enumerate() {
for ws in &m.workspaces {
let names = workspace_names.entry(monitor_idx).or_insert_with(Vec::new);
names.push(ws.name.clone());
}
}
let stackbar_config = StackbarConfig {
mode: global_state.stackbar_mode,
height: global_state.stackbar_height,
width: global_state.stackbar_tab_width,
label: global_state.stackbar_label,
focused_text_colour: colour32(Some(global_state.stackbar_focused_text_colour)),
unfocused_text_colour: colour32(Some(global_state.stackbar_unfocused_text_colour)),
background_colour: colour32(Some(global_state.stackbar_tab_background_colour)),
};
let mut debug_windows = vec![];
unsafe {
EnumWindows(
Some(enum_window),
windows::Win32::Foundation::LPARAM(&mut debug_windows as *mut Vec<Window> as isize),
)
.unwrap();
};
Self {
border_config,
mouse_follows_focus: state.mouse_follows_focus,
monitors,
workspace_names,
debug_hwnd: 0,
debug_windows,
stackbar_config,
debug_rule: None,
}
}
}
extern "system" fn enum_window(
hwnd: windows::Win32::Foundation::HWND,
lparam: windows::Win32::Foundation::LPARAM,
) -> windows::Win32::Foundation::BOOL {
let windows = unsafe { &mut *(lparam.0 as *mut Vec<Window>) };
let window = Window { hwnd: hwnd.0 };
if window.is_window()
&& !window.is_miminized()
&& window.is_visible()
&& window.title().is_ok()
&& window.exe().is_ok()
{
windows.push(window);
}
true.into()
}
fn json_view_ui(ui: &mut egui::Ui, code: &str) {
let language = "json";
let theme = egui_extras::syntax_highlighting::CodeTheme::from_memory(ui.ctx());
egui_extras::syntax_highlighting::code_view_ui(ui, &theme, code, language);
}
impl eframe::App for KomorebiGui {
fn update(&mut self, ctx: &egui::Context, _frame: &mut eframe::Frame) {
egui::CentralPanel::default().show(ctx, |ui| {
ctx.set_pixels_per_point(2.0);
egui::ScrollArea::vertical().show(ui, |ui| {
ui.set_width(ctx.screen_rect().width());
ui.collapsing("Debugging", |ui| {
ui.collapsing("Window Rules", |ui| {
let window = Window {
hwnd: self.debug_hwnd,
};
let label = if let (Ok(title), Ok(exe)) = (window.title(), window.exe()) {
format!("{title} ({exe})")
} else {
String::from("Select a Window")
};
if ui.button("Refresh Windows").clicked() {
let mut debug_windows = vec![];
unsafe {
EnumWindows(
Some(enum_window),
windows::Win32::Foundation::LPARAM(
&mut debug_windows as *mut Vec<Window> as isize,
),
)
.unwrap();
};
self.debug_windows = debug_windows;
}
egui::ComboBox::from_label("Select a Window")
.selected_text(label)
.show_ui(ui, |ui| {
for w in &self.debug_windows {
if ui
.selectable_value(
&mut self.debug_hwnd,
w.hwnd,
format!(
"{} ({})",
w.title().unwrap(),
w.exe().unwrap()
),
)
.changed()
{
let debug_rule: RuleDebug = serde_json::from_str(
&komorebi_client::send_query(
&SocketMessage::DebugWindow(self.debug_hwnd),
)
.unwrap(),
)
.unwrap();
self.debug_rule = Some(debug_rule)
}
}
});
if let Some(debug_rule) = &self.debug_rule {
json_view_ui(ui, &serde_json::to_string_pretty(debug_rule).unwrap())
}
});
});
ui.collapsing("Mouse", |ui| {
if ui
.toggle_value(&mut self.mouse_follows_focus, "Mouse Follows Focus")
.changed()
{
komorebi_client::send_message(&SocketMessage::MouseFollowsFocus(
self.mouse_follows_focus,
))
.unwrap();
}
});
ui.collapsing("Border", |ui| {
if ui
.toggle_value(&mut self.border_config.border_enabled, "Border")
.changed()
{
komorebi_client::send_message(&SocketMessage::Border(
self.border_config.border_enabled,
))
.unwrap();
}
ui.collapsing("Colours", |ui| {
ui.collapsing("Single", |ui| {
if egui::color_picker::color_picker_color32(
ui,
&mut self.border_config.border_colours.single,
Alpha::Opaque,
) {
komorebi_client::send_message(&SocketMessage::BorderColour(
WindowKind::Single,
self.border_config.border_colours.single.r() as u32,
self.border_config.border_colours.single.g() as u32,
self.border_config.border_colours.single.b() as u32,
))
.unwrap();
}
});
ui.collapsing("Stack", |ui| {
if egui::color_picker::color_picker_color32(
ui,
&mut self.border_config.border_colours.stack,
Alpha::Opaque,
) {
komorebi_client::send_message(&SocketMessage::BorderColour(
WindowKind::Stack,
self.border_config.border_colours.stack.r() as u32,
self.border_config.border_colours.stack.g() as u32,
self.border_config.border_colours.stack.b() as u32,
))
.unwrap();
}
});
ui.collapsing("Monocle", |ui| {
if egui::color_picker::color_picker_color32(
ui,
&mut self.border_config.border_colours.monocle,
Alpha::Opaque,
) {
komorebi_client::send_message(&SocketMessage::BorderColour(
WindowKind::Monocle,
self.border_config.border_colours.monocle.r() as u32,
self.border_config.border_colours.monocle.g() as u32,
self.border_config.border_colours.monocle.b() as u32,
))
.unwrap();
}
});
ui.collapsing("Unfocused", |ui| {
if egui::color_picker::color_picker_color32(
ui,
&mut self.border_config.border_colours.unfocused,
Alpha::Opaque,
) {
komorebi_client::send_message(&SocketMessage::BorderColour(
WindowKind::Unfocused,
self.border_config.border_colours.unfocused.r() as u32,
self.border_config.border_colours.unfocused.g() as u32,
self.border_config.border_colours.unfocused.b() as u32,
))
.unwrap();
}
})
});
ui.collapsing("Style", |ui| {
for option in [
BorderStyle::System,
BorderStyle::Rounded,
BorderStyle::Square,
] {
if ui
.add(egui::SelectableLabel::new(
self.border_config.border_style == option,
option.to_string(),
))
.clicked()
{
self.border_config.border_style = option;
komorebi_client::send_message(&SocketMessage::BorderStyle(
self.border_config.border_style,
))
.unwrap();
std::thread::sleep(Duration::from_secs(1));
komorebi_client::send_message(&SocketMessage::Retile).unwrap();
}
}
});
ui.collapsing("Width", |ui| {
if ui
.add(egui::Slider::new(
&mut self.border_config.border_width,
-50..=50,
))
.changed()
{
komorebi_client::send_message(&SocketMessage::BorderWidth(
self.border_config.border_width,
))
.unwrap();
};
});
ui.collapsing("Offset", |ui| {
if ui
.add(egui::Slider::new(
&mut self.border_config.border_offset,
-50..=50,
))
.changed()
{
komorebi_client::send_message(&SocketMessage::BorderOffset(
self.border_config.border_offset,
))
.unwrap();
};
});
});
ui.collapsing("Stackbar", |ui| {
for option in [
StackbarMode::Never,
StackbarMode::OnStack,
StackbarMode::Always,
] {
if ui
.add(egui::SelectableLabel::new(
self.stackbar_config.mode == option,
option.to_string(),
))
.clicked()
{
self.stackbar_config.mode = option;
komorebi_client::send_message(&SocketMessage::StackbarMode(
self.stackbar_config.mode,
))
.unwrap();
komorebi_client::send_message(&SocketMessage::Retile).unwrap()
}
}
ui.collapsing("Label", |ui| {
for option in [StackbarLabel::Process, StackbarLabel::Title] {
if ui
.add(egui::SelectableLabel::new(
self.stackbar_config.label == option,
option.to_string(),
))
.clicked()
{
self.stackbar_config.label = option;
komorebi_client::send_message(&SocketMessage::StackbarLabel(
self.stackbar_config.label,
))
.unwrap();
}
}
});
ui.collapsing("Colours", |ui| {
ui.collapsing("Focused Text", |ui| {
if egui::color_picker::color_picker_color32(
ui,
&mut self.stackbar_config.focused_text_colour,
Alpha::Opaque,
) {
komorebi_client::send_message(
&SocketMessage::StackbarFocusedTextColour(
self.stackbar_config.focused_text_colour.r() as u32,
self.stackbar_config.focused_text_colour.g() as u32,
self.stackbar_config.focused_text_colour.b() as u32,
),
)
.unwrap();
}
});
ui.collapsing("Unfocused Text", |ui| {
if egui::color_picker::color_picker_color32(
ui,
&mut self.stackbar_config.unfocused_text_colour,
Alpha::Opaque,
) {
komorebi_client::send_message(
&SocketMessage::StackbarUnfocusedTextColour(
self.stackbar_config.unfocused_text_colour.r() as u32,
self.stackbar_config.unfocused_text_colour.g() as u32,
self.stackbar_config.unfocused_text_colour.b() as u32,
),
)
.unwrap();
}
});
ui.collapsing("Background", |ui| {
if egui::color_picker::color_picker_color32(
ui,
&mut self.stackbar_config.background_colour,
Alpha::Opaque,
) {
komorebi_client::send_message(
&SocketMessage::StackbarBackgroundColour(
self.stackbar_config.background_colour.r() as u32,
self.stackbar_config.background_colour.g() as u32,
self.stackbar_config.background_colour.b() as u32,
),
)
.unwrap();
}
})
});
ui.collapsing("Width", |ui| {
if ui
.add(egui::Slider::new(&mut self.stackbar_config.width, 0..=500))
.drag_stopped()
{
komorebi_client::send_message(&SocketMessage::StackbarTabWidth(
self.stackbar_config.width,
))
.unwrap();
komorebi_client::send_message(&SocketMessage::Retile).unwrap()
};
});
ui.collapsing("Height", |ui| {
if ui
.add(egui::Slider::new(&mut self.stackbar_config.height, 0..=100))
.drag_stopped()
{
komorebi_client::send_message(&SocketMessage::StackbarHeight(
self.stackbar_config.height,
))
.unwrap();
komorebi_client::send_message(&SocketMessage::Retile).unwrap()
};
});
});
for (monitor_idx, monitor) in self.monitors.iter_mut().enumerate() {
ui.collapsing(
format!(
"Monitor {monitor_idx} ({}x{})",
monitor.size.right, monitor.size.bottom
),
|ui| {
ui.collapsing("Work Area Offset", |ui| {
if ui
.add(
egui::Slider::new(
&mut monitor.work_area_offset.left,
0..=500,
)
.text("Left"),
)
.drag_stopped()
{
komorebi_client::send_message(
&SocketMessage::MonitorWorkAreaOffset(
monitor_idx,
monitor.work_area_offset,
),
)
.unwrap();
};
if ui
.add(
egui::Slider::new(
&mut monitor.work_area_offset.top,
0..=500,
)
.text("Top"),
)
.drag_stopped()
{
komorebi_client::send_message(
&SocketMessage::MonitorWorkAreaOffset(
monitor_idx,
monitor.work_area_offset,
),
)
.unwrap();
};
if ui
.add(
egui::Slider::new(
&mut monitor.work_area_offset.right,
0..=500,
)
.text("Right"),
)
.drag_stopped()
{
komorebi_client::send_message(
&SocketMessage::MonitorWorkAreaOffset(
monitor_idx,
monitor.work_area_offset,
),
)
.unwrap();
};
if ui
.add(
egui::Slider::new(
&mut monitor.work_area_offset.bottom,
0..=500,
)
.text("Bottom"),
)
.drag_stopped()
{
komorebi_client::send_message(
&SocketMessage::MonitorWorkAreaOffset(
monitor_idx,
monitor.work_area_offset,
),
)
.unwrap();
};
});
ui.collapsing("Workspaces", |ui| {
for (workspace_idx, workspace) in
monitor.workspaces.iter_mut().enumerate()
{
ui.collapsing(
format!("Workspace {workspace_idx} ({})", workspace.name),
|ui| {
if ui.button("Focus").clicked() {
komorebi_client::send_message(
&SocketMessage::MouseFollowsFocus(false),
)
.unwrap();
komorebi_client::send_message(
&SocketMessage::FocusMonitorWorkspaceNumber(
monitor_idx,
workspace_idx,
),
)
.unwrap();
komorebi_client::send_message(
&SocketMessage::MouseFollowsFocus(
self.mouse_follows_focus,
),
)
.unwrap();
}
if ui
.toggle_value(&mut workspace.tile, "Tiling")
.changed()
{
komorebi_client::send_message(
&SocketMessage::WorkspaceTiling(
monitor_idx,
workspace_idx,
workspace.tile,
),
)
.unwrap();
}
ui.collapsing("Name", |ui| {
let monitor_workspaces = self
.workspace_names
.get_mut(&monitor_idx)
.unwrap();
let workspace_name =
&mut monitor_workspaces[workspace_idx];
if ui
.text_edit_singleline(workspace_name)
.lost_focus()
{
workspace.name = workspace_name.clone();
komorebi_client::send_message(
&SocketMessage::WorkspaceName(
monitor_idx,
workspace_idx,
workspace.name.clone(),
),
)
.unwrap();
}
});
ui.collapsing("Layout", |ui| {
for option in [
DefaultLayout::BSP,
DefaultLayout::Columns,
DefaultLayout::Rows,
DefaultLayout::VerticalStack,
DefaultLayout::HorizontalStack,
DefaultLayout::UltrawideVerticalStack,
DefaultLayout::Grid,
] {
if ui
.add(egui::SelectableLabel::new(
workspace.layout == option,
option.to_string(),
))
.clicked()
{
workspace.layout = option;
komorebi_client::send_message(
&SocketMessage::WorkspaceLayout(
monitor_idx,
workspace_idx,
workspace.layout,
),
)
.unwrap();
}
}
});
ui.collapsing("Container Padding", |ui| {
if ui
.add(egui::Slider::new(
&mut workspace.container_padding,
0..=100,
))
.drag_stopped()
{
komorebi_client::send_message(
&SocketMessage::ContainerPadding(
monitor_idx,
workspace_idx,
workspace.container_padding,
),
)
.unwrap();
};
});
ui.collapsing("Workspace Padding", |ui| {
if ui
.add(egui::Slider::new(
&mut workspace.workspace_padding,
0..=100,
))
.drag_stopped()
{
komorebi_client::send_message(
&SocketMessage::WorkspacePadding(
monitor_idx,
workspace_idx,
workspace.workspace_padding,
),
)
.unwrap();
};
});
},
);
}
});
},
);
}
});
});
}
}

View File

@@ -1,6 +1,6 @@
[package]
name = "komorebi"
version = "0.1.26-dev.0"
version = "0.1.27-dev.0"
authors = ["Jade Iqbal <jadeiqbal@fastmail.com>"]
description = "A tiling window manager for Windows"
categories = ["tiling-window-manager", "windows"]
@@ -48,5 +48,7 @@ windows-interface = { workspace = true }
winput = "0.2"
winreg = "0.52"
win32-display-data = { git = "https://github.com/LGUG2Z/win32-display-data" }
[features]
deadlock_detection = []

View File

@@ -12,11 +12,12 @@ use crate::border_manager::Z_ORDER;
use crate::WindowsApi;
use crate::WINDOWS_11;
use komorebi_core::ActiveWindowBorderStyle;
use komorebi_core::BorderStyle;
use komorebi_core::Rect;
use std::sync::atomic::Ordering;
use std::sync::mpsc;
use std::time::Duration;
use windows::core::PCWSTR;
use windows::Win32::Foundation::BOOL;
use windows::Win32::Foundation::COLORREF;
@@ -103,6 +104,7 @@ impl Border {
while GetMessageW(&mut message, HWND(hwnd), 0, 0).into() {
TranslateMessage(&message);
DispatchMessageW(&message);
std::thread::sleep(Duration::from_millis(10));
}
}
@@ -193,17 +195,17 @@ impl Border {
// wrong size. In the future we should read the DWM properties
// of windows and attempt to match appropriately.
match *STYLE.lock() {
ActiveWindowBorderStyle::System => {
BorderStyle::System => {
if *WINDOWS_11 {
RoundRect(hdc, 0, 0, rect.right, rect.bottom, 20, 20);
} else {
Rectangle(hdc, 0, 0, rect.right, rect.bottom);
}
}
ActiveWindowBorderStyle::Rounded => {
BorderStyle::Rounded => {
RoundRect(hdc, 0, 0, rect.right, rect.bottom, 20, 20);
}
ActiveWindowBorderStyle::Square => {
BorderStyle::Square => {
Rectangle(hdc, 0, 0, rect.right, rect.bottom);
}
}

View File

@@ -5,7 +5,7 @@ mod border;
use crossbeam_channel::Receiver;
use crossbeam_channel::Sender;
use crossbeam_utils::atomic::AtomicConsume;
use komorebi_core::ActiveWindowBorderStyle;
use komorebi_core::BorderStyle;
use lazy_static::lazy_static;
use parking_lot::Mutex;
use schemars::JsonSchema;
@@ -39,8 +39,7 @@ pub static BORDER_ENABLED: AtomicBool = AtomicBool::new(true);
lazy_static! {
pub static ref Z_ORDER: Arc<Mutex<ZOrder>> = Arc::new(Mutex::new(ZOrder::Bottom));
pub static ref STYLE: Arc<Mutex<ActiveWindowBorderStyle>> =
Arc::new(Mutex::new(ActiveWindowBorderStyle::System));
pub static ref STYLE: Arc<Mutex<BorderStyle>> = Arc::new(Mutex::new(BorderStyle::System));
pub static ref FOCUSED: AtomicU32 =
AtomicU32::new(u32::from(Colour::Rgb(Rgb::new(66, 165, 245))));
pub static ref UNFOCUSED: AtomicU32 =
@@ -160,7 +159,7 @@ pub fn handle_notifications(wm: Arc<Mutex<WindowManager>>) -> color_eyre::Result
let focused_monitor_idx = state.focused_monitor_idx();
for (monitor_idx, m) in state.monitors.elements().iter().enumerate() {
'monitors: for (monitor_idx, m) in state.monitors.elements().iter().enumerate() {
// Only operate on the focused workspace of each monitor
if let Some(ws) = m.focused_workspace() {
// Workspaces with tiling disabled don't have borders
@@ -182,18 +181,6 @@ pub fn handle_notifications(wm: Arc<Mutex<WindowManager>>) -> color_eyre::Result
// Handle the monocle container separately
if let Some(monocle) = ws.monocle_container() {
let mut to_remove = vec![];
for (id, border) in borders.iter() {
if borders_monitors.get(id).copied().unwrap_or_default() == monitor_idx {
border.destroy()?;
to_remove.push(id.clone());
}
}
for id in &to_remove {
borders.remove(id);
}
let border = match borders.entry(monocle.id().clone()) {
Entry::Occupied(entry) => entry.into_mut(),
Entry::Vacant(entry) => {
@@ -209,7 +196,14 @@ pub fn handle_notifications(wm: Arc<Mutex<WindowManager>>) -> color_eyre::Result
{
let mut focus_state = FOCUS_STATE.lock();
focus_state.insert(border.hwnd, WindowKind::Monocle);
focus_state.insert(
border.hwnd,
if monitor_idx != focused_monitor_idx {
WindowKind::Unfocused
} else {
WindowKind::Monocle
},
);
}
let rect = WindowsApi::window_rect(
@@ -217,7 +211,22 @@ pub fn handle_notifications(wm: Arc<Mutex<WindowManager>>) -> color_eyre::Result
)?;
border.update(&rect)?;
continue 'receiver;
let border_hwnd = border.hwnd;
let mut to_remove = vec![];
for (id, b) in borders.iter() {
if borders_monitors.get(id).copied().unwrap_or_default() == monitor_idx
&& border_hwnd != b.hwnd
{
b.destroy()?;
to_remove.push(id.clone());
}
}
for id in &to_remove {
borders.remove(id);
}
continue 'monitors;
}
let is_maximized = WindowsApi::is_zoomed(HWND(
@@ -237,7 +246,7 @@ pub fn handle_notifications(wm: Arc<Mutex<WindowManager>>) -> color_eyre::Result
borders.remove(id);
}
continue 'receiver;
continue 'monitors;
}
// Destroy any borders not associated with the focused workspace

View File

@@ -7,20 +7,13 @@ use serde::Deserialize;
use serde::Serialize;
use crate::ring::Ring;
use crate::stackbar::Stackbar;
use crate::window::Window;
use crate::WindowsApi;
use crate::STACKBAR_MODE;
use komorebi_core::StackbarMode;
#[derive(Debug, Clone, Serialize, Deserialize, Getters, JsonSchema)]
pub struct Container {
#[getset(get = "pub")]
id: String,
windows: Ring<Window>,
#[serde(skip)]
#[getset(get = "pub", get_mut = "pub")]
stackbar: Option<Stackbar>,
}
impl_ring_elements!(Container, Window);
@@ -30,10 +23,6 @@ impl Default for Container {
Self {
id: nanoid!(),
windows: Ring::default(),
stackbar: match *STACKBAR_MODE.lock() {
StackbarMode::Always => Stackbar::create().ok(),
StackbarMode::Never | StackbarMode::OnStack => None,
},
}
}
}
@@ -46,10 +35,6 @@ impl PartialEq for Container {
impl Container {
pub fn hide(&self, omit: Option<isize>) {
if let Some(stackbar) = self.stackbar() {
stackbar.hide();
}
for window in self.windows().iter().rev() {
let mut should_hide = omit.is_none();
@@ -68,10 +53,6 @@ impl Container {
}
pub fn restore(&self) {
if let Some(stackbar) = self.stackbar() {
stackbar.restore();
}
if let Some(window) = self.focused_window() {
window.restore();
}
@@ -124,13 +105,6 @@ impl Container {
pub fn remove_window_by_idx(&mut self, idx: usize) -> Option<Window> {
let window = self.windows_mut().remove(idx);
if matches!(*STACKBAR_MODE.lock(), StackbarMode::OnStack) && self.windows().len() <= 1 {
if let Some(stackbar) = &self.stackbar {
let _ = WindowsApi::close_window(stackbar.hwnd());
self.stackbar = None;
}
}
if idx != 0 {
self.focus_window(idx - 1);
};
@@ -145,14 +119,6 @@ impl Container {
pub fn add_window(&mut self, window: Window) {
self.windows_mut().push_back(window);
if matches!(*STACKBAR_MODE.lock(), StackbarMode::OnStack)
&& self.windows().len() > 1
&& self.stackbar.is_none()
{
self.stackbar = Stackbar::create().ok();
}
self.focus_window(self.windows().len() - 1);
}
@@ -161,41 +127,4 @@ impl Container {
tracing::info!("focusing window");
self.windows.focus(idx);
}
pub fn set_stackbar_mode(&mut self, mode: StackbarMode) {
match mode {
StackbarMode::Always => {
if self.stackbar.is_none() {
self.stackbar = Stackbar::create().ok();
}
}
StackbarMode::Never => {
if let Some(stackbar) = &self.stackbar {
let _ = WindowsApi::close_window(stackbar.hwnd());
}
self.stackbar = None
}
StackbarMode::OnStack => {
if self.windows().len() > 1 && self.stackbar().is_none() {
self.stackbar = Stackbar::create().ok();
}
if let Some(stackbar) = &self.stackbar {
if self.windows().len() == 1 {
let _ = WindowsApi::close_window(stackbar.hwnd());
self.stackbar = None;
}
}
}
}
}
pub fn renew_stackbar(&mut self) {
if let Some(stackbar) = &self.stackbar {
if !WindowsApi::is_window(stackbar.hwnd()) {
self.stackbar = Stackbar::create().ok()
}
}
}
}

View File

@@ -1,75 +0,0 @@
use std::sync::atomic::Ordering;
use color_eyre::Result;
use windows::core::PCWSTR;
use windows::Win32::Foundation::HWND;
use windows::Win32::UI::WindowsAndMessaging::DispatchMessageW;
use windows::Win32::UI::WindowsAndMessaging::FindWindowW;
use windows::Win32::UI::WindowsAndMessaging::GetMessageW;
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::WNDCLASSW;
use crate::windows_callbacks;
use crate::WindowsApi;
use crate::HIDDEN_HWND;
#[derive(Debug, Clone, Copy)]
pub struct Hidden {
pub(crate) hwnd: isize,
}
impl From<isize> for Hidden {
fn from(hwnd: isize) -> Self {
Self { hwnd }
}
}
impl Hidden {
pub const fn hwnd(self) -> HWND {
HWND(self.hwnd)
}
pub fn create(name: &str) -> Result<()> {
let name: Vec<u16> = format!("{name}\0").encode_utf16().collect();
let instance = WindowsApi::module_handle_w()?;
let class_name = PCWSTR(name.as_ptr());
let brush = WindowsApi::create_solid_brush(0);
let window_class = WNDCLASSW {
hInstance: instance.into(),
lpszClassName: class_name,
style: CS_HREDRAW | CS_VREDRAW,
lpfnWndProc: Some(windows_callbacks::hidden_window),
hbrBackground: brush,
..Default::default()
};
let _atom = WindowsApi::register_class_w(&window_class)?;
let name_cl = name.clone();
std::thread::spawn(move || -> Result<()> {
let hwnd = WindowsApi::create_hidden_window(PCWSTR(name_cl.as_ptr()), instance)?;
let hidden = Self::from(hwnd);
let mut message = MSG::default();
unsafe {
while GetMessageW(&mut message, hidden.hwnd(), 0, 0).into() {
DispatchMessageW(&message);
}
}
Ok(())
});
let mut hwnd = HWND(0);
while hwnd == HWND(0) {
hwnd = unsafe { FindWindowW(PCWSTR(name.as_ptr()), PCWSTR::null()) };
}
HIDDEN_HWND.store(hwnd.0, Ordering::SeqCst);
Ok(())
}
}

View File

@@ -4,13 +4,14 @@ pub mod com;
pub mod ring;
pub mod colour;
pub mod container;
pub mod hidden;
pub mod monitor;
pub mod monitor_reconciliator;
pub mod process_command;
pub mod process_event;
pub mod process_movement;
pub mod reaper;
pub mod set_window_position;
pub mod stackbar;
pub mod stackbar_manager;
pub mod static_config;
pub mod styles;
pub mod window;
@@ -33,16 +34,13 @@ use std::path::PathBuf;
use std::process::Command;
use std::sync::atomic::AtomicBool;
use std::sync::atomic::AtomicI32;
use std::sync::atomic::AtomicIsize;
use std::sync::atomic::AtomicU32;
use std::sync::atomic::Ordering;
use std::sync::Arc;
pub use colour::*;
pub use hidden::*;
pub use process_command::*;
pub use process_event::*;
pub use stackbar::*;
pub use static_config::*;
pub use window::*;
pub use window_manager::*;
@@ -58,7 +56,6 @@ use komorebi_core::ApplicationIdentifier;
use komorebi_core::HidingBehaviour;
use komorebi_core::Rect;
use komorebi_core::SocketMessage;
use komorebi_core::StackbarMode;
use os_info::Version;
use parking_lot::Mutex;
use regex::Regex;
@@ -201,7 +198,6 @@ lazy_static! {
// eg. Windows Terminal, IntelliJ IDEA, Firefox
static ref NO_TITLEBAR: Arc<Mutex<Vec<String>>> = Arc::new(Mutex::new(vec![]));
static ref STACKBAR_MODE: Arc<Mutex<StackbarMode >> = Arc::new(Mutex::new(StackbarMode::Never));
static ref WINDOWS_BY_BAR_HWNDS: Arc<Mutex<HashMap<isize, VecDeque<isize>>>> =
Arc::new(Mutex::new(HashMap::new()));
@@ -216,14 +212,6 @@ pub static SESSION_ID: AtomicU32 = AtomicU32::new(0);
pub static REMOVE_TITLEBARS: AtomicBool = AtomicBool::new(false);
pub static HIDDEN_HWND: AtomicIsize = AtomicIsize::new(0);
pub static STACKBAR_FOCUSED_TEXT_COLOUR: AtomicU32 = AtomicU32::new(16777215); // white
pub static STACKBAR_UNFOCUSED_TEXT_COLOUR: AtomicU32 = AtomicU32::new(11776947); // gray text
pub static STACKBAR_TAB_BACKGROUND_COLOUR: AtomicU32 = AtomicU32::new(3355443); // gray
pub static STACKBAR_TAB_HEIGHT: AtomicI32 = AtomicI32::new(40);
pub static STACKBAR_TAB_WIDTH: AtomicI32 = AtomicI32::new(200);
#[must_use]
pub fn current_virtual_desktop() -> Option<Vec<u8>> {
let hkcu = RegKey::predef(HKEY_CURRENT_USER);

View File

@@ -24,12 +24,14 @@ use tracing_subscriber::layer::SubscriberExt;
use tracing_subscriber::EnvFilter;
use komorebi::border_manager;
use komorebi::hidden::Hidden;
use komorebi::load_configuration;
use komorebi::monitor_reconciliator;
use komorebi::process_command::listen_for_commands;
use komorebi::process_command::listen_for_commands_tcp;
use komorebi::process_event::listen_for_events;
use komorebi::process_movement::listen_for_movements;
use komorebi::reaper;
use komorebi::stackbar_manager;
use komorebi::static_config::StaticConfig;
use komorebi::window_manager::WindowManager;
use komorebi::windows_api::WindowsApi;
@@ -52,8 +54,8 @@ fn setup() -> Result<(WorkerGuard, WorkerGuard)> {
std::env::set_var("RUST_LOG", "info");
}
let appender = tracing_appender::rolling::never(std::env::temp_dir(), "komorebi_plaintext.log");
let color_appender = tracing_appender::rolling::never(std::env::temp_dir(), "komorebi.log");
let appender = tracing_appender::rolling::daily(std::env::temp_dir(), "komorebi_plaintext.log");
let color_appender = tracing_appender::rolling::daily(std::env::temp_dir(), "komorebi.log");
let (non_blocking, guard) = tracing_appender::non_blocking(appender);
let (color_non_blocking, color_guard) = tracing_appender::non_blocking(color_appender);
@@ -188,8 +190,6 @@ fn main() -> Result<()> {
#[cfg(feature = "deadlock_detection")]
detect_deadlocks();
Hidden::create("komorebi-hidden")?;
let static_config = opts.config.map_or_else(
|| {
let komorebi_json = HOME_DIR.join("komorebi.json");
@@ -256,7 +256,10 @@ fn main() -> Result<()> {
}
border_manager::listen_for_notifications(wm.clone());
stackbar_manager::listen_for_notifications(wm.clone());
workspace_reconciliator::listen_for_notifications(wm.clone());
monitor_reconciliator::listen_for_notifications(wm.clone())?;
reaper::watch_for_orphans(wm.clone());
let (ctrlc_sender, ctrlc_receiver) = crossbeam_channel::bounded(1);
ctrlc::set_handler(move || {

View File

@@ -27,9 +27,9 @@ pub struct Monitor {
#[getset(get = "pub", set = "pub")]
name: String,
#[getset(get = "pub", set = "pub")]
device: Option<String>,
device: String,
#[getset(get = "pub", set = "pub")]
device_id: Option<String>,
device_id: String,
#[getset(get = "pub", set = "pub")]
size: Rect,
#[getset(get = "pub", set = "pub")]
@@ -50,15 +50,22 @@ pub struct Monitor {
impl_ring_elements!(Monitor, Workspace);
pub fn new(id: isize, size: Rect, work_area_size: Rect, name: String) -> Monitor {
pub fn new(
id: isize,
size: Rect,
work_area_size: Rect,
name: String,
device: String,
device_id: String,
) -> Monitor {
let mut workspaces = Ring::default();
workspaces.elements_mut().push_back(Workspace::default());
Monitor {
id,
name,
device: None,
device_id: None,
device,
device_id,
size,
work_area_size,
work_area_offset: None,

View File

@@ -0,0 +1,190 @@
use std::sync::mpsc;
use windows::core::PCWSTR;
use windows::Win32::Foundation::HWND;
use windows::Win32::Foundation::LPARAM;
use windows::Win32::Foundation::LRESULT;
use windows::Win32::Foundation::WPARAM;
use windows::Win32::UI::WindowsAndMessaging::DefWindowProcW;
use windows::Win32::UI::WindowsAndMessaging::DispatchMessageW;
use windows::Win32::UI::WindowsAndMessaging::GetMessageW;
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::DBT_DEVNODES_CHANGED;
use windows::Win32::UI::WindowsAndMessaging::MSG;
use windows::Win32::UI::WindowsAndMessaging::PBT_APMRESUMEAUTOMATIC;
use windows::Win32::UI::WindowsAndMessaging::PBT_APMRESUMESUSPEND;
use windows::Win32::UI::WindowsAndMessaging::PBT_APMSUSPEND;
use windows::Win32::UI::WindowsAndMessaging::SPI_SETWORKAREA;
use windows::Win32::UI::WindowsAndMessaging::WM_DEVICECHANGE;
use windows::Win32::UI::WindowsAndMessaging::WM_DISPLAYCHANGE;
use windows::Win32::UI::WindowsAndMessaging::WM_POWERBROADCAST;
use windows::Win32::UI::WindowsAndMessaging::WM_SETTINGCHANGE;
use windows::Win32::UI::WindowsAndMessaging::WM_WTSSESSION_CHANGE;
use windows::Win32::UI::WindowsAndMessaging::WNDCLASSW;
use windows::Win32::UI::WindowsAndMessaging::WTS_SESSION_LOCK;
use windows::Win32::UI::WindowsAndMessaging::WTS_SESSION_UNLOCK;
use crate::monitor_reconciliator;
use crate::WindowsApi;
// This is a hidden window specifically spawned to listen to system-wide events related to monitors
#[derive(Debug, Clone, Copy)]
pub struct Hidden {
pub hwnd: isize,
}
impl From<isize> for Hidden {
fn from(hwnd: isize) -> Self {
Self { hwnd }
}
}
impl Hidden {
pub const fn hwnd(self) -> HWND {
HWND(self.hwnd)
}
pub fn create(name: &str) -> color_eyre::Result<Self> {
let name: Vec<u16> = format!("{name}\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(0),
..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_hidden_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);
}
}
Ok(())
});
let hwnd = hwnd_receiver.recv()?;
WindowsApi::wts_register_session_notification(hwnd)?;
Ok(Self { hwnd })
}
pub extern "system" fn callback(
window: HWND,
message: u32,
wparam: WPARAM,
lparam: LPARAM,
) -> LRESULT {
unsafe {
match message {
WM_POWERBROADCAST => {
match wparam.0 as u32 {
// Automatic: System resumed itself from sleep or hibernation
// Suspend: User resumed system from sleep or hibernation
PBT_APMRESUMEAUTOMATIC | PBT_APMRESUMESUSPEND => {
tracing::debug!(
"WM_POWERBROADCAST event received - resume from suspend"
);
let _ = monitor_reconciliator::event_tx().send(
monitor_reconciliator::Notification::ResumingFromSuspendedState,
);
LRESULT(0)
}
// Computer is entering a suspended state
PBT_APMSUSPEND => {
tracing::debug!(
"WM_POWERBROADCAST event received - entering suspended state"
);
let _ = monitor_reconciliator::event_tx()
.send(monitor_reconciliator::Notification::EnteringSuspendedState);
LRESULT(0)
}
_ => LRESULT(0),
}
}
WM_WTSSESSION_CHANGE => {
match wparam.0 as u32 {
WTS_SESSION_LOCK => {
tracing::debug!("WM_WTSSESSION_CHANGE event received with WTS_SESSION_LOCK - screen locked");
let _ = monitor_reconciliator::event_tx()
.send(monitor_reconciliator::Notification::SessionLocked);
}
WTS_SESSION_UNLOCK => {
tracing::debug!("WM_WTSSESSION_CHANGE event received with WTS_SESSION_UNLOCK - screen unlocked");
let _ = monitor_reconciliator::event_tx()
.send(monitor_reconciliator::Notification::SessionUnlocked);
}
_ => {}
}
LRESULT(0)
}
// This event gets sent when:
// - The scaling factor on a display changes
// - The resolution on a display changes
// - A monitor is added
// - A monitor is removed
// Since WM_DEVICECHANGE also notifies on monitor changes, we only handle scaling
// and resolution changes here
WM_DISPLAYCHANGE => {
tracing::debug!(
"WM_DISPLAYCHANGE event received with wparam: {}- work area or display resolution changed", wparam.0
);
let _ = monitor_reconciliator::event_tx()
.send(monitor_reconciliator::Notification::ResolutionScalingChanged);
LRESULT(0)
}
// Unfortunately this is the event sent with ButteryTaskbar which I use a lot
// Original idea from https://stackoverflow.com/a/33762334
WM_SETTINGCHANGE => {
#[allow(clippy::cast_possible_truncation)]
if wparam.0 as u32 == SPI_SETWORKAREA.0 {
tracing::debug!(
"WM_SETTINGCHANGE event received with SPI_SETWORKAREA - work area changed (probably butterytaskbar or something similar)"
);
let _ = monitor_reconciliator::event_tx()
.send(monitor_reconciliator::Notification::WorkAreaChanged);
}
LRESULT(0)
}
// This event + wparam combo is sent 4 times when a monitor is added based on my testing on win11
// Original idea from https://stackoverflow.com/a/33762334
WM_DEVICECHANGE => {
#[allow(clippy::cast_possible_truncation)]
if wparam.0 as u32 == DBT_DEVNODES_CHANGED {
tracing::debug!(
"WM_DEVICECHANGE event received with DBT_DEVNODES_CHANGED - display added or removed"
);
let _ = monitor_reconciliator::event_tx()
.send(monitor_reconciliator::Notification::DisplayConnectionChange);
}
LRESULT(0)
}
_ => DefWindowProcW(window, message, wparam, lparam),
}
}
}
}

View File

@@ -0,0 +1,404 @@
#![deny(clippy::unwrap_used, clippy::expect_used)]
use crate::border_manager;
use crate::monitor;
use crate::monitor::Monitor;
use crate::monitor_reconciliator::hidden::Hidden;
use crate::MonitorConfig;
use crate::WindowManager;
use crate::WindowsApi;
use crossbeam_channel::Receiver;
use crossbeam_channel::Sender;
use crossbeam_utils::atomic::AtomicConsume;
use komorebi_core::Rect;
use parking_lot::Mutex;
use std::collections::HashMap;
use std::sync::atomic::AtomicBool;
use std::sync::atomic::Ordering;
use std::sync::Arc;
use std::sync::OnceLock;
pub mod hidden;
pub enum Notification {
ResolutionScalingChanged,
WorkAreaChanged,
DisplayConnectionChange,
EnteringSuspendedState,
ResumingFromSuspendedState,
SessionLocked,
SessionUnlocked,
}
static ACTIVE: AtomicBool = AtomicBool::new(true);
static CHANNEL: OnceLock<(Sender<Notification>, Receiver<Notification>)> = OnceLock::new();
static MONITOR_CACHE: OnceLock<Mutex<HashMap<String, MonitorConfig>>> = OnceLock::new();
pub fn channel() -> &'static (Sender<Notification>, Receiver<Notification>) {
CHANNEL.get_or_init(|| crossbeam_channel::bounded(1))
}
pub fn event_tx() -> Sender<Notification> {
channel().0.clone()
}
pub fn event_rx() -> Receiver<Notification> {
channel().1.clone()
}
pub fn insert_in_monitor_cache(device_id: &str, config: MonitorConfig) {
let mut monitor_cache = MONITOR_CACHE
.get_or_init(|| Mutex::new(HashMap::new()))
.lock();
monitor_cache.insert(device_id.to_string(), config);
}
pub fn attached_display_devices() -> color_eyre::Result<Vec<Monitor>> {
Ok(win32_display_data::connected_displays()
.flatten()
.map(|display| {
let path = display.device_path;
let mut split: Vec<_> = path.split('#').collect();
split.remove(0);
split.remove(split.len() - 1);
let device = split[0].to_string();
let device_id = split.join("-");
let name = display.device_name.trim_start_matches(r"\\.\").to_string();
let name = name.split('\\').collect::<Vec<_>>()[0].to_string();
monitor::new(
display.hmonitor,
display.size.into(),
display.work_area_size.into(),
name,
device,
device_id,
)
})
.collect::<Vec<_>>())
}
pub fn listen_for_notifications(wm: Arc<Mutex<WindowManager>>) -> color_eyre::Result<()> {
#[allow(clippy::expect_used)]
Hidden::create("komorebi-hidden")?;
tracing::info!("created hidden window to listen for monitor-related events");
std::thread::spawn(move || loop {
match handle_notifications(wm.clone()) {
Ok(()) => {
tracing::warn!("restarting finished thread");
}
Err(error) => {
if cfg!(debug_assertions) {
tracing::error!("restarting failed thread: {:?}", error)
} else {
tracing::error!("restarting failed thread: {}", error)
}
}
}
});
Ok(())
}
pub fn handle_notifications(wm: Arc<Mutex<WindowManager>>) -> color_eyre::Result<()> {
tracing::info!("listening");
let receiver = event_rx();
'receiver: for notification in receiver {
if !ACTIVE.load_consume() {
if matches!(
notification,
Notification::ResumingFromSuspendedState | Notification::SessionUnlocked
) {
tracing::debug!(
"reactivating reconciliator - system has resumed from suspended state or session has been unlocked"
);
ACTIVE.store(true, Ordering::SeqCst);
}
continue 'receiver;
}
let mut wm = wm.lock();
match notification {
Notification::EnteringSuspendedState | Notification::SessionLocked => {
tracing::debug!(
"deactivating reconciliator until system resumes from suspended state or session is unlocked"
);
ACTIVE.store(false, Ordering::SeqCst);
}
Notification::ResumingFromSuspendedState | Notification::SessionUnlocked => {
// this is only handled above if the reconciliator is paused
}
Notification::WorkAreaChanged => {
tracing::debug!("handling work area changed notification");
let offset = wm.work_area_offset;
for monitor in wm.monitors_mut() {
let mut should_update = false;
// Update work areas as necessary
if let Ok(reference) = WindowsApi::monitor(monitor.id()) {
if reference.work_area_size() != monitor.work_area_size() {
monitor.set_work_area_size(Rect {
left: reference.work_area_size().left,
top: reference.work_area_size().top,
right: reference.work_area_size().right,
bottom: reference.work_area_size().bottom,
});
should_update = true;
}
}
if should_update {
tracing::info!("updated work area for {}", monitor.device_id());
monitor.update_focused_workspace(offset)?;
border_manager::event_tx().send(border_manager::Notification)?;
} else {
tracing::debug!(
"work areas match, reconciliation not required for {}",
monitor.device_id()
);
}
}
}
Notification::ResolutionScalingChanged => {
tracing::debug!("handling resolution/scaling changed notification");
let offset = wm.work_area_offset;
for monitor in wm.monitors_mut() {
let mut should_update = false;
// Update sizes and work areas as necessary
if let Ok(reference) = WindowsApi::monitor(monitor.id()) {
if reference.work_area_size() != monitor.work_area_size() {
monitor.set_work_area_size(Rect {
left: reference.work_area_size().left,
top: reference.work_area_size().top,
right: reference.work_area_size().right,
bottom: reference.work_area_size().bottom,
});
should_update = true;
}
if reference.size() != monitor.size() {
monitor.set_size(Rect {
left: reference.size().left,
top: reference.size().top,
right: reference.size().right,
bottom: reference.size().bottom,
});
should_update = true;
}
}
if should_update {
tracing::info!(
"updated monitor resolution/scaling for {}",
monitor.device_id()
);
monitor.update_focused_workspace(offset)?;
border_manager::event_tx().send(border_manager::Notification)?;
} else {
tracing::debug!(
"resolutions match, reconciliation not required for {}",
monitor.device_id()
);
}
}
}
Notification::DisplayConnectionChange => {
tracing::debug!("handling display connection change notification");
let mut monitor_cache = MONITOR_CACHE
.get_or_init(|| Mutex::new(HashMap::new()))
.lock();
let initial_monitor_count = wm.monitors().len();
// Get the currently attached display devices
let attached_devices = attached_display_devices()?;
// Make sure that in our state any attached displays have the latest Win32 data
for monitor in wm.monitors_mut() {
for attached in &attached_devices {
if attached.device_id().eq(monitor.device_id()) {
monitor.set_id(attached.id());
monitor.set_name(attached.name().clone());
monitor.set_size(*attached.size());
monitor.set_work_area_size(*attached.work_area_size());
}
}
}
if initial_monitor_count == attached_devices.len() {
tracing::debug!("monitor counts match, reconciliation not required");
continue 'receiver;
}
if attached_devices.is_empty() {
tracing::debug!(
"no devices found, skipping reconciliation to avoid breaking state"
);
continue 'receiver;
}
if initial_monitor_count > attached_devices.len() {
tracing::info!(
"monitor count mismatch ({initial_monitor_count} vs {}), removing disconnected monitors",
attached_devices.len()
);
// Gather all the containers that will be orphaned from disconnected and invalid displays
let mut orphaned_containers = vec![];
// Collect the ids in our state which aren't in the current attached display ids
// These are monitors that have been removed
let mut newly_removed_displays = vec![];
for m in wm.monitors().iter() {
if !attached_devices
.iter()
.any(|attached| attached.device_id().eq(m.device_id()))
{
newly_removed_displays.push(m.device_id().clone());
for workspace in m.workspaces() {
for container in workspace.containers() {
// Save the orphaned containers from the removed monitor
orphaned_containers.push(container.clone());
}
}
// Let's add their state to the cache for later
monitor_cache.insert(m.device_id().clone(), m.into());
}
}
if !orphaned_containers.is_empty() {
tracing::info!(
"removed orphaned containers from: {newly_removed_displays:?}"
);
}
if !newly_removed_displays.is_empty() {
// After we have cached them, remove them from our state
wm.monitors_mut()
.retain(|m| !newly_removed_displays.contains(m.device_id()));
}
let post_removal_monitor_count = wm.monitors().len();
let focused_monitor_idx = wm.focused_monitor_idx();
if focused_monitor_idx >= post_removal_monitor_count {
wm.focus_monitor(0)?;
}
if !orphaned_containers.is_empty() {
if let Some(primary) = wm.monitors_mut().front_mut() {
if let Some(focused_ws) = primary.focused_workspace_mut() {
let focused_container_idx = focused_ws.focused_container_idx();
// Put the orphaned containers somewhere visible
for container in orphaned_containers {
focused_ws.add_container(container);
}
// Gotta reset the focus or the movement will feel "off"
if initial_monitor_count != post_removal_monitor_count {
focused_ws.focus_container(focused_container_idx);
}
}
}
}
let offset = wm.work_area_offset;
for monitor in wm.monitors_mut() {
// If we have lost a monitor, update everything to filter out any jank
if initial_monitor_count != post_removal_monitor_count {
monitor.update_focused_workspace(offset)?;
}
}
}
let post_removal_monitor_count = wm.monitors().len();
// This is the list of device ids after we have removed detached displays
let post_removal_device_ids = wm
.monitors()
.iter()
.map(Monitor::device_id)
.cloned()
.collect::<Vec<_>>();
// Check for and add any new monitors that may have been plugged in
// Monitor and display index preferences get applied in this function
WindowsApi::load_monitor_information(&mut wm.monitors)?;
let post_addition_monitor_count = wm.monitors().len();
if post_addition_monitor_count > post_removal_monitor_count {
tracing::info!(
"monitor count mismatch ({post_removal_monitor_count} vs {post_addition_monitor_count}), adding connected monitors",
);
// Look in the updated state for new monitors
for m in wm.monitors_mut() {
let device_id = m.device_id().clone();
// We identify a new monitor when we encounter a new device id
if !post_removal_device_ids.contains(&device_id) {
let mut cache_hit = false;
// Check if that device id exists in the cache for this session
if let Some(cached) = monitor_cache.get(&device_id) {
cache_hit = true;
tracing::info!("found monitor and workspace configuration for {device_id} in the monitor cache, applying");
// If it does, load all the monitor settings from the cache entry
m.ensure_workspace_count(cached.workspaces.len());
m.set_work_area_offset(cached.work_area_offset);
m.set_window_based_work_area_offset(
cached.window_based_work_area_offset,
);
m.set_window_based_work_area_offset_limit(
cached.window_based_work_area_offset_limit.unwrap_or(1),
);
for (w_idx, workspace) in m.workspaces_mut().iter_mut().enumerate()
{
if let Some(cached_workspace) = cached.workspaces.get(w_idx) {
workspace.load_static_config(cached_workspace)?;
}
}
}
// Entries in the cache should only be used once; remove the entry there was a cache hit
if cache_hit {
monitor_cache.remove(&device_id);
}
}
}
}
let final_count = wm.monitors().len();
if post_removal_monitor_count != final_count {
wm.retile_all(true)?;
// Second retile to fix DPI/resolution related jank
wm.retile_all(true)?;
// Border updates to fix DPI/resolution related jank
border_manager::event_tx().send(border_manager::Notification)?;
}
}
}
}
Ok(())
}

View File

@@ -43,6 +43,7 @@ use crate::border_manager::STYLE;
use crate::colour::Rgb;
use crate::current_virtual_desktop;
use crate::notify_subscribers;
use crate::stackbar_manager;
use crate::static_config::StaticConfig;
use crate::window::RuleDebug;
use crate::window::Window;
@@ -64,48 +65,38 @@ use crate::MONITOR_INDEX_PREFERENCES;
use crate::NO_TITLEBAR;
use crate::OBJECT_NAME_CHANGE_ON_LAUNCH;
use crate::REMOVE_TITLEBARS;
use crate::STACKBAR_FOCUSED_TEXT_COLOUR;
use crate::STACKBAR_MODE;
use crate::STACKBAR_TAB_BACKGROUND_COLOUR;
use crate::STACKBAR_TAB_HEIGHT;
use crate::STACKBAR_TAB_WIDTH;
use crate::STACKBAR_UNFOCUSED_TEXT_COLOUR;
use crate::SUBSCRIPTION_PIPES;
use crate::SUBSCRIPTION_SOCKETS;
use crate::TCP_CONNECTIONS;
use crate::TRAY_AND_MULTI_WINDOW_IDENTIFIERS;
use crate::WORKSPACE_RULES;
use stackbar_manager::STACKBAR_FOCUSED_TEXT_COLOUR;
use stackbar_manager::STACKBAR_LABEL;
use stackbar_manager::STACKBAR_MODE;
use stackbar_manager::STACKBAR_TAB_BACKGROUND_COLOUR;
use stackbar_manager::STACKBAR_TAB_HEIGHT;
use stackbar_manager::STACKBAR_TAB_WIDTH;
use stackbar_manager::STACKBAR_UNFOCUSED_TEXT_COLOUR;
#[tracing::instrument]
pub fn listen_for_commands(wm: Arc<Mutex<WindowManager>>) {
std::thread::spawn(move || loop {
let listener = wm
.lock()
.command_listener
.try_clone()
.expect("could not clone unix listener");
let listener = wm
.lock()
.command_listener
.try_clone()
.expect("could not clone unix listener");
std::thread::spawn(move || {
tracing::info!("listening on komorebi.sock");
// client is unique for every komorebic command
for client in listener.incoming() {
match client {
Ok(stream) => match read_commands_uds(&wm, stream) {
Ok(()) => {}
Err(error) => {
if cfg!(debug_assertions) {
tracing::error!("{:?}", error)
} else {
tracing::error!("{}", error)
}
}
Err(error) => tracing::error!("{}", error),
},
Err(error) => {
if cfg!(debug_assertions) {
tracing::error!("{:?}", error)
} else {
tracing::error!("{}", error)
}
tracing::error!("{}", error);
break;
}
}
}
@@ -196,6 +187,10 @@ impl WindowManager {
match message {
SocketMessage::Promote => self.promote_container_to_front()?,
SocketMessage::PromoteFocus => self.promote_focus_to_front()?,
SocketMessage::PromoteWindow(direction) => {
self.focus_container_in_direction(direction)?;
self.promote_container_to_front()?
}
SocketMessage::FocusWindow(direction) => {
self.focus_container_in_direction(direction)?;
}
@@ -220,8 +215,18 @@ impl WindowManager {
WindowsApi::center_cursor_in_rect(&focused_window_rect)?;
WindowsApi::left_click();
}
SocketMessage::Close => self.focused_window()?.close()?,
SocketMessage::Minimize => self.focused_window()?.minimize(),
SocketMessage::Close => {
Window {
hwnd: WindowsApi::foreground_window()?,
}
.close()?;
}
SocketMessage::Minimize => {
Window {
hwnd: WindowsApi::foreground_window()?,
}
.minimize();
}
SocketMessage::ToggleFloat => self.toggle_float()?,
SocketMessage::ToggleMonocle => self.toggle_monocle()?,
SocketMessage::ToggleMaximize => self.toggle_maximize()?,
@@ -476,6 +481,15 @@ impl WindowManager {
SocketMessage::MoveWorkspaceToMonitorNumber(monitor_idx) => {
self.move_workspace_to_monitor(monitor_idx)?;
}
SocketMessage::CycleMoveWorkspaceToMonitor(direction) => {
let monitor_idx = direction.next_idx(
self.focused_monitor_idx(),
NonZeroUsize::new(self.monitors().len())
.ok_or_else(|| anyhow!("there must be at least one monitor"))?,
);
self.move_workspace_to_monitor(monitor_idx)?;
}
SocketMessage::TogglePause => {
if self.is_paused {
tracing::info!("resuming");
@@ -771,13 +785,10 @@ impl WindowManager {
SocketMessage::VisibleWindows => {
let mut monitor_visible_windows = HashMap::new();
for (index, monitor) in self.monitors().iter().enumerate() {
for monitor in self.monitors() {
if let Some(ws) = monitor.focused_workspace() {
monitor_visible_windows.insert(
monitor
.device_id()
.clone()
.unwrap_or_else(|| format!("{index}")),
monitor.device_id().clone(),
ws.visible_window_details().clone(),
);
}
@@ -1216,10 +1227,10 @@ impl WindowManager {
SocketMessage::UnmanagedWindowOperationBehaviour(behaviour) => {
self.unmanaged_window_operation_behaviour = behaviour;
}
SocketMessage::ActiveWindowBorder(enable) => {
SocketMessage::Border(enable) => {
border_manager::BORDER_ENABLED.store(enable, Ordering::SeqCst);
}
SocketMessage::ActiveWindowBorderColour(kind, r, g, b) => match kind {
SocketMessage::BorderColour(kind, r, g, b) => match kind {
WindowKind::Single => {
border_manager::FOCUSED.store(Rgb::new(r, g, b).into(), Ordering::SeqCst);
}
@@ -1233,9 +1244,9 @@ impl WindowManager {
border_manager::UNFOCUSED.store(Rgb::new(r, g, b).into(), Ordering::SeqCst);
}
},
SocketMessage::ActiveWindowBorderStyle(style) => {
let mut active_window_border_style = STYLE.lock();
*active_window_border_style = style;
SocketMessage::BorderStyle(style) => {
let mut border_style = STYLE.lock();
*border_style = style;
}
SocketMessage::BorderWidth(width) => {
border_manager::BORDER_WIDTH.store(width, Ordering::SeqCst);
@@ -1244,16 +1255,10 @@ impl WindowManager {
border_manager::BORDER_OFFSET.store(offset, Ordering::SeqCst);
}
SocketMessage::StackbarMode(mode) => {
let mut stackbar_mode = STACKBAR_MODE.lock();
*stackbar_mode = mode;
for m in self.monitors_mut() {
for w in m.workspaces_mut() {
for c in w.containers_mut() {
c.set_stackbar_mode(mode);
}
}
}
STACKBAR_MODE.store(mode);
}
SocketMessage::StackbarLabel(label) => {
STACKBAR_LABEL.store(label);
}
SocketMessage::StackbarFocusedTextColour(r, g, b) => {
let rgb = Rgb::new(r, g, b);
@@ -1340,6 +1345,7 @@ impl WindowManager {
notify_subscribers(&serde_json::to_string(&notification)?)?;
border_manager::event_tx().send(border_manager::Notification)?;
stackbar_manager::event_tx().send(stackbar_manager::Notification)?;
tracing::info!("processed");
Ok(())

View File

@@ -18,11 +18,13 @@ use crate::border_manager::BORDER_OFFSET;
use crate::border_manager::BORDER_WIDTH;
use crate::current_virtual_desktop;
use crate::notify_subscribers;
use crate::stackbar_manager;
use crate::window::should_act;
use crate::window::RuleDebug;
use crate::window_manager::WindowManager;
use crate::window_manager_event::WindowManagerEvent;
use crate::windows_api::WindowsApi;
use crate::winevent::WinEvent;
use crate::workspace_reconciliator;
use crate::workspace_reconciliator::ALT_TAB_HWND;
use crate::workspace_reconciliator::ALT_TAB_HWND_INSTANT;
@@ -35,17 +37,20 @@ use crate::TRAY_AND_MULTI_WINDOW_IDENTIFIERS;
#[tracing::instrument]
pub fn listen_for_events(wm: Arc<Mutex<WindowManager>>) {
std::thread::spawn(move || loop {
let receiver = wm.lock().incoming_events.clone();
std::thread::spawn(move || {
tracing::info!("listening");
let receiver = wm.lock().incoming_events.clone();
for event in receiver {
match wm.lock().process_event(event) {
Ok(()) => {}
Err(error) => {
if cfg!(debug_assertions) {
tracing::error!("{:?}", error)
} else {
tracing::error!("{}", error)
loop {
if let Ok(event) = receiver.recv() {
match wm.lock().process_event(event) {
Ok(()) => {}
Err(error) => {
if cfg!(debug_assertions) {
tracing::error!("{:?}", error)
} else {
tracing::error!("{}", error)
}
}
}
}
@@ -68,7 +73,7 @@ impl WindowManager {
// All event handlers below this point should only be processed if the event is
// related to a window that should be managed by the WindowManager.
if !should_manage && !matches!(event, WindowManagerEvent::DisplayChange(_)) {
if !should_manage {
return Ok(());
}
@@ -88,67 +93,39 @@ impl WindowManager {
match event {
WindowManagerEvent::FocusChange(_, window)
| WindowManagerEvent::Show(_, window)
| WindowManagerEvent::DisplayChange(window)
| WindowManagerEvent::MoveResizeEnd(_, window) => {
self.reconcile_monitors()?;
let monitor_idx = self.monitor_idx_from_window(window)
.ok_or_else(|| anyhow!("there is no monitor associated with this window, it may have already been destroyed"))?;
// This is a hidden window apparently associated with COM support mechanisms (based
// on a post from http://www.databaseteam.org/1-ms-sql-server/a5bb344836fb889c.htm)
//
// The hidden window, OLEChannelWnd, associated with this class (spawned by
// explorer.exe), after some debugging, is observed to always be tied to the primary
// display monitor, or (usually) monitor 0 in the WindowManager state.
//
// Due to this, at least one user in the Discord has witnessed behaviour where, when
// a MonitorPoll event is triggered by OLEChannelWnd, the focused monitor index gets
// set repeatedly to 0, regardless of where the current foreground window is actually
// located.
//
// This check ensures that we only update the focused monitor when the window
// triggering monitor reconciliation is known to not be tied to a specific monitor.
if window.class()? != "OleMainThreadWndClass"
&& self.focused_monitor_idx() != monitor_idx
{
self.focus_monitor(monitor_idx)?;
if let Some(monitor_idx) = self.monitor_idx_from_window(window) {
// This is a hidden window apparently associated with COM support mechanisms (based
// on a post from http://www.databaseteam.org/1-ms-sql-server/a5bb344836fb889c.htm)
//
// The hidden window, OLEChannelWnd, associated with this class (spawned by
// explorer.exe), after some debugging, is observed to always be tied to the primary
// display monitor, or (usually) monitor 0 in the WindowManager state.
//
// Due to this, at least one user in the Discord has witnessed behaviour where, when
// a MonitorPoll event is triggered by OLEChannelWnd, the focused monitor index gets
// set repeatedly to 0, regardless of where the current foreground window is actually
// located.
//
// This check ensures that we only update the focused monitor when the window
// triggering monitor reconciliation is known to not be tied to a specific monitor.
if let Ok(class) = window.class() {
if class != "OleMainThreadWndClass"
&& self.focused_monitor_idx() != monitor_idx
{
self.focus_monitor(monitor_idx)?;
}
}
}
}
_ => {}
}
let offset = self.work_area_offset;
for (i, monitor) in self.monitors_mut().iter_mut().enumerate() {
let work_area = *monitor.work_area_size();
let window_based_work_area_offset = (
monitor.window_based_work_area_offset_limit(),
monitor.window_based_work_area_offset(),
);
let offset = if monitor.work_area_offset().is_some() {
monitor.work_area_offset()
} else {
offset
};
for (j, workspace) in monitor.workspaces_mut().iter_mut().enumerate() {
for monitor in self.monitors_mut() {
for workspace in monitor.workspaces_mut() {
if let WindowManagerEvent::FocusChange(_, window) = event {
let _ = workspace.focus_changed(window.hwnd);
}
let reaped_orphans = workspace.reap_orphans()?;
if reaped_orphans.0 > 0 || reaped_orphans.1 > 0 {
workspace.update(&work_area, offset, window_based_work_area_offset)?;
tracing::info!(
"reaped {} orphan window(s) and {} orphaned container(s) on monitor: {}, workspace: {}",
reaped_orphans.0,
reaped_orphans.1,
i,
j
);
}
}
}
@@ -332,8 +309,10 @@ impl WindowManager {
if proceed {
let behaviour = self.window_container_behaviour;
let workspace = self.focused_workspace_mut()?;
let workspace_contains_window = workspace.contains_window(window.hwnd);
let monocle_container = workspace.monocle_container().clone();
if !workspace.contains_window(window.hwnd) && !needs_reconciliation {
if !workspace_contains_window && !needs_reconciliation {
match behaviour {
WindowContainerBehaviour::Create => {
workspace.new_container_for_window(window);
@@ -348,6 +327,21 @@ impl WindowManager {
}
}
}
if workspace_contains_window {
let mut monocle_window_event = false;
if let Some(ref monocle) = monocle_container {
if let Some(monocle_window) = monocle.focused_window() {
if monocle_window.hwnd == window.hwnd {
monocle_window_event = true;
}
}
}
if !monocle_window_event && monocle_container.is_some() {
window.hide();
}
}
}
}
WindowManagerEvent::MoveResizeStart(_, window) => {
@@ -440,10 +434,7 @@ impl WindowManager {
// If we have moved across the monitors, use that override, otherwise determine
// if a move has taken place by ruling out a resize
let right_bottom_constant = ((BORDER_WIDTH.load(Ordering::SeqCst)
+ BORDER_OFFSET.load(Ordering::SeqCst))
* 2)
.abs();
let right_bottom_constant = 0;
let is_move = moved_across_monitors
|| resize.right.abs() == right_bottom_constant
@@ -580,12 +571,9 @@ impl WindowManager {
}
}
}
WindowManagerEvent::ForceUpdate(_) => {
self.update_focused_workspace(false, true)?;
}
WindowManagerEvent::DisplayChange(..)
| WindowManagerEvent::MouseCapture(..)
| WindowManagerEvent::Cloak(..) => {}
WindowManagerEvent::MouseCapture(..)
| WindowManagerEvent::Cloak(..)
| WindowManagerEvent::TitleUpdate(..) => {}
};
// If we unmanaged a window, it shouldn't be immediately hidden behind managed windows
@@ -621,8 +609,18 @@ impl WindowManager {
notify_subscribers(&serde_json::to_string(&notification)?)?;
border_manager::event_tx().send(border_manager::Notification)?;
stackbar_manager::event_tx().send(stackbar_manager::Notification)?;
// Too many spammy OBJECT_NAMECHANGE events from JetBrains IDEs
if !matches!(
event,
WindowManagerEvent::Show(WinEvent::ObjectNameChange, _)
) {
tracing::info!("processed: {}", event.window().to_string());
} else {
tracing::trace!("processed: {}", event.window().to_string());
}
tracing::info!("processed: {}", event.window().to_string());
Ok(())
}
}

66
komorebi/src/reaper.rs Normal file
View File

@@ -0,0 +1,66 @@
#![deny(clippy::unwrap_used, clippy::expect_used)]
use crate::border_manager;
use crate::WindowManager;
use parking_lot::Mutex;
use std::sync::Arc;
use std::time::Duration;
pub fn watch_for_orphans(wm: Arc<Mutex<WindowManager>>) {
std::thread::spawn(move || loop {
match find_orphans(wm.clone()) {
Ok(()) => {
tracing::warn!("restarting finished thread");
}
Err(error) => {
if cfg!(debug_assertions) {
tracing::error!("restarting failed thread: {:?}", error)
} else {
tracing::error!("restarting failed thread: {}", error)
}
}
}
});
}
pub fn find_orphans(wm: Arc<Mutex<WindowManager>>) -> color_eyre::Result<()> {
tracing::info!("watching");
let arc = wm.clone();
loop {
std::thread::sleep(Duration::from_secs(1));
let mut wm = arc.lock();
let offset = wm.work_area_offset;
for (i, monitor) in wm.monitors_mut().iter_mut().enumerate() {
let work_area = *monitor.work_area_size();
let window_based_work_area_offset = (
monitor.window_based_work_area_offset_limit(),
monitor.window_based_work_area_offset(),
);
let offset = if monitor.work_area_offset().is_some() {
monitor.work_area_offset()
} else {
offset
};
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, offset, window_based_work_area_offset)?;
border_manager::event_tx().send(border_manager::Notification)?;
tracing::info!(
"reaped {} orphan window(s) and {} orphaned container(s) on monitor: {}, workspace: {}",
reaped_orphans.0,
reaped_orphans.1,
i,
j
);
}
}
}
}
}

View File

@@ -1,265 +0,0 @@
use std::collections::VecDeque;
use std::sync::atomic::Ordering;
use color_eyre::eyre::Result;
use schemars::JsonSchema;
use serde::Deserialize;
use serde::Serialize;
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::CreateFontIndirectW;
use windows::Win32::Graphics::Gdi::CreatePen;
use windows::Win32::Graphics::Gdi::CreateSolidBrush;
use windows::Win32::Graphics::Gdi::DrawTextW;
use windows::Win32::Graphics::Gdi::GetDC;
use windows::Win32::Graphics::Gdi::ReleaseDC;
use windows::Win32::Graphics::Gdi::SelectObject;
use windows::Win32::Graphics::Gdi::SetBkColor;
use windows::Win32::Graphics::Gdi::SetTextColor;
use windows::Win32::Graphics::Gdi::DT_CENTER;
use windows::Win32::Graphics::Gdi::DT_END_ELLIPSIS;
use windows::Win32::Graphics::Gdi::DT_SINGLELINE;
use windows::Win32::Graphics::Gdi::DT_VCENTER;
use windows::Win32::Graphics::Gdi::FONT_QUALITY;
use windows::Win32::Graphics::Gdi::FW_BOLD;
use windows::Win32::Graphics::Gdi::LOGFONTW;
use windows::Win32::Graphics::Gdi::PROOF_QUALITY;
use windows::Win32::Graphics::Gdi::PS_SOLID;
use windows::Win32::UI::WindowsAndMessaging::CreateWindowExW;
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::RegisterClassW;
use windows::Win32::UI::WindowsAndMessaging::SetLayeredWindowAttributes;
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::LWA_COLORKEY;
use windows::Win32::UI::WindowsAndMessaging::MSG;
use windows::Win32::UI::WindowsAndMessaging::SW_SHOW;
use windows::Win32::UI::WindowsAndMessaging::WM_DESTROY;
use windows::Win32::UI::WindowsAndMessaging::WM_LBUTTONDOWN;
use windows::Win32::UI::WindowsAndMessaging::WNDCLASSW;
use windows::Win32::UI::WindowsAndMessaging::WS_EX_LAYERED;
use windows::Win32::UI::WindowsAndMessaging::WS_EX_TOOLWINDOW;
use windows::Win32::UI::WindowsAndMessaging::WS_POPUP;
use windows::Win32::UI::WindowsAndMessaging::WS_VISIBLE;
use komorebi_core::Rect;
use crate::window::Window;
use crate::windows_api::WindowsApi;
use crate::DEFAULT_CONTAINER_PADDING;
use crate::STACKBAR_FOCUSED_TEXT_COLOUR;
use crate::STACKBAR_TAB_BACKGROUND_COLOUR;
use crate::STACKBAR_TAB_HEIGHT;
use crate::STACKBAR_TAB_WIDTH;
use crate::STACKBAR_UNFOCUSED_TEXT_COLOUR;
use crate::WINDOWS_BY_BAR_HWNDS;
#[derive(Debug, Clone, Default, Serialize, Deserialize, JsonSchema)]
pub struct Stackbar {
pub(crate) hwnd: isize,
}
impl Stackbar {
unsafe extern "system" fn window_proc(
hwnd: HWND,
msg: u32,
w_param: WPARAM,
l_param: LPARAM,
) -> LRESULT {
match msg {
WM_LBUTTONDOWN => {
let win_hwnds_by_topbar = WINDOWS_BY_BAR_HWNDS.lock();
if let Some(win_hwnds) = win_hwnds_by_topbar.get(&hwnd.0) {
let x = l_param.0 as i32 & 0xFFFF;
let y = (l_param.0 as i32 >> 16) & 0xFFFF;
let width = STACKBAR_TAB_WIDTH.load(Ordering::SeqCst);
let height = STACKBAR_TAB_HEIGHT.load(Ordering::SeqCst);
let gap = DEFAULT_CONTAINER_PADDING.load(Ordering::SeqCst);
for (index, win_hwnd) in win_hwnds.iter().enumerate() {
let left = gap + (index as i32 * (width + gap));
let right = left + width;
let top = 0;
let bottom = height;
if x >= left && x <= right && y >= top && y <= bottom {
let window = Window { hwnd: *win_hwnd };
window.restore();
if let Err(err) = window.focus(false) {
tracing::error!("Stackbar focus error: HWND:{} {}", *win_hwnd, err);
}
}
}
}
WINDOWS_BY_BAR_HWNDS.force_unlock();
LRESULT(0)
}
WM_DESTROY => {
PostQuitMessage(0);
LRESULT(0)
}
_ => DefWindowProcW(hwnd, msg, w_param, l_param),
}
}
pub const fn hwnd(&self) -> HWND {
HWND(self.hwnd)
}
pub fn create() -> Result<Stackbar> {
let name: Vec<u16> = "komorebi_stackbar\0".encode_utf16().collect();
let class_name = PCWSTR(name.as_ptr());
let h_module = WindowsApi::module_handle_w()?;
let wnd_class = WNDCLASSW {
style: CS_HREDRAW | CS_VREDRAW,
lpfnWndProc: Some(Self::window_proc),
hInstance: h_module.into(),
lpszClassName: class_name,
hbrBackground: WindowsApi::create_solid_brush(0),
..Default::default()
};
unsafe {
RegisterClassW(&wnd_class);
}
let (hwnd_sender, hwnd_receiver) = crossbeam_channel::bounded::<HWND>(1);
let name_cl = name.clone();
std::thread::spawn(move || -> Result<()> {
unsafe {
let hwnd = CreateWindowExW(
WS_EX_TOOLWINDOW | WS_EX_LAYERED,
PCWSTR(name_cl.as_ptr()),
PCWSTR(name_cl.as_ptr()),
WS_POPUP | WS_VISIBLE,
0,
0,
0,
0,
None,
None,
h_module,
None,
);
SetLayeredWindowAttributes(hwnd, COLORREF(0), 0, LWA_COLORKEY)?;
hwnd_sender.send(hwnd)?;
let mut msg = MSG::default();
while GetMessageW(&mut msg, hwnd, 0, 0).into() {
TranslateMessage(&msg);
DispatchMessageW(&msg);
}
}
Ok(())
});
Ok(Self {
hwnd: hwnd_receiver.recv()?.0,
})
}
pub fn set_position(&self, layout: &Rect, top: bool) -> Result<()> {
WindowsApi::position_window(self.hwnd(), layout, top)
}
pub fn get_position_from_container_layout(&self, layout: &Rect) -> Rect {
Rect {
bottom: STACKBAR_TAB_HEIGHT.load(Ordering::SeqCst),
..*layout
}
}
pub fn update(&self, windows: &VecDeque<Window>, focused_hwnd: isize) -> Result<()> {
let width = STACKBAR_TAB_WIDTH.load(Ordering::SeqCst);
let height = STACKBAR_TAB_HEIGHT.load(Ordering::SeqCst);
let gap = DEFAULT_CONTAINER_PADDING.load(Ordering::SeqCst);
let background = STACKBAR_TAB_BACKGROUND_COLOUR.load(Ordering::SeqCst);
let focused_text_colour = STACKBAR_FOCUSED_TEXT_COLOUR.load(Ordering::SeqCst);
let unfocused_text_colour = STACKBAR_UNFOCUSED_TEXT_COLOUR.load(Ordering::SeqCst);
unsafe {
let hdc = GetDC(self.hwnd());
let hpen = CreatePen(PS_SOLID, 0, COLORREF(background));
let hbrush = CreateSolidBrush(COLORREF(background));
SelectObject(hdc, hpen);
SelectObject(hdc, hbrush);
SetBkColor(hdc, COLORREF(background));
let hfont = CreateFontIndirectW(&LOGFONTW {
lfWeight: FW_BOLD.0 as i32,
lfQuality: FONT_QUALITY(PROOF_QUALITY.0),
..Default::default()
});
SelectObject(hdc, hfont);
for (i, window) in windows.iter().enumerate() {
if window.hwnd == focused_hwnd {
SetTextColor(hdc, COLORREF(focused_text_colour));
} else {
SetTextColor(hdc, COLORREF(unfocused_text_colour));
}
let left = gap + (i as i32 * (width + gap));
let mut tab_box = Rect {
top: 0,
left,
right: left + width,
bottom: height,
};
WindowsApi::round_rect(hdc, &tab_box, 8);
let exe = window.exe()?;
let exe_trimmed = exe.trim_end_matches(".exe");
let mut tab_title: Vec<u16> = exe_trimmed.encode_utf16().collect();
tab_box.left_padding(10);
tab_box.right_padding(10);
DrawTextW(
hdc,
&mut tab_title,
&mut tab_box.into(),
DT_SINGLELINE | DT_CENTER | DT_VCENTER | DT_END_ELLIPSIS,
);
}
ReleaseDC(self.hwnd(), hdc);
}
let mut windows_hwdns: VecDeque<isize> = VecDeque::new();
for window in windows {
windows_hwdns.push_back(window.hwnd);
}
WINDOWS_BY_BAR_HWNDS.lock().insert(self.hwnd, windows_hwdns);
Ok(())
}
pub fn hide(&self) {
WindowsApi::hide_window(self.hwnd())
}
pub fn restore(&self) {
WindowsApi::show_window(self.hwnd(), SW_SHOW)
}
}

View File

@@ -0,0 +1,210 @@
mod stackbar;
use crate::container::Container;
use crate::stackbar_manager::stackbar::Stackbar;
use crate::WindowManager;
use crate::WindowsApi;
use crate::DEFAULT_CONTAINER_PADDING;
use crossbeam_channel::Receiver;
use crossbeam_channel::Sender;
use crossbeam_utils::atomic::AtomicCell;
use crossbeam_utils::atomic::AtomicConsume;
use komorebi_core::StackbarLabel;
use komorebi_core::StackbarMode;
use lazy_static::lazy_static;
use parking_lot::Mutex;
use std::collections::hash_map::Entry;
use std::collections::HashMap;
use std::sync::atomic::AtomicI32;
use std::sync::atomic::AtomicU32;
use std::sync::Arc;
use std::sync::OnceLock;
use windows::Win32::Foundation::HWND;
pub static STACKBAR_FOCUSED_TEXT_COLOUR: AtomicU32 = AtomicU32::new(16777215); // white
pub static STACKBAR_UNFOCUSED_TEXT_COLOUR: AtomicU32 = AtomicU32::new(11776947); // gray text
pub static STACKBAR_TAB_BACKGROUND_COLOUR: AtomicU32 = AtomicU32::new(3355443); // gray
pub static STACKBAR_TAB_HEIGHT: AtomicI32 = AtomicI32::new(40);
pub static STACKBAR_TAB_WIDTH: AtomicI32 = AtomicI32::new(200);
pub static STACKBAR_LABEL: AtomicCell<StackbarLabel> = AtomicCell::new(StackbarLabel::Process);
pub static STACKBAR_MODE: AtomicCell<StackbarMode> = AtomicCell::new(StackbarMode::OnStack);
lazy_static! {
pub static ref STACKBAR_STATE: Mutex<HashMap<String, Stackbar>> = Mutex::new(HashMap::new());
static ref STACKBARS_MONITORS: Mutex<HashMap<String, usize>> = Mutex::new(HashMap::new());
static ref STACKBARS_CONTAINERS: Mutex<HashMap<isize, Container>> = Mutex::new(HashMap::new());
}
pub struct Notification;
static CHANNEL: OnceLock<(Sender<Notification>, Receiver<Notification>)> = OnceLock::new();
pub fn channel() -> &'static (Sender<Notification>, Receiver<Notification>) {
CHANNEL.get_or_init(crossbeam_channel::unbounded)
}
pub fn event_tx() -> Sender<Notification> {
channel().0.clone()
}
pub fn event_rx() -> Receiver<Notification> {
channel().1.clone()
}
pub fn should_have_stackbar(window_count: usize) -> bool {
match STACKBAR_MODE.load() {
StackbarMode::Always => true,
StackbarMode::OnStack => window_count > 1,
StackbarMode::Never => false,
}
}
pub fn listen_for_notifications(wm: Arc<Mutex<WindowManager>>) {
std::thread::spawn(move || loop {
match handle_notifications(wm.clone()) {
Ok(()) => {
tracing::warn!("restarting finished thread");
}
Err(error) => {
tracing::warn!("restarting failed thread: {}", error);
}
}
});
}
pub fn handle_notifications(wm: Arc<Mutex<WindowManager>>) -> color_eyre::Result<()> {
tracing::info!("listening");
let receiver = event_rx();
'receiver: for _ in receiver {
let mut stackbars = STACKBAR_STATE.lock();
let mut stackbars_monitors = STACKBARS_MONITORS.lock();
// Check the wm state every time we receive a notification
let mut state = wm.lock();
// If stackbars are disabled
if matches!(STACKBAR_MODE.load(), StackbarMode::Never) {
for (_, stackbar) in stackbars.iter() {
stackbar.destroy()?;
}
stackbars.clear();
continue 'receiver;
}
for (monitor_idx, m) in state.monitors_mut().iter_mut().enumerate() {
// Only operate on the focused workspace of each monitor
if let Some(ws) = m.focused_workspace_mut() {
// Workspaces with tiling disabled don't have stackbars
if !ws.tile() {
let mut to_remove = vec![];
for (id, border) in stackbars.iter() {
if stackbars_monitors.get(id).copied().unwrap_or_default() == monitor_idx {
border.destroy()?;
to_remove.push(id.clone());
}
}
for id in &to_remove {
stackbars.remove(id);
}
continue 'receiver;
}
let is_maximized = WindowsApi::is_zoomed(HWND(
WindowsApi::foreground_window().unwrap_or_default(),
));
// Handle the monocle container separately
if ws.monocle_container().is_some() || is_maximized {
// Destroy any stackbars associated with the focused workspace
let mut to_remove = vec![];
for (id, stackbar) in stackbars.iter() {
if stackbars_monitors.get(id).copied().unwrap_or_default() == monitor_idx {
stackbar.destroy()?;
to_remove.push(id.clone());
}
}
for id in &to_remove {
stackbars.remove(id);
}
continue 'receiver;
}
// Destroy any stackbars not associated with the focused workspace
let container_ids = ws
.containers()
.iter()
.map(|c| c.id().clone())
.collect::<Vec<_>>();
let mut to_remove = vec![];
for (id, stackbar) in stackbars.iter() {
if stackbars_monitors.get(id).copied().unwrap_or_default() == monitor_idx
&& !container_ids.contains(id)
{
stackbar.destroy()?;
to_remove.push(id.clone());
}
}
for id in &to_remove {
stackbars.remove(id);
}
let container_padding = ws
.container_padding()
.unwrap_or_else(|| DEFAULT_CONTAINER_PADDING.load_consume());
'containers: for container in ws.containers_mut() {
let should_add_stackbar = match STACKBAR_MODE.load() {
StackbarMode::Always => true,
StackbarMode::OnStack => container.windows().len() > 1,
StackbarMode::Never => false,
};
if !should_add_stackbar {
if let Some(stackbar) = stackbars.get(container.id()) {
stackbar.destroy()?
}
stackbars.remove(container.id());
stackbars_monitors.remove(container.id());
continue 'containers;
}
// Get the stackbar entry for this container from the map or create one
let stackbar = match stackbars.entry(container.id().clone()) {
Entry::Occupied(entry) => entry.into_mut(),
Entry::Vacant(entry) => {
if let Ok(stackbar) = Stackbar::create(container.id()) {
entry.insert(stackbar)
} else {
continue 'receiver;
}
}
};
stackbars_monitors.insert(container.id().clone(), monitor_idx);
let rect = WindowsApi::window_rect(
container
.focused_window()
.copied()
.unwrap_or_default()
.hwnd(),
)?;
stackbar.update(container_padding, container, &rect)?;
}
}
}
}
Ok(())
}

View File

@@ -0,0 +1,328 @@
use crate::border_manager::BORDER_OFFSET;
use crate::border_manager::BORDER_WIDTH;
use crate::border_manager::STYLE;
use crate::container::Container;
use crate::stackbar_manager::STACKBARS_CONTAINERS;
use crate::stackbar_manager::STACKBAR_FOCUSED_TEXT_COLOUR;
use crate::stackbar_manager::STACKBAR_LABEL;
use crate::stackbar_manager::STACKBAR_TAB_BACKGROUND_COLOUR;
use crate::stackbar_manager::STACKBAR_TAB_HEIGHT;
use crate::stackbar_manager::STACKBAR_TAB_WIDTH;
use crate::stackbar_manager::STACKBAR_UNFOCUSED_TEXT_COLOUR;
use crate::WindowsApi;
use crate::DEFAULT_CONTAINER_PADDING;
use crate::WINDOWS_11;
use crossbeam_utils::atomic::AtomicConsume;
use komorebi_core::BorderStyle;
use komorebi_core::Rect;
use komorebi_core::StackbarLabel;
use std::sync::mpsc;
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::CreateFontIndirectW;
use windows::Win32::Graphics::Gdi::CreatePen;
use windows::Win32::Graphics::Gdi::CreateSolidBrush;
use windows::Win32::Graphics::Gdi::DrawTextW;
use windows::Win32::Graphics::Gdi::GetDC;
use windows::Win32::Graphics::Gdi::Rectangle;
use windows::Win32::Graphics::Gdi::ReleaseDC;
use windows::Win32::Graphics::Gdi::RoundRect;
use windows::Win32::Graphics::Gdi::SelectObject;
use windows::Win32::Graphics::Gdi::SetBkColor;
use windows::Win32::Graphics::Gdi::SetTextColor;
use windows::Win32::Graphics::Gdi::DT_CENTER;
use windows::Win32::Graphics::Gdi::DT_END_ELLIPSIS;
use windows::Win32::Graphics::Gdi::DT_SINGLELINE;
use windows::Win32::Graphics::Gdi::DT_VCENTER;
use windows::Win32::Graphics::Gdi::FONT_QUALITY;
use windows::Win32::Graphics::Gdi::FW_BOLD;
use windows::Win32::Graphics::Gdi::LOGFONTW;
use windows::Win32::Graphics::Gdi::PROOF_QUALITY;
use windows::Win32::Graphics::Gdi::PS_SOLID;
use windows::Win32::UI::WindowsAndMessaging::CreateWindowExW;
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::SetLayeredWindowAttributes;
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::LWA_COLORKEY;
use windows::Win32::UI::WindowsAndMessaging::MSG;
use windows::Win32::UI::WindowsAndMessaging::WM_DESTROY;
use windows::Win32::UI::WindowsAndMessaging::WM_LBUTTONDOWN;
use windows::Win32::UI::WindowsAndMessaging::WNDCLASSW;
use windows::Win32::UI::WindowsAndMessaging::WS_EX_LAYERED;
use windows::Win32::UI::WindowsAndMessaging::WS_EX_TOOLWINDOW;
use windows::Win32::UI::WindowsAndMessaging::WS_POPUP;
use windows::Win32::UI::WindowsAndMessaging::WS_VISIBLE;
#[derive(Debug)]
pub struct Stackbar {
pub hwnd: isize,
}
impl From<isize> for Stackbar {
fn from(value: isize) -> Self {
Self { hwnd: value }
}
}
impl Stackbar {
pub const fn hwnd(&self) -> HWND {
HWND(self.hwnd)
}
pub fn create(id: &str) -> color_eyre::Result<Self> {
let name: Vec<u16> = format!("komostackbar-{id}\0").encode_utf16().collect();
let class_name = PCWSTR(name.as_ptr());
let h_module = WindowsApi::module_handle_w()?;
let window_class = WNDCLASSW {
style: CS_HREDRAW | CS_VREDRAW,
lpfnWndProc: Some(Self::callback),
hInstance: h_module.into(),
lpszClassName: class_name,
hbrBackground: WindowsApi::create_solid_brush(0),
..Default::default()
};
let _ = WindowsApi::register_class_w(&window_class);
let (hwnd_sender, hwnd_receiver) = mpsc::channel();
let name_cl = name.clone();
std::thread::spawn(move || -> color_eyre::Result<()> {
unsafe {
let hwnd = CreateWindowExW(
WS_EX_TOOLWINDOW | WS_EX_LAYERED,
PCWSTR(name_cl.as_ptr()),
PCWSTR(name_cl.as_ptr()),
WS_POPUP | WS_VISIBLE,
0,
0,
0,
0,
None,
None,
h_module,
None,
);
SetLayeredWindowAttributes(hwnd, COLORREF(0), 0, LWA_COLORKEY)?;
hwnd_sender.send(hwnd)?;
let mut msg = MSG::default();
while GetMessageW(&mut msg, hwnd, 0, 0).into() {
TranslateMessage(&msg);
DispatchMessageW(&msg);
std::thread::sleep(Duration::from_millis(10));
}
}
Ok(())
});
Ok(Self {
hwnd: hwnd_receiver.recv()?.0,
})
}
pub fn destroy(&self) -> color_eyre::Result<()> {
WindowsApi::close_window(self.hwnd())
}
pub fn update(
&self,
container_padding: i32,
container: &mut Container,
layout: &Rect,
) -> color_eyre::Result<()> {
let width = STACKBAR_TAB_WIDTH.load_consume();
let height = STACKBAR_TAB_HEIGHT.load_consume();
let gap = DEFAULT_CONTAINER_PADDING.load_consume();
let background = STACKBAR_TAB_BACKGROUND_COLOUR.load_consume();
let focused_text_colour = STACKBAR_FOCUSED_TEXT_COLOUR.load_consume();
let unfocused_text_colour = STACKBAR_UNFOCUSED_TEXT_COLOUR.load_consume();
let mut stackbars_containers = STACKBARS_CONTAINERS.lock();
stackbars_containers.insert(self.hwnd, container.clone());
let mut layout = *layout;
let workspace_specific_offset =
BORDER_WIDTH.load_consume() + BORDER_OFFSET.load_consume() + container_padding;
layout.top -= workspace_specific_offset + STACKBAR_TAB_HEIGHT.load_consume();
layout.left -= workspace_specific_offset;
WindowsApi::position_window(self.hwnd(), &layout, false)?;
unsafe {
let hdc = GetDC(self.hwnd());
let hpen = CreatePen(PS_SOLID, 0, COLORREF(background));
let hbrush = CreateSolidBrush(COLORREF(background));
SelectObject(hdc, hpen);
SelectObject(hdc, hbrush);
SetBkColor(hdc, COLORREF(background));
let hfont = CreateFontIndirectW(&LOGFONTW {
lfWeight: FW_BOLD.0 as i32,
lfQuality: FONT_QUALITY(PROOF_QUALITY.0),
..Default::default()
});
SelectObject(hdc, hfont);
for (i, window) in container.windows().iter().enumerate() {
if window.hwnd == container.focused_window().copied().unwrap_or_default().hwnd {
SetTextColor(hdc, COLORREF(focused_text_colour));
} else {
SetTextColor(hdc, COLORREF(unfocused_text_colour));
}
let left = gap + (i as i32 * (width + gap));
let mut rect = Rect {
top: 0,
left,
right: left + width,
bottom: height,
};
match *STYLE.lock() {
BorderStyle::System => {
if *WINDOWS_11 {
RoundRect(hdc, rect.left, rect.top, rect.right, rect.bottom, 20, 20);
} else {
Rectangle(hdc, rect.left, rect.top, rect.right, rect.bottom);
}
}
BorderStyle::Rounded => {
RoundRect(hdc, rect.left, rect.top, rect.right, rect.bottom, 20, 20);
}
BorderStyle::Square => {
Rectangle(hdc, rect.left, rect.top, rect.right, rect.bottom);
}
}
let label = match STACKBAR_LABEL.load() {
StackbarLabel::Process => {
let exe = window.exe()?;
exe.trim_end_matches(".exe").to_string()
}
StackbarLabel::Title => window.title()?,
};
let mut tab_title: Vec<u16> = label.encode_utf16().collect();
rect.left_padding(10);
rect.right_padding(10);
DrawTextW(
hdc,
&mut tab_title,
&mut rect.into(),
DT_SINGLELINE | DT_CENTER | DT_VCENTER | DT_END_ELLIPSIS,
);
}
ReleaseDC(self.hwnd(), hdc);
}
Ok(())
}
pub fn get_position_from_container_layout(&self, layout: &Rect) -> Rect {
Rect {
bottom: STACKBAR_TAB_HEIGHT.load_consume(),
..*layout
}
}
unsafe extern "system" fn callback(
hwnd: HWND,
msg: u32,
w_param: WPARAM,
l_param: LPARAM,
) -> LRESULT {
unsafe {
match msg {
WM_LBUTTONDOWN => {
let stackbars_containers = STACKBARS_CONTAINERS.lock();
if let Some(container) = stackbars_containers.get(&hwnd.0) {
let x = l_param.0 as i32 & 0xFFFF;
let y = (l_param.0 as i32 >> 16) & 0xFFFF;
let width = STACKBAR_TAB_WIDTH.load_consume();
let height = STACKBAR_TAB_HEIGHT.load_consume();
let gap = DEFAULT_CONTAINER_PADDING.load_consume();
let focused_window_idx = container.focused_window_idx();
let focused_window_rect = WindowsApi::window_rect(
container
.focused_window()
.cloned()
.unwrap_or_default()
.hwnd(),
)
.unwrap_or_default();
for (index, window) in container.windows().iter().enumerate() {
let left = gap + (index as i32 * (width + gap));
let right = left + width;
let top = 0;
let bottom = height;
if x >= left && x <= right && y >= top && y <= bottom {
// If we are focusing a window that isn't currently focused in the
// stackbar, make sure we update its location so that it doesn't render
// on top of other tiles before eventually ending up in the correct
// tile
if index != focused_window_idx {
if let Err(err) =
window.set_position(&focused_window_rect, false)
{
tracing::error!(
"stackbar WM_LBUTTONDOWN repositioning error: hwnd {} ({})",
*window,
err
);
}
}
// Restore the window corresponding to the tab we have clicked
window.restore();
if let Err(err) = window.focus(false) {
tracing::error!(
"stackbar WMLBUTTONDOWN focus error: hwnd {} ({})",
*window,
err
);
}
} else {
// Hide any windows in the stack that don't correspond to the window
// we have clicked
window.hide();
}
}
}
LRESULT(0)
}
WM_DESTROY => {
PostQuitMessage(0);
LRESULT(0)
}
_ => DefWindowProcW(hwnd, msg, w_param, l_param),
}
}
}
}

View File

@@ -5,7 +5,15 @@ use crate::border_manager::Z_ORDER;
use crate::colour::Colour;
use crate::current_virtual_desktop;
use crate::monitor::Monitor;
use crate::monitor_reconciliator;
use crate::ring::Ring;
use crate::stackbar_manager::STACKBAR_FOCUSED_TEXT_COLOUR;
use crate::stackbar_manager::STACKBAR_LABEL;
use crate::stackbar_manager::STACKBAR_MODE;
use crate::stackbar_manager::STACKBAR_TAB_BACKGROUND_COLOUR;
use crate::stackbar_manager::STACKBAR_TAB_HEIGHT;
use crate::stackbar_manager::STACKBAR_TAB_WIDTH;
use crate::stackbar_manager::STACKBAR_UNFOCUSED_TEXT_COLOUR;
use crate::window_manager::WindowManager;
use crate::window_manager_event::WindowManagerEvent;
use crate::windows_api::WindowsApi;
@@ -21,14 +29,9 @@ use crate::MANAGE_IDENTIFIERS;
use crate::MONITOR_INDEX_PREFERENCES;
use crate::OBJECT_NAME_CHANGE_ON_LAUNCH;
use crate::REGEX_IDENTIFIERS;
use crate::STACKBAR_FOCUSED_TEXT_COLOUR;
use crate::STACKBAR_MODE;
use crate::STACKBAR_TAB_BACKGROUND_COLOUR;
use crate::STACKBAR_TAB_HEIGHT;
use crate::STACKBAR_TAB_WIDTH;
use crate::STACKBAR_UNFOCUSED_TEXT_COLOUR;
use crate::TRAY_AND_MULTI_WINDOW_IDENTIFIERS;
use crate::WORKSPACE_RULES;
use komorebi_core::StackbarLabel;
use komorebi_core::StackbarMode;
use color_eyre::Result;
@@ -42,8 +45,8 @@ use komorebi_core::config_generation::IdWithIdentifier;
use komorebi_core::config_generation::MatchingRule;
use komorebi_core::config_generation::MatchingStrategy;
use komorebi_core::resolve_home_path;
use komorebi_core::ActiveWindowBorderStyle;
use komorebi_core::ApplicationIdentifier;
use komorebi_core::BorderStyle;
use komorebi_core::DefaultLayout;
use komorebi_core::FocusFollowsMouseImplementation;
use komorebi_core::HidingBehaviour;
@@ -69,7 +72,7 @@ use uds_windows::UnixListener;
use uds_windows::UnixStream;
#[derive(Debug, Serialize, Deserialize, JsonSchema)]
pub struct ActiveWindowBorderColours {
pub struct BorderColours {
/// Border colour when the container contains a single window
pub single: Option<Colour>,
/// Border colour when the container contains multiple windows
@@ -80,7 +83,7 @@ pub struct ActiveWindowBorderColours {
pub unfocused: Option<Colour>,
}
#[derive(Debug, Serialize, Deserialize, JsonSchema)]
#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)]
pub struct WorkspaceConfig {
/// Name
pub name: String,
@@ -194,7 +197,7 @@ impl From<&Workspace> for WorkspaceConfig {
}
}
#[derive(Debug, Serialize, Deserialize, JsonSchema)]
#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)]
pub struct MonitorConfig {
/// Workspace configurations
pub workspaces: Vec<WorkspaceConfig>,
@@ -226,7 +229,7 @@ impl From<&Monitor> for MonitorConfig {
}
#[derive(Debug, Serialize, Deserialize, JsonSchema)]
/// The `komorebi.json` static configuration file reference for `v0.1.25`
/// The `komorebi.json` static configuration file reference for `v0.1.26`
pub struct StaticConfig {
/// DEPRECATED from v0.1.22: no longer required
#[serde(skip_serializing_if = "Option::is_none")]
@@ -262,16 +265,19 @@ pub struct StaticConfig {
pub border_offset: Option<i32>,
/// Display an active window border (default: false)
#[serde(skip_serializing_if = "Option::is_none")]
pub active_window_border: Option<bool>,
#[serde(alias = "active_window_border")]
pub border: Option<bool>,
/// Active window border colours for different container types
#[serde(skip_serializing_if = "Option::is_none")]
pub active_window_border_colours: Option<ActiveWindowBorderColours>,
#[serde(alias = "active_window_border_colours")]
pub border_colours: Option<BorderColours>,
/// Active window border style (default: System)
#[serde(skip_serializing_if = "Option::is_none")]
pub active_window_border_style: Option<ActiveWindowBorderStyle>,
#[serde(alias = "active_window_border_style")]
pub border_style: Option<BorderStyle>,
/// Active window border z-order (default: System)
#[serde(skip_serializing_if = "Option::is_none")]
pub active_window_border_z_order: Option<ZOrder>,
pub border_z_order: Option<ZOrder>,
/// Global default workspace padding (default: 10)
#[serde(skip_serializing_if = "Option::is_none")]
pub default_workspace_padding: Option<i32>,
@@ -316,6 +322,48 @@ pub struct StaticConfig {
pub stackbar: Option<StackbarConfig>,
}
impl StaticConfig {
pub fn aliases(raw: &str) {
let mut map = HashMap::new();
map.insert("border", ["active_window_border"]);
map.insert("border_width", ["active_window_border_width"]);
map.insert("border_offset", ["active_window_border_offset"]);
map.insert("border_colours", ["active_window_border_colours"]);
map.insert("border_style", ["active_window_border_style"]);
let mut display = false;
for (_, aliases) in &map {
for a in aliases {
if raw.contains(a) {
display = true;
}
}
}
if display {
println!("\nYour configuration file contains some options that have been renamed or deprecated:\n");
for (canonical, aliases) in map {
for alias in aliases {
if raw.contains(alias) {
println!(r#""{alias}" is now "{canonical}""#);
}
}
}
}
}
pub fn deprecated(raw: &str) {
let deprecated = ["invisible_borders"];
for option in deprecated {
if raw.contains(option) {
println!(r#""{option}" is deprecated and can be removed"#);
}
}
}
}
#[derive(Debug, Serialize, Deserialize, JsonSchema)]
pub struct TabsConfig {
/// Width of a stackbar tab
@@ -327,11 +375,12 @@ pub struct TabsConfig {
/// Tab background colour
background: Option<Colour>,
}
#[derive(Debug, Serialize, Deserialize, JsonSchema)]
pub struct StackbarConfig {
/// Stackbar height
pub height: Option<i32>,
/// Stackbar height
pub label: Option<StackbarLabel>,
/// Stackbar mode
pub mode: Option<StackbarMode>,
/// Stackbar tab configuration options
@@ -394,7 +443,7 @@ impl From<&WindowManager> for StaticConfig {
let border_colours = if border_manager::FOCUSED.load(Ordering::SeqCst) == 0 {
None
} else {
Option::from(ActiveWindowBorderColours {
Option::from(BorderColours {
single: Option::from(Colour::from(border_manager::FOCUSED.load(Ordering::SeqCst))),
stack: Option::from(Colour::from(border_manager::STACK.load(Ordering::SeqCst))),
monocle: Option::from(Colour::from(border_manager::MONOCLE.load(Ordering::SeqCst))),
@@ -417,12 +466,10 @@ impl From<&WindowManager> for StaticConfig {
app_specific_configuration_path: None,
border_width: Option::from(border_manager::BORDER_WIDTH.load(Ordering::SeqCst)),
border_offset: Option::from(border_manager::BORDER_OFFSET.load(Ordering::SeqCst)),
active_window_border: Option::from(
border_manager::BORDER_ENABLED.load(Ordering::SeqCst),
),
active_window_border_colours: border_colours,
active_window_border_style: Option::from(*STYLE.lock()),
active_window_border_z_order: Option::from(*Z_ORDER.lock()),
border: Option::from(border_manager::BORDER_ENABLED.load(Ordering::SeqCst)),
border_colours,
border_style: Option::from(*STYLE.lock()),
border_z_order: Option::from(*Z_ORDER.lock()),
default_workspace_padding: Option::from(
DEFAULT_WORKSPACE_PADDING.load(Ordering::SeqCst),
),
@@ -474,11 +521,11 @@ impl StaticConfig {
border_manager::BORDER_WIDTH.store(self.border_width.unwrap_or(8), Ordering::SeqCst);
border_manager::BORDER_OFFSET.store(self.border_offset.unwrap_or(-1), Ordering::SeqCst);
if let Some(enabled) = &self.active_window_border {
if let Some(enabled) = &self.border {
border_manager::BORDER_ENABLED.store(*enabled, Ordering::SeqCst);
}
if let Some(colours) = &self.active_window_border_colours {
if let Some(colours) = &self.border_colours {
if let Some(single) = colours.single {
border_manager::FOCUSED.store(u32::from(single), Ordering::SeqCst);
}
@@ -496,8 +543,8 @@ impl StaticConfig {
}
}
let active_window_border_style = self.active_window_border_style.unwrap_or_default();
*STYLE.lock() = active_window_border_style;
let border_style = self.border_style.unwrap_or_default();
*STYLE.lock() = border_style;
let mut float_identifiers = FLOAT_IDENTIFIERS.lock();
let mut regex_identifiers = REGEX_IDENTIFIERS.lock();
@@ -539,9 +586,12 @@ impl StaticConfig {
STACKBAR_TAB_HEIGHT.store(*height, Ordering::SeqCst);
}
if let Some(label) = &stackbar.label {
STACKBAR_LABEL.store(*label);
}
if let Some(mode) = &stackbar.mode {
let mut stackbar_mode = STACKBAR_MODE.lock();
*stackbar_mode = *mode;
STACKBAR_MODE.store(*mode);
}
if let Some(tabs) = &stackbar.tabs {
@@ -641,7 +691,6 @@ impl StaticConfig {
let mut wm = WindowManager {
monitors: Ring::default(),
monitor_cache: HashMap::new(),
incoming_events: incoming,
command_listener: listener,
is_paused: false,
@@ -699,6 +748,13 @@ impl StaticConfig {
if let Some(monitors) = value.monitors {
for (i, monitor) in monitors.iter().enumerate() {
{
let display_index_preferences = DISPLAY_INDEX_PREFERENCES.lock();
if let Some(device_id) = display_index_preferences.get(&i) {
monitor_reconciliator::insert_in_monitor_cache(device_id, monitor.clone());
}
}
if let Some(m) = wm.monitors_mut().get_mut(i) {
m.ensure_workspace_count(monitor.workspaces.len());
m.set_work_area_offset(monitor.work_area_offset);
@@ -733,7 +789,7 @@ impl StaticConfig {
}
}
if value.active_window_border == Some(true) {
if value.border == Some(true) {
border_manager::BORDER_ENABLED.store(true, Ordering::SeqCst);
}
@@ -746,16 +802,6 @@ impl StaticConfig {
value.apply_globals()?;
let stackbar_mode = *STACKBAR_MODE.lock();
for m in wm.monitors_mut() {
for w in m.workspaces_mut() {
for c in w.containers_mut() {
c.set_stackbar_mode(stackbar_mode);
}
}
}
if let Some(monitors) = value.monitors {
for (i, monitor) in monitors.iter().enumerate() {
if let Some(m) = wm.monitors_mut().get_mut(i) {
@@ -792,7 +838,7 @@ impl StaticConfig {
}
}
if let Some(enabled) = value.active_window_border {
if let Some(enabled) = value.border {
border_manager::BORDER_ENABLED.store(enabled, Ordering::SeqCst);
}

View File

@@ -227,6 +227,11 @@ impl Window {
// If the target window is already focused, do nothing.
if let Ok(ihwnd) = WindowsApi::foreground_window() {
if HWND(ihwnd) == self.hwnd() {
// Center cursor in Window
if mouse_follows_focus {
WindowsApi::center_cursor_in_rect(&WindowsApi::window_rect(self.hwnd())?)?;
}
return Ok(());
}
}

View File

@@ -24,9 +24,9 @@ use uds_windows::UnixListener;
use komorebi_core::config_generation::MatchingRule;
use komorebi_core::custom_layout::CustomLayout;
use komorebi_core::ActiveWindowBorderStyle;
use komorebi_core::Arrangement;
use komorebi_core::Axis;
use komorebi_core::BorderStyle;
use komorebi_core::CycleDirection;
use komorebi_core::DefaultLayout;
use komorebi_core::FocusFollowsMouseImplementation;
@@ -37,6 +37,7 @@ use komorebi_core::OperationBehaviour;
use komorebi_core::OperationDirection;
use komorebi_core::Rect;
use komorebi_core::Sizing;
use komorebi_core::StackbarLabel;
use komorebi_core::WindowContainerBehaviour;
use crate::border_manager;
@@ -46,13 +47,20 @@ use crate::current_virtual_desktop;
use crate::load_configuration;
use crate::monitor::Monitor;
use crate::ring::Ring;
use crate::stackbar_manager::STACKBAR_FOCUSED_TEXT_COLOUR;
use crate::stackbar_manager::STACKBAR_LABEL;
use crate::stackbar_manager::STACKBAR_MODE;
use crate::stackbar_manager::STACKBAR_TAB_BACKGROUND_COLOUR;
use crate::stackbar_manager::STACKBAR_TAB_HEIGHT;
use crate::stackbar_manager::STACKBAR_TAB_WIDTH;
use crate::stackbar_manager::STACKBAR_UNFOCUSED_TEXT_COLOUR;
use crate::static_config::StaticConfig;
use crate::window::Window;
use crate::window_manager_event::WindowManagerEvent;
use crate::windows_api::WindowsApi;
use crate::winevent_listener;
use crate::workspace::Workspace;
use crate::ActiveWindowBorderColours;
use crate::BorderColours;
use crate::Colour;
use crate::Rgb;
use crate::WorkspaceRule;
@@ -68,12 +76,6 @@ use crate::MONITOR_INDEX_PREFERENCES;
use crate::NO_TITLEBAR;
use crate::OBJECT_NAME_CHANGE_ON_LAUNCH;
use crate::REMOVE_TITLEBARS;
use crate::STACKBAR_FOCUSED_TEXT_COLOUR;
use crate::STACKBAR_MODE;
use crate::STACKBAR_TAB_BACKGROUND_COLOUR;
use crate::STACKBAR_TAB_HEIGHT;
use crate::STACKBAR_TAB_WIDTH;
use crate::STACKBAR_UNFOCUSED_TEXT_COLOUR;
use crate::TRAY_AND_MULTI_WINDOW_IDENTIFIERS;
use crate::WORKSPACE_RULES;
use komorebi_core::StackbarMode;
@@ -81,7 +83,6 @@ use komorebi_core::StackbarMode;
#[derive(Debug)]
pub struct WindowManager {
pub monitors: Ring<Monitor>,
pub monitor_cache: HashMap<usize, Monitor>,
pub incoming_events: Receiver<WindowManagerEvent>,
pub command_listener: UnixListener,
pub is_paused: bool,
@@ -117,12 +118,13 @@ pub struct State {
#[allow(clippy::struct_excessive_bools)]
#[derive(Debug, Serialize, Deserialize, JsonSchema)]
pub struct GlobalState {
pub active_window_border_enabled: bool,
pub active_window_border_colours: ActiveWindowBorderColours,
pub active_window_border_style: ActiveWindowBorderStyle,
pub border_enabled: bool,
pub border_colours: BorderColours,
pub border_style: BorderStyle,
pub border_offset: i32,
pub border_width: i32,
pub stackbar_mode: StackbarMode,
pub stackbar_label: StackbarLabel,
pub stackbar_focused_text_colour: Colour,
pub stackbar_unfocused_text_colour: Colour,
pub stackbar_tab_background_colour: Colour,
@@ -146,8 +148,8 @@ pub struct GlobalState {
impl Default for GlobalState {
fn default() -> Self {
Self {
active_window_border_enabled: border_manager::BORDER_ENABLED.load(Ordering::SeqCst),
active_window_border_colours: ActiveWindowBorderColours {
border_enabled: border_manager::BORDER_ENABLED.load(Ordering::SeqCst),
border_colours: BorderColours {
single: Option::from(Colour::Rgb(Rgb::from(
border_manager::FOCUSED.load(Ordering::SeqCst),
))),
@@ -161,10 +163,11 @@ impl Default for GlobalState {
border_manager::UNFOCUSED.load(Ordering::SeqCst),
))),
},
active_window_border_style: *STYLE.lock(),
border_style: *STYLE.lock(),
border_offset: border_manager::BORDER_OFFSET.load(Ordering::SeqCst),
border_width: border_manager::BORDER_WIDTH.load(Ordering::SeqCst),
stackbar_mode: *STACKBAR_MODE.lock(),
stackbar_mode: STACKBAR_MODE.load(),
stackbar_label: STACKBAR_LABEL.load(),
stackbar_focused_text_colour: Colour::Rgb(Rgb::from(
STACKBAR_FOCUSED_TEXT_COLOUR.load(Ordering::SeqCst),
)),
@@ -262,7 +265,6 @@ impl WindowManager {
Ok(Self {
monitors: Ring::default(),
monitor_cache: HashMap::new(),
incoming_events: incoming,
command_listener: listener,
is_paused: false,
@@ -385,155 +387,6 @@ impl WindowManager {
None
}
#[tracing::instrument(skip(self))]
pub fn reconcile_monitors(&mut self) -> Result<()> {
if self.pending_move_op.is_some() {
return Ok(());
}
let valid_hmonitors = WindowsApi::valid_hmonitors()?;
let mut valid_names = vec![];
let before_count = self.monitors().len();
for monitor in self.monitors_mut() {
for (valid_name, valid_id) in &valid_hmonitors {
let actual_name = monitor.name().clone();
if actual_name == *valid_name {
monitor.set_id(*valid_id);
valid_names.push(actual_name);
}
}
}
let mut orphaned_containers = vec![];
let mut invalid_indices = vec![];
for (i, invalid) in self
.monitors()
.iter()
.enumerate()
.filter(|(_, m)| !valid_names.contains(m.name()))
{
invalid_indices.push(i);
for workspace in invalid.workspaces() {
for container in workspace.containers() {
// Save the orphaned containers from an invalid monitor
// (which has most likely been disconnected)
orphaned_containers.push(container.clone());
}
}
}
for i in invalid_indices {
if let Some(monitor) = self.monitors().get(i) {
self.monitor_cache.insert(i, monitor.clone());
}
}
// Remove any invalid monitors from our state
self.monitors_mut()
.retain(|m| valid_names.contains(m.name()));
let after_count = self.monitors().len();
if let Some(primary) = self.monitors_mut().front_mut() {
if let Some(focused_ws) = primary.focused_workspace_mut() {
let focused_container_idx = focused_ws.focused_container_idx();
// Put the orphaned containers somewhere visible
for container in orphaned_containers {
focused_ws.add_container(container);
}
// Gotta reset the focus or the movement will feel "off"
if before_count != after_count {
focused_ws.focus_container(focused_container_idx);
}
}
}
let offset = self.work_area_offset;
for monitor in self.monitors_mut() {
// If we have lost a monitor, update everything to filter out any jank
let mut should_update = before_count != after_count;
let reference = WindowsApi::monitor(monitor.id())?;
if reference.work_area_size() != monitor.work_area_size() {
monitor.set_work_area_size(Rect {
left: reference.work_area_size().left,
top: reference.work_area_size().top,
right: reference.work_area_size().right,
bottom: reference.work_area_size().bottom,
});
should_update = true;
}
if reference.size() != monitor.size() {
monitor.set_size(Rect {
left: reference.size().left,
top: reference.size().top,
right: reference.size().right,
bottom: reference.size().bottom,
});
should_update = true;
}
if should_update {
monitor.update_focused_workspace(offset)?;
}
}
#[allow(clippy::needless_collect)]
let old_sizes = self
.monitors()
.iter()
.map(Monitor::size)
.copied()
.collect::<Vec<_>>();
// Check for and add any new monitors that may have been plugged in
WindowsApi::load_monitor_information(&mut self.monitors)?;
let mut check_cache = vec![];
for (i, m) in self.monitors().iter().enumerate() {
if !old_sizes.contains(m.size()) {
check_cache.push(i);
}
}
for i in check_cache {
if let Some(cached) = self.monitor_cache.get(&i).cloned() {
if let Some(monitor) = self.monitors_mut().get_mut(i) {
for (w_idx, workspace) in monitor.workspaces_mut().iter_mut().enumerate() {
if let Some(cached_workspace) = cached.workspaces().get(w_idx) {
workspace.set_layout(cached_workspace.layout().clone());
workspace.set_layout_rules(cached_workspace.layout_rules().clone());
workspace.set_layout_flip(cached_workspace.layout_flip());
workspace.set_workspace_padding(cached_workspace.workspace_padding());
workspace.set_container_padding(cached_workspace.container_padding());
}
}
}
}
}
let final_count = self.monitors().len();
if after_count != final_count {
self.retile_all(true)?;
// Second retile to fix DPI/resolution related jank when a window
// moves between monitors with different resolutions - this doesn't
// really get seen by the user since the screens are flickering anyway
// as a result of the display connections / disconnections
self.retile_all(true)?;
}
Ok(())
}
#[allow(clippy::too_many_arguments)]
#[tracing::instrument(skip(self), level = "debug")]
fn add_window_handle_to_move_based_on_workspace_rule(
@@ -1280,7 +1133,13 @@ impl WindowManager {
tracing::info!("focusing container");
let new_idx = workspace.new_idx_for_direction(direction);
let new_idx = if workspace.monocle_container().is_some() {
None
} else {
workspace.new_idx_for_direction(direction)
};
let mut cross_monitor_monocle = false;
// if there is no container in that direction for this workspace
match new_idx {
@@ -1290,6 +1149,19 @@ impl WindowManager {
.ok_or_else(|| anyhow!("there is no container or monitor in this direction"))?;
self.focus_monitor(monitor_idx)?;
if let Ok(focused_workspace) = self.focused_workspace() {
if let Some(monocle) = focused_workspace.monocle_container() {
if let Some(window) = monocle.focused_window() {
window.focus(self.mouse_follows_focus)?;
WindowsApi::center_cursor_in_rect(&WindowsApi::window_rect(
window.hwnd(),
)?)?;
cross_monitor_monocle = true;
}
}
}
}
Some(idx) => {
let workspace = self.focused_workspace_mut()?;
@@ -1297,20 +1169,10 @@ impl WindowManager {
}
}
// When switching workspaces and landing focus on a window that is not stack, but a stack
// exists, and there is a stackbar visible, when changing focus to that container stack,
// the focused text colour will not be applied until the stack has been cycled at least once
//
// With this piece of code, we check if we have changed focus to a container stack with
// a stackbar, and if we have, we run a quick update to make sure the focused text colour
// has been applied
let focused_window = self.focused_window_mut()?;
let focused_window_hwnd = focused_window.hwnd;
focused_window.focus(self.mouse_follows_focus)?;
let focused_container = self.focused_container()?;
if let Some(stackbar) = focused_container.stackbar() {
stackbar.update(focused_container.windows(), focused_window_hwnd)?;
if !cross_monitor_monocle {
if let Ok(focused_window) = self.focused_window_mut() {
focused_window.focus(self.mouse_follows_focus)?;
}
}
Ok(())
@@ -1361,7 +1223,19 @@ impl WindowManager {
// focus the target monitor
self.focus_monitor(target_monitor_idx)?;
// get the focused workspace on the target monitor
// unset monocle container on target workspace if there is one
let mut target_workspace_has_monocle = false;
if let Ok(target_workspace) = self.focused_workspace() {
if target_workspace.monocle_container().is_some() {
target_workspace_has_monocle = true;
}
}
if target_workspace_has_monocle {
self.toggle_monocle()?;
}
// get a mutable ref to the focused workspace on the target monitor
let target_workspace = self.focused_workspace_mut()?;
// insert the origin container into the focused workspace on the target monitor
@@ -1558,8 +1432,11 @@ impl WindowManager {
})?;
let adjusted_new_index = if new_idx > current_container_idx
&& !matches!(workspace.layout(), Layout::Default(DefaultLayout::Grid))
{
&& !matches!(
workspace.layout(),
Layout::Default(DefaultLayout::Grid)
| Layout::Default(DefaultLayout::UltrawideVerticalStack)
) {
new_idx - 1
} else {
new_idx
@@ -1697,16 +1574,6 @@ impl WindowManager {
self.update_focused_workspace(true, true)?;
// TODO: fix this ugly hack to restore stackbar after monocle is toggled off
let workspace = self.focused_workspace()?;
if workspace.monocle_container().is_none() {
if let Some(container) = workspace.focused_container() {
if container.stackbar().is_some() {
self.retile_all(true)?;
};
}
};
Ok(())
}
@@ -1715,7 +1582,13 @@ impl WindowManager {
tracing::info!("enabling monocle");
let workspace = self.focused_workspace_mut()?;
workspace.new_monocle_container()
workspace.new_monocle_container()?;
for container in workspace.containers_mut() {
container.hide(None);
}
Ok(())
}
#[tracing::instrument(skip(self))]
@@ -1723,6 +1596,11 @@ impl WindowManager {
tracing::info!("disabling monocle");
let workspace = self.focused_workspace_mut()?;
for container in workspace.containers_mut() {
container.restore();
}
workspace.reintegrate_monocle_container()
}
@@ -2327,10 +2205,21 @@ impl WindowManager {
}
}
// our hmonitor might be stale, so if we didn't return above, try querying via the latest
// info taken from win32_display_data and update our hmonitor while we're at it
if let Ok(latest) = WindowsApi::monitor(hmonitor) {
for (i, monitor) in self.monitors_mut().iter_mut().enumerate() {
if monitor.device_id() == latest.device_id() {
monitor.set_id(latest.id());
return Option::from(i);
}
}
}
None
}
pub fn monitor_idx_from_current_pos(&self) -> Option<usize> {
pub fn monitor_idx_from_current_pos(&mut self) -> Option<usize> {
let hmonitor = WindowsApi::monitor_from_point(WindowsApi::cursor_pos().ok()?);
for (i, monitor) in self.monitors().iter().enumerate() {
@@ -2339,6 +2228,17 @@ impl WindowManager {
}
}
// our hmonitor might be stale, so if we didn't return above, try querying via the latest
// info taken from win32_display_data and update our hmonitor while we're at it
if let Ok(latest) = WindowsApi::monitor(hmonitor) {
for (i, monitor) in self.monitors_mut().iter_mut().enumerate() {
if monitor.device_id() == latest.device_id() {
monitor.set_id(latest.id());
return Option::from(i);
}
}
}
None
}

View File

@@ -27,8 +27,7 @@ pub enum WindowManagerEvent {
Manage(Window),
Unmanage(Window),
Raise(Window),
DisplayChange(Window),
ForceUpdate(Window),
TitleUpdate(WinEvent, Window),
}
impl Display for WindowManagerEvent {
@@ -76,11 +75,8 @@ impl Display for WindowManagerEvent {
Self::Raise(window) => {
write!(f, "Raise (Window: {window})")
}
Self::DisplayChange(window) => {
write!(f, "DisplayChange (Window: {window})")
}
Self::ForceUpdate(window) => {
write!(f, "ForceUpdate (Window: {window})")
Self::TitleUpdate(winevent, window) => {
write!(f, "TitleUpdate (WinEvent: {winevent}, Window: {window})")
}
}
}
@@ -101,9 +97,8 @@ impl WindowManagerEvent {
| Self::MouseCapture(_, window)
| Self::Raise(window)
| Self::Manage(window)
| Self::DisplayChange(window)
| Self::Unmanage(window)
| Self::ForceUpdate(window) => window,
| Self::TitleUpdate(_, window) => window,
}
}
@@ -146,7 +141,7 @@ impl WindowManagerEvent {
let class = &window.class().ok()?;
let path = &window.path().ok()?;
let should_trigger = should_act(
let should_trigger_show = should_act(
title,
exe_name,
class,
@@ -156,10 +151,10 @@ impl WindowManagerEvent {
)
.is_some();
if should_trigger {
if should_trigger_show {
Option::from(Self::Show(winevent, window))
} else {
None
Option::from(Self::TitleUpdate(winevent, window))
}
}
_ => None,

View File

@@ -4,9 +4,9 @@ use std::ffi::c_void;
use std::mem::size_of;
use color_eyre::eyre::anyhow;
use color_eyre::eyre::bail;
use color_eyre::eyre::Error;
use color_eyre::Result;
use widestring::U16CStr;
use windows::core::Result as WindowsCrateResult;
use windows::core::PCWSTR;
use windows::core::PWSTR;
@@ -30,14 +30,12 @@ use windows::Win32::Graphics::Dwm::DWM_CLOAKED_APP;
use windows::Win32::Graphics::Dwm::DWM_CLOAKED_INHERITED;
use windows::Win32::Graphics::Dwm::DWM_CLOAKED_SHELL;
use windows::Win32::Graphics::Gdi::CreateSolidBrush;
use windows::Win32::Graphics::Gdi::EnumDisplayDevicesW;
use windows::Win32::Graphics::Gdi::EnumDisplayMonitors;
use windows::Win32::Graphics::Gdi::GetMonitorInfoW;
use windows::Win32::Graphics::Gdi::MonitorFromPoint;
use windows::Win32::Graphics::Gdi::MonitorFromWindow;
use windows::Win32::Graphics::Gdi::Rectangle;
use windows::Win32::Graphics::Gdi::RoundRect;
use windows::Win32::Graphics::Gdi::DISPLAY_DEVICEW;
use windows::Win32::Graphics::Gdi::HBRUSH;
use windows::Win32::Graphics::Gdi::HDC;
use windows::Win32::Graphics::Gdi::HMONITOR;
@@ -46,6 +44,7 @@ use windows::Win32::Graphics::Gdi::MONITORINFOEXW;
use windows::Win32::Graphics::Gdi::MONITOR_DEFAULTTONEAREST;
use windows::Win32::System::LibraryLoader::GetModuleHandleW;
use windows::Win32::System::RemoteDesktop::ProcessIdToSessionId;
use windows::Win32::System::RemoteDesktop::WTSRegisterSessionNotification;
use windows::Win32::System::Threading::GetCurrentProcessId;
use windows::Win32::System::Threading::OpenProcess;
use windows::Win32::System::Threading::QueryFullProcessImageNameW;
@@ -95,7 +94,6 @@ use windows::Win32::UI::WindowsAndMessaging::ShowWindow;
use windows::Win32::UI::WindowsAndMessaging::SystemParametersInfoW;
use windows::Win32::UI::WindowsAndMessaging::WindowFromPoint;
use windows::Win32::UI::WindowsAndMessaging::CW_USEDEFAULT;
use windows::Win32::UI::WindowsAndMessaging::EDD_GET_DEVICE_INTERFACE_NAME;
use windows::Win32::UI::WindowsAndMessaging::GWL_EXSTYLE;
use windows::Win32::UI::WindowsAndMessaging::GWL_STYLE;
use windows::Win32::UI::WindowsAndMessaging::GW_HWNDNEXT;
@@ -140,6 +138,8 @@ use crate::ring::Ring;
use crate::set_window_position::SetWindowPosition;
use crate::windows_callbacks;
use crate::Window;
use crate::DISPLAY_INDEX_PREFERENCES;
use crate::MONITOR_INDEX_PREFERENCES;
pub enum WindowsResult<T, E> {
Err(E),
@@ -220,58 +220,74 @@ impl WindowsApi {
}
pub fn valid_hmonitors() -> Result<Vec<(String, isize)>> {
let mut monitors: Vec<(String, isize)> = vec![];
let monitors_ref: &mut Vec<(String, isize)> = monitors.as_mut();
Self::enum_display_monitors(
Some(windows_callbacks::valid_display_monitors),
monitors_ref as *mut Vec<(String, isize)> as isize,
)?;
Ok(win32_display_data::connected_displays()
.flatten()
.map(|d| {
let name = d.device_name.trim_start_matches(r"\\.\").to_string();
let name = name.split('\\').collect::<Vec<_>>()[0].to_string();
Ok(monitors)
(name, d.hmonitor)
})
.collect::<Vec<_>>())
}
pub fn load_monitor_information(monitors: &mut Ring<Monitor>) -> Result<()> {
Self::enum_display_monitors(
Some(windows_callbacks::enum_display_monitor),
monitors as *mut Ring<Monitor> as isize,
)?;
'read: for display in win32_display_data::connected_displays().flatten() {
let path = display.device_path.clone();
let mut split: Vec<_> = path.split('#').collect();
split.remove(0);
split.remove(split.len() - 1);
let device = split[0].to_string();
let device_id = split.join("-");
Ok(())
}
let name = display.device_name.trim_start_matches(r"\\.\").to_string();
let name = name.split('\\').collect::<Vec<_>>()[0].to_string();
pub fn enum_display_devices(
index: u32,
lp_device: Option<*const u16>,
) -> Result<DISPLAY_DEVICEW> {
#[allow(clippy::option_if_let_else)]
let lp_device = match lp_device {
None => PCWSTR::null(),
Some(lp_device) => PCWSTR(lp_device),
};
for monitor in monitors.elements() {
if device_id.eq(monitor.device_id()) {
continue 'read;
}
}
let mut display_device = DISPLAY_DEVICEW {
cb: u32::try_from(std::mem::size_of::<DISPLAY_DEVICEW>())?,
..Default::default()
};
let m = monitor::new(
display.hmonitor,
display.size.into(),
display.work_area_size.into(),
name,
device,
device_id,
);
match unsafe {
EnumDisplayDevicesW(
lp_device,
index,
std::ptr::addr_of_mut!(display_device),
EDD_GET_DEVICE_INTERFACE_NAME,
)
}
.ok()
{
Ok(()) => {}
Err(error) => {
tracing::error!("enum_display_devices: {}", error);
return Err(error.into());
let mut index_preference = None;
let monitor_index_preferences = MONITOR_INDEX_PREFERENCES.lock();
for (index, monitor_size) in &*monitor_index_preferences {
if m.size() == monitor_size {
index_preference = Option::from(index);
}
}
let display_index_preferences = DISPLAY_INDEX_PREFERENCES.lock();
for (index, id) in &*display_index_preferences {
if id.eq(m.device_id()) {
index_preference = Option::from(index);
}
}
if monitors.elements().is_empty() {
monitors.elements_mut().push_back(m);
} else if let Some(preference) = index_preference {
let current_len = monitors.elements().len();
while *preference > current_len {
monitors.elements_mut().reserve(1);
}
monitors.elements_mut().insert(*preference, m);
} else {
monitors.elements_mut().push_back(m);
}
}
Ok(display_device)
Ok(())
}
pub fn enum_windows(callback: WNDENUMPROC, callback_data_address: isize) -> Result<()> {
@@ -775,20 +791,32 @@ impl WindowsApi {
}
pub fn monitor(hmonitor: isize) -> Result<Monitor> {
let ex_info = Self::monitor_info_w(HMONITOR(hmonitor))?;
let name = U16CStr::from_slice_truncate(&ex_info.szDevice)
.expect("monitor name was not a valid u16 c string")
.to_ustring()
.to_string_lossy()
.trim_start_matches(r"\\.\")
.to_string();
for display in win32_display_data::connected_displays().flatten() {
if display.hmonitor == hmonitor {
let path = display.device_path;
let mut split: Vec<_> = path.split('#').collect();
split.remove(0);
split.remove(split.len() - 1);
let device = split[0].to_string();
let device_id = split.join("-");
Ok(monitor::new(
hmonitor,
ex_info.monitorInfo.rcMonitor.into(),
ex_info.monitorInfo.rcWork.into(),
name,
))
let name = display.device_name.trim_start_matches(r"\\.\").to_string();
let name = name.split('\\').collect::<Vec<_>>()[0].to_string();
let monitor = monitor::new(
hmonitor,
display.size.into(),
display.work_area_size.into(),
name,
device,
device_id,
);
return Ok(monitor);
}
}
bail!("could not find device_id for hmonitor: {hmonitor}");
}
pub fn set_process_dpi_awareness_context() -> Result<()> {
@@ -1031,4 +1059,8 @@ impl WindowsApi {
SendInput(&inputs, std::mem::size_of::<INPUT>() as i32)
}
}
pub fn wts_register_session_notification(hwnd: isize) -> Result<()> {
unsafe { WTSRegisterSessionNotification(HWND(hwnd), 1) }.process()
}
}

View File

@@ -1,129 +1,17 @@
use std::collections::VecDeque;
use widestring::U16CStr;
use windows::Win32::Foundation::BOOL;
use windows::Win32::Foundation::HWND;
use windows::Win32::Foundation::LPARAM;
use windows::Win32::Foundation::LRESULT;
use windows::Win32::Foundation::RECT;
use windows::Win32::Foundation::WPARAM;
use windows::Win32::Graphics::Gdi::HDC;
use windows::Win32::Graphics::Gdi::HMONITOR;
use windows::Win32::UI::Accessibility::HWINEVENTHOOK;
use windows::Win32::UI::WindowsAndMessaging::DefWindowProcW;
use windows::Win32::UI::WindowsAndMessaging::DBT_DEVNODES_CHANGED;
use windows::Win32::UI::WindowsAndMessaging::SPI_ICONVERTICALSPACING;
use windows::Win32::UI::WindowsAndMessaging::SPI_SETWORKAREA;
use windows::Win32::UI::WindowsAndMessaging::WM_DEVICECHANGE;
use windows::Win32::UI::WindowsAndMessaging::WM_DISPLAYCHANGE;
use windows::Win32::UI::WindowsAndMessaging::WM_SETTINGCHANGE;
use crate::container::Container;
use crate::monitor::Monitor;
use crate::ring::Ring;
use crate::window::RuleDebug;
use crate::window::Window;
use crate::window_manager_event::WindowManagerEvent;
use crate::windows_api::WindowsApi;
use crate::winevent::WinEvent;
use crate::winevent_listener;
use crate::DISPLAY_INDEX_PREFERENCES;
use crate::MONITOR_INDEX_PREFERENCES;
pub extern "system" fn valid_display_monitors(
hmonitor: HMONITOR,
_: HDC,
_: *mut RECT,
lparam: LPARAM,
) -> BOOL {
let monitors = unsafe { &mut *(lparam.0 as *mut Vec<(String, isize)>) };
if let Ok(m) = WindowsApi::monitor(hmonitor.0) {
monitors.push((m.name().to_string(), hmonitor.0));
}
true.into()
}
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>) };
// Don't duplicate a monitor that is already being managed
for monitor in monitors.elements() {
if monitor.id() == hmonitor.0 {
return true.into();
}
}
let current_index = monitors.elements().len();
if let Ok(mut m) = WindowsApi::monitor(hmonitor.0) {
#[allow(clippy::cast_possible_truncation)]
if let Ok(d) = WindowsApi::enum_display_devices(current_index as u32, None) {
let name = U16CStr::from_slice_truncate(d.DeviceName.as_ref())
.expect("display device name was not a valid u16 c string")
.to_ustring()
.to_string_lossy()
.trim_start_matches(r"\\.\")
.to_string();
if name.eq(m.name()) {
if let Ok(device) = WindowsApi::enum_display_devices(0, Some(d.DeviceName.as_ptr()))
{
let id = U16CStr::from_slice_truncate(device.DeviceID.as_ref())
.expect("display device id was not a valid u16 c string")
.to_ustring()
.to_string_lossy()
.trim_start_matches(r"\\?\")
.to_string();
let mut split: Vec<_> = id.split('#').collect();
split.remove(0);
split.remove(split.len() - 1);
m.set_device(Option::from(split[0].to_string()));
m.set_device_id(Option::from(split.join("-")));
}
}
}
let monitor_index_preferences = MONITOR_INDEX_PREFERENCES.lock();
let mut index_preference = None;
for (index, monitor_size) in &*monitor_index_preferences {
if m.size() == monitor_size {
index_preference = Option::from(index);
}
}
let display_index_preferences = DISPLAY_INDEX_PREFERENCES.lock();
for (index, device) in &*display_index_preferences {
if let Some(known_device) = m.device_id() {
if device == known_device {
index_preference = Option::from(index);
}
}
}
if monitors.elements().is_empty() {
monitors.elements_mut().push_back(m);
} else if let Some(preference) = index_preference {
let current_len = monitors.elements().len();
if *preference > current_len {
monitors.elements_mut().reserve(1);
}
monitors.elements_mut().insert(*preference, m);
} else {
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>) };
@@ -192,8 +80,19 @@ pub extern "system" fn win_event_hook(
Ok(event) => event,
Err(_) => return,
};
let event_type = match WindowManagerEvent::from_win_event(winevent, window) {
None => return,
None => {
tracing::trace!(
"Unhandled WinEvent: {winevent} (hwnd: {}, exe: {}, title: {}, class: {})",
window.hwnd,
window.exe().unwrap_or_default(),
window.title().unwrap_or_default(),
window.class().unwrap_or_default()
);
return;
}
Some(event) => event,
};
@@ -201,48 +100,3 @@ pub extern "system" fn win_event_hook(
.send(event_type)
.expect("could not send message on winevent_listener::event_tx");
}
pub extern "system" fn hidden_window(
window: HWND,
message: u32,
wparam: WPARAM,
lparam: LPARAM,
) -> LRESULT {
unsafe {
match message {
WM_DISPLAYCHANGE => {
let event_type = WindowManagerEvent::DisplayChange(Window { hwnd: window.0 });
winevent_listener::event_tx()
.send(event_type)
.expect("could not send message on winevent_listener::event_tx");
LRESULT(0)
}
// Added based on this https://stackoverflow.com/a/33762334
WM_SETTINGCHANGE => {
#[allow(clippy::cast_possible_truncation)]
if wparam.0 as u32 == SPI_SETWORKAREA.0
|| wparam.0 as u32 == SPI_ICONVERTICALSPACING.0
{
let event_type = WindowManagerEvent::DisplayChange(Window { hwnd: window.0 });
winevent_listener::event_tx()
.send(event_type)
.expect("could not send message on winevent_listener::event_tx");
}
LRESULT(0)
}
// Added based on this https://stackoverflow.com/a/33762334
WM_DEVICECHANGE => {
#[allow(clippy::cast_possible_truncation)]
if wparam.0 as u32 == DBT_DEVNODES_CHANGED {
let event_type = WindowManagerEvent::DisplayChange(Window { hwnd: window.0 });
winevent_listener::event_tx()
.send(event_type)
.expect("could not send message on winevent_listener::event_tx");
}
LRESULT(0)
}
_ => DefWindowProcW(window, message, wparam, lparam),
}
}
}

View File

@@ -24,6 +24,8 @@ use crate::border_manager::BORDER_OFFSET;
use crate::border_manager::BORDER_WIDTH;
use crate::container::Container;
use crate::ring::Ring;
use crate::stackbar_manager;
use crate::stackbar_manager::STACKBAR_TAB_HEIGHT;
use crate::static_config::WorkspaceConfig;
use crate::window::Window;
use crate::window::WindowDetails;
@@ -33,7 +35,6 @@ use crate::DEFAULT_WORKSPACE_PADDING;
use crate::INITIAL_CONFIGURATION_LOADED;
use crate::NO_TITLEBAR;
use crate::REMOVE_TITLEBARS;
use crate::STACKBAR_TAB_HEIGHT;
#[allow(clippy::struct_field_names)]
#[derive(
@@ -179,6 +180,14 @@ impl Workspace {
}
pub fn restore(&mut self, mouse_follows_focus: bool) -> Result<()> {
if let Some(container) = self.monocle_container_mut() {
if let Some(window) = container.focused_window() {
container.restore();
window.focus(mouse_follows_focus)?;
return Ok(());
}
}
let idx = self.focused_container_idx();
let mut to_focus = None;
@@ -196,10 +205,6 @@ impl Workspace {
container.restore();
}
if let Some(container) = self.monocle_container_mut() {
container.restore();
}
for window in self.floating_windows() {
window.restore();
}
@@ -300,7 +305,7 @@ impl Workspace {
} else if let Some(window) = self.maximized_window_mut() {
window.maximize();
} else if !self.containers().is_empty() {
let layouts = self.layout().as_boxed_arrangement().calculate(
let mut layouts = self.layout().as_boxed_arrangement().calculate(
&adjusted_work_area,
NonZeroUsize::new(self.containers().len()).ok_or_else(|| {
anyhow!(
@@ -315,24 +320,14 @@ impl Workspace {
let should_remove_titlebars = REMOVE_TITLEBARS.load(Ordering::SeqCst);
let no_titlebar = NO_TITLEBAR.lock().clone();
let focused_hwnd = self
.focused_container()
.ok_or_else(|| anyhow!("couldn't find a focused container"))?
.focused_window()
.ok_or_else(|| anyhow!("couldn't find a focused window"))?
.hwnd;
let container_padding = self.container_padding().unwrap_or(0);
let containers = self.containers_mut();
for (i, container) in containers.iter_mut().enumerate() {
container.renew_stackbar();
let container_windows = container.windows().clone();
let container_stackbar = container.stackbar().clone();
let window_count = container.windows().len();
if let (Some(window), Some(layout)) =
(container.focused_window_mut(), layouts.get(i))
(container.focused_window_mut(), layouts.get_mut(i))
{
if should_remove_titlebars && no_titlebar.contains(&window.exe()?) {
window.remove_title_bar()?;
@@ -346,33 +341,23 @@ impl Workspace {
WindowsApi::restore_window(window.hwnd());
}
let mut rect = *layout;
{
let border_offset = BORDER_OFFSET.load(Ordering::SeqCst);
rect.add_padding(border_offset);
layout.add_padding(border_offset);
let width = BORDER_WIDTH.load(Ordering::SeqCst);
rect.add_padding(width);
layout.add_padding(width);
}
if let Some(stackbar) = container_stackbar {
if stackbar
.set_position(
&stackbar.get_position_from_container_layout(layout),
false,
)
.is_ok()
{
stackbar.update(&container_windows, focused_hwnd)?;
let tab_height = STACKBAR_TAB_HEIGHT.load(Ordering::SeqCst);
let total_height = tab_height + container_padding;
if stackbar_manager::should_have_stackbar(window_count) {
let tab_height = STACKBAR_TAB_HEIGHT.load(Ordering::SeqCst);
let total_height = tab_height + container_padding;
rect.top += total_height;
rect.bottom -= total_height;
}
layout.top += total_height;
layout.bottom -= total_height;
}
window.set_position(&rect, false)?;
window.set_position(layout, false)?;
}
}
@@ -401,17 +386,10 @@ impl Workspace {
let containers = self.containers_mut();
for container in containers.iter_mut() {
let container_windows = container.windows().clone();
let container_topbar = container.stackbar().clone();
if let Some(idx) = container.idx_for_window(hwnd) {
container.focus_window(idx);
container.restore();
}
if let Some(stackbar) = container_topbar {
stackbar.update(&container_windows, hwnd)?;
}
}
Ok(())
}
@@ -682,6 +660,10 @@ impl Workspace {
self.set_monocle_container_restore_idx(None);
}
for c in self.containers() {
c.restore();
}
return Ok(());
}
}

View File

@@ -1,6 +1,6 @@
[package]
name = "komorebic-no-console"
version = "0.1.26-dev.0"
version = "0.1.27-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.26-dev.0"
version = "0.1.27-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"]
@@ -16,6 +16,7 @@ komorebi-core = { path = "../komorebi-core" }
komorebi-client = { path = "../komorebi-client" }
clap = { version = "4", features = ["derive", "wrap_help"] }
chrono = "0.4"
color-eyre = { workspace = true }
dirs = { workspace = true }
dunce = { workspace = true }

View File

@@ -1,6 +1,7 @@
#![warn(clippy::all, clippy::nursery, clippy::pedantic)]
#![allow(clippy::missing_errors_doc)]
use chrono::Local;
use std::fs::File;
use std::fs::OpenOptions;
use std::io::BufRead;
@@ -40,6 +41,7 @@ use windows::Win32::UI::WindowsAndMessaging::SW_RESTORE;
use derive_ahk::AhkFunction;
use derive_ahk::AhkLibrary;
use komorebi_client::StaticConfig;
use komorebi_core::config_generation::ApplicationConfigurationGenerator;
use komorebi_core::ApplicationIdentifier;
use komorebi_core::Axis;
@@ -160,6 +162,7 @@ gen_enum_subcommand_args! {
CycleMoveToMonitor: CycleDirection,
CycleMonitor: CycleDirection,
CycleWorkspace: CycleDirection,
CycleMoveWorkspaceToMonitor: CycleDirection,
Stack: OperationDirection,
CycleStack: CycleDirection,
FlipLayout: Axis,
@@ -171,6 +174,7 @@ gen_enum_subcommand_args! {
WindowHidingBehaviour: HidingBehaviour,
CrossMonitorMoveBehaviour: MoveBehaviour,
UnmanagedWindowOperationBehaviour: OperationBehaviour,
PromoteWindow: OperationDirection,
}
macro_rules! gen_target_subcommand_args {
@@ -651,13 +655,13 @@ struct FocusFollowsMouse {
}
#[derive(Parser, AhkFunction)]
struct ActiveWindowBorder {
struct Border {
#[clap(value_enum)]
boolean_state: BooleanState,
}
#[derive(Parser, AhkFunction)]
struct ActiveWindowBorderColour {
struct BorderColour {
#[clap(value_enum, short, long, default_value = "single")]
window_kind: WindowKind,
/// Red
@@ -669,14 +673,14 @@ struct ActiveWindowBorderColour {
}
#[derive(Parser, AhkFunction)]
struct ActiveWindowBorderWidth {
/// Desired width of the active window border
struct BorderWidth {
/// Desired width of the window border
width: i32,
}
#[derive(Parser, AhkFunction)]
struct ActiveWindowBorderOffset {
/// Desired offset of the active window border
struct BorderOffset {
/// Desired offset of the window border
offset: i32,
}
@@ -825,6 +829,8 @@ enum SubCommand {
State,
/// Show a JSON representation of the current global state
GlobalState,
/// Launch the komorebi-gui debugging tool
Gui,
/// Show a JSON representation of visible windows
VisibleWindows,
/// Query the current window manager state
@@ -955,6 +961,9 @@ enum SubCommand {
/// Move the focused workspace to the specified monitor
#[clap(arg_required_else_help = true)]
MoveWorkspaceToMonitor(MoveWorkspaceToMonitor),
/// Move the focused workspace monitor in the given cycle direction
#[clap(arg_required_else_help = true)]
CycleMoveWorkspaceToMonitor(CycleMoveWorkspaceToMonitor),
/// Swap focused monitor workspaces with specified monitor
#[clap(arg_required_else_help = true)]
SwapWorkspacesWithMonitor(SwapWorkspacesWithMonitor),
@@ -1000,6 +1009,8 @@ enum SubCommand {
Promote,
/// Promote the user focus to the top of the tree
PromoteFocus,
/// Promote the window in the specified direction
PromoteWindow(PromoteWindow),
/// Force the retiling of all managed windows
Retile,
/// Set the monitor index preference for a monitor identified using its size
@@ -1141,18 +1152,22 @@ enum SubCommand {
#[clap(hide = true)]
#[clap(alias = "identify-border-overflow")]
IdentifyBorderOverflowApplication(IdentifyBorderOverflowApplication),
/// Enable or disable the active window border
/// Enable or disable borders
#[clap(arg_required_else_help = true)]
ActiveWindowBorder(ActiveWindowBorder),
/// Set the colour for the active window border
#[clap(alias = "active-window-border")]
Border(Border),
/// Set the colour for a window border kind
#[clap(arg_required_else_help = true)]
ActiveWindowBorderColour(ActiveWindowBorderColour),
/// Set the width for the active window border
#[clap(alias = "active-window-border-colour")]
BorderColour(BorderColour),
/// Set the border width
#[clap(arg_required_else_help = true)]
ActiveWindowBorderWidth(ActiveWindowBorderWidth),
/// Set the offset for the active window border
#[clap(alias = "active-window-border-width")]
BorderWidth(BorderWidth),
/// Set the border offset
#[clap(arg_required_else_help = true)]
ActiveWindowBorderOffset(ActiveWindowBorderOffset),
#[clap(alias = "active-window-border-offset")]
BorderOffset(BorderOffset),
/// Enable or disable focus follows mouse for the operating system
#[clap(arg_required_else_help = true)]
FocusFollowsMouse(FocusFollowsMouse),
@@ -1479,7 +1494,8 @@ fn main() -> Result<()> {
println!("\n#Include komorebic.lib.ahk");
}
SubCommand::Log => {
let color_log = std::env::temp_dir().join("komorebi.log");
let timestamp = Local::now().format("%Y-%m-%d").to_string();
let color_log = std::env::temp_dir().join(format!("komorebi.log.{timestamp}"));
let file = TailedFile::new(File::open(color_log)?);
let locked = file.lock();
#[allow(clippy::significant_drop_in_scrutinee, clippy::lines_filter_map_ok)]
@@ -1505,6 +1521,9 @@ fn main() -> Result<()> {
SubCommand::PromoteFocus => {
send_message(&SocketMessage::PromoteFocus.as_bytes()?)?;
}
SubCommand::PromoteWindow(arg) => {
send_message(&SocketMessage::PromoteWindow(arg.operation_direction).as_bytes()?)?;
}
SubCommand::TogglePause => {
send_message(&SocketMessage::TogglePause.as_bytes()?)?;
}
@@ -1579,6 +1598,11 @@ fn main() -> Result<()> {
SubCommand::MoveWorkspaceToMonitor(arg) => {
send_message(&SocketMessage::MoveWorkspaceToMonitorNumber(arg.target).as_bytes()?)?;
}
SubCommand::CycleMoveWorkspaceToMonitor(arg) => {
send_message(
&SocketMessage::CycleMoveWorkspaceToMonitor(arg.cycle_direction).as_bytes()?,
)?;
}
SubCommand::SwapWorkspacesWithMonitor(arg) => {
send_message(&SocketMessage::SwapWorkspacesToMonitorNumber(arg.target).as_bytes()?)?;
}
@@ -1930,6 +1954,26 @@ if (!(Get-Process whkd -ErrorAction SilentlyContinue))
"* Subscribe to https://youtube.com/@LGUG2Z - Live dev videos and feature previews"
);
println!("* Join the Discord https://discord.gg/mGkn66PHkx - Chat, ask questions, share your desktops");
println!("* Read the docs https://lgug2z.github.io/komorebi - Quickly search through all komorebic commands");
let static_config = arg.config.map_or_else(
|| {
let komorebi_json = HOME_DIR.join("komorebi.json");
if komorebi_json.is_file() {
Option::from(komorebi_json)
} else {
None
}
},
Option::from,
);
if let Some(config) = static_config {
let path = resolve_home_path(config)?;
let raw = std::fs::read_to_string(path)?;
StaticConfig::aliases(&raw);
StaticConfig::deprecated(&raw);
}
}
SubCommand::Stop(arg) => {
if arg.whkd {
@@ -2108,6 +2152,9 @@ Stop-Process -Name:komorebi -ErrorAction SilentlyContinue
SubCommand::GlobalState => {
print_query(&SocketMessage::GlobalState.as_bytes()?);
}
SubCommand::Gui => {
Command::new("komorebi-gui").spawn()?;
}
SubCommand::VisibleWindows => {
print_query(&SocketMessage::VisibleWindows.as_bytes()?);
}
@@ -2212,19 +2259,18 @@ Stop-Process -Name:komorebi -ErrorAction SilentlyContinue
SubCommand::MouseFollowsFocus(arg) => {
send_message(&SocketMessage::MouseFollowsFocus(arg.boolean_state.into()).as_bytes()?)?;
}
SubCommand::ActiveWindowBorder(arg) => {
send_message(&SocketMessage::ActiveWindowBorder(arg.boolean_state.into()).as_bytes()?)?;
SubCommand::Border(arg) => {
send_message(&SocketMessage::Border(arg.boolean_state.into()).as_bytes()?)?;
}
SubCommand::ActiveWindowBorderColour(arg) => {
SubCommand::BorderColour(arg) => {
send_message(
&SocketMessage::ActiveWindowBorderColour(arg.window_kind, arg.r, arg.g, arg.b)
.as_bytes()?,
&SocketMessage::BorderColour(arg.window_kind, arg.r, arg.g, arg.b).as_bytes()?,
)?;
}
SubCommand::ActiveWindowBorderWidth(arg) => {
SubCommand::BorderWidth(arg) => {
send_message(&SocketMessage::BorderWidth(arg.width).as_bytes()?)?;
}
SubCommand::ActiveWindowBorderOffset(arg) => {
SubCommand::BorderOffset(arg) => {
send_message(&SocketMessage::BorderOffset(arg.offset).as_bytes()?)?;
}
SubCommand::ResizeDelta(arg) => {

View File

@@ -57,7 +57,7 @@ nav:
- Troubleshooting: troubleshooting.md
- Common workflows:
- common-workflows/komorebi-config-home.md
- common-workflows/active-window-border.md
- common-workflows/borders.md
- common-workflows/stackbar.md
- common-workflows/remove-gaps.md
- common-workflows/ignore-windows.md
@@ -67,7 +67,7 @@ nav:
- common-workflows/mouse-follows-focus.md
- common-workflows/custom-layouts.md
- common-workflows/dynamic-layout-switching.md
# - common-workflows/autohotkey.md
- common-workflows/autohotkey.md
- Release notes:
- release/v0-1-22.md
- Configuration reference: https://komorebi.lgug2z.com/schema
@@ -80,6 +80,7 @@ nav:
- cli/whkdrc.md
- cli/state.md
- cli/global-state.md
- cli/gui.md
- cli/visible-windows.md
- cli/query.md
- cli/subscribe-socket.md
@@ -124,6 +125,7 @@ nav:
- cli/cycle-monitor.md
- cli/cycle-workspace.md
- cli/move-workspace-to-monitor.md
- cli/cycle-move-workspace-to-monitor.md
- cli/swap-workspaces-with-monitor.md
- cli/new-workspace.md
- cli/resize-delta.md
@@ -140,6 +142,7 @@ nav:
- cli/flip-layout.md
- cli/promote.md
- cli/promote-focus.md
- cli/promote-window.md
- cli/retile.md
- cli/monitor-index-preference.md
- cli/display-index-preference.md
@@ -189,10 +192,10 @@ nav:
- cli/identify-layered-application.md
- cli/remove-title-bar.md
- cli/toggle-title-bars.md
- cli/active-window-border.md
- cli/active-window-border-colour.md
- cli/active-window-border-width.md
- cli/active-window-border-offset.md
- cli/border.md
- cli/border-colour.md
- cli/border-width.md
- cli/border-offset.md
- cli/focus-follows-mouse.md
- cli/toggle-focus-follows-mouse.md
- cli/mouse-follows-focus.md

View File

@@ -1,21 +1,20 @@
{
"$schema": "http://json-schema.org/draft-07/schema#",
"title": "StaticConfig",
"description": "The `komorebi.json` static configuration file reference for `v0.1.25`",
"description": "The `komorebi.json` static configuration file reference for `v0.1.26`",
"type": "object",
"properties": {
"active_window_border": {
"app_specific_configuration_path": {
"description": "Path to applications.yaml from komorebi-application-specific-configurations (default: None)",
"type": "string"
},
"border": {
"description": "Display an active window border (default: false)",
"type": "boolean"
},
"active_window_border_colours": {
"border_colours": {
"description": "Active window border colours for different container types",
"type": "object",
"required": [
"monocle",
"single",
"stack"
],
"properties": {
"monocle": {
"description": "Border colour when the container is in monocle mode",
@@ -130,39 +129,47 @@
"type": "string"
}
]
},
"unfocused": {
"description": "Border colour when the container is unfocused",
"anyOf": [
{
"description": "Colour represented as RGB",
"type": "object",
"required": [
"b",
"g",
"r"
],
"properties": {
"b": {
"description": "Blue",
"type": "integer",
"format": "uint32",
"minimum": 0.0
},
"g": {
"description": "Green",
"type": "integer",
"format": "uint32",
"minimum": 0.0
},
"r": {
"description": "Red",
"type": "integer",
"format": "uint32",
"minimum": 0.0
}
}
},
{
"description": "Colour represented as Hex",
"type": "string"
}
]
}
}
},
"active_window_border_style": {
"description": "Active window border style (default: System)",
"oneOf": [
{
"description": "Use the system border style",
"type": "string",
"enum": [
"System"
]
},
{
"description": "Use the Windows 11-style rounded borders",
"type": "string",
"enum": [
"Rounded"
]
},
{
"description": "Use the Windows 10-style square borders",
"type": "string",
"enum": [
"Square"
]
}
]
},
"app_specific_configuration_path": {
"description": "Path to applications.yaml from komorebi-application-specific-configurations (default: None)",
"type": "string"
},
"border_offset": {
"description": "Offset of the window border (default: -1)",
"type": "integer",
@@ -251,11 +258,47 @@
]
}
},
"border_style": {
"description": "Active window border style (default: System)",
"oneOf": [
{
"description": "Use the system border style",
"type": "string",
"enum": [
"System"
]
},
{
"description": "Use the Windows 11-style rounded borders",
"type": "string",
"enum": [
"Rounded"
]
},
{
"description": "Use the Windows 10-style square borders",
"type": "string",
"enum": [
"Square"
]
}
]
},
"border_width": {
"description": "Width of the window border (default: 8)",
"type": "integer",
"format": "int32"
},
"border_z_order": {
"description": "Active window border z-order (default: System)",
"type": "string",
"enum": [
"Top",
"NoTopMost",
"Bottom",
"TopMost"
]
},
"cross_monitor_move_behaviour": {
"description": "Determine what happens when a window is moved across a monitor boundary (default: Swap)",
"oneOf": [
@@ -272,6 +315,13 @@
"enum": [
"Insert"
]
},
{
"description": "Do nothing if trying to move a window container in the direction of an adjacent monitor",
"type": "string",
"enum": [
"NoOp"
]
}
]
},
@@ -668,6 +718,43 @@
"workspaces"
],
"properties": {
"window_based_work_area_offset": {
"description": "Window based work area offset (default: None)",
"type": "object",
"required": [
"bottom",
"left",
"right",
"top"
],
"properties": {
"bottom": {
"description": "The bottom point in a Win32 Rect",
"type": "integer",
"format": "int32"
},
"left": {
"description": "The left point in a Win32 Rect",
"type": "integer",
"format": "int32"
},
"right": {
"description": "The right point in a Win32 Rect",
"type": "integer",
"format": "int32"
},
"top": {
"description": "The top point in a Win32 Rect",
"type": "integer",
"format": "int32"
}
}
},
"window_based_work_area_offset_limit": {
"description": "Open window limit after which the window based work area offset will no longer be applied (default: 1)",
"type": "integer",
"format": "int"
},
"work_area_offset": {
"description": "Monitor-specific work area offset (default: None)",
"type": "object",
@@ -952,6 +1039,14 @@
"type": "integer",
"format": "int32"
},
"label": {
"description": "Stackbar height",
"type": "string",
"enum": [
"Process",
"Title"
]
},
"mode": {
"description": "Stackbar mode",
"type": "string",

Binary file not shown.

View File

@@ -95,6 +95,9 @@
<Component Id='binary2' Guid='*'>
<File Id='exe2' Name='komorebic-no-console.exe' DiskId='1' Source='$(var.CargoTargetBinDir)\komorebic-no-console.exe' KeyPath='yes' />
</Component>
<Component Id='binary3' Guid='*'>
<File Id='exe3' Name='komorebi-gui.exe' DiskId='1' Source='$(var.CargoTargetBinDir)\komorebi-gui.exe' KeyPath='yes' />
</Component>
</Directory>
</Directory>
</Directory>
@@ -113,6 +116,8 @@
<ComponentRef Id='binary2' />
<ComponentRef Id='binary3' />
<Feature Id='Environment' Title='PATH Environment Variable' Description='Add the install location of the [ProductName] executable to the PATH system environment variable. This allows the [ProductName] executable to be called from any location.' Level='1' Absent='allow'>
<ComponentRef Id='Path' />
</Feature>