mirror of
https://github.com/LGUG2Z/komorebi.git
synced 2026-01-20 06:18:04 +01:00
Compare commits
58 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
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')
|
||||
|
||||
|
||||
770
Cargo.lock
generated
770
Cargo.lock
generated
File diff suppressed because it is too large
Load Diff
@@ -1,5 +1,6 @@
|
||||
[workspace]
|
||||
|
||||
resolver = "2"
|
||||
members = [
|
||||
"derive-ahk",
|
||||
"komorebi",
|
||||
@@ -8,11 +9,11 @@ members = [
|
||||
]
|
||||
|
||||
[workspace.dependencies]
|
||||
windows-interface = { version = "0.48" }
|
||||
windows-implement = { version = "0.48" }
|
||||
windows-interface = { version = "0.51" }
|
||||
windows-implement = { version = "0.51" }
|
||||
|
||||
[workspace.dependencies.windows]
|
||||
version = "0.48"
|
||||
version = "0.51"
|
||||
features = [
|
||||
"implement",
|
||||
"Win32_System_Com",
|
||||
|
||||
37
README.md
37
README.md
@@ -3,6 +3,9 @@
|
||||
Tiling Window Management for Windows.
|
||||
|
||||
<p>
|
||||
<a href="https://bdsmovement.net">
|
||||
<img alt="Stand With Palestine" src="https://img.shields.io/badge/Stand_With_Palestine-%F0%9F%87%B5%F0%9F%87%B8-white">
|
||||
</a>
|
||||
<img alt="GitHub Workflow Status" src="https://img.shields.io/github/actions/workflow/status/LGUG2Z/komorebi/.github/workflows/windows.yaml">
|
||||
<img alt="GitHub" src="https://img.shields.io/github/license/LGUG2Z/komorebi">
|
||||
<img alt="GitHub all releases" src="https://img.shields.io/github/downloads/LGUG2Z/komorebi/total">
|
||||
@@ -16,11 +19,8 @@ Tiling Window Management for Windows.
|
||||
<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">
|
||||
<img alt="Twitter Follow" src="https://img.shields.io/twitter/follow/LGUG2Z?style=flat">
|
||||
</a>
|
||||
</p>
|
||||
|
||||
@@ -160,6 +160,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 +187,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 +223,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 +279,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 +300,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,6 +1,6 @@
|
||||
[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
|
||||
|
||||
@@ -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)]
|
||||
|
||||
@@ -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,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -77,6 +77,7 @@ pub enum SocketMessage {
|
||||
AdjustContainerPadding(Sizing, i32),
|
||||
AdjustWorkspacePadding(Sizing, i32),
|
||||
ChangeLayout(DefaultLayout),
|
||||
CycleLayout(CycleDirection),
|
||||
ChangeLayoutCustom(PathBuf),
|
||||
FlipLayout(Axis),
|
||||
// Monitor and Workspace Commands
|
||||
@@ -192,7 +193,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 {
|
||||
|
||||
@@ -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"]
|
||||
@@ -29,6 +29,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,9 +39,9 @@ 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.51"
|
||||
windows-interface = { workspace = true }
|
||||
windows-implement = { workspace = true }
|
||||
windows = { workspace = true }
|
||||
|
||||
@@ -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;
|
||||
@@ -27,6 +32,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 +46,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 +90,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(),
|
||||
@@ -313,7 +369,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,7 +408,7 @@ 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(_) => {
|
||||
tracing::debug!("pushed notification to subscriber: {}", subscriber);
|
||||
|
||||
@@ -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 {
|
||||
@@ -423,6 +448,7 @@ 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())?;
|
||||
}
|
||||
@@ -891,28 +917,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 => {
|
||||
@@ -1182,6 +1255,7 @@ impl WindowManager {
|
||||
|
||||
match message {
|
||||
SocketMessage::ChangeLayout(_)
|
||||
| SocketMessage::CycleLayout(_)
|
||||
| SocketMessage::ChangeLayoutCustom(_)
|
||||
| SocketMessage::FlipLayout(_)
|
||||
| SocketMessage::ManageFocusedWindow
|
||||
@@ -1341,7 +1415,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;
|
||||
}
|
||||
|
||||
@@ -22,7 +22,9 @@ 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;
|
||||
@@ -33,6 +35,7 @@ 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::ApplicationIdentifier;
|
||||
use komorebi_core::DefaultLayout;
|
||||
use komorebi_core::FocusFollowsMouseImplementation;
|
||||
@@ -44,6 +47,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 +71,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 +142,7 @@ impl From<&Workspace> for WorkspaceConfig {
|
||||
let rule = IdWithIdentifier {
|
||||
kind: ApplicationIdentifier::Exe,
|
||||
id: identifier.clone(),
|
||||
matching_strategy: None,
|
||||
};
|
||||
|
||||
if *is_initial {
|
||||
@@ -239,12 +254,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 +308,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 +373,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 +406,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 +430,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 +460,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,56 +506,111 @@ 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);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -500,11 +625,25 @@ impl StaticConfig {
|
||||
let content = std::fs::read_to_string(stringified)?;
|
||||
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 +651,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,7 +756,7 @@ 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");
|
||||
@@ -674,7 +876,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()?;
|
||||
|
||||
|
||||
@@ -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
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -17,6 +17,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 +51,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 +94,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 +129,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(),
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1701,6 +1705,29 @@ impl WindowManager {
|
||||
self.update_focused_workspace(self.mouse_follows_focus)
|
||||
}
|
||||
|
||||
#[tracing::instrument(skip(self))]
|
||||
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(&mut self, path: PathBuf) -> Result<()> {
|
||||
tracing::info!("changing layout");
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -234,9 +234,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 +273,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 +309,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 +349,6 @@ impl WindowsApi {
|
||||
SET_WINDOW_POS_FLAGS(flags),
|
||||
)
|
||||
}
|
||||
.ok()
|
||||
.process()
|
||||
}
|
||||
|
||||
@@ -368,9 +363,7 @@ 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<()> {
|
||||
@@ -436,18 +429,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 +482,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 +558,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 +573,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 +665,6 @@ impl WindowsApi {
|
||||
|
||||
pub fn set_process_dpi_awareness_context() -> Result<()> {
|
||||
unsafe { SetProcessDpiAwarenessContext(DPI_AWARENESS_CONTEXT_PER_MONITOR_AWARE_V2) }
|
||||
.ok()
|
||||
.process()
|
||||
}
|
||||
|
||||
@@ -685,7 +676,6 @@ impl WindowsApi {
|
||||
update_flags: SYSTEM_PARAMETERS_INFO_UPDATE_FLAGS,
|
||||
) -> Result<()> {
|
||||
unsafe { SystemParametersInfoW(action, ui_param, Option::from(pv_param), update_flags) }
|
||||
.ok()
|
||||
.process()
|
||||
}
|
||||
|
||||
@@ -777,19 +767,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> {
|
||||
|
||||
@@ -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())?;
|
||||
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 {
|
||||
@@ -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"]
|
||||
@@ -28,5 +28,5 @@ serde_json = "1"
|
||||
serde_yaml = "0.9"
|
||||
sysinfo = "0.29"
|
||||
uds_windows = "1"
|
||||
which = "4"
|
||||
which = "5"
|
||||
windows = { workspace = true }
|
||||
|
||||
@@ -125,6 +125,7 @@ gen_enum_subcommand_args! {
|
||||
CycleStack: CycleDirection,
|
||||
FlipLayout: Axis,
|
||||
ChangeLayout: DefaultLayout,
|
||||
CycleLayout: CycleDirection,
|
||||
WatchConfiguration: BooleanState,
|
||||
MouseFollowsFocus: BooleanState,
|
||||
Query: StateQuery,
|
||||
@@ -607,6 +608,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,6 +625,16 @@ 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)]
|
||||
@@ -683,6 +695,19 @@ struct AltFocusHack {
|
||||
boolean_state: BooleanState,
|
||||
}
|
||||
|
||||
#[derive(Parser, AhkFunction)]
|
||||
struct EnableAutostart {
|
||||
/// Path to a static configuration JSON file
|
||||
#[clap(action, short, long)]
|
||||
config: String,
|
||||
/// 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 {
|
||||
@@ -695,7 +720,7 @@ enum SubCommand {
|
||||
/// 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
|
||||
@@ -841,6 +866,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 +1064,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,11 +1077,61 @@ pub fn send_message(bytes: &[u8]) -> Result<()> {
|
||||
Ok(stream.write_all(bytes)?)
|
||||
}
|
||||
|
||||
fn startup_dir() -> Result<PathBuf> {
|
||||
let home_dir = dirs::home_dir().expect("unable to obtain user's home folder");
|
||||
let app_data = home_dir.join("AppData");
|
||||
let roaming = app_data.join("Roaming");
|
||||
let microsoft = roaming.join("Microsoft");
|
||||
let windows = microsoft.join("Windows");
|
||||
let start_menu = windows.join("Start Menu");
|
||||
let programs = start_menu.join("Programs");
|
||||
let startup = 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::EnableAutostart(args) => {
|
||||
let mut current_exe = std::env::current_exe().expect("unable to get exec path");
|
||||
current_exe.pop();
|
||||
|
||||
let komorebic_exe = current_exe.join("komorebic.exe");
|
||||
|
||||
let startup_dir = startup_dir()?;
|
||||
let shortcut_file = startup_dir.join("komorebi.lnk");
|
||||
|
||||
let mut arguments = format!("start --config {}", args.config);
|
||||
|
||||
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.to_string_lossy().to_string())
|
||||
.env("TARGET_PATH", komorebic_exe.to_string_lossy().to_string())
|
||||
.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();
|
||||
@@ -1391,10 +1474,22 @@ 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"));
|
||||
}
|
||||
|
||||
if arg.ahk && which(&ahk).is_err() {
|
||||
return Err(anyhow!("could not find autohotkey, please make sure it is installed before using the --ahk flag"));
|
||||
}
|
||||
|
||||
let mut buf: PathBuf;
|
||||
|
||||
// The komorebi.ps1 shim will only exist in the Path if installed by Scoop
|
||||
@@ -1426,10 +1521,10 @@ fn main() -> Result<()> {
|
||||
}
|
||||
|
||||
flags.push(format!(
|
||||
"'--config={}'",
|
||||
"'--config=\"{}\"'",
|
||||
path.as_os_str()
|
||||
.to_string_lossy()
|
||||
.trim_start_matches(r#"\\?\"#),
|
||||
.trim_start_matches(r"\\?\"),
|
||||
));
|
||||
}
|
||||
|
||||
@@ -1445,8 +1540,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")
|
||||
@@ -1495,8 +1595,44 @@ if (!(Get-Process whkd -ErrorAction SilentlyContinue))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if arg.ahk {
|
||||
let home = HOME_DIR.clone();
|
||||
let mut config_ahk = home;
|
||||
config_ahk.push("komorebi.ahk");
|
||||
|
||||
let script = format!(
|
||||
r#"
|
||||
Start-Process {ahk} {config} -WindowStyle hidden
|
||||
"#,
|
||||
config = config_ahk.as_os_str().to_string_lossy()
|
||||
);
|
||||
|
||||
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,6 +1682,9 @@ 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()?,
|
||||
|
||||
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