mirror of
https://github.com/LGUG2Z/komorebi.git
synced 2026-01-12 22:13:00 +01:00
Compare commits
83 Commits
feature/mk
...
feature/st
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
b575cba9f4 | ||
|
|
24ac0c4cd3 | ||
|
|
e0652d953a | ||
|
|
d6e0ecc507 | ||
|
|
ca22cdb07f | ||
|
|
dc38eae2af | ||
|
|
66446f571c | ||
|
|
b2f6329963 | ||
|
|
bc46f65f64 | ||
|
|
69573c383f | ||
|
|
45a3f2a6b5 | ||
|
|
5af00b64cf | ||
|
|
81dff3279c | ||
|
|
2f17e4bb29 | ||
|
|
1b966d3731 | ||
|
|
e58d776f81 | ||
|
|
40e77fddfe | ||
|
|
9c8a50fe80 | ||
|
|
41e9068fca | ||
|
|
3690f8ebd8 | ||
|
|
7b24474ef2 | ||
|
|
f675717844 | ||
|
|
38b0418c3b | ||
|
|
6781f34930 | ||
|
|
4919872e1a | ||
|
|
d730c3c72d | ||
|
|
4e98d7d36d | ||
|
|
2bceff4edc | ||
|
|
b32bce8713 | ||
|
|
47af40cf9e | ||
|
|
2b9fbc2074 | ||
|
|
1a8b6a7398 | ||
|
|
e5cf042ea9 | ||
|
|
c435f84afc | ||
|
|
de0db4d014 | ||
|
|
0afcf6d86a | ||
|
|
e0e3afa5b9 | ||
|
|
94d8f72904 | ||
|
|
9b9777feaf | ||
|
|
0c2e37e127 | ||
|
|
10ae60f79b | ||
|
|
fbb34ba4b3 | ||
|
|
5ee827ecaf | ||
|
|
dc3ffb3bcb | ||
|
|
4affefad88 | ||
|
|
344e6ad2fd | ||
|
|
fd57d32bb5 | ||
|
|
0581950b21 | ||
|
|
a07bb4ac60 | ||
|
|
eab7a64250 | ||
|
|
98244b9572 | ||
|
|
2c156e9a99 | ||
|
|
d33df04f38 | ||
|
|
9c196b99c9 | ||
|
|
9fcf4ec19f | ||
|
|
c3e39311c1 | ||
|
|
e7d928a065 | ||
|
|
608ec03047 | ||
|
|
92359ebaed | ||
|
|
c19f64144a | ||
|
|
8642ac0946 | ||
|
|
dee5842c9c | ||
|
|
a6deeef717 | ||
|
|
0160e8eeeb | ||
|
|
f519cbaf1e | ||
|
|
ef1ce4a389 | ||
|
|
c8f6502b02 | ||
|
|
afd93c34a2 | ||
|
|
d52715a8fa | ||
|
|
549500887f | ||
|
|
40947e39e8 | ||
|
|
17a45804b4 | ||
|
|
0e14f25130 | ||
|
|
380971edee | ||
|
|
52122c401d | ||
|
|
e5ebf55115 | ||
|
|
0c75ec37d0 | ||
|
|
731a4465f1 | ||
|
|
596884e9fd | ||
|
|
5ef53c2b68 | ||
|
|
a00a85e63f | ||
|
|
e0aa0ac843 | ||
|
|
3e6e586d5b |
16
.github/workflows/windows.yaml
vendored
16
.github/workflows/windows.yaml
vendored
@@ -73,6 +73,11 @@ jobs:
|
||||
- name: Install the target
|
||||
run: |
|
||||
rustup target install ${{ matrix.target }}
|
||||
- name: Run Cargo checks
|
||||
run: |
|
||||
cargo fmt --check
|
||||
cargo check
|
||||
cargo clippy
|
||||
- name: Run a full build
|
||||
run: |
|
||||
cargo build --locked --release --target ${{ matrix.target }}
|
||||
@@ -87,10 +92,17 @@ jobs:
|
||||
path: |
|
||||
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.pdb
|
||||
target/${{ matrix.target }}/release/komorebic.pdb
|
||||
target/${{ matrix.target }}/release/komorebic-no-console.pdb
|
||||
target/wix/komorebi-*.msi
|
||||
retention-days: 7
|
||||
- name: Check GoReleaser
|
||||
uses: goreleaser/goreleaser-action@v3
|
||||
with:
|
||||
version: latest
|
||||
args: build --skip=validate --clean
|
||||
|
||||
# Release
|
||||
- name: Generate changelog
|
||||
@@ -104,12 +116,12 @@ jobs:
|
||||
if: startsWith(github.ref, 'refs/tags/v')
|
||||
with:
|
||||
version: latest
|
||||
args: release --skip-validate --clean --release-notes=CHANGELOG.md
|
||||
args: release --skip=validate --clean --release-notes=CHANGELOG.md
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
SCOOP_TOKEN: ${{ secrets.SCOOP_TOKEN }}
|
||||
- name: Add MSI to release
|
||||
uses: softprops/action-gh-release@v1
|
||||
uses: softprops/action-gh-release@v2
|
||||
if: startsWith(github.ref, 'refs/tags/v')
|
||||
with:
|
||||
files: "target/wix/komorebi-*.msi"
|
||||
|
||||
3
.gitignore
vendored
3
.gitignore
vendored
@@ -3,4 +3,5 @@
|
||||
/target
|
||||
CHANGELOG.md
|
||||
dummy.go
|
||||
komorebi.ahk
|
||||
komorebi.ahk
|
||||
komorebic/applications.yaml
|
||||
@@ -34,7 +34,7 @@ builds:
|
||||
hooks:
|
||||
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"
|
||||
- cp ".\target\x86_64-pc-windows-msvc\release\komorebic-no-console.exe" ".\dist\komorebic-no-console_windows_amd64_v1\komorebic-no-console.exe"
|
||||
|
||||
archives:
|
||||
- name_template: "{{ .ProjectName }}-{{ .Version }}-x86_64-pc-windows-msvc"
|
||||
|
||||
687
Cargo.lock
generated
687
Cargo.lock
generated
File diff suppressed because it is too large
Load Diff
@@ -4,20 +4,22 @@ resolver = "2"
|
||||
members = [
|
||||
"derive-ahk",
|
||||
"komorebi",
|
||||
"komorebi-client",
|
||||
"komorebi-core",
|
||||
"komorebic",
|
||||
"komorebic-no-console",
|
||||
]
|
||||
|
||||
[workspace.dependencies]
|
||||
windows-interface = { version = "0.52" }
|
||||
windows-implement = { version = "0.52" }
|
||||
windows-interface = { version = "0.53" }
|
||||
windows-implement = { version = "0.53" }
|
||||
dunce = "1"
|
||||
dirs = "5"
|
||||
color-eyre = "0.6"
|
||||
serde_json = { package = "serde_json_lenient", version = "0.1" }
|
||||
|
||||
[workspace.dependencies.windows]
|
||||
version = "0.52"
|
||||
version = "0.54"
|
||||
features = [
|
||||
"implement",
|
||||
"Win32_System_Com",
|
||||
|
||||
202
README.md
202
README.md
@@ -82,6 +82,8 @@ A [detailed installation and quickstart
|
||||
guide](https://lgug2z.github.io/komorebi/installation.html) is available which shows how to get started
|
||||
using `scoop`, `winget` or building from source.
|
||||
|
||||
[](https://www.youtube.com/watch?v=H9-_c1egQ4g)
|
||||
|
||||
# Demonstrations
|
||||
|
||||
[@haxibami](https://github.com/haxibami) showing _komorebi_ running on Windows
|
||||
@@ -99,20 +101,76 @@ widget enabled. The original video can be viewed
|
||||
|
||||
https://user-images.githubusercontent.com/13164844/163496414-a9cde3d1-b8a7-4a7a-96fb-a8985380bc70.mp4
|
||||
|
||||
# Development
|
||||
# Contribution Guidelines
|
||||
|
||||
If you would like to contribute code to this repository, there are a few requests that I have to ensure a foundation of
|
||||
code quality, consistency and commit hygiene:
|
||||
If you would like to contribute to `komorebi` please take the time to carefully read the guidelines below.
|
||||
|
||||
## Commit hygiene
|
||||
|
||||
- Flatten all `use` statements
|
||||
- Run `cargo +nightly clippy` and ensure that all lints and suggestions have been addressed before committing
|
||||
- Run `cargo +stable clippy` and ensure that all lints and suggestions have been addressed before committing
|
||||
- Run `cargo +nightly fmt --all` to ensure consistent formatting before committing
|
||||
- Use `git cz` with
|
||||
the [Commitizen CLI](https://github.com/commitizen/cz-cli#conventional-commit-messages-as-a-global-utility) to prepare
|
||||
commit messages
|
||||
- Provide at least one short sentence or paragraph in your commit message body to describe your thought process for the
|
||||
- Provide **at least** one short sentence or paragraph in your commit message body to describe your thought process for the
|
||||
changes being committed
|
||||
|
||||
## PRs should contain only a single feature or bug fix
|
||||
|
||||
It is very difficult to review pull requests which touch multiple unrelated features and parts of the codebase.
|
||||
|
||||
Please do not submit pull requests like this; you will be asked to separate them into smaller PRs that deal only with
|
||||
one feature or bug fix at a time.
|
||||
|
||||
If you are working on multiple features and bug fixes, I suggest that you cut a branch called `local-trunk`
|
||||
from `master` which you keep up to date, and rebase the various independent branches you are working on onto that branch
|
||||
if you want to test them together or create a build with everything integrated.
|
||||
|
||||
## Refactors to the codebase must have prior approval
|
||||
|
||||
`komorebi` is a mature codebase with an internal consistency and structure that has developed organically over close to
|
||||
half a decade.
|
||||
|
||||
There are [countless hours of live coding videos](https://youtube.com/@LGUG2Z) demonstrating work on this project and
|
||||
showing new contributors how to do everything from basic tasks like implementing new `komorebic` commands to
|
||||
distinguishing monitors by manufacturer hardware identifiers and video card ports.
|
||||
|
||||
Refactors to the structure of the codebase are not taken lightly and require prior discussion and approval.
|
||||
|
||||
Please do not start refactoring the codebase with the expectation of having your changes integrated until you receive an
|
||||
explicit approval or a request to do so.
|
||||
|
||||
Similarly, when implementing features and bug fixes, please stick to the structure of the codebase as much as possible
|
||||
and do not take this as an opportunity to do some "refactoring along the way".
|
||||
|
||||
It is extremely difficult to review PRs for features and bug fixes if they are lost in sweeping changes to the structure
|
||||
of the codebase.
|
||||
|
||||
## Breaking changes to user-facing interfaces are unacceptable
|
||||
|
||||
This includes but is not limited to:
|
||||
|
||||
- All `komorebic` commands
|
||||
- The `komorebi.json` schema
|
||||
- The [`komorebi-application-specific-configuration`](https://github.com/LGUG2Z/komorebi-application-specific-configuration)
|
||||
schema
|
||||
|
||||
No user should ever find that their configuration file has stopped working after upgrading to a new version
|
||||
of `komorebi`.
|
||||
|
||||
More often than not there are ways to reformulate changes that may initially seem like they require breaking user-facing
|
||||
interfaces into additive changes.
|
||||
|
||||
For some inspiration please take a look
|
||||
at [this commit](https://github.com/LGUG2Z/komorebi/commit/e7d928a065eb63bb4ea1fb864c69c1cae8cc763b) which added the
|
||||
ability for users to specify colours in `komorebi.json` in Hex format alongside RGB.
|
||||
|
||||
There is also a process in place for graceful, non-breaking, deprecation of configuration options that are no longer
|
||||
required.
|
||||
|
||||
# Development
|
||||
|
||||
If you use IntelliJ, you should enable the following settings to ensure that code generated by macros is recognised by
|
||||
the IDE for completions and navigation:
|
||||
|
||||
@@ -151,20 +209,21 @@ found, information about it will appear in the log which can be shared when open
|
||||
# Window Manager State and Integrations
|
||||
|
||||
The current state of the window manager can be queried using the `komorebic state` command, which returns a JSON
|
||||
representation of the `State` struct, which includes the current state of `WindowManager`.
|
||||
representation of the `State` struct.
|
||||
|
||||
This may also be polled to build further integrations and widgets on top of (if you ever wanted to build something
|
||||
like [Stackline](https://github.com/AdamWagner/stackline) for Windows, you could do it by polling this command).
|
||||
This may also be polled to build further integrations and widgets on top of.
|
||||
|
||||
# Window Manager Event Subscriptions
|
||||
|
||||
It is also possible to subscribe to notifications of every `WindowManagerEvent` and `SocketMessage` handled
|
||||
## Named Pipes
|
||||
|
||||
It is possible to subscribe to notifications of every `WindowManagerEvent` and `SocketMessage` handled
|
||||
by `komorebi` using [Named Pipes](https://docs.microsoft.com/en-us/windows/win32/ipc/named-pipes).
|
||||
|
||||
First, your application must create a named pipe. Once the named pipe has been created, run the following command:
|
||||
|
||||
```powershell
|
||||
komorebic.exe subscribe <your pipe name>
|
||||
komorebic.exe subscribe-pipe <your pipe name>
|
||||
```
|
||||
|
||||
Note that you do not have to include the full path of the named pipe, just the name.
|
||||
@@ -188,12 +247,125 @@ You may then filter on the `type` key to listen to the events that you are inter
|
||||
notification types, refer to the enum variants of `WindowManagerEvent` in `komorebi` and `SocketMessage`
|
||||
in `komorebi-core`.
|
||||
|
||||
An example of how to create a named pipe and a subscription to `komorebi`'s handled events in Python
|
||||
by [@denBot](https://github.com/denBot) can be
|
||||
found [here](https://gist.github.com/denBot/4136279812f87819f86d99eba77c1ee0).
|
||||
Below is an example of how you can subscribe to and filter on events using a named pipe in `nodejs`.
|
||||
|
||||
An example of how to create a named pipe and a subscription to `komorebi`'s handled events in Rust can also be found
|
||||
in the [`komokana`](https://github.com/LGUG2Z/komokana) repository.
|
||||
```javascript
|
||||
const { exec } = require("child_process");
|
||||
const net = require("net");
|
||||
|
||||
const pipeName = "\\\\.\\pipe\\komorebi-js";
|
||||
const server = net.createServer((stream) => {
|
||||
console.log("Client connected");
|
||||
|
||||
// Every time there is a workspace-related event, let's log the names of all
|
||||
// workspaces on the currently focused monitor, and then log the name of the
|
||||
// currently focused workspace on that monitor
|
||||
|
||||
stream.on("data", (data) => {
|
||||
let json = JSON.parse(data.toString());
|
||||
let event = json.event;
|
||||
|
||||
if (event.type.includes("Workspace")) {
|
||||
let monitors = json.state.monitors;
|
||||
let current_monitor = monitors.elements[monitors.focused];
|
||||
let workspaces = monitors.elements[monitors.focused].workspaces;
|
||||
let current_workspace = workspaces.elements[workspaces.focused];
|
||||
|
||||
console.log(
|
||||
workspaces.elements
|
||||
.map((workspace) => workspace.name)
|
||||
.filter((name) => name !== null)
|
||||
);
|
||||
console.log(current_workspace.name);
|
||||
}
|
||||
});
|
||||
|
||||
stream.on("end", () => {
|
||||
console.log("Client disconnected");
|
||||
});
|
||||
});
|
||||
|
||||
server.listen(pipeName, () => {
|
||||
console.log("Named pipe server listening");
|
||||
});
|
||||
|
||||
const command = "komorebic subscribe-pipe komorebi-js";
|
||||
|
||||
exec(command, (error, stdout, stderr) => {
|
||||
if (error) {
|
||||
console.error(`Error executing command: ${error}`);
|
||||
return;
|
||||
}
|
||||
});
|
||||
```
|
||||
|
||||
## Unix Domain Sockets
|
||||
|
||||
It is possible to subscribe to notifications of every `WindowManagerEvent` and `SocketMessage` handled
|
||||
by `komorebi` using [Unix Domain Sockets](https://devblogs.microsoft.com/commandline/af_unix-comes-to-windows/).
|
||||
|
||||
UDS are also the only mode of communication between `komorebi` and `komorebic`.
|
||||
|
||||
First, your application must create a socket in `$ENV:LocalAppData\komorebi`. Once the socket has been created, run the
|
||||
following command:
|
||||
|
||||
```powershell
|
||||
komorebic.exe subscribe-socket <your socket name>
|
||||
```
|
||||
|
||||
If the socket exists, komorebi will start pushing JSON data of successfully handled events and messages as in the
|
||||
example above in the Named Pipes section.
|
||||
|
||||
## Rust Client
|
||||
|
||||
As of `v0.1.22` it is possible to use the `komorebi-client` crate to subscribe to notifications of
|
||||
every `WindowManagerEvent` and `SocketMessage` handled by `komorebi` in a Rust codebase.
|
||||
|
||||
Below is a simple example of how to use `komorebi-client` in a basic Rust application.
|
||||
|
||||
```rust
|
||||
// komorebi-client = { git = "https://github.com/LGUG2Z/komorebi", tag = "v0.1.22"}
|
||||
|
||||
use anyhow::Result;
|
||||
use komorebi_client::Notification;
|
||||
use komorebi_client::NotificationEvent;
|
||||
use komorebi_client::UnixListener;
|
||||
use komorebi_client::WindowManagerEvent;
|
||||
use std::io::BufRead;
|
||||
use std::io::BufReader;
|
||||
use std::io::Read;
|
||||
|
||||
pub fn main() -> anyhow::Result<()> {
|
||||
let socket = komorebi_client::subscribe(NAME)?;
|
||||
|
||||
for incoming in socket.incoming() {
|
||||
match incoming {
|
||||
Ok(data) => {
|
||||
let reader = BufReader::new(data.try_clone()?);
|
||||
|
||||
for line in reader.lines().flatten() {
|
||||
let notification: Notification = match serde_json::from_str(&line) {
|
||||
Ok(notification) => notification,
|
||||
Err(error) => {
|
||||
log::debug!("discarding malformed komorebi notification: {error}");
|
||||
continue;
|
||||
}
|
||||
};
|
||||
|
||||
// match and filter on desired notifications
|
||||
}
|
||||
}
|
||||
Err(error) => {
|
||||
log::debug!("{error}");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
```
|
||||
|
||||
A read-world example can be found
|
||||
in [komokana](https://github.com/LGUG2Z/komokana/blob/feature/komorebi-uds/src/main.rs).
|
||||
|
||||
## Subscription Event Notification Schema
|
||||
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
# alt-focus-hack
|
||||
|
||||
```
|
||||
Enable or disable a hack simulating ALT key presses to ensure focus changes succeed
|
||||
DEPRECATED since v0.1.22
|
||||
|
||||
Usage: komorebic.exe alt-focus-hack <BOOLEAN_STATE>
|
||||
|
||||
|
||||
@@ -7,7 +7,7 @@ Usage: komorebic.exe change-layout <DEFAULT_LAYOUT>
|
||||
|
||||
Arguments:
|
||||
<DEFAULT_LAYOUT>
|
||||
[possible values: bsp, columns, rows, vertical-stack, horizontal-stack, ultrawide-vertical-stack]
|
||||
[possible values: bsp, columns, rows, vertical-stack, horizontal-stack, ultrawide-vertical-stack, grid]
|
||||
|
||||
Options:
|
||||
-h, --help
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
# check
|
||||
|
||||
```
|
||||
Output various important komorebi-related environment values
|
||||
Check komorebi configuration and related files for common errors
|
||||
|
||||
Usage: komorebic.exe check
|
||||
|
||||
|
||||
12
docs/cli/configuration.md
Normal file
12
docs/cli/configuration.md
Normal file
@@ -0,0 +1,12 @@
|
||||
# configuration
|
||||
|
||||
```
|
||||
Show the path to komorebi.json
|
||||
|
||||
Usage: komorebic.exe configuration
|
||||
|
||||
Options:
|
||||
-h, --help
|
||||
Print help
|
||||
|
||||
```
|
||||
@@ -1,10 +0,0 @@
|
||||
# docgen
|
||||
|
||||
```
|
||||
Usage: komorebic.exe docgen
|
||||
|
||||
Options:
|
||||
-h, --help
|
||||
Print help
|
||||
|
||||
```
|
||||
@@ -13,7 +13,7 @@ Arguments:
|
||||
The number of window containers on-screen required to trigger this layout rule
|
||||
|
||||
<LAYOUT>
|
||||
[possible values: bsp, columns, rows, vertical-stack, horizontal-stack, ultrawide-vertical-stack]
|
||||
[possible values: bsp, columns, rows, vertical-stack, horizontal-stack, ultrawide-vertical-stack, grid]
|
||||
|
||||
Options:
|
||||
-h, --help
|
||||
|
||||
@@ -10,7 +10,7 @@ Arguments:
|
||||
Target workspace name
|
||||
|
||||
<VALUE>
|
||||
[possible values: bsp, columns, rows, vertical-stack, horizontal-stack, ultrawide-vertical-stack]
|
||||
[possible values: bsp, columns, rows, vertical-stack, horizontal-stack, ultrawide-vertical-stack, grid]
|
||||
|
||||
Options:
|
||||
-h, --help
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
# subscribe
|
||||
# subscribe-pipe
|
||||
|
||||
```
|
||||
Subscribe to komorebi events
|
||||
Subscribe to komorebi events using a Named Pipe
|
||||
|
||||
Usage: komorebic.exe subscribe <NAMED_PIPE>
|
||||
Usage: komorebic.exe subscribe-pipe <NAMED_PIPE>
|
||||
|
||||
Arguments:
|
||||
<NAMED_PIPE>
|
||||
16
docs/cli/subscribe-socket.md
Normal file
16
docs/cli/subscribe-socket.md
Normal file
@@ -0,0 +1,16 @@
|
||||
# subscribe-socket
|
||||
|
||||
```
|
||||
Subscribe to komorebi events using a Unix Domain Socket
|
||||
|
||||
Usage: komorebic.exe subscribe-socket <SOCKET>
|
||||
|
||||
Arguments:
|
||||
<SOCKET>
|
||||
Name of the socket to send event notifications to
|
||||
|
||||
Options:
|
||||
-h, --help
|
||||
Print help
|
||||
|
||||
```
|
||||
@@ -1,9 +1,9 @@
|
||||
# unsubscribe
|
||||
# unsubscribe-pipe
|
||||
|
||||
```
|
||||
Unsubscribe from komorebi events
|
||||
|
||||
Usage: komorebic.exe unsubscribe <NAMED_PIPE>
|
||||
Usage: komorebic.exe unsubscribe-pipe <NAMED_PIPE>
|
||||
|
||||
Arguments:
|
||||
<NAMED_PIPE>
|
||||
16
docs/cli/unsubscribe-socket.md
Normal file
16
docs/cli/unsubscribe-socket.md
Normal file
@@ -0,0 +1,16 @@
|
||||
# unsubscribe-socket
|
||||
|
||||
```
|
||||
Unsubscribe from komorebi events
|
||||
|
||||
Usage: komorebic.exe unsubscribe-socket <SOCKET>
|
||||
|
||||
Arguments:
|
||||
<SOCKET>
|
||||
Name of the socket to stop sending event notifications to
|
||||
|
||||
Options:
|
||||
-h, --help
|
||||
Print help
|
||||
|
||||
```
|
||||
12
docs/cli/whkdrc.md
Normal file
12
docs/cli/whkdrc.md
Normal file
@@ -0,0 +1,12 @@
|
||||
# whkdrc
|
||||
|
||||
```
|
||||
Show the path to whkdrc
|
||||
|
||||
Usage: komorebic.exe whkdrc
|
||||
|
||||
Options:
|
||||
-h, --help
|
||||
Print help
|
||||
|
||||
```
|
||||
@@ -16,7 +16,7 @@ Arguments:
|
||||
The number of window containers on-screen required to trigger this layout rule
|
||||
|
||||
<LAYOUT>
|
||||
[possible values: bsp, columns, rows, vertical-stack, horizontal-stack, ultrawide-vertical-stack]
|
||||
[possible values: bsp, columns, rows, vertical-stack, horizontal-stack, ultrawide-vertical-stack, grid]
|
||||
|
||||
Options:
|
||||
-h, --help
|
||||
|
||||
@@ -13,7 +13,7 @@ Arguments:
|
||||
Workspace index on the specified monitor (zero-indexed)
|
||||
|
||||
<VALUE>
|
||||
[possible values: bsp, columns, rows, vertical-stack, horizontal-stack, ultrawide-vertical-stack]
|
||||
[possible values: bsp, columns, rows, vertical-stack, horizontal-stack, ultrawide-vertical-stack, grid]
|
||||
|
||||
Options:
|
||||
-h, --help
|
||||
|
||||
@@ -34,7 +34,5 @@ windows managed by `komorebi`.
|
||||
This feature is not considered stable and you may encounter visual artifacts
|
||||
from time to time.
|
||||
|
||||
<!-- TODO: Record a new video -->
|
||||
|
||||
[](https://www.youtube.com/watch?v=ywiAvoMV_gE)
|
||||
video](https://img.youtube.com/vi/7_9D22t7KK4/hqdefault.jpg)](https://www.youtube.com/watch?v=7_9D22t7KK4)
|
||||
|
||||
@@ -25,3 +25,6 @@ If you already have configuration files that you wish to keep, move them to the
|
||||
|
||||
The next time you run `komorebic start`, any files created by or loaded by
|
||||
_komorebi_ will be placed or expected to exist in this folder.
|
||||
|
||||
[](https://www.youtube.com/watch?v=C_KWUqQ6kko)
|
||||
|
||||
@@ -2,16 +2,18 @@
|
||||
|
||||
If you would like to remove all gaps by default, both between windows
|
||||
themselves, and between the monitor edges and the windows, you can set the
|
||||
following two configuration options to `0` in the `komorebi.json` configuration
|
||||
file.
|
||||
following configuration options to `0` and `-1` in the `komorebi.json`
|
||||
configuration file.
|
||||
|
||||
```json
|
||||
{
|
||||
"default_workspace_padding": 0,
|
||||
"default_container_padding": 0
|
||||
"default_container_padding": 0,
|
||||
"border_width": 0,
|
||||
"border_offset": -1
|
||||
}
|
||||
```
|
||||
|
||||
<!-- TODO: Record a new video -->
|
||||
A restart of `komorebi` is required after changing these settings.
|
||||
|
||||
[](https://www.youtube.com/watch?v=eGr07mymgWE)
|
||||
[](https://www.youtube.com/watch?v=6QYLao953XE)
|
||||
|
||||
@@ -16,6 +16,12 @@ the example files have been downloaded. For most new users this will be in the
|
||||
komorebic quickstart
|
||||
```
|
||||
|
||||
With the example configurations downloaded, you can now start `komorebi` and `whkd.
|
||||
|
||||
```powershell
|
||||
komorebic start --whkd
|
||||
```
|
||||
|
||||
## komorebi.json
|
||||
|
||||
The example window manager configuration sets some sane defaults and provides
|
||||
@@ -139,6 +145,19 @@ If you have an ultrawide monitor, I recommend using this layout.
|
||||
+-----+-----------+-----+
|
||||
```
|
||||
|
||||
### Grid
|
||||
If you like the `grid` layout in [LeftWM](https://github.com/leftwm/leftwm-layouts) this is almost exactly the same!
|
||||
|
||||
```
|
||||
+-----+-----+ +---+---+---+ +---+---+---+ +---+---+---+
|
||||
| | | | | | | | | | | | | | |
|
||||
| | | | | | | | | | | | | +---+
|
||||
+-----+-----+ | +---+---+ +---+---+---+ +---+---| |
|
||||
| | | | | | | | | | | | | +---+
|
||||
| | | | | | | | | | | | | | |
|
||||
+-----+-----+ +---+---+---+ +---+---+---+ +---+---+---+
|
||||
4 windows 5 windows 6 windows 7 windows
|
||||
```
|
||||
|
||||
## whkdrc
|
||||
|
||||
|
||||
@@ -1,15 +1,17 @@
|
||||
{
|
||||
"$schema": "https://raw.githubusercontent.com/LGUG2Z/komorebi/v0.1.20/schema.json",
|
||||
"$schema": "https://raw.githubusercontent.com/LGUG2Z/komorebi/v0.1.22/schema.json",
|
||||
"app_specific_configuration_path": "$Env:USERPROFILE/applications.yaml",
|
||||
"window_hiding_behaviour": "Cloak",
|
||||
"cross_monitor_move_behaviour": "Insert",
|
||||
"default_workspace_padding": 20,
|
||||
"default_container_padding": 20,
|
||||
"border_padding": 8,
|
||||
"border_offset": -1,
|
||||
"active_window_border": false,
|
||||
"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 }
|
||||
"single": "#42a5f5",
|
||||
"stack": "#00a542",
|
||||
"monocle": "#ff3399"
|
||||
},
|
||||
"monitors": [
|
||||
{
|
||||
|
||||
56
docs/release/v0-1-22.md
Normal file
56
docs/release/v0-1-22.md
Normal file
@@ -0,0 +1,56 @@
|
||||
In addition to the [changelog](https://github.com/LGUG2Z/komorebi/releases/tag/v0.1.22) of new features and fixes,
|
||||
please note the following changes from `v0.1.21` to adjust your configuration files accordingly.
|
||||
|
||||
## tl;dr
|
||||
|
||||
The way windows are sized and drawn has been improved to remove the need to manually specify and remove invisible
|
||||
borders for applications that overflow them. If you use the active window border, the first time you launch `v0.1.22`
|
||||
you may end up with a _huge_ border due to these changes.
|
||||
|
||||
`active_window_border_width` and `active_window_border_offset` have been renamed to `border_width` and `border_offset`
|
||||
as they now also apply outside the context of the active window border.
|
||||
|
||||
```json
|
||||
{
|
||||
"active_window_border": true,
|
||||
"border_width": 8,
|
||||
"border_offset": -1
|
||||
}
|
||||
```
|
||||
|
||||
Users of the active window border should start from these settings and read the notes below before making further
|
||||
adjustments.
|
||||
|
||||
## Changes to `active_window_border`, and window sizing:
|
||||
|
||||
- The border no longer creates a second drop-shadow around the active window
|
||||
- Windows are now sized to fill the layout region entirely, ignoring window decorations such as drop shadows
|
||||
- Border offset now starts exactly at the paint edge of the window on all sides
|
||||
- Windows are sized such that the border offset and border width are taken into account
|
||||
|
||||
## Recommended patterns
|
||||
|
||||
### Gapless
|
||||
|
||||
- Disable "transparency effects" Personalization > Colors
|
||||
- Set the following settings in `komorebi.json`:
|
||||
```json
|
||||
{
|
||||
"default_workspace_padding": 0,
|
||||
"default_container_padding": 0,
|
||||
"border_offset": -1,
|
||||
"border_width": 0
|
||||
}
|
||||
```
|
||||
|
||||
### 1px border
|
||||
|
||||
A 1px border is drawn around the window edge. Users may see a gap for a single pixel, if the system theme has a
|
||||
transparent edge - this is the windows themed edge, and is not present for all applications.
|
||||
|
||||
```json
|
||||
{
|
||||
"border_offset": 0,
|
||||
"border_width": 1
|
||||
}
|
||||
```
|
||||
12
komorebi-client/Cargo.toml
Normal file
12
komorebi-client/Cargo.toml
Normal file
@@ -0,0 +1,12 @@
|
||||
[package]
|
||||
name = "komorebi-client"
|
||||
version = "0.1.23-dev.0"
|
||||
edition = "2021"
|
||||
|
||||
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
|
||||
|
||||
[dependencies]
|
||||
komorebi = { path = "../komorebi" }
|
||||
komorebi-core = { path = "../komorebi-core" }
|
||||
uds_windows = "1"
|
||||
serde_json = { workspace = true }
|
||||
79
komorebi-client/src/lib.rs
Normal file
79
komorebi-client/src/lib.rs
Normal file
@@ -0,0 +1,79 @@
|
||||
#![warn(clippy::all, clippy::nursery, clippy::pedantic)]
|
||||
#![allow(clippy::missing_errors_doc)]
|
||||
|
||||
pub use komorebi::container::Container;
|
||||
pub use komorebi::monitor::Monitor;
|
||||
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::Notification;
|
||||
pub use komorebi::NotificationEvent;
|
||||
pub use komorebi::State;
|
||||
pub use komorebi_core::Arrangement;
|
||||
pub use komorebi_core::Axis;
|
||||
pub use komorebi_core::CustomLayout;
|
||||
pub use komorebi_core::CycleDirection;
|
||||
pub use komorebi_core::DefaultLayout;
|
||||
pub use komorebi_core::Direction;
|
||||
pub use komorebi_core::Layout;
|
||||
pub use komorebi_core::OperationDirection;
|
||||
pub use komorebi_core::Rect;
|
||||
pub use komorebi_core::SocketMessage;
|
||||
|
||||
use komorebi::DATA_DIR;
|
||||
|
||||
use std::io::BufReader;
|
||||
use std::io::Read;
|
||||
use std::io::Write;
|
||||
use std::net::Shutdown;
|
||||
pub use uds_windows::UnixListener;
|
||||
use uds_windows::UnixStream;
|
||||
|
||||
const KOMOREBI: &str = "komorebi.sock";
|
||||
|
||||
pub fn send_message(message: &SocketMessage) -> std::io::Result<()> {
|
||||
let socket = DATA_DIR.join(KOMOREBI);
|
||||
let mut connected = false;
|
||||
while !connected {
|
||||
if let Ok(mut stream) = UnixStream::connect(&socket) {
|
||||
connected = true;
|
||||
stream.write_all(serde_json::to_string(message)?.as_bytes())?;
|
||||
}
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
pub fn send_query(message: &SocketMessage) -> std::io::Result<String> {
|
||||
let socket = DATA_DIR.join(KOMOREBI);
|
||||
|
||||
let mut stream = UnixStream::connect(socket)?;
|
||||
stream.write_all(serde_json::to_string(message)?.as_bytes())?;
|
||||
stream.shutdown(Shutdown::Write)?;
|
||||
|
||||
let mut reader = BufReader::new(stream);
|
||||
let mut response = String::new();
|
||||
reader.read_to_string(&mut response)?;
|
||||
|
||||
Ok(response)
|
||||
}
|
||||
|
||||
pub fn subscribe(name: &str) -> std::io::Result<UnixListener> {
|
||||
let socket = DATA_DIR.join(name);
|
||||
|
||||
match std::fs::remove_file(&socket) {
|
||||
Ok(()) => {}
|
||||
Err(error) => match error.kind() {
|
||||
std::io::ErrorKind::NotFound => {}
|
||||
_ => {
|
||||
return Err(error);
|
||||
}
|
||||
},
|
||||
};
|
||||
|
||||
let listener = UnixListener::bind(&socket)?;
|
||||
|
||||
send_message(&SocketMessage::AddSubscriberSocket(name.to_string()))?;
|
||||
|
||||
Ok(listener)
|
||||
}
|
||||
@@ -1,6 +1,6 @@
|
||||
[package]
|
||||
name = "komorebi-core"
|
||||
version = "0.1.20"
|
||||
version = "0.1.23-dev.0"
|
||||
edition = "2021"
|
||||
|
||||
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
|
||||
@@ -8,7 +8,7 @@ edition = "2021"
|
||||
[dependencies]
|
||||
clap = { version = "4", features = ["derive"] }
|
||||
serde = { version = "1", features = ["derive"] }
|
||||
serde_json = "1"
|
||||
serde_json = { workspace = true }
|
||||
serde_yaml = "0.9"
|
||||
strum = { version = "0.26", features = ["derive"] }
|
||||
schemars = "0.8"
|
||||
|
||||
@@ -131,11 +131,65 @@ impl Arrangement for DefaultLayout {
|
||||
layouts
|
||||
}
|
||||
Self::UltrawideVerticalStack => ultrawide(area, len, layout_flip, resize_dimensions),
|
||||
#[allow(
|
||||
clippy::cast_precision_loss,
|
||||
clippy::cast_possible_truncation,
|
||||
clippy::cast_possible_wrap
|
||||
)]
|
||||
Self::Grid => {
|
||||
// Shamelessly lifted from LeftWM
|
||||
// https://github.com/leftwm/leftwm/blob/18675067b8450e520ef75db2ebbb0d973aa1199e/leftwm-core/src/layouts/grid_horizontal.rs
|
||||
let mut layouts: Vec<Rect> = vec![];
|
||||
layouts.resize(len, Rect::default());
|
||||
|
||||
let len = len as i32;
|
||||
|
||||
let num_cols = (len as f32).sqrt().ceil() as i32;
|
||||
let mut iter = layouts.iter_mut().enumerate().peekable();
|
||||
|
||||
for col in 0..num_cols {
|
||||
let iter_peek = iter.peek().map(|x| x.0).unwrap_or_default() as i32;
|
||||
let remaining_windows = len - iter_peek;
|
||||
let remaining_columns = num_cols - col;
|
||||
let num_rows_in_this_col = remaining_windows / remaining_columns;
|
||||
|
||||
let win_height = area.bottom / num_rows_in_this_col;
|
||||
let win_width = area.right / num_cols;
|
||||
|
||||
for row in 0..num_rows_in_this_col {
|
||||
if let Some((_idx, win)) = iter.next() {
|
||||
let mut left = area.left + win_width * col;
|
||||
let mut top = area.top + win_height * row;
|
||||
|
||||
match layout_flip {
|
||||
Some(Axis::Horizontal) => {
|
||||
left = area.right - win_width * (col + 1) + area.left;
|
||||
}
|
||||
Some(Axis::Vertical) => {
|
||||
top = area.bottom - win_height * (row + 1) + area.top;
|
||||
}
|
||||
Some(Axis::HorizontalAndVertical) => {
|
||||
left = area.right - win_width * (col + 1) + area.left;
|
||||
top = area.bottom - win_height * (row + 1) + area.top;
|
||||
}
|
||||
None => {} // No flip
|
||||
}
|
||||
|
||||
win.bottom = win_height;
|
||||
win.right = win_width;
|
||||
win.left = left;
|
||||
win.top = top;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
layouts
|
||||
}
|
||||
};
|
||||
|
||||
dimensions
|
||||
.iter_mut()
|
||||
.for_each(|l| l.add_padding(container_padding));
|
||||
.for_each(|l| l.add_padding(container_padding.unwrap_or_default()));
|
||||
|
||||
dimensions
|
||||
}
|
||||
@@ -258,7 +312,7 @@ impl Arrangement for CustomLayout {
|
||||
|
||||
dimensions
|
||||
.iter_mut()
|
||||
.for_each(|l| l.add_padding(container_padding));
|
||||
.for_each(|l| l.add_padding(container_padding.unwrap_or_default()));
|
||||
|
||||
dimensions
|
||||
}
|
||||
|
||||
@@ -8,7 +8,9 @@ use strum::EnumString;
|
||||
|
||||
use crate::ApplicationIdentifier;
|
||||
|
||||
#[derive(Clone, Debug, Serialize, Deserialize, Display, EnumString, ValueEnum, JsonSchema)]
|
||||
#[derive(
|
||||
Clone, Copy, Debug, Serialize, Deserialize, Display, EnumString, ValueEnum, JsonSchema,
|
||||
)]
|
||||
#[strum(serialize_all = "snake_case")]
|
||||
#[serde(rename_all = "snake_case")]
|
||||
pub enum ApplicationOptions {
|
||||
@@ -50,6 +52,13 @@ impl ApplicationOptions {
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Eq, PartialEq, Serialize, Deserialize, JsonSchema)]
|
||||
#[serde(untagged)]
|
||||
pub enum MatchingRule {
|
||||
Simple(IdWithIdentifier),
|
||||
Composite(Vec<IdWithIdentifier>),
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Eq, PartialEq, Serialize, Deserialize, JsonSchema)]
|
||||
pub struct IdWithIdentifier {
|
||||
pub kind: ApplicationIdentifier,
|
||||
@@ -66,6 +75,10 @@ pub enum MatchingStrategy {
|
||||
EndsWith,
|
||||
Contains,
|
||||
Regex,
|
||||
DoesNotEndWith,
|
||||
DoesNotStartWith,
|
||||
DoesNotEqual,
|
||||
DoesNotContain,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Serialize, Deserialize, JsonSchema)]
|
||||
@@ -95,14 +108,14 @@ pub struct ApplicationConfiguration {
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub options: Option<Vec<ApplicationOptions>>,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub float_identifiers: Option<Vec<IdWithIdentifierAndComment>>,
|
||||
pub float_identifiers: Option<Vec<MatchingRule>>,
|
||||
}
|
||||
|
||||
impl ApplicationConfiguration {
|
||||
pub fn populate_default_matching_strategies(&mut self) {
|
||||
if self.identifier.matching_strategy.is_none() {
|
||||
match self.identifier.kind {
|
||||
ApplicationIdentifier::Exe => {
|
||||
ApplicationIdentifier::Exe | ApplicationIdentifier::Path => {
|
||||
self.identifier.matching_strategy = Option::from(MatchingStrategy::Equals);
|
||||
}
|
||||
ApplicationIdentifier::Class | ApplicationIdentifier::Title => {}
|
||||
@@ -181,19 +194,21 @@ impl ApplicationConfigurationGenerator {
|
||||
}
|
||||
|
||||
if let Some(float_identifiers) = app.float_identifiers {
|
||||
for float in float_identifiers {
|
||||
let float_rule =
|
||||
format!("komorebic.exe float-rule {} \"{}\"", float.kind, float.id);
|
||||
for matching_rule in float_identifiers {
|
||||
if let MatchingRule::Simple(float) = matching_rule {
|
||||
let float_rule =
|
||||
format!("komorebic.exe float-rule {} \"{}\"", float.kind, float.id);
|
||||
|
||||
// Don't want to send duped signals especially as configs get larger
|
||||
if !float_rules.contains(&float_rule) {
|
||||
float_rules.push(float_rule.clone());
|
||||
// Don't want to send duped signals especially as configs get larger
|
||||
if !float_rules.contains(&float_rule) {
|
||||
float_rules.push(float_rule.clone());
|
||||
|
||||
if let Some(comment) = float.comment {
|
||||
lines.push(format!("# {comment}"));
|
||||
};
|
||||
// if let Some(comment) = float.comment {
|
||||
// lines.push(format!("# {comment}"));
|
||||
// };
|
||||
|
||||
lines.push(float_rule);
|
||||
lines.push(float_rule);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -230,21 +245,23 @@ impl ApplicationConfigurationGenerator {
|
||||
}
|
||||
|
||||
if let Some(float_identifiers) = app.float_identifiers {
|
||||
for float in float_identifiers {
|
||||
let float_rule = format!(
|
||||
"RunWait('komorebic.exe float-rule {} \"{}\"', , \"Hide\")",
|
||||
float.kind, float.id
|
||||
);
|
||||
for matching_rule in float_identifiers {
|
||||
if let MatchingRule::Simple(float) = matching_rule {
|
||||
let float_rule = format!(
|
||||
"RunWait('komorebic.exe float-rule {} \"{}\"', , \"Hide\")",
|
||||
float.kind, float.id
|
||||
);
|
||||
|
||||
// Don't want to send duped signals especially as configs get larger
|
||||
if !float_rules.contains(&float_rule) {
|
||||
float_rules.push(float_rule.clone());
|
||||
// Don't want to send duped signals especially as configs get larger
|
||||
if !float_rules.contains(&float_rule) {
|
||||
float_rules.push(float_rule.clone());
|
||||
|
||||
if let Some(comment) = float.comment {
|
||||
lines.push(format!("; {comment}"));
|
||||
};
|
||||
// if let Some(comment) = float.comment {
|
||||
// lines.push(format!("; {comment}"));
|
||||
// };
|
||||
|
||||
lines.push(float_rule);
|
||||
lines.push(float_rule);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -20,6 +20,7 @@ pub enum DefaultLayout {
|
||||
VerticalStack,
|
||||
HorizontalStack,
|
||||
UltrawideVerticalStack,
|
||||
Grid,
|
||||
// NOTE: If any new layout is added, please make sure to register the same in `DefaultLayout::cycle`
|
||||
}
|
||||
|
||||
@@ -135,7 +136,8 @@ impl DefaultLayout {
|
||||
Self::Rows => Self::VerticalStack,
|
||||
Self::VerticalStack => Self::HorizontalStack,
|
||||
Self::HorizontalStack => Self::UltrawideVerticalStack,
|
||||
Self::UltrawideVerticalStack => Self::BSP,
|
||||
Self::UltrawideVerticalStack => Self::Grid,
|
||||
Self::Grid => Self::BSP,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -147,7 +149,8 @@ impl DefaultLayout {
|
||||
Self::HorizontalStack => Self::VerticalStack,
|
||||
Self::VerticalStack => Self::Rows,
|
||||
Self::Rows => Self::Columns,
|
||||
Self::Columns => Self::BSP,
|
||||
Self::Columns => Self::Grid,
|
||||
Self::Grid => Self::BSP,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -19,10 +19,30 @@ pub trait Direction {
|
||||
idx: usize,
|
||||
count: usize,
|
||||
) -> bool;
|
||||
fn up_index(&self, idx: usize) -> usize;
|
||||
fn down_index(&self, idx: usize) -> usize;
|
||||
fn left_index(&self, idx: usize) -> usize;
|
||||
fn right_index(&self, idx: usize) -> usize;
|
||||
fn up_index(
|
||||
&self,
|
||||
op_direction: Option<OperationDirection>,
|
||||
idx: usize,
|
||||
count: Option<usize>,
|
||||
) -> usize;
|
||||
fn down_index(
|
||||
&self,
|
||||
op_direction: Option<OperationDirection>,
|
||||
idx: usize,
|
||||
count: Option<usize>,
|
||||
) -> usize;
|
||||
fn left_index(
|
||||
&self,
|
||||
op_direction: Option<OperationDirection>,
|
||||
idx: usize,
|
||||
count: Option<usize>,
|
||||
) -> usize;
|
||||
fn right_index(
|
||||
&self,
|
||||
op_direction: Option<OperationDirection>,
|
||||
idx: usize,
|
||||
count: Option<usize>,
|
||||
) -> usize;
|
||||
}
|
||||
|
||||
impl Direction for DefaultLayout {
|
||||
@@ -35,28 +55,28 @@ impl Direction for DefaultLayout {
|
||||
match op_direction {
|
||||
OperationDirection::Left => {
|
||||
if self.is_valid_direction(op_direction, idx, count) {
|
||||
Option::from(self.left_index(idx))
|
||||
Option::from(self.left_index(Some(op_direction), idx, Some(count)))
|
||||
} else {
|
||||
None
|
||||
}
|
||||
}
|
||||
OperationDirection::Right => {
|
||||
if self.is_valid_direction(op_direction, idx, count) {
|
||||
Option::from(self.right_index(idx))
|
||||
Option::from(self.right_index(Some(op_direction), idx, Some(count)))
|
||||
} else {
|
||||
None
|
||||
}
|
||||
}
|
||||
OperationDirection::Up => {
|
||||
if self.is_valid_direction(op_direction, idx, count) {
|
||||
Option::from(self.up_index(idx))
|
||||
Option::from(self.up_index(Some(op_direction), idx, Some(count)))
|
||||
} else {
|
||||
None
|
||||
}
|
||||
}
|
||||
OperationDirection::Down => {
|
||||
if self.is_valid_direction(op_direction, idx, count) {
|
||||
Option::from(self.down_index(idx))
|
||||
Option::from(self.down_index(Some(op_direction), idx, Some(count)))
|
||||
} else {
|
||||
None
|
||||
}
|
||||
@@ -77,6 +97,7 @@ impl Direction for DefaultLayout {
|
||||
Self::Rows | Self::HorizontalStack => idx != 0,
|
||||
Self::VerticalStack => idx != 0 && idx != 1,
|
||||
Self::UltrawideVerticalStack => idx > 2,
|
||||
Self::Grid => !is_grid_edge(op_direction, idx, count),
|
||||
},
|
||||
OperationDirection::Down => match self {
|
||||
Self::BSP => count > 2 && idx != count - 1 && idx % 2 != 0,
|
||||
@@ -85,6 +106,7 @@ impl Direction for DefaultLayout {
|
||||
Self::VerticalStack => idx != 0 && idx != count - 1,
|
||||
Self::HorizontalStack => idx == 0,
|
||||
Self::UltrawideVerticalStack => idx > 1 && idx != count - 1,
|
||||
Self::Grid => !is_grid_edge(op_direction, idx, count),
|
||||
},
|
||||
OperationDirection::Left => match self {
|
||||
Self::BSP => count > 1 && idx != 0,
|
||||
@@ -92,6 +114,7 @@ impl Direction for DefaultLayout {
|
||||
Self::Rows => false,
|
||||
Self::HorizontalStack => idx != 0 && idx != 1,
|
||||
Self::UltrawideVerticalStack => count > 1 && idx != 1,
|
||||
Self::Grid => !is_grid_edge(op_direction, idx, count),
|
||||
},
|
||||
OperationDirection::Right => match self {
|
||||
Self::BSP => count > 1 && idx % 2 == 0 && idx != count - 1,
|
||||
@@ -104,11 +127,17 @@ impl Direction for DefaultLayout {
|
||||
2 => idx != 0,
|
||||
_ => idx < 2,
|
||||
},
|
||||
Self::Grid => !is_grid_edge(op_direction, idx, count),
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
fn up_index(&self, idx: usize) -> usize {
|
||||
fn up_index(
|
||||
&self,
|
||||
op_direction: Option<OperationDirection>,
|
||||
idx: usize,
|
||||
count: Option<usize>,
|
||||
) -> usize {
|
||||
match self {
|
||||
Self::BSP => {
|
||||
if idx % 2 == 0 {
|
||||
@@ -120,18 +149,30 @@ impl Direction for DefaultLayout {
|
||||
Self::Columns => unreachable!(),
|
||||
Self::Rows | Self::VerticalStack | Self::UltrawideVerticalStack => idx - 1,
|
||||
Self::HorizontalStack => 0,
|
||||
Self::Grid => grid_neighbor(op_direction, idx, count),
|
||||
}
|
||||
}
|
||||
|
||||
fn down_index(&self, idx: usize) -> usize {
|
||||
fn down_index(
|
||||
&self,
|
||||
op_direction: Option<OperationDirection>,
|
||||
idx: usize,
|
||||
count: Option<usize>,
|
||||
) -> usize {
|
||||
match self {
|
||||
Self::BSP | Self::Rows | Self::VerticalStack | Self::UltrawideVerticalStack => idx + 1,
|
||||
Self::Columns => unreachable!(),
|
||||
Self::HorizontalStack => 1,
|
||||
Self::Grid => grid_neighbor(op_direction, idx, count),
|
||||
}
|
||||
}
|
||||
|
||||
fn left_index(&self, idx: usize) -> usize {
|
||||
fn left_index(
|
||||
&self,
|
||||
op_direction: Option<OperationDirection>,
|
||||
idx: usize,
|
||||
count: Option<usize>,
|
||||
) -> usize {
|
||||
match self {
|
||||
Self::BSP => {
|
||||
if idx % 2 == 0 {
|
||||
@@ -148,10 +189,16 @@ impl Direction for DefaultLayout {
|
||||
1 => unreachable!(),
|
||||
_ => 0,
|
||||
},
|
||||
Self::Grid => grid_neighbor(op_direction, idx, count),
|
||||
}
|
||||
}
|
||||
|
||||
fn right_index(&self, idx: usize) -> usize {
|
||||
fn right_index(
|
||||
&self,
|
||||
op_direction: Option<OperationDirection>,
|
||||
idx: usize,
|
||||
count: Option<usize>,
|
||||
) -> usize {
|
||||
match self {
|
||||
Self::BSP | Self::Columns | Self::HorizontalStack => idx + 1,
|
||||
Self::Rows => unreachable!(),
|
||||
@@ -161,10 +208,126 @@ impl Direction for DefaultLayout {
|
||||
0 => 2,
|
||||
_ => unreachable!(),
|
||||
},
|
||||
Self::Grid => grid_neighbor(op_direction, idx, count),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
struct GridItem {
|
||||
state: GridItemState,
|
||||
row: usize,
|
||||
num_rows: usize,
|
||||
touching_edges: GridTouchingEdges,
|
||||
}
|
||||
|
||||
enum GridItemState {
|
||||
Valid,
|
||||
Invalid,
|
||||
}
|
||||
|
||||
#[allow(clippy::struct_excessive_bools)]
|
||||
struct GridTouchingEdges {
|
||||
left: bool,
|
||||
right: bool,
|
||||
up: bool,
|
||||
down: bool,
|
||||
}
|
||||
|
||||
#[allow(
|
||||
clippy::cast_possible_truncation,
|
||||
clippy::cast_precision_loss,
|
||||
clippy::cast_sign_loss
|
||||
)]
|
||||
fn get_grid_item(idx: usize, count: usize) -> GridItem {
|
||||
let num_cols = (count as f32).sqrt().ceil() as usize;
|
||||
let mut iter = 0;
|
||||
|
||||
for col in 0..num_cols {
|
||||
let remaining_windows = count - iter;
|
||||
let remaining_columns = num_cols - col;
|
||||
let num_rows_in_this_col = remaining_windows / remaining_columns;
|
||||
|
||||
for row in 0..num_rows_in_this_col {
|
||||
if iter == idx {
|
||||
return GridItem {
|
||||
state: GridItemState::Valid,
|
||||
row: row + 1,
|
||||
num_rows: num_rows_in_this_col,
|
||||
touching_edges: GridTouchingEdges {
|
||||
left: col == 0,
|
||||
right: col == num_cols - 1,
|
||||
up: row == 0,
|
||||
down: row == num_rows_in_this_col - 1,
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
iter += 1;
|
||||
}
|
||||
}
|
||||
|
||||
GridItem {
|
||||
state: GridItemState::Invalid,
|
||||
row: 0,
|
||||
num_rows: 0,
|
||||
touching_edges: GridTouchingEdges {
|
||||
left: true,
|
||||
right: true,
|
||||
up: true,
|
||||
down: true,
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
fn is_grid_edge(op_direction: OperationDirection, idx: usize, count: usize) -> bool {
|
||||
let item = get_grid_item(idx, count);
|
||||
|
||||
match item.state {
|
||||
GridItemState::Invalid => false,
|
||||
GridItemState::Valid => match op_direction {
|
||||
OperationDirection::Left => item.touching_edges.left,
|
||||
OperationDirection::Right => item.touching_edges.right,
|
||||
OperationDirection::Up => item.touching_edges.up,
|
||||
OperationDirection::Down => item.touching_edges.down,
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
fn grid_neighbor(
|
||||
op_direction: Option<OperationDirection>,
|
||||
idx: usize,
|
||||
count: Option<usize>,
|
||||
) -> usize {
|
||||
let Some(op_direction) = op_direction else {
|
||||
return 0;
|
||||
};
|
||||
|
||||
let Some(count) = count else {
|
||||
return 0;
|
||||
};
|
||||
|
||||
let item = get_grid_item(idx, count);
|
||||
|
||||
match op_direction {
|
||||
OperationDirection::Left => {
|
||||
let item_from_prev_col = get_grid_item(idx - item.row, count);
|
||||
|
||||
if item.touching_edges.up && item.num_rows != item_from_prev_col.num_rows {
|
||||
return idx - (item.num_rows - 1);
|
||||
}
|
||||
|
||||
if item.num_rows != item_from_prev_col.num_rows && !item.touching_edges.down {
|
||||
return idx - (item.num_rows - 1);
|
||||
}
|
||||
|
||||
idx - item.num_rows
|
||||
}
|
||||
OperationDirection::Right => idx + item.num_rows,
|
||||
OperationDirection::Up => idx - 1,
|
||||
OperationDirection::Down => idx + 1,
|
||||
}
|
||||
}
|
||||
|
||||
impl Direction for CustomLayout {
|
||||
fn index_in_direction(
|
||||
&self,
|
||||
@@ -179,28 +342,28 @@ impl Direction for CustomLayout {
|
||||
match op_direction {
|
||||
OperationDirection::Left => {
|
||||
if self.is_valid_direction(op_direction, idx, count) {
|
||||
Option::from(self.left_index(idx))
|
||||
Option::from(self.left_index(None, idx, None))
|
||||
} else {
|
||||
None
|
||||
}
|
||||
}
|
||||
OperationDirection::Right => {
|
||||
if self.is_valid_direction(op_direction, idx, count) {
|
||||
Option::from(self.right_index(idx))
|
||||
Option::from(self.right_index(None, idx, None))
|
||||
} else {
|
||||
None
|
||||
}
|
||||
}
|
||||
OperationDirection::Up => {
|
||||
if self.is_valid_direction(op_direction, idx, count) {
|
||||
Option::from(self.up_index(idx))
|
||||
Option::from(self.up_index(None, idx, None))
|
||||
} else {
|
||||
None
|
||||
}
|
||||
}
|
||||
OperationDirection::Down => {
|
||||
if self.is_valid_direction(op_direction, idx, count) {
|
||||
Option::from(self.down_index(idx))
|
||||
Option::from(self.down_index(None, idx, None))
|
||||
} else {
|
||||
None
|
||||
}
|
||||
@@ -254,15 +417,30 @@ impl Direction for CustomLayout {
|
||||
}
|
||||
}
|
||||
|
||||
fn up_index(&self, idx: usize) -> usize {
|
||||
fn up_index(
|
||||
&self,
|
||||
_op_direction: Option<OperationDirection>,
|
||||
idx: usize,
|
||||
_count: Option<usize>,
|
||||
) -> usize {
|
||||
idx - 1
|
||||
}
|
||||
|
||||
fn down_index(&self, idx: usize) -> usize {
|
||||
fn down_index(
|
||||
&self,
|
||||
_op_direction: Option<OperationDirection>,
|
||||
idx: usize,
|
||||
_count: Option<usize>,
|
||||
) -> usize {
|
||||
idx + 1
|
||||
}
|
||||
|
||||
fn left_index(&self, idx: usize) -> usize {
|
||||
fn left_index(
|
||||
&self,
|
||||
_op_direction: Option<OperationDirection>,
|
||||
idx: usize,
|
||||
_count: Option<usize>,
|
||||
) -> usize {
|
||||
let column_idx = self.column_for_container_idx(idx);
|
||||
if column_idx - 1 == 0 {
|
||||
0
|
||||
@@ -271,7 +449,12 @@ impl Direction for CustomLayout {
|
||||
}
|
||||
}
|
||||
|
||||
fn right_index(&self, idx: usize) -> usize {
|
||||
fn right_index(
|
||||
&self,
|
||||
_op_direction: Option<OperationDirection>,
|
||||
idx: usize,
|
||||
_count: Option<usize>,
|
||||
) -> usize {
|
||||
let column_idx = self.column_for_container_idx(idx);
|
||||
self.first_container_idx(column_idx + 1)
|
||||
}
|
||||
|
||||
@@ -156,8 +156,10 @@ pub enum SocketMessage {
|
||||
ToggleMouseFollowsFocus,
|
||||
RemoveTitleBar(ApplicationIdentifier, String),
|
||||
ToggleTitleBars,
|
||||
AddSubscriber(String),
|
||||
RemoveSubscriber(String),
|
||||
AddSubscriberSocket(String),
|
||||
RemoveSubscriberSocket(String),
|
||||
AddSubscriberPipe(String),
|
||||
RemoveSubscriberPipe(String),
|
||||
ApplicationSpecificConfigurationSchema,
|
||||
NotificationSchema,
|
||||
SocketSchema,
|
||||
@@ -221,6 +223,8 @@ pub enum ApplicationIdentifier {
|
||||
Class,
|
||||
#[serde(alias = "title")]
|
||||
Title,
|
||||
#[serde(alias = "path")]
|
||||
Path,
|
||||
}
|
||||
|
||||
#[derive(
|
||||
|
||||
@@ -26,9 +26,24 @@ impl From<RECT> for Rect {
|
||||
}
|
||||
}
|
||||
|
||||
impl From<Rect> for RECT {
|
||||
fn from(rect: Rect) -> Self {
|
||||
Self {
|
||||
left: rect.left,
|
||||
top: rect.top,
|
||||
right: rect.right,
|
||||
bottom: rect.bottom,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Rect {
|
||||
pub fn add_padding(&mut self, padding: Option<i32>) {
|
||||
if let Some(padding) = padding {
|
||||
/// decrease the size of self by the padding amount.
|
||||
pub fn add_padding<T>(&mut self, padding: T)
|
||||
where
|
||||
T: Into<Option<i32>>,
|
||||
{
|
||||
if let Some(padding) = padding.into() {
|
||||
self.left += padding;
|
||||
self.top += padding;
|
||||
self.right -= padding * 2;
|
||||
@@ -36,6 +51,22 @@ impl Rect {
|
||||
}
|
||||
}
|
||||
|
||||
/// increase the size of self by the margin amount.
|
||||
pub fn add_margin(&mut self, margin: i32) {
|
||||
self.left -= margin;
|
||||
self.top -= margin;
|
||||
self.right += margin * 2;
|
||||
self.bottom += margin * 2;
|
||||
}
|
||||
|
||||
pub fn left_padding(&mut self, padding: i32) {
|
||||
self.left += padding;
|
||||
}
|
||||
|
||||
pub fn right_padding(&mut self, padding: i32) {
|
||||
self.right -= padding;
|
||||
}
|
||||
|
||||
#[must_use]
|
||||
pub const fn contains_point(&self, point: (i32, i32)) -> bool {
|
||||
point.0 >= self.left
|
||||
@@ -43,4 +74,14 @@ impl Rect {
|
||||
&& point.1 >= self.top
|
||||
&& point.1 <= self.top + self.bottom
|
||||
}
|
||||
|
||||
#[must_use]
|
||||
pub const fn scale(&self, system_dpi: i32, rect_dpi: i32) -> Rect {
|
||||
Rect {
|
||||
left: (self.left * rect_dpi) / system_dpi,
|
||||
top: (self.top * rect_dpi) / system_dpi,
|
||||
right: (self.right * rect_dpi) / system_dpi,
|
||||
bottom: (self.bottom * rect_dpi) / system_dpi,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,25 +0,0 @@
|
||||
{
|
||||
"$schema": "https://raw.githubusercontent.com/LGUG2Z/komorebi/v0.1.20/schema.json",
|
||||
"app_specific_configuration_path": "$Env:USERPROFILE/applications.yaml",
|
||||
"window_hiding_behaviour": "Cloak",
|
||||
"cross_monitor_move_behaviour": "Insert",
|
||||
"default_workspace_padding": 20,
|
||||
"default_container_padding": 20,
|
||||
"active_window_border": false,
|
||||
"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 }
|
||||
},
|
||||
"monitors": [
|
||||
{
|
||||
"workspaces": [
|
||||
{ "name": "I", "layout": "BSP" },
|
||||
{ "name": "II", "layout": "VerticalStack" },
|
||||
{ "name": "III", "layout": "HorizontalStack" },
|
||||
{ "name": "IV", "layout": "UltrawideVerticalStack" },
|
||||
{ "name": "V", "layout": "Rows" }
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -1,6 +1,6 @@
|
||||
[package]
|
||||
name = "komorebi"
|
||||
version = "0.1.20"
|
||||
version = "0.1.23-dev.0"
|
||||
authors = ["Jade Iqbal <jadeiqbal@fastmail.com>"]
|
||||
description = "A tiling window manager for Windows"
|
||||
categories = ["tiling-window-manager", "windows"]
|
||||
@@ -15,37 +15,38 @@ komorebi-core = { path = "../komorebi-core" }
|
||||
|
||||
bitflags = "2"
|
||||
clap = { version = "4", features = ["derive"] }
|
||||
color-eyre = { workspace = true }
|
||||
crossbeam-channel = "0.5"
|
||||
crossbeam-utils = "0.8"
|
||||
ctrlc = "3"
|
||||
dirs = { workspace = true }
|
||||
getset = "0.1"
|
||||
hotwatch = "0.4"
|
||||
hex_color = { version = "3", features = ["serde"] }
|
||||
hotwatch = "0.5"
|
||||
lazy_static = "1"
|
||||
miow = "0.5"
|
||||
nanoid = "0.4"
|
||||
net2 = "0.2"
|
||||
os_info = "3.7"
|
||||
os_info = "3.8"
|
||||
parking_lot = { version = "0.12", features = ["deadlock_detection"] }
|
||||
paste = "1"
|
||||
regex = "1"
|
||||
schemars = "0.8"
|
||||
serde = { version = "1", features = ["derive"] }
|
||||
serde_json = "1"
|
||||
serde_json = { workspace = true }
|
||||
strum = { version = "0.26", features = ["derive"] }
|
||||
sysinfo = "0.30"
|
||||
tracing = "0.1"
|
||||
tracing-appender = "0.2"
|
||||
tracing-subscriber = { version = "0.3", features = ["env-filter"] }
|
||||
uds_windows = "1"
|
||||
which = "5"
|
||||
which = "6"
|
||||
widestring = "1"
|
||||
windows = { workspace = true }
|
||||
windows-implement = { workspace = true }
|
||||
windows-interface = { workspace = true }
|
||||
winput = "0.2"
|
||||
winreg = "0.52"
|
||||
windows-interface = { workspace = true }
|
||||
windows-implement = { workspace = true }
|
||||
windows = { workspace = true }
|
||||
color-eyre = { workspace = true }
|
||||
dirs = { workspace = true }
|
||||
widestring = "1"
|
||||
|
||||
[features]
|
||||
deadlock_detection = []
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
use std::sync::atomic::Ordering;
|
||||
use std::time::Duration;
|
||||
|
||||
use color_eyre::Result;
|
||||
use windows::core::PCWSTR;
|
||||
@@ -12,19 +11,14 @@ use windows::Win32::UI::WindowsAndMessaging::CS_VREDRAW;
|
||||
use windows::Win32::UI::WindowsAndMessaging::MSG;
|
||||
use windows::Win32::UI::WindowsAndMessaging::WNDCLASSW;
|
||||
|
||||
use komorebi_core::Rect;
|
||||
|
||||
use crate::window::should_act;
|
||||
use crate::window::Window;
|
||||
use crate::windows_callbacks;
|
||||
use crate::WindowsApi;
|
||||
use crate::BORDER_HWND;
|
||||
use crate::BORDER_OFFSET;
|
||||
use crate::BORDER_OVERFLOW_IDENTIFIERS;
|
||||
use crate::BORDER_RECT;
|
||||
use crate::REGEX_IDENTIFIERS;
|
||||
use crate::BORDER_WIDTH;
|
||||
use crate::TRANSPARENCY_COLOUR;
|
||||
use crate::WINDOWS_11;
|
||||
|
||||
#[derive(Debug, Clone, Copy)]
|
||||
pub struct Border {
|
||||
@@ -68,7 +62,6 @@ impl Border {
|
||||
unsafe {
|
||||
while GetMessageW(&mut message, border.hwnd(), 0, 0).into() {
|
||||
DispatchMessageW(&message);
|
||||
std::thread::sleep(Duration::from_millis(10));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -82,10 +75,6 @@ impl Border {
|
||||
|
||||
BORDER_HWND.store(hwnd.0, Ordering::SeqCst);
|
||||
|
||||
if *WINDOWS_11 {
|
||||
WindowsApi::round_corners(hwnd.0)?;
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
@@ -97,12 +86,7 @@ impl Border {
|
||||
}
|
||||
}
|
||||
|
||||
pub fn set_position(
|
||||
self,
|
||||
window: Window,
|
||||
invisible_borders: &Rect,
|
||||
activate: bool,
|
||||
) -> Result<()> {
|
||||
pub fn set_position(self, window: Window, activate: bool) -> Result<()> {
|
||||
if self.hwnd == 0 {
|
||||
Ok(())
|
||||
} else {
|
||||
@@ -111,38 +95,10 @@ impl Border {
|
||||
}
|
||||
|
||||
let mut rect = WindowsApi::window_rect(window.hwnd())?;
|
||||
rect.top -= invisible_borders.bottom;
|
||||
rect.bottom += invisible_borders.bottom;
|
||||
rect.add_padding(-BORDER_OFFSET.load(Ordering::SeqCst));
|
||||
|
||||
let border_overflows = BORDER_OVERFLOW_IDENTIFIERS.lock();
|
||||
let regex_identifiers = REGEX_IDENTIFIERS.lock();
|
||||
|
||||
let title = &window.title()?;
|
||||
let exe_name = &window.exe()?;
|
||||
let class = &window.class()?;
|
||||
|
||||
let should_expand_border = should_act(
|
||||
title,
|
||||
exe_name,
|
||||
class,
|
||||
&border_overflows,
|
||||
®ex_identifiers,
|
||||
);
|
||||
|
||||
if should_expand_border {
|
||||
rect.left -= invisible_borders.left;
|
||||
rect.top -= invisible_borders.top;
|
||||
rect.right += invisible_borders.right;
|
||||
rect.bottom += invisible_borders.bottom;
|
||||
}
|
||||
|
||||
let border_offset = BORDER_OFFSET.lock();
|
||||
if let Some(border_offset) = *border_offset {
|
||||
rect.left -= border_offset.left;
|
||||
rect.top -= border_offset.top;
|
||||
rect.right += border_offset.right;
|
||||
rect.bottom += border_offset.bottom;
|
||||
}
|
||||
let border_width = BORDER_WIDTH.load(Ordering::SeqCst);
|
||||
rect.add_margin(border_width);
|
||||
|
||||
*BORDER_RECT.lock() = rect;
|
||||
|
||||
|
||||
103
komorebi/src/colour.rs
Normal file
103
komorebi/src/colour.rs
Normal file
@@ -0,0 +1,103 @@
|
||||
use hex_color::HexColor;
|
||||
use schemars::gen::SchemaGenerator;
|
||||
use schemars::schema::InstanceType;
|
||||
use schemars::schema::Schema;
|
||||
use schemars::schema::SchemaObject;
|
||||
use schemars::JsonSchema;
|
||||
use serde::Deserialize;
|
||||
use serde::Serialize;
|
||||
|
||||
#[derive(Debug, Copy, Clone, Serialize, Deserialize, JsonSchema)]
|
||||
#[serde(untagged)]
|
||||
pub enum Colour {
|
||||
/// Colour represented as RGB
|
||||
Rgb(Rgb),
|
||||
/// Colour represented as Hex
|
||||
Hex(Hex),
|
||||
}
|
||||
|
||||
impl From<Rgb> for Colour {
|
||||
fn from(value: Rgb) -> Self {
|
||||
Self::Rgb(value)
|
||||
}
|
||||
}
|
||||
|
||||
impl From<u32> for Colour {
|
||||
fn from(value: u32) -> Self {
|
||||
Self::Rgb(Rgb::from(value))
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Copy, Clone, Serialize, Deserialize)]
|
||||
pub struct Hex(HexColor);
|
||||
|
||||
impl JsonSchema for Hex {
|
||||
fn schema_name() -> String {
|
||||
String::from("Hex")
|
||||
}
|
||||
|
||||
fn json_schema(_: &mut SchemaGenerator) -> Schema {
|
||||
SchemaObject {
|
||||
instance_type: Some(InstanceType::String.into()),
|
||||
..Default::default()
|
||||
}
|
||||
.into()
|
||||
}
|
||||
}
|
||||
|
||||
impl From<Colour> for u32 {
|
||||
fn from(value: Colour) -> Self {
|
||||
match value {
|
||||
Colour::Rgb(val) => val.into(),
|
||||
Colour::Hex(val) => (Rgb::from(val)).into(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Copy, Clone, Serialize, Deserialize, JsonSchema)]
|
||||
pub struct Rgb {
|
||||
/// Red
|
||||
pub r: u32,
|
||||
/// Green
|
||||
pub g: u32,
|
||||
/// Blue
|
||||
pub b: u32,
|
||||
}
|
||||
|
||||
impl Rgb {
|
||||
pub fn new(r: u32, g: u32, b: u32) -> Self {
|
||||
Self { r, g, b }
|
||||
}
|
||||
}
|
||||
|
||||
impl From<Hex> for Rgb {
|
||||
fn from(value: Hex) -> Self {
|
||||
value.0.into()
|
||||
}
|
||||
}
|
||||
|
||||
impl From<HexColor> for Rgb {
|
||||
fn from(value: HexColor) -> Self {
|
||||
Self {
|
||||
r: value.r as u32,
|
||||
g: value.g as u32,
|
||||
b: value.b as u32,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl From<Rgb> for u32 {
|
||||
fn from(value: Rgb) -> Self {
|
||||
value.r | (value.g << 8) | (value.b << 16)
|
||||
}
|
||||
}
|
||||
|
||||
impl From<u32> for Rgb {
|
||||
fn from(value: u32) -> Self {
|
||||
Self {
|
||||
r: value & 0xff,
|
||||
g: value >> 8 & 0xff,
|
||||
b: value >> 16 & 0xff,
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -10,7 +10,6 @@ use interfaces::IServiceProvider;
|
||||
|
||||
use std::ffi::c_void;
|
||||
|
||||
use windows::core::ComInterface;
|
||||
use windows::core::Interface;
|
||||
use windows::Win32::Foundation::HWND;
|
||||
use windows::Win32::System::Com::CoCreateInstance;
|
||||
|
||||
@@ -3,17 +3,22 @@ use std::collections::VecDeque;
|
||||
use getset::Getters;
|
||||
use nanoid::nanoid;
|
||||
use schemars::JsonSchema;
|
||||
use serde::Deserialize;
|
||||
use serde::Serialize;
|
||||
|
||||
use crate::ring::Ring;
|
||||
use crate::stackbar::Stackbar;
|
||||
use crate::window::Window;
|
||||
use crate::StackbarMode;
|
||||
use crate::STACKBAR_MODE;
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Getters, JsonSchema)]
|
||||
#[derive(Debug, Clone, Serialize, Deserialize, Getters, JsonSchema)]
|
||||
pub struct Container {
|
||||
#[serde(skip_serializing)]
|
||||
#[getset(get = "pub")]
|
||||
id: String,
|
||||
windows: Ring<Window>,
|
||||
#[getset(get = "pub", get_mut = "pub")]
|
||||
stackbar: Option<Stackbar>,
|
||||
}
|
||||
|
||||
impl_ring_elements!(Container, Window);
|
||||
@@ -23,6 +28,10 @@ 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,
|
||||
},
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -34,6 +43,38 @@ 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();
|
||||
|
||||
if !should_hide {
|
||||
if let Some(omit) = omit {
|
||||
if omit != window.hwnd {
|
||||
should_hide = true
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if should_hide {
|
||||
window.hide();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub fn restore(&self) {
|
||||
if let Some(stackbar) = self.stackbar() {
|
||||
stackbar.restore();
|
||||
}
|
||||
|
||||
if let Some(window) = self.focused_window() {
|
||||
window.restore();
|
||||
}
|
||||
}
|
||||
|
||||
pub fn load_focused_window(&mut self) {
|
||||
let focused_idx = self.focused_window_idx();
|
||||
for (i, window) in self.windows_mut().iter_mut().enumerate() {
|
||||
@@ -81,6 +122,10 @@ 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 {
|
||||
self.stackbar = None;
|
||||
}
|
||||
|
||||
if idx != 0 {
|
||||
self.focus_window(idx - 1);
|
||||
};
|
||||
@@ -95,6 +140,14 @@ 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);
|
||||
}
|
||||
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
use std::sync::atomic::Ordering;
|
||||
use std::time::Duration;
|
||||
|
||||
use color_eyre::Result;
|
||||
use windows::core::PCWSTR;
|
||||
@@ -59,7 +58,6 @@ impl Hidden {
|
||||
unsafe {
|
||||
while GetMessageW(&mut message, hidden.hwnd(), 0, 0).into() {
|
||||
DispatchMessageW(&message);
|
||||
std::thread::sleep(Duration::from_millis(10));
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
374
komorebi/src/lib.rs
Normal file
374
komorebi/src/lib.rs
Normal file
@@ -0,0 +1,374 @@
|
||||
pub mod border;
|
||||
pub mod com;
|
||||
#[macro_use]
|
||||
pub mod ring;
|
||||
pub mod colour;
|
||||
pub mod container;
|
||||
pub mod hidden;
|
||||
pub mod monitor;
|
||||
pub mod process_command;
|
||||
pub mod process_event;
|
||||
pub mod process_movement;
|
||||
pub mod set_window_position;
|
||||
pub mod stackbar;
|
||||
pub mod static_config;
|
||||
pub mod styles;
|
||||
pub mod window;
|
||||
pub mod window_manager;
|
||||
pub mod window_manager_event;
|
||||
pub mod windows_api;
|
||||
pub mod windows_callbacks;
|
||||
pub mod winevent;
|
||||
pub mod winevent_listener;
|
||||
pub mod workspace;
|
||||
|
||||
use lazy_static::lazy_static;
|
||||
use std::collections::HashMap;
|
||||
use std::collections::VecDeque;
|
||||
use std::fs::File;
|
||||
use std::io::Write;
|
||||
use std::net::TcpStream;
|
||||
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 hidden::*;
|
||||
pub use process_command::*;
|
||||
pub use process_event::*;
|
||||
pub use stackbar::*;
|
||||
pub use static_config::*;
|
||||
pub use window_manager::*;
|
||||
pub use window_manager_event::*;
|
||||
pub use windows_api::WindowsApi;
|
||||
pub use windows_api::*;
|
||||
|
||||
use color_eyre::Result;
|
||||
use komorebi_core::config_generation::IdWithIdentifier;
|
||||
use komorebi_core::config_generation::MatchingRule;
|
||||
use komorebi_core::config_generation::MatchingStrategy;
|
||||
use komorebi_core::ApplicationIdentifier;
|
||||
use komorebi_core::HidingBehaviour;
|
||||
use komorebi_core::Rect;
|
||||
use komorebi_core::SocketMessage;
|
||||
use os_info::Version;
|
||||
use parking_lot::Mutex;
|
||||
use regex::Regex;
|
||||
use schemars::JsonSchema;
|
||||
use serde::Deserialize;
|
||||
use serde::Serialize;
|
||||
use uds_windows::UnixStream;
|
||||
use which::which;
|
||||
use winreg::enums::HKEY_CURRENT_USER;
|
||||
use winreg::RegKey;
|
||||
|
||||
type WorkspaceRule = (usize, usize, bool);
|
||||
|
||||
lazy_static! {
|
||||
static ref HIDDEN_HWNDS: Arc<Mutex<Vec<isize>>> = Arc::new(Mutex::new(vec![]));
|
||||
static ref LAYERED_WHITELIST: Arc<Mutex<Vec<MatchingRule>>> = Arc::new(Mutex::new(vec![
|
||||
MatchingRule::Simple(IdWithIdentifier {
|
||||
kind: ApplicationIdentifier::Exe,
|
||||
id: String::from("steam.exe"),
|
||||
matching_strategy: Option::from(MatchingStrategy::Equals),
|
||||
}),
|
||||
]));
|
||||
static ref TRAY_AND_MULTI_WINDOW_IDENTIFIERS: Arc<Mutex<Vec<MatchingRule>>> =
|
||||
Arc::new(Mutex::new(vec![
|
||||
MatchingRule::Simple(IdWithIdentifier {
|
||||
kind: ApplicationIdentifier::Exe,
|
||||
id: String::from("explorer.exe"),
|
||||
matching_strategy: Option::from(MatchingStrategy::Equals),
|
||||
}),
|
||||
MatchingRule::Simple(IdWithIdentifier {
|
||||
kind: ApplicationIdentifier::Exe,
|
||||
id: String::from("firefox.exe"),
|
||||
matching_strategy: Option::from(MatchingStrategy::Equals),
|
||||
}),
|
||||
MatchingRule::Simple(IdWithIdentifier {
|
||||
kind: ApplicationIdentifier::Exe,
|
||||
id: String::from("chrome.exe"),
|
||||
matching_strategy: Option::from(MatchingStrategy::Equals),
|
||||
}),
|
||||
MatchingRule::Simple(IdWithIdentifier {
|
||||
kind: ApplicationIdentifier::Exe,
|
||||
id: String::from("idea64.exe"),
|
||||
matching_strategy: Option::from(MatchingStrategy::Equals),
|
||||
}),
|
||||
MatchingRule::Simple(IdWithIdentifier {
|
||||
kind: ApplicationIdentifier::Exe,
|
||||
id: String::from("ApplicationFrameHost.exe"),
|
||||
matching_strategy: Option::from(MatchingStrategy::Equals),
|
||||
}),
|
||||
MatchingRule::Simple(IdWithIdentifier {
|
||||
kind: ApplicationIdentifier::Exe,
|
||||
id: String::from("steam.exe"),
|
||||
matching_strategy: Option::from(MatchingStrategy::Equals),
|
||||
})
|
||||
]));
|
||||
static ref OBJECT_NAME_CHANGE_ON_LAUNCH: Arc<Mutex<Vec<MatchingRule>>> = Arc::new(Mutex::new(vec![
|
||||
MatchingRule::Simple(IdWithIdentifier {
|
||||
kind: ApplicationIdentifier::Exe,
|
||||
id: String::from("firefox.exe"),
|
||||
matching_strategy: Option::from(MatchingStrategy::Equals),
|
||||
}),
|
||||
MatchingRule::Simple(IdWithIdentifier {
|
||||
kind: ApplicationIdentifier::Exe,
|
||||
id: String::from("idea64.exe"),
|
||||
matching_strategy: Option::from(MatchingStrategy::Equals),
|
||||
}),
|
||||
]));
|
||||
static ref MONITOR_INDEX_PREFERENCES: Arc<Mutex<HashMap<usize, Rect>>> =
|
||||
Arc::new(Mutex::new(HashMap::new()));
|
||||
static ref DISPLAY_INDEX_PREFERENCES: Arc<Mutex<HashMap<usize, String>>> =
|
||||
Arc::new(Mutex::new(HashMap::new()));
|
||||
static ref WORKSPACE_RULES: Arc<Mutex<HashMap<String, WorkspaceRule>>> =
|
||||
Arc::new(Mutex::new(HashMap::new()));
|
||||
static ref REGEX_IDENTIFIERS: Arc<Mutex<HashMap<String, Regex>>> =
|
||||
Arc::new(Mutex::new(HashMap::new()));
|
||||
static ref MANAGE_IDENTIFIERS: Arc<Mutex<Vec<MatchingRule>>> = Arc::new(Mutex::new(vec![]));
|
||||
static ref FLOAT_IDENTIFIERS: Arc<Mutex<Vec<MatchingRule>>> = Arc::new(Mutex::new(vec![
|
||||
// mstsc.exe creates these on Windows 11 when a WSL process is launched
|
||||
// https://github.com/LGUG2Z/komorebi/issues/74
|
||||
MatchingRule::Simple(IdWithIdentifier {
|
||||
kind: ApplicationIdentifier::Class,
|
||||
id: String::from("OPContainerClass"),
|
||||
matching_strategy: Option::from(MatchingStrategy::Equals),
|
||||
}),
|
||||
MatchingRule::Simple(IdWithIdentifier {
|
||||
kind: ApplicationIdentifier::Class,
|
||||
id: String::from("IHWindowClass"),
|
||||
matching_strategy: Option::from(MatchingStrategy::Equals),
|
||||
})
|
||||
]));
|
||||
|
||||
static ref PERMAIGNORE_CLASSES: Arc<Mutex<Vec<String>>> = Arc::new(Mutex::new(vec![
|
||||
"Chrome_RenderWidgetHostHWND".to_string(),
|
||||
]));
|
||||
static ref BORDER_OVERFLOW_IDENTIFIERS: Arc<Mutex<Vec<MatchingRule>>> = Arc::new(Mutex::new(vec![]));
|
||||
static ref WSL2_UI_PROCESSES: Arc<Mutex<Vec<String>>> = Arc::new(Mutex::new(vec![
|
||||
"X410.exe".to_string(),
|
||||
"vcxsrv.exe".to_string(),
|
||||
]));
|
||||
static ref SUBSCRIPTION_PIPES: Arc<Mutex<HashMap<String, File>>> =
|
||||
Arc::new(Mutex::new(HashMap::new()));
|
||||
static ref SUBSCRIPTION_SOCKETS: Arc<Mutex<HashMap<String, PathBuf>>> =
|
||||
Arc::new(Mutex::new(HashMap::new()));
|
||||
static ref TCP_CONNECTIONS: Arc<Mutex<HashMap<String, TcpStream>>> =
|
||||
Arc::new(Mutex::new(HashMap::new()));
|
||||
static ref HIDING_BEHAVIOUR: Arc<Mutex<HidingBehaviour>> =
|
||||
Arc::new(Mutex::new(HidingBehaviour::Minimize));
|
||||
pub static ref HOME_DIR: PathBuf = {
|
||||
std::env::var("KOMOREBI_CONFIG_HOME").map_or_else(|_| dirs::home_dir().expect("there is no home directory"), |home_path| {
|
||||
let home = PathBuf::from(&home_path);
|
||||
|
||||
if home.as_path().is_dir() {
|
||||
home
|
||||
} else {
|
||||
panic!(
|
||||
"$Env:KOMOREBI_CONFIG_HOME is set to '{home_path}', which is not a valid directory",
|
||||
);
|
||||
}
|
||||
})
|
||||
};
|
||||
pub static ref DATA_DIR: PathBuf = dirs::data_local_dir().expect("there is no local data directory").join("komorebi");
|
||||
pub static ref AHK_EXE: String = {
|
||||
let mut ahk: String = String::from("autohotkey.exe");
|
||||
|
||||
if let Ok(komorebi_ahk_exe) = std::env::var("KOMOREBI_AHK_EXE") {
|
||||
if which(&komorebi_ahk_exe).is_ok() {
|
||||
ahk = komorebi_ahk_exe;
|
||||
}
|
||||
}
|
||||
|
||||
ahk
|
||||
};
|
||||
static ref WINDOWS_11: bool = {
|
||||
matches!(
|
||||
os_info::get().version(),
|
||||
Version::Semantic(_, _, x) if x >= &22000
|
||||
)
|
||||
};
|
||||
|
||||
static ref BORDER_RECT: Arc<Mutex<Rect>> =
|
||||
Arc::new(Mutex::new(Rect::default()));
|
||||
|
||||
|
||||
// Use app-specific titlebar removal options where possible
|
||||
// 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()));
|
||||
|
||||
}
|
||||
|
||||
pub static DEFAULT_WORKSPACE_PADDING: AtomicI32 = AtomicI32::new(10);
|
||||
pub static DEFAULT_CONTAINER_PADDING: AtomicI32 = AtomicI32::new(10);
|
||||
|
||||
pub static INITIAL_CONFIGURATION_LOADED: AtomicBool = AtomicBool::new(false);
|
||||
pub static CUSTOM_FFM: AtomicBool = AtomicBool::new(false);
|
||||
pub static SESSION_ID: AtomicU32 = AtomicU32::new(0);
|
||||
pub static BORDER_ENABLED: AtomicBool = AtomicBool::new(false);
|
||||
pub static BORDER_HWND: AtomicIsize = AtomicIsize::new(0);
|
||||
pub static BORDER_HIDDEN: AtomicBool = AtomicBool::new(false);
|
||||
pub static BORDER_COLOUR_SINGLE: AtomicU32 = AtomicU32::new(0);
|
||||
pub static BORDER_COLOUR_STACK: AtomicU32 = AtomicU32::new(0);
|
||||
pub static BORDER_COLOUR_MONOCLE: AtomicU32 = AtomicU32::new(0);
|
||||
pub static BORDER_COLOUR_CURRENT: AtomicU32 = AtomicU32::new(0);
|
||||
pub static BORDER_WIDTH: AtomicI32 = AtomicI32::new(8);
|
||||
pub static BORDER_OFFSET: AtomicI32 = AtomicI32::new(-1);
|
||||
|
||||
// 0 0 0 aka pure black, I doubt anyone will want this as a border colour
|
||||
pub const TRANSPARENCY_COLOUR: u32 = 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);
|
||||
|
||||
// This is the path on Windows 10
|
||||
let mut current = hkcu
|
||||
.open_subkey(format!(
|
||||
r#"SOFTWARE\Microsoft\Windows\CurrentVersion\Explorer\SessionInfo\{}\VirtualDesktops"#,
|
||||
SESSION_ID.load(Ordering::SeqCst)
|
||||
))
|
||||
.ok()
|
||||
.and_then(
|
||||
|desktops| match desktops.get_raw_value("CurrentVirtualDesktop") {
|
||||
Ok(current) => Option::from(current.bytes),
|
||||
Err(_) => None,
|
||||
},
|
||||
);
|
||||
|
||||
// This is the path on Windows 11
|
||||
if current.is_none() {
|
||||
current = hkcu
|
||||
.open_subkey(r"SOFTWARE\Microsoft\Windows\CurrentVersion\Explorer\VirtualDesktops")
|
||||
.ok()
|
||||
.and_then(
|
||||
|desktops| match desktops.get_raw_value("CurrentVirtualDesktop") {
|
||||
Ok(current) => Option::from(current.bytes),
|
||||
Err(_) => None,
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
// For Win10 users that do not use virtual desktops, the CurrentVirtualDesktop value will not
|
||||
// exist until one has been created in the task view
|
||||
|
||||
// The registry value will also not exist on user login if virtual desktops have been created
|
||||
// but the task view has not been initiated
|
||||
|
||||
// In both of these cases, we return None, and the virtual desktop validation will never run. In
|
||||
// the latter case, if the user desires this validation after initiating the task view, komorebi
|
||||
// should be restarted, and then when this // fn runs again for the first time, it will pick up
|
||||
// the value of CurrentVirtualDesktop and validate against it accordingly
|
||||
current
|
||||
}
|
||||
|
||||
#[derive(Debug, Serialize, Deserialize, JsonSchema)]
|
||||
#[serde(untagged)]
|
||||
pub enum NotificationEvent {
|
||||
WindowManager(WindowManagerEvent),
|
||||
Socket(SocketMessage),
|
||||
}
|
||||
|
||||
#[derive(Debug, Serialize, Deserialize, JsonSchema)]
|
||||
pub struct Notification {
|
||||
pub event: NotificationEvent,
|
||||
pub state: State,
|
||||
}
|
||||
|
||||
pub fn notify_subscribers(notification: &str) -> Result<()> {
|
||||
let mut stale_sockets = vec![];
|
||||
let mut sockets = SUBSCRIPTION_SOCKETS.lock();
|
||||
|
||||
for (socket, path) in &mut *sockets {
|
||||
match UnixStream::connect(path) {
|
||||
Ok(mut stream) => {
|
||||
tracing::debug!("pushed notification to subscriber: {socket}");
|
||||
stream.write_all(notification.as_bytes())?;
|
||||
}
|
||||
Err(_) => {
|
||||
stale_sockets.push(socket.clone());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
for socket in stale_sockets {
|
||||
tracing::warn!("removing stale subscription: {socket}");
|
||||
sockets.remove(&socket);
|
||||
}
|
||||
|
||||
let mut stale_pipes = vec![];
|
||||
let mut pipes = SUBSCRIPTION_PIPES.lock();
|
||||
for (subscriber, pipe) in &mut *pipes {
|
||||
match writeln!(pipe, "{notification}") {
|
||||
Ok(()) => {
|
||||
tracing::debug!("pushed notification to subscriber: {subscriber}");
|
||||
}
|
||||
Err(error) => {
|
||||
// ERROR_FILE_NOT_FOUND
|
||||
// 2 (0x2)
|
||||
// The system cannot find the file specified.
|
||||
|
||||
// ERROR_NO_DATA
|
||||
// 232 (0xE8)
|
||||
// The pipe is being closed.
|
||||
|
||||
// Remove the subscription; the process will have to subscribe again
|
||||
if let Some(2 | 232) = error.raw_os_error() {
|
||||
stale_pipes.push(subscriber.clone());
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
for subscriber in stale_pipes {
|
||||
tracing::warn!("removing stale subscription: {}", subscriber);
|
||||
pipes.remove(&subscriber);
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn load_configuration() -> Result<()> {
|
||||
let config_pwsh = HOME_DIR.join("komorebi.ps1");
|
||||
let config_ahk = HOME_DIR.join("komorebi.ahk");
|
||||
|
||||
if config_pwsh.exists() {
|
||||
let powershell_exe = if which("pwsh.exe").is_ok() {
|
||||
"pwsh.exe"
|
||||
} else {
|
||||
"powershell.exe"
|
||||
};
|
||||
|
||||
tracing::info!("loading configuration file: {}", config_pwsh.display());
|
||||
|
||||
Command::new(powershell_exe)
|
||||
.arg(config_pwsh.as_os_str())
|
||||
.output()?;
|
||||
} else if config_ahk.exists() && which(&*AHK_EXE).is_ok() {
|
||||
tracing::info!("loading configuration file: {}", config_ahk.display());
|
||||
|
||||
Command::new(&*AHK_EXE)
|
||||
.arg(config_ahk.as_os_str())
|
||||
.output()?;
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
@@ -6,16 +6,7 @@
|
||||
clippy::significant_drop_in_scrutinee
|
||||
)]
|
||||
|
||||
use std::collections::HashMap;
|
||||
use std::fs::File;
|
||||
use std::io::Write;
|
||||
use std::net::TcpStream;
|
||||
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;
|
||||
#[cfg(feature = "deadlock_detection")]
|
||||
@@ -23,222 +14,29 @@ use std::time::Duration;
|
||||
|
||||
use clap::Parser;
|
||||
use color_eyre::Result;
|
||||
use crossbeam_channel::Receiver;
|
||||
use crossbeam_channel::Sender;
|
||||
use crossbeam_utils::Backoff;
|
||||
use lazy_static::lazy_static;
|
||||
use os_info::Version;
|
||||
#[cfg(feature = "deadlock_detection")]
|
||||
use parking_lot::deadlock;
|
||||
use parking_lot::Mutex;
|
||||
use regex::Regex;
|
||||
use schemars::JsonSchema;
|
||||
use serde::Serialize;
|
||||
use sysinfo::Process;
|
||||
use tracing_appender::non_blocking::WorkerGuard;
|
||||
use tracing_subscriber::layer::SubscriberExt;
|
||||
use tracing_subscriber::EnvFilter;
|
||||
use which::which;
|
||||
use winreg::enums::HKEY_CURRENT_USER;
|
||||
use winreg::RegKey;
|
||||
|
||||
use crate::hidden::Hidden;
|
||||
use komorebi_core::config_generation::IdWithIdentifier;
|
||||
use komorebi_core::config_generation::MatchingStrategy;
|
||||
use komorebi_core::ApplicationIdentifier;
|
||||
use komorebi_core::HidingBehaviour;
|
||||
use komorebi_core::Rect;
|
||||
use komorebi_core::SocketMessage;
|
||||
|
||||
use crate::process_command::listen_for_commands;
|
||||
use crate::process_command::listen_for_commands_tcp;
|
||||
use crate::process_event::listen_for_events;
|
||||
use crate::process_movement::listen_for_movements;
|
||||
use crate::static_config::StaticConfig;
|
||||
use crate::window_manager::State;
|
||||
use crate::window_manager::WindowManager;
|
||||
use crate::window_manager_event::WindowManagerEvent;
|
||||
use crate::windows_api::WindowsApi;
|
||||
|
||||
#[macro_use]
|
||||
mod ring;
|
||||
|
||||
mod border;
|
||||
mod com;
|
||||
mod container;
|
||||
mod hidden;
|
||||
mod monitor;
|
||||
mod process_command;
|
||||
mod process_event;
|
||||
mod process_movement;
|
||||
mod set_window_position;
|
||||
mod static_config;
|
||||
mod styles;
|
||||
mod window;
|
||||
mod window_manager;
|
||||
mod window_manager_event;
|
||||
mod windows_api;
|
||||
mod windows_callbacks;
|
||||
mod winevent;
|
||||
mod winevent_listener;
|
||||
mod workspace;
|
||||
|
||||
type WorkspaceRule = (usize, usize, bool);
|
||||
|
||||
lazy_static! {
|
||||
static ref HIDDEN_HWNDS: Arc<Mutex<Vec<isize>>> = Arc::new(Mutex::new(vec![]));
|
||||
static ref LAYERED_WHITELIST: Arc<Mutex<Vec<IdWithIdentifier>>> = Arc::new(Mutex::new(vec![
|
||||
IdWithIdentifier {
|
||||
kind: ApplicationIdentifier::Exe,
|
||||
id: String::from("steam.exe"),
|
||||
matching_strategy: Option::from(MatchingStrategy::Equals),
|
||||
},
|
||||
]));
|
||||
static ref TRAY_AND_MULTI_WINDOW_IDENTIFIERS: Arc<Mutex<Vec<IdWithIdentifier>>> =
|
||||
Arc::new(Mutex::new(vec![
|
||||
IdWithIdentifier {
|
||||
kind: ApplicationIdentifier::Exe,
|
||||
id: String::from("explorer.exe"),
|
||||
matching_strategy: Option::from(MatchingStrategy::Equals),
|
||||
},
|
||||
IdWithIdentifier {
|
||||
kind: ApplicationIdentifier::Exe,
|
||||
id: String::from("firefox.exe"),
|
||||
matching_strategy: Option::from(MatchingStrategy::Equals),
|
||||
},
|
||||
IdWithIdentifier {
|
||||
kind: ApplicationIdentifier::Exe,
|
||||
id: String::from("chrome.exe"),
|
||||
matching_strategy: Option::from(MatchingStrategy::Equals),
|
||||
},
|
||||
IdWithIdentifier {
|
||||
kind: ApplicationIdentifier::Exe,
|
||||
id: String::from("idea64.exe"),
|
||||
matching_strategy: Option::from(MatchingStrategy::Equals),
|
||||
},
|
||||
IdWithIdentifier {
|
||||
kind: ApplicationIdentifier::Exe,
|
||||
id: String::from("ApplicationFrameHost.exe"),
|
||||
matching_strategy: Option::from(MatchingStrategy::Equals),
|
||||
},
|
||||
IdWithIdentifier {
|
||||
kind: ApplicationIdentifier::Exe,
|
||||
id: String::from("steam.exe"),
|
||||
matching_strategy: Option::from(MatchingStrategy::Equals),
|
||||
}
|
||||
]));
|
||||
static ref OBJECT_NAME_CHANGE_ON_LAUNCH: Arc<Mutex<Vec<IdWithIdentifier>>> = Arc::new(Mutex::new(vec![
|
||||
IdWithIdentifier {
|
||||
kind: ApplicationIdentifier::Exe,
|
||||
id: String::from("firefox.exe"),
|
||||
matching_strategy: Option::from(MatchingStrategy::Equals),
|
||||
},
|
||||
IdWithIdentifier {
|
||||
kind: ApplicationIdentifier::Exe,
|
||||
id: String::from("idea64.exe"),
|
||||
matching_strategy: Option::from(MatchingStrategy::Equals),
|
||||
},
|
||||
]));
|
||||
static ref MONITOR_INDEX_PREFERENCES: Arc<Mutex<HashMap<usize, Rect>>> =
|
||||
Arc::new(Mutex::new(HashMap::new()));
|
||||
static ref DISPLAY_INDEX_PREFERENCES: Arc<Mutex<HashMap<usize, String>>> =
|
||||
Arc::new(Mutex::new(HashMap::new()));
|
||||
static ref WORKSPACE_RULES: Arc<Mutex<HashMap<String, WorkspaceRule>>> =
|
||||
Arc::new(Mutex::new(HashMap::new()));
|
||||
static ref REGEX_IDENTIFIERS: Arc<Mutex<HashMap<String, Regex>>> =
|
||||
Arc::new(Mutex::new(HashMap::new()));
|
||||
static ref MANAGE_IDENTIFIERS: Arc<Mutex<Vec<IdWithIdentifier>>> = Arc::new(Mutex::new(vec![]));
|
||||
static ref FLOAT_IDENTIFIERS: Arc<Mutex<Vec<IdWithIdentifier>>> = Arc::new(Mutex::new(vec![
|
||||
// mstsc.exe creates these on Windows 11 when a WSL process is launched
|
||||
// https://github.com/LGUG2Z/komorebi/issues/74
|
||||
IdWithIdentifier {
|
||||
kind: ApplicationIdentifier::Class,
|
||||
id: String::from("OPContainerClass"),
|
||||
matching_strategy: Option::from(MatchingStrategy::Equals),
|
||||
},
|
||||
IdWithIdentifier {
|
||||
kind: ApplicationIdentifier::Class,
|
||||
id: String::from("IHWindowClass"),
|
||||
matching_strategy: Option::from(MatchingStrategy::Equals),
|
||||
}
|
||||
]));
|
||||
static ref PERMAIGNORE_CLASSES: Arc<Mutex<Vec<String>>> = Arc::new(Mutex::new(vec![
|
||||
"Chrome_RenderWidgetHostHWND".to_string(),
|
||||
]));
|
||||
static ref BORDER_OVERFLOW_IDENTIFIERS: Arc<Mutex<Vec<IdWithIdentifier>>> = Arc::new(Mutex::new(vec![]));
|
||||
static ref WSL2_UI_PROCESSES: Arc<Mutex<Vec<String>>> = Arc::new(Mutex::new(vec![
|
||||
"X410.exe".to_string(),
|
||||
"vcxsrv.exe".to_string(),
|
||||
]));
|
||||
static ref SUBSCRIPTION_PIPES: Arc<Mutex<HashMap<String, File>>> =
|
||||
Arc::new(Mutex::new(HashMap::new()));
|
||||
static ref TCP_CONNECTIONS: Arc<Mutex<HashMap<String, TcpStream>>> =
|
||||
Arc::new(Mutex::new(HashMap::new()));
|
||||
static ref HIDING_BEHAVIOUR: Arc<Mutex<HidingBehaviour>> =
|
||||
Arc::new(Mutex::new(HidingBehaviour::Minimize));
|
||||
static ref HOME_DIR: PathBuf = {
|
||||
std::env::var("KOMOREBI_CONFIG_HOME").map_or_else(|_| dirs::home_dir().expect("there is no home directory"), |home_path| {
|
||||
let home = PathBuf::from(&home_path);
|
||||
|
||||
if home.as_path().is_dir() {
|
||||
home
|
||||
} else {
|
||||
panic!(
|
||||
"$Env:KOMOREBI_CONFIG_HOME is set to '{home_path}', which is not a valid directory",
|
||||
);
|
||||
}
|
||||
})
|
||||
};
|
||||
static ref DATA_DIR: PathBuf = dirs::data_local_dir().expect("there is no local data directory").join("komorebi");
|
||||
static ref AHK_EXE: String = {
|
||||
let mut ahk: String = String::from("autohotkey.exe");
|
||||
|
||||
if let Ok(komorebi_ahk_exe) = std::env::var("KOMOREBI_AHK_EXE") {
|
||||
if which(&komorebi_ahk_exe).is_ok() {
|
||||
ahk = komorebi_ahk_exe;
|
||||
}
|
||||
}
|
||||
|
||||
ahk
|
||||
};
|
||||
static ref WINDOWS_11: bool = {
|
||||
matches!(
|
||||
os_info::get().version(),
|
||||
Version::Semantic(_, _, x) if x >= &22000
|
||||
)
|
||||
};
|
||||
|
||||
static ref BORDER_RECT: Arc<Mutex<Rect>> =
|
||||
Arc::new(Mutex::new(Rect::default()));
|
||||
|
||||
static ref BORDER_OFFSET: Arc<Mutex<Option<Rect>>> =
|
||||
Arc::new(Mutex::new(None));
|
||||
|
||||
// Use app-specific titlebar removal options where possible
|
||||
// eg. Windows Terminal, IntelliJ IDEA, Firefox
|
||||
static ref NO_TITLEBAR: Arc<Mutex<Vec<String>>> = Arc::new(Mutex::new(vec![]));
|
||||
}
|
||||
|
||||
pub static DEFAULT_WORKSPACE_PADDING: AtomicI32 = AtomicI32::new(10);
|
||||
pub static DEFAULT_CONTAINER_PADDING: AtomicI32 = AtomicI32::new(10);
|
||||
|
||||
pub static INITIAL_CONFIGURATION_LOADED: AtomicBool = AtomicBool::new(false);
|
||||
pub static CUSTOM_FFM: AtomicBool = AtomicBool::new(false);
|
||||
pub static SESSION_ID: AtomicU32 = AtomicU32::new(0);
|
||||
pub static ALT_FOCUS_HACK: AtomicBool = AtomicBool::new(false);
|
||||
pub static BORDER_ENABLED: AtomicBool = AtomicBool::new(false);
|
||||
pub static BORDER_HWND: AtomicIsize = AtomicIsize::new(0);
|
||||
pub static BORDER_HIDDEN: AtomicBool = AtomicBool::new(false);
|
||||
pub static BORDER_COLOUR_SINGLE: AtomicU32 = AtomicU32::new(0);
|
||||
pub static BORDER_COLOUR_STACK: AtomicU32 = AtomicU32::new(0);
|
||||
pub static BORDER_COLOUR_MONOCLE: AtomicU32 = AtomicU32::new(0);
|
||||
pub static BORDER_COLOUR_CURRENT: AtomicU32 = AtomicU32::new(0);
|
||||
pub static BORDER_WIDTH: AtomicI32 = AtomicI32::new(20);
|
||||
// 0 0 0 aka pure black, I doubt anyone will want this as a border colour
|
||||
pub const TRANSPARENCY_COLOUR: u32 = 0;
|
||||
pub static REMOVE_TITLEBARS: AtomicBool = AtomicBool::new(false);
|
||||
|
||||
pub static HIDDEN_HWND: AtomicIsize = AtomicIsize::new(0);
|
||||
use komorebi::hidden::Hidden;
|
||||
use komorebi::load_configuration;
|
||||
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::static_config::StaticConfig;
|
||||
use komorebi::window_manager::WindowManager;
|
||||
use komorebi::windows_api::WindowsApi;
|
||||
use komorebi::winevent_listener;
|
||||
use komorebi::CUSTOM_FFM;
|
||||
use komorebi::HOME_DIR;
|
||||
use komorebi::INITIAL_CONFIGURATION_LOADED;
|
||||
use komorebi::SESSION_ID;
|
||||
|
||||
fn setup() -> Result<(WorkerGuard, WorkerGuard)> {
|
||||
if std::env::var("RUST_LIB_BACKTRACE").is_err() {
|
||||
@@ -303,124 +101,6 @@ fn setup() -> Result<(WorkerGuard, WorkerGuard)> {
|
||||
Ok((guard, color_guard))
|
||||
}
|
||||
|
||||
pub fn load_configuration() -> Result<()> {
|
||||
let config_pwsh = HOME_DIR.join("komorebi.ps1");
|
||||
let config_ahk = HOME_DIR.join("komorebi.ahk");
|
||||
|
||||
if config_pwsh.exists() {
|
||||
let powershell_exe = if which("pwsh.exe").is_ok() {
|
||||
"pwsh.exe"
|
||||
} else {
|
||||
"powershell.exe"
|
||||
};
|
||||
|
||||
tracing::info!("loading configuration file: {}", config_pwsh.display());
|
||||
|
||||
Command::new(powershell_exe)
|
||||
.arg(config_pwsh.as_os_str())
|
||||
.output()?;
|
||||
} else if config_ahk.exists() && which(&*AHK_EXE).is_ok() {
|
||||
tracing::info!("loading configuration file: {}", config_ahk.display());
|
||||
|
||||
Command::new(&*AHK_EXE)
|
||||
.arg(config_ahk.as_os_str())
|
||||
.output()?;
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[must_use]
|
||||
pub fn current_virtual_desktop() -> Option<Vec<u8>> {
|
||||
let hkcu = RegKey::predef(HKEY_CURRENT_USER);
|
||||
|
||||
// This is the path on Windows 10
|
||||
let mut current = hkcu
|
||||
.open_subkey(format!(
|
||||
r#"SOFTWARE\Microsoft\Windows\CurrentVersion\Explorer\SessionInfo\{}\VirtualDesktops"#,
|
||||
SESSION_ID.load(Ordering::SeqCst)
|
||||
))
|
||||
.ok()
|
||||
.and_then(
|
||||
|desktops| match desktops.get_raw_value("CurrentVirtualDesktop") {
|
||||
Ok(current) => Option::from(current.bytes),
|
||||
Err(_) => None,
|
||||
},
|
||||
);
|
||||
|
||||
// This is the path on Windows 11
|
||||
if current.is_none() {
|
||||
current = hkcu
|
||||
.open_subkey(r"SOFTWARE\Microsoft\Windows\CurrentVersion\Explorer\VirtualDesktops")
|
||||
.ok()
|
||||
.and_then(
|
||||
|desktops| match desktops.get_raw_value("CurrentVirtualDesktop") {
|
||||
Ok(current) => Option::from(current.bytes),
|
||||
Err(_) => None,
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
// For Win10 users that do not use virtual desktops, the CurrentVirtualDesktop value will not
|
||||
// exist until one has been created in the task view
|
||||
|
||||
// The registry value will also not exist on user login if virtual desktops have been created
|
||||
// but the task view has not been initiated
|
||||
|
||||
// In both of these cases, we return None, and the virtual desktop validation will never run. In
|
||||
// the latter case, if the user desires this validation after initiating the task view, komorebi
|
||||
// should be restarted, and then when this // fn runs again for the first time, it will pick up
|
||||
// the value of CurrentVirtualDesktop and validate against it accordingly
|
||||
current
|
||||
}
|
||||
|
||||
#[derive(Debug, Serialize, JsonSchema)]
|
||||
#[serde(untagged)]
|
||||
pub enum NotificationEvent {
|
||||
WindowManager(WindowManagerEvent),
|
||||
Socket(SocketMessage),
|
||||
}
|
||||
|
||||
#[derive(Debug, Serialize, JsonSchema)]
|
||||
pub struct Notification {
|
||||
pub event: NotificationEvent,
|
||||
pub state: State,
|
||||
}
|
||||
|
||||
pub fn notify_subscribers(notification: &str) -> Result<()> {
|
||||
let mut stale_subscriptions = vec![];
|
||||
let mut subscriptions = SUBSCRIPTION_PIPES.lock();
|
||||
for (subscriber, pipe) in &mut *subscriptions {
|
||||
match writeln!(pipe, "{notification}") {
|
||||
Ok(()) => {
|
||||
tracing::debug!("pushed notification to subscriber: {}", subscriber);
|
||||
}
|
||||
Err(error) => {
|
||||
// ERROR_FILE_NOT_FOUND
|
||||
// 2 (0x2)
|
||||
// The system cannot find the file specified.
|
||||
|
||||
// ERROR_NO_DATA
|
||||
// 232 (0xE8)
|
||||
// The pipe is being closed.
|
||||
|
||||
// Remove the subscription; the process will have to subscribe again
|
||||
if let Some(2 | 232) = error.raw_os_error() {
|
||||
let subscriber_cl = subscriber.clone();
|
||||
stale_subscriptions.push(subscriber_cl);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
for subscriber in stale_subscriptions {
|
||||
tracing::warn!("removing stale subscription: {}", subscriber);
|
||||
subscriptions.remove(&subscriber);
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[cfg(feature = "deadlock_detection")]
|
||||
#[tracing::instrument]
|
||||
fn detect_deadlocks() {
|
||||
@@ -482,8 +162,8 @@ fn main() -> Result<()> {
|
||||
if matched_procs.len() > 1 {
|
||||
let mut len = matched_procs.len();
|
||||
for proc in matched_procs {
|
||||
if let Some(root) = proc.root() {
|
||||
if root.ends_with("shims") {
|
||||
if let Some(executable_path) = proc.exe() {
|
||||
if executable_path.to_string_lossy().contains("shims") {
|
||||
len -= 1;
|
||||
}
|
||||
}
|
||||
@@ -500,15 +180,11 @@ fn main() -> Result<()> {
|
||||
|
||||
WindowsApi::foreground_lock_timeout()?;
|
||||
|
||||
winevent_listener::start();
|
||||
|
||||
#[cfg(feature = "deadlock_detection")]
|
||||
detect_deadlocks();
|
||||
|
||||
let (outgoing, incoming): (Sender<WindowManagerEvent>, Receiver<WindowManagerEvent>) =
|
||||
crossbeam_channel::unbounded();
|
||||
|
||||
let winevent_listener = winevent_listener::new(Arc::new(Mutex::new(outgoing)));
|
||||
winevent_listener.start();
|
||||
|
||||
Hidden::create("komorebi-hidden")?;
|
||||
|
||||
let static_config = opts.config.map_or_else(
|
||||
@@ -531,12 +207,12 @@ fn main() -> Result<()> {
|
||||
|
||||
Arc::new(Mutex::new(StaticConfig::preload(
|
||||
config,
|
||||
Arc::new(Mutex::new(incoming)),
|
||||
winevent_listener::event_rx(),
|
||||
)?))
|
||||
} else {
|
||||
Arc::new(Mutex::new(WindowManager::new(Arc::new(Mutex::new(
|
||||
incoming,
|
||||
)))?))
|
||||
Arc::new(Mutex::new(WindowManager::new(
|
||||
winevent_listener::event_rx(),
|
||||
)?))
|
||||
};
|
||||
|
||||
wm.lock().init()?;
|
||||
|
||||
@@ -9,6 +9,7 @@ use getset::Getters;
|
||||
use getset::MutGetters;
|
||||
use getset::Setters;
|
||||
use schemars::JsonSchema;
|
||||
use serde::Deserialize;
|
||||
use serde::Serialize;
|
||||
|
||||
use komorebi_core::Rect;
|
||||
@@ -17,7 +18,9 @@ use crate::container::Container;
|
||||
use crate::ring::Ring;
|
||||
use crate::workspace::Workspace;
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Getters, CopyGetters, MutGetters, Setters, JsonSchema)]
|
||||
#[derive(
|
||||
Debug, Clone, Serialize, Deserialize, Getters, CopyGetters, MutGetters, Setters, JsonSchema,
|
||||
)]
|
||||
pub struct Monitor {
|
||||
#[getset(get_copy = "pub", set = "pub")]
|
||||
id: isize,
|
||||
@@ -34,10 +37,9 @@ pub struct Monitor {
|
||||
#[getset(get_copy = "pub", set = "pub")]
|
||||
work_area_offset: Option<Rect>,
|
||||
workspaces: Ring<Workspace>,
|
||||
#[serde(skip_serializing)]
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
#[getset(get_copy = "pub", set = "pub")]
|
||||
last_focused_workspace: Option<usize>,
|
||||
#[serde(skip_serializing)]
|
||||
#[getset(get_mut = "pub")]
|
||||
workspace_names: HashMap<usize, String>,
|
||||
}
|
||||
@@ -69,7 +71,7 @@ impl Monitor {
|
||||
if i == focused_idx {
|
||||
workspace.restore(mouse_follows_focus)?;
|
||||
} else {
|
||||
workspace.hide();
|
||||
workspace.hide(None);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -190,11 +192,7 @@ impl Monitor {
|
||||
self.workspaces().len()
|
||||
}
|
||||
|
||||
pub fn update_focused_workspace(
|
||||
&mut self,
|
||||
offset: Option<Rect>,
|
||||
invisible_borders: &Rect,
|
||||
) -> Result<()> {
|
||||
pub fn update_focused_workspace(&mut self, offset: Option<Rect>) -> Result<()> {
|
||||
let work_area = *self.work_area_size();
|
||||
let offset = if self.work_area_offset().is_some() {
|
||||
self.work_area_offset()
|
||||
@@ -204,7 +202,7 @@ impl Monitor {
|
||||
|
||||
self.focused_workspace_mut()
|
||||
.ok_or_else(|| anyhow!("there is no workspace"))?
|
||||
.update(&work_area, offset, invisible_borders)?;
|
||||
.update(&work_area, offset)?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
@@ -4,7 +4,6 @@ use std::fs::OpenOptions;
|
||||
use std::io::BufRead;
|
||||
use std::io::BufReader;
|
||||
use std::io::Read;
|
||||
use std::io::Write;
|
||||
use std::net::TcpListener;
|
||||
use std::net::TcpStream;
|
||||
use std::num::NonZeroUsize;
|
||||
@@ -24,6 +23,7 @@ use uds_windows::UnixStream;
|
||||
|
||||
use komorebi_core::config_generation::ApplicationConfiguration;
|
||||
use komorebi_core::config_generation::IdWithIdentifier;
|
||||
use komorebi_core::config_generation::MatchingRule;
|
||||
use komorebi_core::config_generation::MatchingStrategy;
|
||||
use komorebi_core::ApplicationIdentifier;
|
||||
use komorebi_core::Axis;
|
||||
@@ -39,6 +39,7 @@ use komorebi_core::WindowContainerBehaviour;
|
||||
use komorebi_core::WindowKind;
|
||||
|
||||
use crate::border::Border;
|
||||
use crate::colour::Rgb;
|
||||
use crate::current_virtual_desktop;
|
||||
use crate::notify_subscribers;
|
||||
use crate::static_config::StaticConfig;
|
||||
@@ -48,7 +49,6 @@ use crate::window_manager::WindowManager;
|
||||
use crate::windows_api::WindowsApi;
|
||||
use crate::Notification;
|
||||
use crate::NotificationEvent;
|
||||
use crate::ALT_FOCUS_HACK;
|
||||
use crate::BORDER_COLOUR_CURRENT;
|
||||
use crate::BORDER_COLOUR_MONOCLE;
|
||||
use crate::BORDER_COLOUR_SINGLE;
|
||||
@@ -72,6 +72,7 @@ use crate::NO_TITLEBAR;
|
||||
use crate::OBJECT_NAME_CHANGE_ON_LAUNCH;
|
||||
use crate::REMOVE_TITLEBARS;
|
||||
use crate::SUBSCRIPTION_PIPES;
|
||||
use crate::SUBSCRIPTION_SOCKETS;
|
||||
use crate::TCP_CONNECTIONS;
|
||||
use crate::TRAY_AND_MULTI_WINDOW_IDENTIFIERS;
|
||||
use crate::WORKSPACE_RULES;
|
||||
@@ -144,8 +145,15 @@ pub fn listen_for_commands_tcp(wm: Arc<Mutex<WindowManager>>, port: usize) {
|
||||
}
|
||||
|
||||
impl WindowManager {
|
||||
#[tracing::instrument(skip(self))]
|
||||
pub fn process_command(&mut self, message: SocketMessage) -> Result<()> {
|
||||
// TODO(raggi): wrap reply in a newtype that can decorate a human friendly
|
||||
// name for the peer, such as getting the pid of the komorebic process for
|
||||
// the UDS or the IP:port for TCP.
|
||||
#[tracing::instrument(skip(self, reply))]
|
||||
pub fn process_command(
|
||||
&mut self,
|
||||
message: SocketMessage,
|
||||
mut reply: impl std::io::Write,
|
||||
) -> Result<()> {
|
||||
if let Some(virtual_desktop_id) = &self.virtual_desktop_id {
|
||||
if let Some(id) = current_virtual_desktop() {
|
||||
if id != *virtual_desktop_id {
|
||||
@@ -209,6 +217,7 @@ impl WindowManager {
|
||||
SocketMessage::UnstackWindow => self.remove_window_from_container()?,
|
||||
SocketMessage::CycleStack(direction) => {
|
||||
self.cycle_container_window_in_direction(direction)?;
|
||||
self.focused_window()?.focus(self.mouse_follows_focus)?;
|
||||
}
|
||||
SocketMessage::ForceFocus => {
|
||||
let focused_window = self.focused_window()?;
|
||||
@@ -266,17 +275,19 @@ impl WindowManager {
|
||||
|
||||
let mut should_push = true;
|
||||
for m in &*manage_identifiers {
|
||||
if m.id.eq(id) {
|
||||
should_push = false;
|
||||
if let MatchingRule::Simple(m) = m {
|
||||
if m.id.eq(id) {
|
||||
should_push = false;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if should_push {
|
||||
manage_identifiers.push(IdWithIdentifier {
|
||||
manage_identifiers.push(MatchingRule::Simple(IdWithIdentifier {
|
||||
kind: identifier,
|
||||
id: id.clone(),
|
||||
matching_strategy: Option::from(MatchingStrategy::Legacy),
|
||||
});
|
||||
}));
|
||||
}
|
||||
}
|
||||
SocketMessage::FloatRule(identifier, ref id) => {
|
||||
@@ -284,20 +295,21 @@ impl WindowManager {
|
||||
|
||||
let mut should_push = true;
|
||||
for f in &*float_identifiers {
|
||||
if f.id.eq(id) {
|
||||
should_push = false;
|
||||
if let MatchingRule::Simple(f) = f {
|
||||
if f.id.eq(id) {
|
||||
should_push = false;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if should_push {
|
||||
float_identifiers.push(IdWithIdentifier {
|
||||
float_identifiers.push(MatchingRule::Simple(IdWithIdentifier {
|
||||
kind: identifier,
|
||||
id: id.clone(),
|
||||
matching_strategy: Option::from(MatchingStrategy::Legacy),
|
||||
});
|
||||
}));
|
||||
}
|
||||
|
||||
let invisible_borders = self.invisible_borders;
|
||||
let offset = self.work_area_offset;
|
||||
|
||||
let mut hwnds_to_purge = vec![];
|
||||
@@ -309,6 +321,11 @@ impl WindowManager {
|
||||
{
|
||||
for window in container.windows() {
|
||||
match identifier {
|
||||
ApplicationIdentifier::Path => {
|
||||
if window.path()? == *id {
|
||||
hwnds_to_purge.push((i, window.hwnd));
|
||||
}
|
||||
}
|
||||
ApplicationIdentifier::Exe => {
|
||||
if window.exe()? == *id {
|
||||
hwnds_to_purge.push((i, window.hwnd));
|
||||
@@ -340,7 +357,7 @@ impl WindowManager {
|
||||
.ok_or_else(|| anyhow!("there is no focused workspace"))?
|
||||
.remove_window(hwnd)?;
|
||||
|
||||
monitor.update_focused_workspace(offset, &invisible_borders)?;
|
||||
monitor.update_focused_workspace(offset)?;
|
||||
}
|
||||
}
|
||||
SocketMessage::FocusedWorkspaceContainerPadding(adjustment) => {
|
||||
@@ -743,15 +760,11 @@ impl WindowManager {
|
||||
Err(error) => error.to_string(),
|
||||
};
|
||||
|
||||
let socket = DATA_DIR.join("komorebic.sock");
|
||||
tracing::info!("replying to state");
|
||||
|
||||
let mut connected = false;
|
||||
while !connected {
|
||||
if let Ok(mut stream) = UnixStream::connect(&socket) {
|
||||
connected = true;
|
||||
stream.write_all(state.as_bytes())?;
|
||||
}
|
||||
}
|
||||
reply.write_all(state.as_bytes())?;
|
||||
|
||||
tracing::info!("replying to state done");
|
||||
}
|
||||
SocketMessage::VisibleWindows => {
|
||||
let mut monitor_visible_windows = HashMap::new();
|
||||
@@ -774,15 +787,7 @@ impl WindowManager {
|
||||
Err(error) => error.to_string(),
|
||||
};
|
||||
|
||||
let socket = DATA_DIR.join("komorebic.sock");
|
||||
|
||||
let mut connected = false;
|
||||
while !connected {
|
||||
if let Ok(mut stream) = UnixStream::connect(&socket) {
|
||||
connected = true;
|
||||
stream.write_all(visible_windows_state.as_bytes())?;
|
||||
}
|
||||
}
|
||||
reply.write_all(visible_windows_state.as_bytes())?;
|
||||
}
|
||||
|
||||
SocketMessage::Query(query) => {
|
||||
@@ -801,15 +806,7 @@ impl WindowManager {
|
||||
}
|
||||
.to_string();
|
||||
|
||||
let socket = DATA_DIR.join("komorebic.sock");
|
||||
|
||||
let mut connected = false;
|
||||
while !connected {
|
||||
if let Ok(mut stream) = UnixStream::connect(&socket) {
|
||||
connected = true;
|
||||
stream.write_all(response.as_bytes())?;
|
||||
}
|
||||
}
|
||||
reply.write_all(response.as_bytes())?;
|
||||
}
|
||||
SocketMessage::ResizeWindowEdge(direction, sizing) => {
|
||||
self.resize_window(direction, sizing, self.resize_delta, true)?;
|
||||
@@ -1034,17 +1031,19 @@ impl WindowManager {
|
||||
|
||||
let mut should_push = true;
|
||||
for i in &*identifiers {
|
||||
if i.id.eq(id) {
|
||||
should_push = false;
|
||||
if let MatchingRule::Simple(i) = i {
|
||||
if i.id.eq(id) {
|
||||
should_push = false;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if should_push {
|
||||
identifiers.push(IdWithIdentifier {
|
||||
identifiers.push(MatchingRule::Simple(IdWithIdentifier {
|
||||
kind: identifier,
|
||||
id: id.clone(),
|
||||
matching_strategy: Option::from(MatchingStrategy::Legacy),
|
||||
});
|
||||
}));
|
||||
}
|
||||
}
|
||||
SocketMessage::IdentifyObjectNameChangeApplication(identifier, ref id) => {
|
||||
@@ -1052,34 +1051,38 @@ impl WindowManager {
|
||||
|
||||
let mut should_push = true;
|
||||
for i in &*identifiers {
|
||||
if i.id.eq(id) {
|
||||
should_push = false;
|
||||
if let MatchingRule::Simple(i) = i {
|
||||
if i.id.eq(id) {
|
||||
should_push = false;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if should_push {
|
||||
identifiers.push(IdWithIdentifier {
|
||||
identifiers.push(MatchingRule::Simple(IdWithIdentifier {
|
||||
kind: identifier,
|
||||
id: id.clone(),
|
||||
matching_strategy: Option::from(MatchingStrategy::Legacy),
|
||||
});
|
||||
}));
|
||||
}
|
||||
}
|
||||
SocketMessage::IdentifyTrayApplication(identifier, ref id) => {
|
||||
let mut identifiers = TRAY_AND_MULTI_WINDOW_IDENTIFIERS.lock();
|
||||
let mut should_push = true;
|
||||
for i in &*identifiers {
|
||||
if i.id.eq(id) {
|
||||
should_push = false;
|
||||
if let MatchingRule::Simple(i) = i {
|
||||
if i.id.eq(id) {
|
||||
should_push = false;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if should_push {
|
||||
identifiers.push(IdWithIdentifier {
|
||||
identifiers.push(MatchingRule::Simple(IdWithIdentifier {
|
||||
kind: identifier,
|
||||
id: id.clone(),
|
||||
matching_strategy: Option::from(MatchingStrategy::Legacy),
|
||||
});
|
||||
}));
|
||||
}
|
||||
}
|
||||
SocketMessage::IdentifyLayeredApplication(identifier, ref id) => {
|
||||
@@ -1087,17 +1090,19 @@ impl WindowManager {
|
||||
|
||||
let mut should_push = true;
|
||||
for i in &*identifiers {
|
||||
if i.id.eq(id) {
|
||||
should_push = false;
|
||||
if let MatchingRule::Simple(i) = i {
|
||||
if i.id.eq(id) {
|
||||
should_push = false;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if should_push {
|
||||
identifiers.push(IdWithIdentifier {
|
||||
identifiers.push(MatchingRule::Simple(IdWithIdentifier {
|
||||
kind: identifier,
|
||||
id: id.clone(),
|
||||
matching_strategy: Option::from(MatchingStrategy::Legacy),
|
||||
});
|
||||
}));
|
||||
}
|
||||
}
|
||||
SocketMessage::ManageFocusedWindow => {
|
||||
@@ -1106,10 +1111,7 @@ impl WindowManager {
|
||||
SocketMessage::UnmanageFocusedWindow => {
|
||||
self.unmanage_focused_window()?;
|
||||
}
|
||||
SocketMessage::InvisibleBorders(rect) => {
|
||||
self.invisible_borders = rect;
|
||||
self.retile_all(false)?;
|
||||
}
|
||||
SocketMessage::InvisibleBorders(_rect) => {}
|
||||
SocketMessage::WorkAreaOffset(rect) => {
|
||||
self.work_area_offset = Option::from(rect);
|
||||
self.retile_all(false)?;
|
||||
@@ -1170,7 +1172,16 @@ impl WindowManager {
|
||||
workspace.set_resize_dimensions(resize);
|
||||
self.update_focused_workspace(false)?;
|
||||
}
|
||||
SocketMessage::AddSubscriber(ref subscriber) => {
|
||||
SocketMessage::AddSubscriberSocket(ref socket) => {
|
||||
let mut sockets = SUBSCRIPTION_SOCKETS.lock();
|
||||
let socket_path = DATA_DIR.join(socket);
|
||||
sockets.insert(socket.clone(), socket_path);
|
||||
}
|
||||
SocketMessage::RemoveSubscriberSocket(ref socket) => {
|
||||
let mut sockets = SUBSCRIPTION_SOCKETS.lock();
|
||||
sockets.remove(socket);
|
||||
}
|
||||
SocketMessage::AddSubscriberPipe(ref subscriber) => {
|
||||
let mut pipes = SUBSCRIPTION_PIPES.lock();
|
||||
let pipe_path = format!(r"\\.\pipe\{subscriber}");
|
||||
let pipe = connect(&pipe_path).map_err(|_| {
|
||||
@@ -1179,7 +1190,7 @@ impl WindowManager {
|
||||
|
||||
pipes.insert(subscriber.clone(), pipe);
|
||||
}
|
||||
SocketMessage::RemoveSubscriber(ref subscriber) => {
|
||||
SocketMessage::RemoveSubscriberPipe(ref subscriber) => {
|
||||
let mut pipes = SUBSCRIPTION_PIPES.lock();
|
||||
pipes.remove(subscriber);
|
||||
}
|
||||
@@ -1238,14 +1249,14 @@ impl WindowManager {
|
||||
SocketMessage::ActiveWindowBorderColour(kind, r, g, b) => {
|
||||
match kind {
|
||||
WindowKind::Single => {
|
||||
BORDER_COLOUR_SINGLE.store(r | (g << 8) | (b << 16), Ordering::SeqCst);
|
||||
BORDER_COLOUR_CURRENT.store(r | (g << 8) | (b << 16), Ordering::SeqCst);
|
||||
BORDER_COLOUR_SINGLE.store(Rgb::new(r, g, b).into(), Ordering::SeqCst);
|
||||
BORDER_COLOUR_CURRENT.store(Rgb::new(r, g, b).into(), Ordering::SeqCst);
|
||||
}
|
||||
WindowKind::Stack => {
|
||||
BORDER_COLOUR_STACK.store(r | (g << 8) | (b << 16), Ordering::SeqCst);
|
||||
BORDER_COLOUR_STACK.store(Rgb::new(r, g, b).into(), Ordering::SeqCst);
|
||||
}
|
||||
WindowKind::Monocle => {
|
||||
BORDER_COLOUR_MONOCLE.store(r | (g << 8) | (b << 16), Ordering::SeqCst);
|
||||
BORDER_COLOUR_MONOCLE.store(Rgb::new(r, g, b).into(), Ordering::SeqCst);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1256,60 +1267,29 @@ impl WindowManager {
|
||||
WindowsApi::invalidate_border_rect()?;
|
||||
}
|
||||
SocketMessage::ActiveWindowBorderOffset(offset) => {
|
||||
let mut current_border_offset = BORDER_OFFSET.lock();
|
||||
|
||||
let new_border_offset = Rect {
|
||||
left: offset,
|
||||
top: offset,
|
||||
right: offset * 2,
|
||||
bottom: offset * 2,
|
||||
};
|
||||
|
||||
*current_border_offset = Option::from(new_border_offset);
|
||||
|
||||
BORDER_OFFSET.store(offset, Ordering::SeqCst);
|
||||
WindowsApi::invalidate_border_rect()?;
|
||||
}
|
||||
SocketMessage::AltFocusHack(enable) => {
|
||||
ALT_FOCUS_HACK.store(enable, Ordering::SeqCst);
|
||||
SocketMessage::AltFocusHack(_) => {
|
||||
tracing::info!("this action is deprecated");
|
||||
}
|
||||
SocketMessage::ApplicationSpecificConfigurationSchema => {
|
||||
let asc = schema_for!(Vec<ApplicationConfiguration>);
|
||||
let schema = serde_json::to_string_pretty(&asc)?;
|
||||
let socket = DATA_DIR.join("komorebic.sock");
|
||||
|
||||
let mut connected = false;
|
||||
while !connected {
|
||||
if let Ok(mut stream) = UnixStream::connect(&socket) {
|
||||
connected = true;
|
||||
stream.write_all(schema.as_bytes())?;
|
||||
}
|
||||
}
|
||||
reply.write_all(schema.as_bytes())?;
|
||||
}
|
||||
SocketMessage::NotificationSchema => {
|
||||
let notification = schema_for!(Notification);
|
||||
let schema = serde_json::to_string_pretty(¬ification)?;
|
||||
let socket = DATA_DIR.join("komorebic.sock");
|
||||
|
||||
let mut connected = false;
|
||||
while !connected {
|
||||
if let Ok(mut stream) = UnixStream::connect(&socket) {
|
||||
connected = true;
|
||||
stream.write_all(schema.as_bytes())?;
|
||||
}
|
||||
}
|
||||
reply.write_all(schema.as_bytes())?;
|
||||
}
|
||||
SocketMessage::SocketSchema => {
|
||||
let socket_message = schema_for!(SocketMessage);
|
||||
let schema = serde_json::to_string_pretty(&socket_message)?;
|
||||
let socket = DATA_DIR.join("komorebic.sock");
|
||||
|
||||
let mut connected = false;
|
||||
while !connected {
|
||||
if let Ok(mut stream) = UnixStream::connect(&socket) {
|
||||
connected = true;
|
||||
stream.write_all(schema.as_bytes())?;
|
||||
}
|
||||
}
|
||||
reply.write_all(schema.as_bytes())?;
|
||||
}
|
||||
SocketMessage::StaticConfigSchema => {
|
||||
let settings = SchemaSettings::default().with(|s| {
|
||||
@@ -1321,27 +1301,13 @@ impl WindowManager {
|
||||
let gen = settings.into_generator();
|
||||
let socket_message = gen.into_root_schema_for::<StaticConfig>();
|
||||
let schema = serde_json::to_string_pretty(&socket_message)?;
|
||||
let socket = DATA_DIR.join("komorebic.sock");
|
||||
|
||||
let mut connected = false;
|
||||
while !connected {
|
||||
if let Ok(mut stream) = UnixStream::connect(&socket) {
|
||||
connected = true;
|
||||
stream.write_all(schema.as_bytes())?;
|
||||
}
|
||||
}
|
||||
reply.write_all(schema.as_bytes())?;
|
||||
}
|
||||
SocketMessage::GenerateStaticConfig => {
|
||||
let config = serde_json::to_string_pretty(&StaticConfig::from(&*self))?;
|
||||
let socket = DATA_DIR.join("komorebic.sock");
|
||||
|
||||
let mut connected = false;
|
||||
while !connected {
|
||||
if let Ok(mut stream) = UnixStream::connect(&socket) {
|
||||
connected = true;
|
||||
stream.write_all(config.as_bytes())?;
|
||||
}
|
||||
}
|
||||
reply.write_all(config.as_bytes())?;
|
||||
}
|
||||
SocketMessage::RemoveTitleBar(_, ref id) => {
|
||||
let mut identifiers = NO_TITLEBAR.lock();
|
||||
@@ -1428,9 +1394,6 @@ impl WindowManager {
|
||||
| SocketMessage::FocusWorkspaceNumber(_) => {
|
||||
let foreground = WindowsApi::foreground_window()?;
|
||||
let foreground_window = Window { hwnd: foreground };
|
||||
let mut rect = WindowsApi::window_rect(foreground_window.hwnd())?;
|
||||
rect.top -= self.invisible_borders.bottom;
|
||||
rect.bottom += self.invisible_borders.bottom;
|
||||
|
||||
let monocle = BORDER_COLOUR_MONOCLE.load(Ordering::SeqCst);
|
||||
if monocle != 0 && self.focused_workspace()?.monocle_container().is_some() {
|
||||
@@ -1440,14 +1403,18 @@ impl WindowManager {
|
||||
);
|
||||
}
|
||||
|
||||
let stack = BORDER_COLOUR_STACK.load(Ordering::SeqCst);
|
||||
if stack != 0 && self.focused_container()?.windows().len() > 1 {
|
||||
BORDER_COLOUR_CURRENT
|
||||
.store(stack, Ordering::SeqCst);
|
||||
// it is not acceptable to fail here; we need to be able to send the event to
|
||||
// subscribers
|
||||
if self.focused_container().is_ok() {
|
||||
let stack = BORDER_COLOUR_STACK.load(Ordering::SeqCst);
|
||||
if stack != 0 && self.focused_container()?.windows().len() > 1 {
|
||||
BORDER_COLOUR_CURRENT
|
||||
.store(stack, Ordering::SeqCst);
|
||||
}
|
||||
}
|
||||
|
||||
let border = Border::from(BORDER_HWND.load(Ordering::SeqCst));
|
||||
border.set_position(foreground_window, &self.invisible_borders, false)?;
|
||||
border.set_position(foreground_window, false)?;
|
||||
}
|
||||
SocketMessage::TogglePause => {
|
||||
let is_paused = self.is_paused;
|
||||
@@ -1457,7 +1424,7 @@ impl WindowManager {
|
||||
border.hide()?;
|
||||
} else {
|
||||
let focused = self.focused_window()?;
|
||||
border.set_position(*focused, &self.invisible_borders, true)?;
|
||||
border.set_position(*focused, true)?;
|
||||
focused.focus(false)?;
|
||||
}
|
||||
}
|
||||
@@ -1467,7 +1434,7 @@ impl WindowManager {
|
||||
|
||||
if tiling_enabled {
|
||||
let focused = self.focused_window()?;
|
||||
border.set_position(*focused, &self.invisible_borders, true)?;
|
||||
border.set_position(*focused, true)?;
|
||||
focused.focus(false)?;
|
||||
} else {
|
||||
border.hide()?;
|
||||
@@ -1526,9 +1493,13 @@ impl WindowManager {
|
||||
}
|
||||
}
|
||||
|
||||
pub fn read_commands_uds(wm: &Arc<Mutex<WindowManager>>, stream: UnixStream) -> Result<()> {
|
||||
let stream = BufReader::new(stream);
|
||||
for line in stream.lines() {
|
||||
pub fn read_commands_uds(wm: &Arc<Mutex<WindowManager>>, mut stream: UnixStream) -> Result<()> {
|
||||
let reader = BufReader::new(stream.try_clone()?);
|
||||
// TODO(raggi): while this processes more than one command, if there are
|
||||
// replies there is no clearly defined protocol for framing yet - it's
|
||||
// perhaps whole-json objects for now, but termination is signalled by
|
||||
// socket shutdown.
|
||||
for line in reader.lines() {
|
||||
let message = SocketMessage::from_str(&line?)?;
|
||||
|
||||
let mut wm = wm.lock();
|
||||
@@ -1536,7 +1507,7 @@ pub fn read_commands_uds(wm: &Arc<Mutex<WindowManager>>, stream: UnixStream) ->
|
||||
if wm.is_paused {
|
||||
return match message {
|
||||
SocketMessage::TogglePause | SocketMessage::State | SocketMessage::Stop => {
|
||||
Ok(wm.process_command(message)?)
|
||||
Ok(wm.process_command(message, &mut stream)?)
|
||||
}
|
||||
_ => {
|
||||
tracing::trace!("ignoring while paused");
|
||||
@@ -1545,7 +1516,7 @@ pub fn read_commands_uds(wm: &Arc<Mutex<WindowManager>>, stream: UnixStream) ->
|
||||
};
|
||||
}
|
||||
|
||||
wm.process_command(message.clone())?;
|
||||
wm.process_command(message.clone(), &mut stream)?;
|
||||
notify_subscribers(&serde_json::to_string(&Notification {
|
||||
event: NotificationEvent::Socket(message.clone()),
|
||||
state: wm.as_ref().into(),
|
||||
@@ -1560,11 +1531,11 @@ pub fn read_commands_tcp(
|
||||
stream: &mut TcpStream,
|
||||
addr: &str,
|
||||
) -> Result<()> {
|
||||
let mut stream = BufReader::new(stream);
|
||||
let mut reader = BufReader::new(stream.try_clone()?);
|
||||
|
||||
loop {
|
||||
let mut buf = vec![0; 1024];
|
||||
match stream.read(&mut buf) {
|
||||
match reader.read(&mut buf) {
|
||||
Err(..) => {
|
||||
tracing::warn!("removing disconnected tcp client: {addr}");
|
||||
let mut connections = TCP_CONNECTIONS.lock();
|
||||
@@ -1585,7 +1556,7 @@ pub fn read_commands_tcp(
|
||||
if wm.is_paused {
|
||||
return match message {
|
||||
SocketMessage::TogglePause | SocketMessage::State | SocketMessage::Stop => {
|
||||
Ok(wm.process_command(message)?)
|
||||
Ok(wm.process_command(message, stream)?)
|
||||
}
|
||||
_ => {
|
||||
tracing::trace!("ignoring while paused");
|
||||
@@ -1594,7 +1565,7 @@ pub fn read_commands_tcp(
|
||||
};
|
||||
}
|
||||
|
||||
wm.process_command(message.clone())?;
|
||||
wm.process_command(message.clone(), &mut *stream)?;
|
||||
notify_subscribers(&serde_json::to_string(&Notification {
|
||||
event: NotificationEvent::Socket(message.clone()),
|
||||
state: wm.as_ref().into(),
|
||||
|
||||
@@ -28,6 +28,8 @@ use crate::BORDER_COLOUR_STACK;
|
||||
use crate::BORDER_ENABLED;
|
||||
use crate::BORDER_HIDDEN;
|
||||
use crate::BORDER_HWND;
|
||||
use crate::BORDER_OFFSET;
|
||||
use crate::BORDER_WIDTH;
|
||||
use crate::DATA_DIR;
|
||||
use crate::HIDDEN_HWNDS;
|
||||
use crate::REGEX_IDENTIFIERS;
|
||||
@@ -35,7 +37,7 @@ use crate::TRAY_AND_MULTI_WINDOW_IDENTIFIERS;
|
||||
|
||||
#[tracing::instrument]
|
||||
pub fn listen_for_events(wm: Arc<Mutex<WindowManager>>) {
|
||||
let receiver = wm.lock().incoming_events.lock().clone();
|
||||
let receiver = wm.lock().incoming_events.clone();
|
||||
|
||||
std::thread::spawn(move || {
|
||||
tracing::info!("listening");
|
||||
@@ -109,7 +111,6 @@ impl WindowManager {
|
||||
_ => {}
|
||||
}
|
||||
|
||||
let invisible_borders = self.invisible_borders;
|
||||
let offset = self.work_area_offset;
|
||||
|
||||
for (i, monitor) in self.monitors_mut().iter_mut().enumerate() {
|
||||
@@ -123,7 +124,7 @@ impl WindowManager {
|
||||
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, &invisible_borders)?;
|
||||
workspace.update(&work_area, offset)?;
|
||||
tracing::info!(
|
||||
"reaped {} orphan window(s) and {} orphaned container(s) on monitor: {}, workspace: {}",
|
||||
reaped_orphans.0,
|
||||
@@ -146,7 +147,7 @@ impl WindowManager {
|
||||
|
||||
match event {
|
||||
WindowManagerEvent::Raise(window) => {
|
||||
window.raise();
|
||||
window.focus(false)?;
|
||||
self.has_pending_raise_op = false;
|
||||
}
|
||||
WindowManagerEvent::Destroy(_, window) | WindowManagerEvent::Unmanage(window) => {
|
||||
@@ -186,6 +187,7 @@ impl WindowManager {
|
||||
let title = &window.title()?;
|
||||
let exe_name = &window.exe()?;
|
||||
let class = &window.class()?;
|
||||
let path = &window.path()?;
|
||||
|
||||
// We don't want to purge windows that have been deliberately hidden by us, eg. when
|
||||
// they are not on the top of a container stack.
|
||||
@@ -194,6 +196,7 @@ impl WindowManager {
|
||||
title,
|
||||
exe_name,
|
||||
class,
|
||||
path,
|
||||
&tray_and_multi_window_identifiers,
|
||||
®ex_identifiers,
|
||||
);
|
||||
@@ -303,21 +306,24 @@ impl WindowManager {
|
||||
}
|
||||
}
|
||||
WindowManagerEvent::MoveResizeStart(_, window) => {
|
||||
let monitor_idx = self.focused_monitor_idx();
|
||||
let workspace_idx = self
|
||||
.focused_monitor()
|
||||
.ok_or_else(|| anyhow!("there is no monitor with this idx"))?
|
||||
.focused_workspace_idx();
|
||||
let container_idx = self
|
||||
.focused_monitor()
|
||||
.ok_or_else(|| anyhow!("there is no monitor with this idx"))?
|
||||
.focused_workspace()
|
||||
.ok_or_else(|| anyhow!("there is no workspace with this idx"))?
|
||||
.focused_container_idx();
|
||||
if *self.focused_workspace()?.tile() {
|
||||
let monitor_idx = self.focused_monitor_idx();
|
||||
let workspace_idx = self
|
||||
.focused_monitor()
|
||||
.ok_or_else(|| anyhow!("there is no monitor with this idx"))?
|
||||
.focused_workspace_idx();
|
||||
let container_idx = self
|
||||
.focused_monitor()
|
||||
.ok_or_else(|| anyhow!("there is no monitor with this idx"))?
|
||||
.focused_workspace()
|
||||
.ok_or_else(|| anyhow!("there is no workspace with this idx"))?
|
||||
.focused_container_idx();
|
||||
|
||||
WindowsApi::bring_window_to_top(window.hwnd())?;
|
||||
WindowsApi::bring_window_to_top(window.hwnd())?;
|
||||
|
||||
self.pending_move_op = Option::from((monitor_idx, workspace_idx, container_idx));
|
||||
self.pending_move_op =
|
||||
Option::from((monitor_idx, workspace_idx, container_idx));
|
||||
}
|
||||
}
|
||||
WindowManagerEvent::MoveResizeEnd(_, window) => {
|
||||
// We need this because if the event ends on a different monitor,
|
||||
@@ -331,7 +337,6 @@ impl WindowManager {
|
||||
.ok_or_else(|| anyhow!("cannot get monitor idx from current position"))?;
|
||||
|
||||
let new_window_behaviour = self.window_container_behaviour;
|
||||
let invisible_borders = self.invisible_borders;
|
||||
|
||||
let workspace = self.focused_workspace_mut()?;
|
||||
if !workspace
|
||||
@@ -341,7 +346,7 @@ impl WindowManager {
|
||||
{
|
||||
let focused_container_idx = workspace.focused_container_idx();
|
||||
|
||||
let mut new_position = WindowsApi::window_rect(window.hwnd())?;
|
||||
let new_position = WindowsApi::window_rect(window.hwnd())?;
|
||||
|
||||
let old_position = *workspace
|
||||
.latest_layout()
|
||||
@@ -366,12 +371,6 @@ impl WindowManager {
|
||||
}
|
||||
}
|
||||
|
||||
// Adjust for the invisible borders
|
||||
new_position.left += invisible_borders.left;
|
||||
new_position.top += invisible_borders.top;
|
||||
new_position.right -= invisible_borders.right;
|
||||
new_position.bottom -= invisible_borders.bottom;
|
||||
|
||||
let resize = Rect {
|
||||
left: new_position.left - old_position.left,
|
||||
top: new_position.top - old_position.top,
|
||||
@@ -381,10 +380,14 @@ 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 is_move = moved_across_monitors
|
||||
|| resize.right == 0 && resize.bottom == 0
|
||||
|| resize.right.abs() == invisible_borders.right
|
||||
&& resize.bottom.abs() == invisible_borders.bottom;
|
||||
|| resize.right.abs() == right_bottom_constant
|
||||
&& resize.bottom.abs() == right_bottom_constant;
|
||||
|
||||
if is_move {
|
||||
tracing::info!("moving with mouse");
|
||||
@@ -476,17 +479,17 @@ impl WindowManager {
|
||||
let mut ops = vec![];
|
||||
|
||||
macro_rules! resize_op {
|
||||
($coordinate:expr, $comparator:tt, $direction:expr) => {{
|
||||
let adjusted = $coordinate * 2;
|
||||
let sizing = if adjusted $comparator 0 {
|
||||
Sizing::Decrease
|
||||
} else {
|
||||
Sizing::Increase
|
||||
};
|
||||
($coordinate:expr, $comparator:tt, $direction:expr) => {{
|
||||
let adjusted = $coordinate * 2;
|
||||
let sizing = if adjusted $comparator 0 {
|
||||
Sizing::Decrease
|
||||
} else {
|
||||
Sizing::Increase
|
||||
};
|
||||
|
||||
($direction, sizing, adjusted.abs())
|
||||
}};
|
||||
}
|
||||
($direction, sizing, adjusted.abs())
|
||||
}};
|
||||
}
|
||||
|
||||
if resize.left != 0 {
|
||||
ops.push(resize_op!(resize.left, >, OperationDirection::Left));
|
||||
@@ -496,11 +499,14 @@ impl WindowManager {
|
||||
ops.push(resize_op!(resize.top, >, OperationDirection::Up));
|
||||
}
|
||||
|
||||
if resize.right != 0 && resize.left == 0 {
|
||||
let top_left_constant = BORDER_WIDTH.load(Ordering::SeqCst)
|
||||
+ BORDER_OFFSET.load(Ordering::SeqCst);
|
||||
|
||||
if resize.right != 0 && resize.left == top_left_constant {
|
||||
ops.push(resize_op!(resize.right, <, OperationDirection::Right));
|
||||
}
|
||||
|
||||
if resize.bottom != 0 && resize.top == 0 {
|
||||
if resize.bottom != 0 && resize.top == top_left_constant {
|
||||
ops.push(resize_op!(resize.bottom, <, OperationDirection::Down));
|
||||
}
|
||||
|
||||
@@ -512,12 +518,21 @@ impl WindowManager {
|
||||
}
|
||||
}
|
||||
}
|
||||
WindowManagerEvent::ForceUpdate(_) => {
|
||||
self.update_focused_workspace(false)?;
|
||||
}
|
||||
WindowManagerEvent::DisplayChange(..)
|
||||
| WindowManagerEvent::MouseCapture(..)
|
||||
| WindowManagerEvent::Cloak(..)
|
||||
| WindowManagerEvent::Uncloak(..) => {}
|
||||
};
|
||||
|
||||
if !self.focused_workspace()?.tile() {
|
||||
let border = Border::from(BORDER_HWND.load(Ordering::SeqCst));
|
||||
border.hide()?;
|
||||
BORDER_HIDDEN.store(true, Ordering::SeqCst);
|
||||
}
|
||||
|
||||
if *self.focused_workspace()?.tile() && BORDER_ENABLED.load(Ordering::SeqCst) {
|
||||
match event {
|
||||
WindowManagerEvent::MoveResizeStart(_, _) => {
|
||||
@@ -529,6 +544,7 @@ impl WindowManager {
|
||||
| WindowManagerEvent::Show(_, window)
|
||||
| WindowManagerEvent::FocusChange(_, window)
|
||||
| WindowManagerEvent::Hide(_, window)
|
||||
| WindowManagerEvent::Uncloak(_, window)
|
||||
| WindowManagerEvent::Minimize(_, window) => {
|
||||
let border = Border::from(BORDER_HWND.load(Ordering::SeqCst));
|
||||
let mut target_window = None;
|
||||
@@ -585,15 +601,10 @@ impl WindowManager {
|
||||
}
|
||||
|
||||
if let Some(target_window) = target_window {
|
||||
let window = target_window;
|
||||
let mut rect = WindowsApi::window_rect(window.hwnd())?;
|
||||
rect.top -= self.invisible_borders.bottom;
|
||||
rect.bottom += self.invisible_borders.bottom;
|
||||
|
||||
let activate = BORDER_HIDDEN.load(Ordering::SeqCst);
|
||||
|
||||
WindowsApi::invalidate_border_rect()?;
|
||||
border.set_position(target_window, &self.invisible_borders, activate)?;
|
||||
border.set_position(target_window, activate)?;
|
||||
|
||||
if activate {
|
||||
BORDER_HIDDEN.store(false, Ordering::SeqCst);
|
||||
@@ -606,7 +617,7 @@ impl WindowManager {
|
||||
|
||||
// If we unmanaged a window, it shouldn't be immediately hidden behind managed windows
|
||||
if let WindowManagerEvent::Unmanage(window) = event {
|
||||
window.center(&self.focused_monitor_work_area()?, &invisible_borders)?;
|
||||
window.center(&self.focused_monitor_work_area()?)?;
|
||||
}
|
||||
|
||||
// If there are no more windows on the workspace, we shouldn't show the border window
|
||||
|
||||
@@ -1,9 +1,10 @@
|
||||
use std::collections::VecDeque;
|
||||
|
||||
use schemars::JsonSchema;
|
||||
use serde::Deserialize;
|
||||
use serde::Serialize;
|
||||
|
||||
#[derive(Debug, Clone, Serialize, JsonSchema)]
|
||||
#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)]
|
||||
pub struct Ring<T> {
|
||||
elements: VecDeque<T>,
|
||||
focused: usize,
|
||||
|
||||
294
komorebi/src/stackbar.rs
Normal file
294
komorebi/src/stackbar.rs
Normal file
@@ -0,0 +1,294 @@
|
||||
use std::collections::VecDeque;
|
||||
use std::sync::atomic::Ordering;
|
||||
use std::time::Duration;
|
||||
|
||||
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::winevent::WinEvent;
|
||||
use crate::winevent_listener;
|
||||
use crate::WindowManagerEvent;
|
||||
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::TRANSPARENCY_COLOUR;
|
||||
use crate::WINDOWS_BY_BAR_HWNDS;
|
||||
|
||||
#[derive(Debug, Default, Serialize, Deserialize, JsonSchema)]
|
||||
pub struct Stackbar {
|
||||
pub(crate) hwnd: isize,
|
||||
#[serde(skip)]
|
||||
pub is_cloned: bool,
|
||||
}
|
||||
|
||||
impl Drop for Stackbar {
|
||||
fn drop(&mut self) {
|
||||
if !self.is_cloned {
|
||||
let _ = WindowsApi::close_window(self.hwnd());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Clone for Stackbar {
|
||||
fn clone(&self) -> Self {
|
||||
Self {
|
||||
hwnd: self.hwnd,
|
||||
is_cloned: true,
|
||||
}
|
||||
}
|
||||
}
|
||||
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 };
|
||||
let event_sender = winevent_listener::event_tx();
|
||||
let _ = event_sender.send(WindowManagerEvent::FocusChange(
|
||||
WinEvent::ObjectFocus,
|
||||
window,
|
||||
));
|
||||
let _ = event_sender.send(WindowManagerEvent::ForceUpdate(window));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
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(TRANSPARENCY_COLOUR),
|
||||
..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(TRANSPARENCY_COLOUR), 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,
|
||||
..Default::default()
|
||||
})
|
||||
}
|
||||
|
||||
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));
|
||||
|
||||
window.focus(false)?;
|
||||
} 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)
|
||||
}
|
||||
}
|
||||
@@ -1,4 +1,5 @@
|
||||
use crate::border::Border;
|
||||
use crate::colour::Colour;
|
||||
use crate::current_virtual_desktop;
|
||||
use crate::monitor::Monitor;
|
||||
use crate::ring::Ring;
|
||||
@@ -26,15 +27,24 @@ 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 color_eyre::Result;
|
||||
use crossbeam_channel::Receiver;
|
||||
use hotwatch::notify::DebouncedEvent;
|
||||
use hotwatch::EventKind;
|
||||
use hotwatch::Hotwatch;
|
||||
use komorebi_core::config_generation::ApplicationConfiguration;
|
||||
use komorebi_core::config_generation::ApplicationConfigurationGenerator;
|
||||
use komorebi_core::config_generation::ApplicationOptions;
|
||||
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::ApplicationIdentifier;
|
||||
@@ -62,34 +72,14 @@ use std::sync::Arc;
|
||||
use uds_windows::UnixListener;
|
||||
use uds_windows::UnixStream;
|
||||
|
||||
#[derive(Debug, Serialize, Deserialize, JsonSchema)]
|
||||
pub struct Rgb {
|
||||
/// Red
|
||||
pub r: u32,
|
||||
/// Green
|
||||
pub g: u32,
|
||||
/// Blue
|
||||
pub b: u32,
|
||||
}
|
||||
|
||||
impl From<u32> for Rgb {
|
||||
fn from(value: u32) -> Self {
|
||||
Self {
|
||||
r: value & 0xff,
|
||||
g: value >> 8 & 0xff,
|
||||
b: value >> 16 & 0xff,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Serialize, Deserialize, JsonSchema)]
|
||||
pub struct ActiveWindowBorderColours {
|
||||
/// Border colour when the container contains a single window
|
||||
pub single: Rgb,
|
||||
pub single: Colour,
|
||||
/// Border colour when the container contains multiple windows
|
||||
pub stack: Rgb,
|
||||
pub stack: Colour,
|
||||
/// Border colour when the container is in monocle mode
|
||||
pub monocle: Rgb,
|
||||
pub monocle: Colour,
|
||||
}
|
||||
|
||||
#[derive(Debug, Serialize, Deserialize, JsonSchema)]
|
||||
@@ -232,7 +222,7 @@ impl From<&Monitor> for MonitorConfig {
|
||||
#[derive(Debug, Serialize, Deserialize, JsonSchema)]
|
||||
/// The `komorebi.json` static configuration file reference for `v0.1.20`
|
||||
pub struct StaticConfig {
|
||||
/// Dimensions of Windows' own invisible borders; don't set these yourself unless you are told to
|
||||
/// DEPRECATED from v0.1.22: no longer required
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub invisible_borders: Option<Rect>,
|
||||
/// Delta to resize windows by (default 50)
|
||||
@@ -256,12 +246,14 @@ pub struct StaticConfig {
|
||||
/// Path to applications.yaml from komorebi-application-specific-configurations (default: None)
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub app_specific_configuration_path: Option<PathBuf>,
|
||||
/// Width of the active window border (default: 20)
|
||||
/// Width of the window border (default: 8)
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub active_window_border_width: Option<i32>,
|
||||
/// Offset of the active window border (default: None)
|
||||
#[serde(alias = "active_window_border_width")]
|
||||
pub border_width: Option<i32>,
|
||||
/// Offset of the window border (default: -1)
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub active_window_border_offset: Option<i32>,
|
||||
#[serde(alias = "active_window_border_offset")]
|
||||
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>,
|
||||
@@ -277,10 +269,6 @@ pub struct StaticConfig {
|
||||
/// Monitor and workspace configurations
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub monitors: Option<Vec<MonitorConfig>>,
|
||||
/// DEPRECATED from v0.1.20: no longer required
|
||||
#[schemars(skip)]
|
||||
#[serde(skip_serializing)]
|
||||
pub alt_focus_hack: Option<bool>,
|
||||
/// Which Windows signal to use when hiding windows (default: minimize)
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub window_hiding_behaviour: Option<HidingBehaviour>,
|
||||
@@ -289,40 +277,57 @@ pub struct StaticConfig {
|
||||
pub global_work_area_offset: Option<Rect>,
|
||||
/// Individual window floating rules
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub float_rules: Option<Vec<IdWithIdentifier>>,
|
||||
pub float_rules: Option<Vec<MatchingRule>>,
|
||||
/// Individual window force-manage rules
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub manage_rules: Option<Vec<IdWithIdentifier>>,
|
||||
pub manage_rules: Option<Vec<MatchingRule>>,
|
||||
/// Identify border overflow applications
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub border_overflow_applications: Option<Vec<IdWithIdentifier>>,
|
||||
pub border_overflow_applications: Option<Vec<MatchingRule>>,
|
||||
/// Identify tray and multi-window applications
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub tray_and_multi_window_applications: Option<Vec<IdWithIdentifier>>,
|
||||
pub tray_and_multi_window_applications: Option<Vec<MatchingRule>>,
|
||||
/// Identify applications that have the WS_EX_LAYERED extended window style
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub layered_applications: Option<Vec<IdWithIdentifier>>,
|
||||
pub layered_applications: Option<Vec<MatchingRule>>,
|
||||
/// Identify applications that send EVENT_OBJECT_NAMECHANGE on launch (very rare)
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub object_name_change_applications: Option<Vec<IdWithIdentifier>>,
|
||||
pub object_name_change_applications: Option<Vec<MatchingRule>>,
|
||||
/// Set monitor index preferences
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub monitor_index_preferences: Option<HashMap<usize, Rect>>,
|
||||
/// Set display index preferences
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub display_index_preferences: Option<HashMap<usize, String>>,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub stackbar: Option<StackbarConfig>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Copy, Clone, Serialize, Deserialize, JsonSchema)]
|
||||
pub enum StackbarMode {
|
||||
Always,
|
||||
Never,
|
||||
OnStack,
|
||||
}
|
||||
|
||||
#[derive(Debug, Serialize, Deserialize, JsonSchema)]
|
||||
pub struct TabsConfig {
|
||||
width: Option<i32>,
|
||||
focused_text: Option<Colour>,
|
||||
unfocused_text: Option<Colour>,
|
||||
background: Option<Colour>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Serialize, Deserialize, JsonSchema)]
|
||||
pub struct StackbarConfig {
|
||||
height: Option<i32>,
|
||||
mode: Option<StackbarMode>,
|
||||
tabs: Option<TabsConfig>,
|
||||
}
|
||||
|
||||
impl From<&WindowManager> for StaticConfig {
|
||||
#[allow(clippy::too_many_lines)]
|
||||
fn from(value: &WindowManager) -> Self {
|
||||
let default_invisible_borders = Rect {
|
||||
left: 7,
|
||||
top: 0,
|
||||
right: 14,
|
||||
bottom: 7,
|
||||
};
|
||||
|
||||
let mut monitors = vec![];
|
||||
for m in value.monitors() {
|
||||
monitors.push(MonitorConfig::from(m));
|
||||
@@ -377,13 +382,13 @@ impl From<&WindowManager> for StaticConfig {
|
||||
None
|
||||
} else {
|
||||
Option::from(ActiveWindowBorderColours {
|
||||
single: Rgb::from(BORDER_COLOUR_SINGLE.load(Ordering::SeqCst)),
|
||||
stack: Rgb::from(if BORDER_COLOUR_STACK.load(Ordering::SeqCst) == 0 {
|
||||
single: Colour::from(BORDER_COLOUR_SINGLE.load(Ordering::SeqCst)),
|
||||
stack: Colour::from(if BORDER_COLOUR_STACK.load(Ordering::SeqCst) == 0 {
|
||||
BORDER_COLOUR_SINGLE.load(Ordering::SeqCst)
|
||||
} else {
|
||||
BORDER_COLOUR_STACK.load(Ordering::SeqCst)
|
||||
}),
|
||||
monocle: Rgb::from(if BORDER_COLOUR_MONOCLE.load(Ordering::SeqCst) == 0 {
|
||||
monocle: Colour::from(if BORDER_COLOUR_MONOCLE.load(Ordering::SeqCst) == 0 {
|
||||
BORDER_COLOUR_SINGLE.load(Ordering::SeqCst)
|
||||
} else {
|
||||
BORDER_COLOUR_MONOCLE.load(Ordering::SeqCst)
|
||||
@@ -392,11 +397,7 @@ impl From<&WindowManager> for StaticConfig {
|
||||
};
|
||||
|
||||
Self {
|
||||
invisible_borders: if value.invisible_borders == default_invisible_borders {
|
||||
None
|
||||
} else {
|
||||
Option::from(value.invisible_borders)
|
||||
},
|
||||
invisible_borders: None,
|
||||
resize_delta: Option::from(value.resize_delta),
|
||||
window_container_behaviour: Option::from(value.window_container_behaviour),
|
||||
cross_monitor_move_behaviour: Option::from(value.cross_monitor_move_behaviour),
|
||||
@@ -406,10 +407,8 @@ impl From<&WindowManager> for StaticConfig {
|
||||
focus_follows_mouse: value.focus_follows_mouse,
|
||||
mouse_follows_focus: Option::from(value.mouse_follows_focus),
|
||||
app_specific_configuration_path: None,
|
||||
active_window_border_width: Option::from(BORDER_WIDTH.load(Ordering::SeqCst)),
|
||||
active_window_border_offset: BORDER_OFFSET
|
||||
.lock()
|
||||
.map_or(None, |offset| Option::from(offset.left)),
|
||||
border_width: Option::from(BORDER_WIDTH.load(Ordering::SeqCst)),
|
||||
border_offset: Option::from(BORDER_OFFSET.load(Ordering::SeqCst)),
|
||||
active_window_border: Option::from(BORDER_ENABLED.load(Ordering::SeqCst)),
|
||||
active_window_border_colours: border_colours,
|
||||
default_workspace_padding: Option::from(
|
||||
@@ -419,7 +418,6 @@ impl From<&WindowManager> for StaticConfig {
|
||||
DEFAULT_CONTAINER_PADDING.load(Ordering::SeqCst),
|
||||
),
|
||||
monitors: Option::from(monitors),
|
||||
alt_focus_hack: None,
|
||||
window_hiding_behaviour: Option::from(*HIDING_BEHAVIOUR.lock()),
|
||||
global_work_area_offset: value.work_area_offset,
|
||||
float_rules: None,
|
||||
@@ -430,6 +428,7 @@ impl From<&WindowManager> for StaticConfig {
|
||||
object_name_change_applications: None,
|
||||
monitor_index_preferences: Option::from(MONITOR_INDEX_PREFERENCES.lock().clone()),
|
||||
display_index_preferences: Option::from(DISPLAY_INDEX_PREFERENCES.lock().clone()),
|
||||
stackbar: None,
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -460,49 +459,22 @@ impl StaticConfig {
|
||||
DEFAULT_WORKSPACE_PADDING.store(workspace, Ordering::SeqCst);
|
||||
}
|
||||
|
||||
self.active_window_border_width.map_or_else(
|
||||
self.border_width.map_or_else(
|
||||
|| {
|
||||
BORDER_WIDTH.store(20, Ordering::SeqCst);
|
||||
BORDER_WIDTH.store(8, Ordering::SeqCst);
|
||||
},
|
||||
|width| {
|
||||
BORDER_WIDTH.store(width, Ordering::SeqCst);
|
||||
},
|
||||
);
|
||||
self.active_window_border_offset.map_or_else(
|
||||
|| {
|
||||
let mut border_offset = BORDER_OFFSET.lock();
|
||||
*border_offset = None;
|
||||
},
|
||||
|offset| {
|
||||
let new_border_offset = Rect {
|
||||
left: offset,
|
||||
top: offset,
|
||||
right: offset * 2,
|
||||
bottom: offset * 2,
|
||||
};
|
||||
|
||||
let mut border_offset = BORDER_OFFSET.lock();
|
||||
*border_offset = Some(new_border_offset);
|
||||
},
|
||||
);
|
||||
BORDER_OFFSET.store(self.border_offset.unwrap_or(-1), Ordering::SeqCst);
|
||||
|
||||
if let Some(colours) = &self.active_window_border_colours {
|
||||
BORDER_COLOUR_SINGLE.store(
|
||||
colours.single.r | (colours.single.g << 8) | (colours.single.b << 16),
|
||||
Ordering::SeqCst,
|
||||
);
|
||||
BORDER_COLOUR_CURRENT.store(
|
||||
colours.single.r | (colours.single.g << 8) | (colours.single.b << 16),
|
||||
Ordering::SeqCst,
|
||||
);
|
||||
BORDER_COLOUR_STACK.store(
|
||||
colours.stack.r | (colours.stack.g << 8) | (colours.stack.b << 16),
|
||||
Ordering::SeqCst,
|
||||
);
|
||||
BORDER_COLOUR_MONOCLE.store(
|
||||
colours.monocle.r | (colours.monocle.g << 8) | (colours.monocle.b << 16),
|
||||
Ordering::SeqCst,
|
||||
);
|
||||
BORDER_COLOUR_SINGLE.store(u32::from(colours.single), Ordering::SeqCst);
|
||||
BORDER_COLOUR_CURRENT.store(u32::from(colours.single), Ordering::SeqCst);
|
||||
BORDER_COLOUR_STACK.store(u32::from(colours.stack), Ordering::SeqCst);
|
||||
BORDER_COLOUR_MONOCLE.store(u32::from(colours.monocle), Ordering::SeqCst);
|
||||
}
|
||||
|
||||
let mut float_identifiers = FLOAT_IDENTIFIERS.lock();
|
||||
@@ -513,104 +485,65 @@ impl StaticConfig {
|
||||
let mut object_name_change_identifiers = OBJECT_NAME_CHANGE_ON_LAUNCH.lock();
|
||||
let mut layered_identifiers = LAYERED_WHITELIST.lock();
|
||||
|
||||
if let Some(float) = &mut self.float_rules {
|
||||
for identifier in float {
|
||||
if identifier.matching_strategy.is_none() {
|
||||
identifier.matching_strategy = Option::from(MatchingStrategy::Legacy);
|
||||
}
|
||||
|
||||
if !float_identifiers.contains(identifier) {
|
||||
float_identifiers.push(identifier.clone());
|
||||
|
||||
if matches!(identifier.matching_strategy, Some(MatchingStrategy::Regex)) {
|
||||
let re = Regex::new(&identifier.id)?;
|
||||
regex_identifiers.insert(identifier.id.clone(), re);
|
||||
}
|
||||
}
|
||||
}
|
||||
if let Some(rules) = &mut self.float_rules {
|
||||
populate_rules(rules, &mut float_identifiers, &mut regex_identifiers)?;
|
||||
}
|
||||
|
||||
if let Some(manage) = &mut self.manage_rules {
|
||||
for identifier in manage {
|
||||
if identifier.matching_strategy.is_none() {
|
||||
identifier.matching_strategy = Option::from(MatchingStrategy::Legacy);
|
||||
}
|
||||
|
||||
if !manage_identifiers.contains(identifier) {
|
||||
manage_identifiers.push(identifier.clone());
|
||||
|
||||
if matches!(identifier.matching_strategy, Some(MatchingStrategy::Regex)) {
|
||||
let re = Regex::new(&identifier.id)?;
|
||||
regex_identifiers.insert(identifier.id.clone(), re);
|
||||
}
|
||||
}
|
||||
}
|
||||
if let Some(rules) = &mut self.manage_rules {
|
||||
populate_rules(rules, &mut manage_identifiers, &mut regex_identifiers)?;
|
||||
}
|
||||
|
||||
if let Some(identifiers) = &mut self.object_name_change_applications {
|
||||
for identifier in identifiers {
|
||||
if identifier.matching_strategy.is_none() {
|
||||
identifier.matching_strategy = Option::from(MatchingStrategy::Legacy);
|
||||
}
|
||||
|
||||
if !object_name_change_identifiers.contains(identifier) {
|
||||
object_name_change_identifiers.push(identifier.clone());
|
||||
|
||||
if matches!(identifier.matching_strategy, Some(MatchingStrategy::Regex)) {
|
||||
let re = Regex::new(&identifier.id)?;
|
||||
regex_identifiers.insert(identifier.id.clone(), re);
|
||||
}
|
||||
}
|
||||
}
|
||||
if let Some(rules) = &mut self.object_name_change_applications {
|
||||
populate_rules(
|
||||
rules,
|
||||
&mut object_name_change_identifiers,
|
||||
&mut regex_identifiers,
|
||||
)?;
|
||||
}
|
||||
|
||||
if let Some(identifiers) = &mut self.layered_applications {
|
||||
for identifier in identifiers {
|
||||
if identifier.matching_strategy.is_none() {
|
||||
identifier.matching_strategy = Option::from(MatchingStrategy::Legacy);
|
||||
}
|
||||
|
||||
if !layered_identifiers.contains(identifier) {
|
||||
layered_identifiers.push(identifier.clone());
|
||||
|
||||
if matches!(identifier.matching_strategy, Some(MatchingStrategy::Regex)) {
|
||||
let re = Regex::new(&identifier.id)?;
|
||||
regex_identifiers.insert(identifier.id.clone(), re);
|
||||
}
|
||||
}
|
||||
}
|
||||
if let Some(rules) = &mut self.layered_applications {
|
||||
populate_rules(rules, &mut layered_identifiers, &mut regex_identifiers)?;
|
||||
}
|
||||
|
||||
if let Some(identifiers) = &mut self.border_overflow_applications {
|
||||
for identifier in identifiers {
|
||||
if identifier.matching_strategy.is_none() {
|
||||
identifier.matching_strategy = Option::from(MatchingStrategy::Legacy);
|
||||
}
|
||||
|
||||
if !border_overflow_identifiers.contains(identifier) {
|
||||
border_overflow_identifiers.push(identifier.clone());
|
||||
|
||||
if matches!(identifier.matching_strategy, Some(MatchingStrategy::Regex)) {
|
||||
let re = Regex::new(&identifier.id)?;
|
||||
regex_identifiers.insert(identifier.id.clone(), re);
|
||||
}
|
||||
}
|
||||
}
|
||||
if let Some(rules) = &mut self.border_overflow_applications {
|
||||
populate_rules(
|
||||
rules,
|
||||
&mut border_overflow_identifiers,
|
||||
&mut regex_identifiers,
|
||||
)?;
|
||||
}
|
||||
|
||||
if let Some(identifiers) = &mut self.tray_and_multi_window_applications {
|
||||
for identifier in identifiers {
|
||||
if identifier.matching_strategy.is_none() {
|
||||
identifier.matching_strategy = Option::from(MatchingStrategy::Legacy);
|
||||
if let Some(rules) = &mut self.tray_and_multi_window_applications {
|
||||
populate_rules(
|
||||
rules,
|
||||
&mut tray_and_multi_window_identifiers,
|
||||
&mut regex_identifiers,
|
||||
)?;
|
||||
}
|
||||
|
||||
if let Some(stackbar) = &self.stackbar {
|
||||
if let Some(height) = &stackbar.height {
|
||||
STACKBAR_TAB_HEIGHT.store(*height, Ordering::SeqCst);
|
||||
}
|
||||
if let Some(mode) = &stackbar.mode {
|
||||
let mut stackbar_mode = STACKBAR_MODE.lock();
|
||||
*stackbar_mode = *mode;
|
||||
}
|
||||
if let Some(tabs) = &stackbar.tabs {
|
||||
if let Some(background) = &tabs.background {
|
||||
STACKBAR_TAB_BACKGROUND_COLOUR.store((*background).into(), Ordering::SeqCst);
|
||||
}
|
||||
|
||||
if !tray_and_multi_window_identifiers.contains(identifier) {
|
||||
tray_and_multi_window_identifiers.push(identifier.clone());
|
||||
if let Some(colour) = &tabs.focused_text {
|
||||
STACKBAR_FOCUSED_TEXT_COLOUR.store((*colour).into(), Ordering::SeqCst);
|
||||
}
|
||||
|
||||
if matches!(identifier.matching_strategy, Some(MatchingStrategy::Regex)) {
|
||||
let re = Regex::new(&identifier.id)?;
|
||||
regex_identifiers.insert(identifier.id.clone(), re);
|
||||
}
|
||||
if let Some(colour) = &tabs.unfocused_text {
|
||||
STACKBAR_UNFOCUSED_TEXT_COLOUR.store((*colour).into(), Ordering::SeqCst);
|
||||
}
|
||||
|
||||
if let Some(width) = &tabs.width {
|
||||
STACKBAR_TAB_WIDTH.store(*width, Ordering::SeqCst);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -621,120 +554,48 @@ impl StaticConfig {
|
||||
let asc = ApplicationConfigurationGenerator::load(&content)?;
|
||||
|
||||
for mut entry in asc {
|
||||
if let Some(float) = entry.float_identifiers {
|
||||
for f in float {
|
||||
let mut without_comment: IdWithIdentifier = f.into();
|
||||
if without_comment.matching_strategy.is_none() {
|
||||
without_comment.matching_strategy =
|
||||
Option::from(MatchingStrategy::Legacy);
|
||||
}
|
||||
|
||||
if !float_identifiers.contains(&without_comment) {
|
||||
float_identifiers.push(without_comment.clone());
|
||||
|
||||
if matches!(
|
||||
without_comment.matching_strategy,
|
||||
Some(MatchingStrategy::Regex)
|
||||
) {
|
||||
let re = Regex::new(&without_comment.id)?;
|
||||
regex_identifiers.insert(without_comment.id.clone(), re);
|
||||
}
|
||||
}
|
||||
}
|
||||
if let Some(rules) = &mut entry.float_identifiers {
|
||||
populate_rules(rules, &mut float_identifiers, &mut regex_identifiers)?;
|
||||
}
|
||||
if let Some(options) = entry.options {
|
||||
|
||||
if let Some(ref options) = entry.options {
|
||||
let options = options.clone();
|
||||
for o in options {
|
||||
match o {
|
||||
ApplicationOptions::ObjectNameChange => {
|
||||
if entry.identifier.matching_strategy.is_none() {
|
||||
entry.identifier.matching_strategy =
|
||||
Option::from(MatchingStrategy::Legacy);
|
||||
}
|
||||
|
||||
if !object_name_change_identifiers.contains(&entry.identifier) {
|
||||
object_name_change_identifiers.push(entry.identifier.clone());
|
||||
|
||||
if matches!(
|
||||
entry.identifier.matching_strategy,
|
||||
Some(MatchingStrategy::Regex)
|
||||
) {
|
||||
let re = Regex::new(&entry.identifier.id)?;
|
||||
regex_identifiers.insert(entry.identifier.id.clone(), re);
|
||||
}
|
||||
}
|
||||
populate_option(
|
||||
&mut entry,
|
||||
&mut object_name_change_identifiers,
|
||||
&mut regex_identifiers,
|
||||
)?;
|
||||
}
|
||||
ApplicationOptions::Layered => {
|
||||
if entry.identifier.matching_strategy.is_none() {
|
||||
entry.identifier.matching_strategy =
|
||||
Option::from(MatchingStrategy::Legacy);
|
||||
}
|
||||
|
||||
if !layered_identifiers.contains(&entry.identifier) {
|
||||
layered_identifiers.push(entry.identifier.clone());
|
||||
|
||||
if matches!(
|
||||
entry.identifier.matching_strategy,
|
||||
Some(MatchingStrategy::Regex)
|
||||
) {
|
||||
let re = Regex::new(&entry.identifier.id)?;
|
||||
regex_identifiers.insert(entry.identifier.id.clone(), re);
|
||||
}
|
||||
}
|
||||
populate_option(
|
||||
&mut entry,
|
||||
&mut layered_identifiers,
|
||||
&mut regex_identifiers,
|
||||
)?;
|
||||
}
|
||||
ApplicationOptions::BorderOverflow => {
|
||||
if entry.identifier.matching_strategy.is_none() {
|
||||
entry.identifier.matching_strategy =
|
||||
Option::from(MatchingStrategy::Legacy);
|
||||
}
|
||||
|
||||
if !border_overflow_identifiers.contains(&entry.identifier) {
|
||||
border_overflow_identifiers.push(entry.identifier.clone());
|
||||
|
||||
if matches!(
|
||||
entry.identifier.matching_strategy,
|
||||
Some(MatchingStrategy::Regex)
|
||||
) {
|
||||
let re = Regex::new(&entry.identifier.id)?;
|
||||
regex_identifiers.insert(entry.identifier.id.clone(), re);
|
||||
}
|
||||
}
|
||||
populate_option(
|
||||
&mut entry,
|
||||
&mut border_overflow_identifiers,
|
||||
&mut regex_identifiers,
|
||||
)?;
|
||||
}
|
||||
ApplicationOptions::TrayAndMultiWindow => {
|
||||
if entry.identifier.matching_strategy.is_none() {
|
||||
entry.identifier.matching_strategy =
|
||||
Option::from(MatchingStrategy::Legacy);
|
||||
}
|
||||
|
||||
if !tray_and_multi_window_identifiers.contains(&entry.identifier) {
|
||||
tray_and_multi_window_identifiers
|
||||
.push(entry.identifier.clone());
|
||||
|
||||
if matches!(
|
||||
entry.identifier.matching_strategy,
|
||||
Some(MatchingStrategy::Regex)
|
||||
) {
|
||||
let re = Regex::new(&entry.identifier.id)?;
|
||||
regex_identifiers.insert(entry.identifier.id.clone(), re);
|
||||
}
|
||||
}
|
||||
populate_option(
|
||||
&mut entry,
|
||||
&mut tray_and_multi_window_identifiers,
|
||||
&mut regex_identifiers,
|
||||
)?;
|
||||
}
|
||||
ApplicationOptions::Force => {
|
||||
if entry.identifier.matching_strategy.is_none() {
|
||||
entry.identifier.matching_strategy =
|
||||
Option::from(MatchingStrategy::Legacy);
|
||||
}
|
||||
|
||||
if !manage_identifiers.contains(&entry.identifier) {
|
||||
manage_identifiers.push(entry.identifier.clone());
|
||||
|
||||
if matches!(
|
||||
entry.identifier.matching_strategy,
|
||||
Some(MatchingStrategy::Regex)
|
||||
) {
|
||||
let re = Regex::new(&entry.identifier.id)?;
|
||||
regex_identifiers.insert(entry.identifier.id.clone(), re);
|
||||
}
|
||||
}
|
||||
populate_option(
|
||||
&mut entry,
|
||||
&mut manage_identifiers,
|
||||
&mut regex_identifiers,
|
||||
)?;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -748,7 +609,7 @@ impl StaticConfig {
|
||||
#[allow(clippy::too_many_lines)]
|
||||
pub fn preload(
|
||||
path: &PathBuf,
|
||||
incoming: Arc<Mutex<Receiver<WindowManagerEvent>>>,
|
||||
incoming: Receiver<WindowManagerEvent>,
|
||||
) -> Result<WindowManager> {
|
||||
let content = std::fs::read_to_string(path)?;
|
||||
let mut value: Self = serde_json::from_str(&content)?;
|
||||
@@ -775,12 +636,6 @@ impl StaticConfig {
|
||||
incoming_events: incoming,
|
||||
command_listener: listener,
|
||||
is_paused: false,
|
||||
invisible_borders: value.invisible_borders.unwrap_or(Rect {
|
||||
left: 7,
|
||||
top: 0,
|
||||
right: 14,
|
||||
bottom: 7,
|
||||
}),
|
||||
virtual_desktop_id: current_virtual_desktop(),
|
||||
work_area_offset: value.global_work_area_offset,
|
||||
window_container_behaviour: value
|
||||
@@ -811,10 +666,10 @@ impl StaticConfig {
|
||||
|
||||
let bytes = SocketMessage::ReloadStaticConfiguration(path.clone()).as_bytes()?;
|
||||
|
||||
wm.hotwatch.watch(path, move |event| match event {
|
||||
wm.hotwatch.watch(path, move |event| match event.kind {
|
||||
// Editing in Notepad sends a NoticeWrite while editing in (Neo)Vim sends
|
||||
// a NoticeRemove, presumably because of the use of swap files?
|
||||
DebouncedEvent::NoticeWrite(_) | DebouncedEvent::NoticeRemove(_) => {
|
||||
EventKind::Modify(_) | EventKind::Remove(_) => {
|
||||
let socket = DATA_DIR.join("komorebi.sock");
|
||||
let mut stream =
|
||||
UnixStream::connect(socket).expect("could not connect to komorebi.sock");
|
||||
@@ -927,10 +782,6 @@ impl StaticConfig {
|
||||
wm.hide_border()?;
|
||||
}
|
||||
|
||||
if let Some(val) = value.invisible_borders {
|
||||
wm.invisible_borders = val;
|
||||
}
|
||||
|
||||
if let Some(val) = value.window_container_behaviour {
|
||||
wm.window_container_behaviour = val;
|
||||
}
|
||||
@@ -963,6 +814,76 @@ impl StaticConfig {
|
||||
|
||||
wm.focus_follows_mouse = value.focus_follows_mouse;
|
||||
|
||||
let monitor_count = wm.monitors().len();
|
||||
|
||||
for i in 0..monitor_count {
|
||||
wm.update_focused_workspace_by_monitor_idx(i)?;
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
fn populate_option(
|
||||
entry: &mut ApplicationConfiguration,
|
||||
identifiers: &mut Vec<MatchingRule>,
|
||||
regex_identifiers: &mut HashMap<String, Regex>,
|
||||
) -> Result<()> {
|
||||
if entry.identifier.matching_strategy.is_none() {
|
||||
entry.identifier.matching_strategy = Option::from(MatchingStrategy::Legacy);
|
||||
}
|
||||
|
||||
let rule = MatchingRule::Simple(entry.identifier.clone());
|
||||
|
||||
if !identifiers.contains(&rule) {
|
||||
identifiers.push(rule);
|
||||
|
||||
if matches!(
|
||||
entry.identifier.matching_strategy,
|
||||
Some(MatchingStrategy::Regex)
|
||||
) {
|
||||
let re = Regex::new(&entry.identifier.id)?;
|
||||
regex_identifiers.insert(entry.identifier.id.clone(), re);
|
||||
}
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn populate_rules(
|
||||
matching_rules: &mut Vec<MatchingRule>,
|
||||
identifiers: &mut Vec<MatchingRule>,
|
||||
regex_identifiers: &mut HashMap<String, Regex>,
|
||||
) -> Result<()> {
|
||||
for matching_rule in matching_rules {
|
||||
if !identifiers.contains(matching_rule) {
|
||||
match matching_rule {
|
||||
MatchingRule::Simple(simple) => {
|
||||
if simple.matching_strategy.is_none() {
|
||||
simple.matching_strategy = Option::from(MatchingStrategy::Legacy);
|
||||
}
|
||||
|
||||
if matches!(simple.matching_strategy, Some(MatchingStrategy::Regex)) {
|
||||
let re = Regex::new(&simple.id)?;
|
||||
regex_identifiers.insert(simple.id.clone(), re);
|
||||
}
|
||||
}
|
||||
MatchingRule::Composite(composite) => {
|
||||
for rule in composite {
|
||||
if rule.matching_strategy.is_none() {
|
||||
rule.matching_strategy = Option::from(MatchingStrategy::Legacy);
|
||||
}
|
||||
|
||||
if matches!(rule.matching_strategy, Some(MatchingStrategy::Regex)) {
|
||||
let re = Regex::new(&rule.id)?;
|
||||
regex_identifiers.insert(rule.id.clone(), re);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
identifiers.push(matching_rule.clone());
|
||||
}
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
@@ -4,23 +4,22 @@ use std::convert::TryFrom;
|
||||
use std::fmt::Display;
|
||||
use std::fmt::Formatter;
|
||||
use std::fmt::Write as _;
|
||||
use std::sync::atomic::Ordering;
|
||||
use std::time::Duration;
|
||||
|
||||
use color_eyre::eyre;
|
||||
use color_eyre::eyre::anyhow;
|
||||
use color_eyre::Result;
|
||||
use komorebi_core::config_generation::IdWithIdentifier;
|
||||
use komorebi_core::config_generation::MatchingRule;
|
||||
use komorebi_core::config_generation::MatchingStrategy;
|
||||
use regex::Regex;
|
||||
use schemars::JsonSchema;
|
||||
use serde::ser::Error;
|
||||
use serde::ser::SerializeStruct;
|
||||
use serde::Deserialize;
|
||||
use serde::Serialize;
|
||||
use serde::Serializer;
|
||||
use windows::Win32::Foundation::HWND;
|
||||
use winput::press;
|
||||
use winput::release;
|
||||
use winput::Vk;
|
||||
|
||||
use komorebi_core::ApplicationIdentifier;
|
||||
use komorebi_core::HidingBehaviour;
|
||||
@@ -30,8 +29,6 @@ use crate::styles::ExtendedWindowStyle;
|
||||
use crate::styles::WindowStyle;
|
||||
use crate::window_manager_event::WindowManagerEvent;
|
||||
use crate::windows_api::WindowsApi;
|
||||
use crate::ALT_FOCUS_HACK;
|
||||
use crate::BORDER_OVERFLOW_IDENTIFIERS;
|
||||
use crate::FLOAT_IDENTIFIERS;
|
||||
use crate::HIDDEN_HWNDS;
|
||||
use crate::HIDING_BEHAVIOUR;
|
||||
@@ -42,7 +39,7 @@ use crate::PERMAIGNORE_CLASSES;
|
||||
use crate::REGEX_IDENTIFIERS;
|
||||
use crate::WSL2_UI_PROCESSES;
|
||||
|
||||
#[derive(Debug, Clone, Copy, JsonSchema)]
|
||||
#[derive(Debug, Clone, Copy, Deserialize, JsonSchema)]
|
||||
pub struct Window {
|
||||
pub(crate) hwnd: isize,
|
||||
}
|
||||
@@ -128,7 +125,7 @@ impl Window {
|
||||
HWND(self.hwnd)
|
||||
}
|
||||
|
||||
pub fn center(&mut self, work_area: &Rect, invisible_borders: &Rect) -> Result<()> {
|
||||
pub fn center(&mut self, work_area: &Rect) -> Result<()> {
|
||||
let half_width = work_area.right / 2;
|
||||
let half_weight = work_area.bottom / 2;
|
||||
|
||||
@@ -139,45 +136,23 @@ impl Window {
|
||||
right: half_width,
|
||||
bottom: half_weight,
|
||||
},
|
||||
invisible_borders,
|
||||
true,
|
||||
)
|
||||
}
|
||||
|
||||
pub fn set_position(
|
||||
&mut self,
|
||||
layout: &Rect,
|
||||
invisible_borders: &Rect,
|
||||
top: bool,
|
||||
) -> Result<()> {
|
||||
let mut rect = *layout;
|
||||
|
||||
let border_overflows = BORDER_OVERFLOW_IDENTIFIERS.lock();
|
||||
let regex_identifiers = REGEX_IDENTIFIERS.lock();
|
||||
|
||||
let title = &self.title()?;
|
||||
let class = &self.class()?;
|
||||
let exe_name = &self.exe()?;
|
||||
|
||||
let should_remove_border = !should_act(
|
||||
title,
|
||||
exe_name,
|
||||
class,
|
||||
&border_overflows,
|
||||
®ex_identifiers,
|
||||
);
|
||||
|
||||
if should_remove_border {
|
||||
// Remove the invisible borders
|
||||
rect.left -= invisible_borders.left;
|
||||
rect.top -= invisible_borders.top;
|
||||
rect.right += invisible_borders.right;
|
||||
rect.bottom += invisible_borders.bottom;
|
||||
}
|
||||
|
||||
pub fn set_position(&mut self, layout: &Rect, top: bool) -> Result<()> {
|
||||
let rect = *layout;
|
||||
WindowsApi::position_window(self.hwnd(), &rect, top)
|
||||
}
|
||||
|
||||
pub fn is_maximized(self) -> bool {
|
||||
WindowsApi::is_zoomed(self.hwnd())
|
||||
}
|
||||
|
||||
pub fn is_miminized(self) -> bool {
|
||||
WindowsApi::is_iconic(self.hwnd())
|
||||
}
|
||||
|
||||
pub fn hide(self) {
|
||||
let mut programmatically_hidden_hwnds = HIDDEN_HWNDS.lock();
|
||||
if !programmatically_hidden_hwnds.contains(&self.hwnd) {
|
||||
@@ -242,57 +217,6 @@ impl Window {
|
||||
WindowsApi::unmaximize_window(self.hwnd());
|
||||
}
|
||||
|
||||
pub fn raise(self) {
|
||||
// Attach komorebi thread to Window thread
|
||||
let (_, window_thread_id) = WindowsApi::window_thread_process_id(self.hwnd());
|
||||
let current_thread_id = WindowsApi::current_thread_id();
|
||||
|
||||
// This can be allowed to fail if a window doesn't have a message queue or if a journal record
|
||||
// hook has been installed
|
||||
// https://docs.microsoft.com/en-us/windows/win32/api/winuser/nf-winuser-attachthreadinput#remarks
|
||||
match WindowsApi::attach_thread_input(current_thread_id, window_thread_id, true) {
|
||||
Ok(()) => {}
|
||||
Err(error) => {
|
||||
tracing::error!(
|
||||
"could not attach to window thread input processing mechanism, but continuing execution of raise(): {}",
|
||||
error
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
// Raise Window to foreground
|
||||
match WindowsApi::set_foreground_window(self.hwnd()) {
|
||||
Ok(()) => {}
|
||||
Err(error) => {
|
||||
tracing::error!(
|
||||
"could not set as foreground window, but continuing execution of raise(): {}",
|
||||
error
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
// This isn't really needed when the above command works as expected via AHK
|
||||
match WindowsApi::set_focus(self.hwnd()) {
|
||||
Ok(()) => {}
|
||||
Err(error) => {
|
||||
tracing::error!(
|
||||
"could not set focus, but continuing execution of raise(): {}",
|
||||
error
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
match WindowsApi::attach_thread_input(current_thread_id, window_thread_id, false) {
|
||||
Ok(()) => {}
|
||||
Err(error) => {
|
||||
tracing::error!(
|
||||
"could not detach from window thread input processing mechanism, but continuing execution of raise(): {}",
|
||||
error
|
||||
);
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
pub fn focus(self, mouse_follows_focus: bool) -> Result<()> {
|
||||
// Attach komorebi thread to Window thread
|
||||
let (_, window_thread_id) = WindowsApi::window_thread_process_id(self.hwnd());
|
||||
@@ -316,12 +240,7 @@ impl Window {
|
||||
let mut tried_resetting_foreground_access = false;
|
||||
let mut max_attempts = 10;
|
||||
|
||||
let hotkey_uses_alt = WindowsApi::alt_is_pressed();
|
||||
while !foregrounded && max_attempts > 0 {
|
||||
if ALT_FOCUS_HACK.load(Ordering::SeqCst) {
|
||||
press(Vk::Alt);
|
||||
}
|
||||
|
||||
match WindowsApi::set_foreground_window(self.hwnd()) {
|
||||
Ok(()) => {
|
||||
foregrounded = true;
|
||||
@@ -342,10 +261,6 @@ impl Window {
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
if ALT_FOCUS_HACK.load(Ordering::SeqCst) && !hotkey_uses_alt {
|
||||
release(Vk::Alt);
|
||||
}
|
||||
}
|
||||
|
||||
// Center cursor in Window
|
||||
@@ -413,6 +328,14 @@ impl Window {
|
||||
WindowsApi::window_text_w(self.hwnd())
|
||||
}
|
||||
|
||||
pub fn path(self) -> Result<String> {
|
||||
let (process_id, _) = WindowsApi::window_thread_process_id(self.hwnd());
|
||||
let handle = WindowsApi::process_handle(process_id)?;
|
||||
let path = WindowsApi::exe_path(handle);
|
||||
WindowsApi::close_process(handle)?;
|
||||
path
|
||||
}
|
||||
|
||||
pub fn exe(self) -> Result<String> {
|
||||
let (process_id, _) = WindowsApi::window_thread_process_id(self.hwnd());
|
||||
let handle = WindowsApi::process_handle(process_id)?;
|
||||
@@ -476,8 +399,8 @@ impl Window {
|
||||
(true, _) |
|
||||
// If not allowing cloaked windows, we need to ensure the window is not cloaked
|
||||
(false, false) => {
|
||||
if let (Ok(title), Ok(exe_name), Ok(class)) = (self.title(), self.exe(), self.class()) {
|
||||
return Ok(window_is_eligible(&title, &exe_name, &class, &self.style()?, &self.ex_style()?, event));
|
||||
if let (Ok(title), Ok(exe_name), Ok(class), Ok(path)) = (self.title(), self.exe(), self.class(), self.path()) {
|
||||
return Ok(window_is_eligible(&title, &exe_name, &class, &path, &self.style()?, &self.ex_style()?, event));
|
||||
}
|
||||
}
|
||||
_ => {}
|
||||
@@ -491,6 +414,7 @@ fn window_is_eligible(
|
||||
title: &String,
|
||||
exe_name: &String,
|
||||
class: &String,
|
||||
path: &str,
|
||||
style: &WindowStyle,
|
||||
ex_style: &ExtendedWindowStyle,
|
||||
event: Option<WindowManagerEvent>,
|
||||
@@ -509,6 +433,7 @@ fn window_is_eligible(
|
||||
title,
|
||||
exe_name,
|
||||
class,
|
||||
path,
|
||||
&float_identifiers,
|
||||
®ex_identifiers,
|
||||
);
|
||||
@@ -518,6 +443,7 @@ fn window_is_eligible(
|
||||
title,
|
||||
exe_name,
|
||||
class,
|
||||
path,
|
||||
&manage_identifiers,
|
||||
®ex_identifiers,
|
||||
);
|
||||
@@ -531,6 +457,7 @@ fn window_is_eligible(
|
||||
title,
|
||||
exe_name,
|
||||
class,
|
||||
path,
|
||||
&layered_whitelist,
|
||||
®ex_identifiers,
|
||||
);
|
||||
@@ -548,6 +475,10 @@ fn window_is_eligible(
|
||||
titlebars_removed.contains(exe_name)
|
||||
};
|
||||
|
||||
if exe_name.contains("firefox") {
|
||||
std::thread::sleep(Duration::from_millis(10));
|
||||
}
|
||||
|
||||
if (allow_wsl2_gui || allow_titlebar_removed || style.contains(WindowStyle::CAPTION) && ex_style.contains(ExtendedWindowStyle::WINDOWEDGE))
|
||||
&& !ex_style.contains(ExtendedWindowStyle::DLGMODALFRAME)
|
||||
// Get a lot of dupe events coming through that make the redrawing go crazy
|
||||
@@ -558,8 +489,13 @@ fn window_is_eligible(
|
||||
|| managed_override
|
||||
{
|
||||
return true;
|
||||
} else if event.is_some() {
|
||||
tracing::debug!("ignoring (exe: {}, title: {})", exe_name, title);
|
||||
} else if let Some(event) = event {
|
||||
tracing::debug!(
|
||||
"ignoring (exe: {}, title: {}, event: {})",
|
||||
exe_name,
|
||||
title,
|
||||
event
|
||||
);
|
||||
}
|
||||
|
||||
false
|
||||
@@ -570,125 +506,291 @@ pub fn should_act(
|
||||
title: &str,
|
||||
exe_name: &str,
|
||||
class: &str,
|
||||
identifiers: &[IdWithIdentifier],
|
||||
path: &str,
|
||||
identifiers: &[MatchingRule],
|
||||
regex_identifiers: &HashMap<String, Regex>,
|
||||
) -> bool {
|
||||
let mut should_act = false;
|
||||
for identifier in identifiers {
|
||||
match identifier.matching_strategy {
|
||||
None => {
|
||||
panic!("there is no matching strategy identified for this rule");
|
||||
match identifier {
|
||||
MatchingRule::Simple(identifier) => {
|
||||
if should_act_individual(
|
||||
title,
|
||||
exe_name,
|
||||
class,
|
||||
path,
|
||||
identifier,
|
||||
regex_identifiers,
|
||||
) {
|
||||
should_act = true
|
||||
};
|
||||
}
|
||||
Some(MatchingStrategy::Legacy) => match identifier.kind {
|
||||
ApplicationIdentifier::Title => {
|
||||
if title.starts_with(&identifier.id) || title.ends_with(&identifier.id) {
|
||||
should_act = true;
|
||||
}
|
||||
MatchingRule::Composite(identifiers) => {
|
||||
let mut composite_results = vec![];
|
||||
for identifier in identifiers {
|
||||
composite_results.push(should_act_individual(
|
||||
title,
|
||||
exe_name,
|
||||
class,
|
||||
path,
|
||||
identifier,
|
||||
regex_identifiers,
|
||||
));
|
||||
}
|
||||
ApplicationIdentifier::Class => {
|
||||
if class.starts_with(&identifier.id) || class.ends_with(&identifier.id) {
|
||||
should_act = true;
|
||||
}
|
||||
|
||||
if composite_results.iter().all(|&x| x) {
|
||||
should_act = true;
|
||||
}
|
||||
ApplicationIdentifier::Exe => {
|
||||
if exe_name.eq(&identifier.id) {
|
||||
should_act = true;
|
||||
}
|
||||
}
|
||||
},
|
||||
Some(MatchingStrategy::Equals) => match identifier.kind {
|
||||
ApplicationIdentifier::Title => {
|
||||
if title.eq(&identifier.id) {
|
||||
should_act = true;
|
||||
}
|
||||
}
|
||||
ApplicationIdentifier::Class => {
|
||||
if class.eq(&identifier.id) {
|
||||
should_act = true;
|
||||
}
|
||||
}
|
||||
ApplicationIdentifier::Exe => {
|
||||
if exe_name.eq(&identifier.id) {
|
||||
should_act = true;
|
||||
}
|
||||
}
|
||||
},
|
||||
Some(MatchingStrategy::StartsWith) => match identifier.kind {
|
||||
ApplicationIdentifier::Title => {
|
||||
if title.starts_with(&identifier.id) {
|
||||
should_act = true;
|
||||
}
|
||||
}
|
||||
ApplicationIdentifier::Class => {
|
||||
if class.starts_with(&identifier.id) {
|
||||
should_act = true;
|
||||
}
|
||||
}
|
||||
ApplicationIdentifier::Exe => {
|
||||
if exe_name.starts_with(&identifier.id) {
|
||||
should_act = true;
|
||||
}
|
||||
}
|
||||
},
|
||||
Some(MatchingStrategy::EndsWith) => match identifier.kind {
|
||||
ApplicationIdentifier::Title => {
|
||||
if title.ends_with(&identifier.id) {
|
||||
should_act = true;
|
||||
}
|
||||
}
|
||||
ApplicationIdentifier::Class => {
|
||||
if class.ends_with(&identifier.id) {
|
||||
should_act = true;
|
||||
}
|
||||
}
|
||||
ApplicationIdentifier::Exe => {
|
||||
if exe_name.ends_with(&identifier.id) {
|
||||
should_act = true;
|
||||
}
|
||||
}
|
||||
},
|
||||
Some(MatchingStrategy::Contains) => match identifier.kind {
|
||||
ApplicationIdentifier::Title => {
|
||||
if title.contains(&identifier.id) {
|
||||
should_act = true;
|
||||
}
|
||||
}
|
||||
ApplicationIdentifier::Class => {
|
||||
if class.contains(&identifier.id) {
|
||||
should_act = true;
|
||||
}
|
||||
}
|
||||
ApplicationIdentifier::Exe => {
|
||||
if exe_name.contains(&identifier.id) {
|
||||
should_act = true;
|
||||
}
|
||||
}
|
||||
},
|
||||
Some(MatchingStrategy::Regex) => match identifier.kind {
|
||||
ApplicationIdentifier::Title => {
|
||||
if let Some(re) = regex_identifiers.get(&identifier.id) {
|
||||
if re.is_match(title) {
|
||||
should_act = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
ApplicationIdentifier::Class => {
|
||||
if let Some(re) = regex_identifiers.get(&identifier.id) {
|
||||
if re.is_match(class) {
|
||||
should_act = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
ApplicationIdentifier::Exe => {
|
||||
if let Some(re) = regex_identifiers.get(&identifier.id) {
|
||||
if re.is_match(exe_name) {
|
||||
should_act = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
should_act
|
||||
}
|
||||
|
||||
pub fn should_act_individual(
|
||||
title: &str,
|
||||
exe_name: &str,
|
||||
class: &str,
|
||||
path: &str,
|
||||
identifier: &IdWithIdentifier,
|
||||
regex_identifiers: &HashMap<String, Regex>,
|
||||
) -> bool {
|
||||
let mut should_act = false;
|
||||
|
||||
match identifier.matching_strategy {
|
||||
None => {
|
||||
panic!("there is no matching strategy identified for this rule");
|
||||
}
|
||||
Some(MatchingStrategy::Legacy) => match identifier.kind {
|
||||
ApplicationIdentifier::Title => {
|
||||
if title.starts_with(&identifier.id) || title.ends_with(&identifier.id) {
|
||||
should_act = true;
|
||||
}
|
||||
}
|
||||
ApplicationIdentifier::Class => {
|
||||
if class.starts_with(&identifier.id) || class.ends_with(&identifier.id) {
|
||||
should_act = true;
|
||||
}
|
||||
}
|
||||
ApplicationIdentifier::Exe => {
|
||||
if exe_name.eq(&identifier.id) {
|
||||
should_act = true;
|
||||
}
|
||||
}
|
||||
ApplicationIdentifier::Path => {
|
||||
if path.eq(&identifier.id) {
|
||||
should_act = true;
|
||||
}
|
||||
}
|
||||
},
|
||||
Some(MatchingStrategy::Equals) => match identifier.kind {
|
||||
ApplicationIdentifier::Title => {
|
||||
if title.eq(&identifier.id) {
|
||||
should_act = true;
|
||||
}
|
||||
}
|
||||
ApplicationIdentifier::Class => {
|
||||
if class.eq(&identifier.id) {
|
||||
should_act = true;
|
||||
}
|
||||
}
|
||||
ApplicationIdentifier::Exe => {
|
||||
if exe_name.eq(&identifier.id) {
|
||||
should_act = true;
|
||||
}
|
||||
}
|
||||
ApplicationIdentifier::Path => {
|
||||
if path.eq(&identifier.id) {
|
||||
should_act = true;
|
||||
}
|
||||
}
|
||||
},
|
||||
Some(MatchingStrategy::DoesNotEqual) => match identifier.kind {
|
||||
ApplicationIdentifier::Title => {
|
||||
if !title.eq(&identifier.id) {
|
||||
should_act = true;
|
||||
}
|
||||
}
|
||||
ApplicationIdentifier::Class => {
|
||||
if !class.eq(&identifier.id) {
|
||||
should_act = true;
|
||||
}
|
||||
}
|
||||
ApplicationIdentifier::Exe => {
|
||||
if !exe_name.eq(&identifier.id) {
|
||||
should_act = true;
|
||||
}
|
||||
}
|
||||
ApplicationIdentifier::Path => {
|
||||
if !path.eq(&identifier.id) {
|
||||
should_act = true;
|
||||
}
|
||||
}
|
||||
},
|
||||
Some(MatchingStrategy::StartsWith) => match identifier.kind {
|
||||
ApplicationIdentifier::Title => {
|
||||
if title.starts_with(&identifier.id) {
|
||||
should_act = true;
|
||||
}
|
||||
}
|
||||
ApplicationIdentifier::Class => {
|
||||
if class.starts_with(&identifier.id) {
|
||||
should_act = true;
|
||||
}
|
||||
}
|
||||
ApplicationIdentifier::Exe => {
|
||||
if exe_name.starts_with(&identifier.id) {
|
||||
should_act = true;
|
||||
}
|
||||
}
|
||||
ApplicationIdentifier::Path => {
|
||||
if path.starts_with(&identifier.id) {
|
||||
should_act = true;
|
||||
}
|
||||
}
|
||||
},
|
||||
Some(MatchingStrategy::DoesNotStartWith) => match identifier.kind {
|
||||
ApplicationIdentifier::Title => {
|
||||
if !title.starts_with(&identifier.id) {
|
||||
should_act = true;
|
||||
}
|
||||
}
|
||||
ApplicationIdentifier::Class => {
|
||||
if !class.starts_with(&identifier.id) {
|
||||
should_act = true;
|
||||
}
|
||||
}
|
||||
ApplicationIdentifier::Exe => {
|
||||
if !exe_name.starts_with(&identifier.id) {
|
||||
should_act = true;
|
||||
}
|
||||
}
|
||||
ApplicationIdentifier::Path => {
|
||||
if !path.starts_with(&identifier.id) {
|
||||
should_act = true;
|
||||
}
|
||||
}
|
||||
},
|
||||
Some(MatchingStrategy::EndsWith) => match identifier.kind {
|
||||
ApplicationIdentifier::Title => {
|
||||
if title.ends_with(&identifier.id) {
|
||||
should_act = true;
|
||||
}
|
||||
}
|
||||
ApplicationIdentifier::Class => {
|
||||
if class.ends_with(&identifier.id) {
|
||||
should_act = true;
|
||||
}
|
||||
}
|
||||
ApplicationIdentifier::Exe => {
|
||||
if exe_name.ends_with(&identifier.id) {
|
||||
should_act = true;
|
||||
}
|
||||
}
|
||||
ApplicationIdentifier::Path => {
|
||||
if path.ends_with(&identifier.id) {
|
||||
should_act = true;
|
||||
}
|
||||
}
|
||||
},
|
||||
Some(MatchingStrategy::DoesNotEndWith) => match identifier.kind {
|
||||
ApplicationIdentifier::Title => {
|
||||
if !title.ends_with(&identifier.id) {
|
||||
should_act = true;
|
||||
}
|
||||
}
|
||||
ApplicationIdentifier::Class => {
|
||||
if !class.ends_with(&identifier.id) {
|
||||
should_act = true;
|
||||
}
|
||||
}
|
||||
ApplicationIdentifier::Exe => {
|
||||
if !exe_name.ends_with(&identifier.id) {
|
||||
should_act = true;
|
||||
}
|
||||
}
|
||||
ApplicationIdentifier::Path => {
|
||||
if !path.ends_with(&identifier.id) {
|
||||
should_act = true;
|
||||
}
|
||||
}
|
||||
},
|
||||
Some(MatchingStrategy::Contains) => match identifier.kind {
|
||||
ApplicationIdentifier::Title => {
|
||||
if title.contains(&identifier.id) {
|
||||
should_act = true;
|
||||
}
|
||||
}
|
||||
ApplicationIdentifier::Class => {
|
||||
if class.contains(&identifier.id) {
|
||||
should_act = true;
|
||||
}
|
||||
}
|
||||
ApplicationIdentifier::Exe => {
|
||||
if exe_name.contains(&identifier.id) {
|
||||
should_act = true;
|
||||
}
|
||||
}
|
||||
ApplicationIdentifier::Path => {
|
||||
if path.contains(&identifier.id) {
|
||||
should_act = true;
|
||||
}
|
||||
}
|
||||
},
|
||||
Some(MatchingStrategy::DoesNotContain) => match identifier.kind {
|
||||
ApplicationIdentifier::Title => {
|
||||
if !title.contains(&identifier.id) {
|
||||
should_act = true;
|
||||
}
|
||||
}
|
||||
ApplicationIdentifier::Class => {
|
||||
if !class.contains(&identifier.id) {
|
||||
should_act = true;
|
||||
}
|
||||
}
|
||||
ApplicationIdentifier::Exe => {
|
||||
if !exe_name.contains(&identifier.id) {
|
||||
should_act = true;
|
||||
}
|
||||
}
|
||||
ApplicationIdentifier::Path => {
|
||||
if !path.contains(&identifier.id) {
|
||||
should_act = true;
|
||||
}
|
||||
}
|
||||
},
|
||||
Some(MatchingStrategy::Regex) => match identifier.kind {
|
||||
ApplicationIdentifier::Title => {
|
||||
if let Some(re) = regex_identifiers.get(&identifier.id) {
|
||||
if re.is_match(title) {
|
||||
should_act = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
ApplicationIdentifier::Class => {
|
||||
if let Some(re) = regex_identifiers.get(&identifier.id) {
|
||||
if re.is_match(class) {
|
||||
should_act = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
ApplicationIdentifier::Exe => {
|
||||
if let Some(re) = regex_identifiers.get(&identifier.id) {
|
||||
if re.is_match(exe_name) {
|
||||
should_act = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
ApplicationIdentifier::Path => {
|
||||
if let Some(re) = regex_identifiers.get(&identifier.id) {
|
||||
if re.is_match(path) {
|
||||
should_act = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
}
|
||||
|
||||
should_act
|
||||
}
|
||||
|
||||
@@ -12,14 +12,16 @@ use color_eyre::eyre::anyhow;
|
||||
use color_eyre::eyre::bail;
|
||||
use color_eyre::Result;
|
||||
use crossbeam_channel::Receiver;
|
||||
use hotwatch::notify::DebouncedEvent;
|
||||
use hotwatch::notify::ErrorKind as NotifyErrorKind;
|
||||
use hotwatch::EventKind;
|
||||
use hotwatch::Hotwatch;
|
||||
use parking_lot::Mutex;
|
||||
use schemars::JsonSchema;
|
||||
use serde::Deserialize;
|
||||
use serde::Serialize;
|
||||
use uds_windows::UnixListener;
|
||||
|
||||
use komorebi_core::config_generation::IdWithIdentifier;
|
||||
use komorebi_core::config_generation::MatchingRule;
|
||||
use komorebi_core::custom_layout::CustomLayout;
|
||||
use komorebi_core::Arrangement;
|
||||
use komorebi_core::Axis;
|
||||
@@ -44,7 +46,7 @@ use crate::static_config::StaticConfig;
|
||||
use crate::window::Window;
|
||||
use crate::window_manager_event::WindowManagerEvent;
|
||||
use crate::windows_api::WindowsApi;
|
||||
use crate::winevent_listener::WINEVENT_CALLBACK_CHANNEL;
|
||||
use crate::winevent_listener;
|
||||
use crate::workspace::Workspace;
|
||||
use crate::BORDER_HWND;
|
||||
use crate::BORDER_OVERFLOW_IDENTIFIERS;
|
||||
@@ -65,10 +67,9 @@ use crate::WORKSPACE_RULES;
|
||||
pub struct WindowManager {
|
||||
pub monitors: Ring<Monitor>,
|
||||
pub monitor_cache: HashMap<usize, Monitor>,
|
||||
pub incoming_events: Arc<Mutex<Receiver<WindowManagerEvent>>>,
|
||||
pub incoming_events: Receiver<WindowManagerEvent>,
|
||||
pub command_listener: UnixListener,
|
||||
pub is_paused: bool,
|
||||
pub invisible_borders: Rect,
|
||||
pub work_area_offset: Option<Rect>,
|
||||
pub resize_delta: i32,
|
||||
pub window_container_behaviour: WindowContainerBehaviour,
|
||||
@@ -84,11 +85,10 @@ pub struct WindowManager {
|
||||
}
|
||||
|
||||
#[allow(clippy::struct_excessive_bools)]
|
||||
#[derive(Debug, Serialize, JsonSchema)]
|
||||
#[derive(Debug, Serialize, Deserialize, JsonSchema)]
|
||||
pub struct State {
|
||||
pub monitors: Ring<Monitor>,
|
||||
pub is_paused: bool,
|
||||
pub invisible_borders: Rect,
|
||||
pub resize_delta: i32,
|
||||
pub new_window_behaviour: WindowContainerBehaviour,
|
||||
pub cross_monitor_move_behaviour: MoveBehaviour,
|
||||
@@ -97,12 +97,12 @@ pub struct State {
|
||||
pub mouse_follows_focus: bool,
|
||||
pub has_pending_raise_op: bool,
|
||||
pub remove_titlebars: bool,
|
||||
pub float_identifiers: Vec<IdWithIdentifier>,
|
||||
pub manage_identifiers: Vec<IdWithIdentifier>,
|
||||
pub layered_whitelist: Vec<IdWithIdentifier>,
|
||||
pub tray_and_multi_window_identifiers: Vec<IdWithIdentifier>,
|
||||
pub border_overflow_identifiers: Vec<IdWithIdentifier>,
|
||||
pub name_change_on_launch_identifiers: Vec<IdWithIdentifier>,
|
||||
pub float_identifiers: Vec<MatchingRule>,
|
||||
pub manage_identifiers: Vec<MatchingRule>,
|
||||
pub layered_whitelist: Vec<MatchingRule>,
|
||||
pub tray_and_multi_window_identifiers: Vec<MatchingRule>,
|
||||
pub border_overflow_identifiers: Vec<MatchingRule>,
|
||||
pub name_change_on_launch_identifiers: Vec<MatchingRule>,
|
||||
pub monitor_index_preferences: HashMap<usize, Rect>,
|
||||
pub display_index_preferences: HashMap<usize, String>,
|
||||
}
|
||||
@@ -118,7 +118,6 @@ impl From<&WindowManager> for State {
|
||||
Self {
|
||||
monitors: wm.monitors.clone(),
|
||||
is_paused: wm.is_paused,
|
||||
invisible_borders: wm.invisible_borders,
|
||||
work_area_offset: wm.work_area_offset,
|
||||
resize_delta: wm.resize_delta,
|
||||
new_window_behaviour: wm.window_container_behaviour,
|
||||
@@ -167,7 +166,7 @@ impl EnforceWorkspaceRuleOp {
|
||||
|
||||
impl WindowManager {
|
||||
#[tracing::instrument]
|
||||
pub fn new(incoming: Arc<Mutex<Receiver<WindowManagerEvent>>>) -> Result<Self> {
|
||||
pub fn new(incoming: Receiver<WindowManagerEvent>) -> Result<Self> {
|
||||
let socket = DATA_DIR.join("komorebi.sock");
|
||||
|
||||
match std::fs::remove_file(&socket) {
|
||||
@@ -189,12 +188,6 @@ impl WindowManager {
|
||||
incoming_events: incoming,
|
||||
command_listener: listener,
|
||||
is_paused: false,
|
||||
invisible_borders: Rect {
|
||||
left: 7,
|
||||
top: 0,
|
||||
right: 14,
|
||||
bottom: 7,
|
||||
},
|
||||
virtual_desktop_id: current_virtual_desktop(),
|
||||
work_area_offset: None,
|
||||
window_container_behaviour: WindowContainerBehaviour::Create,
|
||||
@@ -219,15 +212,16 @@ impl WindowManager {
|
||||
|
||||
#[tracing::instrument(skip(self))]
|
||||
pub fn show_border(&self) -> Result<()> {
|
||||
let foreground = WindowsApi::foreground_window()?;
|
||||
let foreground_window = Window { hwnd: foreground };
|
||||
let mut rect = WindowsApi::window_rect(foreground_window.hwnd())?;
|
||||
rect.top -= self.invisible_borders.bottom;
|
||||
rect.bottom += self.invisible_borders.bottom;
|
||||
if self.focused_container().is_ok() {
|
||||
let foreground = WindowsApi::foreground_window()?;
|
||||
let foreground_window = Window { hwnd: foreground };
|
||||
|
||||
let border = Border::from(BORDER_HWND.load(Ordering::SeqCst));
|
||||
border.set_position(foreground_window, &self.invisible_borders, true)?;
|
||||
WindowsApi::invalidate_border_rect()
|
||||
let border = Border::from(BORDER_HWND.load(Ordering::SeqCst));
|
||||
border.set_position(foreground_window, true)?;
|
||||
WindowsApi::invalidate_border_rect()?;
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[tracing::instrument(skip(self))]
|
||||
@@ -272,18 +266,18 @@ impl WindowManager {
|
||||
match self.hotwatch.unwatch(&config) {
|
||||
Ok(()) => {}
|
||||
Err(error) => match error {
|
||||
hotwatch::Error::Notify(error) => match error {
|
||||
hotwatch::notify::Error::WatchNotFound => {}
|
||||
error => return Err(error.into()),
|
||||
hotwatch::Error::Notify(ref notify_error) => match notify_error.kind {
|
||||
NotifyErrorKind::WatchNotFound => {}
|
||||
_ => return Err(error.into()),
|
||||
},
|
||||
error @ hotwatch::Error::Io(_) => return Err(error.into()),
|
||||
},
|
||||
}
|
||||
|
||||
self.hotwatch.watch(config, |event| match event {
|
||||
self.hotwatch.watch(config, |event| match event.kind {
|
||||
// Editing in Notepad sends a NoticeWrite while editing in (Neo)Vim sends
|
||||
// a NoticeRemove, presumably because of the use of swap files?
|
||||
DebouncedEvent::NoticeWrite(_) | DebouncedEvent::NoticeRemove(_) => {
|
||||
EventKind::Modify(_) | EventKind::Remove(_) => {
|
||||
std::thread::spawn(|| {
|
||||
load_configuration().expect("could not load configuration");
|
||||
});
|
||||
@@ -402,7 +396,6 @@ impl WindowManager {
|
||||
}
|
||||
}
|
||||
|
||||
let invisible_borders = self.invisible_borders;
|
||||
let offset = self.work_area_offset;
|
||||
|
||||
for monitor in self.monitors_mut() {
|
||||
@@ -433,7 +426,7 @@ impl WindowManager {
|
||||
}
|
||||
|
||||
if should_update {
|
||||
monitor.update_focused_workspace(offset, &invisible_borders)?;
|
||||
monitor.update_focused_workspace(offset)?;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -635,7 +628,6 @@ impl WindowManager {
|
||||
|
||||
#[tracing::instrument(skip(self))]
|
||||
pub fn retile_all(&mut self, preserve_resize_dimensions: bool) -> Result<()> {
|
||||
let invisible_borders = self.invisible_borders;
|
||||
let offset = self.work_area_offset;
|
||||
|
||||
for monitor in self.monitors_mut() {
|
||||
@@ -657,7 +649,7 @@ impl WindowManager {
|
||||
}
|
||||
}
|
||||
|
||||
workspace.update(&work_area, offset, &invisible_borders)?;
|
||||
workspace.update(&work_area, offset)?;
|
||||
}
|
||||
|
||||
Ok(())
|
||||
@@ -667,75 +659,56 @@ impl WindowManager {
|
||||
pub fn manage_focused_window(&mut self) -> Result<()> {
|
||||
let hwnd = WindowsApi::foreground_window()?;
|
||||
let event = WindowManagerEvent::Manage(Window { hwnd });
|
||||
Ok(WINEVENT_CALLBACK_CHANNEL.lock().0.send(event)?)
|
||||
Ok(winevent_listener::event_tx().send(event)?)
|
||||
}
|
||||
|
||||
#[tracing::instrument(skip(self))]
|
||||
pub fn unmanage_focused_window(&mut self) -> Result<()> {
|
||||
let hwnd = WindowsApi::foreground_window()?;
|
||||
let event = WindowManagerEvent::Unmanage(Window { hwnd });
|
||||
Ok(WINEVENT_CALLBACK_CHANNEL.lock().0.send(event)?)
|
||||
Ok(winevent_listener::event_tx().send(event)?)
|
||||
}
|
||||
|
||||
#[tracing::instrument(skip(self))]
|
||||
pub fn raise_window_at_cursor_pos(&mut self) -> Result<()> {
|
||||
let mut hwnd = WindowsApi::window_at_cursor_pos()?;
|
||||
let mut hwnd = None;
|
||||
|
||||
if self.has_pending_raise_op
|
||||
|| self.focused_window()?.hwnd == hwnd
|
||||
// Sometimes we need this check, because the focus may have been given by a click
|
||||
// to a non-window such as the taskbar or system tray, and komorebi doesn't know that
|
||||
// the focused window of the workspace is not actually focused by the OS at that point
|
||||
|| WindowsApi::foreground_window()? == hwnd
|
||||
{
|
||||
Ok(())
|
||||
} else {
|
||||
let mut known_hwnd = false;
|
||||
for monitor in self.monitors() {
|
||||
for workspace in monitor.workspaces() {
|
||||
if workspace.contains_window(hwnd) {
|
||||
known_hwnd = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// TODO: Not sure if this needs to be made configurable just yet...
|
||||
let overlay_classes = [
|
||||
// Chromium/Electron
|
||||
"Chrome_RenderWidgetHostHWND".to_string(),
|
||||
// Explorer
|
||||
"DirectUIHWND".to_string(),
|
||||
"SysTreeView32".to_string(),
|
||||
"ToolbarWindow32".to_string(),
|
||||
"NetUIHWND".to_string(),
|
||||
];
|
||||
|
||||
if !known_hwnd {
|
||||
let class = Window { hwnd }.class()?;
|
||||
// Some applications (Electron/Chromium-based, explorer) have (invisible?) overlays
|
||||
// windows that we need to look beyond to find the actual window to raise
|
||||
if overlay_classes.contains(&class) {
|
||||
for monitor in self.monitors() {
|
||||
for workspace in monitor.workspaces() {
|
||||
if let Some(exe_hwnd) = workspace.hwnd_from_exe(&Window { hwnd }.exe()?)
|
||||
{
|
||||
hwnd = exe_hwnd;
|
||||
known_hwnd = true;
|
||||
}
|
||||
for monitor in self.monitors() {
|
||||
for workspace in monitor.workspaces() {
|
||||
if let Some(container_idx) = workspace.container_idx_from_current_point() {
|
||||
if let Some(container) = workspace.containers().get(container_idx) {
|
||||
if let Some(window) = container.focused_window() {
|
||||
hwnd = Some(window.hwnd);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if known_hwnd {
|
||||
let event = WindowManagerEvent::Raise(Window { hwnd });
|
||||
self.has_pending_raise_op = true;
|
||||
Ok(WINEVENT_CALLBACK_CHANNEL.lock().0.send(event)?)
|
||||
} else {
|
||||
tracing::debug!("not raising unknown window: {}", Window { hwnd });
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
if let Some(hwnd) = hwnd {
|
||||
if self.has_pending_raise_op
|
||||
|| self.focused_window()?.hwnd == hwnd
|
||||
// Sometimes we need this check, because the focus may have been given by a click
|
||||
// to a non-window such as the taskbar or system tray, and komorebi doesn't know that
|
||||
// the focused window of the workspace is not actually focused by the OS at that point
|
||||
|| WindowsApi::foreground_window()? == hwnd
|
||||
{
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
let event = WindowManagerEvent::Raise(Window { hwnd });
|
||||
self.has_pending_raise_op = true;
|
||||
winevent_listener::event_tx().send(event)?;
|
||||
} else {
|
||||
tracing::debug!(
|
||||
"not raising unknown window: {}",
|
||||
Window {
|
||||
hwnd: WindowsApi::window_at_cursor_pos()?
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[tracing::instrument(skip(self))]
|
||||
@@ -829,12 +802,11 @@ impl WindowManager {
|
||||
pub fn update_focused_workspace(&mut self, follow_focus: bool) -> Result<()> {
|
||||
tracing::info!("updating");
|
||||
|
||||
let invisible_borders = self.invisible_borders;
|
||||
let offset = self.work_area_offset;
|
||||
|
||||
self.focused_monitor_mut()
|
||||
.ok_or_else(|| anyhow!("there is no monitor"))?
|
||||
.update_focused_workspace(offset, &invisible_borders)?;
|
||||
.update_focused_workspace(offset)?;
|
||||
|
||||
if follow_focus {
|
||||
if let Some(window) = self.focused_workspace()?.maximized_window() {
|
||||
@@ -865,6 +837,30 @@ impl WindowManager {
|
||||
}
|
||||
}
|
||||
|
||||
// if we passed false for follow_focus and there is a container on the workspace
|
||||
if !follow_focus && self.focused_container_mut().is_ok() {
|
||||
// and we have a stack with >1 windows
|
||||
if self.focused_container_mut()?.windows().len() > 1
|
||||
// and we don't have a maxed window
|
||||
&& self.focused_workspace()?.maximized_window().is_none()
|
||||
// and we don't have a monocle container
|
||||
&& self.focused_workspace()?.monocle_container().is_none()
|
||||
{
|
||||
if let Ok(window) = self.focused_window_mut() {
|
||||
window.focus(self.mouse_follows_focus)?;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// This is to correctly restore and focus when switching to a workspace which
|
||||
// contains a managed maximized window
|
||||
if !follow_focus {
|
||||
if let Some(window) = self.focused_workspace()?.maximized_window() {
|
||||
window.restore();
|
||||
window.focus(self.mouse_follows_focus)?;
|
||||
}
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
@@ -999,13 +995,12 @@ impl WindowManager {
|
||||
}
|
||||
|
||||
pub fn update_focused_workspace_by_monitor_idx(&mut self, idx: usize) -> Result<()> {
|
||||
let invisible_borders = self.invisible_borders;
|
||||
let offset = self.work_area_offset;
|
||||
|
||||
self.monitors_mut()
|
||||
.get_mut(idx)
|
||||
.ok_or_else(|| anyhow!("there is no monitor"))?
|
||||
.update_focused_workspace(offset, &invisible_borders)
|
||||
.update_focused_workspace(offset)
|
||||
}
|
||||
|
||||
#[tracing::instrument(skip(self))]
|
||||
@@ -1095,7 +1090,6 @@ impl WindowManager {
|
||||
|
||||
tracing::info!("moving container");
|
||||
|
||||
let invisible_borders = self.invisible_borders;
|
||||
let offset = self.work_area_offset;
|
||||
let mouse_follows_focus = self.mouse_follows_focus;
|
||||
|
||||
@@ -1114,7 +1108,7 @@ impl WindowManager {
|
||||
.remove_focused_container()
|
||||
.ok_or_else(|| anyhow!("there is no container"))?;
|
||||
|
||||
monitor.update_focused_workspace(offset, &invisible_borders)?;
|
||||
monitor.update_focused_workspace(offset)?;
|
||||
|
||||
let target_monitor = self
|
||||
.monitors_mut()
|
||||
@@ -1123,7 +1117,7 @@ impl WindowManager {
|
||||
|
||||
target_monitor.add_container(container, workspace_idx)?;
|
||||
target_monitor.load_focused_workspace(mouse_follows_focus)?;
|
||||
target_monitor.update_focused_workspace(offset, &invisible_borders)?;
|
||||
target_monitor.update_focused_workspace(offset)?;
|
||||
|
||||
if follow {
|
||||
self.focus_monitor(monitor_idx)?;
|
||||
@@ -1203,7 +1197,21 @@ impl WindowManager {
|
||||
}
|
||||
}
|
||||
|
||||
self.focused_window_mut()?.focus(self.mouse_follows_focus)?;
|
||||
// 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)?;
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
@@ -1305,12 +1313,11 @@ impl WindowManager {
|
||||
// make sure to update the origin monitor workspace layout because it is no
|
||||
// longer focused so it won't get updated at the end of this fn
|
||||
let offset = self.work_area_offset;
|
||||
let invisible_borders = self.invisible_borders;
|
||||
|
||||
self.monitors_mut()
|
||||
.get_mut(origin_monitor_idx)
|
||||
.ok_or_else(|| anyhow!("there is no monitor at this index"))?
|
||||
.update_focused_workspace(offset, &invisible_borders)?;
|
||||
.update_focused_workspace(offset)?;
|
||||
|
||||
let a = self
|
||||
.focused_monitor()
|
||||
@@ -1443,7 +1450,9 @@ impl WindowManager {
|
||||
anyhow!("this is not a valid direction from the current position")
|
||||
})?;
|
||||
|
||||
let adjusted_new_index = if new_idx > current_container_idx {
|
||||
let adjusted_new_index = if new_idx > current_container_idx
|
||||
&& !matches!(workspace.layout(), Layout::Default(DefaultLayout::Grid))
|
||||
{
|
||||
new_idx - 1
|
||||
} else {
|
||||
new_idx
|
||||
@@ -1460,9 +1469,15 @@ impl WindowManager {
|
||||
pub fn promote_container_to_front(&mut self) -> Result<()> {
|
||||
self.handle_unmanaged_window_behaviour()?;
|
||||
|
||||
let workspace = self.focused_workspace_mut()?;
|
||||
|
||||
if matches!(workspace.layout(), Layout::Default(DefaultLayout::Grid)) {
|
||||
tracing::debug!("ignoring promote command for grid layout");
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
tracing::info!("promoting container");
|
||||
|
||||
let workspace = self.focused_workspace_mut()?;
|
||||
workspace.promote_container()?;
|
||||
self.update_focused_workspace(self.mouse_follows_focus)
|
||||
}
|
||||
@@ -1471,9 +1486,15 @@ impl WindowManager {
|
||||
pub fn promote_focus_to_front(&mut self) -> Result<()> {
|
||||
self.handle_unmanaged_window_behaviour()?;
|
||||
|
||||
let workspace = self.focused_workspace_mut()?;
|
||||
|
||||
if matches!(workspace.layout(), Layout::Default(DefaultLayout::Grid)) {
|
||||
tracing::info!("ignoring promote focus command for grid layout");
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
tracing::info!("promoting focus");
|
||||
|
||||
let workspace = self.focused_workspace_mut()?;
|
||||
let target_idx = match workspace.layout() {
|
||||
Layout::Default(_) => 0,
|
||||
Layout::Custom(custom) => custom
|
||||
@@ -1534,7 +1555,6 @@ impl WindowManager {
|
||||
tracing::info!("floating window");
|
||||
|
||||
let work_area = self.focused_monitor_work_area()?;
|
||||
let invisible_borders = self.invisible_borders;
|
||||
|
||||
let workspace = self.focused_workspace_mut()?;
|
||||
workspace.new_floating_window()?;
|
||||
@@ -1544,7 +1564,7 @@ impl WindowManager {
|
||||
.last_mut()
|
||||
.ok_or_else(|| anyhow!("there is no floating window"))?;
|
||||
|
||||
window.center(&work_area, &invisible_borders)?;
|
||||
window.center(&work_area)?;
|
||||
window.focus(self.mouse_follows_focus)?;
|
||||
|
||||
Ok(())
|
||||
@@ -1620,10 +1640,10 @@ impl WindowManager {
|
||||
|
||||
#[tracing::instrument(skip(self))]
|
||||
pub fn flip_layout(&mut self, layout_flip: Axis) -> Result<()> {
|
||||
tracing::info!("flipping layout");
|
||||
|
||||
let workspace = self.focused_workspace_mut()?;
|
||||
|
||||
tracing::info!("flipping layout");
|
||||
|
||||
#[allow(clippy::match_same_arms)]
|
||||
match workspace.layout_flip() {
|
||||
None => {
|
||||
@@ -1801,7 +1821,6 @@ impl WindowManager {
|
||||
) -> Result<()> {
|
||||
tracing::info!("setting workspace layout");
|
||||
|
||||
let invisible_borders = self.invisible_borders;
|
||||
let offset = self.work_area_offset;
|
||||
let focused_monitor_idx = self.focused_monitor_idx();
|
||||
|
||||
@@ -1830,7 +1849,7 @@ impl WindowManager {
|
||||
|
||||
// If this is the focused workspace on a non-focused screen, let's update it
|
||||
if focused_monitor_idx != monitor_idx && focused_workspace_idx == workspace_idx {
|
||||
workspace.update(&work_area, offset, &invisible_borders)?;
|
||||
workspace.update(&work_area, offset)?;
|
||||
Ok(())
|
||||
} else {
|
||||
Ok(self.update_focused_workspace(false)?)
|
||||
@@ -1850,7 +1869,6 @@ impl WindowManager {
|
||||
{
|
||||
tracing::info!("setting workspace layout");
|
||||
|
||||
let invisible_borders = self.invisible_borders;
|
||||
let offset = self.work_area_offset;
|
||||
let focused_monitor_idx = self.focused_monitor_idx();
|
||||
|
||||
@@ -1881,7 +1899,7 @@ impl WindowManager {
|
||||
|
||||
// If this is the focused workspace on a non-focused screen, let's update it
|
||||
if focused_monitor_idx != monitor_idx && focused_workspace_idx == workspace_idx {
|
||||
workspace.update(&work_area, offset, &invisible_borders)?;
|
||||
workspace.update(&work_area, offset)?;
|
||||
Ok(())
|
||||
} else {
|
||||
Ok(self.update_focused_workspace(false)?)
|
||||
@@ -1896,7 +1914,6 @@ impl WindowManager {
|
||||
) -> Result<()> {
|
||||
tracing::info!("setting workspace layout");
|
||||
|
||||
let invisible_borders = self.invisible_borders;
|
||||
let offset = self.work_area_offset;
|
||||
let focused_monitor_idx = self.focused_monitor_idx();
|
||||
|
||||
@@ -1923,7 +1940,7 @@ impl WindowManager {
|
||||
|
||||
// If this is the focused workspace on a non-focused screen, let's update it
|
||||
if focused_monitor_idx != monitor_idx && focused_workspace_idx == workspace_idx {
|
||||
workspace.update(&work_area, offset, &invisible_borders)?;
|
||||
workspace.update(&work_area, offset)?;
|
||||
Ok(())
|
||||
} else {
|
||||
Ok(self.update_focused_workspace(false)?)
|
||||
@@ -1939,7 +1956,6 @@ impl WindowManager {
|
||||
) -> Result<()> {
|
||||
tracing::info!("setting workspace layout");
|
||||
|
||||
let invisible_borders = self.invisible_borders;
|
||||
let offset = self.work_area_offset;
|
||||
let focused_monitor_idx = self.focused_monitor_idx();
|
||||
|
||||
@@ -1965,7 +1981,7 @@ impl WindowManager {
|
||||
|
||||
// If this is the focused workspace on a non-focused screen, let's update it
|
||||
if focused_monitor_idx != monitor_idx && focused_workspace_idx == workspace_idx {
|
||||
workspace.update(&work_area, offset, &invisible_borders)?;
|
||||
workspace.update(&work_area, offset)?;
|
||||
Ok(())
|
||||
} else {
|
||||
Ok(self.update_focused_workspace(false)?)
|
||||
@@ -1984,7 +2000,6 @@ impl WindowManager {
|
||||
{
|
||||
tracing::info!("setting workspace layout");
|
||||
let layout = CustomLayout::from_path(path)?;
|
||||
let invisible_borders = self.invisible_borders;
|
||||
let offset = self.work_area_offset;
|
||||
let focused_monitor_idx = self.focused_monitor_idx();
|
||||
|
||||
@@ -2011,7 +2026,7 @@ impl WindowManager {
|
||||
|
||||
// If this is the focused workspace on a non-focused screen, let's update it
|
||||
if focused_monitor_idx != monitor_idx && focused_workspace_idx == workspace_idx {
|
||||
workspace.update(&work_area, offset, &invisible_borders)?;
|
||||
workspace.update(&work_area, offset)?;
|
||||
Ok(())
|
||||
} else {
|
||||
Ok(self.update_focused_workspace(false)?)
|
||||
|
||||
@@ -2,6 +2,7 @@ use std::fmt::Display;
|
||||
use std::fmt::Formatter;
|
||||
|
||||
use schemars::JsonSchema;
|
||||
use serde::Deserialize;
|
||||
use serde::Serialize;
|
||||
|
||||
use crate::window::should_act;
|
||||
@@ -10,7 +11,7 @@ use crate::winevent::WinEvent;
|
||||
use crate::OBJECT_NAME_CHANGE_ON_LAUNCH;
|
||||
use crate::REGEX_IDENTIFIERS;
|
||||
|
||||
#[derive(Debug, Copy, Clone, Serialize, JsonSchema)]
|
||||
#[derive(Debug, Copy, Clone, Serialize, Deserialize, JsonSchema)]
|
||||
#[serde(tag = "type", content = "content")]
|
||||
pub enum WindowManagerEvent {
|
||||
Destroy(WinEvent, Window),
|
||||
@@ -27,6 +28,7 @@ pub enum WindowManagerEvent {
|
||||
Unmanage(Window),
|
||||
Raise(Window),
|
||||
DisplayChange(Window),
|
||||
ForceUpdate(Window),
|
||||
}
|
||||
|
||||
impl Display for WindowManagerEvent {
|
||||
@@ -77,6 +79,9 @@ impl Display for WindowManagerEvent {
|
||||
Self::DisplayChange(window) => {
|
||||
write!(f, "DisplayChange (Window: {window})")
|
||||
}
|
||||
Self::ForceUpdate(window) => {
|
||||
write!(f, "ForceUpdate (Window: {window})")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -97,7 +102,8 @@ impl WindowManagerEvent {
|
||||
| Self::Raise(window)
|
||||
| Self::Manage(window)
|
||||
| Self::DisplayChange(window)
|
||||
| Self::Unmanage(window) => window,
|
||||
| Self::Unmanage(window)
|
||||
| Self::ForceUpdate(window) => window,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -138,11 +144,13 @@ impl WindowManagerEvent {
|
||||
let title = &window.title().ok()?;
|
||||
let exe_name = &window.exe().ok()?;
|
||||
let class = &window.class().ok()?;
|
||||
let path = &window.path().ok()?;
|
||||
|
||||
let should_trigger = should_act(
|
||||
title,
|
||||
exe_name,
|
||||
class,
|
||||
path,
|
||||
&object_name_change_on_launch,
|
||||
®ex_identifiers,
|
||||
);
|
||||
|
||||
@@ -22,6 +22,7 @@ use windows::Win32::Foundation::WPARAM;
|
||||
use windows::Win32::Graphics::Dwm::DwmGetWindowAttribute;
|
||||
use windows::Win32::Graphics::Dwm::DwmSetWindowAttribute;
|
||||
use windows::Win32::Graphics::Dwm::DWMWA_CLOAKED;
|
||||
use windows::Win32::Graphics::Dwm::DWMWA_EXTENDED_FRAME_BOUNDS;
|
||||
use windows::Win32::Graphics::Dwm::DWMWA_WINDOW_CORNER_PREFERENCE;
|
||||
use windows::Win32::Graphics::Dwm::DWMWCP_ROUND;
|
||||
use windows::Win32::Graphics::Dwm::DWMWINDOWATTRIBUTE;
|
||||
@@ -35,6 +36,8 @@ use windows::Win32::Graphics::Gdi::GetMonitorInfoW;
|
||||
use windows::Win32::Graphics::Gdi::InvalidateRect;
|
||||
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;
|
||||
@@ -65,6 +68,7 @@ use windows::Win32::UI::Input::KeyboardAndMouse::INPUT_MOUSE;
|
||||
use windows::Win32::UI::Input::KeyboardAndMouse::MOUSEEVENTF_LEFTDOWN;
|
||||
use windows::Win32::UI::Input::KeyboardAndMouse::MOUSEEVENTF_LEFTUP;
|
||||
use windows::Win32::UI::Input::KeyboardAndMouse::MOUSEINPUT;
|
||||
use windows::Win32::UI::Input::KeyboardAndMouse::VK_LBUTTON;
|
||||
use windows::Win32::UI::Input::KeyboardAndMouse::VK_MENU;
|
||||
use windows::Win32::UI::WindowsAndMessaging::AllowSetForegroundWindow;
|
||||
use windows::Win32::UI::WindowsAndMessaging::BringWindowToTop;
|
||||
@@ -82,6 +86,7 @@ use windows::Win32::UI::WindowsAndMessaging::GetWindowThreadProcessId;
|
||||
use windows::Win32::UI::WindowsAndMessaging::IsIconic;
|
||||
use windows::Win32::UI::WindowsAndMessaging::IsWindow;
|
||||
use windows::Win32::UI::WindowsAndMessaging::IsWindowVisible;
|
||||
use windows::Win32::UI::WindowsAndMessaging::IsZoomed;
|
||||
use windows::Win32::UI::WindowsAndMessaging::PostMessageW;
|
||||
use windows::Win32::UI::WindowsAndMessaging::RealGetWindowClassW;
|
||||
use windows::Win32::UI::WindowsAndMessaging::RegisterClassW;
|
||||
@@ -100,7 +105,7 @@ use windows::Win32::UI::WindowsAndMessaging::GWL_STYLE;
|
||||
use windows::Win32::UI::WindowsAndMessaging::GW_HWNDNEXT;
|
||||
use windows::Win32::UI::WindowsAndMessaging::HWND_BOTTOM;
|
||||
use windows::Win32::UI::WindowsAndMessaging::HWND_NOTOPMOST;
|
||||
use windows::Win32::UI::WindowsAndMessaging::HWND_TOPMOST;
|
||||
use windows::Win32::UI::WindowsAndMessaging::HWND_TOP;
|
||||
use windows::Win32::UI::WindowsAndMessaging::LWA_ALPHA;
|
||||
use windows::Win32::UI::WindowsAndMessaging::LWA_COLORKEY;
|
||||
use windows::Win32::UI::WindowsAndMessaging::SET_WINDOW_POS_FLAGS;
|
||||
@@ -125,8 +130,7 @@ use windows::Win32::UI::WindowsAndMessaging::WS_DISABLED;
|
||||
use windows::Win32::UI::WindowsAndMessaging::WS_EX_LAYERED;
|
||||
use windows::Win32::UI::WindowsAndMessaging::WS_EX_NOACTIVATE;
|
||||
use windows::Win32::UI::WindowsAndMessaging::WS_EX_TOOLWINDOW;
|
||||
use windows::Win32::UI::WindowsAndMessaging::WS_MAXIMIZEBOX;
|
||||
use windows::Win32::UI::WindowsAndMessaging::WS_MINIMIZEBOX;
|
||||
use windows::Win32::UI::WindowsAndMessaging::WS_EX_TOPMOST;
|
||||
use windows::Win32::UI::WindowsAndMessaging::WS_POPUP;
|
||||
use windows::Win32::UI::WindowsAndMessaging::WS_SYSMENU;
|
||||
|
||||
@@ -264,7 +268,7 @@ impl WindowsApi {
|
||||
}
|
||||
.ok()
|
||||
{
|
||||
Ok(_) => {}
|
||||
Ok(()) => {}
|
||||
Err(error) => {
|
||||
tracing::error!("enum_display_devices: {}", error);
|
||||
return Err(error.into());
|
||||
@@ -339,14 +343,25 @@ impl WindowsApi {
|
||||
unsafe { MonitorFromPoint(point, MONITOR_DEFAULTTONEAREST) }.0
|
||||
}
|
||||
|
||||
/// position window resizes the target window to the given layout, adjusting
|
||||
/// the layout to account for any window shadow borders (the window painted
|
||||
/// region will match layout on completion).
|
||||
pub fn position_window(hwnd: HWND, layout: &Rect, top: bool) -> Result<()> {
|
||||
let flags = SetWindowPosition::NO_ACTIVATE
|
||||
| SetWindowPosition::NO_SEND_CHANGING
|
||||
| SetWindowPosition::NO_COPY_BITS
|
||||
| SetWindowPosition::FRAME_CHANGED;
|
||||
|
||||
let position = if top { HWND_TOPMOST } else { HWND_BOTTOM };
|
||||
Self::set_window_pos(hwnd, layout, position, flags.bits())
|
||||
let shadow_rect = Self::shadow_rect(hwnd)?;
|
||||
let rect = Rect {
|
||||
left: layout.left + shadow_rect.left,
|
||||
top: layout.top + shadow_rect.top,
|
||||
right: layout.right + shadow_rect.right,
|
||||
bottom: layout.bottom + shadow_rect.bottom,
|
||||
};
|
||||
|
||||
let position = if top { HWND_TOP } else { HWND_NOTOPMOST };
|
||||
Self::set_window_pos(hwnd, &rect, position, flags.bits())
|
||||
}
|
||||
|
||||
pub fn bring_window_to_top(hwnd: HWND) -> Result<()> {
|
||||
@@ -356,7 +371,7 @@ impl WindowsApi {
|
||||
pub fn raise_window(hwnd: HWND) -> Result<()> {
|
||||
let flags = SetWindowPosition::NO_MOVE;
|
||||
|
||||
let position = HWND_TOPMOST;
|
||||
let position = HWND_TOP;
|
||||
Self::set_window_pos(hwnd, &Rect::default(), position, flags.bits())
|
||||
}
|
||||
|
||||
@@ -367,6 +382,18 @@ impl WindowsApi {
|
||||
SetWindowPosition::NO_ACTIVATE
|
||||
};
|
||||
|
||||
// TODO(raggi): This leaves the window behind the active window, which
|
||||
// can result e.g. single pixel window borders being invisible in the
|
||||
// case of opaque window borders (e.g. EPIC Games Launcher). Ideally
|
||||
// we'd be able to pass a parent window to place ourselves just in front
|
||||
// of, however the SetWindowPos API explicitly ignores that parameter
|
||||
// unless the window being positioned is being activated - and we don't
|
||||
// want to activate the border window here. We can hopefully find a
|
||||
// better workaround in the future.
|
||||
// The trade-off chosen prevents the border window from sitting over the
|
||||
// top of other pop-up dialogs such as a file picker dialog from
|
||||
// Firefox. When adjusting this in the future, it's important to check
|
||||
// those dialog cases.
|
||||
let position = HWND_NOTOPMOST;
|
||||
Self::set_window_pos(hwnd, layout, position, flags.bits())
|
||||
}
|
||||
@@ -378,7 +405,8 @@ impl WindowsApi {
|
||||
Self::set_window_pos(hwnd, &Rect::default(), position, flags.bits())
|
||||
}
|
||||
|
||||
pub fn set_window_pos(hwnd: HWND, layout: &Rect, position: HWND, flags: u32) -> Result<()> {
|
||||
/// set_window_pos calls SetWindowPos without any accounting for Window decorations.
|
||||
fn set_window_pos(hwnd: HWND, layout: &Rect, position: HWND, flags: u32) -> Result<()> {
|
||||
unsafe {
|
||||
SetWindowPos(
|
||||
hwnd,
|
||||
@@ -393,7 +421,7 @@ impl WindowsApi {
|
||||
.process()
|
||||
}
|
||||
|
||||
fn show_window(hwnd: HWND, command: SHOW_WINDOW_CMD) {
|
||||
pub fn show_window(hwnd: HWND, command: SHOW_WINDOW_CMD) {
|
||||
// BOOL is returned but does not signify whether or not the operation was succesful
|
||||
// https://docs.microsoft.com/en-us/windows/win32/api/winuser/nf-winuser-showwindow
|
||||
unsafe { ShowWindow(hwnd, command) };
|
||||
@@ -470,11 +498,56 @@ impl WindowsApi {
|
||||
|
||||
pub fn window_rect(hwnd: HWND) -> Result<Rect> {
|
||||
let mut rect = unsafe { std::mem::zeroed() };
|
||||
unsafe { GetWindowRect(hwnd, &mut rect) }.process()?;
|
||||
|
||||
Ok(Rect::from(rect))
|
||||
if Self::dwm_get_window_attribute(hwnd, DWMWA_EXTENDED_FRAME_BOUNDS, &mut rect).is_ok() {
|
||||
// TODO(raggi): once we declare DPI awareness, we will need to scale the rect.
|
||||
// let window_scale = unsafe { GetDpiForWindow(hwnd) };
|
||||
// let system_scale = unsafe { GetDpiForSystem() };
|
||||
// Ok(Rect::from(rect).scale(system_scale.try_into()?, window_scale.try_into()?))
|
||||
Ok(Rect::from(rect))
|
||||
} else {
|
||||
unsafe { GetWindowRect(hwnd, &mut rect) }.process()?;
|
||||
Ok(Rect::from(rect))
|
||||
}
|
||||
}
|
||||
|
||||
/// shadow_rect computes the offset of the shadow position of the window to
|
||||
/// the window painted region. The four values in the returned Rect can be
|
||||
/// added to a position rect to compute a size for set_window_pos that will
|
||||
/// fill the target area, ignoring shadows.
|
||||
fn shadow_rect(hwnd: HWND) -> Result<Rect> {
|
||||
let window_rect = Self::window_rect(hwnd)?;
|
||||
|
||||
let mut srect = Default::default();
|
||||
unsafe { GetWindowRect(hwnd, &mut srect) }.process()?;
|
||||
let shadow_rect = Rect::from(srect);
|
||||
|
||||
Ok(Rect {
|
||||
left: shadow_rect.left - window_rect.left,
|
||||
top: shadow_rect.top - window_rect.top,
|
||||
right: shadow_rect.right - window_rect.right,
|
||||
bottom: shadow_rect.bottom - window_rect.bottom,
|
||||
})
|
||||
}
|
||||
|
||||
pub fn round_rect(hdc: HDC, rect: &Rect, border_radius: i32) {
|
||||
unsafe {
|
||||
RoundRect(
|
||||
hdc,
|
||||
rect.left,
|
||||
rect.top,
|
||||
rect.right,
|
||||
rect.bottom,
|
||||
border_radius,
|
||||
border_radius,
|
||||
);
|
||||
}
|
||||
}
|
||||
pub fn rectangle(hdc: HDC, rect: &Rect) {
|
||||
unsafe {
|
||||
Rectangle(hdc, rect.left, rect.top, rect.right, rect.bottom);
|
||||
}
|
||||
}
|
||||
fn set_cursor_pos(x: i32, y: i32) -> Result<()> {
|
||||
unsafe { SetCursorPos(x, y) }.process()
|
||||
}
|
||||
@@ -677,6 +750,10 @@ impl WindowsApi {
|
||||
unsafe { IsIconic(hwnd) }.into()
|
||||
}
|
||||
|
||||
pub fn is_zoomed(hwnd: HWND) -> bool {
|
||||
unsafe { IsZoomed(hwnd) }.into()
|
||||
}
|
||||
|
||||
pub fn monitor_info_w(hmonitor: HMONITOR) -> Result<MONITORINFOEXW> {
|
||||
let mut ex_info = MONITORINFOEXW::default();
|
||||
ex_info.monitorInfo.cbSize = u32::try_from(std::mem::size_of::<MONITORINFOEXW>())?;
|
||||
@@ -844,10 +921,10 @@ impl WindowsApi {
|
||||
pub fn create_border_window(name: PCWSTR, instance: HMODULE) -> Result<isize> {
|
||||
unsafe {
|
||||
let hwnd = CreateWindowExW(
|
||||
WS_EX_TOOLWINDOW | WS_EX_LAYERED,
|
||||
WS_EX_TOOLWINDOW | WS_EX_LAYERED | WS_EX_TOPMOST | WS_EX_NOACTIVATE,
|
||||
name,
|
||||
name,
|
||||
WS_POPUP | WS_SYSMENU | WS_MAXIMIZEBOX | WS_MINIMIZEBOX,
|
||||
WS_POPUP | WS_SYSMENU,
|
||||
CW_USEDEFAULT,
|
||||
CW_USEDEFAULT,
|
||||
CW_USEDEFAULT,
|
||||
@@ -908,6 +985,13 @@ impl WindowsApi {
|
||||
actual != 0
|
||||
}
|
||||
|
||||
pub fn lbutton_is_pressed() -> bool {
|
||||
let state = unsafe { GetKeyState(i32::from(VK_LBUTTON.0)) };
|
||||
#[allow(clippy::cast_sign_loss)]
|
||||
let actual = (state as u16) & 0x8000;
|
||||
actual != 0
|
||||
}
|
||||
|
||||
pub fn left_click() -> u32 {
|
||||
let inputs = [
|
||||
INPUT {
|
||||
|
||||
@@ -13,11 +13,13 @@ use windows::Win32::Graphics::Gdi::BeginPaint;
|
||||
use windows::Win32::Graphics::Gdi::CreatePen;
|
||||
use windows::Win32::Graphics::Gdi::EndPaint;
|
||||
use windows::Win32::Graphics::Gdi::Rectangle;
|
||||
use windows::Win32::Graphics::Gdi::RoundRect;
|
||||
use windows::Win32::Graphics::Gdi::SelectObject;
|
||||
use windows::Win32::Graphics::Gdi::ValidateRect;
|
||||
use windows::Win32::Graphics::Gdi::HDC;
|
||||
use windows::Win32::Graphics::Gdi::HMONITOR;
|
||||
use windows::Win32::Graphics::Gdi::PAINTSTRUCT;
|
||||
use windows::Win32::Graphics::Gdi::PS_INSIDEFRAME;
|
||||
use windows::Win32::Graphics::Gdi::PS_SOLID;
|
||||
use windows::Win32::UI::Accessibility::HWINEVENTHOOK;
|
||||
use windows::Win32::UI::WindowsAndMessaging::DefWindowProcW;
|
||||
@@ -37,13 +39,15 @@ use crate::ring::Ring;
|
||||
use crate::window::Window;
|
||||
use crate::window_manager_event::WindowManagerEvent;
|
||||
use crate::windows_api::WindowsApi;
|
||||
use crate::winevent_listener::WINEVENT_CALLBACK_CHANNEL;
|
||||
use crate::winevent::WinEvent;
|
||||
use crate::winevent_listener;
|
||||
use crate::BORDER_COLOUR_CURRENT;
|
||||
use crate::BORDER_RECT;
|
||||
use crate::BORDER_WIDTH;
|
||||
use crate::DISPLAY_INDEX_PREFERENCES;
|
||||
use crate::MONITOR_INDEX_PREFERENCES;
|
||||
use crate::TRANSPARENCY_COLOUR;
|
||||
use crate::WINDOWS_11;
|
||||
|
||||
pub extern "system" fn valid_display_monitors(
|
||||
hmonitor: HMONITOR,
|
||||
@@ -146,12 +150,17 @@ pub extern "system" fn enum_window(hwnd: HWND, lparam: LPARAM) -> BOOL {
|
||||
let is_visible = WindowsApi::is_window_visible(hwnd);
|
||||
let is_window = WindowsApi::is_window(hwnd);
|
||||
let is_minimized = WindowsApi::is_iconic(hwnd);
|
||||
let is_maximized = WindowsApi::is_zoomed(hwnd);
|
||||
|
||||
if is_visible && is_window && !is_minimized {
|
||||
let window = Window { hwnd: hwnd.0 };
|
||||
|
||||
if let Ok(should_manage) = window.should_manage(None) {
|
||||
if should_manage {
|
||||
if is_maximized {
|
||||
WindowsApi::restore_window(hwnd);
|
||||
}
|
||||
|
||||
let mut container = Container::default();
|
||||
container.windows_mut().push_back(window);
|
||||
containers.push_back(container);
|
||||
@@ -178,7 +187,10 @@ pub extern "system" fn win_event_hook(
|
||||
|
||||
let window = Window { hwnd: hwnd.0 };
|
||||
|
||||
let winevent = unsafe { ::std::mem::transmute(event) };
|
||||
let winevent = match WinEvent::try_from(event) {
|
||||
Ok(event) => event,
|
||||
Err(_) => return,
|
||||
};
|
||||
let event_type = match WindowManagerEvent::from_win_event(winevent, window) {
|
||||
None => return,
|
||||
Some(event) => event,
|
||||
@@ -186,11 +198,9 @@ pub extern "system" fn win_event_hook(
|
||||
|
||||
if let Ok(should_manage) = window.should_manage(Option::from(event_type)) {
|
||||
if should_manage {
|
||||
WINEVENT_CALLBACK_CHANNEL
|
||||
.lock()
|
||||
.0
|
||||
winevent_listener::event_tx()
|
||||
.send(event_type)
|
||||
.expect("could not send message on WINEVENT_CALLBACK_CHANNEL");
|
||||
.expect("could not send message on winevent_listener::event_tx");
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -208,7 +218,7 @@ pub extern "system" fn border_window(
|
||||
let mut ps = PAINTSTRUCT::default();
|
||||
let hdc = BeginPaint(window, &mut ps);
|
||||
let hpen = CreatePen(
|
||||
PS_SOLID,
|
||||
PS_SOLID | PS_INSIDEFRAME,
|
||||
BORDER_WIDTH.load(Ordering::SeqCst),
|
||||
COLORREF(BORDER_COLOUR_CURRENT.load(Ordering::SeqCst)),
|
||||
);
|
||||
@@ -216,7 +226,17 @@ pub extern "system" fn border_window(
|
||||
|
||||
SelectObject(hdc, hpen);
|
||||
SelectObject(hdc, hbrush);
|
||||
Rectangle(hdc, 0, 0, border_rect.right, border_rect.bottom);
|
||||
// TODO(raggi): this is approximately the correct curvature for
|
||||
// the top left of a Windows 11 window (DWMWCP_DEFAULT), but
|
||||
// often the bottom right has a different shape. Furthermore if
|
||||
// the window was made with DWMWCP_ROUNDSMALL then this is the
|
||||
// wrong size. In the future we should read the DWM properties
|
||||
// of windows and attempt to match appropriately.
|
||||
if *WINDOWS_11 {
|
||||
RoundRect(hdc, 0, 0, border_rect.right, border_rect.bottom, 20, 20);
|
||||
} else {
|
||||
Rectangle(hdc, 0, 0, border_rect.right, border_rect.bottom);
|
||||
}
|
||||
EndPaint(window, &ps);
|
||||
ValidateRect(window, None);
|
||||
|
||||
@@ -241,11 +261,9 @@ pub extern "system" fn hidden_window(
|
||||
match message {
|
||||
WM_DISPLAYCHANGE => {
|
||||
let event_type = WindowManagerEvent::DisplayChange(Window { hwnd: window.0 });
|
||||
WINEVENT_CALLBACK_CHANNEL
|
||||
.lock()
|
||||
.0
|
||||
winevent_listener::event_tx()
|
||||
.send(event_type)
|
||||
.expect("could not send message on WINEVENT_CALLBACK_CHANNEL");
|
||||
.expect("could not send message on winevent_listener::event_tx");
|
||||
|
||||
LRESULT(0)
|
||||
}
|
||||
@@ -256,11 +274,9 @@ pub extern "system" fn hidden_window(
|
||||
|| wparam.0 as u32 == SPI_ICONVERTICALSPACING.0
|
||||
{
|
||||
let event_type = WindowManagerEvent::DisplayChange(Window { hwnd: window.0 });
|
||||
WINEVENT_CALLBACK_CHANNEL
|
||||
.lock()
|
||||
.0
|
||||
winevent_listener::event_tx()
|
||||
.send(event_type)
|
||||
.expect("could not send message on WINEVENT_CALLBACK_CHANNEL");
|
||||
.expect("could not send message on winevent_listener::event_tx");
|
||||
}
|
||||
LRESULT(0)
|
||||
}
|
||||
@@ -269,11 +285,9 @@ pub extern "system" fn hidden_window(
|
||||
#[allow(clippy::cast_possible_truncation)]
|
||||
if wparam.0 as u32 == DBT_DEVNODES_CHANGED {
|
||||
let event_type = WindowManagerEvent::DisplayChange(Window { hwnd: window.0 });
|
||||
WINEVENT_CALLBACK_CHANNEL
|
||||
.lock()
|
||||
.0
|
||||
winevent_listener::event_tx()
|
||||
.send(event_type)
|
||||
.expect("could not send message on WINEVENT_CALLBACK_CHANNEL");
|
||||
.expect("could not send message on winevent_listener::event_tx");
|
||||
}
|
||||
LRESULT(0)
|
||||
}
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
#![allow(clippy::use_self)]
|
||||
|
||||
use schemars::JsonSchema;
|
||||
use serde::Deserialize;
|
||||
use serde::Serialize;
|
||||
use strum::Display;
|
||||
use windows::Win32::UI::WindowsAndMessaging::EVENT_AIA_END;
|
||||
@@ -88,7 +89,7 @@ use windows::Win32::UI::WindowsAndMessaging::EVENT_UIA_EVENTID_START;
|
||||
use windows::Win32::UI::WindowsAndMessaging::EVENT_UIA_PROPID_END;
|
||||
use windows::Win32::UI::WindowsAndMessaging::EVENT_UIA_PROPID_START;
|
||||
|
||||
#[derive(Clone, Copy, PartialEq, Eq, Debug, Serialize, Display, JsonSchema)]
|
||||
#[derive(Clone, Copy, PartialEq, Eq, Debug, Serialize, Deserialize, Display, JsonSchema)]
|
||||
#[repr(u32)]
|
||||
#[allow(dead_code)]
|
||||
pub enum WinEvent {
|
||||
@@ -177,3 +178,100 @@ pub enum WinEvent {
|
||||
UiaPropIdSEnd = EVENT_UIA_PROPID_END,
|
||||
UiaPropIdStart = EVENT_UIA_PROPID_START,
|
||||
}
|
||||
|
||||
impl TryFrom<u32> for WinEvent {
|
||||
type Error = ();
|
||||
|
||||
fn try_from(value: u32) -> Result<Self, Self::Error> {
|
||||
match value {
|
||||
EVENT_AIA_END => Ok(Self::AiaEnd),
|
||||
EVENT_AIA_START => Ok(Self::AiaStart),
|
||||
EVENT_CONSOLE_CARET => Ok(Self::ConsoleCaret),
|
||||
EVENT_CONSOLE_END => Ok(Self::ConsoleEnd),
|
||||
EVENT_CONSOLE_END_APPLICATION => Ok(Self::ConsoleEndApplication),
|
||||
EVENT_CONSOLE_LAYOUT => Ok(Self::ConsoleLayout),
|
||||
EVENT_CONSOLE_START_APPLICATION => Ok(Self::ConsoleStartApplication),
|
||||
EVENT_CONSOLE_UPDATE_REGION => Ok(Self::ConsoleUpdateRegion),
|
||||
EVENT_CONSOLE_UPDATE_SCROLL => Ok(Self::ConsoleUpdateScroll),
|
||||
EVENT_CONSOLE_UPDATE_SIMPLE => Ok(Self::ConsoleUpdateSimple),
|
||||
EVENT_OBJECT_ACCELERATORCHANGE => Ok(Self::ObjectAcceleratorChange),
|
||||
EVENT_OBJECT_CLOAKED => Ok(Self::ObjectCloaked),
|
||||
EVENT_OBJECT_CONTENTSCROLLED => Ok(Self::ObjectContentScrolled),
|
||||
EVENT_OBJECT_CREATE => Ok(Self::ObjectCreate),
|
||||
EVENT_OBJECT_DEFACTIONCHANGE => Ok(Self::ObjectDefActionChange),
|
||||
EVENT_OBJECT_DESCRIPTIONCHANGE => Ok(Self::ObjectDescriptionChange),
|
||||
EVENT_OBJECT_DESTROY => Ok(Self::ObjectDestroy),
|
||||
EVENT_OBJECT_DRAGCANCEL => Ok(Self::ObjectDragCancel),
|
||||
EVENT_OBJECT_DRAGCOMPLETE => Ok(Self::ObjectDragComplete),
|
||||
EVENT_OBJECT_DRAGDROPPED => Ok(Self::ObjectDragDropped),
|
||||
EVENT_OBJECT_DRAGENTER => Ok(Self::ObjectDragEnter),
|
||||
EVENT_OBJECT_DRAGLEAVE => Ok(Self::ObjectDragLeave),
|
||||
EVENT_OBJECT_DRAGSTART => Ok(Self::ObjectDragStart),
|
||||
EVENT_OBJECT_END => Ok(Self::ObjectEnd),
|
||||
EVENT_OBJECT_FOCUS => Ok(Self::ObjectFocus),
|
||||
EVENT_OBJECT_HELPCHANGE => Ok(Self::ObjectHelpChange),
|
||||
EVENT_OBJECT_HIDE => Ok(Self::ObjectHide),
|
||||
EVENT_OBJECT_HOSTEDOBJECTSINVALIDATED => Ok(Self::ObjectHostedObjectsInvalidated),
|
||||
EVENT_OBJECT_IME_CHANGE => Ok(Self::ObjectImeChange),
|
||||
EVENT_OBJECT_IME_HIDE => Ok(Self::ObjectImeHide),
|
||||
EVENT_OBJECT_IME_SHOW => Ok(Self::ObjectImeShow),
|
||||
EVENT_OBJECT_INVOKED => Ok(Self::ObjectInvoked),
|
||||
EVENT_OBJECT_LIVEREGIONCHANGED => Ok(Self::ObjectLiveRegionChanged),
|
||||
EVENT_OBJECT_LOCATIONCHANGE => Ok(Self::ObjectLocationChange),
|
||||
EVENT_OBJECT_NAMECHANGE => Ok(Self::ObjectNameChange),
|
||||
EVENT_OBJECT_PARENTCHANGE => Ok(Self::ObjectParentChange),
|
||||
EVENT_OBJECT_REORDER => Ok(Self::ObjectReorder),
|
||||
EVENT_OBJECT_SELECTION => Ok(Self::ObjectSelection),
|
||||
EVENT_OBJECT_SELECTIONADD => Ok(Self::ObjectSelectionAdd),
|
||||
EVENT_OBJECT_SELECTIONREMOVE => Ok(Self::ObjectSelectionRemove),
|
||||
EVENT_OBJECT_SELECTIONWITHIN => Ok(Self::ObjectSelectionWithin),
|
||||
EVENT_OBJECT_SHOW => Ok(Self::ObjectShow),
|
||||
EVENT_OBJECT_STATECHANGE => Ok(Self::ObjectStateChange),
|
||||
EVENT_OBJECT_TEXTEDIT_CONVERSIONTARGETCHANGED => {
|
||||
Ok(Self::ObjectTextEditConversionTargetChanged)
|
||||
}
|
||||
EVENT_OBJECT_TEXTSELECTIONCHANGED => Ok(Self::ObjectTextSelectionChanged),
|
||||
EVENT_OBJECT_UNCLOAKED => Ok(Self::ObjectUncloaked),
|
||||
EVENT_OBJECT_VALUECHANGE => Ok(Self::ObjectValueChange),
|
||||
EVENT_OEM_DEFINED_END => Ok(Self::OemDefinedEnd),
|
||||
EVENT_OEM_DEFINED_START => Ok(Self::OemDefinedStart),
|
||||
EVENT_SYSTEM_ALERT => Ok(Self::SystemAlert),
|
||||
EVENT_SYSTEM_ARRANGMENTPREVIEW => Ok(Self::SystemArrangementPreview),
|
||||
EVENT_SYSTEM_CAPTUREEND => Ok(Self::SystemCaptureEnd),
|
||||
EVENT_SYSTEM_CAPTURESTART => Ok(Self::SystemCaptureStart),
|
||||
EVENT_SYSTEM_CONTEXTHELPEND => Ok(Self::SystemContextHelpEnd),
|
||||
EVENT_SYSTEM_CONTEXTHELPSTART => Ok(Self::SystemContextHelpStart),
|
||||
EVENT_SYSTEM_DESKTOPSWITCH => Ok(Self::SystemDesktopSwitch),
|
||||
EVENT_SYSTEM_DIALOGEND => Ok(Self::SystemDialogEnd),
|
||||
EVENT_SYSTEM_DIALOGSTART => Ok(Self::SystemDialogStart),
|
||||
EVENT_SYSTEM_DRAGDROPEND => Ok(Self::SystemDragDropEnd),
|
||||
EVENT_SYSTEM_DRAGDROPSTART => Ok(Self::SystemDragDropStart),
|
||||
EVENT_SYSTEM_END => Ok(Self::SystemEnd),
|
||||
EVENT_SYSTEM_FOREGROUND => Ok(Self::SystemForeground),
|
||||
EVENT_SYSTEM_IME_KEY_NOTIFICATION => Ok(Self::SystemImeKeyNotification),
|
||||
EVENT_SYSTEM_MENUEND => Ok(Self::SystemMenuEnd),
|
||||
EVENT_SYSTEM_MENUPOPUPEND => Ok(Self::SystemMenuPopupEnd),
|
||||
EVENT_SYSTEM_MENUPOPUPSTART => Ok(Self::SystemMenuPopupStart),
|
||||
EVENT_SYSTEM_MENUSTART => Ok(Self::SystemMenuStart),
|
||||
EVENT_SYSTEM_MINIMIZEEND => Ok(Self::SystemMinimizeEnd),
|
||||
EVENT_SYSTEM_MINIMIZESTART => Ok(Self::SystemMinimizeStart),
|
||||
EVENT_SYSTEM_MOVESIZEEND => Ok(Self::SystemMoveSizeEnd),
|
||||
EVENT_SYSTEM_MOVESIZESTART => Ok(Self::SystemMoveSizeStart),
|
||||
EVENT_SYSTEM_SCROLLINGEND => Ok(Self::SystemScrollingEnd),
|
||||
EVENT_SYSTEM_SCROLLINGSTART => Ok(Self::SystemScrollingStart),
|
||||
EVENT_SYSTEM_SOUND => Ok(Self::SystemSound),
|
||||
EVENT_SYSTEM_SWITCHEND => Ok(Self::SystemSwitchEnd),
|
||||
EVENT_SYSTEM_SWITCHER_APPDROPPED => Ok(Self::SystemSwitcherAppDropped),
|
||||
EVENT_SYSTEM_SWITCHER_APPGRABBED => Ok(Self::SystemSwitcherAppGrabbed),
|
||||
EVENT_SYSTEM_SWITCHER_APPOVERTARGET => Ok(Self::SystemSwitcherAppOverTarget),
|
||||
EVENT_SYSTEM_SWITCHER_CANCELLED => Ok(Self::SystemSwitcherCancelled),
|
||||
EVENT_SYSTEM_SWITCHSTART => Ok(Self::SystemSwitchStart),
|
||||
EVENT_UIA_EVENTID_END => Ok(Self::UiaEventIdSEnd),
|
||||
EVENT_UIA_EVENTID_START => Ok(Self::UiaEventIdStart),
|
||||
EVENT_UIA_PROPID_END => Ok(Self::UiaPropIdSEnd),
|
||||
EVENT_UIA_PROPID_START => Ok(Self::UiaPropIdStart),
|
||||
|
||||
_ => Err(()),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,105 +1,62 @@
|
||||
use std::sync::atomic::AtomicIsize;
|
||||
use std::sync::atomic::Ordering;
|
||||
use std::sync::Arc;
|
||||
use std::time::Duration;
|
||||
use std::sync::OnceLock;
|
||||
|
||||
use crossbeam_channel::Receiver;
|
||||
use crossbeam_channel::Sender;
|
||||
use lazy_static::lazy_static;
|
||||
use parking_lot::Mutex;
|
||||
use windows::Win32::Foundation::HWND;
|
||||
use windows::Win32::UI::Accessibility::SetWinEventHook;
|
||||
use windows::Win32::UI::WindowsAndMessaging::DispatchMessageW;
|
||||
use windows::Win32::UI::WindowsAndMessaging::PeekMessageW;
|
||||
use windows::Win32::UI::WindowsAndMessaging::GetMessageW;
|
||||
use windows::Win32::UI::WindowsAndMessaging::TranslateMessage;
|
||||
use windows::Win32::UI::WindowsAndMessaging::EVENT_MAX;
|
||||
use windows::Win32::UI::WindowsAndMessaging::EVENT_MIN;
|
||||
use windows::Win32::UI::WindowsAndMessaging::MSG;
|
||||
use windows::Win32::UI::WindowsAndMessaging::PM_REMOVE;
|
||||
|
||||
use crate::window_manager_event::WindowManagerEvent;
|
||||
use crate::windows_callbacks;
|
||||
|
||||
lazy_static! {
|
||||
pub static ref WINEVENT_CALLBACK_CHANNEL: Arc<Mutex<(Sender<WindowManagerEvent>, Receiver<WindowManagerEvent>)>> =
|
||||
Arc::new(Mutex::new(crossbeam_channel::unbounded()));
|
||||
}
|
||||
static CHANNEL: OnceLock<(Sender<WindowManagerEvent>, Receiver<WindowManagerEvent>)> =
|
||||
OnceLock::new();
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct WinEventListener {
|
||||
hook: Arc<AtomicIsize>,
|
||||
outgoing_events: Arc<Mutex<Sender<WindowManagerEvent>>>,
|
||||
}
|
||||
static EVENT_PUMP: OnceLock<std::thread::JoinHandle<()>> = OnceLock::new();
|
||||
|
||||
pub fn new(outgoing: Arc<Mutex<Sender<WindowManagerEvent>>>) -> WinEventListener {
|
||||
WinEventListener {
|
||||
hook: Arc::new(AtomicIsize::new(0)),
|
||||
outgoing_events: outgoing,
|
||||
}
|
||||
}
|
||||
|
||||
impl WinEventListener {
|
||||
pub fn start(self) {
|
||||
let hook = self.hook.clone();
|
||||
let outgoing = self.outgoing_events.lock().clone();
|
||||
|
||||
std::thread::spawn(move || unsafe {
|
||||
let hook_ref = SetWinEventHook(
|
||||
EVENT_MIN,
|
||||
EVENT_MAX,
|
||||
None,
|
||||
Some(windows_callbacks::win_event_hook),
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
);
|
||||
|
||||
hook.store(hook_ref.0, Ordering::SeqCst);
|
||||
|
||||
// The code in the callback doesn't work in its own loop, needs to be within
|
||||
// the MessageLoop callback for the winevent callback to even fire
|
||||
MessageLoop::start(10, |_msg| {
|
||||
if let Ok(event) = WINEVENT_CALLBACK_CHANNEL.lock().1.try_recv() {
|
||||
match outgoing.send(event) {
|
||||
Ok(()) => {}
|
||||
Err(error) => {
|
||||
tracing::error!("{}", error);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
true
|
||||
});
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Copy, Clone)]
|
||||
pub struct MessageLoop;
|
||||
|
||||
impl MessageLoop {
|
||||
pub fn start(sleep: u64, cb: impl Fn(Option<MSG>) -> bool) {
|
||||
Self::start_with_sleep(sleep, cb);
|
||||
}
|
||||
|
||||
fn start_with_sleep(sleep: u64, cb: impl Fn(Option<MSG>) -> bool) {
|
||||
let mut msg: MSG = MSG::default();
|
||||
loop {
|
||||
let mut value: Option<MSG> = None;
|
||||
pub fn start() {
|
||||
EVENT_PUMP.get_or_init(|| {
|
||||
std::thread::spawn(move || {
|
||||
unsafe {
|
||||
if !bool::from(!PeekMessageW(&mut msg, HWND(0), 0, 0, PM_REMOVE)) {
|
||||
SetWinEventHook(
|
||||
EVENT_MIN,
|
||||
EVENT_MAX,
|
||||
None,
|
||||
Some(windows_callbacks::win_event_hook),
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
)
|
||||
};
|
||||
|
||||
loop {
|
||||
let mut msg: MSG = MSG::default();
|
||||
unsafe {
|
||||
if !GetMessageW(&mut msg, HWND(0), 0, 0).as_bool() {
|
||||
tracing::info!("windows event processing shutdown");
|
||||
break;
|
||||
};
|
||||
TranslateMessage(&msg);
|
||||
DispatchMessageW(&msg);
|
||||
|
||||
value = Some(msg);
|
||||
}
|
||||
}
|
||||
|
||||
std::thread::sleep(Duration::from_millis(sleep));
|
||||
|
||||
if !cb(value) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
||||
});
|
||||
}
|
||||
|
||||
fn channel() -> &'static (Sender<WindowManagerEvent>, Receiver<WindowManagerEvent>) {
|
||||
CHANNEL.get_or_init(crossbeam_channel::unbounded)
|
||||
}
|
||||
|
||||
pub fn event_tx() -> Sender<WindowManagerEvent> {
|
||||
channel().0.clone()
|
||||
}
|
||||
|
||||
pub fn event_rx() -> Receiver<WindowManagerEvent> {
|
||||
channel().1.clone()
|
||||
}
|
||||
|
||||
@@ -9,6 +9,7 @@ use getset::Getters;
|
||||
use getset::MutGetters;
|
||||
use getset::Setters;
|
||||
use schemars::JsonSchema;
|
||||
use serde::Deserialize;
|
||||
use serde::Serialize;
|
||||
|
||||
use komorebi_core::Axis;
|
||||
@@ -25,25 +26,31 @@ use crate::static_config::WorkspaceConfig;
|
||||
use crate::window::Window;
|
||||
use crate::window::WindowDetails;
|
||||
use crate::windows_api::WindowsApi;
|
||||
use crate::BORDER_OFFSET;
|
||||
use crate::BORDER_WIDTH;
|
||||
use crate::DEFAULT_CONTAINER_PADDING;
|
||||
use crate::DEFAULT_WORKSPACE_PADDING;
|
||||
use crate::INITIAL_CONFIGURATION_LOADED;
|
||||
use crate::NO_TITLEBAR;
|
||||
use crate::REMOVE_TITLEBARS;
|
||||
use crate::STACKBAR_TAB_HEIGHT;
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Getters, CopyGetters, MutGetters, Setters, JsonSchema)]
|
||||
#[allow(clippy::struct_field_names)]
|
||||
#[derive(
|
||||
Debug, Clone, Serialize, Deserialize, Getters, CopyGetters, MutGetters, Setters, JsonSchema,
|
||||
)]
|
||||
pub struct Workspace {
|
||||
#[getset(get = "pub", set = "pub")]
|
||||
name: Option<String>,
|
||||
containers: Ring<Container>,
|
||||
#[getset(get = "pub", get_mut = "pub", set = "pub")]
|
||||
monocle_container: Option<Container>,
|
||||
#[serde(skip_serializing)]
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
#[getset(get_copy = "pub", set = "pub")]
|
||||
monocle_container_restore_idx: Option<usize>,
|
||||
#[getset(get = "pub", get_mut = "pub", set = "pub")]
|
||||
maximized_window: Option<Window>,
|
||||
#[serde(skip_serializing)]
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
#[getset(get_copy = "pub", set = "pub")]
|
||||
maximized_window_restore_idx: Option<usize>,
|
||||
#[getset(get = "pub", get_mut = "pub")]
|
||||
@@ -58,7 +65,6 @@ pub struct Workspace {
|
||||
workspace_padding: Option<i32>,
|
||||
#[getset(get_copy = "pub", set = "pub")]
|
||||
container_padding: Option<i32>,
|
||||
#[serde(skip_serializing)]
|
||||
#[getset(get = "pub", set = "pub")]
|
||||
latest_layout: Vec<Rect>,
|
||||
#[getset(get = "pub", get_mut = "pub", set = "pub")]
|
||||
@@ -138,55 +144,66 @@ impl Workspace {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn hide(&mut self) {
|
||||
for container in self.containers_mut() {
|
||||
for window in container.windows_mut() {
|
||||
pub fn hide(&mut self, omit: Option<isize>) {
|
||||
for window in self.floating_windows_mut().iter_mut().rev() {
|
||||
let mut should_hide = omit.is_none();
|
||||
|
||||
if !should_hide {
|
||||
if let Some(omit) = omit {
|
||||
if omit != window.hwnd {
|
||||
should_hide = true
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if should_hide {
|
||||
window.hide();
|
||||
}
|
||||
}
|
||||
|
||||
for container in self.containers_mut() {
|
||||
container.hide(omit)
|
||||
}
|
||||
|
||||
if let Some(window) = self.maximized_window() {
|
||||
window.hide();
|
||||
}
|
||||
|
||||
if let Some(container) = self.monocle_container_mut() {
|
||||
for window in container.windows_mut() {
|
||||
window.hide();
|
||||
}
|
||||
}
|
||||
|
||||
for window in self.floating_windows() {
|
||||
window.hide();
|
||||
container.hide(omit)
|
||||
}
|
||||
}
|
||||
|
||||
pub fn restore(&mut self, mouse_follows_focus: bool) -> Result<()> {
|
||||
let idx = self.focused_container_idx();
|
||||
let mut to_focus = None;
|
||||
|
||||
for (i, container) in self.containers_mut().iter_mut().enumerate() {
|
||||
if let Some(window) = container.focused_window_mut() {
|
||||
window.restore();
|
||||
|
||||
if idx == i {
|
||||
to_focus = Option::from(*window);
|
||||
}
|
||||
}
|
||||
|
||||
container.restore();
|
||||
}
|
||||
|
||||
if let Some(window) = self.maximized_window() {
|
||||
window.maximize();
|
||||
for container in self.containers_mut() {
|
||||
container.restore();
|
||||
}
|
||||
|
||||
if let Some(container) = self.monocle_container_mut() {
|
||||
for window in container.windows_mut() {
|
||||
window.restore();
|
||||
}
|
||||
container.restore();
|
||||
}
|
||||
|
||||
for window in self.floating_windows() {
|
||||
window.restore();
|
||||
}
|
||||
|
||||
if let Some(container) = self.focused_container_mut() {
|
||||
container.focus_window(container.focused_window_idx());
|
||||
}
|
||||
|
||||
// Do this here to make sure that an error doesn't stop the restoration of other windows
|
||||
// Maximised windows should always be drawn at the top of the Z order
|
||||
if let Some(window) = to_focus {
|
||||
@@ -198,12 +215,7 @@ impl Workspace {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn update(
|
||||
&mut self,
|
||||
work_area: &Rect,
|
||||
offset: Option<Rect>,
|
||||
invisible_borders: &Rect,
|
||||
) -> Result<()> {
|
||||
pub fn update(&mut self, work_area: &Rect, offset: Option<Rect>) -> Result<()> {
|
||||
if !INITIAL_CONFIGURATION_LOADED.load(Ordering::SeqCst) {
|
||||
return Ok(());
|
||||
}
|
||||
@@ -222,7 +234,7 @@ impl Workspace {
|
||||
},
|
||||
);
|
||||
|
||||
adjusted_work_area.add_padding(self.workspace_padding());
|
||||
adjusted_work_area.add_padding(self.workspace_padding().unwrap_or_default());
|
||||
|
||||
self.enforce_resize_constraints();
|
||||
|
||||
@@ -244,11 +256,19 @@ impl Workspace {
|
||||
}
|
||||
}
|
||||
|
||||
let managed_maximized_window = self.maximized_window().is_some();
|
||||
|
||||
if *self.tile() {
|
||||
if let Some(container) = self.monocle_container_mut() {
|
||||
if let Some(window) = container.focused_window_mut() {
|
||||
adjusted_work_area.add_padding(container_padding);
|
||||
window.set_position(&adjusted_work_area, invisible_borders, true)?;
|
||||
adjusted_work_area.add_padding(container_padding.unwrap_or_default());
|
||||
{
|
||||
let border_offset = BORDER_OFFSET.load(Ordering::SeqCst);
|
||||
adjusted_work_area.add_padding(border_offset);
|
||||
let width = BORDER_WIDTH.load(Ordering::SeqCst);
|
||||
adjusted_work_area.add_padding(width);
|
||||
}
|
||||
window.set_position(&adjusted_work_area, true)?;
|
||||
};
|
||||
} else if let Some(window) = self.maximized_window_mut() {
|
||||
window.maximize();
|
||||
@@ -268,16 +288,59 @@ impl Workspace {
|
||||
let should_remove_titlebars = REMOVE_TITLEBARS.load(Ordering::SeqCst);
|
||||
let no_titlebar = NO_TITLEBAR.lock().clone();
|
||||
|
||||
let windows = self.visible_windows_mut();
|
||||
for (i, window) in windows.into_iter().enumerate() {
|
||||
if let (Some(window), Some(layout)) = (window, layouts.get(i)) {
|
||||
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() {
|
||||
let container_windows = container.windows().clone();
|
||||
let container_topbar = container.stackbar().clone();
|
||||
|
||||
if let (Some(window), Some(layout)) =
|
||||
(container.focused_window_mut(), layouts.get(i))
|
||||
{
|
||||
if should_remove_titlebars && no_titlebar.contains(&window.exe()?) {
|
||||
window.remove_title_bar()?;
|
||||
} else if no_titlebar.contains(&window.exe()?) {
|
||||
window.add_title_bar()?;
|
||||
}
|
||||
|
||||
window.set_position(layout, invisible_borders, false)?;
|
||||
// If a window has been unmaximized via toggle-maximize, this block
|
||||
// will make sure that it is unmaximized via restore_window
|
||||
if window.is_maximized() && !managed_maximized_window {
|
||||
WindowsApi::restore_window(window.hwnd());
|
||||
}
|
||||
|
||||
let mut rect = *layout;
|
||||
{
|
||||
let border_offset = BORDER_OFFSET.load(Ordering::SeqCst);
|
||||
rect.add_padding(border_offset);
|
||||
|
||||
let width = BORDER_WIDTH.load(Ordering::SeqCst);
|
||||
rect.add_padding(width);
|
||||
}
|
||||
|
||||
if let Some(stackbar) = container_topbar {
|
||||
stackbar.set_position(
|
||||
&stackbar.get_position_from_container_layout(layout),
|
||||
false,
|
||||
)?;
|
||||
|
||||
stackbar.update(&container_windows, focused_hwnd)?;
|
||||
let tab_height = STACKBAR_TAB_HEIGHT.load(Ordering::SeqCst);
|
||||
let total_height = tab_height + container_padding;
|
||||
|
||||
rect.top += total_height;
|
||||
rect.bottom -= total_height;
|
||||
}
|
||||
|
||||
window.set_position(&rect, false)?;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -352,7 +415,18 @@ impl Workspace {
|
||||
.idx_for_window(hwnd)
|
||||
.ok_or_else(|| anyhow!("there is no window"))?;
|
||||
|
||||
let mut should_load = false;
|
||||
|
||||
if container.focused_window_idx() != window_idx {
|
||||
should_load = true
|
||||
}
|
||||
|
||||
container.focus_window(window_idx);
|
||||
|
||||
if should_load {
|
||||
container.load_focused_window();
|
||||
}
|
||||
|
||||
self.focus_container(container_idx);
|
||||
|
||||
Ok(())
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
[package]
|
||||
name = "komorebic-no-console"
|
||||
version = "0.1.19"
|
||||
version = "0.1.23-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"]
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
[package]
|
||||
name = "komorebic"
|
||||
version = "0.1.20"
|
||||
version = "0.1.23-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"]
|
||||
@@ -21,15 +21,18 @@ dunce = { workspace = true }
|
||||
fs-tail = "0.1"
|
||||
heck = "0.4"
|
||||
lazy_static = "1"
|
||||
miette = { version = "5", features = ["fancy"] }
|
||||
miette = { version = "7", features = ["fancy"] }
|
||||
paste = "1"
|
||||
powershell_script = "1.0"
|
||||
reqwest = { version = "0.11", features = ["blocking"] }
|
||||
serde = { version = "1", features = ["derive"] }
|
||||
serde_json = "1"
|
||||
serde_json = { workspace = true }
|
||||
serde_yaml = "0.9"
|
||||
sysinfo = "0.30"
|
||||
thiserror = "1"
|
||||
uds_windows = "1"
|
||||
which = "5"
|
||||
windows = { workspace = true }
|
||||
which = "6"
|
||||
windows = { workspace = true }
|
||||
|
||||
[build-dependencies]
|
||||
reqwest = { version = "0.11", features = ["blocking"] }
|
||||
|
||||
8
komorebic/build.rs
Normal file
8
komorebic/build.rs
Normal file
@@ -0,0 +1,8 @@
|
||||
fn main() {
|
||||
if std::fs::metadata("applications.yaml").is_err() {
|
||||
let applications_yaml = reqwest::blocking::get(
|
||||
"https://raw.githubusercontent.com/LGUG2Z/komorebi-application-specific-configuration/master/applications.yaml"
|
||||
).unwrap().text().unwrap();
|
||||
std::fs::write("applications.yaml", applications_yaml).unwrap();
|
||||
}
|
||||
}
|
||||
@@ -5,8 +5,9 @@ use std::fs::File;
|
||||
use std::fs::OpenOptions;
|
||||
use std::io::BufRead;
|
||||
use std::io::BufReader;
|
||||
use std::io::ErrorKind;
|
||||
use std::io::Read;
|
||||
use std::io::Write;
|
||||
use std::net::Shutdown;
|
||||
use std::path::Path;
|
||||
use std::path::PathBuf;
|
||||
use std::process::Command;
|
||||
@@ -20,6 +21,7 @@ use clap::ValueEnum;
|
||||
use color_eyre::eyre::anyhow;
|
||||
use color_eyre::eyre::bail;
|
||||
use color_eyre::Result;
|
||||
use dirs::data_local_dir;
|
||||
use fs_tail::TailedFile;
|
||||
use heck::ToKebabCase;
|
||||
use komorebi_core::resolve_home_path;
|
||||
@@ -29,7 +31,6 @@ use miette::Report;
|
||||
use miette::SourceOffset;
|
||||
use miette::SourceSpan;
|
||||
use paste::paste;
|
||||
use uds_windows::UnixListener;
|
||||
use uds_windows::UnixStream;
|
||||
use which::which;
|
||||
use windows::Win32::Foundation::HWND;
|
||||
@@ -113,7 +114,7 @@ trait AhkFunction {
|
||||
struct ConfigurationError {
|
||||
message: String,
|
||||
#[source_code]
|
||||
src: NamedSource,
|
||||
src: NamedSource<String>,
|
||||
#[label("This bit here")]
|
||||
bad_bit: SourceSpan,
|
||||
}
|
||||
@@ -720,13 +721,25 @@ struct LoadCustomLayout {
|
||||
}
|
||||
|
||||
#[derive(Parser, AhkFunction)]
|
||||
struct Subscribe {
|
||||
struct SubscribeSocket {
|
||||
/// Name of the socket to send event notifications to
|
||||
socket: String,
|
||||
}
|
||||
|
||||
#[derive(Parser, AhkFunction)]
|
||||
struct UnsubscribeSocket {
|
||||
/// Name of the socket to stop sending event notifications to
|
||||
socket: String,
|
||||
}
|
||||
|
||||
#[derive(Parser, AhkFunction)]
|
||||
struct SubscribePipe {
|
||||
/// Name of the pipe to send event notifications to (without "\\.\pipe\" prepended)
|
||||
named_pipe: String,
|
||||
}
|
||||
|
||||
#[derive(Parser, AhkFunction)]
|
||||
struct Unsubscribe {
|
||||
struct UnsubscribePipe {
|
||||
/// Name of the pipe to stop sending event notifications to (without "\\.\pipe\" prepended)
|
||||
named_pipe: String,
|
||||
}
|
||||
@@ -792,8 +805,14 @@ enum SubCommand {
|
||||
Start(Start),
|
||||
/// Stop the komorebi.exe process and restore all hidden windows
|
||||
Stop(Stop),
|
||||
/// Output various important komorebi-related environment values
|
||||
/// Check komorebi configuration and related files for common errors
|
||||
Check,
|
||||
/// Show the path to komorebi.json
|
||||
#[clap(alias = "config")]
|
||||
Configuration,
|
||||
/// Show the path to whkdrc
|
||||
#[clap(alias = "whkd")]
|
||||
Whkdrc,
|
||||
/// Show a JSON representation of the current window manager state
|
||||
State,
|
||||
/// Show a JSON representation of visible windows
|
||||
@@ -801,12 +820,20 @@ enum SubCommand {
|
||||
/// Query the current window manager state
|
||||
#[clap(arg_required_else_help = true)]
|
||||
Query(Query),
|
||||
/// Subscribe to komorebi events
|
||||
/// Subscribe to komorebi events using a Unix Domain Socket
|
||||
#[clap(arg_required_else_help = true)]
|
||||
Subscribe(Subscribe),
|
||||
SubscribeSocket(SubscribeSocket),
|
||||
/// Unsubscribe from komorebi events
|
||||
#[clap(arg_required_else_help = true)]
|
||||
Unsubscribe(Unsubscribe),
|
||||
UnsubscribeSocket(UnsubscribeSocket),
|
||||
/// Subscribe to komorebi events using a Named Pipe
|
||||
#[clap(arg_required_else_help = true)]
|
||||
#[clap(alias = "subscribe")]
|
||||
SubscribePipe(SubscribePipe),
|
||||
/// Unsubscribe from komorebi events
|
||||
#[clap(arg_required_else_help = true)]
|
||||
#[clap(alias = "unsubscribe")]
|
||||
UnsubscribePipe(UnsubscribePipe),
|
||||
/// Tail komorebi.exe's process logs (cancel with Ctrl-C)
|
||||
Log,
|
||||
/// Quicksave the current resize layout dimensions
|
||||
@@ -1050,8 +1077,9 @@ enum SubCommand {
|
||||
WatchConfiguration(WatchConfiguration),
|
||||
/// Signal that the final configuration option has been sent
|
||||
CompleteConfiguration,
|
||||
/// Enable or disable a hack simulating ALT key presses to ensure focus changes succeed
|
||||
/// DEPRECATED since v0.1.22
|
||||
#[clap(arg_required_else_help = true)]
|
||||
#[clap(hide = true)]
|
||||
AltFocusHack(AltFocusHack),
|
||||
/// Set the window behaviour when switching workspaces / cycling stacks
|
||||
#[clap(arg_required_else_help = true)]
|
||||
@@ -1160,46 +1188,31 @@ enum SubCommand {
|
||||
pub fn send_message(bytes: &[u8]) -> Result<()> {
|
||||
let socket = DATA_DIR.join("komorebi.sock");
|
||||
|
||||
let mut connected = false;
|
||||
while !connected {
|
||||
if let Ok(mut stream) = UnixStream::connect(&socket) {
|
||||
connected = true;
|
||||
stream.write_all(bytes)?;
|
||||
}
|
||||
}
|
||||
|
||||
Ok(())
|
||||
let mut stream = UnixStream::connect(socket)?;
|
||||
stream.write_all(bytes)?;
|
||||
Ok(stream.shutdown(Shutdown::Write)?)
|
||||
}
|
||||
|
||||
fn with_komorebic_socket<F: Fn() -> Result<()>>(f: F) -> Result<()> {
|
||||
let socket = DATA_DIR.join("komorebic.sock");
|
||||
pub fn send_query(bytes: &[u8]) -> Result<String> {
|
||||
let socket = DATA_DIR.join("komorebi.sock");
|
||||
|
||||
match std::fs::remove_file(&socket) {
|
||||
Ok(()) => {}
|
||||
Err(error) => match error.kind() {
|
||||
// Doing this because ::exists() doesn't work reliably on Windows via IntelliJ
|
||||
ErrorKind::NotFound => {}
|
||||
_ => {
|
||||
return Err(error.into());
|
||||
}
|
||||
},
|
||||
};
|
||||
let mut stream = UnixStream::connect(socket)?;
|
||||
stream.write_all(bytes)?;
|
||||
stream.shutdown(Shutdown::Write)?;
|
||||
|
||||
f()?;
|
||||
let mut reader = BufReader::new(stream);
|
||||
let mut response = String::new();
|
||||
reader.read_to_string(&mut response)?;
|
||||
|
||||
let listener = UnixListener::bind(socket)?;
|
||||
match listener.accept() {
|
||||
Ok(incoming) => {
|
||||
let stream = BufReader::new(incoming.0);
|
||||
for line in stream.lines() {
|
||||
println!("{}", line?);
|
||||
}
|
||||
Ok(response)
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
Err(error) => {
|
||||
panic!("{}", error);
|
||||
}
|
||||
// print_query is a helper that queries komorebi and prints the response.
|
||||
// panics on error.
|
||||
fn print_query(bytes: &[u8]) {
|
||||
match send_query(bytes) {
|
||||
Ok(response) => println!("{response}"),
|
||||
Err(error) => panic!("{}", error),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1243,27 +1256,20 @@ fn main() -> Result<()> {
|
||||
}
|
||||
}
|
||||
SubCommand::Quickstart => {
|
||||
let version = env!("CARGO_PKG_VERSION");
|
||||
|
||||
let home_dir = dirs::home_dir().expect("could not find home dir");
|
||||
let config_dir = home_dir.join(".config");
|
||||
let local_appdata_dir = data_local_dir().expect("could not find localdata dir");
|
||||
let data_dir = local_appdata_dir.join("komorebi");
|
||||
std::fs::create_dir_all(&config_dir)?;
|
||||
std::fs::create_dir_all(data_dir)?;
|
||||
|
||||
let komorebi_json = reqwest::blocking::get(
|
||||
format!("https://raw.githubusercontent.com/LGUG2Z/komorebi/v{version}/komorebi.example.json")
|
||||
)?.text()?;
|
||||
let komorebi_json = include_str!("../../docs/komorebi.example.json");
|
||||
std::fs::write(HOME_DIR.join("komorebi.json"), komorebi_json)?;
|
||||
|
||||
let applications_yaml = reqwest::blocking::get(
|
||||
"https://raw.githubusercontent.com/LGUG2Z/komorebi-application-specific-configuration/master/applications.yaml"
|
||||
)?
|
||||
.text()?;
|
||||
let applications_yaml = include_str!("../applications.yaml");
|
||||
std::fs::write(HOME_DIR.join("applications.yaml"), applications_yaml)?;
|
||||
|
||||
let whkdrc = reqwest::blocking::get(format!(
|
||||
"https://raw.githubusercontent.com/LGUG2Z/komorebi/v{version}/whkdrc.sample"
|
||||
))?
|
||||
.text()?;
|
||||
let whkdrc = include_str!("../../docs/whkdrc.sample");
|
||||
std::fs::write(config_dir.join("whkdrc"), whkdrc)?;
|
||||
|
||||
println!("Example ~/komorebi.json, ~/.config/whkdrc and latest ~/applications.yaml files downloaded");
|
||||
@@ -1351,7 +1357,7 @@ fn main() -> Result<()> {
|
||||
let diagnostic = ConfigurationError {
|
||||
message: msgs[0].to_string(),
|
||||
src: NamedSource::new("komorebi.json", config_source.clone()),
|
||||
bad_bit: SourceSpan::new(offset, 2.into()),
|
||||
bad_bit: SourceSpan::new(offset, 2),
|
||||
};
|
||||
|
||||
println!("{:?}", Report::new(diagnostic));
|
||||
@@ -1405,6 +1411,20 @@ fn main() -> Result<()> {
|
||||
println!("If running 'komorebic start --await-configuration', you will manually have to call the following command to begin tiling: komorebic complete-configuration\n");
|
||||
}
|
||||
}
|
||||
SubCommand::Configuration => {
|
||||
let static_config = HOME_DIR.join("komorebi.json");
|
||||
|
||||
if static_config.exists() {
|
||||
println!("{}", static_config.display());
|
||||
}
|
||||
}
|
||||
SubCommand::Whkdrc => {
|
||||
let whkdrc = WHKD_CONFIG_DIR.join("whkdrc");
|
||||
|
||||
if whkdrc.exists() {
|
||||
println!("{}", whkdrc.display());
|
||||
}
|
||||
}
|
||||
SubCommand::AhkLibrary => {
|
||||
let library = HOME_DIR.join("komorebic.lib.ahk");
|
||||
let mut file = OpenOptions::new()
|
||||
@@ -1438,7 +1458,7 @@ fn main() -> Result<()> {
|
||||
let color_log = std::env::temp_dir().join("komorebi.log");
|
||||
let file = TailedFile::new(File::open(color_log)?);
|
||||
let locked = file.lock();
|
||||
#[allow(clippy::significant_drop_in_scrutinee)]
|
||||
#[allow(clippy::significant_drop_in_scrutinee, clippy::lines_filter_map_ok)]
|
||||
for line in locked.lines().flatten() {
|
||||
println!("{line}");
|
||||
}
|
||||
@@ -1735,7 +1755,7 @@ fn main() -> Result<()> {
|
||||
let exec = if let Ok(output) = Command::new("where.exe").arg("komorebi.ps1").output() {
|
||||
let stdout = String::from_utf8(output.stdout)?;
|
||||
match stdout.trim() {
|
||||
stdout if stdout.is_empty() => None,
|
||||
"" => None,
|
||||
// It's possible that a komorebi.ps1 config will be in %USERPROFILE% - ignore this
|
||||
stdout if !stdout.contains("scoop") => None,
|
||||
stdout => {
|
||||
@@ -1996,15 +2016,13 @@ Stop-Process -Name:whkd -ErrorAction SilentlyContinue
|
||||
)?;
|
||||
}
|
||||
SubCommand::State => {
|
||||
with_komorebic_socket(|| send_message(&SocketMessage::State.as_bytes()?))?;
|
||||
print_query(&SocketMessage::State.as_bytes()?);
|
||||
}
|
||||
SubCommand::VisibleWindows => {
|
||||
with_komorebic_socket(|| send_message(&SocketMessage::VisibleWindows.as_bytes()?))?;
|
||||
print_query(&SocketMessage::VisibleWindows.as_bytes()?);
|
||||
}
|
||||
SubCommand::Query(arg) => {
|
||||
with_komorebic_socket(|| {
|
||||
send_message(&SocketMessage::Query(arg.state_query).as_bytes()?)
|
||||
})?;
|
||||
print_query(&SocketMessage::Query(arg.state_query).as_bytes()?);
|
||||
}
|
||||
SubCommand::RestoreWindows => {
|
||||
let hwnd_json = DATA_DIR.join("komorebi.hwnd.json");
|
||||
@@ -2095,11 +2113,17 @@ Stop-Process -Name:whkd -ErrorAction SilentlyContinue
|
||||
SubCommand::LoadResize(arg) => {
|
||||
send_message(&SocketMessage::Load(resolve_home_path(arg.path)?).as_bytes()?)?;
|
||||
}
|
||||
SubCommand::Subscribe(arg) => {
|
||||
send_message(&SocketMessage::AddSubscriber(arg.named_pipe).as_bytes()?)?;
|
||||
SubCommand::SubscribeSocket(arg) => {
|
||||
send_message(&SocketMessage::AddSubscriberSocket(arg.socket).as_bytes()?)?;
|
||||
}
|
||||
SubCommand::Unsubscribe(arg) => {
|
||||
send_message(&SocketMessage::RemoveSubscriber(arg.named_pipe).as_bytes()?)?;
|
||||
SubCommand::UnsubscribeSocket(arg) => {
|
||||
send_message(&SocketMessage::RemoveSubscriberSocket(arg.socket).as_bytes()?)?;
|
||||
}
|
||||
SubCommand::SubscribePipe(arg) => {
|
||||
send_message(&SocketMessage::AddSubscriberPipe(arg.named_pipe).as_bytes()?)?;
|
||||
}
|
||||
SubCommand::UnsubscribePipe(arg) => {
|
||||
send_message(&SocketMessage::RemoveSubscriberPipe(arg.named_pipe).as_bytes()?)?;
|
||||
}
|
||||
SubCommand::ToggleMouseFollowsFocus => {
|
||||
send_message(&SocketMessage::ToggleMouseFollowsFocus.as_bytes()?)?;
|
||||
@@ -2235,23 +2259,19 @@ Stop-Process -Name:whkd -ErrorAction SilentlyContinue
|
||||
);
|
||||
}
|
||||
SubCommand::ApplicationSpecificConfigurationSchema => {
|
||||
with_komorebic_socket(|| {
|
||||
send_message(&SocketMessage::ApplicationSpecificConfigurationSchema.as_bytes()?)
|
||||
})?;
|
||||
print_query(&SocketMessage::ApplicationSpecificConfigurationSchema.as_bytes()?);
|
||||
}
|
||||
SubCommand::NotificationSchema => {
|
||||
with_komorebic_socket(|| send_message(&SocketMessage::NotificationSchema.as_bytes()?))?;
|
||||
print_query(&SocketMessage::NotificationSchema.as_bytes()?);
|
||||
}
|
||||
SubCommand::SocketSchema => {
|
||||
with_komorebic_socket(|| send_message(&SocketMessage::SocketSchema.as_bytes()?))?;
|
||||
print_query(&SocketMessage::SocketSchema.as_bytes()?);
|
||||
}
|
||||
SubCommand::StaticConfigSchema => {
|
||||
with_komorebic_socket(|| send_message(&SocketMessage::StaticConfigSchema.as_bytes()?))?;
|
||||
print_query(&SocketMessage::StaticConfigSchema.as_bytes()?);
|
||||
}
|
||||
SubCommand::GenerateStaticConfig => {
|
||||
with_komorebic_socket(|| {
|
||||
send_message(&SocketMessage::GenerateStaticConfig.as_bytes()?)
|
||||
})?;
|
||||
print_query(&SocketMessage::GenerateStaticConfig.as_bytes()?);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
11
mkdocs.yml
11
mkdocs.yml
@@ -62,17 +62,23 @@ nav:
|
||||
- common-workflows/custom-layouts.md
|
||||
- common-workflows/dynamic-layout-switching.md
|
||||
# - common-workflows/autohotkey.md
|
||||
- Release notes:
|
||||
- release/v0-1-22.md
|
||||
- Configuration reference: https://komorebi.lgug2z.com/schema
|
||||
- CLI reference:
|
||||
- cli/quickstart.md
|
||||
- cli/start.md
|
||||
- cli/stop.md
|
||||
- cli/check.md
|
||||
- cli/configuration.md
|
||||
- cli/whkdrc.md
|
||||
- cli/state.md
|
||||
- cli/visible-windows.md
|
||||
- cli/query.md
|
||||
- cli/subscribe.md
|
||||
- cli/unsubscribe.md
|
||||
- cli/subscribe-socket.md
|
||||
- cli/unsubscribe-socket.md
|
||||
- cli/subscribe-pipe.md
|
||||
- cli/unsubscribe-pipe.md
|
||||
- cli/log.md
|
||||
- cli/quick-save-resize.md
|
||||
- cli/quick-load-resize.md
|
||||
@@ -160,7 +166,6 @@ nav:
|
||||
- cli/reload-configuration.md
|
||||
- cli/watch-configuration.md
|
||||
- cli/complete-configuration.md
|
||||
- cli/alt-focus-hack.md
|
||||
- cli/window-hiding-behaviour.md
|
||||
- cli/cross-monitor-move-behaviour.md
|
||||
- cli/toggle-cross-monitor-move-behaviour.md
|
||||
|
||||
@@ -44,7 +44,8 @@
|
||||
"enum": [
|
||||
"Exe",
|
||||
"Class",
|
||||
"Title"
|
||||
"Title",
|
||||
"Path"
|
||||
]
|
||||
},
|
||||
"ApplicationOptions": {
|
||||
|
||||
360
schema.json
360
schema.json
@@ -19,107 +19,129 @@
|
||||
"properties": {
|
||||
"monocle": {
|
||||
"description": "Border colour when the container is in monocle mode",
|
||||
"type": "object",
|
||||
"required": [
|
||||
"b",
|
||||
"g",
|
||||
"r"
|
||||
],
|
||||
"properties": {
|
||||
"b": {
|
||||
"description": "Blue",
|
||||
"type": "integer",
|
||||
"format": "uint32",
|
||||
"minimum": 0.0
|
||||
"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
|
||||
}
|
||||
}
|
||||
},
|
||||
"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"
|
||||
}
|
||||
}
|
||||
]
|
||||
},
|
||||
"single": {
|
||||
"description": "Border colour when the container contains a single window",
|
||||
"type": "object",
|
||||
"required": [
|
||||
"b",
|
||||
"g",
|
||||
"r"
|
||||
],
|
||||
"properties": {
|
||||
"b": {
|
||||
"description": "Blue",
|
||||
"type": "integer",
|
||||
"format": "uint32",
|
||||
"minimum": 0.0
|
||||
"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
|
||||
}
|
||||
}
|
||||
},
|
||||
"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"
|
||||
}
|
||||
}
|
||||
]
|
||||
},
|
||||
"stack": {
|
||||
"description": "Border colour when the container contains multiple windows",
|
||||
"type": "object",
|
||||
"required": [
|
||||
"b",
|
||||
"g",
|
||||
"r"
|
||||
],
|
||||
"properties": {
|
||||
"b": {
|
||||
"description": "Blue",
|
||||
"type": "integer",
|
||||
"format": "uint32",
|
||||
"minimum": 0.0
|
||||
"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
|
||||
}
|
||||
}
|
||||
},
|
||||
"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_offset": {
|
||||
"description": "Offset of the active window border (default: None)",
|
||||
"type": "integer",
|
||||
"format": "int32"
|
||||
},
|
||||
"active_window_border_width": {
|
||||
"description": "Width of the active window border (default: 20)",
|
||||
"type": "integer",
|
||||
"format": "int32"
|
||||
},
|
||||
"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",
|
||||
"format": "int32"
|
||||
},
|
||||
"border_overflow_applications": {
|
||||
"description": "Identify border overflow applications",
|
||||
"type": "array",
|
||||
@@ -138,7 +160,8 @@
|
||||
"enum": [
|
||||
"Exe",
|
||||
"Class",
|
||||
"Title"
|
||||
"Title",
|
||||
"Path"
|
||||
]
|
||||
},
|
||||
"matching_strategy": {
|
||||
@@ -155,6 +178,11 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"border_width": {
|
||||
"description": "Width of the window border (default: 8)",
|
||||
"type": "integer",
|
||||
"format": "int32"
|
||||
},
|
||||
"cross_monitor_move_behaviour": {
|
||||
"description": "Determine what happens when a window is moved across a monitor boundary (default: Swap)",
|
||||
"oneOf": [
|
||||
@@ -209,7 +237,8 @@
|
||||
"enum": [
|
||||
"Exe",
|
||||
"Class",
|
||||
"Title"
|
||||
"Title",
|
||||
"Path"
|
||||
]
|
||||
},
|
||||
"matching_strategy": {
|
||||
@@ -278,7 +307,7 @@
|
||||
}
|
||||
},
|
||||
"invisible_borders": {
|
||||
"description": "Dimensions of Windows' own invisible borders; don't set these yourself unless you are told to",
|
||||
"description": "DEPRECATED from v0.1.22: no longer required",
|
||||
"type": "object",
|
||||
"required": [
|
||||
"bottom",
|
||||
@@ -327,7 +356,8 @@
|
||||
"enum": [
|
||||
"Exe",
|
||||
"Class",
|
||||
"Title"
|
||||
"Title",
|
||||
"Path"
|
||||
]
|
||||
},
|
||||
"matching_strategy": {
|
||||
@@ -362,7 +392,8 @@
|
||||
"enum": [
|
||||
"Exe",
|
||||
"Class",
|
||||
"Title"
|
||||
"Title",
|
||||
"Path"
|
||||
]
|
||||
},
|
||||
"matching_strategy": {
|
||||
@@ -498,7 +529,8 @@
|
||||
"enum": [
|
||||
"Exe",
|
||||
"Class",
|
||||
"Title"
|
||||
"Title",
|
||||
"Path"
|
||||
]
|
||||
},
|
||||
"matching_strategy": {
|
||||
@@ -524,7 +556,8 @@
|
||||
"Rows",
|
||||
"VerticalStack",
|
||||
"HorizontalStack",
|
||||
"UltrawideVerticalStack"
|
||||
"UltrawideVerticalStack",
|
||||
"Grid"
|
||||
]
|
||||
},
|
||||
"layout_rules": {
|
||||
@@ -538,7 +571,8 @@
|
||||
"Rows",
|
||||
"VerticalStack",
|
||||
"HorizontalStack",
|
||||
"UltrawideVerticalStack"
|
||||
"UltrawideVerticalStack",
|
||||
"Grid"
|
||||
]
|
||||
}
|
||||
},
|
||||
@@ -569,7 +603,8 @@
|
||||
"enum": [
|
||||
"Exe",
|
||||
"Class",
|
||||
"Title"
|
||||
"Title",
|
||||
"Path"
|
||||
]
|
||||
},
|
||||
"matching_strategy": {
|
||||
@@ -614,7 +649,8 @@
|
||||
"enum": [
|
||||
"Exe",
|
||||
"Class",
|
||||
"Title"
|
||||
"Title",
|
||||
"Path"
|
||||
]
|
||||
},
|
||||
"matching_strategy": {
|
||||
@@ -636,6 +672,143 @@
|
||||
"type": "integer",
|
||||
"format": "int32"
|
||||
},
|
||||
"stackbar": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"height": {
|
||||
"type": "integer",
|
||||
"format": "int32"
|
||||
},
|
||||
"mode": {
|
||||
"type": "string",
|
||||
"enum": [
|
||||
"Always",
|
||||
"Never",
|
||||
"OnStack"
|
||||
]
|
||||
},
|
||||
"tabs": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"background": {
|
||||
"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"
|
||||
}
|
||||
]
|
||||
},
|
||||
"focused_text": {
|
||||
"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"
|
||||
}
|
||||
]
|
||||
},
|
||||
"unfocused_text": {
|
||||
"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"
|
||||
}
|
||||
]
|
||||
},
|
||||
"width": {
|
||||
"type": "integer",
|
||||
"format": "int32"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"tray_and_multi_window_applications": {
|
||||
"description": "Identify tray and multi-window applications",
|
||||
"type": "array",
|
||||
@@ -654,7 +827,8 @@
|
||||
"enum": [
|
||||
"Exe",
|
||||
"Class",
|
||||
"Title"
|
||||
"Title",
|
||||
"Path"
|
||||
]
|
||||
},
|
||||
"matching_strategy": {
|
||||
|
||||
@@ -1,63 +0,0 @@
|
||||
.shell powershell
|
||||
|
||||
# Reload whkd configuration
|
||||
# alt + o : taskkill /f /im whkd.exe && start /b whkd # if shell is cmd
|
||||
alt + o : taskkill /f /im whkd.exe; Start-Process whkd -WindowStyle hidden # if shell is pwsh / powershell
|
||||
alt + shift + o : komorebic reload-configuration
|
||||
|
||||
# App shortcuts - these require shell to be pwsh / powershell
|
||||
# The apps will be focused if open, or launched if not open
|
||||
# alt + f : if ($wshell.AppActivate('Firefox') -eq $False) { start firefox }
|
||||
# alt + b : if ($wshell.AppActivate('Chrome') -eq $False) { start chrome }
|
||||
|
||||
# Focus windows
|
||||
alt + h : komorebic focus left
|
||||
alt + j : komorebic focus down
|
||||
alt + k : komorebic focus up
|
||||
alt + l : komorebic focus right
|
||||
alt + shift + oem_4 : komorebic cycle-focus previous # oem_4 is [
|
||||
alt + shift + oem_6 : komorebic cycle-focus next # oem_6 is ]
|
||||
|
||||
# Move windows
|
||||
alt + shift + h : komorebic move left
|
||||
alt + shift + j : komorebic move down
|
||||
alt + shift + k : komorebic move up
|
||||
alt + shift + l : komorebic move right
|
||||
alt + shift + return : komorebic promote
|
||||
|
||||
# Stack windows
|
||||
alt + left : komorebic stack left
|
||||
alt + down : komorebic stack down
|
||||
alt + up : komorebic stack up
|
||||
alt + right : komorebic stack right
|
||||
alt + oem_1 : komorebic unstack # oem_1 is ;
|
||||
alt + oem_4 : komorebic cycle-stack previous # oem_4 is [
|
||||
alt + oem_6 : komorebic cycle-stack next # oem_6 is ]
|
||||
|
||||
# Resize
|
||||
alt + oem_plus : komorebic resize-axis horizontal increase
|
||||
alt + oem_minus : komorebic resize-axis horizontal decrease
|
||||
alt + shift + oem_plus : komorebic resize-axis vertical increase
|
||||
alt + shift + oem_minus : komorebic resize-axis vertical decrease
|
||||
|
||||
# Manipulate windows
|
||||
alt + t : komorebic toggle-float
|
||||
alt + shift + f : komorebic toggle-monocle
|
||||
|
||||
# Window manager options
|
||||
alt + shift + r : komorebic retile
|
||||
alt + p : komorebic toggle-pause
|
||||
|
||||
# Layouts
|
||||
alt + x : komorebic flip-layout horizontal
|
||||
alt + y : komorebic flip-layout vertical
|
||||
|
||||
# Workspaces
|
||||
alt + 1 : komorebic focus-workspace 0
|
||||
alt + 2 : komorebic focus-workspace 1
|
||||
alt + 3 : komorebic focus-workspace 2
|
||||
|
||||
# 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
|
||||
Reference in New Issue
Block a user