mirror of
https://github.com/LGUG2Z/komorebi.git
synced 2026-01-20 06:18:04 +01:00
Compare commits
77 Commits
v0.1.17
...
feature/fo
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
b101ea5916 | ||
|
|
8afad7246f | ||
|
|
8e9d63cd36 | ||
|
|
a673c69744 | ||
|
|
c0b65677a2 | ||
|
|
0cf7147e39 | ||
|
|
b39e65642b | ||
|
|
a68f3843b7 | ||
|
|
b3e989c6c7 | ||
|
|
4534a1511e | ||
|
|
1f87e3acd2 | ||
|
|
f67d0c7b9b | ||
|
|
aa0277d58c | ||
|
|
42ac13e0bd | ||
|
|
2a1d87d41b | ||
|
|
0d1595e543 | ||
|
|
4283dfb0ba | ||
|
|
2a6aa09b07 | ||
|
|
8c6bd13511 | ||
|
|
ef61239580 | ||
|
|
96bf37b98d | ||
|
|
5fd90d222d | ||
|
|
6ffdc1e90e | ||
|
|
2595fa601f | ||
|
|
41cd5ec222 | ||
|
|
e75ab17602 | ||
|
|
79dfa45ec4 | ||
|
|
8771aab3f3 | ||
|
|
f659deb5e3 | ||
|
|
e9fb9297e9 | ||
|
|
557d1962a7 | ||
|
|
d7991d2087 | ||
|
|
5606f1c5c4 | ||
|
|
41a627e7dd | ||
|
|
2b49ade205 | ||
|
|
a0bebc7be8 | ||
|
|
038256de6b | ||
|
|
8c30b894fe | ||
|
|
fce86397a5 | ||
|
|
f55b10caa0 | ||
|
|
ce9d23e72e | ||
|
|
d79e54fad8 | ||
|
|
a15c769bfc | ||
|
|
89aa0387d7 | ||
|
|
1e23a11b94 | ||
|
|
e5a8146927 | ||
|
|
b048e7c3aa | ||
|
|
3ff3961381 | ||
|
|
0797316ee6 | ||
|
|
57cc02f083 | ||
|
|
45912745d6 | ||
|
|
0088ca8cdb | ||
|
|
6c912b660e | ||
|
|
f02b7229fd | ||
|
|
ffaab771f3 | ||
|
|
710a8d76ea | ||
|
|
09792a7924 | ||
|
|
c1061dd3be | ||
|
|
f8a096090f | ||
|
|
e4ccd45176 | ||
|
|
f4e0fb9469 | ||
|
|
d8892f3ff2 | ||
|
|
fec56b1971 | ||
|
|
15aedf1ead | ||
|
|
86ed7e1297 | ||
|
|
c132665ac7 | ||
|
|
cfe4062369 | ||
|
|
acf201d9bc | ||
|
|
fa4d14799e | ||
|
|
6979f4e699 | ||
|
|
d3585ad720 | ||
|
|
e240bc7706 | ||
|
|
0ed732d085 | ||
|
|
fa11594103 | ||
|
|
88aaf1f368 | ||
|
|
c5bf62fac0 | ||
|
|
528a0bf9ff |
4
.github/workflows/windows.yaml
vendored
4
.github/workflows/windows.yaml
vendored
@@ -28,7 +28,7 @@ jobs:
|
||||
target:
|
||||
- x86_64-pc-windows-msvc
|
||||
steps:
|
||||
- uses: actions/checkout@v3
|
||||
- uses: actions/checkout@v4
|
||||
with:
|
||||
fetch-depth: 0
|
||||
- name: Prep cargo dirs
|
||||
@@ -116,7 +116,7 @@ jobs:
|
||||
|
||||
winget:
|
||||
name: Publish to WinGet
|
||||
runs-on: windows-latest
|
||||
runs-on: ubuntu-latest
|
||||
needs: build
|
||||
if: startsWith(github.ref, 'refs/tags/v')
|
||||
|
||||
|
||||
851
Cargo.lock
generated
851
Cargo.lock
generated
File diff suppressed because it is too large
Load Diff
10
Cargo.toml
10
Cargo.toml
@@ -1,5 +1,6 @@
|
||||
[workspace]
|
||||
|
||||
resolver = "2"
|
||||
members = [
|
||||
"derive-ahk",
|
||||
"komorebi",
|
||||
@@ -8,11 +9,14 @@ members = [
|
||||
]
|
||||
|
||||
[workspace.dependencies]
|
||||
windows-interface = { version = "0.48" }
|
||||
windows-implement = { version = "0.48" }
|
||||
windows-interface = { version = "0.52" }
|
||||
windows-implement = { version = "0.52" }
|
||||
dunce = "1"
|
||||
dirs = "5"
|
||||
color-eyre = "0.6"
|
||||
|
||||
[workspace.dependencies.windows]
|
||||
version = "0.48"
|
||||
version = "0.52"
|
||||
features = [
|
||||
"implement",
|
||||
"Win32_System_Com",
|
||||
|
||||
62
README.md
62
README.md
@@ -13,14 +13,14 @@ Tiling Window Management for Windows.
|
||||
<a href="https://github.com/sponsors/LGUG2Z">
|
||||
<img alt="GitHub Sponsors" src="https://img.shields.io/github/sponsors/LGUG2Z">
|
||||
</a>
|
||||
<a href="https://ko-fi.com/lgug2z">
|
||||
<img alt="Ko-fi" src="https://img.shields.io/badge/kofi-tip-green">
|
||||
</a>
|
||||
<a href="https://notado.app/feeds/jado/software-development">
|
||||
<img alt="Notado Feed" src="https://img.shields.io/badge/Notado-Subscribe-informational">
|
||||
</a>
|
||||
<a href="https://jeezy.substack.com">
|
||||
<img alt="Substack Read" src="https://img.shields.io/badge/Substack-Read-orange">
|
||||
</a>
|
||||
<a href="https://twitter.com/LGUG2Z">
|
||||
<img alt="Twitter Follow" src="https://img.shields.io/twitter/follow/LGUG2Z">
|
||||
<a href="https://www.youtube.com/channel/UCeai3-do-9O4MNy9_xjO6mg?sub_confirmation=1">
|
||||
<img alt="YouTube" src="https://img.shields.io/youtube/channel/subscribers/UCeai3-do-9O4MNy9_xjO6mg">
|
||||
</a>
|
||||
</p>
|
||||
|
||||
@@ -88,21 +88,20 @@ Articles, blog posts, demos, and videos about _komorebi_ can be added to this li
|
||||
_komorebi_ is a free and open-source project, and one that encourages you to make charitable donations if
|
||||
you find the software to be useful and have the financial means.
|
||||
|
||||
I encourage you to make a charitable donation
|
||||
to [Fresh Start Refugee](https://www.freshstartrefugee.org/donate) before
|
||||
you consider sponsoring me on GitHub.
|
||||
I encourage you to make a charitable donation to the [Palestine Children's
|
||||
Relief Fund](https://pcrf1.app.neoncrm.com/forms/gaza-recovery) and [Fresh
|
||||
Start Refugee](https://www.freshstartrefugee.org/donate) before you consider
|
||||
sponsoring me on GitHub.
|
||||
|
||||
## GitHub Sponsors
|
||||
## Project Sponsorship
|
||||
|
||||
[GitHub Sponsors is enabled for this project](https://github.com/sponsors/LGUG2Z). Users who sponsor my work
|
||||
on `komorebi` at any of the predefined monthly tiers will be given access to a private fork of this repository where I
|
||||
push features-in-progress that are not yet quite ready to be pushed on the main repository.
|
||||
[GitHub Sponsors is enabled for this
|
||||
project](https://github.com/sponsors/LGUG2Z). Unfortunately I don't have
|
||||
anything specific to offer besides my gratitude and shout outs at the end of
|
||||
_komorebi_ live development videos and tutorials.
|
||||
|
||||
There will never be any feature of `komorebi` that is gated behind sponsorship; every new feature will always be
|
||||
available for free in the public repository once it meets the requisite level of code quality and completion.
|
||||
|
||||
Features-in-progress that are available in early access will be tagged in the issues with
|
||||
an ["early access" label](https://github.com/LGUG2Z/komorebi/issues?q=is%3Aopen+is%3Aissue+label%3A%22early+access%22).
|
||||
If you would like to tip or sponsor the project but are unable to use GitHub
|
||||
Sponsors, you may also sponsor through [Ko-fi](https://ko-fi.com/lgug2z).
|
||||
|
||||
## Demonstrations
|
||||
|
||||
@@ -160,6 +159,13 @@ This means that:
|
||||
|
||||
### Quickstart
|
||||
|
||||
It highly recommended that you enable support for long paths in Windows by running the following command in an
|
||||
Administrator Terminal before starting with this quickstart guide:
|
||||
|
||||
```powershell
|
||||
Set-ItemProperty 'HKLM:\SYSTEM\CurrentControlSet\Control\FileSystem' -Name 'LongPathsEnabled' -Value 1
|
||||
```
|
||||
|
||||
Make sure that you have either the [Scoop Package Manager](https://scoop.sh) or WinGet installed, then run the following
|
||||
commands at a PowerShell prompt. If you are using WinGet, make sure that you open a new terminal window or reload your
|
||||
profile after running the installation steps. Since this is not required when using `scoop`, I personally recommend that
|
||||
@@ -180,27 +186,27 @@ winget install LGUG2Z.whkd
|
||||
winget install LGUG2Z.komorebi
|
||||
|
||||
# save the example configuration to ~/komorebi.json
|
||||
iwr https://raw.githubusercontent.com/LGUG2Z/komorebi/master/komorebi.example.json -OutFile $Env:USERPROFILE\komorebi.example.json
|
||||
iwr https://raw.githubusercontent.com/LGUG2Z/komorebi/v0.1.19/komorebi.example.json -OutFile "$Env:USERPROFILE\komorebi.json"
|
||||
|
||||
# save the latest generated app-specific config tweaks and fixes
|
||||
komorebic fetch-app-specific-configuration
|
||||
|
||||
# ensure the ~/.config folder exists
|
||||
mkdir $Env:USERPROFILE\.config -ea 0
|
||||
mkdir "$Env:USERPROFILE\.config" -ea 0
|
||||
|
||||
# save the sample whkdrc file with key bindings to ~/.config/whkdrc
|
||||
iwr https://raw.githubusercontent.com/LGUG2Z/komorebi/master/whkdrc.sample -OutFile $Env:USERPROFILE\.config\whkdrc
|
||||
iwr https://raw.githubusercontent.com/LGUG2Z/komorebi/v0.1.19/whkdrc.sample -OutFile "$Env:USERPROFILE\.config\whkdrc"
|
||||
|
||||
# start komorebi and whkd
|
||||
komorebic start -c $Env:USERPROFILE\komorebi.json --whkd
|
||||
komorebic start -c "$Env:USERPROFILE\komorebi.json" --whkd
|
||||
```
|
||||
|
||||
Thanks to [@sitiom](https://github.com/sitiom) for getting _komorebi_ added to both the popular Scoop Extras bucket and
|
||||
to WinGet.
|
||||
|
||||
<!-- You can watch a walkthrough video of this quickstart below on YouTube. -->
|
||||
You can watch a walkthrough video of this quickstart below on YouTube.
|
||||
|
||||
<!-- [](https://www.youtube.com/watch?v=cBnLIwMtv8g) -->
|
||||
[](https://www.youtube.com/watch?v=hDDxtvpjpHs)
|
||||
|
||||
#### Using Autohotkey
|
||||
|
||||
@@ -216,13 +222,13 @@ it solely to handle hotkey bindings.
|
||||
|
||||
```powershell
|
||||
# save the latest generated komorebic library to ~/komorebic.lib.ahk
|
||||
iwr https://raw.githubusercontent.com/LGUG2Z/komorebi/master/komorebic.lib.ahk -OutFile $Env:USERPROFILE\komorebic.lib.ahk
|
||||
iwr https://raw.githubusercontent.com/LGUG2Z/komorebi/v0.1.19/komorebic.lib.ahk -OutFile $Env:USERPROFILE\komorebic.lib.ahk
|
||||
|
||||
# save the latest generated app-specific config tweaks and fixes to ~/komorebi.generated.ahk
|
||||
iwr https://raw.githubusercontent.com/LGUG2Z/komorebi/master/komorebi.generated.ahk -OutFile $Env:USERPROFILE\komorebi.generated.ahk
|
||||
iwr https://raw.githubusercontent.com/LGUG2Z/komorebi/v0.1.19/komorebi.generated.ahk -OutFile $Env:USERPROFILE\komorebi.generated.ahk
|
||||
|
||||
# save the sample komorebi configuration file to ~/komorebi.ahk
|
||||
iwr https://raw.githubusercontent.com/LGUG2Z/komorebi/master/komorebi.sample.ahk -OutFile $Env:USERPROFILE\komorebi.ahk
|
||||
iwr https://raw.githubusercontent.com/LGUG2Z/komorebi/v0.1.19/komorebi.sample.ahk -OutFile $Env:USERPROFILE\komorebi.ahk
|
||||
```
|
||||
|
||||
### GitHub Releases
|
||||
@@ -272,7 +278,7 @@ of the window manager. There is a [complete JSON Schema for this configuration f
|
||||
users with auto-completions in their editors.
|
||||
|
||||
If you are running with a dynamic configuration script as recommended in v0.1.16 and earlier, `komorebi` will find the
|
||||
sample `komorebi.ps1` file in your `$Env:USERPROFILE` directory and automatically load it. This file also starts `whkd` using the sample `whkrc` file
|
||||
sample `komorebi.ps1` file in your `$Env:USERPROFILE` directory and automatically load it. This file also starts `whkd` using the sample `whkdrc` file
|
||||
in your `$Env:USERPROFILE\.config` directory.
|
||||
|
||||
Alternatively, if you have AutoHotKey installed and a `komorebi.ahk` file in `$Env:UserProfile` directory, `komorebi`
|
||||
@@ -293,6 +299,8 @@ There are four configuration options that you may need to set yourself, if you m
|
||||
- The applications.yaml path
|
||||
- Any individual application rules you have that are not in applications.yaml
|
||||
|
||||
[](https://www.youtube.com/watch?v=yqCAOJgL3C0)
|
||||
|
||||
#### Configuration with `komorebic`
|
||||
|
||||
As previously mentioned, this project does not handle anything related to keybindings and shortcuts directly. I
|
||||
|
||||
@@ -1,16 +1,18 @@
|
||||
[package]
|
||||
name = "komorebi-core"
|
||||
version = "0.1.17"
|
||||
version = "0.1.19"
|
||||
edition = "2021"
|
||||
|
||||
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
|
||||
|
||||
[dependencies]
|
||||
clap = { version = "4", features = ["derive"] }
|
||||
color-eyre = "0.6"
|
||||
serde = { version = "1", features = ["derive"] }
|
||||
serde_json = "1"
|
||||
serde_yaml = "0.9"
|
||||
strum = { version = "0.25", features = ["derive"] }
|
||||
schemars = "0.8"
|
||||
color-eyre = { workspace = true }
|
||||
windows = { workspace = true }
|
||||
dunce = { workspace = true }
|
||||
dirs = { workspace = true }
|
||||
|
||||
@@ -130,85 +130,7 @@ impl Arrangement for DefaultLayout {
|
||||
|
||||
layouts
|
||||
}
|
||||
Self::UltrawideVerticalStack => {
|
||||
let mut layouts: Vec<Rect> = vec![];
|
||||
|
||||
let primary_right = match len {
|
||||
1 => area.right,
|
||||
_ => area.right / 2,
|
||||
};
|
||||
|
||||
let secondary_right = match len {
|
||||
1 => 0,
|
||||
2 => area.right - primary_right,
|
||||
_ => (area.right - primary_right) / 2,
|
||||
};
|
||||
|
||||
let (primary_left, secondary_left, stack_left) = match len {
|
||||
1 => (area.left, 0, 0),
|
||||
2 => {
|
||||
let mut primary = area.left + secondary_right;
|
||||
let mut secondary = area.left;
|
||||
|
||||
match layout_flip {
|
||||
Some(Axis::Horizontal | Axis::HorizontalAndVertical) if len > 1 => {
|
||||
primary = area.left;
|
||||
secondary = area.left + primary_right;
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
|
||||
(primary, secondary, 0)
|
||||
}
|
||||
_ => {
|
||||
let primary = area.left + secondary_right;
|
||||
let mut secondary = area.left;
|
||||
let mut stack = area.left + primary_right + secondary_right;
|
||||
|
||||
match layout_flip {
|
||||
Some(Axis::Horizontal | Axis::HorizontalAndVertical) if len > 1 => {
|
||||
secondary = area.left + primary_right + secondary_right;
|
||||
stack = area.left;
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
|
||||
(primary, secondary, stack)
|
||||
}
|
||||
};
|
||||
|
||||
if len >= 1 {
|
||||
layouts.push(Rect {
|
||||
left: primary_left,
|
||||
top: area.top,
|
||||
right: primary_right,
|
||||
bottom: area.bottom,
|
||||
});
|
||||
|
||||
if len >= 2 {
|
||||
layouts.push(Rect {
|
||||
left: secondary_left,
|
||||
top: area.top,
|
||||
right: secondary_right,
|
||||
bottom: area.bottom,
|
||||
});
|
||||
|
||||
if len > 2 {
|
||||
layouts.append(&mut rows(
|
||||
&Rect {
|
||||
left: stack_left,
|
||||
top: area.top,
|
||||
right: secondary_right,
|
||||
bottom: area.bottom,
|
||||
},
|
||||
len - 2,
|
||||
));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
layouts
|
||||
}
|
||||
Self::UltrawideVerticalStack => ultrawide(area, len, layout_flip, resize_dimensions),
|
||||
};
|
||||
|
||||
dimensions
|
||||
@@ -586,3 +508,190 @@ fn recursive_fibonacci(
|
||||
res
|
||||
}
|
||||
}
|
||||
|
||||
fn calculate_ultrawide_adjustment(resize_dimensions: &[Option<Rect>]) -> Vec<Rect> {
|
||||
let len = resize_dimensions.len();
|
||||
let mut result = vec![Rect::default(); len];
|
||||
match len {
|
||||
// One container can't be resized
|
||||
0 | 1 => (),
|
||||
2 => {
|
||||
let (primary, secondary) = result.split_at_mut(1);
|
||||
let primary = &mut primary[0];
|
||||
let secondary = &mut secondary[0];
|
||||
// With two containers on screen container 0 is on the right
|
||||
if let Some(resize_primary) = resize_dimensions[0] {
|
||||
resize_left(primary, resize_primary.left);
|
||||
resize_right(secondary, resize_primary.left);
|
||||
}
|
||||
|
||||
if let Some(resize_secondary) = resize_dimensions[1] {
|
||||
resize_left(primary, resize_secondary.right);
|
||||
resize_right(secondary, resize_secondary.right);
|
||||
}
|
||||
}
|
||||
_ => {
|
||||
let (primary, rest) = result.split_at_mut(1);
|
||||
let (secondary, tertiary) = rest.split_at_mut(1);
|
||||
let primary = &mut primary[0];
|
||||
let secondary = &mut secondary[0];
|
||||
// With three or more containers container 0 is in the center
|
||||
if let Some(resize_primary) = resize_dimensions[0] {
|
||||
resize_left(primary, resize_primary.left);
|
||||
resize_right(primary, resize_primary.right);
|
||||
|
||||
resize_right(secondary, resize_primary.left);
|
||||
|
||||
for vertical_element in &mut *tertiary {
|
||||
resize_left(vertical_element, resize_primary.right);
|
||||
}
|
||||
}
|
||||
|
||||
// Container 1 is on the left
|
||||
if let Some(resize_secondary) = resize_dimensions[1] {
|
||||
resize_left(primary, resize_secondary.right);
|
||||
resize_right(secondary, resize_secondary.right);
|
||||
}
|
||||
|
||||
// Handle stack on the right
|
||||
for (i, rect) in resize_dimensions[2..].iter().enumerate() {
|
||||
if let Some(rect) = rect {
|
||||
resize_right(primary, rect.left);
|
||||
tertiary
|
||||
.iter_mut()
|
||||
.for_each(|vertical_element| resize_left(vertical_element, rect.left));
|
||||
|
||||
// Containers in stack except first can be resized up displacing container
|
||||
// above them
|
||||
if i != 0 {
|
||||
resize_bottom(&mut tertiary[i - 1], rect.top);
|
||||
resize_top(&mut tertiary[i], rect.top);
|
||||
}
|
||||
|
||||
// Containers in stack except last can be resized down displacing container
|
||||
// below them
|
||||
if i != tertiary.len() - 1 {
|
||||
resize_bottom(&mut tertiary[i], rect.bottom);
|
||||
resize_top(&mut tertiary[i + 1], rect.bottom);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
result
|
||||
}
|
||||
|
||||
fn resize_left(rect: &mut Rect, resize: i32) {
|
||||
rect.left += resize / 2;
|
||||
rect.right += -resize / 2;
|
||||
}
|
||||
|
||||
fn resize_right(rect: &mut Rect, resize: i32) {
|
||||
rect.right += resize / 2;
|
||||
}
|
||||
|
||||
fn resize_top(rect: &mut Rect, resize: i32) {
|
||||
rect.top += resize / 2;
|
||||
rect.bottom += -resize / 2;
|
||||
}
|
||||
|
||||
fn resize_bottom(rect: &mut Rect, resize: i32) {
|
||||
rect.bottom += resize / 2;
|
||||
}
|
||||
|
||||
fn ultrawide(
|
||||
area: &Rect,
|
||||
len: usize,
|
||||
layout_flip: Option<Axis>,
|
||||
resize_dimensions: &[Option<Rect>],
|
||||
) -> Vec<Rect> {
|
||||
let mut layouts: Vec<Rect> = vec![];
|
||||
|
||||
let primary_right = match len {
|
||||
1 => area.right,
|
||||
_ => area.right / 2,
|
||||
};
|
||||
|
||||
let secondary_right = match len {
|
||||
1 => 0,
|
||||
2 => area.right - primary_right,
|
||||
_ => (area.right - primary_right) / 2,
|
||||
};
|
||||
|
||||
let (primary_left, secondary_left, stack_left) = match len {
|
||||
1 => (area.left, 0, 0),
|
||||
2 => {
|
||||
let mut primary = area.left + secondary_right;
|
||||
let mut secondary = area.left;
|
||||
|
||||
match layout_flip {
|
||||
Some(Axis::Horizontal | Axis::HorizontalAndVertical) if len > 1 => {
|
||||
primary = area.left;
|
||||
secondary = area.left + primary_right;
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
|
||||
(primary, secondary, 0)
|
||||
}
|
||||
_ => {
|
||||
let primary = area.left + secondary_right;
|
||||
let mut secondary = area.left;
|
||||
let mut stack = area.left + primary_right + secondary_right;
|
||||
|
||||
match layout_flip {
|
||||
Some(Axis::Horizontal | Axis::HorizontalAndVertical) if len > 1 => {
|
||||
secondary = area.left + primary_right + secondary_right;
|
||||
stack = area.left;
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
|
||||
(primary, secondary, stack)
|
||||
}
|
||||
};
|
||||
|
||||
if len >= 1 {
|
||||
layouts.push(Rect {
|
||||
left: primary_left,
|
||||
top: area.top,
|
||||
right: primary_right,
|
||||
bottom: area.bottom,
|
||||
});
|
||||
|
||||
if len >= 2 {
|
||||
layouts.push(Rect {
|
||||
left: secondary_left,
|
||||
top: area.top,
|
||||
right: secondary_right,
|
||||
bottom: area.bottom,
|
||||
});
|
||||
|
||||
if len > 2 {
|
||||
layouts.append(&mut rows(
|
||||
&Rect {
|
||||
left: stack_left,
|
||||
top: area.top,
|
||||
right: secondary_right,
|
||||
bottom: area.bottom,
|
||||
},
|
||||
len - 2,
|
||||
));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
let adjustment = calculate_ultrawide_adjustment(resize_dimensions);
|
||||
layouts
|
||||
.iter_mut()
|
||||
.zip(adjustment.iter())
|
||||
.for_each(|(layout, adjustment)| {
|
||||
layout.top += adjustment.top;
|
||||
layout.bottom += adjustment.bottom;
|
||||
layout.left += adjustment.left;
|
||||
layout.right += adjustment.right;
|
||||
});
|
||||
|
||||
layouts
|
||||
}
|
||||
|
||||
@@ -50,10 +50,22 @@ impl ApplicationOptions {
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Serialize, Deserialize, JsonSchema)]
|
||||
#[derive(Clone, Debug, Eq, PartialEq, Serialize, Deserialize, JsonSchema)]
|
||||
pub struct IdWithIdentifier {
|
||||
pub kind: ApplicationIdentifier,
|
||||
pub id: String,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub matching_strategy: Option<MatchingStrategy>,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Eq, PartialEq, Serialize, Deserialize, JsonSchema)]
|
||||
pub enum MatchingStrategy {
|
||||
Legacy,
|
||||
Equals,
|
||||
StartsWith,
|
||||
EndsWith,
|
||||
Contains,
|
||||
Regex,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Serialize, Deserialize, JsonSchema)]
|
||||
@@ -62,6 +74,18 @@ pub struct IdWithIdentifierAndComment {
|
||||
pub id: String,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub comment: Option<String>,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub matching_strategy: Option<MatchingStrategy>,
|
||||
}
|
||||
|
||||
impl From<IdWithIdentifierAndComment> for IdWithIdentifier {
|
||||
fn from(value: IdWithIdentifierAndComment) -> Self {
|
||||
Self {
|
||||
kind: value.kind,
|
||||
id: value.id.clone(),
|
||||
matching_strategy: value.matching_strategy,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Serialize, Deserialize, JsonSchema)]
|
||||
@@ -74,6 +98,19 @@ pub struct ApplicationConfiguration {
|
||||
pub float_identifiers: Option<Vec<IdWithIdentifierAndComment>>,
|
||||
}
|
||||
|
||||
impl ApplicationConfiguration {
|
||||
pub fn populate_default_matching_strategies(&mut self) {
|
||||
if self.identifier.matching_strategy.is_none() {
|
||||
match self.identifier.kind {
|
||||
ApplicationIdentifier::Exe => {
|
||||
self.identifier.matching_strategy = Option::from(MatchingStrategy::Equals);
|
||||
}
|
||||
ApplicationIdentifier::Class | ApplicationIdentifier::Title => {}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Serialize, Deserialize, JsonSchema)]
|
||||
pub struct ApplicationConfigurationGenerator;
|
||||
|
||||
@@ -84,6 +121,10 @@ impl ApplicationConfigurationGenerator {
|
||||
|
||||
pub fn format(content: &str) -> Result<String> {
|
||||
let mut cfgen = Self::load(content)?;
|
||||
for cfg in &mut cfgen {
|
||||
cfg.populate_default_matching_strategies();
|
||||
}
|
||||
|
||||
cfgen.sort_by(|a, b| a.name.cmp(&b.name));
|
||||
Ok(serde_yaml::to_string(&cfgen)?)
|
||||
}
|
||||
|
||||
@@ -3,9 +3,10 @@ use std::fs::File;
|
||||
use std::io::BufReader;
|
||||
use std::ops::Deref;
|
||||
use std::ops::DerefMut;
|
||||
use std::path::PathBuf;
|
||||
use std::path::Path;
|
||||
|
||||
use color_eyre::eyre::anyhow;
|
||||
use color_eyre::eyre::bail;
|
||||
use color_eyre::Result;
|
||||
use schemars::JsonSchema;
|
||||
use serde::Deserialize;
|
||||
@@ -31,23 +32,20 @@ impl DerefMut for CustomLayout {
|
||||
}
|
||||
|
||||
impl CustomLayout {
|
||||
pub fn from_path_buf(path: PathBuf) -> Result<Self> {
|
||||
let invalid_filetype = anyhow!("custom layouts must be json or yaml files");
|
||||
pub fn from_path<P: AsRef<Path>>(path: P) -> Result<Self> {
|
||||
let path = path.as_ref();
|
||||
let layout: Self = match path.extension() {
|
||||
Some(extension) => {
|
||||
if extension == "yaml" || extension == "yml" {
|
||||
serde_yaml::from_reader(BufReader::new(File::open(path)?))?
|
||||
} else if extension == "json" {
|
||||
serde_json::from_reader(BufReader::new(File::open(path)?))?
|
||||
} else {
|
||||
return Err(invalid_filetype);
|
||||
}
|
||||
Some(extension) if extension == "yaml" || extension == "yml" => {
|
||||
serde_json::from_reader(BufReader::new(File::open(path)?))?
|
||||
}
|
||||
None => return Err(invalid_filetype),
|
||||
Some(extension) if extension == "json" => {
|
||||
serde_json::from_reader(BufReader::new(File::open(path)?))?
|
||||
}
|
||||
_ => return Err(anyhow!("custom layouts must be json or yaml files")),
|
||||
};
|
||||
|
||||
if !layout.is_valid() {
|
||||
return Err(anyhow!("the layout file provided was invalid"));
|
||||
bail!("the layout file provided was invalid");
|
||||
}
|
||||
|
||||
Ok(layout)
|
||||
|
||||
@@ -20,6 +20,7 @@ pub enum DefaultLayout {
|
||||
VerticalStack,
|
||||
HorizontalStack,
|
||||
UltrawideVerticalStack,
|
||||
// NOTE: If any new layout is added, please make sure to register the same in `DefaultLayout::cycle`
|
||||
}
|
||||
|
||||
impl DefaultLayout {
|
||||
@@ -33,7 +34,7 @@ impl DefaultLayout {
|
||||
sizing: Sizing,
|
||||
delta: i32,
|
||||
) -> Option<Rect> {
|
||||
if !matches!(self, Self::BSP) {
|
||||
if !matches!(self, Self::BSP) && !matches!(self, Self::UltrawideVerticalStack) {
|
||||
return None;
|
||||
};
|
||||
|
||||
@@ -125,4 +126,28 @@ impl DefaultLayout {
|
||||
Option::from(r)
|
||||
}
|
||||
}
|
||||
|
||||
#[must_use]
|
||||
pub const fn cycle_next(self) -> Self {
|
||||
match self {
|
||||
Self::BSP => Self::Columns,
|
||||
Self::Columns => Self::Rows,
|
||||
Self::Rows => Self::VerticalStack,
|
||||
Self::VerticalStack => Self::HorizontalStack,
|
||||
Self::HorizontalStack => Self::UltrawideVerticalStack,
|
||||
Self::UltrawideVerticalStack => Self::BSP,
|
||||
}
|
||||
}
|
||||
|
||||
#[must_use]
|
||||
pub const fn cycle_previous(self) -> Self {
|
||||
match self {
|
||||
Self::BSP => Self::UltrawideVerticalStack,
|
||||
Self::UltrawideVerticalStack => Self::HorizontalStack,
|
||||
Self::HorizontalStack => Self::VerticalStack,
|
||||
Self::VerticalStack => Self::Rows,
|
||||
Self::Rows => Self::Columns,
|
||||
Self::Columns => Self::BSP,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,10 +1,12 @@
|
||||
#![warn(clippy::all, clippy::nursery, clippy::pedantic)]
|
||||
#![allow(clippy::missing_errors_doc, clippy::use_self)]
|
||||
|
||||
use std::path::Path;
|
||||
use std::path::PathBuf;
|
||||
use std::str::FromStr;
|
||||
|
||||
use clap::ValueEnum;
|
||||
use color_eyre::eyre::anyhow;
|
||||
use color_eyre::Result;
|
||||
use schemars::JsonSchema;
|
||||
use serde::Deserialize;
|
||||
@@ -77,6 +79,7 @@ pub enum SocketMessage {
|
||||
AdjustContainerPadding(Sizing, i32),
|
||||
AdjustWorkspacePadding(Sizing, i32),
|
||||
ChangeLayout(DefaultLayout),
|
||||
CycleLayout(CycleDirection),
|
||||
ChangeLayoutCustom(PathBuf),
|
||||
FlipLayout(Axis),
|
||||
// Monitor and Workspace Commands
|
||||
@@ -101,8 +104,10 @@ pub enum SocketMessage {
|
||||
FocusNamedWorkspace(String),
|
||||
ContainerPadding(usize, usize, i32),
|
||||
NamedWorkspaceContainerPadding(String, i32),
|
||||
FocusedWorkspaceContainerPadding(i32),
|
||||
WorkspacePadding(usize, usize, i32),
|
||||
NamedWorkspacePadding(String, i32),
|
||||
FocusedWorkspacePadding(i32),
|
||||
WorkspaceTiling(usize, usize, bool),
|
||||
NamedWorkspaceTiling(String, bool),
|
||||
WorkspaceName(usize, usize, String),
|
||||
@@ -192,7 +197,17 @@ pub enum StateQuery {
|
||||
}
|
||||
|
||||
#[derive(
|
||||
Copy, Clone, Debug, Serialize, Deserialize, Display, EnumString, ValueEnum, JsonSchema,
|
||||
Copy,
|
||||
Clone,
|
||||
Debug,
|
||||
Eq,
|
||||
PartialEq,
|
||||
Serialize,
|
||||
Deserialize,
|
||||
Display,
|
||||
EnumString,
|
||||
ValueEnum,
|
||||
JsonSchema,
|
||||
)]
|
||||
#[strum(serialize_all = "snake_case")]
|
||||
pub enum ApplicationIdentifier {
|
||||
@@ -285,3 +300,36 @@ impl Sizing {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub fn resolve_home_path<P: AsRef<Path>>(path: P) -> Result<PathBuf> {
|
||||
let mut resolved_path = PathBuf::new();
|
||||
let mut resolved = false;
|
||||
for c in path.as_ref().components() {
|
||||
match c {
|
||||
std::path::Component::Normal(c)
|
||||
if (c == "~" || c == "$Env:USERPROFILE" || c == "$HOME") && !resolved =>
|
||||
{
|
||||
let home = dirs::home_dir().ok_or_else(|| anyhow!("there is no home directory"))?;
|
||||
|
||||
resolved_path.extend(home.components());
|
||||
resolved = true;
|
||||
}
|
||||
|
||||
_ => resolved_path.push(c),
|
||||
}
|
||||
}
|
||||
|
||||
let parent = resolved_path
|
||||
.parent()
|
||||
.ok_or_else(|| anyhow!("cannot parse parent directory"))?;
|
||||
|
||||
Ok(if parent.is_dir() {
|
||||
let file = resolved_path
|
||||
.components()
|
||||
.last()
|
||||
.ok_or_else(|| anyhow!("cannot parse filename"))?;
|
||||
dunce::canonicalize(parent)?.join(file)
|
||||
} else {
|
||||
resolved_path
|
||||
})
|
||||
}
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
{
|
||||
"$schema": "https://raw.githubusercontent.com/LGUG2Z/komorebi/master/schema.json",
|
||||
"app_specific_configuration_path": "$Env:USERPROFILE/applications.yaml",
|
||||
"window_hiding_behaviour": "Cloak",
|
||||
"cross_monitor_move_behaviour": "Insert",
|
||||
|
||||
@@ -27,6 +27,10 @@ RunWait('komorebic.exe identify-tray-application exe "Akiflow.exe"', , "Hide")
|
||||
; Android Studio
|
||||
RunWait('komorebic.exe identify-object-name-change-application exe "studio64.exe"', , "Hide")
|
||||
|
||||
; Anki
|
||||
; If you have disabled minimize/close to tray for this application, you can delete/comment out the next line
|
||||
RunWait('komorebic.exe identify-tray-application exe "anki.exe"', , "Hide")
|
||||
|
||||
; ArmCord
|
||||
RunWait('komorebic.exe identify-border-overflow-application exe "ArmCord.exe"', , "Hide")
|
||||
; If you have disabled minimize/close to tray for this application, you can delete/comment out the next line
|
||||
@@ -36,6 +40,7 @@ RunWait('komorebic.exe identify-tray-application exe "ArmCord.exe"', , "Hide")
|
||||
; If you have disabled minimize/close to tray for this application, you can delete/comment out the next line
|
||||
RunWait('komorebic.exe identify-tray-application exe "AutoHotkeyU64.exe"', , "Hide")
|
||||
RunWait('komorebic.exe float-rule title "Window Spy"', , "Hide")
|
||||
RunWait('komorebic.exe float-rule exe "AutoHotkeyUX.exe"', , "Hide")
|
||||
|
||||
; Beeper
|
||||
RunWait('komorebic.exe identify-border-overflow-application exe "Beeper.exe"', , "Hide")
|
||||
@@ -52,6 +57,19 @@ RunWait('komorebic.exe float-rule exe "Bloxstrap.exe"', , "Hide")
|
||||
; Calculator
|
||||
RunWait('komorebic.exe float-rule title "Calculator"', , "Hide")
|
||||
|
||||
; Clash Verge
|
||||
RunWait('komorebic.exe identify-border-overflow-application exe "Clash Verge.exe"', , "Hide")
|
||||
; If you have disabled minimize/close to tray for this application, you can delete/comment out the next line
|
||||
RunWait('komorebic.exe identify-tray-application exe "Clash Verge.exe"', , "Hide")
|
||||
|
||||
; Clementine
|
||||
; If you have disabled minimize/close to tray for this application, you can delete/comment out the next line
|
||||
RunWait('komorebic.exe identify-tray-application exe "clementine.exe"', , "Hide")
|
||||
|
||||
; CopyQ
|
||||
; If you have disabled minimize/close to tray for this application, you can delete/comment out the next line
|
||||
RunWait('komorebic.exe identify-tray-application exe "copyq.exe"', , "Hide")
|
||||
|
||||
; Credential Manager UI Host
|
||||
; Targets the Windows popup prompting you for a PIN instead of a password on 1Password etc.
|
||||
RunWait('komorebic.exe float-rule exe "CredentialUIBroker.exe"', , "Hide")
|
||||
@@ -91,6 +109,9 @@ RunWait('komorebic.exe identify-border-overflow-application exe "DiscordPTB.exe"
|
||||
; If you have disabled minimize/close to tray for this application, you can delete/comment out the next line
|
||||
RunWait('komorebic.exe identify-tray-application exe "DiscordPTB.exe"', , "Hide")
|
||||
|
||||
; Docker Desktop
|
||||
RunWait('komorebic.exe identify-border-overflow-application exe "Docker Desktop.exe"', , "Hide")
|
||||
|
||||
; Dropbox
|
||||
RunWait('komorebic.exe float-rule exe "Dropbox.exe"', , "Hide")
|
||||
|
||||
@@ -123,6 +144,13 @@ RunWait('komorebic.exe identify-border-overflow-application exe "EpicGamesLaunch
|
||||
; If you have disabled minimize/close to tray for this application, you can delete/comment out the next line
|
||||
RunWait('komorebic.exe identify-tray-application exe "EpicGamesLauncher.exe"', , "Hide")
|
||||
|
||||
; Everything
|
||||
; If you have disabled minimize/close to tray for this application, you can delete/comment out the next line
|
||||
RunWait('komorebic.exe identify-tray-application exe "Everything.exe"', , "Hide")
|
||||
|
||||
; Figma
|
||||
RunWait('komorebic.exe identify-border-overflow-application exe "Figma.exe"', , "Hide")
|
||||
|
||||
; Flow Launcher
|
||||
RunWait('komorebic.exe identify-border-overflow-application exe "Flow.Launcher.exe"', , "Hide")
|
||||
|
||||
@@ -143,11 +171,17 @@ RunWait('komorebic.exe identify-border-overflow-application exe "GodotManager.ex
|
||||
RunWait('komorebic.exe manage-rule exe "GodotManager.exe"', , "Hide")
|
||||
RunWait('komorebic.exe identify-object-name-change-application exe "GodotManager.exe"', , "Hide")
|
||||
|
||||
; Golden Dict
|
||||
; If you have disabled minimize/close to tray for this application, you can delete/comment out the next line
|
||||
RunWait('komorebic.exe identify-tray-application exe "GoldenDict.exe"', , "Hide")
|
||||
|
||||
; Google Chrome
|
||||
; If you have disabled minimize/close to tray for this application, you can delete/comment out the next line
|
||||
RunWait('komorebic.exe identify-tray-application exe "chrome.exe"', , "Hide")
|
||||
|
||||
; Google Drive
|
||||
; If you have disabled minimize/close to tray for this application, you can delete/comment out the next line
|
||||
RunWait('komorebic.exe identify-tray-application exe "GoogleDriveFS.exe"', , "Hide")
|
||||
RunWait('komorebic.exe float-rule exe "GoogleDriveFS.exe"', , "Hide")
|
||||
|
||||
; Houdoku
|
||||
@@ -256,6 +290,10 @@ RunWait('komorebic.exe identify-border-overflow-application exe "NVIDIA GeForce
|
||||
; If you have disabled minimize/close to tray for this application, you can delete/comment out the next line
|
||||
RunWait('komorebic.exe identify-tray-application exe "NZXT CAM.exe"', , "Hide")
|
||||
|
||||
; NetEase Cloud Music
|
||||
; If you have disabled minimize/close to tray for this application, you can delete/comment out the next line
|
||||
RunWait('komorebic.exe identify-tray-application exe "cloudmusic.exe"', , "Hide")
|
||||
|
||||
; NiceHash Miner
|
||||
RunWait('komorebic.exe identify-border-overflow-application exe "nhm_app.exe"', , "Hide")
|
||||
RunWait('komorebic.exe manage-rule exe "nhm_app.exe"', , "Hide")
|
||||
@@ -288,6 +326,10 @@ RunWait('komorebic.exe manage-rule exe "Obsidian.exe"', , "Hide")
|
||||
; OneDrive
|
||||
RunWait('komorebic.exe float-rule class "OneDriveReactNativeWin32WindowClass"', , "Hide")
|
||||
|
||||
; OneQuick
|
||||
; If you have disabled minimize/close to tray for this application, you can delete/comment out the next line
|
||||
RunWait('komorebic.exe identify-tray-application exe "OneQuick.exe"', , "Hide")
|
||||
|
||||
; OpenRGB
|
||||
; If you have disabled minimize/close to tray for this application, you can delete/comment out the next line
|
||||
RunWait('komorebic.exe identify-tray-application exe "OpenRGB.exe"', , "Hide")
|
||||
@@ -295,6 +337,13 @@ RunWait('komorebic.exe identify-tray-application exe "OpenRGB.exe"', , "Hide")
|
||||
; Paradox Launcher
|
||||
RunWait('komorebic.exe float-rule exe "Paradox Launcher.exe"', , "Hide")
|
||||
|
||||
; Playnite
|
||||
RunWait('komorebic.exe identify-border-overflow-application exe "Playnite.DesktopApp.exe"', , "Hide")
|
||||
; If you have disabled minimize/close to tray for this application, you can delete/comment out the next line
|
||||
RunWait('komorebic.exe identify-tray-application exe "Playnite.DesktopApp.exe"', , "Hide")
|
||||
; Target fullscreen app
|
||||
RunWait('komorebic.exe float-rule exe "Playnite.FullscreenApp.exe"', , "Hide")
|
||||
|
||||
; Plexamp
|
||||
RunWait('komorebic.exe identify-border-overflow-application exe "Plexamp.exe"', , "Hide")
|
||||
|
||||
@@ -321,6 +370,13 @@ RunWait('komorebic.exe identify-object-name-change-application exe "pycharm64.ex
|
||||
; If you have disabled minimize/close to tray for this application, you can delete/comment out the next line
|
||||
RunWait('komorebic.exe identify-tray-application exe "pycharm64.exe"', , "Hide")
|
||||
|
||||
; QQ
|
||||
; If you have disabled minimize/close to tray for this application, you can delete/comment out the next line
|
||||
RunWait('komorebic.exe identify-tray-application exe "QQ.exe"', , "Hide")
|
||||
RunWait('komorebic.exe float-rule title "图片查看器"', , "Hide")
|
||||
RunWait('komorebic.exe float-rule title "群聊的聊天记录"', , "Hide")
|
||||
RunWait('komorebic.exe float-rule title "语音通话"', , "Hide")
|
||||
|
||||
; QtScrcpy
|
||||
; If you have disabled minimize/close to tray for this application, you can delete/comment out the next line
|
||||
RunWait('komorebic.exe identify-tray-application exe "QtScrcpy.exe"', , "Hide")
|
||||
@@ -348,6 +404,15 @@ RunWait('komorebic.exe identify-border-overflow-application exe "RoundedTB.exe"'
|
||||
; If you have disabled minimize/close to tray for this application, you can delete/comment out the next line
|
||||
RunWait('komorebic.exe identify-tray-application exe "RoundedTB.exe"', , "Hide")
|
||||
|
||||
; RustRover
|
||||
RunWait('komorebic.exe identify-object-name-change-application exe "rustrover64.exe"', , "Hide")
|
||||
; If you have disabled minimize/close to tray for this application, you can delete/comment out the next line
|
||||
RunWait('komorebic.exe identify-tray-application exe "rustrover64.exe"', , "Hide")
|
||||
|
||||
; Sandboxie Plus
|
||||
; If you have disabled minimize/close to tray for this application, you can delete/comment out the next line
|
||||
RunWait('komorebic.exe identify-tray-application exe "SandMan.exe"', , "Hide")
|
||||
|
||||
; ShareX
|
||||
RunWait('komorebic.exe identify-border-overflow-application exe "ShareX.exe"', , "Hide")
|
||||
; If you have disabled minimize/close to tray for this application, you can delete/comment out the next line
|
||||
@@ -358,7 +423,7 @@ RunWait('komorebic.exe float-rule exe "sideloadly.exe"', , "Hide")
|
||||
|
||||
; Signal
|
||||
; If you have disabled minimize/close to tray for this application, you can delete/comment out the next line
|
||||
RunWait('komorebic.exe identify-tray-application exe "signal.exe"', , "Hide")
|
||||
RunWait('komorebic.exe identify-tray-application exe "Signal.exe"', , "Hide")
|
||||
|
||||
; SiriKali
|
||||
; If you have disabled minimize/close to tray for this application, you can delete/comment out the next line
|
||||
@@ -484,6 +549,10 @@ RunWait('komorebic.exe float-rule title "Control Panel"', , "Hide")
|
||||
; Windows Installer
|
||||
RunWait('komorebic.exe float-rule exe "msiexec.exe"', , "Hide")
|
||||
|
||||
; Windows Subsystem for Android
|
||||
; Targets splash/startup screen
|
||||
RunWait('komorebic.exe float-rule class "android(splash)"', , "Hide")
|
||||
|
||||
; WingetUI
|
||||
; If you have disabled minimize/close to tray for this application, you can delete/comment out the next line
|
||||
RunWait('komorebic.exe identify-tray-application exe "WingetUI.exe"', , "Hide")
|
||||
@@ -503,6 +572,9 @@ RunWait('komorebic.exe identify-tray-application exe "xampp-control.exe"', , "Hi
|
||||
; Zoom
|
||||
RunWait('komorebic.exe float-rule exe "Zoom.exe"', , "Hide")
|
||||
|
||||
; mpv
|
||||
RunWait('komorebic.exe identify-object-name-change-application class "mpv"', , "Hide")
|
||||
|
||||
; mpv.net
|
||||
RunWait('komorebic.exe identify-object-name-change-application exe "mpvnet.exe"', , "Hide")
|
||||
|
||||
|
||||
@@ -27,6 +27,10 @@ komorebic.exe identify-tray-application exe "Akiflow.exe"
|
||||
# Android Studio
|
||||
komorebic.exe identify-object-name-change-application exe "studio64.exe"
|
||||
|
||||
# Anki
|
||||
# If you have disabled minimize/close to tray for this application, you can delete/comment out the next line
|
||||
komorebic.exe identify-tray-application exe "anki.exe"
|
||||
|
||||
# ArmCord
|
||||
komorebic.exe identify-border-overflow-application exe "ArmCord.exe"
|
||||
# If you have disabled minimize/close to tray for this application, you can delete/comment out the next line
|
||||
@@ -36,6 +40,7 @@ komorebic.exe identify-tray-application exe "ArmCord.exe"
|
||||
# If you have disabled minimize/close to tray for this application, you can delete/comment out the next line
|
||||
komorebic.exe identify-tray-application exe "AutoHotkeyU64.exe"
|
||||
komorebic.exe float-rule title "Window Spy"
|
||||
komorebic.exe float-rule exe "AutoHotkeyUX.exe"
|
||||
|
||||
# Beeper
|
||||
komorebic.exe identify-border-overflow-application exe "Beeper.exe"
|
||||
@@ -52,6 +57,19 @@ komorebic.exe float-rule exe "Bloxstrap.exe"
|
||||
# Calculator
|
||||
komorebic.exe float-rule title "Calculator"
|
||||
|
||||
# Clash Verge
|
||||
komorebic.exe identify-border-overflow-application exe "Clash Verge.exe"
|
||||
# If you have disabled minimize/close to tray for this application, you can delete/comment out the next line
|
||||
komorebic.exe identify-tray-application exe "Clash Verge.exe"
|
||||
|
||||
# Clementine
|
||||
# If you have disabled minimize/close to tray for this application, you can delete/comment out the next line
|
||||
komorebic.exe identify-tray-application exe "clementine.exe"
|
||||
|
||||
# CopyQ
|
||||
# If you have disabled minimize/close to tray for this application, you can delete/comment out the next line
|
||||
komorebic.exe identify-tray-application exe "copyq.exe"
|
||||
|
||||
# Credential Manager UI Host
|
||||
# Targets the Windows popup prompting you for a PIN instead of a password on 1Password etc.
|
||||
komorebic.exe float-rule exe "CredentialUIBroker.exe"
|
||||
@@ -91,6 +109,9 @@ komorebic.exe identify-border-overflow-application exe "DiscordPTB.exe"
|
||||
# If you have disabled minimize/close to tray for this application, you can delete/comment out the next line
|
||||
komorebic.exe identify-tray-application exe "DiscordPTB.exe"
|
||||
|
||||
# Docker Desktop
|
||||
komorebic.exe identify-border-overflow-application exe "Docker Desktop.exe"
|
||||
|
||||
# Dropbox
|
||||
komorebic.exe float-rule exe "Dropbox.exe"
|
||||
|
||||
@@ -123,6 +144,13 @@ komorebic.exe identify-border-overflow-application exe "EpicGamesLauncher.exe"
|
||||
# If you have disabled minimize/close to tray for this application, you can delete/comment out the next line
|
||||
komorebic.exe identify-tray-application exe "EpicGamesLauncher.exe"
|
||||
|
||||
# Everything
|
||||
# If you have disabled minimize/close to tray for this application, you can delete/comment out the next line
|
||||
komorebic.exe identify-tray-application exe "Everything.exe"
|
||||
|
||||
# Figma
|
||||
komorebic.exe identify-border-overflow-application exe "Figma.exe"
|
||||
|
||||
# Flow Launcher
|
||||
komorebic.exe identify-border-overflow-application exe "Flow.Launcher.exe"
|
||||
|
||||
@@ -143,11 +171,17 @@ komorebic.exe identify-border-overflow-application exe "GodotManager.exe"
|
||||
komorebic.exe manage-rule exe "GodotManager.exe"
|
||||
komorebic.exe identify-object-name-change-application exe "GodotManager.exe"
|
||||
|
||||
# Golden Dict
|
||||
# If you have disabled minimize/close to tray for this application, you can delete/comment out the next line
|
||||
komorebic.exe identify-tray-application exe "GoldenDict.exe"
|
||||
|
||||
# Google Chrome
|
||||
# If you have disabled minimize/close to tray for this application, you can delete/comment out the next line
|
||||
komorebic.exe identify-tray-application exe "chrome.exe"
|
||||
|
||||
# Google Drive
|
||||
# If you have disabled minimize/close to tray for this application, you can delete/comment out the next line
|
||||
komorebic.exe identify-tray-application exe "GoogleDriveFS.exe"
|
||||
komorebic.exe float-rule exe "GoogleDriveFS.exe"
|
||||
|
||||
# Houdoku
|
||||
@@ -256,6 +290,10 @@ komorebic.exe identify-border-overflow-application exe "NVIDIA GeForce Experienc
|
||||
# If you have disabled minimize/close to tray for this application, you can delete/comment out the next line
|
||||
komorebic.exe identify-tray-application exe "NZXT CAM.exe"
|
||||
|
||||
# NetEase Cloud Music
|
||||
# If you have disabled minimize/close to tray for this application, you can delete/comment out the next line
|
||||
komorebic.exe identify-tray-application exe "cloudmusic.exe"
|
||||
|
||||
# NiceHash Miner
|
||||
komorebic.exe identify-border-overflow-application exe "nhm_app.exe"
|
||||
komorebic.exe manage-rule exe "nhm_app.exe"
|
||||
@@ -288,6 +326,10 @@ komorebic.exe manage-rule exe "Obsidian.exe"
|
||||
# OneDrive
|
||||
komorebic.exe float-rule class "OneDriveReactNativeWin32WindowClass"
|
||||
|
||||
# OneQuick
|
||||
# If you have disabled minimize/close to tray for this application, you can delete/comment out the next line
|
||||
komorebic.exe identify-tray-application exe "OneQuick.exe"
|
||||
|
||||
# OpenRGB
|
||||
# If you have disabled minimize/close to tray for this application, you can delete/comment out the next line
|
||||
komorebic.exe identify-tray-application exe "OpenRGB.exe"
|
||||
@@ -295,6 +337,13 @@ komorebic.exe identify-tray-application exe "OpenRGB.exe"
|
||||
# Paradox Launcher
|
||||
komorebic.exe float-rule exe "Paradox Launcher.exe"
|
||||
|
||||
# Playnite
|
||||
komorebic.exe identify-border-overflow-application exe "Playnite.DesktopApp.exe"
|
||||
# If you have disabled minimize/close to tray for this application, you can delete/comment out the next line
|
||||
komorebic.exe identify-tray-application exe "Playnite.DesktopApp.exe"
|
||||
# Target fullscreen app
|
||||
komorebic.exe float-rule exe "Playnite.FullscreenApp.exe"
|
||||
|
||||
# Plexamp
|
||||
komorebic.exe identify-border-overflow-application exe "Plexamp.exe"
|
||||
|
||||
@@ -321,6 +370,13 @@ komorebic.exe identify-object-name-change-application exe "pycharm64.exe"
|
||||
# If you have disabled minimize/close to tray for this application, you can delete/comment out the next line
|
||||
komorebic.exe identify-tray-application exe "pycharm64.exe"
|
||||
|
||||
# QQ
|
||||
# If you have disabled minimize/close to tray for this application, you can delete/comment out the next line
|
||||
komorebic.exe identify-tray-application exe "QQ.exe"
|
||||
komorebic.exe float-rule title "图片查看器"
|
||||
komorebic.exe float-rule title "群聊的聊天记录"
|
||||
komorebic.exe float-rule title "语音通话"
|
||||
|
||||
# QtScrcpy
|
||||
# If you have disabled minimize/close to tray for this application, you can delete/comment out the next line
|
||||
komorebic.exe identify-tray-application exe "QtScrcpy.exe"
|
||||
@@ -348,6 +404,15 @@ komorebic.exe identify-border-overflow-application exe "RoundedTB.exe"
|
||||
# If you have disabled minimize/close to tray for this application, you can delete/comment out the next line
|
||||
komorebic.exe identify-tray-application exe "RoundedTB.exe"
|
||||
|
||||
# RustRover
|
||||
komorebic.exe identify-object-name-change-application exe "rustrover64.exe"
|
||||
# If you have disabled minimize/close to tray for this application, you can delete/comment out the next line
|
||||
komorebic.exe identify-tray-application exe "rustrover64.exe"
|
||||
|
||||
# Sandboxie Plus
|
||||
# If you have disabled minimize/close to tray for this application, you can delete/comment out the next line
|
||||
komorebic.exe identify-tray-application exe "SandMan.exe"
|
||||
|
||||
# ShareX
|
||||
komorebic.exe identify-border-overflow-application exe "ShareX.exe"
|
||||
# If you have disabled minimize/close to tray for this application, you can delete/comment out the next line
|
||||
@@ -358,7 +423,7 @@ komorebic.exe float-rule exe "sideloadly.exe"
|
||||
|
||||
# Signal
|
||||
# If you have disabled minimize/close to tray for this application, you can delete/comment out the next line
|
||||
komorebic.exe identify-tray-application exe "signal.exe"
|
||||
komorebic.exe identify-tray-application exe "Signal.exe"
|
||||
|
||||
# SiriKali
|
||||
# If you have disabled minimize/close to tray for this application, you can delete/comment out the next line
|
||||
@@ -484,6 +549,10 @@ komorebic.exe float-rule title "Control Panel"
|
||||
# Windows Installer
|
||||
komorebic.exe float-rule exe "msiexec.exe"
|
||||
|
||||
# Windows Subsystem for Android
|
||||
# Targets splash/startup screen
|
||||
komorebic.exe float-rule class "android(splash)"
|
||||
|
||||
# WingetUI
|
||||
# If you have disabled minimize/close to tray for this application, you can delete/comment out the next line
|
||||
komorebic.exe identify-tray-application exe "WingetUI.exe"
|
||||
@@ -503,6 +572,9 @@ komorebic.exe identify-tray-application exe "xampp-control.exe"
|
||||
# Zoom
|
||||
komorebic.exe float-rule exe "Zoom.exe"
|
||||
|
||||
# mpv
|
||||
komorebic.exe identify-object-name-change-application class "mpv"
|
||||
|
||||
# mpv.net
|
||||
komorebic.exe identify-object-name-change-application exe "mpvnet.exe"
|
||||
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
[package]
|
||||
name = "komorebi"
|
||||
version = "0.1.17"
|
||||
version = "0.1.19"
|
||||
authors = ["Jade Iqbal <jadeiqbal@fastmail.com>"]
|
||||
description = "A tiling window manager for Windows"
|
||||
categories = ["tiling-window-manager", "windows"]
|
||||
@@ -15,11 +15,9 @@ komorebi-core = { path = "../komorebi-core" }
|
||||
|
||||
bitflags = "2"
|
||||
clap = { version = "4", features = ["derive"] }
|
||||
color-eyre = "0.6"
|
||||
crossbeam-channel = "0.5"
|
||||
crossbeam-utils = "0.8"
|
||||
ctrlc = "3"
|
||||
dirs = "5"
|
||||
getset = "0.1"
|
||||
hotwatch = "0.4"
|
||||
lazy_static = "1"
|
||||
@@ -29,6 +27,7 @@ net2 = "0.2"
|
||||
os_info = "3.7"
|
||||
parking_lot = { version = "0.12", features = ["deadlock_detection"] }
|
||||
paste = "1"
|
||||
regex = "1"
|
||||
schemars = "0.8"
|
||||
serde = { version = "1", features = ["derive"] }
|
||||
serde_json = "1"
|
||||
@@ -38,12 +37,14 @@ tracing = "0.1"
|
||||
tracing-appender = "0.2"
|
||||
tracing-subscriber = { version = "0.3", features = ["env-filter"] }
|
||||
uds_windows = "1"
|
||||
which = "4"
|
||||
which = "5"
|
||||
winput = "0.2"
|
||||
winreg = "0.50"
|
||||
winreg = "0.52"
|
||||
windows-interface = { workspace = true }
|
||||
windows-implement = { workspace = true }
|
||||
windows = { workspace = true }
|
||||
color-eyre = { workspace = true }
|
||||
dirs = { workspace = true }
|
||||
|
||||
[features]
|
||||
deadlock_detection = []
|
||||
|
||||
@@ -14,6 +14,7 @@ use windows::Win32::UI::WindowsAndMessaging::WNDCLASSA;
|
||||
|
||||
use komorebi_core::Rect;
|
||||
|
||||
use crate::window::should_act;
|
||||
use crate::window::Window;
|
||||
use crate::windows_callbacks;
|
||||
use crate::WindowsApi;
|
||||
@@ -21,6 +22,7 @@ use crate::BORDER_HWND;
|
||||
use crate::BORDER_OFFSET;
|
||||
use crate::BORDER_OVERFLOW_IDENTIFIERS;
|
||||
use crate::BORDER_RECT;
|
||||
use crate::REGEX_IDENTIFIERS;
|
||||
use crate::TRANSPARENCY_COLOUR;
|
||||
use crate::WINDOWS_11;
|
||||
|
||||
@@ -46,7 +48,7 @@ impl Border {
|
||||
let class_name = PCSTR(name.as_ptr());
|
||||
let brush = WindowsApi::create_solid_brush(TRANSPARENCY_COLOUR);
|
||||
let window_class = WNDCLASSA {
|
||||
hInstance: instance,
|
||||
hInstance: instance.into(),
|
||||
lpszClassName: class_name,
|
||||
style: CS_HREDRAW | CS_VREDRAW,
|
||||
lpfnWndProc: Some(windows_callbacks::border_window),
|
||||
@@ -108,19 +110,24 @@ impl Border {
|
||||
Self::create("komorebi-border-window")?;
|
||||
}
|
||||
|
||||
let mut should_expand_border = false;
|
||||
|
||||
let mut rect = WindowsApi::window_rect(window.hwnd())?;
|
||||
rect.top -= invisible_borders.bottom;
|
||||
rect.bottom += invisible_borders.bottom;
|
||||
|
||||
let border_overflows = BORDER_OVERFLOW_IDENTIFIERS.lock();
|
||||
if border_overflows.contains(&window.title()?)
|
||||
|| border_overflows.contains(&window.exe()?)
|
||||
|| border_overflows.contains(&window.class()?)
|
||||
{
|
||||
should_expand_border = true;
|
||||
}
|
||||
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;
|
||||
|
||||
@@ -39,7 +39,7 @@ impl Hidden {
|
||||
let class_name = PCSTR(name.as_ptr());
|
||||
let brush = WindowsApi::create_solid_brush(TRANSPARENCY_COLOUR);
|
||||
let window_class = WNDCLASSA {
|
||||
hInstance: instance,
|
||||
hInstance: instance.into(),
|
||||
lpszClassName: class_name,
|
||||
style: CS_HREDRAW | CS_VREDRAW,
|
||||
lpfnWndProc: Some(windows_callbacks::hidden_window),
|
||||
|
||||
@@ -1,5 +1,10 @@
|
||||
#![warn(clippy::all, clippy::nursery, clippy::pedantic)]
|
||||
#![allow(clippy::missing_errors_doc, clippy::redundant_pub_crate)]
|
||||
#![allow(
|
||||
clippy::missing_errors_doc,
|
||||
clippy::redundant_pub_crate,
|
||||
clippy::significant_drop_tightening,
|
||||
clippy::significant_drop_in_scrutinee
|
||||
)]
|
||||
|
||||
use std::collections::HashMap;
|
||||
use std::fs::File;
|
||||
@@ -17,7 +22,6 @@ use std::sync::Arc;
|
||||
use std::time::Duration;
|
||||
|
||||
use clap::Parser;
|
||||
use color_eyre::eyre::anyhow;
|
||||
use color_eyre::Result;
|
||||
use crossbeam_channel::Receiver;
|
||||
use crossbeam_channel::Sender;
|
||||
@@ -27,6 +31,7 @@ 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;
|
||||
@@ -40,6 +45,9 @@ 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;
|
||||
@@ -81,36 +89,83 @@ 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<String>>> =
|
||||
Arc::new(Mutex::new(vec!["steam.exe".to_string()]));
|
||||
static ref TRAY_AND_MULTI_WINDOW_IDENTIFIERS: Arc<Mutex<Vec<String>>> =
|
||||
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![
|
||||
"explorer.exe".to_string(),
|
||||
"firefox.exe".to_string(),
|
||||
"chrome.exe".to_string(),
|
||||
"idea64.exe".to_string(),
|
||||
"ApplicationFrameHost.exe".to_string(),
|
||||
"steam.exe".to_string(),
|
||||
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<String>>> = Arc::new(Mutex::new(vec![
|
||||
"firefox.exe".to_string(),
|
||||
"idea64.exe".to_string(),
|
||||
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 WORKSPACE_RULES: Arc<Mutex<HashMap<String, WorkspaceRule>>> =
|
||||
Arc::new(Mutex::new(HashMap::new()));
|
||||
static ref MANAGE_IDENTIFIERS: Arc<Mutex<Vec<String>>> = Arc::new(Mutex::new(vec![]));
|
||||
static ref FLOAT_IDENTIFIERS: Arc<Mutex<Vec<String>>> = Arc::new(Mutex::new(vec![
|
||||
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
|
||||
"OPContainerClass".to_string(),
|
||||
"IHWindowClass".to_string()
|
||||
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<String>>> = Arc::new(Mutex::new(vec![]));
|
||||
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(),
|
||||
@@ -196,7 +251,7 @@ fn setup() -> Result<(WorkerGuard, WorkerGuard)> {
|
||||
std::env::set_var("RUST_LOG", "info");
|
||||
}
|
||||
|
||||
let appender = tracing_appender::rolling::never(DATA_DIR.clone(), "komorebi.log");
|
||||
let appender = tracing_appender::rolling::never(&*DATA_DIR, "komorebi.log");
|
||||
let color_appender = tracing_appender::rolling::never(std::env::temp_dir(), "komorebi.log");
|
||||
let (non_blocking, guard) = tracing_appender::non_blocking(appender);
|
||||
let (color_non_blocking, color_guard) = tracing_appender::non_blocking(color_appender);
|
||||
@@ -249,13 +304,8 @@ fn setup() -> Result<(WorkerGuard, WorkerGuard)> {
|
||||
}
|
||||
|
||||
pub fn load_configuration() -> Result<()> {
|
||||
let home = HOME_DIR.clone();
|
||||
|
||||
let mut config_pwsh = home.clone();
|
||||
config_pwsh.push("komorebi.ps1");
|
||||
|
||||
let mut config_ahk = home;
|
||||
config_ahk.push("komorebi.ahk");
|
||||
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() {
|
||||
@@ -264,25 +314,13 @@ pub fn load_configuration() -> Result<()> {
|
||||
"powershell.exe"
|
||||
};
|
||||
|
||||
tracing::info!(
|
||||
"loading configuration file: {}",
|
||||
config_pwsh
|
||||
.as_os_str()
|
||||
.to_str()
|
||||
.ok_or_else(|| anyhow!("cannot convert path to string"))?
|
||||
);
|
||||
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
|
||||
.as_os_str()
|
||||
.to_str()
|
||||
.ok_or_else(|| anyhow!("cannot convert path to string"))?
|
||||
);
|
||||
tracing::info!("loading configuration file: {}", config_ahk.display());
|
||||
|
||||
Command::new(&*AHK_EXE)
|
||||
.arg(config_ahk.as_os_str())
|
||||
@@ -313,7 +351,7 @@ pub fn current_virtual_desktop() -> Option<Vec<u8>> {
|
||||
// This is the path on Windows 11
|
||||
if current.is_none() {
|
||||
current = hkcu
|
||||
.open_subkey(r#"SOFTWARE\Microsoft\Windows\CurrentVersion\Explorer\VirtualDesktops"#)
|
||||
.open_subkey(r"SOFTWARE\Microsoft\Windows\CurrentVersion\Explorer\VirtualDesktops")
|
||||
.ok()
|
||||
.and_then(
|
||||
|desktops| match desktops.get_raw_value("CurrentVirtualDesktop") {
|
||||
@@ -352,9 +390,9 @@ pub struct Notification {
|
||||
pub fn notify_subscribers(notification: &str) -> Result<()> {
|
||||
let mut stale_subscriptions = vec![];
|
||||
let mut subscriptions = SUBSCRIPTION_PIPES.lock();
|
||||
for (subscriber, pipe) in subscriptions.iter_mut() {
|
||||
for (subscriber, pipe) in &mut *subscriptions {
|
||||
match writeln!(pipe, "{notification}") {
|
||||
Ok(_) => {
|
||||
Ok(()) => {
|
||||
tracing::debug!("pushed notification to subscriber: {}", subscriber);
|
||||
}
|
||||
Err(error) => {
|
||||
@@ -458,6 +496,8 @@ fn main() -> Result<()> {
|
||||
// File logging worker guard has to have an assignment in the main fn to work
|
||||
let (_guard, _color_guard) = setup()?;
|
||||
|
||||
WindowsApi::foreground_lock_timeout()?;
|
||||
|
||||
#[cfg(feature = "deadlock_detection")]
|
||||
detect_deadlocks();
|
||||
|
||||
@@ -472,7 +512,7 @@ fn main() -> Result<()> {
|
||||
let wm = if let Some(config) = &opts.config {
|
||||
tracing::info!(
|
||||
"creating window manager from static configuration file: {}",
|
||||
config.as_os_str().to_str().unwrap()
|
||||
config.display()
|
||||
);
|
||||
|
||||
Arc::new(Mutex::new(StaticConfig::preload(
|
||||
@@ -501,9 +541,7 @@ fn main() -> Result<()> {
|
||||
}
|
||||
|
||||
if opts.config.is_none() {
|
||||
std::thread::spawn(|| {
|
||||
load_configuration().expect("could not load configuration");
|
||||
});
|
||||
std::thread::spawn(|| load_configuration().expect("could not load configuration"));
|
||||
|
||||
if opts.await_configuration {
|
||||
let backoff = Backoff::new();
|
||||
|
||||
@@ -2,6 +2,7 @@ use std::collections::HashMap;
|
||||
use std::collections::VecDeque;
|
||||
|
||||
use color_eyre::eyre::anyhow;
|
||||
use color_eyre::eyre::bail;
|
||||
use color_eyre::Result;
|
||||
use getset::CopyGetters;
|
||||
use getset::Getters;
|
||||
@@ -120,9 +121,7 @@ impl Monitor {
|
||||
.ok_or_else(|| anyhow!("there is no workspace"))?;
|
||||
|
||||
if workspace.maximized_window().is_some() {
|
||||
return Err(anyhow!(
|
||||
"cannot move native maximized window to another monitor or workspace"
|
||||
));
|
||||
bail!("cannot move native maximized window to another monitor or workspace");
|
||||
}
|
||||
|
||||
let container = workspace
|
||||
|
||||
@@ -20,6 +20,8 @@ use parking_lot::Mutex;
|
||||
use schemars::schema_for;
|
||||
use uds_windows::UnixStream;
|
||||
|
||||
use komorebi_core::config_generation::IdWithIdentifier;
|
||||
use komorebi_core::config_generation::MatchingStrategy;
|
||||
use komorebi_core::ApplicationIdentifier;
|
||||
use komorebi_core::Axis;
|
||||
use komorebi_core::FocusFollowsMouseImplementation;
|
||||
@@ -238,16 +240,40 @@ impl WindowManager {
|
||||
self.handle_definitive_workspace_rules(id, monitor_idx, workspace_idx)?;
|
||||
}
|
||||
}
|
||||
SocketMessage::ManageRule(_, ref id) => {
|
||||
SocketMessage::ManageRule(identifier, ref id) => {
|
||||
let mut manage_identifiers = MANAGE_IDENTIFIERS.lock();
|
||||
if !manage_identifiers.contains(id) {
|
||||
manage_identifiers.push(id.to_string());
|
||||
|
||||
let mut should_push = true;
|
||||
for m in &*manage_identifiers {
|
||||
if m.id.eq(id) {
|
||||
should_push = false;
|
||||
}
|
||||
}
|
||||
|
||||
if should_push {
|
||||
manage_identifiers.push(IdWithIdentifier {
|
||||
kind: identifier,
|
||||
id: id.clone(),
|
||||
matching_strategy: Option::from(MatchingStrategy::Legacy),
|
||||
});
|
||||
}
|
||||
}
|
||||
SocketMessage::FloatRule(identifier, ref id) => {
|
||||
let mut float_identifiers = FLOAT_IDENTIFIERS.lock();
|
||||
if !float_identifiers.contains(id) {
|
||||
float_identifiers.push(id.to_string());
|
||||
|
||||
let mut should_push = true;
|
||||
for f in &*float_identifiers {
|
||||
if f.id.eq(id) {
|
||||
should_push = false;
|
||||
}
|
||||
}
|
||||
|
||||
if should_push {
|
||||
float_identifiers.push(IdWithIdentifier {
|
||||
kind: identifier,
|
||||
id: id.clone(),
|
||||
matching_strategy: Option::from(MatchingStrategy::Legacy),
|
||||
});
|
||||
}
|
||||
|
||||
let invisible_borders = self.invisible_borders;
|
||||
@@ -259,9 +285,8 @@ impl WindowManager {
|
||||
.focused_workspace()
|
||||
.ok_or_else(|| anyhow!("there is no workspace"))?
|
||||
.containers()
|
||||
.iter()
|
||||
{
|
||||
for window in container.windows().iter() {
|
||||
for window in container.windows() {
|
||||
match identifier {
|
||||
ApplicationIdentifier::Exe => {
|
||||
if window.exe()? == *id {
|
||||
@@ -297,6 +322,28 @@ impl WindowManager {
|
||||
monitor.update_focused_workspace(offset, &invisible_borders)?;
|
||||
}
|
||||
}
|
||||
SocketMessage::FocusedWorkspaceContainerPadding(adjustment) => {
|
||||
let focused_monitor_idx = self.focused_monitor_idx();
|
||||
|
||||
let focused_monitor = self
|
||||
.focused_monitor()
|
||||
.ok_or_else(|| anyhow!("there is no monitor"))?;
|
||||
|
||||
let focused_workspace_idx = focused_monitor.focused_workspace_idx();
|
||||
|
||||
self.set_container_padding(focused_monitor_idx, focused_workspace_idx, adjustment)?;
|
||||
}
|
||||
SocketMessage::FocusedWorkspacePadding(adjustment) => {
|
||||
let focused_monitor_idx = self.focused_monitor_idx();
|
||||
|
||||
let focused_monitor = self
|
||||
.focused_monitor()
|
||||
.ok_or_else(|| anyhow!("there is no monitor"))?;
|
||||
|
||||
let focused_workspace_idx = focused_monitor.focused_workspace_idx();
|
||||
|
||||
self.set_workspace_padding(focused_monitor_idx, focused_workspace_idx, adjustment)?;
|
||||
}
|
||||
SocketMessage::AdjustContainerPadding(sizing, adjustment) => {
|
||||
self.adjust_container_padding(sizing, adjustment)?;
|
||||
}
|
||||
@@ -423,11 +470,12 @@ impl WindowManager {
|
||||
SocketMessage::Retile => self.retile_all(false)?,
|
||||
SocketMessage::FlipLayout(layout_flip) => self.flip_layout(layout_flip)?,
|
||||
SocketMessage::ChangeLayout(layout) => self.change_workspace_layout_default(layout)?,
|
||||
SocketMessage::CycleLayout(direction) => self.cycle_layout(direction)?,
|
||||
SocketMessage::ChangeLayoutCustom(ref path) => {
|
||||
self.change_workspace_custom_layout(path.clone())?;
|
||||
self.change_workspace_custom_layout(path)?;
|
||||
}
|
||||
SocketMessage::WorkspaceLayoutCustom(monitor_idx, workspace_idx, ref path) => {
|
||||
self.set_workspace_layout_custom(monitor_idx, workspace_idx, path.clone())?;
|
||||
self.set_workspace_layout_custom(monitor_idx, workspace_idx, path)?;
|
||||
}
|
||||
SocketMessage::WorkspaceTiling(monitor_idx, workspace_idx, tile) => {
|
||||
self.set_workspace_tiling(monitor_idx, workspace_idx, tile)?;
|
||||
@@ -458,7 +506,7 @@ impl WindowManager {
|
||||
monitor_idx,
|
||||
workspace_idx,
|
||||
at_container_count,
|
||||
path.clone(),
|
||||
path,
|
||||
)?;
|
||||
}
|
||||
SocketMessage::ClearWorkspaceLayoutRules(monitor_idx, workspace_idx) => {
|
||||
@@ -468,7 +516,7 @@ impl WindowManager {
|
||||
if let Some((monitor_idx, workspace_idx)) =
|
||||
self.monitor_workspace_index_by_name(workspace)
|
||||
{
|
||||
self.set_workspace_layout_custom(monitor_idx, workspace_idx, path.clone())?;
|
||||
self.set_workspace_layout_custom(monitor_idx, workspace_idx, path)?;
|
||||
}
|
||||
}
|
||||
SocketMessage::NamedWorkspaceTiling(ref workspace, tile) => {
|
||||
@@ -509,7 +557,7 @@ impl WindowManager {
|
||||
monitor_idx,
|
||||
workspace_idx,
|
||||
at_container_count,
|
||||
path.clone(),
|
||||
path,
|
||||
)?;
|
||||
}
|
||||
}
|
||||
@@ -643,10 +691,7 @@ impl WindowManager {
|
||||
Err(error) => error.to_string(),
|
||||
};
|
||||
|
||||
let mut socket = DATA_DIR.clone();
|
||||
socket.push("komorebic.sock");
|
||||
let socket = socket.as_path();
|
||||
|
||||
let socket = DATA_DIR.join("komorebic.sock");
|
||||
let mut stream = UnixStream::connect(socket)?;
|
||||
stream.write_all(state.as_bytes())?;
|
||||
}
|
||||
@@ -666,10 +711,7 @@ impl WindowManager {
|
||||
}
|
||||
.to_string();
|
||||
|
||||
let mut socket = DATA_DIR.clone();
|
||||
socket.push("komorebic.sock");
|
||||
let socket = socket.as_path();
|
||||
|
||||
let socket = DATA_DIR.join("komorebic.sock");
|
||||
let mut stream = UnixStream::connect(socket)?;
|
||||
stream.write_all(response.as_bytes())?;
|
||||
}
|
||||
@@ -891,28 +933,75 @@ impl WindowManager {
|
||||
SocketMessage::WatchConfiguration(enable) => {
|
||||
self.watch_configuration(enable)?;
|
||||
}
|
||||
SocketMessage::IdentifyBorderOverflowApplication(_, ref id) => {
|
||||
SocketMessage::IdentifyBorderOverflowApplication(identifier, ref id) => {
|
||||
let mut identifiers = BORDER_OVERFLOW_IDENTIFIERS.lock();
|
||||
if !identifiers.contains(id) {
|
||||
identifiers.push(id.to_string());
|
||||
|
||||
let mut should_push = true;
|
||||
for i in &*identifiers {
|
||||
if i.id.eq(id) {
|
||||
should_push = false;
|
||||
}
|
||||
}
|
||||
|
||||
if should_push {
|
||||
identifiers.push(IdWithIdentifier {
|
||||
kind: identifier,
|
||||
id: id.clone(),
|
||||
matching_strategy: Option::from(MatchingStrategy::Legacy),
|
||||
});
|
||||
}
|
||||
}
|
||||
SocketMessage::IdentifyObjectNameChangeApplication(_, ref id) => {
|
||||
SocketMessage::IdentifyObjectNameChangeApplication(identifier, ref id) => {
|
||||
let mut identifiers = OBJECT_NAME_CHANGE_ON_LAUNCH.lock();
|
||||
if !identifiers.contains(id) {
|
||||
identifiers.push(id.to_string());
|
||||
|
||||
let mut should_push = true;
|
||||
for i in &*identifiers {
|
||||
if i.id.eq(id) {
|
||||
should_push = false;
|
||||
}
|
||||
}
|
||||
|
||||
if should_push {
|
||||
identifiers.push(IdWithIdentifier {
|
||||
kind: identifier,
|
||||
id: id.clone(),
|
||||
matching_strategy: Option::from(MatchingStrategy::Legacy),
|
||||
});
|
||||
}
|
||||
}
|
||||
SocketMessage::IdentifyTrayApplication(_, ref id) => {
|
||||
SocketMessage::IdentifyTrayApplication(identifier, ref id) => {
|
||||
let mut identifiers = TRAY_AND_MULTI_WINDOW_IDENTIFIERS.lock();
|
||||
if !identifiers.contains(id) {
|
||||
identifiers.push(id.to_string());
|
||||
let mut should_push = true;
|
||||
for i in &*identifiers {
|
||||
if i.id.eq(id) {
|
||||
should_push = false;
|
||||
}
|
||||
}
|
||||
|
||||
if should_push {
|
||||
identifiers.push(IdWithIdentifier {
|
||||
kind: identifier,
|
||||
id: id.clone(),
|
||||
matching_strategy: Option::from(MatchingStrategy::Legacy),
|
||||
});
|
||||
}
|
||||
}
|
||||
SocketMessage::IdentifyLayeredApplication(_, ref id) => {
|
||||
SocketMessage::IdentifyLayeredApplication(identifier, ref id) => {
|
||||
let mut identifiers = LAYERED_WHITELIST.lock();
|
||||
if !identifiers.contains(id) {
|
||||
identifiers.push(id.to_string());
|
||||
|
||||
let mut should_push = true;
|
||||
for i in &*identifiers {
|
||||
if i.id.eq(id) {
|
||||
should_push = false;
|
||||
}
|
||||
}
|
||||
|
||||
if should_push {
|
||||
identifiers.push(IdWithIdentifier {
|
||||
kind: identifier,
|
||||
id: id.clone(),
|
||||
matching_strategy: Option::from(MatchingStrategy::Legacy),
|
||||
});
|
||||
}
|
||||
}
|
||||
SocketMessage::ManageFocusedWindow => {
|
||||
@@ -939,8 +1028,7 @@ impl WindowManager {
|
||||
let workspace = self.focused_workspace()?;
|
||||
let resize = workspace.resize_dimensions();
|
||||
|
||||
let mut quicksave_json = std::env::temp_dir();
|
||||
quicksave_json.push("komorebi.quicksave.json");
|
||||
let quicksave_json = std::env::temp_dir().join("komorebi.quicksave.json");
|
||||
|
||||
let file = OpenOptions::new()
|
||||
.write(true)
|
||||
@@ -953,15 +1041,10 @@ impl WindowManager {
|
||||
SocketMessage::QuickLoad => {
|
||||
let workspace = self.focused_workspace_mut()?;
|
||||
|
||||
let mut quicksave_json = std::env::temp_dir();
|
||||
quicksave_json.push("komorebi.quicksave.json");
|
||||
let quicksave_json = std::env::temp_dir().join("komorebi.quicksave.json");
|
||||
|
||||
let file = File::open(&quicksave_json).map_err(|_| {
|
||||
anyhow!(
|
||||
"no quicksave found at {}",
|
||||
quicksave_json.display().to_string()
|
||||
)
|
||||
})?;
|
||||
let file = File::open(&quicksave_json)
|
||||
.map_err(|_| anyhow!("no quicksave found at {}", quicksave_json.display()))?;
|
||||
|
||||
let resize: Vec<Option<Rect>> = serde_json::from_reader(file)?;
|
||||
|
||||
@@ -976,15 +1059,15 @@ impl WindowManager {
|
||||
.write(true)
|
||||
.truncate(true)
|
||||
.create(true)
|
||||
.open(path.clone())?;
|
||||
.open(path)?;
|
||||
|
||||
serde_json::to_writer_pretty(&file, &resize)?;
|
||||
}
|
||||
SocketMessage::Load(ref path) => {
|
||||
let workspace = self.focused_workspace_mut()?;
|
||||
|
||||
let file = File::open(path)
|
||||
.map_err(|_| anyhow!("no file found at {}", path.display().to_string()))?;
|
||||
let file =
|
||||
File::open(path).map_err(|_| anyhow!("no file found at {}", path.display()))?;
|
||||
|
||||
let resize: Vec<Option<Rect>> = serde_json::from_reader(file)?;
|
||||
|
||||
@@ -1096,9 +1179,7 @@ impl WindowManager {
|
||||
SocketMessage::NotificationSchema => {
|
||||
let notification = schema_for!(Notification);
|
||||
let schema = serde_json::to_string_pretty(¬ification)?;
|
||||
let mut socket = DATA_DIR.clone();
|
||||
socket.push("komorebic.sock");
|
||||
let socket = socket.as_path();
|
||||
let socket = DATA_DIR.join("komorebic.sock");
|
||||
|
||||
let mut stream = UnixStream::connect(socket)?;
|
||||
stream.write_all(schema.as_bytes())?;
|
||||
@@ -1106,9 +1187,7 @@ impl WindowManager {
|
||||
SocketMessage::SocketSchema => {
|
||||
let socket_message = schema_for!(SocketMessage);
|
||||
let schema = serde_json::to_string_pretty(&socket_message)?;
|
||||
let mut socket = DATA_DIR.clone();
|
||||
socket.push("komorebic.sock");
|
||||
let socket = socket.as_path();
|
||||
let socket = DATA_DIR.join("komorebic.sock");
|
||||
|
||||
let mut stream = UnixStream::connect(socket)?;
|
||||
stream.write_all(schema.as_bytes())?;
|
||||
@@ -1116,18 +1195,14 @@ impl WindowManager {
|
||||
SocketMessage::StaticConfigSchema => {
|
||||
let socket_message = schema_for!(StaticConfig);
|
||||
let schema = serde_json::to_string_pretty(&socket_message)?;
|
||||
let mut socket = DATA_DIR.clone();
|
||||
socket.push("komorebic.sock");
|
||||
let socket = socket.as_path();
|
||||
let socket = DATA_DIR.join("komorebic.sock");
|
||||
|
||||
let mut stream = UnixStream::connect(socket)?;
|
||||
stream.write_all(schema.as_bytes())?;
|
||||
}
|
||||
SocketMessage::GenerateStaticConfig => {
|
||||
let config = serde_json::to_string_pretty(&StaticConfig::from(&*self))?;
|
||||
let mut socket = DATA_DIR.clone();
|
||||
socket.push("komorebic.sock");
|
||||
let socket = socket.as_path();
|
||||
let socket = DATA_DIR.join("komorebic.sock");
|
||||
|
||||
let mut stream = UnixStream::connect(socket)?;
|
||||
stream.write_all(config.as_bytes())?;
|
||||
@@ -1182,6 +1257,7 @@ impl WindowManager {
|
||||
|
||||
match message {
|
||||
SocketMessage::ChangeLayout(_)
|
||||
| SocketMessage::CycleLayout(_)
|
||||
| SocketMessage::ChangeLayoutCustom(_)
|
||||
| SocketMessage::FlipLayout(_)
|
||||
| SocketMessage::ManageFocusedWindow
|
||||
@@ -1341,7 +1417,8 @@ pub fn read_commands_tcp(
|
||||
break;
|
||||
}
|
||||
Ok(size) => {
|
||||
let Ok(message) = SocketMessage::from_str(&String::from_utf8_lossy(&buf[..size])) else {
|
||||
let Ok(message) = SocketMessage::from_str(&String::from_utf8_lossy(&buf[..size]))
|
||||
else {
|
||||
tracing::warn!("client sent an invalid message, disconnecting: {addr}");
|
||||
let mut connections = TCP_CONNECTIONS.lock();
|
||||
connections.remove(addr);
|
||||
|
||||
@@ -15,6 +15,7 @@ use komorebi_core::WindowContainerBehaviour;
|
||||
use crate::border::Border;
|
||||
use crate::current_virtual_desktop;
|
||||
use crate::notify_subscribers;
|
||||
use crate::window::should_act;
|
||||
use crate::window_manager::WindowManager;
|
||||
use crate::window_manager_event::WindowManagerEvent;
|
||||
use crate::windows_api::WindowsApi;
|
||||
@@ -29,6 +30,7 @@ use crate::BORDER_HIDDEN;
|
||||
use crate::BORDER_HWND;
|
||||
use crate::DATA_DIR;
|
||||
use crate::HIDDEN_HWNDS;
|
||||
use crate::REGEX_IDENTIFIERS;
|
||||
use crate::TRAY_AND_MULTI_WINDOW_IDENTIFIERS;
|
||||
|
||||
#[tracing::instrument]
|
||||
@@ -179,15 +181,26 @@ impl WindowManager {
|
||||
{
|
||||
let tray_and_multi_window_identifiers =
|
||||
TRAY_AND_MULTI_WINDOW_IDENTIFIERS.lock();
|
||||
let regex_identifiers = REGEX_IDENTIFIERS.lock();
|
||||
|
||||
let title = &window.title()?;
|
||||
let exe_name = &window.exe()?;
|
||||
let class = &window.class()?;
|
||||
|
||||
// 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.
|
||||
let programmatically_hidden_hwnds = HIDDEN_HWNDS.lock();
|
||||
let should_act = should_act(
|
||||
title,
|
||||
exe_name,
|
||||
class,
|
||||
&tray_and_multi_window_identifiers,
|
||||
®ex_identifiers,
|
||||
);
|
||||
|
||||
if ((!window.is_window()
|
||||
|| tray_and_multi_window_identifiers.contains(&window.exe()?))
|
||||
|| tray_and_multi_window_identifiers.contains(&window.class()?))
|
||||
&& !programmatically_hidden_hwnds.contains(&window.hwnd)
|
||||
if !window.is_window()
|
||||
|| should_act
|
||||
|| !programmatically_hidden_hwnds.contains(&window.hwnd)
|
||||
{
|
||||
hide = true;
|
||||
}
|
||||
|
||||
@@ -31,7 +31,7 @@ pub fn listen_for_movements(wm: Arc<Mutex<WindowManager>>) {
|
||||
Event::MouseMoveRelative { .. } => {
|
||||
if !ignore_movement {
|
||||
match wm.lock().raise_window_at_cursor_pos() {
|
||||
Ok(_) => {}
|
||||
Ok(()) => {}
|
||||
Err(error) => tracing::error!("{}", error),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -4,6 +4,7 @@ use crate::monitor::Monitor;
|
||||
use crate::ring::Ring;
|
||||
use crate::window_manager::WindowManager;
|
||||
use crate::window_manager_event::WindowManagerEvent;
|
||||
use crate::windows_api::WindowsApi;
|
||||
use crate::workspace::Workspace;
|
||||
use crate::ALT_FOCUS_HACK;
|
||||
use crate::BORDER_COLOUR_CURRENT;
|
||||
@@ -22,17 +23,20 @@ use crate::FLOAT_IDENTIFIERS;
|
||||
use crate::HIDING_BEHAVIOUR;
|
||||
use crate::LAYERED_WHITELIST;
|
||||
use crate::MANAGE_IDENTIFIERS;
|
||||
use crate::MONITOR_INDEX_PREFERENCES;
|
||||
use crate::OBJECT_NAME_CHANGE_ON_LAUNCH;
|
||||
use crate::REGEX_IDENTIFIERS;
|
||||
use crate::TRAY_AND_MULTI_WINDOW_IDENTIFIERS;
|
||||
use crate::WORKSPACE_RULES;
|
||||
use color_eyre::Result;
|
||||
use crossbeam_channel::Receiver;
|
||||
use dirs::home_dir;
|
||||
use hotwatch::notify::DebouncedEvent;
|
||||
use hotwatch::Hotwatch;
|
||||
use komorebi_core::config_generation::ApplicationConfigurationGenerator;
|
||||
use komorebi_core::config_generation::ApplicationOptions;
|
||||
use komorebi_core::config_generation::IdWithIdentifier;
|
||||
use komorebi_core::config_generation::MatchingStrategy;
|
||||
use komorebi_core::resolve_home_path;
|
||||
use komorebi_core::ApplicationIdentifier;
|
||||
use komorebi_core::DefaultLayout;
|
||||
use komorebi_core::FocusFollowsMouseImplementation;
|
||||
@@ -44,6 +48,7 @@ use komorebi_core::Rect;
|
||||
use komorebi_core::SocketMessage;
|
||||
use komorebi_core::WindowContainerBehaviour;
|
||||
use parking_lot::Mutex;
|
||||
use regex::Regex;
|
||||
use schemars::JsonSchema;
|
||||
use serde::Deserialize;
|
||||
use serde::Serialize;
|
||||
@@ -67,6 +72,16 @@ pub struct Rgb {
|
||||
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
|
||||
@@ -128,6 +143,7 @@ impl From<&Workspace> for WorkspaceConfig {
|
||||
let rule = IdWithIdentifier {
|
||||
kind: ApplicationIdentifier::Exe,
|
||||
id: identifier.clone(),
|
||||
matching_strategy: None,
|
||||
};
|
||||
|
||||
if *is_initial {
|
||||
@@ -239,12 +255,18 @@ 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)
|
||||
/// DEPRECATED from v0.1.19: use active_window_border_width instead
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub border_width: Option<i32>,
|
||||
/// Offset of the active window border (default: None)
|
||||
/// DEPRECATED from v0.1.19: use active_window_border_offset instead
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub border_offset: Option<Rect>,
|
||||
/// Width of the active window border (default: 20)
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub active_window_border_width: Option<i32>,
|
||||
/// Offset of the active window border (default: None)
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub active_window_border_offset: Option<i32>,
|
||||
/// Display an active window border (default: false)
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub active_window_border: Option<bool>,
|
||||
@@ -287,9 +309,13 @@ pub struct StaticConfig {
|
||||
/// 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>>,
|
||||
/// Set monitor index preferences
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub monitor_index_preferences: Option<HashMap<usize, Rect>>,
|
||||
}
|
||||
|
||||
impl From<&WindowManager> for StaticConfig {
|
||||
#[allow(clippy::too_many_lines)]
|
||||
fn from(value: &WindowManager) -> Self {
|
||||
let default_invisible_borders = Rect {
|
||||
left: 7,
|
||||
@@ -348,6 +374,24 @@ impl From<&WindowManager> for StaticConfig {
|
||||
}
|
||||
}
|
||||
|
||||
let border_colours = if BORDER_COLOUR_SINGLE.load(Ordering::SeqCst) == 0 {
|
||||
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 {
|
||||
BORDER_COLOUR_SINGLE.load(Ordering::SeqCst)
|
||||
} else {
|
||||
BORDER_COLOUR_STACK.load(Ordering::SeqCst)
|
||||
}),
|
||||
monocle: Rgb::from(if BORDER_COLOUR_MONOCLE.load(Ordering::SeqCst) == 0 {
|
||||
BORDER_COLOUR_SINGLE.load(Ordering::SeqCst)
|
||||
} else {
|
||||
BORDER_COLOUR_MONOCLE.load(Ordering::SeqCst)
|
||||
}),
|
||||
})
|
||||
};
|
||||
|
||||
Self {
|
||||
invisible_borders: if value.invisible_borders == default_invisible_borders {
|
||||
None
|
||||
@@ -363,10 +407,14 @@ 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,
|
||||
border_width: Option::from(BORDER_WIDTH.load(Ordering::SeqCst)),
|
||||
border_offset: *BORDER_OFFSET.lock(),
|
||||
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: None,
|
||||
border_offset: None,
|
||||
active_window_border: Option::from(BORDER_ENABLED.load(Ordering::SeqCst)),
|
||||
active_window_border_colours: None,
|
||||
active_window_border_colours: border_colours,
|
||||
default_workspace_padding: Option::from(
|
||||
DEFAULT_WORKSPACE_PADDING.load(Ordering::SeqCst),
|
||||
),
|
||||
@@ -383,13 +431,19 @@ impl From<&WindowManager> for StaticConfig {
|
||||
tray_and_multi_window_applications: None,
|
||||
layered_applications: None,
|
||||
object_name_change_applications: None,
|
||||
monitor_index_preferences: Option::from(MONITOR_INDEX_PREFERENCES.lock().clone()),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl StaticConfig {
|
||||
#[allow(clippy::cognitive_complexity, clippy::too_many_lines)]
|
||||
fn apply_globals(&self) -> Result<()> {
|
||||
fn apply_globals(&mut self) -> Result<()> {
|
||||
if let Some(monitor_index_preferences) = &self.monitor_index_preferences {
|
||||
let mut preferences = MONITOR_INDEX_PREFERENCES.lock();
|
||||
*preferences = monitor_index_preferences.clone();
|
||||
}
|
||||
|
||||
if let Some(behaviour) = self.window_hiding_behaviour {
|
||||
let mut window_hiding_behaviour = HIDING_BEHAVIOUR.lock();
|
||||
*window_hiding_behaviour = behaviour;
|
||||
@@ -407,14 +461,31 @@ impl StaticConfig {
|
||||
DEFAULT_WORKSPACE_PADDING.store(workspace, Ordering::SeqCst);
|
||||
}
|
||||
|
||||
if let Some(width) = self.border_width {
|
||||
BORDER_WIDTH.store(width, Ordering::SeqCst);
|
||||
}
|
||||
self.active_window_border_width.map_or_else(
|
||||
|| {
|
||||
BORDER_WIDTH.store(20, 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,
|
||||
};
|
||||
|
||||
if let Some(offset) = self.border_offset {
|
||||
let mut border_offset = BORDER_OFFSET.lock();
|
||||
*border_offset = Some(offset);
|
||||
}
|
||||
let mut border_offset = BORDER_OFFSET.lock();
|
||||
*border_offset = Some(new_border_offset);
|
||||
},
|
||||
);
|
||||
|
||||
if let Some(colours) = &self.active_window_border_colours {
|
||||
BORDER_COLOUR_SINGLE.store(
|
||||
@@ -436,75 +507,139 @@ impl StaticConfig {
|
||||
}
|
||||
|
||||
let mut float_identifiers = FLOAT_IDENTIFIERS.lock();
|
||||
let mut regex_identifiers = REGEX_IDENTIFIERS.lock();
|
||||
let mut manage_identifiers = MANAGE_IDENTIFIERS.lock();
|
||||
let mut tray_and_multi_window_identifiers = TRAY_AND_MULTI_WINDOW_IDENTIFIERS.lock();
|
||||
let mut border_overflow_identifiers = BORDER_OVERFLOW_IDENTIFIERS.lock();
|
||||
let mut object_name_change_identifiers = OBJECT_NAME_CHANGE_ON_LAUNCH.lock();
|
||||
let mut layered_identifiers = LAYERED_WHITELIST.lock();
|
||||
|
||||
if let Some(float) = &self.float_rules {
|
||||
if let Some(float) = &mut self.float_rules {
|
||||
for identifier in float {
|
||||
if !float_identifiers.contains(&identifier.id) {
|
||||
float_identifiers.push(identifier.id.clone());
|
||||
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(float) = &self.manage_rules {
|
||||
for identifier in float {
|
||||
if !manage_identifiers.contains(&identifier.id) {
|
||||
manage_identifiers.push(identifier.id.clone());
|
||||
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(identifiers) = &self.object_name_change_applications {
|
||||
if let Some(identifiers) = &mut self.object_name_change_applications {
|
||||
for identifier in identifiers {
|
||||
if !object_name_change_identifiers.contains(&identifier.id) {
|
||||
object_name_change_identifiers.push(identifier.id.clone());
|
||||
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(identifiers) = &self.layered_applications {
|
||||
if let Some(identifiers) = &mut self.layered_applications {
|
||||
for identifier in identifiers {
|
||||
if !layered_identifiers.contains(&identifier.id) {
|
||||
layered_identifiers.push(identifier.id.clone());
|
||||
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(identifiers) = &self.border_overflow_applications {
|
||||
if let Some(identifiers) = &mut self.border_overflow_applications {
|
||||
for identifier in identifiers {
|
||||
if !border_overflow_identifiers.contains(&identifier.id) {
|
||||
border_overflow_identifiers.push(identifier.id.clone());
|
||||
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(identifiers) = &self.tray_and_multi_window_applications {
|
||||
if let Some(identifiers) = &mut self.tray_and_multi_window_applications {
|
||||
for identifier in identifiers {
|
||||
if !tray_and_multi_window_identifiers.contains(&identifier.id) {
|
||||
tray_and_multi_window_identifiers.push(identifier.id.clone());
|
||||
if identifier.matching_strategy.is_none() {
|
||||
identifier.matching_strategy = Option::from(MatchingStrategy::Legacy);
|
||||
}
|
||||
|
||||
if !tray_and_multi_window_identifiers.contains(identifier) {
|
||||
tray_and_multi_window_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(path) = &self.app_specific_configuration_path {
|
||||
let stringified = path.to_string_lossy();
|
||||
let stringified = stringified.replace(
|
||||
"$Env:USERPROFILE",
|
||||
&home_dir().expect("no home dir").to_string_lossy(),
|
||||
);
|
||||
|
||||
let content = std::fs::read_to_string(stringified)?;
|
||||
let path = resolve_home_path(path)?;
|
||||
let content = std::fs::read_to_string(path)?;
|
||||
let asc = ApplicationConfigurationGenerator::load(&content)?;
|
||||
|
||||
for entry in asc {
|
||||
for mut entry in asc {
|
||||
if let Some(float) = entry.float_identifiers {
|
||||
for f in float {
|
||||
if !float_identifiers.contains(&f.id) {
|
||||
float_identifiers.push(f.id.clone());
|
||||
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);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -512,31 +647,94 @@ impl StaticConfig {
|
||||
for o in options {
|
||||
match o {
|
||||
ApplicationOptions::ObjectNameChange => {
|
||||
if !object_name_change_identifiers.contains(&entry.identifier.id) {
|
||||
object_name_change_identifiers
|
||||
.push(entry.identifier.id.clone());
|
||||
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);
|
||||
}
|
||||
}
|
||||
}
|
||||
ApplicationOptions::Layered => {
|
||||
if !layered_identifiers.contains(&entry.identifier.id) {
|
||||
layered_identifiers.push(entry.identifier.id.clone());
|
||||
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);
|
||||
}
|
||||
}
|
||||
}
|
||||
ApplicationOptions::BorderOverflow => {
|
||||
if !border_overflow_identifiers.contains(&entry.identifier.id) {
|
||||
border_overflow_identifiers.push(entry.identifier.id.clone());
|
||||
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);
|
||||
}
|
||||
}
|
||||
}
|
||||
ApplicationOptions::TrayAndMultiWindow => {
|
||||
if !tray_and_multi_window_identifiers.contains(&entry.identifier.id)
|
||||
{
|
||||
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.id.clone());
|
||||
.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);
|
||||
}
|
||||
}
|
||||
}
|
||||
ApplicationOptions::Force => {
|
||||
if !manage_identifiers.contains(&entry.identifier.id) {
|
||||
manage_identifiers.push(entry.identifier.id.clone());
|
||||
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);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -554,13 +752,13 @@ impl StaticConfig {
|
||||
incoming: Arc<Mutex<Receiver<WindowManagerEvent>>>,
|
||||
) -> Result<WindowManager> {
|
||||
let content = std::fs::read_to_string(path)?;
|
||||
let value: Self = serde_json::from_str(&content)?;
|
||||
let mut value: Self = serde_json::from_str(&content)?;
|
||||
value.apply_globals()?;
|
||||
|
||||
let socket = DATA_DIR.join("komorebi.sock");
|
||||
|
||||
match std::fs::remove_file(&socket) {
|
||||
Ok(_) => {}
|
||||
Ok(()) => {}
|
||||
Err(error) => match error.kind() {
|
||||
// Doing this because ::exists() doesn't work reliably on Windows via IntelliJ
|
||||
ErrorKind::NotFound => {}
|
||||
@@ -604,6 +802,14 @@ impl StaticConfig {
|
||||
already_moved_window_handles: Arc::new(Mutex::new(HashSet::new())),
|
||||
};
|
||||
|
||||
match value.focus_follows_mouse {
|
||||
None => WindowsApi::disable_focus_follows_mouse()?,
|
||||
Some(FocusFollowsMouseImplementation::Windows) => {
|
||||
WindowsApi::enable_focus_follows_mouse()?;
|
||||
}
|
||||
Some(FocusFollowsMouseImplementation::Komorebi) => {}
|
||||
};
|
||||
|
||||
let bytes = SocketMessage::ReloadStaticConfiguration(path.clone()).as_bytes()?;
|
||||
|
||||
wm.hotwatch.watch(path, move |event| match event {
|
||||
@@ -674,7 +880,7 @@ impl StaticConfig {
|
||||
|
||||
pub fn reload(path: &PathBuf, wm: &mut WindowManager) -> Result<()> {
|
||||
let content = std::fs::read_to_string(path)?;
|
||||
let value: Self = serde_json::from_str(&content)?;
|
||||
let mut value: Self = serde_json::from_str(&content)?;
|
||||
|
||||
value.apply_globals()?;
|
||||
|
||||
@@ -747,6 +953,15 @@ impl StaticConfig {
|
||||
}
|
||||
|
||||
wm.work_area_offset = value.global_work_area_offset;
|
||||
|
||||
match value.focus_follows_mouse {
|
||||
None => WindowsApi::disable_focus_follows_mouse()?,
|
||||
Some(FocusFollowsMouseImplementation::Windows) => {
|
||||
WindowsApi::enable_focus_follows_mouse()?;
|
||||
}
|
||||
Some(FocusFollowsMouseImplementation::Komorebi) => {}
|
||||
};
|
||||
|
||||
wm.focus_follows_mouse = value.focus_follows_mouse;
|
||||
|
||||
Ok(())
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
use crate::com::SetCloak;
|
||||
use std::collections::HashMap;
|
||||
use std::convert::TryFrom;
|
||||
use std::fmt::Display;
|
||||
use std::fmt::Formatter;
|
||||
@@ -7,6 +8,9 @@ use std::sync::atomic::Ordering;
|
||||
|
||||
use color_eyre::eyre::anyhow;
|
||||
use color_eyre::Result;
|
||||
use komorebi_core::config_generation::IdWithIdentifier;
|
||||
use komorebi_core::config_generation::MatchingStrategy;
|
||||
use regex::Regex;
|
||||
use schemars::JsonSchema;
|
||||
use serde::ser::Error;
|
||||
use serde::ser::SerializeStruct;
|
||||
@@ -17,6 +21,7 @@ use winput::press;
|
||||
use winput::release;
|
||||
use winput::Vk;
|
||||
|
||||
use komorebi_core::ApplicationIdentifier;
|
||||
use komorebi_core::HidingBehaviour;
|
||||
use komorebi_core::Rect;
|
||||
|
||||
@@ -33,6 +38,7 @@ use crate::LAYERED_WHITELIST;
|
||||
use crate::MANAGE_IDENTIFIERS;
|
||||
use crate::NO_TITLEBAR;
|
||||
use crate::PERMAIGNORE_CLASSES;
|
||||
use crate::REGEX_IDENTIFIERS;
|
||||
use crate::WSL2_UI_PROCESSES;
|
||||
|
||||
#[derive(Debug, Clone, Copy, JsonSchema)]
|
||||
@@ -124,15 +130,21 @@ impl Window {
|
||||
top: bool,
|
||||
) -> Result<()> {
|
||||
let mut rect = *layout;
|
||||
let mut should_remove_border = true;
|
||||
|
||||
let border_overflows = BORDER_OVERFLOW_IDENTIFIERS.lock();
|
||||
if border_overflows.contains(&self.title()?)
|
||||
|| border_overflows.contains(&self.exe()?)
|
||||
|| border_overflows.contains(&self.class()?)
|
||||
{
|
||||
should_remove_border = false;
|
||||
}
|
||||
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
|
||||
@@ -229,7 +241,7 @@ impl Window {
|
||||
|
||||
// Raise Window to foreground
|
||||
match WindowsApi::set_foreground_window(self.hwnd()) {
|
||||
Ok(_) => {}
|
||||
Ok(()) => {}
|
||||
Err(error) => {
|
||||
tracing::error!(
|
||||
"could not set as foreground window, but continuing execution of raise(): {}",
|
||||
@@ -240,7 +252,7 @@ impl Window {
|
||||
|
||||
// This isn't really needed when the above command works as expected via AHK
|
||||
match WindowsApi::set_focus(self.hwnd()) {
|
||||
Ok(_) => {}
|
||||
Ok(()) => {}
|
||||
Err(error) => {
|
||||
tracing::error!(
|
||||
"could not set focus, but continuing execution of raise(): {}",
|
||||
@@ -290,7 +302,7 @@ impl Window {
|
||||
}
|
||||
|
||||
match WindowsApi::set_foreground_window(self.hwnd()) {
|
||||
Ok(_) => {
|
||||
Ok(()) => {
|
||||
foregrounded = true;
|
||||
}
|
||||
Err(error) => {
|
||||
@@ -322,7 +334,7 @@ impl Window {
|
||||
|
||||
// This isn't really needed when the above command works as expected via AHK
|
||||
match WindowsApi::set_focus(self.hwnd()) {
|
||||
Ok(_) => {}
|
||||
Ok(()) => {}
|
||||
Err(error) => {
|
||||
tracing::error!(
|
||||
"could not set focus, but continuing execution of focus(): {}",
|
||||
@@ -348,8 +360,7 @@ impl Window {
|
||||
let mut ex_style = self.ex_style()?;
|
||||
ex_style.insert(ExtendedWindowStyle::LAYERED);
|
||||
self.update_ex_style(&ex_style)?;
|
||||
WindowsApi::set_transparent(self.hwnd());
|
||||
Ok(())
|
||||
WindowsApi::set_transparent(self.hwnd())
|
||||
}
|
||||
|
||||
pub fn opaque(self) -> Result<()> {
|
||||
@@ -470,42 +481,38 @@ fn window_is_eligible(
|
||||
}
|
||||
}
|
||||
|
||||
let mut should_float = false;
|
||||
let regex_identifiers = REGEX_IDENTIFIERS.lock();
|
||||
|
||||
{
|
||||
let float_identifiers = FLOAT_IDENTIFIERS.lock();
|
||||
for identifier in float_identifiers.iter() {
|
||||
if title.starts_with(identifier) || title.ends_with(identifier) {
|
||||
should_float = true;
|
||||
}
|
||||
let float_identifiers = FLOAT_IDENTIFIERS.lock();
|
||||
let should_float = should_act(
|
||||
title,
|
||||
exe_name,
|
||||
class,
|
||||
&float_identifiers,
|
||||
®ex_identifiers,
|
||||
);
|
||||
|
||||
if class.starts_with(identifier) || class.ends_with(identifier) {
|
||||
should_float = true;
|
||||
}
|
||||
|
||||
if identifier == exe_name {
|
||||
should_float = true;
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
let managed_override = {
|
||||
let manage_identifiers = MANAGE_IDENTIFIERS.lock();
|
||||
manage_identifiers.contains(exe_name)
|
||||
|| manage_identifiers.contains(class)
|
||||
|| manage_identifiers.contains(title)
|
||||
};
|
||||
let manage_identifiers = MANAGE_IDENTIFIERS.lock();
|
||||
let managed_override = should_act(
|
||||
title,
|
||||
exe_name,
|
||||
class,
|
||||
&manage_identifiers,
|
||||
®ex_identifiers,
|
||||
);
|
||||
|
||||
if should_float && !managed_override {
|
||||
return false;
|
||||
}
|
||||
|
||||
let allow_layered = {
|
||||
let layered_whitelist = LAYERED_WHITELIST.lock();
|
||||
layered_whitelist.contains(exe_name)
|
||||
|| layered_whitelist.contains(class)
|
||||
|| layered_whitelist.contains(title)
|
||||
};
|
||||
let layered_whitelist = LAYERED_WHITELIST.lock();
|
||||
let allow_layered = should_act(
|
||||
title,
|
||||
exe_name,
|
||||
class,
|
||||
&layered_whitelist,
|
||||
®ex_identifiers,
|
||||
);
|
||||
|
||||
// TODO: might need this for transparency
|
||||
// let allow_layered = true;
|
||||
@@ -536,3 +543,131 @@ fn window_is_eligible(
|
||||
|
||||
false
|
||||
}
|
||||
|
||||
#[allow(clippy::cognitive_complexity, clippy::too_many_lines)]
|
||||
pub fn should_act(
|
||||
title: &str,
|
||||
exe_name: &str,
|
||||
class: &str,
|
||||
identifiers: &[IdWithIdentifier],
|
||||
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");
|
||||
}
|
||||
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;
|
||||
}
|
||||
}
|
||||
},
|
||||
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
|
||||
}
|
||||
|
||||
@@ -3,11 +3,13 @@ use std::collections::HashSet;
|
||||
use std::collections::VecDeque;
|
||||
use std::io::ErrorKind;
|
||||
use std::num::NonZeroUsize;
|
||||
use std::path::Path;
|
||||
use std::path::PathBuf;
|
||||
use std::sync::atomic::Ordering;
|
||||
use std::sync::Arc;
|
||||
|
||||
use color_eyre::eyre::anyhow;
|
||||
use color_eyre::eyre::bail;
|
||||
use color_eyre::Result;
|
||||
use crossbeam_channel::Receiver;
|
||||
use hotwatch::notify::DebouncedEvent;
|
||||
@@ -17,6 +19,7 @@ use schemars::JsonSchema;
|
||||
use serde::Serialize;
|
||||
use uds_windows::UnixListener;
|
||||
|
||||
use komorebi_core::config_generation::IdWithIdentifier;
|
||||
use komorebi_core::custom_layout::CustomLayout;
|
||||
use komorebi_core::Arrangement;
|
||||
use komorebi_core::Axis;
|
||||
@@ -50,6 +53,7 @@ use crate::FLOAT_IDENTIFIERS;
|
||||
use crate::HOME_DIR;
|
||||
use crate::LAYERED_WHITELIST;
|
||||
use crate::MANAGE_IDENTIFIERS;
|
||||
use crate::MONITOR_INDEX_PREFERENCES;
|
||||
use crate::NO_TITLEBAR;
|
||||
use crate::OBJECT_NAME_CHANGE_ON_LAUNCH;
|
||||
use crate::REMOVE_TITLEBARS;
|
||||
@@ -92,12 +96,13 @@ pub struct State {
|
||||
pub mouse_follows_focus: bool,
|
||||
pub has_pending_raise_op: bool,
|
||||
pub remove_titlebars: bool,
|
||||
pub float_identifiers: Vec<String>,
|
||||
pub manage_identifiers: Vec<String>,
|
||||
pub layered_whitelist: Vec<String>,
|
||||
pub tray_and_multi_window_identifiers: Vec<String>,
|
||||
pub border_overflow_identifiers: Vec<String>,
|
||||
pub name_change_on_launch_identifiers: Vec<String>,
|
||||
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 monitor_index_preferences: HashMap<usize, Rect>,
|
||||
}
|
||||
|
||||
impl AsRef<Self> for WindowManager {
|
||||
@@ -126,6 +131,7 @@ impl From<&WindowManager> for State {
|
||||
tray_and_multi_window_identifiers: TRAY_AND_MULTI_WINDOW_IDENTIFIERS.lock().clone(),
|
||||
border_overflow_identifiers: BORDER_OVERFLOW_IDENTIFIERS.lock().clone(),
|
||||
name_change_on_launch_identifiers: OBJECT_NAME_CHANGE_ON_LAUNCH.lock().clone(),
|
||||
monitor_index_preferences: MONITOR_INDEX_PREFERENCES.lock().clone(),
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -162,7 +168,7 @@ impl WindowManager {
|
||||
let socket = DATA_DIR.join("komorebi.sock");
|
||||
|
||||
match std::fs::remove_file(&socket) {
|
||||
Ok(_) => {}
|
||||
Ok(()) => {}
|
||||
Err(error) => match error.kind() {
|
||||
// Doing this because ::exists() doesn't work reliably on Windows via IntelliJ
|
||||
ErrorKind::NotFound => {}
|
||||
@@ -243,13 +249,8 @@ impl WindowManager {
|
||||
|
||||
#[tracing::instrument(skip(self))]
|
||||
pub fn watch_configuration(&mut self, enable: bool) -> Result<()> {
|
||||
let home = HOME_DIR.clone();
|
||||
|
||||
let mut config_pwsh = home.clone();
|
||||
config_pwsh.push("komorebi.ps1");
|
||||
|
||||
let mut config_ahk = home;
|
||||
config_ahk.push("komorebi.ahk");
|
||||
let config_pwsh = HOME_DIR.join("komorebi.ps1");
|
||||
let config_ahk = HOME_DIR.join("komorebi.ahk");
|
||||
|
||||
if config_pwsh.exists() {
|
||||
self.configure_watcher(enable, config_pwsh)?;
|
||||
@@ -261,50 +262,39 @@ impl WindowManager {
|
||||
}
|
||||
|
||||
fn configure_watcher(&mut self, enable: bool, config: PathBuf) -> Result<()> {
|
||||
if config.exists() {
|
||||
if enable {
|
||||
tracing::info!(
|
||||
"watching configuration for changes: {}",
|
||||
config
|
||||
.as_os_str()
|
||||
.to_str()
|
||||
.ok_or_else(|| anyhow!("cannot convert path to string"))?
|
||||
);
|
||||
// Always make absolutely sure that there isn't an already existing watch, because
|
||||
// hotwatch allows multiple watches to be registered for the same path
|
||||
match self.hotwatch.unwatch(config.clone()) {
|
||||
Ok(_) => {}
|
||||
Err(error) => match error {
|
||||
hotwatch::Error::Notify(error) => match error {
|
||||
hotwatch::notify::Error::WatchNotFound => {}
|
||||
error => return Err(error.into()),
|
||||
},
|
||||
error @ hotwatch::Error::Io(_) => return Err(error.into()),
|
||||
if enable {
|
||||
tracing::info!("watching configuration for changes: {}", config.display());
|
||||
// Always make absolutely sure that there isn't an already existing watch, because
|
||||
// hotwatch allows multiple watches to be registered for the same path
|
||||
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()),
|
||||
},
|
||||
error @ hotwatch::Error::Io(_) => return Err(error.into()),
|
||||
},
|
||||
}
|
||||
|
||||
self.hotwatch.watch(config, |event| match event {
|
||||
// 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(_) => {
|
||||
std::thread::spawn(|| {
|
||||
load_configuration().expect("could not load configuration");
|
||||
});
|
||||
}
|
||||
_ => {}
|
||||
})?;
|
||||
} else {
|
||||
tracing::info!(
|
||||
"no longer watching configuration for changes: {}",
|
||||
config.display()
|
||||
);
|
||||
|
||||
self.hotwatch.watch(config, |event| match event {
|
||||
// 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(_) => {
|
||||
std::thread::spawn(|| {
|
||||
load_configuration().expect("could not load configuration");
|
||||
});
|
||||
}
|
||||
_ => {}
|
||||
})?;
|
||||
} else {
|
||||
tracing::info!(
|
||||
"no longer watching configuration for changes: {}",
|
||||
config
|
||||
.as_os_str()
|
||||
.to_str()
|
||||
.ok_or_else(|| anyhow!("cannot convert path to string"))?
|
||||
);
|
||||
|
||||
self.hotwatch.unwatch(config)?;
|
||||
};
|
||||
}
|
||||
self.hotwatch.unwatch(config)?;
|
||||
};
|
||||
|
||||
Ok(())
|
||||
}
|
||||
@@ -864,7 +854,7 @@ impl WindowManager {
|
||||
// attach to the thread of the desktop window always seems to result in "Access is
|
||||
// denied (os error 5)"
|
||||
match WindowsApi::set_foreground_window(desktop_window.hwnd()) {
|
||||
Ok(_) => {}
|
||||
Ok(()) => {}
|
||||
Err(error) => {
|
||||
tracing::warn!("{} {}:{}", error, file!(), line!());
|
||||
}
|
||||
@@ -998,9 +988,7 @@ impl WindowManager {
|
||||
let workspace = self.focused_workspace()?;
|
||||
let focused_hwnd = WindowsApi::foreground_window()?;
|
||||
if !workspace.contains_managed_window(focused_hwnd) {
|
||||
return Err(anyhow!(
|
||||
"ignoring commands while active window is not managed by komorebi"
|
||||
));
|
||||
bail!("ignoring commands while active window is not managed by komorebi");
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1116,9 +1104,7 @@ impl WindowManager {
|
||||
.ok_or_else(|| anyhow!("there is no workspace"))?;
|
||||
|
||||
if workspace.maximized_window().is_some() {
|
||||
return Err(anyhow!(
|
||||
"cannot move native maximized window to another monitor or workspace"
|
||||
));
|
||||
bail!("cannot move native maximized window to another monitor or workspace");
|
||||
}
|
||||
|
||||
let container = workspace
|
||||
@@ -1228,9 +1214,7 @@ impl WindowManager {
|
||||
// removing this messes up the monitor / container / window index somewhere
|
||||
// and results in the wrong window getting moved across the monitor boundary
|
||||
if workspace.is_focused_window_monocle_or_maximized()? {
|
||||
return Err(anyhow!(
|
||||
"ignoring command while active window is in monocle mode or maximized"
|
||||
));
|
||||
bail!("ignoring command while active window is in monocle mode or maximized");
|
||||
}
|
||||
|
||||
tracing::info!("moving container");
|
||||
@@ -1392,9 +1376,7 @@ impl WindowManager {
|
||||
|
||||
let workspace = self.focused_workspace_mut()?;
|
||||
if workspace.is_focused_window_monocle_or_maximized()? {
|
||||
return Err(anyhow!(
|
||||
"ignoring command while active window is in monocle mode or maximized"
|
||||
));
|
||||
bail!("ignoring command while active window is in monocle mode or maximized");
|
||||
}
|
||||
|
||||
tracing::info!("moving container");
|
||||
@@ -1421,7 +1403,7 @@ impl WindowManager {
|
||||
.ok_or_else(|| anyhow!("there must be at least one window in a container"))?;
|
||||
|
||||
if len.get() == 1 {
|
||||
return Err(anyhow!("there is only one window in this container"));
|
||||
bail!("there is only one window in this container");
|
||||
}
|
||||
|
||||
let current_idx = container.focused_window_idx();
|
||||
@@ -1506,7 +1488,7 @@ impl WindowManager {
|
||||
tracing::info!("removing window");
|
||||
|
||||
if self.focused_container()?.windows().len() == 1 {
|
||||
return Err(anyhow!("a container must have at least one window"));
|
||||
bail!("a container must have at least one window");
|
||||
}
|
||||
|
||||
let workspace = self.focused_workspace_mut()?;
|
||||
@@ -1702,10 +1684,36 @@ impl WindowManager {
|
||||
}
|
||||
|
||||
#[tracing::instrument(skip(self))]
|
||||
pub fn change_workspace_custom_layout(&mut self, path: PathBuf) -> Result<()> {
|
||||
pub fn cycle_layout(&mut self, direction: CycleDirection) -> Result<()> {
|
||||
tracing::info!("cycling layout");
|
||||
|
||||
let workspace = self.focused_workspace_mut()?;
|
||||
let current_layout = workspace.layout();
|
||||
|
||||
match current_layout {
|
||||
Layout::Default(current) => {
|
||||
let new_layout = match direction {
|
||||
CycleDirection::Previous => current.cycle_previous(),
|
||||
CycleDirection::Next => current.cycle_next(),
|
||||
};
|
||||
|
||||
tracing::info!("next layout: {new_layout}");
|
||||
workspace.set_layout(Layout::Default(new_layout));
|
||||
}
|
||||
Layout::Custom(_) => {}
|
||||
}
|
||||
|
||||
self.update_focused_workspace(self.mouse_follows_focus)
|
||||
}
|
||||
|
||||
#[tracing::instrument(skip(self))]
|
||||
pub fn change_workspace_custom_layout<P>(&mut self, path: P) -> Result<()>
|
||||
where
|
||||
P: AsRef<Path> + std::fmt::Debug,
|
||||
{
|
||||
tracing::info!("changing layout");
|
||||
|
||||
let layout = CustomLayout::from_path_buf(path)?;
|
||||
let layout = CustomLayout::from_path(path)?;
|
||||
let workspace = self.focused_workspace_mut()?;
|
||||
|
||||
match workspace.layout() {
|
||||
@@ -1827,13 +1835,16 @@ impl WindowManager {
|
||||
}
|
||||
|
||||
#[tracing::instrument(skip(self))]
|
||||
pub fn add_workspace_layout_custom_rule(
|
||||
pub fn add_workspace_layout_custom_rule<P>(
|
||||
&mut self,
|
||||
monitor_idx: usize,
|
||||
workspace_idx: usize,
|
||||
at_container_count: usize,
|
||||
path: PathBuf,
|
||||
) -> Result<()> {
|
||||
path: P,
|
||||
) -> Result<()>
|
||||
where
|
||||
P: AsRef<Path> + std::fmt::Debug,
|
||||
{
|
||||
tracing::info!("setting workspace layout");
|
||||
|
||||
let invisible_borders = self.invisible_borders;
|
||||
@@ -1858,7 +1869,7 @@ impl WindowManager {
|
||||
.get_mut(workspace_idx)
|
||||
.ok_or_else(|| anyhow!("there is no monitor"))?;
|
||||
|
||||
let layout = CustomLayout::from_path_buf(path)?;
|
||||
let layout = CustomLayout::from_path(path)?;
|
||||
|
||||
let rules: &mut Vec<(usize, Layout)> = workspace.layout_rules_mut();
|
||||
rules.retain(|pair| pair.0 != at_container_count);
|
||||
@@ -1959,14 +1970,17 @@ impl WindowManager {
|
||||
}
|
||||
|
||||
#[tracing::instrument(skip(self))]
|
||||
pub fn set_workspace_layout_custom(
|
||||
pub fn set_workspace_layout_custom<P>(
|
||||
&mut self,
|
||||
monitor_idx: usize,
|
||||
workspace_idx: usize,
|
||||
path: PathBuf,
|
||||
) -> Result<()> {
|
||||
path: P,
|
||||
) -> Result<()>
|
||||
where
|
||||
P: AsRef<Path> + std::fmt::Debug,
|
||||
{
|
||||
tracing::info!("setting workspace layout");
|
||||
let layout = CustomLayout::from_path_buf(path)?;
|
||||
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();
|
||||
@@ -2137,7 +2151,7 @@ impl WindowManager {
|
||||
if self.monitors().get(idx).is_some() {
|
||||
self.monitors.focus(idx);
|
||||
} else {
|
||||
return Err(anyhow!("this is not a valid monitor index"));
|
||||
bail!("this is not a valid monitor index");
|
||||
}
|
||||
|
||||
Ok(())
|
||||
|
||||
@@ -4,9 +4,11 @@ use std::fmt::Formatter;
|
||||
use schemars::JsonSchema;
|
||||
use serde::Serialize;
|
||||
|
||||
use crate::window::should_act;
|
||||
use crate::window::Window;
|
||||
use crate::winevent::WinEvent;
|
||||
use crate::OBJECT_NAME_CHANGE_ON_LAUNCH;
|
||||
use crate::REGEX_IDENTIFIERS;
|
||||
|
||||
#[derive(Debug, Copy, Clone, Serialize, JsonSchema)]
|
||||
#[serde(tag = "type", content = "content")]
|
||||
@@ -131,11 +133,21 @@ impl WindowManagerEvent {
|
||||
// [yatta\src\windows_event.rs:110] event = 32779 ObjectLocationChange
|
||||
|
||||
let object_name_change_on_launch = OBJECT_NAME_CHANGE_ON_LAUNCH.lock();
|
||||
let regex_identifiers = REGEX_IDENTIFIERS.lock();
|
||||
|
||||
if object_name_change_on_launch.contains(&window.exe().ok()?)
|
||||
|| object_name_change_on_launch.contains(&window.class().ok()?)
|
||||
|| object_name_change_on_launch.contains(&window.title().ok()?)
|
||||
{
|
||||
let title = &window.title().ok()?;
|
||||
let exe_name = &window.exe().ok()?;
|
||||
let class = &window.class().ok()?;
|
||||
|
||||
let should_trigger = should_act(
|
||||
title,
|
||||
exe_name,
|
||||
class,
|
||||
&object_name_change_on_launch,
|
||||
®ex_identifiers,
|
||||
);
|
||||
|
||||
if should_trigger {
|
||||
Option::from(Self::Show(winevent, window))
|
||||
} else {
|
||||
None
|
||||
|
||||
@@ -105,7 +105,9 @@ use windows::Win32::UI::WindowsAndMessaging::SET_WINDOW_POS_FLAGS;
|
||||
use windows::Win32::UI::WindowsAndMessaging::SHOW_WINDOW_CMD;
|
||||
use windows::Win32::UI::WindowsAndMessaging::SPIF_SENDCHANGE;
|
||||
use windows::Win32::UI::WindowsAndMessaging::SPI_GETACTIVEWINDOWTRACKING;
|
||||
use windows::Win32::UI::WindowsAndMessaging::SPI_GETFOREGROUNDLOCKTIMEOUT;
|
||||
use windows::Win32::UI::WindowsAndMessaging::SPI_SETACTIVEWINDOWTRACKING;
|
||||
use windows::Win32::UI::WindowsAndMessaging::SPI_SETFOREGROUNDLOCKTIMEOUT;
|
||||
use windows::Win32::UI::WindowsAndMessaging::SW_HIDE;
|
||||
use windows::Win32::UI::WindowsAndMessaging::SW_MAXIMIZE;
|
||||
use windows::Win32::UI::WindowsAndMessaging::SW_MINIMIZE;
|
||||
@@ -234,9 +236,7 @@ impl WindowsApi {
|
||||
}
|
||||
|
||||
pub fn enum_windows(callback: WNDENUMPROC, callback_data_address: isize) -> Result<()> {
|
||||
unsafe { EnumWindows(callback, LPARAM(callback_data_address)) }
|
||||
.ok()
|
||||
.process()
|
||||
unsafe { EnumWindows(callback, LPARAM(callback_data_address)) }.process()
|
||||
}
|
||||
|
||||
pub fn load_workspace_information(monitors: &mut Ring<Monitor>) -> Result<()> {
|
||||
@@ -275,9 +275,7 @@ impl WindowsApi {
|
||||
}
|
||||
|
||||
pub fn allow_set_foreground_window(process_id: u32) -> Result<()> {
|
||||
unsafe { AllowSetForegroundWindow(process_id) }
|
||||
.ok()
|
||||
.process()
|
||||
unsafe { AllowSetForegroundWindow(process_id) }.process()
|
||||
}
|
||||
|
||||
pub fn monitor_from_window(hwnd: HWND) -> isize {
|
||||
@@ -313,7 +311,7 @@ impl WindowsApi {
|
||||
}
|
||||
|
||||
pub fn bring_window_to_top(hwnd: HWND) -> Result<()> {
|
||||
unsafe { BringWindowToTop(hwnd) }.ok().process()
|
||||
unsafe { BringWindowToTop(hwnd) }.process()
|
||||
}
|
||||
|
||||
pub fn raise_window(hwnd: HWND) -> Result<()> {
|
||||
@@ -353,7 +351,6 @@ impl WindowsApi {
|
||||
SET_WINDOW_POS_FLAGS(flags),
|
||||
)
|
||||
}
|
||||
.ok()
|
||||
.process()
|
||||
}
|
||||
|
||||
@@ -368,14 +365,12 @@ impl WindowsApi {
|
||||
}
|
||||
|
||||
fn post_message(hwnd: HWND, message: u32, wparam: WPARAM, lparam: LPARAM) -> Result<()> {
|
||||
unsafe { PostMessageW(hwnd, message, wparam, lparam) }
|
||||
.ok()
|
||||
.process()
|
||||
unsafe { PostMessageW(hwnd, message, wparam, lparam) }.process()
|
||||
}
|
||||
|
||||
pub fn close_window(hwnd: HWND) -> Result<()> {
|
||||
match Self::post_message(hwnd, WM_CLOSE, WPARAM(0), LPARAM(0)) {
|
||||
Ok(_) => Ok(()),
|
||||
Ok(()) => Ok(()),
|
||||
Err(_) => Err(anyhow!("could not close window")),
|
||||
}
|
||||
}
|
||||
@@ -436,18 +431,18 @@ impl WindowsApi {
|
||||
|
||||
pub fn window_rect(hwnd: HWND) -> Result<Rect> {
|
||||
let mut rect = unsafe { std::mem::zeroed() };
|
||||
unsafe { GetWindowRect(hwnd, &mut rect) }.ok().process()?;
|
||||
unsafe { GetWindowRect(hwnd, &mut rect) }.process()?;
|
||||
|
||||
Ok(Rect::from(rect))
|
||||
}
|
||||
|
||||
fn set_cursor_pos(x: i32, y: i32) -> Result<()> {
|
||||
unsafe { SetCursorPos(x, y) }.ok().process()
|
||||
unsafe { SetCursorPos(x, y) }.process()
|
||||
}
|
||||
|
||||
pub fn cursor_pos() -> Result<POINT> {
|
||||
let mut cursor_pos = POINT::default();
|
||||
unsafe { GetCursorPos(&mut cursor_pos) }.ok().process()?;
|
||||
unsafe { GetCursorPos(&mut cursor_pos) }.process()?;
|
||||
|
||||
Ok(cursor_pos)
|
||||
}
|
||||
@@ -489,7 +484,7 @@ impl WindowsApi {
|
||||
let mut session_id = 0;
|
||||
|
||||
unsafe {
|
||||
if ProcessIdToSessionId(process_id, &mut session_id).as_bool() {
|
||||
if ProcessIdToSessionId(process_id, &mut session_id).is_ok() {
|
||||
Ok(session_id)
|
||||
} else {
|
||||
Err(anyhow!("could not determine current session id"))
|
||||
@@ -565,7 +560,7 @@ impl WindowsApi {
|
||||
}
|
||||
|
||||
pub fn close_process(handle: HANDLE) -> Result<()> {
|
||||
unsafe { CloseHandle(handle) }.ok().process()
|
||||
unsafe { CloseHandle(handle) }.process()
|
||||
}
|
||||
|
||||
pub fn process_handle(process_id: u32) -> Result<HANDLE> {
|
||||
@@ -580,7 +575,6 @@ impl WindowsApi {
|
||||
unsafe {
|
||||
QueryFullProcessImageNameW(handle, PROCESS_NAME_WIN32, PWSTR(text_ptr), &mut len)
|
||||
}
|
||||
.ok()
|
||||
.process()?;
|
||||
|
||||
Ok(String::from_utf16(&path[..len as usize])?)
|
||||
@@ -673,7 +667,6 @@ impl WindowsApi {
|
||||
|
||||
pub fn set_process_dpi_awareness_context() -> Result<()> {
|
||||
unsafe { SetProcessDpiAwarenessContext(DPI_AWARENESS_CONTEXT_PER_MONITOR_AWARE_V2) }
|
||||
.ok()
|
||||
.process()
|
||||
}
|
||||
|
||||
@@ -685,10 +678,45 @@ impl WindowsApi {
|
||||
update_flags: SYSTEM_PARAMETERS_INFO_UPDATE_FLAGS,
|
||||
) -> Result<()> {
|
||||
unsafe { SystemParametersInfoW(action, ui_param, Option::from(pv_param), update_flags) }
|
||||
.ok()
|
||||
.process()
|
||||
}
|
||||
|
||||
#[tracing::instrument]
|
||||
pub fn foreground_lock_timeout() -> Result<()> {
|
||||
let mut value: u32 = 0;
|
||||
|
||||
Self::system_parameters_info_w(
|
||||
SPI_GETFOREGROUNDLOCKTIMEOUT,
|
||||
0,
|
||||
std::ptr::addr_of_mut!(value).cast(),
|
||||
SPIF_SENDCHANGE,
|
||||
)?;
|
||||
|
||||
tracing::info!("current value of ForegroundLockTimeout is {value}");
|
||||
|
||||
if value != 0 {
|
||||
tracing::info!("updating value of ForegroundLockTimeout to {value} in order to enable keyboard-driven focus updating");
|
||||
|
||||
Self::system_parameters_info_w(
|
||||
SPI_SETFOREGROUNDLOCKTIMEOUT,
|
||||
0,
|
||||
0 as *mut c_void,
|
||||
SPIF_SENDCHANGE,
|
||||
)?;
|
||||
|
||||
Self::system_parameters_info_w(
|
||||
SPI_GETFOREGROUNDLOCKTIMEOUT,
|
||||
0,
|
||||
std::ptr::addr_of_mut!(value).cast(),
|
||||
SPIF_SENDCHANGE,
|
||||
)?;
|
||||
|
||||
tracing::info!("updated value of ForegroundLockTimeout is now {value}");
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[allow(dead_code)]
|
||||
pub fn focus_follows_mouse() -> Result<bool> {
|
||||
let mut is_enabled: BOOL = unsafe { std::mem::zeroed() };
|
||||
@@ -777,19 +805,21 @@ impl WindowsApi {
|
||||
None,
|
||||
);
|
||||
|
||||
SetLayeredWindowAttributes(hwnd, COLORREF(TRANSPARENCY_COLOUR), 0, LWA_COLORKEY);
|
||||
SetLayeredWindowAttributes(hwnd, COLORREF(TRANSPARENCY_COLOUR), 0, LWA_COLORKEY)?;
|
||||
|
||||
hwnd
|
||||
}
|
||||
.process()
|
||||
}
|
||||
|
||||
pub fn set_transparent(hwnd: HWND) {
|
||||
pub fn set_transparent(hwnd: HWND) -> Result<()> {
|
||||
unsafe {
|
||||
#[allow(clippy::cast_sign_loss)]
|
||||
// TODO: alpha should be configurable
|
||||
SetLayeredWindowAttributes(hwnd, COLORREF(-1i32 as u32), 150, LWA_ALPHA);
|
||||
SetLayeredWindowAttributes(hwnd, COLORREF(-1i32 as u32), 150, LWA_ALPHA)?;
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn create_hidden_window(name: PCSTR, instance: HMODULE) -> Result<isize> {
|
||||
|
||||
@@ -61,7 +61,7 @@ impl WinEventListener {
|
||||
MessageLoop::start(10, |_msg| {
|
||||
if let Ok(event) = WINEVENT_CALLBACK_CHANNEL.lock().1.try_recv() {
|
||||
match outgoing.send(event) {
|
||||
Ok(_) => {}
|
||||
Ok(()) => {}
|
||||
Err(error) => {
|
||||
tracing::error!("{}", error);
|
||||
}
|
||||
|
||||
@@ -104,11 +104,17 @@ impl Workspace {
|
||||
|
||||
if let Some(layout) = &config.layout {
|
||||
self.layout = Layout::Default(*layout);
|
||||
self.tile = true;
|
||||
}
|
||||
|
||||
if let Some(pathbuf) = &config.custom_layout {
|
||||
let layout = CustomLayout::from_path_buf(pathbuf.clone())?;
|
||||
let layout = CustomLayout::from_path(pathbuf)?;
|
||||
self.layout = Layout::Custom(layout);
|
||||
self.tile = true;
|
||||
}
|
||||
|
||||
if config.custom_layout.is_none() && config.layout.is_none() {
|
||||
self.tile = false;
|
||||
}
|
||||
|
||||
if let Some(layout_rules) = &config.layout_rules {
|
||||
@@ -123,7 +129,7 @@ impl Workspace {
|
||||
if let Some(layout_rules) = &config.custom_layout_rules {
|
||||
let rules = self.layout_rules_mut();
|
||||
for (count, pathbuf) in layout_rules {
|
||||
let rule = CustomLayout::from_path_buf(pathbuf.clone())?;
|
||||
let rule = CustomLayout::from_path(pathbuf)?;
|
||||
rules.push((*count, Layout::Custom(rule)));
|
||||
}
|
||||
}
|
||||
@@ -775,6 +781,16 @@ impl Workspace {
|
||||
}
|
||||
|
||||
fn enforce_resize_constraints(&mut self) {
|
||||
match self.layout {
|
||||
Layout::Default(DefaultLayout::BSP) => self.enforce_resize_constraints_for_bsp(),
|
||||
Layout::Default(DefaultLayout::UltrawideVerticalStack) => {
|
||||
self.enforce_resize_for_ultrawide();
|
||||
}
|
||||
_ => self.enforce_no_resize(),
|
||||
}
|
||||
}
|
||||
|
||||
fn enforce_resize_constraints_for_bsp(&mut self) {
|
||||
for (i, rect) in self.resize_dimensions_mut().iter_mut().enumerate() {
|
||||
if let Some(rect) = rect {
|
||||
// Even containers can't be resized to the bottom
|
||||
@@ -800,6 +816,72 @@ impl Workspace {
|
||||
}
|
||||
}
|
||||
|
||||
fn enforce_resize_for_ultrawide(&mut self) {
|
||||
let resize_dimensions = self.resize_dimensions_mut();
|
||||
match resize_dimensions.len() {
|
||||
// Single window can not be resized at all
|
||||
0 | 1 => self.enforce_no_resize(),
|
||||
// Two windows can only be resized in the middle
|
||||
2 => {
|
||||
// Zero is actually on the right
|
||||
if let Some(mut right) = resize_dimensions[0] {
|
||||
right.top = 0;
|
||||
right.bottom = 0;
|
||||
right.right = 0;
|
||||
}
|
||||
|
||||
// One is on the left
|
||||
if let Some(mut left) = resize_dimensions[1] {
|
||||
left.top = 0;
|
||||
left.bottom = 0;
|
||||
left.left = 0;
|
||||
}
|
||||
}
|
||||
// Three or more windows means 0 is in center, 1 is at the left, 2.. are a vertical
|
||||
// stack on the right
|
||||
_ => {
|
||||
// Central can be resized left or right
|
||||
if let Some(mut right) = resize_dimensions[0] {
|
||||
right.top = 0;
|
||||
right.bottom = 0;
|
||||
}
|
||||
|
||||
// Left one can only be resized to the right
|
||||
if let Some(mut left) = resize_dimensions[1] {
|
||||
left.top = 0;
|
||||
left.bottom = 0;
|
||||
left.left = 0;
|
||||
}
|
||||
|
||||
// Handle stack on the right
|
||||
let stack_size = resize_dimensions[2..].len();
|
||||
for (i, rect) in resize_dimensions[2..].iter_mut().enumerate() {
|
||||
if let Some(rect) = rect {
|
||||
// No containers can resize to the right
|
||||
rect.right = 0;
|
||||
|
||||
// First container in stack cant resize up
|
||||
if i == 0 {
|
||||
rect.top = 0;
|
||||
} else if i == stack_size - 1 {
|
||||
// Last cant be resized to the bottom
|
||||
rect.bottom = 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn enforce_no_resize(&mut self) {
|
||||
for rect in self.resize_dimensions_mut().iter_mut().flatten() {
|
||||
rect.left = 0;
|
||||
rect.right = 0;
|
||||
rect.top = 0;
|
||||
rect.bottom = 0;
|
||||
}
|
||||
}
|
||||
|
||||
pub fn new_monocle_container(&mut self) -> Result<()> {
|
||||
let focused_idx = self.focused_container_idx();
|
||||
let container = self
|
||||
|
||||
@@ -196,6 +196,10 @@ ChangeLayout(default_layout) {
|
||||
RunWait("komorebic.exe change-layout " default_layout, , "Hide")
|
||||
}
|
||||
|
||||
CycleLayout(operation_direction) {
|
||||
RunWait("komorebic.exe cycle-layout " operation_direction, , "Hide")
|
||||
}
|
||||
|
||||
LoadCustomLayout(path) {
|
||||
RunWait("komorebic.exe load-custom-layout " path, , "Hide")
|
||||
}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
[package]
|
||||
name = "komorebic"
|
||||
version = "0.1.17"
|
||||
version = "0.1.19"
|
||||
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"]
|
||||
@@ -15,8 +15,6 @@ derive-ahk = { path = "../derive-ahk" }
|
||||
komorebi-core = { path = "../komorebi-core" }
|
||||
|
||||
clap = { version = "4", features = ["derive", "wrap_help"] }
|
||||
color-eyre = "0.6"
|
||||
dirs = "5"
|
||||
fs-tail = "0.1"
|
||||
heck = "0.4"
|
||||
lazy_static = "1"
|
||||
@@ -28,5 +26,8 @@ serde_json = "1"
|
||||
serde_yaml = "0.9"
|
||||
sysinfo = "0.29"
|
||||
uds_windows = "1"
|
||||
which = "4"
|
||||
which = "5"
|
||||
windows = { workspace = true }
|
||||
color-eyre = { workspace = true }
|
||||
dirs = { workspace = true }
|
||||
dunce = { workspace = true }
|
||||
@@ -16,9 +16,11 @@ use std::time::Duration;
|
||||
use clap::Parser;
|
||||
use clap::ValueEnum;
|
||||
use color_eyre::eyre::anyhow;
|
||||
use color_eyre::eyre::bail;
|
||||
use color_eyre::Result;
|
||||
use fs_tail::TailedFile;
|
||||
use heck::ToKebabCase;
|
||||
use komorebi_core::resolve_home_path;
|
||||
use lazy_static::lazy_static;
|
||||
use paste::paste;
|
||||
use sysinfo::SystemExt;
|
||||
@@ -125,6 +127,7 @@ gen_enum_subcommand_args! {
|
||||
CycleStack: CycleDirection,
|
||||
FlipLayout: Axis,
|
||||
ChangeLayout: DefaultLayout,
|
||||
CycleLayout: CycleDirection,
|
||||
WatchConfiguration: BooleanState,
|
||||
MouseFollowsFocus: BooleanState,
|
||||
Query: StateQuery,
|
||||
@@ -258,7 +261,7 @@ pub struct WorkspaceCustomLayout {
|
||||
workspace: usize,
|
||||
|
||||
/// JSON or YAML file from which the custom layout definition should be loaded
|
||||
path: String,
|
||||
path: PathBuf,
|
||||
}
|
||||
|
||||
#[derive(Parser, AhkFunction)]
|
||||
@@ -267,7 +270,7 @@ pub struct NamedWorkspaceCustomLayout {
|
||||
workspace: String,
|
||||
|
||||
/// JSON or YAML file from which the custom layout definition should be loaded
|
||||
path: String,
|
||||
path: PathBuf,
|
||||
}
|
||||
|
||||
#[derive(Parser, AhkFunction)]
|
||||
@@ -309,7 +312,7 @@ pub struct WorkspaceCustomLayoutRule {
|
||||
at_container_count: usize,
|
||||
|
||||
/// JSON or YAML file from which the custom layout definition should be loaded
|
||||
path: String,
|
||||
path: PathBuf,
|
||||
}
|
||||
|
||||
#[derive(Parser, AhkFunction)]
|
||||
@@ -321,7 +324,7 @@ pub struct NamedWorkspaceCustomLayoutRule {
|
||||
at_container_count: usize,
|
||||
|
||||
/// JSON or YAML file from which the custom layout definition should be loaded
|
||||
path: String,
|
||||
path: PathBuf,
|
||||
}
|
||||
|
||||
#[derive(Parser, AhkFunction)]
|
||||
@@ -430,6 +433,24 @@ pub struct SendToMonitorWorkspace {
|
||||
target_workspace: usize,
|
||||
}
|
||||
|
||||
macro_rules! gen_focused_workspace_padding_subcommand_args {
|
||||
// SubCommand Pattern
|
||||
( $( $name:ident ),+ $(,)? ) => {
|
||||
$(
|
||||
#[derive(clap::Parser, derive_ahk::AhkFunction)]
|
||||
pub struct $name {
|
||||
/// Pixels size to set as an integer
|
||||
size: i32,
|
||||
}
|
||||
)+
|
||||
};
|
||||
}
|
||||
|
||||
gen_focused_workspace_padding_subcommand_args! {
|
||||
FocusedWorkspaceContainerPadding,
|
||||
FocusedWorkspacePadding,
|
||||
}
|
||||
|
||||
macro_rules! gen_padding_subcommand_args {
|
||||
// SubCommand Pattern
|
||||
( $( $name:ident ),+ $(,)? ) => {
|
||||
@@ -607,6 +628,7 @@ struct ActiveWindowBorderOffset {
|
||||
}
|
||||
|
||||
#[derive(Parser, AhkFunction)]
|
||||
#[allow(clippy::struct_excessive_bools)]
|
||||
struct Start {
|
||||
/// Allow the use of komorebi's custom focus-follows-mouse implementation
|
||||
#[clap(action, short, long = "ffm")]
|
||||
@@ -623,24 +645,34 @@ struct Start {
|
||||
/// Start whkd in a background process
|
||||
#[clap(action, long)]
|
||||
whkd: bool,
|
||||
/// Start autohotkey configuration file
|
||||
#[clap(action, long)]
|
||||
ahk: bool,
|
||||
}
|
||||
|
||||
#[derive(Parser, AhkFunction)]
|
||||
struct Stop {
|
||||
/// Stop whkd if it is running as a background process
|
||||
#[clap(action, long)]
|
||||
whkd: bool,
|
||||
}
|
||||
|
||||
#[derive(Parser, AhkFunction)]
|
||||
struct SaveResize {
|
||||
/// File to which the resize layout dimensions should be saved
|
||||
path: String,
|
||||
path: PathBuf,
|
||||
}
|
||||
|
||||
#[derive(Parser, AhkFunction)]
|
||||
struct LoadResize {
|
||||
/// File from which the resize layout dimensions should be loaded
|
||||
path: String,
|
||||
path: PathBuf,
|
||||
}
|
||||
|
||||
#[derive(Parser, AhkFunction)]
|
||||
struct LoadCustomLayout {
|
||||
/// JSON or YAML file from which the custom layout definition should be loaded
|
||||
path: String,
|
||||
path: PathBuf,
|
||||
}
|
||||
|
||||
#[derive(Parser, AhkFunction)]
|
||||
@@ -658,23 +690,23 @@ struct Unsubscribe {
|
||||
#[derive(Parser, AhkFunction)]
|
||||
struct AhkAppSpecificConfiguration {
|
||||
/// YAML file from which the application-specific configurations should be loaded
|
||||
path: String,
|
||||
path: PathBuf,
|
||||
/// Optional YAML file of overrides to apply over the first file
|
||||
override_path: Option<String>,
|
||||
override_path: Option<PathBuf>,
|
||||
}
|
||||
|
||||
#[derive(Parser, AhkFunction)]
|
||||
struct PwshAppSpecificConfiguration {
|
||||
/// YAML file from which the application-specific configurations should be loaded
|
||||
path: String,
|
||||
path: PathBuf,
|
||||
/// Optional YAML file of overrides to apply over the first file
|
||||
override_path: Option<String>,
|
||||
override_path: Option<PathBuf>,
|
||||
}
|
||||
|
||||
#[derive(Parser, AhkFunction)]
|
||||
struct FormatAppSpecificConfiguration {
|
||||
/// YAML file from which the application-specific configurations should be loaded
|
||||
path: String,
|
||||
path: PathBuf,
|
||||
}
|
||||
|
||||
#[derive(Parser, AhkFunction)]
|
||||
@@ -683,6 +715,22 @@ struct AltFocusHack {
|
||||
boolean_state: BooleanState,
|
||||
}
|
||||
|
||||
#[derive(Parser, AhkFunction)]
|
||||
struct EnableAutostart {
|
||||
/// Path to a static configuration JSON file
|
||||
#[clap(action, short, long)]
|
||||
config: PathBuf,
|
||||
/// Enable komorebi's custom focus-follows-mouse implementation
|
||||
#[clap(action, short, long = "ffm")]
|
||||
ffm: bool,
|
||||
/// Enable autostart of whkd
|
||||
#[clap(action, long)]
|
||||
whkd: bool,
|
||||
/// Enable autostart of ahk
|
||||
#[clap(action, long)]
|
||||
ahk: bool,
|
||||
}
|
||||
|
||||
#[derive(Parser)]
|
||||
#[clap(author, about, version)]
|
||||
struct Opts {
|
||||
@@ -692,10 +740,12 @@ struct Opts {
|
||||
|
||||
#[derive(Parser, AhkLibrary)]
|
||||
enum SubCommand {
|
||||
/// Gather example configurations for a new-user quickstart
|
||||
Quickstart,
|
||||
/// Start komorebi.exe as a background process
|
||||
Start(Start),
|
||||
/// Stop the komorebi.exe process and restore all hidden windows
|
||||
Stop,
|
||||
Stop(Stop),
|
||||
/// Output various important komorebi-related environment values
|
||||
Check,
|
||||
/// Show a JSON representation of the current window manager state
|
||||
@@ -832,6 +882,12 @@ enum SubCommand {
|
||||
/// Set offsets for a monitor to exclude parts of the work area from tiling
|
||||
#[clap(arg_required_else_help = true)]
|
||||
MonitorWorkAreaOffset(MonitorWorkAreaOffset),
|
||||
/// Set container padding on the focused workspace
|
||||
#[clap(arg_required_else_help = true)]
|
||||
FocusedWorkspaceContainerPadding(FocusedWorkspaceContainerPadding),
|
||||
/// Set workspace padding on the focused workspace
|
||||
#[clap(arg_required_else_help = true)]
|
||||
FocusedWorkspacePadding(FocusedWorkspacePadding),
|
||||
/// Adjust container padding on the focused workspace
|
||||
#[clap(arg_required_else_help = true)]
|
||||
AdjustContainerPadding(AdjustContainerPadding),
|
||||
@@ -841,6 +897,9 @@ enum SubCommand {
|
||||
/// Set the layout on the focused workspace
|
||||
#[clap(arg_required_else_help = true)]
|
||||
ChangeLayout(ChangeLayout),
|
||||
/// Cycle between available layouts
|
||||
#[clap(arg_required_else_help = true)]
|
||||
CycleLayout(CycleLayout),
|
||||
/// Load a custom layout from file for the focused workspace
|
||||
#[clap(arg_required_else_help = true)]
|
||||
LoadCustomLayout(LoadCustomLayout),
|
||||
@@ -1036,6 +1095,11 @@ enum SubCommand {
|
||||
StaticConfigSchema,
|
||||
/// Generates a static configuration JSON file based on the current window manager state
|
||||
GenerateStaticConfig,
|
||||
/// Generates the komorebi.lnk shortcut in shell:startup to autostart komorebi
|
||||
#[clap(arg_required_else_help = true)]
|
||||
EnableAutostart(EnableAutostart),
|
||||
/// Deletes the komorebi.lnk shortcut in shell:startup to disable autostart
|
||||
DisableAutostart,
|
||||
}
|
||||
|
||||
pub fn send_message(bytes: &[u8]) -> Result<()> {
|
||||
@@ -1044,16 +1108,136 @@ pub fn send_message(bytes: &[u8]) -> Result<()> {
|
||||
Ok(stream.write_all(bytes)?)
|
||||
}
|
||||
|
||||
fn with_komorebic_socket<F: Fn() -> Result<()>>(f: F) -> Result<()> {
|
||||
let socket = DATA_DIR.join("komorebic.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());
|
||||
}
|
||||
},
|
||||
};
|
||||
|
||||
f()?;
|
||||
|
||||
let listener = UnixListener::bind(socket)?;
|
||||
match listener.accept() {
|
||||
Ok(incoming) => {
|
||||
let stream = BufReader::new(incoming.0);
|
||||
for line in stream.lines() {
|
||||
println!("{}", line?);
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
Err(error) => {
|
||||
panic!("{}", error);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn startup_dir() -> Result<PathBuf> {
|
||||
let startup = dirs::home_dir()
|
||||
.expect("unable to obtain user's home folder")
|
||||
.join("AppData")
|
||||
.join("Roaming")
|
||||
.join("Microsoft")
|
||||
.join("Windows")
|
||||
.join("Start Menu")
|
||||
.join("Programs")
|
||||
.join("Startup");
|
||||
|
||||
if !startup.is_dir() {
|
||||
std::fs::create_dir_all(&startup)?;
|
||||
}
|
||||
|
||||
Ok(startup)
|
||||
}
|
||||
|
||||
#[allow(clippy::too_many_lines, clippy::cognitive_complexity)]
|
||||
fn main() -> Result<()> {
|
||||
let opts: Opts = Opts::parse();
|
||||
|
||||
match opts.subcmd {
|
||||
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");
|
||||
std::fs::create_dir_all(&config_dir)?;
|
||||
|
||||
let komorebi_json = reqwest::blocking::get(
|
||||
format!("https://raw.githubusercontent.com/LGUG2Z/komorebi/v{version}/komorebi.example.json")
|
||||
)?.text()?;
|
||||
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()?;
|
||||
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()?;
|
||||
std::fs::write(config_dir.join("whkdrc"), whkdrc)?;
|
||||
|
||||
println!("Example ~/komorebi.json, ~/.config/whkdrc and latest ~/applications.yaml files downloaded");
|
||||
println!(
|
||||
"You can now run komorebic start -c \"$Env:USERPROFILE\\komorebi.json\" --whkd"
|
||||
);
|
||||
}
|
||||
SubCommand::EnableAutostart(args) => {
|
||||
let mut current_exe_dir = std::env::current_exe().expect("unable to get exec path");
|
||||
current_exe_dir.pop();
|
||||
|
||||
let komorebic_exe = current_exe_dir.join("komorebic.exe");
|
||||
let komorebic_exe = dunce::simplified(&komorebic_exe);
|
||||
|
||||
let startup_dir = startup_dir()?;
|
||||
let shortcut_file = startup_dir.join("komorebi.lnk");
|
||||
let shortcut_file = dunce::simplified(&shortcut_file);
|
||||
|
||||
let mut arguments = format!(
|
||||
"start --config {}",
|
||||
dunce::canonicalize(args.config)?.display()
|
||||
);
|
||||
|
||||
if args.ffm {
|
||||
arguments.push_str(" --ffm");
|
||||
}
|
||||
|
||||
if args.whkd {
|
||||
arguments.push_str(" --whkd");
|
||||
} else if args.ahk {
|
||||
arguments.push_str(" --ahk");
|
||||
}
|
||||
|
||||
Command::new("powershell")
|
||||
.arg("-c")
|
||||
.arg("$WshShell = New-Object -comObject WScript.Shell; $Shortcut = $WshShell.CreateShortcut($env:SHORTCUT_PATH); $Shortcut.TargetPath = $env:TARGET_PATH; $Shortcut.Arguments = $env:TARGET_ARGS; $Shortcut.Save()")
|
||||
.env("SHORTCUT_PATH", shortcut_file.as_os_str())
|
||||
.env("TARGET_PATH", komorebic_exe.as_os_str())
|
||||
.env("TARGET_ARGS", arguments)
|
||||
.output()?;
|
||||
}
|
||||
SubCommand::DisableAutostart => {
|
||||
let startup_dir = startup_dir()?;
|
||||
let shortcut_file = startup_dir.join("komorebi.lnk");
|
||||
|
||||
if shortcut_file.is_file() {
|
||||
std::fs::remove_file(shortcut_file)?;
|
||||
}
|
||||
}
|
||||
SubCommand::Check => {
|
||||
let home = HOME_DIR.clone();
|
||||
let home_lossy_string = home.to_string_lossy();
|
||||
let home_display = HOME_DIR.display();
|
||||
if HAS_CUSTOM_CONFIG_HOME.load(Ordering::SeqCst) {
|
||||
println!("KOMOREBI_CONFIG_HOME detected: {home_lossy_string}\n");
|
||||
println!("KOMOREBI_CONFIG_HOME detected: {home_display}\n");
|
||||
} else {
|
||||
println!(
|
||||
"No KOMOREBI_CONFIG_HOME detected, defaulting to {}\n",
|
||||
@@ -1063,19 +1247,25 @@ fn main() -> Result<()> {
|
||||
);
|
||||
}
|
||||
|
||||
println!("Looking for configuration files in {home_lossy_string}\n");
|
||||
println!("Looking for configuration files in {home_display}\n");
|
||||
|
||||
let mut config_pwsh = home.clone();
|
||||
config_pwsh.push("komorebi.ps1");
|
||||
let static_config = HOME_DIR.join("komorebi.json");
|
||||
let config_pwsh = HOME_DIR.join("komorebi.ps1");
|
||||
let config_ahk = HOME_DIR.join("komorebi.ahk");
|
||||
let config_whkd = dirs::home_dir()
|
||||
.expect("no home dir found")
|
||||
.join(".config")
|
||||
.join("whkdrc");
|
||||
|
||||
let mut config_ahk = home.clone();
|
||||
config_ahk.push("komorebi.ahk");
|
||||
|
||||
if config_pwsh.exists() {
|
||||
if static_config.exists() {
|
||||
println!("Found komorebi.json; this file can be passed to the start command with the --config flag\n");
|
||||
if config_whkd.exists() {
|
||||
println!("Found ~/.config/whkdrc; key bindings will be loaded from here when whkd is started, and you can start it automatically using the --whkd flag\n");
|
||||
} else {
|
||||
println!("No ~/.config/whkdrc found; you may not be able to control komorebi with your keyboard\n");
|
||||
}
|
||||
} else if config_pwsh.exists() {
|
||||
println!("Found komorebi.ps1; this file will be autoloaded by komorebi\n");
|
||||
let mut config_whkd = dirs::home_dir().expect("could not find home dir");
|
||||
config_whkd.push(".config");
|
||||
config_whkd.push("whkdrc");
|
||||
if config_whkd.exists() {
|
||||
println!("Found ~/.config/whkdrc; key bindings will be loaded from here when whkd is started\n");
|
||||
} else {
|
||||
@@ -1084,13 +1274,12 @@ fn main() -> Result<()> {
|
||||
} else if config_ahk.exists() {
|
||||
println!("Found komorebi.ahk; this file will be autoloaded by komorebi\n");
|
||||
} else {
|
||||
println!("No komorebi configuration found in {home_lossy_string}\n");
|
||||
println!("No komorebi configuration found in {home_display}\n");
|
||||
println!("If running 'komorebic start --await-configuration', you will manually have to call the following command to begin tiling: komorebic complete-configuration\n");
|
||||
}
|
||||
}
|
||||
SubCommand::AhkLibrary => {
|
||||
let mut library = HOME_DIR.clone();
|
||||
library.push("komorebic.lib.ahk");
|
||||
let library = HOME_DIR.join("komorebic.lib.ahk");
|
||||
let mut file = OpenOptions::new()
|
||||
.write(true)
|
||||
.create(true)
|
||||
@@ -1098,15 +1287,16 @@ fn main() -> Result<()> {
|
||||
.open(library.clone())?;
|
||||
|
||||
let output: String = SubCommand::generate_ahk_library();
|
||||
let fixed_output = output.replace("%id%", "\"%id%\"");
|
||||
let fixed_id = output.replace("%id%", "\"%id%\"");
|
||||
let fixed_stop_def = fixed_id.replace("Stop(whkd)", "Stop()");
|
||||
let fixed_output =
|
||||
fixed_stop_def.replace("komorebic.exe stop --whkd %whkd%", "komorebic.exe stop");
|
||||
|
||||
file.write_all(fixed_output.as_bytes())?;
|
||||
|
||||
println!(
|
||||
"\nAHKv1 helper library for komorebic written to {}",
|
||||
library.to_str().ok_or_else(|| anyhow!(
|
||||
"could not find the path to the generated ahk lib file"
|
||||
))?
|
||||
library.to_string_lossy()
|
||||
);
|
||||
|
||||
println!("\nYou can convert this file to AHKv2 syntax using https://github.com/mmikeww/AHK-v2-script-converter");
|
||||
@@ -1118,8 +1308,7 @@ fn main() -> Result<()> {
|
||||
println!("\n#Include komorebic.lib.ahk");
|
||||
}
|
||||
SubCommand::Log => {
|
||||
let mut color_log = std::env::temp_dir();
|
||||
color_log.push("komorebi.log");
|
||||
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)]
|
||||
@@ -1272,6 +1461,12 @@ fn main() -> Result<()> {
|
||||
&SocketMessage::NamedWorkspacePadding(arg.workspace, arg.size).as_bytes()?,
|
||||
)?;
|
||||
}
|
||||
SubCommand::FocusedWorkspacePadding(arg) => {
|
||||
send_message(&SocketMessage::FocusedWorkspacePadding(arg.size).as_bytes()?)?;
|
||||
}
|
||||
SubCommand::FocusedWorkspaceContainerPadding(arg) => {
|
||||
send_message(&SocketMessage::FocusedWorkspaceContainerPadding(arg.size).as_bytes()?)?;
|
||||
}
|
||||
SubCommand::AdjustWorkspacePadding(arg) => {
|
||||
send_message(
|
||||
&SocketMessage::AdjustWorkspacePadding(arg.sizing, arg.adjustment).as_bytes()?,
|
||||
@@ -1313,7 +1508,7 @@ fn main() -> Result<()> {
|
||||
&SocketMessage::WorkspaceLayoutCustom(
|
||||
arg.monitor,
|
||||
arg.workspace,
|
||||
resolve_windows_path(&arg.path)?,
|
||||
resolve_home_path(arg.path)?,
|
||||
)
|
||||
.as_bytes()?,
|
||||
)?;
|
||||
@@ -1322,7 +1517,7 @@ fn main() -> Result<()> {
|
||||
send_message(
|
||||
&SocketMessage::NamedWorkspaceLayoutCustom(
|
||||
arg.workspace,
|
||||
resolve_windows_path(&arg.path)?,
|
||||
resolve_home_path(arg.path)?,
|
||||
)
|
||||
.as_bytes()?,
|
||||
)?;
|
||||
@@ -1354,7 +1549,7 @@ fn main() -> Result<()> {
|
||||
arg.monitor,
|
||||
arg.workspace,
|
||||
arg.at_container_count,
|
||||
resolve_windows_path(&arg.path)?,
|
||||
resolve_home_path(arg.path)?,
|
||||
)
|
||||
.as_bytes()?,
|
||||
)?;
|
||||
@@ -1364,7 +1559,7 @@ fn main() -> Result<()> {
|
||||
&SocketMessage::NamedWorkspaceLayoutCustomRule(
|
||||
arg.workspace,
|
||||
arg.at_container_count,
|
||||
resolve_windows_path(&arg.path)?,
|
||||
resolve_home_path(arg.path)?,
|
||||
)
|
||||
.as_bytes()?,
|
||||
)?;
|
||||
@@ -1391,8 +1586,20 @@ fn main() -> Result<()> {
|
||||
)?;
|
||||
}
|
||||
SubCommand::Start(arg) => {
|
||||
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;
|
||||
}
|
||||
}
|
||||
|
||||
if arg.whkd && which("whkd").is_err() {
|
||||
return Err(anyhow!("could not find whkd, please make sure it is installed before using the --whkd flag"));
|
||||
bail!("could not find whkd, please make sure it is installed before using the --whkd flag");
|
||||
}
|
||||
|
||||
if arg.ahk && which(&ahk).is_err() {
|
||||
bail!("could not find autohotkey, please make sure it is installed before using the --ahk flag");
|
||||
}
|
||||
|
||||
let mut buf: PathBuf;
|
||||
@@ -1409,7 +1616,7 @@ fn main() -> Result<()> {
|
||||
buf.pop(); // %USERPROFILE%\scoop\shims
|
||||
buf.pop(); // %USERPROFILE%\scoop
|
||||
buf.push("apps\\komorebi\\current\\komorebi.exe"); //%USERPROFILE%\scoop\komorebi\current\komorebi.exe
|
||||
Option::from(buf.to_str().ok_or_else(|| {
|
||||
Some(buf.to_str().ok_or_else(|| {
|
||||
anyhow!("cannot create a string from the scoop komorebi path")
|
||||
})?)
|
||||
}
|
||||
@@ -1420,17 +1627,13 @@ fn main() -> Result<()> {
|
||||
|
||||
let mut flags = vec![];
|
||||
if let Some(config) = arg.config {
|
||||
let path = resolve_windows_path(config.as_os_str().to_str().unwrap())?;
|
||||
let path = resolve_home_path(config)?;
|
||||
if !path.is_file() {
|
||||
return Err(anyhow!("could not find file: {}", path.to_string_lossy()));
|
||||
bail!("could not find file: {}", path.display());
|
||||
}
|
||||
|
||||
flags.push(format!(
|
||||
"'--config={}'",
|
||||
path.as_os_str()
|
||||
.to_string_lossy()
|
||||
.trim_start_matches(r#"\\?\"#),
|
||||
));
|
||||
// we don't need to replace UNC prefix here as `resolve_home_path` already did
|
||||
flags.push(format!("'--config=\"{}\"'", path.display()));
|
||||
}
|
||||
|
||||
if arg.ffm {
|
||||
@@ -1445,8 +1648,13 @@ fn main() -> Result<()> {
|
||||
flags.push(format!("'--tcp-port={port}'"));
|
||||
}
|
||||
|
||||
let argument_list = flags.join(",");
|
||||
let script = {
|
||||
let script = if flags.is_empty() {
|
||||
format!(
|
||||
"Start-Process '{}' -WindowStyle hidden",
|
||||
exec.unwrap_or("komorebi.exe")
|
||||
)
|
||||
} else {
|
||||
let argument_list = flags.join(",");
|
||||
format!(
|
||||
"Start-Process '{}' -ArgumentList {argument_list} -WindowStyle hidden",
|
||||
exec.unwrap_or("komorebi.exe")
|
||||
@@ -1480,12 +1688,12 @@ fn main() -> Result<()> {
|
||||
}
|
||||
|
||||
if arg.whkd {
|
||||
let script = r#"
|
||||
let script = r"
|
||||
if (!(Get-Process whkd -ErrorAction SilentlyContinue))
|
||||
{
|
||||
Start-Process whkd -WindowStyle hidden
|
||||
}
|
||||
"#;
|
||||
";
|
||||
match powershell_script::run(script) {
|
||||
Ok(_) => {
|
||||
println!("{script}");
|
||||
@@ -1495,8 +1703,43 @@ if (!(Get-Process whkd -ErrorAction SilentlyContinue))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if arg.ahk {
|
||||
let config_ahk = HOME_DIR.join("komorebi.ahk");
|
||||
let config_ahk = dunce::simplified(&config_ahk);
|
||||
|
||||
let script = format!(
|
||||
r#"
|
||||
Start-Process {ahk} {config} -WindowStyle hidden
|
||||
"#,
|
||||
config = config_ahk.display()
|
||||
);
|
||||
|
||||
match powershell_script::run(&script) {
|
||||
Ok(_) => {
|
||||
println!("{script}");
|
||||
}
|
||||
Err(error) => {
|
||||
println!("Error: {error}");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
SubCommand::Stop => {
|
||||
SubCommand::Stop(arg) => {
|
||||
if arg.whkd {
|
||||
let script = r"
|
||||
Stop-Process -Name:whkd -ErrorAction SilentlyContinue
|
||||
";
|
||||
match powershell_script::run(script) {
|
||||
Ok(_) => {
|
||||
println!("{script}");
|
||||
}
|
||||
Err(error) => {
|
||||
println!("Error: {error}");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
send_message(&SocketMessage::Stop.as_bytes()?)?;
|
||||
}
|
||||
SubCommand::FloatRule(arg) => {
|
||||
@@ -1546,9 +1789,12 @@ if (!(Get-Process whkd -ErrorAction SilentlyContinue))
|
||||
SubCommand::ChangeLayout(arg) => {
|
||||
send_message(&SocketMessage::ChangeLayout(arg.default_layout).as_bytes()?)?;
|
||||
}
|
||||
SubCommand::CycleLayout(arg) => {
|
||||
send_message(&SocketMessage::CycleLayout(arg.cycle_direction).as_bytes()?)?;
|
||||
}
|
||||
SubCommand::LoadCustomLayout(arg) => {
|
||||
send_message(
|
||||
&SocketMessage::ChangeLayoutCustom(resolve_windows_path(&arg.path)?).as_bytes()?,
|
||||
&SocketMessage::ChangeLayoutCustom(resolve_home_path(arg.path)?).as_bytes()?,
|
||||
)?;
|
||||
}
|
||||
SubCommand::FlipLayout(arg) => {
|
||||
@@ -1614,74 +1860,12 @@ if (!(Get-Process whkd -ErrorAction SilentlyContinue))
|
||||
)?;
|
||||
}
|
||||
SubCommand::State => {
|
||||
let home = DATA_DIR.clone();
|
||||
let mut socket = home;
|
||||
socket.push("komorebic.sock");
|
||||
let socket = socket.as_path();
|
||||
|
||||
match std::fs::remove_file(socket) {
|
||||
Ok(_) => {}
|
||||
Err(error) => match error.kind() {
|
||||
// Doing this because ::exists() doesn't work reliably on Windows via IntelliJ
|
||||
ErrorKind::NotFound => {}
|
||||
_ => {
|
||||
return Err(error.into());
|
||||
}
|
||||
},
|
||||
};
|
||||
|
||||
let listener = UnixListener::bind(socket)?;
|
||||
|
||||
send_message(&SocketMessage::State.as_bytes()?)?;
|
||||
|
||||
match listener.accept() {
|
||||
Ok(incoming) => {
|
||||
let stream = BufReader::new(incoming.0);
|
||||
for line in stream.lines() {
|
||||
println!("{}", line?);
|
||||
}
|
||||
|
||||
return Ok(());
|
||||
}
|
||||
Err(error) => {
|
||||
panic!("{}", error);
|
||||
}
|
||||
}
|
||||
with_komorebic_socket(|| send_message(&SocketMessage::State.as_bytes()?))?;
|
||||
}
|
||||
SubCommand::Query(arg) => {
|
||||
let home = DATA_DIR.clone();
|
||||
let mut socket = home;
|
||||
socket.push("komorebic.sock");
|
||||
let socket = socket.as_path();
|
||||
|
||||
match std::fs::remove_file(socket) {
|
||||
Ok(_) => {}
|
||||
Err(error) => match error.kind() {
|
||||
// Doing this because ::exists() doesn't work reliably on Windows via IntelliJ
|
||||
ErrorKind::NotFound => {}
|
||||
_ => {
|
||||
return Err(error.into());
|
||||
}
|
||||
},
|
||||
};
|
||||
|
||||
let listener = UnixListener::bind(socket)?;
|
||||
|
||||
send_message(&SocketMessage::Query(arg.state_query).as_bytes()?)?;
|
||||
|
||||
match listener.accept() {
|
||||
Ok(incoming) => {
|
||||
let stream = BufReader::new(incoming.0);
|
||||
for line in stream.lines() {
|
||||
println!("{}", line?);
|
||||
}
|
||||
|
||||
return Ok(());
|
||||
}
|
||||
Err(error) => {
|
||||
panic!("{}", error);
|
||||
}
|
||||
}
|
||||
with_komorebic_socket(|| {
|
||||
send_message(&SocketMessage::Query(arg.state_query).as_bytes()?)
|
||||
})?;
|
||||
}
|
||||
SubCommand::RestoreWindows => {
|
||||
let hwnd_json = DATA_DIR.join("komorebi.hwnd.json");
|
||||
@@ -1745,9 +1929,7 @@ if (!(Get-Process whkd -ErrorAction SilentlyContinue))
|
||||
match target.identifier {
|
||||
ApplicationIdentifier::Exe => {}
|
||||
_ => {
|
||||
return Err(anyhow!(
|
||||
"this command requires applications to be identified by their exe"
|
||||
));
|
||||
bail!("this command requires applications to be identified by their exe");
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1769,10 +1951,10 @@ if (!(Get-Process whkd -ErrorAction SilentlyContinue))
|
||||
send_message(&SocketMessage::QuickLoad.as_bytes()?)?;
|
||||
}
|
||||
SubCommand::SaveResize(arg) => {
|
||||
send_message(&SocketMessage::Save(resolve_windows_path(&arg.path)?).as_bytes()?)?;
|
||||
send_message(&SocketMessage::Save(resolve_home_path(arg.path)?).as_bytes()?)?;
|
||||
}
|
||||
SubCommand::LoadResize(arg) => {
|
||||
send_message(&SocketMessage::Load(resolve_windows_path(&arg.path)?).as_bytes()?)?;
|
||||
send_message(&SocketMessage::Load(resolve_home_path(arg.path)?).as_bytes()?)?;
|
||||
}
|
||||
SubCommand::Subscribe(arg) => {
|
||||
send_message(&SocketMessage::AddSubscriber(arg.named_pipe).as_bytes()?)?;
|
||||
@@ -1825,10 +2007,9 @@ if (!(Get-Process whkd -ErrorAction SilentlyContinue))
|
||||
)?;
|
||||
}
|
||||
SubCommand::AhkAppSpecificConfiguration(arg) => {
|
||||
let content = std::fs::read_to_string(resolve_windows_path(&arg.path)?)?;
|
||||
let content = std::fs::read_to_string(resolve_home_path(arg.path)?)?;
|
||||
let lines = if let Some(override_path) = arg.override_path {
|
||||
let override_content =
|
||||
std::fs::read_to_string(resolve_windows_path(&override_path)?)?;
|
||||
let override_content = std::fs::read_to_string(resolve_home_path(override_path)?)?;
|
||||
|
||||
ApplicationConfigurationGenerator::generate_ahk(
|
||||
&content,
|
||||
@@ -1838,28 +2019,24 @@ if (!(Get-Process whkd -ErrorAction SilentlyContinue))
|
||||
ApplicationConfigurationGenerator::generate_ahk(&content, None)?
|
||||
};
|
||||
|
||||
let mut generated_config = HOME_DIR.clone();
|
||||
generated_config.push("komorebi.generated.ahk");
|
||||
let generated_config = HOME_DIR.join("komorebi.generated.ahk");
|
||||
let mut file = OpenOptions::new()
|
||||
.write(true)
|
||||
.create(true)
|
||||
.truncate(true)
|
||||
.open(generated_config.clone())?;
|
||||
.open(&generated_config)?;
|
||||
|
||||
file.write_all(lines.join("\n").as_bytes())?;
|
||||
|
||||
println!(
|
||||
"\nApplication-specific generated configuration written to {}",
|
||||
generated_config.to_str().ok_or_else(|| anyhow!(
|
||||
"could not find the path to the generated configuration file"
|
||||
))?
|
||||
generated_config.display()
|
||||
);
|
||||
}
|
||||
SubCommand::PwshAppSpecificConfiguration(arg) => {
|
||||
let content = std::fs::read_to_string(resolve_windows_path(&arg.path)?)?;
|
||||
let content = std::fs::read_to_string(resolve_home_path(arg.path)?)?;
|
||||
let lines = if let Some(override_path) = arg.override_path {
|
||||
let override_content =
|
||||
std::fs::read_to_string(resolve_windows_path(&override_path)?)?;
|
||||
let override_content = std::fs::read_to_string(resolve_home_path(override_path)?)?;
|
||||
|
||||
ApplicationConfigurationGenerator::generate_pwsh(
|
||||
&content,
|
||||
@@ -1869,25 +2046,22 @@ if (!(Get-Process whkd -ErrorAction SilentlyContinue))
|
||||
ApplicationConfigurationGenerator::generate_pwsh(&content, None)?
|
||||
};
|
||||
|
||||
let mut generated_config = HOME_DIR.clone();
|
||||
generated_config.push("komorebi.generated.ps1");
|
||||
let generated_config = HOME_DIR.join("komorebi.generated.ps1");
|
||||
let mut file = OpenOptions::new()
|
||||
.write(true)
|
||||
.create(true)
|
||||
.truncate(true)
|
||||
.open(generated_config.clone())?;
|
||||
.open(&generated_config)?;
|
||||
|
||||
file.write_all(lines.join("\n").as_bytes())?;
|
||||
|
||||
println!(
|
||||
"\nApplication-specific generated configuration written to {}",
|
||||
generated_config.to_str().ok_or_else(|| anyhow!(
|
||||
"could not find the path to the generated configuration file"
|
||||
))?
|
||||
generated_config.display()
|
||||
);
|
||||
}
|
||||
SubCommand::FormatAppSpecificConfiguration(arg) => {
|
||||
let file_path = resolve_windows_path(&arg.path)?;
|
||||
let file_path = resolve_home_path(arg.path)?;
|
||||
let content = std::fs::read_to_string(&file_path)?;
|
||||
let formatted_content = ApplicationConfigurationGenerator::format(&content)?;
|
||||
|
||||
@@ -1905,8 +2079,7 @@ if (!(Get-Process whkd -ErrorAction SilentlyContinue))
|
||||
let content = reqwest::blocking::get("https://raw.githubusercontent.com/LGUG2Z/komorebi-application-specific-configuration/master/applications.yaml")?
|
||||
.text()?;
|
||||
|
||||
let mut output_file = HOME_DIR.clone();
|
||||
output_file.push("applications.yaml");
|
||||
let output_file = HOME_DIR.join("applications.yaml");
|
||||
|
||||
let mut file = OpenOptions::new()
|
||||
.write(true)
|
||||
@@ -1916,189 +2089,31 @@ if (!(Get-Process whkd -ErrorAction SilentlyContinue))
|
||||
|
||||
file.write_all(content.as_bytes())?;
|
||||
|
||||
let output_path = output_file.to_str().unwrap().to_string();
|
||||
let output_path = output_path.replace('\\', "/");
|
||||
|
||||
println!("Latest version of applications.yaml from https://github.com/LGUG2Z/komorebi-application-specific-configuration downloaded\n");
|
||||
println!(
|
||||
"You can add this to your komorebi.json static configuration file like this: \n\n\"app_specific_configuration_path\": \"{output_path}\"",
|
||||
"You can add this to your komorebi.json static configuration file like this: \n\n\"app_specific_configuration_path\": \"{}\"",
|
||||
output_file.display()
|
||||
);
|
||||
}
|
||||
SubCommand::NotificationSchema => {
|
||||
let home = DATA_DIR.clone();
|
||||
let mut socket = home;
|
||||
socket.push("komorebic.sock");
|
||||
let socket = socket.as_path();
|
||||
|
||||
match std::fs::remove_file(socket) {
|
||||
Ok(_) => {}
|
||||
Err(error) => match error.kind() {
|
||||
// Doing this because ::exists() doesn't work reliably on Windows via IntelliJ
|
||||
ErrorKind::NotFound => {}
|
||||
_ => {
|
||||
return Err(error.into());
|
||||
}
|
||||
},
|
||||
};
|
||||
|
||||
send_message(&SocketMessage::NotificationSchema.as_bytes()?)?;
|
||||
|
||||
let listener = UnixListener::bind(socket)?;
|
||||
match listener.accept() {
|
||||
Ok(incoming) => {
|
||||
let stream = BufReader::new(incoming.0);
|
||||
for line in stream.lines() {
|
||||
println!("{}", line?);
|
||||
}
|
||||
|
||||
return Ok(());
|
||||
}
|
||||
Err(error) => {
|
||||
panic!("{}", error);
|
||||
}
|
||||
}
|
||||
with_komorebic_socket(|| send_message(&SocketMessage::NotificationSchema.as_bytes()?))?;
|
||||
}
|
||||
SubCommand::SocketSchema => {
|
||||
let home = DATA_DIR.clone();
|
||||
let mut socket = home;
|
||||
socket.push("komorebic.sock");
|
||||
let socket = socket.as_path();
|
||||
|
||||
match std::fs::remove_file(socket) {
|
||||
Ok(_) => {}
|
||||
Err(error) => match error.kind() {
|
||||
// Doing this because ::exists() doesn't work reliably on Windows via IntelliJ
|
||||
ErrorKind::NotFound => {}
|
||||
_ => {
|
||||
return Err(error.into());
|
||||
}
|
||||
},
|
||||
};
|
||||
|
||||
send_message(&SocketMessage::SocketSchema.as_bytes()?)?;
|
||||
|
||||
let listener = UnixListener::bind(socket)?;
|
||||
match listener.accept() {
|
||||
Ok(incoming) => {
|
||||
let stream = BufReader::new(incoming.0);
|
||||
for line in stream.lines() {
|
||||
println!("{}", line?);
|
||||
}
|
||||
|
||||
return Ok(());
|
||||
}
|
||||
Err(error) => {
|
||||
panic!("{}", error);
|
||||
}
|
||||
}
|
||||
with_komorebic_socket(|| send_message(&SocketMessage::SocketSchema.as_bytes()?))?;
|
||||
}
|
||||
SubCommand::StaticConfigSchema => {
|
||||
let home = DATA_DIR.clone();
|
||||
let mut socket = home;
|
||||
socket.push("komorebic.sock");
|
||||
let socket = socket.as_path();
|
||||
|
||||
match std::fs::remove_file(socket) {
|
||||
Ok(_) => {}
|
||||
Err(error) => match error.kind() {
|
||||
// Doing this because ::exists() doesn't work reliably on Windows via IntelliJ
|
||||
ErrorKind::NotFound => {}
|
||||
_ => {
|
||||
return Err(error.into());
|
||||
}
|
||||
},
|
||||
};
|
||||
|
||||
send_message(&SocketMessage::StaticConfigSchema.as_bytes()?)?;
|
||||
|
||||
let listener = UnixListener::bind(socket)?;
|
||||
match listener.accept() {
|
||||
Ok(incoming) => {
|
||||
let stream = BufReader::new(incoming.0);
|
||||
for line in stream.lines() {
|
||||
println!("{}", line?);
|
||||
}
|
||||
|
||||
return Ok(());
|
||||
}
|
||||
Err(error) => {
|
||||
panic!("{}", error);
|
||||
}
|
||||
}
|
||||
with_komorebic_socket(|| send_message(&SocketMessage::StaticConfigSchema.as_bytes()?))?;
|
||||
}
|
||||
SubCommand::GenerateStaticConfig => {
|
||||
let home = DATA_DIR.clone();
|
||||
let mut socket = home;
|
||||
socket.push("komorebic.sock");
|
||||
let socket = socket.as_path();
|
||||
|
||||
match std::fs::remove_file(socket) {
|
||||
Ok(_) => {}
|
||||
Err(error) => match error.kind() {
|
||||
// Doing this because ::exists() doesn't work reliably on Windows via IntelliJ
|
||||
ErrorKind::NotFound => {}
|
||||
_ => {
|
||||
return Err(error.into());
|
||||
}
|
||||
},
|
||||
};
|
||||
|
||||
send_message(&SocketMessage::GenerateStaticConfig.as_bytes()?)?;
|
||||
|
||||
let listener = UnixListener::bind(socket)?;
|
||||
match listener.accept() {
|
||||
Ok(incoming) => {
|
||||
let stream = BufReader::new(incoming.0);
|
||||
for line in stream.lines() {
|
||||
println!("{}", line?);
|
||||
}
|
||||
|
||||
return Ok(());
|
||||
}
|
||||
Err(error) => {
|
||||
panic!("{}", error);
|
||||
}
|
||||
}
|
||||
with_komorebic_socket(|| {
|
||||
send_message(&SocketMessage::GenerateStaticConfig.as_bytes()?)
|
||||
})?;
|
||||
}
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn resolve_windows_path(raw_path: &str) -> Result<PathBuf> {
|
||||
let path = if raw_path.starts_with('~') {
|
||||
raw_path.replacen(
|
||||
'~',
|
||||
&dirs::home_dir()
|
||||
.ok_or_else(|| anyhow!("there is no home directory"))?
|
||||
.display()
|
||||
.to_string(),
|
||||
1,
|
||||
)
|
||||
} else {
|
||||
raw_path.to_string()
|
||||
};
|
||||
|
||||
let full_path = PathBuf::from(path);
|
||||
|
||||
let parent = full_path
|
||||
.parent()
|
||||
.ok_or_else(|| anyhow!("cannot parse directory"))?;
|
||||
|
||||
Ok(if parent.is_dir() {
|
||||
let file = full_path
|
||||
.components()
|
||||
.last()
|
||||
.ok_or_else(|| anyhow!("cannot parse filename"))?;
|
||||
|
||||
let mut canonicalized = std::fs::canonicalize(parent)?;
|
||||
canonicalized.push(file);
|
||||
canonicalized
|
||||
} else {
|
||||
full_path
|
||||
})
|
||||
}
|
||||
|
||||
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
|
||||
|
||||
53
schema.json
53
schema.json
@@ -21,6 +21,22 @@
|
||||
}
|
||||
]
|
||||
},
|
||||
"active_window_border_offset": {
|
||||
"description": "Offset of the active window border (default: None)",
|
||||
"type": [
|
||||
"integer",
|
||||
"null"
|
||||
],
|
||||
"format": "int32"
|
||||
},
|
||||
"active_window_border_width": {
|
||||
"description": "Width of the active window border (default: 20)",
|
||||
"type": [
|
||||
"integer",
|
||||
"null"
|
||||
],
|
||||
"format": "int32"
|
||||
},
|
||||
"alt_focus_hack": {
|
||||
"description": "Always send the ALT key when using focus commands (default: false)",
|
||||
"type": [
|
||||
@@ -36,7 +52,7 @@
|
||||
]
|
||||
},
|
||||
"border_offset": {
|
||||
"description": "Offset of the active window border (default: None)",
|
||||
"description": "DEPRECATED from v0.1.19: use active_window_border_offset instead",
|
||||
"anyOf": [
|
||||
{
|
||||
"$ref": "#/definitions/Rect"
|
||||
@@ -57,7 +73,7 @@
|
||||
}
|
||||
},
|
||||
"border_width": {
|
||||
"description": "Width of the active window border (default: 20)",
|
||||
"description": "DEPRECATED from v0.1.19: use active_window_border_width instead",
|
||||
"type": [
|
||||
"integer",
|
||||
"null"
|
||||
@@ -154,6 +170,16 @@
|
||||
"$ref": "#/definitions/IdWithIdentifier"
|
||||
}
|
||||
},
|
||||
"monitor_index_preferences": {
|
||||
"description": "Set monitor index preferences",
|
||||
"type": [
|
||||
"object",
|
||||
"null"
|
||||
],
|
||||
"additionalProperties": {
|
||||
"$ref": "#/definitions/Rect"
|
||||
}
|
||||
},
|
||||
"monitors": {
|
||||
"description": "Monitor and workspace configurations",
|
||||
"type": [
|
||||
@@ -342,9 +368,30 @@
|
||||
},
|
||||
"kind": {
|
||||
"$ref": "#/definitions/ApplicationIdentifier"
|
||||
},
|
||||
"matching_strategy": {
|
||||
"anyOf": [
|
||||
{
|
||||
"$ref": "#/definitions/MatchingStrategy"
|
||||
},
|
||||
{
|
||||
"type": "null"
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
},
|
||||
"MatchingStrategy": {
|
||||
"type": "string",
|
||||
"enum": [
|
||||
"Legacy",
|
||||
"Equals",
|
||||
"StartsWith",
|
||||
"EndsWith",
|
||||
"Contains",
|
||||
"Regex"
|
||||
]
|
||||
},
|
||||
"MonitorConfig": {
|
||||
"type": "object",
|
||||
"required": [
|
||||
@@ -571,4 +618,4 @@
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user