mirror of
https://github.com/LGUG2Z/komorebi.git
synced 2026-02-10 15:27:42 +01:00
Compare commits
15 Commits
v0.1.6
...
feature/re
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
987dc2b8dd | ||
|
|
29a6c39084 | ||
|
|
5d0806a8c9 | ||
|
|
6c53fd7830 | ||
|
|
6ae59671a2 | ||
|
|
f17bfe267e | ||
|
|
840af215a0 | ||
|
|
6981d778a9 | ||
|
|
5d6351f48d | ||
|
|
ac0f33f7ed | ||
|
|
f19bd3032b | ||
|
|
3f3c2815da | ||
|
|
7070878f4a | ||
|
|
d3cb9e07f7 | ||
|
|
6f6181625f |
46
Cargo.lock
generated
46
Cargo.lock
generated
@@ -286,6 +286,12 @@ dependencies = [
|
||||
"winapi 0.3.9",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "dtoa"
|
||||
version = "0.4.8"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "56899898ce76aaf4a0f24d914c97ea6ed976d42fec6ad33fcbb0a1103e07b2b0"
|
||||
|
||||
[[package]]
|
||||
name = "either"
|
||||
version = "1.6.1"
|
||||
@@ -519,6 +525,7 @@ dependencies = [
|
||||
"hotwatch",
|
||||
"komorebi-core",
|
||||
"lazy_static",
|
||||
"miow 0.3.7",
|
||||
"nanoid",
|
||||
"parking_lot",
|
||||
"paste",
|
||||
@@ -544,6 +551,7 @@ dependencies = [
|
||||
"color-eyre",
|
||||
"serde",
|
||||
"serde_json",
|
||||
"serde_yaml",
|
||||
"strum",
|
||||
]
|
||||
|
||||
@@ -584,6 +592,12 @@ version = "0.2.103"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "dd8f7255a17a627354f321ef0055d63b898c6fb27eff628af4d1b66b7331edf6"
|
||||
|
||||
[[package]]
|
||||
name = "linked-hash-map"
|
||||
version = "0.5.4"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "7fb9b38af92608140b86b693604b9ffcc5824240a484d1ecd4795bacb2fe88f3"
|
||||
|
||||
[[package]]
|
||||
name = "lock_api"
|
||||
version = "0.4.5"
|
||||
@@ -649,7 +663,7 @@ dependencies = [
|
||||
"kernel32-sys",
|
||||
"libc",
|
||||
"log",
|
||||
"miow",
|
||||
"miow 0.2.2",
|
||||
"net2",
|
||||
"slab",
|
||||
"winapi 0.2.8",
|
||||
@@ -679,6 +693,15 @@ dependencies = [
|
||||
"ws2_32-sys",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "miow"
|
||||
version = "0.3.7"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "b9f1c5b025cda876f66ef43a113f91ebc9f4ccef34843000e0adf6ebbab84e21"
|
||||
dependencies = [
|
||||
"winapi 0.3.9",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "nanoid"
|
||||
version = "0.4.0"
|
||||
@@ -1111,6 +1134,18 @@ dependencies = [
|
||||
"serde",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "serde_yaml"
|
||||
version = "0.8.21"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "d8c608a35705a5d3cdc9fbe403147647ff34b921f8e833e49306df898f9b20af"
|
||||
dependencies = [
|
||||
"dtoa",
|
||||
"indexmap",
|
||||
"serde",
|
||||
"yaml-rust",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "sharded-slab"
|
||||
version = "0.1.4"
|
||||
@@ -1525,3 +1560,12 @@ dependencies = [
|
||||
"winapi 0.2.8",
|
||||
"winapi-build",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "yaml-rust"
|
||||
version = "0.4.5"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "56c1936c4cc7a1c9ab21a1ebb602eb942ba868cbd44a99cb7cdc5892335e1c85"
|
||||
dependencies = [
|
||||
"linked-hash-map",
|
||||
]
|
||||
|
||||
95
README.md
95
README.md
@@ -208,6 +208,59 @@ dimensions and are not coupled to the applications that were running at the time
|
||||
When layouts that expect more or less windows than the number currently on the focused workspace are loaded, `komorebi`
|
||||
will automatically reconcile the difference.
|
||||
|
||||
#### Creating and Loading Custom Layouts
|
||||
|
||||
Particularly for users of ultrawide monitors, traditional tiling layouts may not seem like the most efficient use of
|
||||
screen space. If you feel this is the case with any of the default layouts, you are also welcome to create your own
|
||||
custom layouts and save them as JSON or YAML.
|
||||
|
||||
If you're not comfortable writing the layouts directly in JSON or YAML, you can use
|
||||
the [komorebi Custom Layout Generator](https://lgug2z.github.io/komorebi-custom-layout-generator/) to interactively
|
||||
define a custom layout, and then copy the generated JSON content.
|
||||
|
||||
Custom layouts can be loaded on the current workspace or configured for a specific workspace with the following
|
||||
commands:
|
||||
|
||||
```powershell
|
||||
komorebic.exe load-custom-layout ~/custom.yaml
|
||||
komorebic.exe workspace-custom-layout 0 0 ~/custom.yaml
|
||||
```
|
||||
|
||||
The fundamental building block of a custom _komorebi_ layout is the Column.
|
||||
|
||||
Columns come in three variants:
|
||||
|
||||
- **Primary**: This is where your primary focus will be on the screen most of the time. There must be exactly one Primary
|
||||
Column in any custom layout. Optionally, you can specify the percentage of the screen width that you want the Primary
|
||||
Column to occupy.
|
||||
- **Secondary**: This is an optional column that can either be full height of split horizontally into a fixed number of
|
||||
maximum rows. There can be any number of Secondary Columns in a custom layout.
|
||||
- **Tertiary**: This is the final column where any remaining windows will be split horizontally into rows as they get added.
|
||||
|
||||
If there is only one window on the screen when a custom layout is selected, that window will take up the full work area
|
||||
of the screen.
|
||||
|
||||
If the number of windows is equal to or less than the total number of columns defined in a custom layout, the windows
|
||||
will be arranged in an equal-width columns.
|
||||
|
||||
When the number of windows is greater than the number of columns defined in the custom layout, the windows will begin to
|
||||
be arranged according to the constraints set on the Primary and Secondary columns of the layout.
|
||||
|
||||
Here is an example custom layout that can be used as a starting point for your own:
|
||||
|
||||
YAML
|
||||
|
||||
```yaml
|
||||
- column: Secondary
|
||||
configuration:
|
||||
Horizontal: 2 # max number of rows,
|
||||
- column: Primary
|
||||
configuration:
|
||||
WidthPercentage: 45 # percentage of screen
|
||||
- column: Tertiary
|
||||
configuration: Horizontal
|
||||
```
|
||||
|
||||
## Configuration with `komorebic`
|
||||
|
||||
As previously mentioned, this project does not handle anything related to keybindings and shortcuts directly. I
|
||||
@@ -223,6 +276,8 @@ start Start komorebi.exe as a background process
|
||||
stop Stop the komorebi.exe process and restore all hidden windows
|
||||
state Show a JSON representation of the current window manager state
|
||||
query Query the current window manager state
|
||||
subscribe Subscribe to komorebi events
|
||||
unsubscribe Unsubscribe from komorebi events
|
||||
log Tail komorebi.exe's process logs (cancel with Ctrl-C)
|
||||
quick-save Quicksave the current resize layout dimensions
|
||||
quick-load Load the last quicksaved resize layout dimensions
|
||||
@@ -250,6 +305,7 @@ work-area-offset Set offsets to exclude parts of the work area from
|
||||
adjust-container-padding Adjust container padding on the focused workspace
|
||||
adjust-workspace-padding Adjust workspace padding on the focused workspace
|
||||
change-layout Set the layout on the focused workspace
|
||||
load-custom-layout Load a custom layout from file for the focused workspace
|
||||
flip-layout Flip the layout on the focused workspace (BSP only)
|
||||
promote Promote the focused window to the top of the tree
|
||||
retile Force the retiling of all managed windows
|
||||
@@ -257,6 +313,7 @@ ensure-workspaces Create at least this many workspaces for the speci
|
||||
container-padding Set the container padding for the specified workspace
|
||||
workspace-padding Set the workspace padding for the specified workspace
|
||||
workspace-layout Set the layout for the specified workspace
|
||||
workspace-custom-layout Set a custom layout for the specified workspace
|
||||
workspace-tiling Enable or disable window tiling for the specified workspace
|
||||
workspace-name Set the workspace name for the specified workspace
|
||||
toggle-pause Toggle the window manager on and off across all monitors
|
||||
@@ -317,6 +374,7 @@ used [is available here](komorebi.sample.with.lib.ahk).
|
||||
- [x] Main half-height window with vertical stack layout (`horizontal-stack`)
|
||||
- [x] Main half-width window with horizontal stack layout (`vertical-stack`)
|
||||
- [x] 2x Main window (half and quarter-width) with horizontal stack layout (`ultrawide-vertical-stack`)
|
||||
- [x] Load custom layouts from JSON and YAML representations
|
||||
- [x] Floating rules based on exe name, window title and class
|
||||
- [x] Workspace rules based on exe name and window class
|
||||
- [x] Additional manage rules based on exe name and window class
|
||||
@@ -337,6 +395,7 @@ used [is available here](komorebi.sample.with.lib.ahk).
|
||||
- [x] Helper library for AutoHotKey
|
||||
- [x] View window manager state
|
||||
- [x] Query window manager state
|
||||
- [x] Subscribe to event and message notifications
|
||||
|
||||
## Development
|
||||
|
||||
@@ -394,3 +453,39 @@ representation of the `State` struct, which includes the current state of `Windo
|
||||
|
||||
This may also be polled to build further integrations and widgets on top of (if you ever wanted to build something
|
||||
like [Stackline](https://github.com/AdamWagner/stackline) for Windows, you could do it by polling this command).
|
||||
|
||||
## Window Manager Event Subscriptions
|
||||
|
||||
It is also possible to subscribe to notifications of every `WindowManagerEvent` and `SocketMessage` handled
|
||||
by `komorebi` using [Named Pipes](https://docs.microsoft.com/en-us/windows/win32/ipc/named-pipes).
|
||||
|
||||
First, your application must create a named pipe. Once the named pipe has been created, run the following command:
|
||||
|
||||
```powershell
|
||||
komorebic.exe subscribe <your pipe name>
|
||||
```
|
||||
|
||||
Note that you do not have to include the full path of the named pipe, just the name.
|
||||
|
||||
If the named pipe exists, `komorebi` will start pushing JSON data of successfully handled events and messages:
|
||||
|
||||
```json lines
|
||||
{"event":{"type":"AddSubscriber","content":"yasb"},"state":{...}}
|
||||
{"event":{"type":"FocusWindow","content":"Left"},"state":{...}}
|
||||
{"event":{"type":"FocusChange","content":["SystemForeground",{"hwnd":131444,"title":"komorebi – README.md","exe":"idea64.exe","class":"SunAwtFrame","rect":{"left":13,"top":60,"right":1520,"bottom":1655}}]},"state":{...}}
|
||||
{"event":{"type":"MonitorPoll","content":["ObjectCreate",{"hwnd":5572450,"title":"OLEChannelWnd","exe":"explorer.exe","class":"OleMainThreadWndClass","rect":{"left":0,"top":0,"right":0,"bottom":0}}]},"state":{...}}
|
||||
{"event":{"type":"FocusWindow","content":"Right"},"state":{...}}
|
||||
{"event":{"type":"FocusChange","content":["SystemForeground",{"hwnd":132968,"title":"Windows PowerShell","exe":"WindowsTerminal.exe","class":"CASCADIA_HOSTING_WINDOW_CLASS","rect":{"left":1539,"top":60,"right":1520,"bottom":821}}]},"state":{}...}
|
||||
{"event":{"type":"FocusWindow","content":"Down"},"state":{...}}
|
||||
{"event":{"type":"FocusChange","content":["SystemForeground",{"hwnd":329264,"title":"den — Mozilla Firefox","exe":"firefox.exe","class":"MozillaWindowClass","rect":{"left":1539,"top":894,"right":1520,"bottom":821}}]},"state":{...}}
|
||||
{"event":{"type":"FocusWindow","content":"Up"},"state":{...}}
|
||||
{"event":{"type":"FocusChange","content":["SystemForeground",{"hwnd":132968,"title":"Windows PowerShell","exe":"WindowsTerminal.exe","class":"CASCADIA_HOSTING_WINDOW_CLASS","rect":{"left":1539,"top":60,"right":1520,"bottom":821}}]},"state":{...}}
|
||||
```
|
||||
|
||||
You may then filter on the `type` key to listen to the events that you are interested in. For a full list of possible
|
||||
notification types, refer to the enum variants of `WindowManagerEvent` in `komorebi` and `SocketMessage`
|
||||
in `komorebi-core`.
|
||||
|
||||
An example of how to create a named pipe and a subscription to `komorebi`'s handled events in Python
|
||||
by [@denBot](https://github.com/denBot) can be
|
||||
found [here](https://gist.github.com/denBot/4136279812f87819f86d99eba77c1ee0).
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
name = "bindings"
|
||||
version = "0.1.0"
|
||||
authors = ["Jade Iqbal"]
|
||||
edition = "2018"
|
||||
edition = "2021"
|
||||
|
||||
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
|
||||
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
[package]
|
||||
name = "derive-ahk"
|
||||
version = "0.1.0"
|
||||
edition = "2018"
|
||||
edition = "2021"
|
||||
|
||||
[lib]
|
||||
proc-macro = true
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
[package]
|
||||
name = "komorebi-core"
|
||||
version = "0.1.6"
|
||||
edition = "2018"
|
||||
edition = "2021"
|
||||
|
||||
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
|
||||
|
||||
@@ -12,4 +12,5 @@ clap = "3.0.0-beta.4"
|
||||
color-eyre = "0.5"
|
||||
serde = { version = "1", features = ["derive"] }
|
||||
serde_json = "1"
|
||||
serde_yaml = "0.8"
|
||||
strum = { version = "0.21", features = ["derive"] }
|
||||
|
||||
584
komorebi-core/src/arrangement.rs
Normal file
584
komorebi-core/src/arrangement.rs
Normal file
@@ -0,0 +1,584 @@
|
||||
use std::num::NonZeroUsize;
|
||||
|
||||
use clap::ArgEnum;
|
||||
use serde::Deserialize;
|
||||
use serde::Serialize;
|
||||
use strum::Display;
|
||||
use strum::EnumString;
|
||||
|
||||
use crate::custom_layout::Column;
|
||||
use crate::custom_layout::ColumnSplit;
|
||||
use crate::custom_layout::ColumnSplitWithCapacity;
|
||||
use crate::CustomLayout;
|
||||
use crate::DefaultLayout;
|
||||
use crate::Rect;
|
||||
|
||||
pub trait Arrangement {
|
||||
fn calculate(
|
||||
&self,
|
||||
area: &Rect,
|
||||
len: NonZeroUsize,
|
||||
container_padding: Option<i32>,
|
||||
layout_flip: Option<Flip>,
|
||||
resize_dimensions: &[Option<Rect>],
|
||||
) -> Vec<Rect>;
|
||||
}
|
||||
|
||||
impl Arrangement for DefaultLayout {
|
||||
#[allow(clippy::too_many_lines)]
|
||||
fn calculate(
|
||||
&self,
|
||||
area: &Rect,
|
||||
len: NonZeroUsize,
|
||||
container_padding: Option<i32>,
|
||||
layout_flip: Option<Flip>,
|
||||
resize_dimensions: &[Option<Rect>],
|
||||
) -> Vec<Rect> {
|
||||
let len = usize::from(len);
|
||||
let mut dimensions = match self {
|
||||
DefaultLayout::BSP => recursive_fibonacci(
|
||||
0,
|
||||
len,
|
||||
area,
|
||||
layout_flip,
|
||||
calculate_resize_adjustments(resize_dimensions),
|
||||
),
|
||||
DefaultLayout::Columns => columns(area, len),
|
||||
DefaultLayout::Rows => rows(area, len),
|
||||
DefaultLayout::VerticalStack => {
|
||||
let mut layouts: Vec<Rect> = vec![];
|
||||
|
||||
let primary_right = match len {
|
||||
1 => area.right,
|
||||
_ => area.right / 2,
|
||||
};
|
||||
|
||||
let mut main_left = area.left;
|
||||
let mut stack_left = area.left + primary_right;
|
||||
|
||||
match layout_flip {
|
||||
Some(Flip::Horizontal | Flip::HorizontalAndVertical) if len > 1 => {
|
||||
main_left = main_left + area.right - primary_right;
|
||||
stack_left = area.left;
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
|
||||
if len >= 1 {
|
||||
layouts.push(Rect {
|
||||
left: main_left,
|
||||
top: area.top,
|
||||
right: primary_right,
|
||||
bottom: area.bottom,
|
||||
});
|
||||
|
||||
if len > 1 {
|
||||
layouts.append(&mut rows(
|
||||
&Rect {
|
||||
left: stack_left,
|
||||
top: area.top,
|
||||
right: area.right - primary_right,
|
||||
bottom: area.bottom,
|
||||
},
|
||||
len - 1,
|
||||
));
|
||||
}
|
||||
}
|
||||
|
||||
layouts
|
||||
}
|
||||
DefaultLayout::HorizontalStack => {
|
||||
let mut layouts: Vec<Rect> = vec![];
|
||||
|
||||
let bottom = match len {
|
||||
1 => area.bottom,
|
||||
_ => area.bottom / 2,
|
||||
};
|
||||
|
||||
let mut main_top = area.top;
|
||||
let mut stack_top = area.top + bottom;
|
||||
|
||||
match layout_flip {
|
||||
Some(Flip::Vertical | Flip::HorizontalAndVertical) if len > 1 => {
|
||||
main_top = main_top + area.bottom - bottom;
|
||||
stack_top = area.top;
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
|
||||
if len >= 1 {
|
||||
layouts.push(Rect {
|
||||
left: area.left,
|
||||
top: main_top,
|
||||
right: area.right,
|
||||
bottom,
|
||||
});
|
||||
|
||||
if len > 1 {
|
||||
layouts.append(&mut columns(
|
||||
&Rect {
|
||||
left: area.left,
|
||||
top: stack_top,
|
||||
right: area.right,
|
||||
bottom: area.bottom - bottom,
|
||||
},
|
||||
len - 1,
|
||||
));
|
||||
}
|
||||
}
|
||||
|
||||
layouts
|
||||
}
|
||||
DefaultLayout::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(Flip::Horizontal | Flip::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(Flip::Horizontal | Flip::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
|
||||
}
|
||||
};
|
||||
|
||||
dimensions
|
||||
.iter_mut()
|
||||
.for_each(|l| l.add_padding(container_padding));
|
||||
|
||||
dimensions
|
||||
}
|
||||
}
|
||||
|
||||
impl Arrangement for CustomLayout {
|
||||
fn calculate(
|
||||
&self,
|
||||
area: &Rect,
|
||||
len: NonZeroUsize,
|
||||
container_padding: Option<i32>,
|
||||
_layout_flip: Option<Flip>,
|
||||
_resize_dimensions: &[Option<Rect>],
|
||||
) -> Vec<Rect> {
|
||||
let mut dimensions = vec![];
|
||||
let container_count = len.get();
|
||||
|
||||
if container_count <= self.len() {
|
||||
let mut layouts = columns(area, container_count);
|
||||
dimensions.append(&mut layouts);
|
||||
} else {
|
||||
let count_map = self.column_container_counts();
|
||||
|
||||
// If there are not enough windows to trigger the final tertiary
|
||||
// column in the custom layout, use an offset to reduce the number of
|
||||
// columns to calculate each column's area by, so that we don't have
|
||||
// an empty ghost tertiary column and the screen space can be maximised
|
||||
// until there are enough windows to create it
|
||||
let mut tertiary_trigger_threshold = 0;
|
||||
|
||||
// always -1 because we don't insert the tertiary column in the count_map
|
||||
for i in 0..self.len() - 1 {
|
||||
tertiary_trigger_threshold += count_map.get(&i).unwrap();
|
||||
}
|
||||
|
||||
let enable_tertiary_column = len.get() > tertiary_trigger_threshold;
|
||||
|
||||
let offset = if enable_tertiary_column {
|
||||
None
|
||||
} else {
|
||||
Option::from(1)
|
||||
};
|
||||
|
||||
#[allow(clippy::cast_possible_truncation, clippy::cast_possible_wrap)]
|
||||
let primary_right = self.primary_width_percentage().map_or_else(
|
||||
|| area.right / self.len() as i32,
|
||||
|percentage| (area.right / 100) * percentage as i32,
|
||||
);
|
||||
|
||||
for (idx, column) in self.iter().enumerate() {
|
||||
// If we are offsetting a tertiary column for which the threshold
|
||||
// has not yet been met, this loop should not run for that final
|
||||
// tertiary column
|
||||
if idx < self.len() - offset.unwrap_or(0) {
|
||||
let column_area = if idx == 0 {
|
||||
Self::column_area_with_last(self.len(), area, primary_right, None, offset)
|
||||
} else {
|
||||
Self::column_area_with_last(
|
||||
self.len(),
|
||||
area,
|
||||
primary_right,
|
||||
Option::from(dimensions[self.first_container_idx(idx - 1)]),
|
||||
offset,
|
||||
)
|
||||
};
|
||||
|
||||
match column {
|
||||
Column::Primary(Option::Some(_)) => {
|
||||
let main_column_area = if idx == 0 {
|
||||
Self::main_column_area(area, primary_right, None)
|
||||
} else {
|
||||
Self::main_column_area(
|
||||
area,
|
||||
primary_right,
|
||||
Option::from(dimensions[self.first_container_idx(idx - 1)]),
|
||||
)
|
||||
};
|
||||
|
||||
dimensions.push(main_column_area);
|
||||
}
|
||||
Column::Primary(None) | Column::Secondary(None) => {
|
||||
dimensions.push(column_area);
|
||||
}
|
||||
Column::Secondary(Some(split)) => match split {
|
||||
ColumnSplitWithCapacity::Horizontal(capacity) => {
|
||||
let mut rows = rows(&column_area, *capacity);
|
||||
dimensions.append(&mut rows);
|
||||
}
|
||||
ColumnSplitWithCapacity::Vertical(capacity) => {
|
||||
let mut columns = columns(&column_area, *capacity);
|
||||
dimensions.append(&mut columns);
|
||||
}
|
||||
},
|
||||
Column::Tertiary(split) => {
|
||||
let column_area = Self::column_area_with_last(
|
||||
self.len(),
|
||||
area,
|
||||
primary_right,
|
||||
Option::from(dimensions[self.first_container_idx(idx - 1)]),
|
||||
offset,
|
||||
);
|
||||
|
||||
let remaining = container_count - tertiary_trigger_threshold;
|
||||
|
||||
match split {
|
||||
ColumnSplit::Horizontal => {
|
||||
let mut rows = rows(&column_area, remaining);
|
||||
dimensions.append(&mut rows);
|
||||
}
|
||||
ColumnSplit::Vertical => {
|
||||
let mut columns = columns(&column_area, remaining);
|
||||
dimensions.append(&mut columns);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
dimensions
|
||||
.iter_mut()
|
||||
.for_each(|l| l.add_padding(container_padding));
|
||||
|
||||
dimensions
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, Copy, Debug, Serialize, Deserialize, Display, EnumString, ArgEnum)]
|
||||
#[strum(serialize_all = "snake_case")]
|
||||
pub enum Flip {
|
||||
Horizontal,
|
||||
Vertical,
|
||||
HorizontalAndVertical,
|
||||
}
|
||||
|
||||
#[must_use]
|
||||
fn columns(area: &Rect, len: usize) -> Vec<Rect> {
|
||||
#[allow(clippy::cast_possible_wrap, clippy::cast_possible_truncation)]
|
||||
let right = area.right / len as i32;
|
||||
let mut left = 0;
|
||||
|
||||
let mut layouts: Vec<Rect> = vec![];
|
||||
for _ in 0..len {
|
||||
layouts.push(Rect {
|
||||
left: area.left + left,
|
||||
top: area.top,
|
||||
right,
|
||||
bottom: area.bottom,
|
||||
});
|
||||
|
||||
left += right;
|
||||
}
|
||||
|
||||
layouts
|
||||
}
|
||||
|
||||
#[must_use]
|
||||
fn rows(area: &Rect, len: usize) -> Vec<Rect> {
|
||||
#[allow(clippy::cast_possible_wrap, clippy::cast_possible_truncation)]
|
||||
let bottom = area.bottom / len as i32;
|
||||
let mut top = 0;
|
||||
|
||||
let mut layouts: Vec<Rect> = vec![];
|
||||
for _ in 0..len {
|
||||
layouts.push(Rect {
|
||||
left: area.left,
|
||||
top: area.top + top,
|
||||
right: area.right,
|
||||
bottom,
|
||||
});
|
||||
|
||||
top += bottom;
|
||||
}
|
||||
|
||||
layouts
|
||||
}
|
||||
|
||||
fn calculate_resize_adjustments(resize_dimensions: &[Option<Rect>]) -> Vec<Option<Rect>> {
|
||||
let mut resize_adjustments = resize_dimensions.to_vec();
|
||||
|
||||
// This needs to be aware of layout flips
|
||||
for (i, opt) in resize_dimensions.iter().enumerate() {
|
||||
if let Some(resize_ref) = opt {
|
||||
if i > 0 {
|
||||
if resize_ref.left != 0 {
|
||||
#[allow(clippy::if_not_else)]
|
||||
let range = if i == 1 {
|
||||
0..1
|
||||
} else if i & 1 != 0 {
|
||||
i - 1..i
|
||||
} else {
|
||||
i - 2..i
|
||||
};
|
||||
|
||||
for n in range {
|
||||
let should_adjust = n % 2 == 0;
|
||||
if should_adjust {
|
||||
if let Some(Some(adjacent_resize)) = resize_adjustments.get_mut(n) {
|
||||
adjacent_resize.right += resize_ref.left;
|
||||
} else {
|
||||
resize_adjustments[n] = Option::from(Rect {
|
||||
left: 0,
|
||||
top: 0,
|
||||
right: resize_ref.left,
|
||||
bottom: 0,
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if let Some(rr) = resize_adjustments[i].as_mut() {
|
||||
rr.left = 0;
|
||||
}
|
||||
}
|
||||
|
||||
if resize_ref.top != 0 {
|
||||
let range = if i == 1 {
|
||||
0..1
|
||||
} else if i & 1 == 0 {
|
||||
i - 1..i
|
||||
} else {
|
||||
i - 2..i
|
||||
};
|
||||
|
||||
for n in range {
|
||||
let should_adjust = n % 2 != 0;
|
||||
if should_adjust {
|
||||
if let Some(Some(adjacent_resize)) = resize_adjustments.get_mut(n) {
|
||||
adjacent_resize.bottom += resize_ref.top;
|
||||
} else {
|
||||
resize_adjustments[n] = Option::from(Rect {
|
||||
left: 0,
|
||||
top: 0,
|
||||
right: 0,
|
||||
bottom: resize_ref.top,
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if let Some(Some(resize)) = resize_adjustments.get_mut(i) {
|
||||
resize.top = 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
let cleaned_resize_adjustments: Vec<_> = resize_adjustments
|
||||
.iter()
|
||||
.map(|adjustment| match adjustment {
|
||||
None => None,
|
||||
Some(rect) if rect.eq(&Rect::default()) => None,
|
||||
Some(_) => *adjustment,
|
||||
})
|
||||
.collect();
|
||||
|
||||
cleaned_resize_adjustments
|
||||
}
|
||||
|
||||
fn recursive_fibonacci(
|
||||
idx: usize,
|
||||
count: usize,
|
||||
area: &Rect,
|
||||
layout_flip: Option<Flip>,
|
||||
resize_adjustments: Vec<Option<Rect>>,
|
||||
) -> Vec<Rect> {
|
||||
let mut a = *area;
|
||||
|
||||
let resized = if let Some(Some(r)) = resize_adjustments.get(idx) {
|
||||
a.left += r.left;
|
||||
a.top += r.top;
|
||||
a.right += r.right;
|
||||
a.bottom += r.bottom;
|
||||
a
|
||||
} else {
|
||||
*area
|
||||
};
|
||||
|
||||
let half_width = area.right / 2;
|
||||
let half_height = area.bottom / 2;
|
||||
let half_resized_width = resized.right / 2;
|
||||
let half_resized_height = resized.bottom / 2;
|
||||
|
||||
let (main_x, alt_x, alt_y, main_y);
|
||||
|
||||
if let Some(flip) = layout_flip {
|
||||
match flip {
|
||||
Flip::Horizontal => {
|
||||
main_x = resized.left + half_width + (half_width - half_resized_width);
|
||||
alt_x = resized.left;
|
||||
|
||||
alt_y = resized.top + half_resized_height;
|
||||
main_y = resized.top;
|
||||
}
|
||||
Flip::Vertical => {
|
||||
main_y = resized.top + half_height + (half_height - half_resized_height);
|
||||
alt_y = resized.top;
|
||||
|
||||
main_x = resized.left;
|
||||
alt_x = resized.left + half_resized_width;
|
||||
}
|
||||
Flip::HorizontalAndVertical => {
|
||||
main_x = resized.left + half_width + (half_width - half_resized_width);
|
||||
alt_x = resized.left;
|
||||
main_y = resized.top + half_height + (half_height - half_resized_height);
|
||||
alt_y = resized.top;
|
||||
}
|
||||
}
|
||||
} else {
|
||||
main_x = resized.left;
|
||||
alt_x = resized.left + half_resized_width;
|
||||
main_y = resized.top;
|
||||
alt_y = resized.top + half_resized_height;
|
||||
}
|
||||
|
||||
#[allow(clippy::if_not_else)]
|
||||
if count == 0 {
|
||||
vec![]
|
||||
} else if count == 1 {
|
||||
vec![Rect {
|
||||
left: resized.left,
|
||||
top: resized.top,
|
||||
right: resized.right,
|
||||
bottom: resized.bottom,
|
||||
}]
|
||||
} else if idx % 2 != 0 {
|
||||
let mut res = vec![Rect {
|
||||
left: resized.left,
|
||||
top: main_y,
|
||||
right: resized.right,
|
||||
bottom: half_resized_height,
|
||||
}];
|
||||
res.append(&mut recursive_fibonacci(
|
||||
idx + 1,
|
||||
count - 1,
|
||||
&Rect {
|
||||
left: area.left,
|
||||
top: alt_y,
|
||||
right: area.right,
|
||||
bottom: area.bottom - half_resized_height,
|
||||
},
|
||||
layout_flip,
|
||||
resize_adjustments,
|
||||
));
|
||||
res
|
||||
} else {
|
||||
let mut res = vec![Rect {
|
||||
left: main_x,
|
||||
top: resized.top,
|
||||
right: half_resized_width,
|
||||
bottom: resized.bottom,
|
||||
}];
|
||||
res.append(&mut recursive_fibonacci(
|
||||
idx + 1,
|
||||
count - 1,
|
||||
&Rect {
|
||||
left: alt_x,
|
||||
top: area.top,
|
||||
right: area.right - half_resized_width,
|
||||
bottom: area.bottom,
|
||||
},
|
||||
layout_flip,
|
||||
resize_adjustments,
|
||||
));
|
||||
res
|
||||
}
|
||||
}
|
||||
262
komorebi-core/src/custom_layout.rs
Normal file
262
komorebi-core/src/custom_layout.rs
Normal file
@@ -0,0 +1,262 @@
|
||||
use std::collections::HashMap;
|
||||
use std::fs::File;
|
||||
use std::io::BufReader;
|
||||
use std::ops::Deref;
|
||||
use std::path::PathBuf;
|
||||
|
||||
use color_eyre::eyre::anyhow;
|
||||
use color_eyre::Result;
|
||||
use serde::Deserialize;
|
||||
use serde::Serialize;
|
||||
|
||||
use crate::Rect;
|
||||
|
||||
#[derive(Clone, Debug, Serialize, Deserialize)]
|
||||
pub struct CustomLayout(Vec<Column>);
|
||||
|
||||
impl Deref for CustomLayout {
|
||||
type Target = Vec<Column>;
|
||||
|
||||
fn deref(&self) -> &Self::Target {
|
||||
&self.0
|
||||
}
|
||||
}
|
||||
|
||||
impl CustomLayout {
|
||||
pub fn from_path_buf(path: PathBuf) -> Result<Self> {
|
||||
let invalid_filetype = anyhow!("custom layouts must be json or yaml files");
|
||||
let layout: Self = match path.extension() {
|
||||
Some(extension) => {
|
||||
if extension == "yaml" || extension == "yml" {
|
||||
serde_yaml::from_reader(BufReader::new(File::open(path)?))?
|
||||
} else if extension == "json" {
|
||||
serde_json::from_reader(BufReader::new(File::open(path)?))?
|
||||
} else {
|
||||
return Err(invalid_filetype);
|
||||
}
|
||||
}
|
||||
None => return Err(invalid_filetype),
|
||||
};
|
||||
|
||||
if !layout.is_valid() {
|
||||
return Err(anyhow!("the layout file provided was invalid"));
|
||||
}
|
||||
|
||||
Ok(layout)
|
||||
}
|
||||
|
||||
#[must_use]
|
||||
pub fn column_with_idx(&self, idx: usize) -> (usize, Option<&Column>) {
|
||||
let column_idx = self.column_for_container_idx(idx);
|
||||
let column = self.get(column_idx);
|
||||
(column_idx, column)
|
||||
}
|
||||
|
||||
#[must_use]
|
||||
pub fn primary_idx(&self) -> Option<usize> {
|
||||
for (i, column) in self.iter().enumerate() {
|
||||
if let Column::Primary(_) = column {
|
||||
return Option::from(i);
|
||||
}
|
||||
}
|
||||
|
||||
None
|
||||
}
|
||||
|
||||
#[must_use]
|
||||
pub fn primary_width_percentage(&self) -> Option<usize> {
|
||||
for column in self.iter() {
|
||||
if let Column::Primary(Option::Some(ColumnWidth::WidthPercentage(percentage))) = column
|
||||
{
|
||||
return Option::from(*percentage);
|
||||
}
|
||||
}
|
||||
|
||||
None
|
||||
}
|
||||
|
||||
#[must_use]
|
||||
pub fn is_valid(&self) -> bool {
|
||||
// A valid layout must have at least one column
|
||||
if self.is_empty() {
|
||||
return false;
|
||||
};
|
||||
|
||||
// Vertical column splits aren't supported at the moment
|
||||
for column in self.iter() {
|
||||
match column {
|
||||
Column::Tertiary(ColumnSplit::Vertical)
|
||||
| Column::Secondary(Some(ColumnSplitWithCapacity::Vertical(_))) => return false,
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
|
||||
// The final column must not have a fixed capacity
|
||||
match self.last() {
|
||||
Some(Column::Tertiary(_)) => {}
|
||||
_ => return false,
|
||||
}
|
||||
|
||||
let mut primaries = 0;
|
||||
let mut tertiaries = 0;
|
||||
|
||||
for column in self.iter() {
|
||||
match column {
|
||||
Column::Primary(_) => primaries += 1,
|
||||
Column::Tertiary(_) => tertiaries += 1,
|
||||
Column::Secondary(_) => {}
|
||||
}
|
||||
}
|
||||
|
||||
// There must only be one primary and one tertiary column
|
||||
matches!(primaries, 1) && matches!(tertiaries, 1)
|
||||
}
|
||||
|
||||
pub(crate) fn column_container_counts(&self) -> HashMap<usize, usize> {
|
||||
let mut count_map = HashMap::new();
|
||||
|
||||
for (idx, column) in self.iter().enumerate() {
|
||||
match column {
|
||||
Column::Primary(_) | Column::Secondary(None) => {
|
||||
count_map.insert(idx, 1);
|
||||
}
|
||||
Column::Secondary(Some(split)) => {
|
||||
count_map.insert(
|
||||
idx,
|
||||
match split {
|
||||
ColumnSplitWithCapacity::Vertical(n)
|
||||
| ColumnSplitWithCapacity::Horizontal(n) => *n,
|
||||
},
|
||||
);
|
||||
}
|
||||
Column::Tertiary(_) => {}
|
||||
}
|
||||
}
|
||||
|
||||
count_map
|
||||
}
|
||||
|
||||
#[must_use]
|
||||
pub fn first_container_idx(&self, col_idx: usize) -> usize {
|
||||
let count_map = self.column_container_counts();
|
||||
let mut container_idx_accumulator = 0;
|
||||
|
||||
for i in 0..col_idx {
|
||||
if let Some(n) = count_map.get(&i) {
|
||||
container_idx_accumulator += n;
|
||||
}
|
||||
}
|
||||
|
||||
container_idx_accumulator
|
||||
}
|
||||
|
||||
#[must_use]
|
||||
pub fn column_for_container_idx(&self, idx: usize) -> usize {
|
||||
let count_map = self.column_container_counts();
|
||||
let mut container_idx_accumulator = 0;
|
||||
|
||||
// always -1 because we don't insert the tertiary column in the count_map
|
||||
for i in 0..self.len() - 1 {
|
||||
if let Some(n) = count_map.get(&i) {
|
||||
container_idx_accumulator += n;
|
||||
|
||||
// The accumulator becomes greater than the window container index
|
||||
// for the first time when we reach a column that contains that
|
||||
// window container index
|
||||
if container_idx_accumulator > idx {
|
||||
return i;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// If the accumulator never reaches a point where it is greater than the
|
||||
// window container index, then the only remaining possibility is the
|
||||
// final tertiary column
|
||||
self.len() - 1
|
||||
}
|
||||
|
||||
#[must_use]
|
||||
pub fn column_area(&self, work_area: &Rect, idx: usize, offset: Option<usize>) -> Rect {
|
||||
let divisor = offset.map_or_else(|| self.len(), |offset| self.len() - offset);
|
||||
|
||||
#[allow(clippy::cast_possible_wrap, clippy::cast_possible_truncation)]
|
||||
let equal_width = work_area.right / divisor as i32;
|
||||
let mut left = work_area.left;
|
||||
let right = equal_width;
|
||||
|
||||
for _ in 0..idx {
|
||||
left += right;
|
||||
}
|
||||
|
||||
Rect {
|
||||
left,
|
||||
top: work_area.top,
|
||||
right,
|
||||
bottom: work_area.bottom,
|
||||
}
|
||||
}
|
||||
|
||||
#[must_use]
|
||||
pub fn column_area_with_last(
|
||||
len: usize,
|
||||
work_area: &Rect,
|
||||
primary_right: i32,
|
||||
last_column: Option<Rect>,
|
||||
offset: Option<usize>,
|
||||
) -> Rect {
|
||||
let divisor = offset.map_or_else(|| len - 1, |offset| len - offset - 1);
|
||||
|
||||
#[allow(clippy::cast_possible_wrap, clippy::cast_possible_truncation)]
|
||||
let equal_width = (work_area.right - primary_right) / divisor as i32;
|
||||
let left = last_column.map_or(work_area.left, |last| last.left + last.right);
|
||||
let right = equal_width;
|
||||
|
||||
Rect {
|
||||
left,
|
||||
top: work_area.top,
|
||||
right,
|
||||
bottom: work_area.bottom,
|
||||
}
|
||||
}
|
||||
|
||||
#[must_use]
|
||||
pub fn main_column_area(
|
||||
work_area: &Rect,
|
||||
primary_right: i32,
|
||||
last_column: Option<Rect>,
|
||||
) -> Rect {
|
||||
let left = last_column.map_or(work_area.left, |last| last.left + last.right);
|
||||
|
||||
Rect {
|
||||
left,
|
||||
top: work_area.top,
|
||||
right: primary_right,
|
||||
bottom: work_area.bottom,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, Copy, Debug, Serialize, Deserialize)]
|
||||
#[serde(tag = "column", content = "configuration")]
|
||||
pub enum Column {
|
||||
Primary(Option<ColumnWidth>),
|
||||
Secondary(Option<ColumnSplitWithCapacity>),
|
||||
Tertiary(ColumnSplit),
|
||||
}
|
||||
|
||||
#[derive(Clone, Copy, Debug, Serialize, Deserialize)]
|
||||
pub enum ColumnWidth {
|
||||
WidthPercentage(usize),
|
||||
}
|
||||
|
||||
#[derive(Clone, Copy, Debug, Serialize, Deserialize)]
|
||||
pub enum ColumnSplit {
|
||||
Horizontal,
|
||||
Vertical,
|
||||
}
|
||||
|
||||
#[derive(Clone, Copy, Debug, Serialize, Deserialize)]
|
||||
pub enum ColumnSplitWithCapacity {
|
||||
Horizontal(usize),
|
||||
Vertical(usize),
|
||||
}
|
||||
125
komorebi-core/src/default_layout.rs
Normal file
125
komorebi-core/src/default_layout.rs
Normal file
@@ -0,0 +1,125 @@
|
||||
use clap::ArgEnum;
|
||||
use serde::Deserialize;
|
||||
use serde::Serialize;
|
||||
use strum::Display;
|
||||
use strum::EnumString;
|
||||
|
||||
use crate::OperationDirection;
|
||||
use crate::Rect;
|
||||
use crate::Sizing;
|
||||
|
||||
#[derive(Clone, Copy, Debug, Serialize, Deserialize, Display, EnumString, ArgEnum)]
|
||||
#[strum(serialize_all = "snake_case")]
|
||||
pub enum DefaultLayout {
|
||||
BSP,
|
||||
Columns,
|
||||
Rows,
|
||||
VerticalStack,
|
||||
HorizontalStack,
|
||||
UltrawideVerticalStack,
|
||||
}
|
||||
|
||||
impl DefaultLayout {
|
||||
#[must_use]
|
||||
#[allow(clippy::cast_precision_loss)]
|
||||
pub fn resize(
|
||||
&self,
|
||||
unaltered: &Rect,
|
||||
resize: &Option<Rect>,
|
||||
edge: OperationDirection,
|
||||
sizing: Sizing,
|
||||
step: Option<i32>,
|
||||
) -> Option<Rect> {
|
||||
if !matches!(self, Self::BSP) {
|
||||
return None;
|
||||
};
|
||||
|
||||
let max_divisor = 1.005;
|
||||
let mut r = resize.unwrap_or_default();
|
||||
|
||||
let resize_step = step.unwrap_or(50);
|
||||
|
||||
match edge {
|
||||
OperationDirection::Left => match sizing {
|
||||
Sizing::Increase => {
|
||||
// Some final checks to make sure the user can't infinitely resize to
|
||||
// the point of pushing other windows out of bounds
|
||||
|
||||
// Note: These checks cannot take into account the changes made to the
|
||||
// edges of adjacent windows at operation time, so it is still possible
|
||||
// to push windows out of bounds by maxing out an Increase Left on a
|
||||
// Window with index 1, and then maxing out a Decrease Right on a Window
|
||||
// with index 0. I don't think it's worth trying to defensively program
|
||||
// against this; if people end up in this situation they are better off
|
||||
// just hitting the retile command
|
||||
let diff = ((r.left + -resize_step) as f32).abs();
|
||||
let max = unaltered.right as f32 / max_divisor;
|
||||
if diff < max {
|
||||
r.left += -resize_step;
|
||||
}
|
||||
}
|
||||
Sizing::Decrease => {
|
||||
let diff = ((r.left - -resize_step) as f32).abs();
|
||||
let max = unaltered.right as f32 / max_divisor;
|
||||
if diff < max {
|
||||
r.left -= -resize_step;
|
||||
}
|
||||
}
|
||||
},
|
||||
OperationDirection::Up => match sizing {
|
||||
Sizing::Increase => {
|
||||
let diff = ((r.top + resize_step) as f32).abs();
|
||||
let max = unaltered.bottom as f32 / max_divisor;
|
||||
if diff < max {
|
||||
r.top += -resize_step;
|
||||
}
|
||||
}
|
||||
Sizing::Decrease => {
|
||||
let diff = ((r.top - resize_step) as f32).abs();
|
||||
let max = unaltered.bottom as f32 / max_divisor;
|
||||
if diff < max {
|
||||
r.top -= -resize_step;
|
||||
}
|
||||
}
|
||||
},
|
||||
OperationDirection::Right => match sizing {
|
||||
Sizing::Increase => {
|
||||
let diff = ((r.right + resize_step) as f32).abs();
|
||||
let max = unaltered.right as f32 / max_divisor;
|
||||
if diff < max {
|
||||
r.right += resize_step;
|
||||
}
|
||||
}
|
||||
Sizing::Decrease => {
|
||||
let diff = ((r.right - resize_step) as f32).abs();
|
||||
let max = unaltered.right as f32 / max_divisor;
|
||||
if diff < max {
|
||||
r.right -= resize_step;
|
||||
}
|
||||
}
|
||||
},
|
||||
OperationDirection::Down => match sizing {
|
||||
Sizing::Increase => {
|
||||
let diff = ((r.bottom + resize_step) as f32).abs();
|
||||
let max = unaltered.bottom as f32 / max_divisor;
|
||||
if diff < max {
|
||||
r.bottom += resize_step;
|
||||
}
|
||||
}
|
||||
Sizing::Decrease => {
|
||||
let diff = ((r.bottom - resize_step) as f32).abs();
|
||||
let max = unaltered.bottom as f32 / max_divisor;
|
||||
if diff < max {
|
||||
r.bottom -= resize_step;
|
||||
}
|
||||
}
|
||||
},
|
||||
};
|
||||
|
||||
if r.eq(&Rect::default()) {
|
||||
None
|
||||
} else {
|
||||
Option::from(r)
|
||||
}
|
||||
}
|
||||
}
|
||||
289
komorebi-core/src/direction.rs
Normal file
289
komorebi-core/src/direction.rs
Normal file
@@ -0,0 +1,289 @@
|
||||
use crate::custom_layout::Column;
|
||||
use crate::custom_layout::ColumnSplit;
|
||||
use crate::custom_layout::ColumnSplitWithCapacity;
|
||||
use crate::custom_layout::CustomLayout;
|
||||
use crate::DefaultLayout;
|
||||
use crate::OperationDirection;
|
||||
|
||||
pub trait Direction {
|
||||
fn index_in_direction(
|
||||
&self,
|
||||
op_direction: OperationDirection,
|
||||
idx: usize,
|
||||
count: usize,
|
||||
) -> Option<usize>;
|
||||
|
||||
fn is_valid_direction(
|
||||
&self,
|
||||
op_direction: OperationDirection,
|
||||
idx: usize,
|
||||
count: usize,
|
||||
) -> bool;
|
||||
fn up_index(&self, idx: usize) -> usize;
|
||||
fn down_index(&self, idx: usize) -> usize;
|
||||
fn left_index(&self, idx: usize) -> usize;
|
||||
fn right_index(&self, idx: usize) -> usize;
|
||||
}
|
||||
|
||||
impl Direction for DefaultLayout {
|
||||
fn index_in_direction(
|
||||
&self,
|
||||
op_direction: OperationDirection,
|
||||
idx: usize,
|
||||
count: usize,
|
||||
) -> Option<usize> {
|
||||
match op_direction {
|
||||
OperationDirection::Left => {
|
||||
if self.is_valid_direction(op_direction, idx, count) {
|
||||
Option::from(self.left_index(idx))
|
||||
} else {
|
||||
None
|
||||
}
|
||||
}
|
||||
OperationDirection::Right => {
|
||||
if self.is_valid_direction(op_direction, idx, count) {
|
||||
Option::from(self.right_index(idx))
|
||||
} else {
|
||||
None
|
||||
}
|
||||
}
|
||||
OperationDirection::Up => {
|
||||
if self.is_valid_direction(op_direction, idx, count) {
|
||||
Option::from(self.up_index(idx))
|
||||
} else {
|
||||
None
|
||||
}
|
||||
}
|
||||
OperationDirection::Down => {
|
||||
if self.is_valid_direction(op_direction, idx, count) {
|
||||
Option::from(self.down_index(idx))
|
||||
} else {
|
||||
None
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn is_valid_direction(
|
||||
&self,
|
||||
op_direction: OperationDirection,
|
||||
idx: usize,
|
||||
count: usize,
|
||||
) -> bool {
|
||||
match op_direction {
|
||||
OperationDirection::Up => match self {
|
||||
DefaultLayout::BSP => count > 2 && idx != 0 && idx != 1,
|
||||
DefaultLayout::Columns => false,
|
||||
DefaultLayout::Rows | DefaultLayout::HorizontalStack => idx != 0,
|
||||
DefaultLayout::VerticalStack => idx != 0 && idx != 1,
|
||||
DefaultLayout::UltrawideVerticalStack => idx > 2,
|
||||
},
|
||||
OperationDirection::Down => match self {
|
||||
DefaultLayout::BSP => count > 2 && idx != count - 1 && idx % 2 != 0,
|
||||
DefaultLayout::Columns => false,
|
||||
DefaultLayout::Rows => idx != count - 1,
|
||||
DefaultLayout::VerticalStack => idx != 0 && idx != count - 1,
|
||||
DefaultLayout::HorizontalStack => idx == 0,
|
||||
DefaultLayout::UltrawideVerticalStack => idx > 1 && idx != count - 1,
|
||||
},
|
||||
OperationDirection::Left => match self {
|
||||
DefaultLayout::BSP => count > 1 && idx != 0,
|
||||
DefaultLayout::Columns | DefaultLayout::VerticalStack => idx != 0,
|
||||
DefaultLayout::Rows => false,
|
||||
DefaultLayout::HorizontalStack => idx != 0 && idx != 1,
|
||||
DefaultLayout::UltrawideVerticalStack => count > 1 && idx != 1,
|
||||
},
|
||||
OperationDirection::Right => match self {
|
||||
DefaultLayout::BSP => count > 1 && idx % 2 == 0 && idx != count - 1,
|
||||
DefaultLayout::Columns => idx != count - 1,
|
||||
DefaultLayout::Rows => false,
|
||||
DefaultLayout::VerticalStack => idx == 0,
|
||||
DefaultLayout::HorizontalStack => idx != 0 && idx != count - 1,
|
||||
DefaultLayout::UltrawideVerticalStack => match count {
|
||||
0 | 1 => false,
|
||||
2 => idx != 0,
|
||||
_ => idx < 2,
|
||||
},
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
fn up_index(&self, idx: usize) -> usize {
|
||||
match self {
|
||||
DefaultLayout::BSP => {
|
||||
if idx % 2 == 0 {
|
||||
idx - 1
|
||||
} else {
|
||||
idx - 2
|
||||
}
|
||||
}
|
||||
DefaultLayout::Columns => unreachable!(),
|
||||
DefaultLayout::Rows
|
||||
| DefaultLayout::VerticalStack
|
||||
| DefaultLayout::UltrawideVerticalStack => idx - 1,
|
||||
DefaultLayout::HorizontalStack => 0,
|
||||
}
|
||||
}
|
||||
|
||||
fn down_index(&self, idx: usize) -> usize {
|
||||
match self {
|
||||
DefaultLayout::BSP
|
||||
| DefaultLayout::Rows
|
||||
| DefaultLayout::VerticalStack
|
||||
| DefaultLayout::UltrawideVerticalStack => idx + 1,
|
||||
DefaultLayout::Columns => unreachable!(),
|
||||
DefaultLayout::HorizontalStack => 1,
|
||||
}
|
||||
}
|
||||
|
||||
fn left_index(&self, idx: usize) -> usize {
|
||||
match self {
|
||||
DefaultLayout::BSP => {
|
||||
if idx % 2 == 0 {
|
||||
idx - 2
|
||||
} else {
|
||||
idx - 1
|
||||
}
|
||||
}
|
||||
DefaultLayout::Columns | DefaultLayout::HorizontalStack => idx - 1,
|
||||
DefaultLayout::Rows => unreachable!(),
|
||||
DefaultLayout::VerticalStack => 0,
|
||||
DefaultLayout::UltrawideVerticalStack => match idx {
|
||||
0 => 1,
|
||||
1 => unreachable!(),
|
||||
_ => 0,
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
fn right_index(&self, idx: usize) -> usize {
|
||||
match self {
|
||||
DefaultLayout::BSP | DefaultLayout::Columns | DefaultLayout::HorizontalStack => idx + 1,
|
||||
DefaultLayout::Rows => unreachable!(),
|
||||
DefaultLayout::VerticalStack => 1,
|
||||
DefaultLayout::UltrawideVerticalStack => match idx {
|
||||
1 => 0,
|
||||
0 => 2,
|
||||
_ => unreachable!(),
|
||||
},
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Direction for CustomLayout {
|
||||
fn index_in_direction(
|
||||
&self,
|
||||
op_direction: OperationDirection,
|
||||
idx: usize,
|
||||
count: usize,
|
||||
) -> Option<usize> {
|
||||
if count <= self.len() {
|
||||
return DefaultLayout::Columns.index_in_direction(op_direction, idx, count);
|
||||
}
|
||||
|
||||
match op_direction {
|
||||
OperationDirection::Left => {
|
||||
if self.is_valid_direction(op_direction, idx, count) {
|
||||
Option::from(self.left_index(idx))
|
||||
} else {
|
||||
None
|
||||
}
|
||||
}
|
||||
OperationDirection::Right => {
|
||||
if self.is_valid_direction(op_direction, idx, count) {
|
||||
Option::from(self.right_index(idx))
|
||||
} else {
|
||||
None
|
||||
}
|
||||
}
|
||||
OperationDirection::Up => {
|
||||
if self.is_valid_direction(op_direction, idx, count) {
|
||||
Option::from(self.up_index(idx))
|
||||
} else {
|
||||
None
|
||||
}
|
||||
}
|
||||
OperationDirection::Down => {
|
||||
if self.is_valid_direction(op_direction, idx, count) {
|
||||
Option::from(self.down_index(idx))
|
||||
} else {
|
||||
None
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn is_valid_direction(
|
||||
&self,
|
||||
op_direction: OperationDirection,
|
||||
idx: usize,
|
||||
count: usize,
|
||||
) -> bool {
|
||||
if count <= self.len() {
|
||||
return DefaultLayout::Columns.is_valid_direction(op_direction, idx, count);
|
||||
}
|
||||
|
||||
match op_direction {
|
||||
OperationDirection::Left => idx != 0 && self.column_for_container_idx(idx) != 0,
|
||||
OperationDirection::Right => {
|
||||
idx != count - 1 && self.column_for_container_idx(idx) != self.len() - 1
|
||||
}
|
||||
OperationDirection::Up => {
|
||||
if idx == 0 {
|
||||
return false;
|
||||
}
|
||||
|
||||
let (column_idx, column) = self.column_with_idx(idx);
|
||||
match column {
|
||||
None => false,
|
||||
Some(column) => match column {
|
||||
Column::Secondary(Some(ColumnSplitWithCapacity::Horizontal(_)))
|
||||
| Column::Tertiary(ColumnSplit::Horizontal) => {
|
||||
self.column_for_container_idx(idx - 1) == column_idx
|
||||
}
|
||||
_ => false,
|
||||
},
|
||||
}
|
||||
}
|
||||
OperationDirection::Down => {
|
||||
if idx == count - 1 {
|
||||
return false;
|
||||
}
|
||||
|
||||
let (column_idx, column) = self.column_with_idx(idx);
|
||||
match column {
|
||||
None => false,
|
||||
Some(column) => match column {
|
||||
Column::Secondary(Some(ColumnSplitWithCapacity::Horizontal(_)))
|
||||
| Column::Tertiary(ColumnSplit::Horizontal) => {
|
||||
self.column_for_container_idx(idx + 1) == column_idx
|
||||
}
|
||||
_ => false,
|
||||
},
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn up_index(&self, idx: usize) -> usize {
|
||||
idx - 1
|
||||
}
|
||||
|
||||
fn down_index(&self, idx: usize) -> usize {
|
||||
idx + 1
|
||||
}
|
||||
|
||||
fn left_index(&self, idx: usize) -> usize {
|
||||
let column_idx = self.column_for_container_idx(idx);
|
||||
if column_idx - 1 == 0 {
|
||||
0
|
||||
} else {
|
||||
self.first_container_idx(column_idx - 1)
|
||||
}
|
||||
}
|
||||
|
||||
fn right_index(&self, idx: usize) -> usize {
|
||||
let column_idx = self.column_for_container_idx(idx);
|
||||
self.first_container_idx(column_idx + 1)
|
||||
}
|
||||
}
|
||||
@@ -1,565 +1,31 @@
|
||||
use std::num::NonZeroUsize;
|
||||
|
||||
use clap::ArgEnum;
|
||||
use serde::Deserialize;
|
||||
use serde::Serialize;
|
||||
use strum::Display;
|
||||
use strum::EnumString;
|
||||
|
||||
use crate::OperationDirection;
|
||||
use crate::Rect;
|
||||
use crate::Sizing;
|
||||
use crate::Arrangement;
|
||||
use crate::CustomLayout;
|
||||
use crate::DefaultLayout;
|
||||
use crate::Direction;
|
||||
|
||||
#[derive(Clone, Copy, Debug, Serialize, Deserialize, Display, EnumString, ArgEnum)]
|
||||
#[strum(serialize_all = "snake_case")]
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub enum Layout {
|
||||
BSP,
|
||||
Columns,
|
||||
Rows,
|
||||
VerticalStack,
|
||||
HorizontalStack,
|
||||
UltrawideVerticalStack,
|
||||
}
|
||||
|
||||
#[derive(Clone, Copy, Debug, Serialize, Deserialize, Display, EnumString, ArgEnum)]
|
||||
#[strum(serialize_all = "snake_case")]
|
||||
pub enum Flip {
|
||||
Horizontal,
|
||||
Vertical,
|
||||
HorizontalAndVertical,
|
||||
Default(DefaultLayout),
|
||||
Custom(CustomLayout),
|
||||
}
|
||||
|
||||
impl Layout {
|
||||
#[must_use]
|
||||
#[allow(clippy::cast_precision_loss)]
|
||||
pub fn resize(
|
||||
&self,
|
||||
unaltered: &Rect,
|
||||
resize: &Option<Rect>,
|
||||
edge: OperationDirection,
|
||||
sizing: Sizing,
|
||||
step: Option<i32>,
|
||||
) -> Option<Rect> {
|
||||
if !matches!(self, Self::BSP) {
|
||||
return None;
|
||||
};
|
||||
|
||||
let max_divisor = 1.005;
|
||||
let mut r = resize.unwrap_or_default();
|
||||
|
||||
let resize_step = step.unwrap_or(50);
|
||||
|
||||
match edge {
|
||||
OperationDirection::Left => match sizing {
|
||||
Sizing::Increase => {
|
||||
// Some final checks to make sure the user can't infinitely resize to
|
||||
// the point of pushing other windows out of bounds
|
||||
|
||||
// Note: These checks cannot take into account the changes made to the
|
||||
// edges of adjacent windows at operation time, so it is still possible
|
||||
// to push windows out of bounds by maxing out an Increase Left on a
|
||||
// Window with index 1, and then maxing out a Decrease Right on a Window
|
||||
// with index 0. I don't think it's worth trying to defensively program
|
||||
// against this; if people end up in this situation they are better off
|
||||
// just hitting the retile command
|
||||
let diff = ((r.left + -resize_step) as f32).abs();
|
||||
let max = unaltered.right as f32 / max_divisor;
|
||||
if diff < max {
|
||||
r.left += -resize_step;
|
||||
}
|
||||
}
|
||||
Sizing::Decrease => {
|
||||
let diff = ((r.left - -resize_step) as f32).abs();
|
||||
let max = unaltered.right as f32 / max_divisor;
|
||||
if diff < max {
|
||||
r.left -= -resize_step;
|
||||
}
|
||||
}
|
||||
},
|
||||
OperationDirection::Up => match sizing {
|
||||
Sizing::Increase => {
|
||||
let diff = ((r.top + resize_step) as f32).abs();
|
||||
let max = unaltered.bottom as f32 / max_divisor;
|
||||
if diff < max {
|
||||
r.top += -resize_step;
|
||||
}
|
||||
}
|
||||
Sizing::Decrease => {
|
||||
let diff = ((r.top - resize_step) as f32).abs();
|
||||
let max = unaltered.bottom as f32 / max_divisor;
|
||||
if diff < max {
|
||||
r.top -= -resize_step;
|
||||
}
|
||||
}
|
||||
},
|
||||
OperationDirection::Right => match sizing {
|
||||
Sizing::Increase => {
|
||||
let diff = ((r.right + resize_step) as f32).abs();
|
||||
let max = unaltered.right as f32 / max_divisor;
|
||||
if diff < max {
|
||||
r.right += resize_step;
|
||||
}
|
||||
}
|
||||
Sizing::Decrease => {
|
||||
let diff = ((r.right - resize_step) as f32).abs();
|
||||
let max = unaltered.right as f32 / max_divisor;
|
||||
if diff < max {
|
||||
r.right -= resize_step;
|
||||
}
|
||||
}
|
||||
},
|
||||
OperationDirection::Down => match sizing {
|
||||
Sizing::Increase => {
|
||||
let diff = ((r.bottom + resize_step) as f32).abs();
|
||||
let max = unaltered.bottom as f32 / max_divisor;
|
||||
if diff < max {
|
||||
r.bottom += resize_step;
|
||||
}
|
||||
}
|
||||
Sizing::Decrease => {
|
||||
let diff = ((r.bottom - resize_step) as f32).abs();
|
||||
let max = unaltered.bottom as f32 / max_divisor;
|
||||
if diff < max {
|
||||
r.bottom -= resize_step;
|
||||
}
|
||||
}
|
||||
},
|
||||
};
|
||||
|
||||
if r.eq(&Rect::default()) {
|
||||
None
|
||||
} else {
|
||||
Option::from(r)
|
||||
pub fn as_boxed_direction(&self) -> Box<dyn Direction> {
|
||||
match self {
|
||||
Layout::Default(layout) => Box::new(*layout),
|
||||
Layout::Custom(layout) => Box::new(layout.clone()),
|
||||
}
|
||||
}
|
||||
|
||||
#[must_use]
|
||||
#[allow(
|
||||
clippy::cast_possible_truncation,
|
||||
clippy::cast_possible_wrap,
|
||||
clippy::too_many_lines
|
||||
)]
|
||||
pub fn calculate(
|
||||
&self,
|
||||
area: &Rect,
|
||||
len: NonZeroUsize,
|
||||
container_padding: Option<i32>,
|
||||
layout_flip: Option<Flip>,
|
||||
resize_dimensions: &[Option<Rect>],
|
||||
) -> Vec<Rect> {
|
||||
let len = usize::from(len);
|
||||
let mut dimensions = match self {
|
||||
Layout::BSP => recursive_fibonacci(
|
||||
0,
|
||||
len,
|
||||
area,
|
||||
layout_flip,
|
||||
calculate_resize_adjustments(resize_dimensions),
|
||||
),
|
||||
Layout::Columns => {
|
||||
let right = area.right / len as i32;
|
||||
let mut left = 0;
|
||||
|
||||
let mut layouts: Vec<Rect> = vec![];
|
||||
for _ in 0..len {
|
||||
layouts.push(Rect {
|
||||
left: area.left + left,
|
||||
top: area.top,
|
||||
right,
|
||||
bottom: area.bottom,
|
||||
});
|
||||
|
||||
left += right;
|
||||
}
|
||||
|
||||
layouts
|
||||
}
|
||||
Layout::Rows => {
|
||||
let bottom = area.bottom / len as i32;
|
||||
let mut top = 0;
|
||||
|
||||
let mut layouts: Vec<Rect> = vec![];
|
||||
for _ in 0..len {
|
||||
layouts.push(Rect {
|
||||
left: area.left,
|
||||
top: area.top + top,
|
||||
right: area.right,
|
||||
bottom,
|
||||
});
|
||||
|
||||
top += bottom;
|
||||
}
|
||||
|
||||
layouts
|
||||
}
|
||||
Layout::VerticalStack => {
|
||||
let mut layouts: Vec<Rect> = vec![];
|
||||
layouts.resize(len, Rect::default());
|
||||
|
||||
let primary_right = match len {
|
||||
1 => area.right,
|
||||
_ => area.right / 2,
|
||||
};
|
||||
|
||||
let mut main_left = area.left;
|
||||
let mut stack_left = area.left + primary_right;
|
||||
|
||||
match layout_flip {
|
||||
Some(Flip::Horizontal | Flip::HorizontalAndVertical) if len > 1 => {
|
||||
main_left = main_left + area.right - primary_right;
|
||||
stack_left = area.left;
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
|
||||
let mut iter = layouts.iter_mut();
|
||||
{
|
||||
if let Some(first) = iter.next() {
|
||||
first.left = main_left;
|
||||
first.top = area.top;
|
||||
first.right = primary_right;
|
||||
first.bottom = area.bottom;
|
||||
}
|
||||
}
|
||||
|
||||
let bottom = area.bottom / (len - 1) as i32;
|
||||
let mut top = 0;
|
||||
|
||||
for next in iter {
|
||||
next.left = stack_left;
|
||||
next.top = area.top + top;
|
||||
next.right = area.right - primary_right;
|
||||
next.bottom = bottom;
|
||||
|
||||
top += bottom;
|
||||
}
|
||||
|
||||
layouts
|
||||
}
|
||||
Layout::HorizontalStack => {
|
||||
let mut layouts: Vec<Rect> = vec![];
|
||||
layouts.resize(len, Rect::default());
|
||||
|
||||
let bottom = match len {
|
||||
1 => area.bottom,
|
||||
_ => area.bottom / 2,
|
||||
};
|
||||
|
||||
let mut main_top = area.top;
|
||||
let mut stack_top = area.top + bottom;
|
||||
|
||||
match layout_flip {
|
||||
Some(Flip::Vertical | Flip::HorizontalAndVertical) if len > 1 => {
|
||||
main_top = main_top + area.bottom - bottom;
|
||||
stack_top = area.top;
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
|
||||
let mut iter = layouts.iter_mut();
|
||||
{
|
||||
if let Some(first) = iter.next() {
|
||||
first.left = area.left;
|
||||
first.top = main_top;
|
||||
first.right = area.right;
|
||||
first.bottom = bottom;
|
||||
}
|
||||
}
|
||||
|
||||
let right = area.right / (len - 1) as i32;
|
||||
let mut left = 0;
|
||||
|
||||
for next in iter {
|
||||
next.left = area.left + left;
|
||||
next.top = stack_top;
|
||||
next.right = right;
|
||||
next.bottom = area.bottom - bottom;
|
||||
|
||||
left += right;
|
||||
}
|
||||
|
||||
layouts
|
||||
}
|
||||
Layout::UltrawideVerticalStack => {
|
||||
let mut layouts: Vec<Rect> = vec![];
|
||||
layouts.resize(len, Rect::default());
|
||||
|
||||
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(Flip::Horizontal | Flip::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(Flip::Horizontal | Flip::HorizontalAndVertical) if len > 1 => {
|
||||
secondary = area.left + primary_right + secondary_right;
|
||||
stack = area.left;
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
|
||||
(primary, secondary, stack)
|
||||
}
|
||||
};
|
||||
|
||||
let mut iter = layouts.iter_mut();
|
||||
|
||||
{
|
||||
if let Some(first) = iter.next() {
|
||||
first.left = primary_left;
|
||||
first.top = area.top;
|
||||
first.right = primary_right;
|
||||
first.bottom = area.bottom;
|
||||
}
|
||||
}
|
||||
|
||||
{
|
||||
if let Some(second) = iter.next() {
|
||||
second.left = secondary_left;
|
||||
second.top = area.top;
|
||||
second.right = secondary_right;
|
||||
second.bottom = area.bottom;
|
||||
}
|
||||
}
|
||||
|
||||
if len > 2 {
|
||||
let height = area.bottom / (len - 2) as i32;
|
||||
let mut y = 0;
|
||||
|
||||
for next in iter {
|
||||
next.left = stack_left;
|
||||
next.top = area.top + y;
|
||||
next.right = secondary_right;
|
||||
next.bottom = height;
|
||||
y += height;
|
||||
}
|
||||
}
|
||||
layouts
|
||||
}
|
||||
};
|
||||
|
||||
dimensions
|
||||
.iter_mut()
|
||||
.for_each(|l| l.add_padding(container_padding));
|
||||
|
||||
dimensions
|
||||
}
|
||||
}
|
||||
|
||||
fn calculate_resize_adjustments(resize_dimensions: &[Option<Rect>]) -> Vec<Option<Rect>> {
|
||||
let mut resize_adjustments = resize_dimensions.to_vec();
|
||||
|
||||
// This needs to be aware of layout flips
|
||||
for (i, opt) in resize_dimensions.iter().enumerate() {
|
||||
if let Some(resize_ref) = opt {
|
||||
if i > 0 {
|
||||
if resize_ref.left != 0 {
|
||||
#[allow(clippy::if_not_else)]
|
||||
let range = if i == 1 {
|
||||
0..1
|
||||
} else if i & 1 != 0 {
|
||||
i - 1..i
|
||||
} else {
|
||||
i - 2..i
|
||||
};
|
||||
|
||||
for n in range {
|
||||
let should_adjust = n % 2 == 0;
|
||||
if should_adjust {
|
||||
if let Some(Some(adjacent_resize)) = resize_adjustments.get_mut(n) {
|
||||
adjacent_resize.right += resize_ref.left;
|
||||
} else {
|
||||
resize_adjustments[n] = Option::from(Rect {
|
||||
left: 0,
|
||||
top: 0,
|
||||
right: resize_ref.left,
|
||||
bottom: 0,
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if let Some(rr) = resize_adjustments[i].as_mut() {
|
||||
rr.left = 0;
|
||||
}
|
||||
}
|
||||
|
||||
if resize_ref.top != 0 {
|
||||
let range = if i == 1 {
|
||||
0..1
|
||||
} else if i & 1 == 0 {
|
||||
i - 1..i
|
||||
} else {
|
||||
i - 2..i
|
||||
};
|
||||
|
||||
for n in range {
|
||||
let should_adjust = n % 2 != 0;
|
||||
if should_adjust {
|
||||
if let Some(Some(adjacent_resize)) = resize_adjustments.get_mut(n) {
|
||||
adjacent_resize.bottom += resize_ref.top;
|
||||
} else {
|
||||
resize_adjustments[n] = Option::from(Rect {
|
||||
left: 0,
|
||||
top: 0,
|
||||
right: 0,
|
||||
bottom: resize_ref.top,
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if let Some(Some(resize)) = resize_adjustments.get_mut(i) {
|
||||
resize.top = 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
pub fn as_boxed_arrangement(&self) -> Box<dyn Arrangement> {
|
||||
match self {
|
||||
Layout::Default(layout) => Box::new(*layout),
|
||||
Layout::Custom(layout) => Box::new(layout.clone()),
|
||||
}
|
||||
}
|
||||
|
||||
let cleaned_resize_adjustments: Vec<_> = resize_adjustments
|
||||
.iter()
|
||||
.map(|adjustment| match adjustment {
|
||||
None => None,
|
||||
Some(rect) if rect.eq(&Rect::default()) => None,
|
||||
Some(_) => *adjustment,
|
||||
})
|
||||
.collect();
|
||||
|
||||
cleaned_resize_adjustments
|
||||
}
|
||||
|
||||
fn recursive_fibonacci(
|
||||
idx: usize,
|
||||
count: usize,
|
||||
area: &Rect,
|
||||
layout_flip: Option<Flip>,
|
||||
resize_adjustments: Vec<Option<Rect>>,
|
||||
) -> Vec<Rect> {
|
||||
let mut a = *area;
|
||||
|
||||
let resized = if let Some(Some(r)) = resize_adjustments.get(idx) {
|
||||
a.left += r.left;
|
||||
a.top += r.top;
|
||||
a.right += r.right;
|
||||
a.bottom += r.bottom;
|
||||
a
|
||||
} else {
|
||||
*area
|
||||
};
|
||||
|
||||
let half_width = area.right / 2;
|
||||
let half_height = area.bottom / 2;
|
||||
let half_resized_width = resized.right / 2;
|
||||
let half_resized_height = resized.bottom / 2;
|
||||
|
||||
let (main_x, alt_x, alt_y, main_y);
|
||||
|
||||
if let Some(flip) = layout_flip {
|
||||
match flip {
|
||||
Flip::Horizontal => {
|
||||
main_x = resized.left + half_width + (half_width - half_resized_width);
|
||||
alt_x = resized.left;
|
||||
|
||||
alt_y = resized.top + half_resized_height;
|
||||
main_y = resized.top;
|
||||
}
|
||||
Flip::Vertical => {
|
||||
main_y = resized.top + half_height + (half_height - half_resized_height);
|
||||
alt_y = resized.top;
|
||||
|
||||
main_x = resized.left;
|
||||
alt_x = resized.left + half_resized_width;
|
||||
}
|
||||
Flip::HorizontalAndVertical => {
|
||||
main_x = resized.left + half_width + (half_width - half_resized_width);
|
||||
alt_x = resized.left;
|
||||
main_y = resized.top + half_height + (half_height - half_resized_height);
|
||||
alt_y = resized.top;
|
||||
}
|
||||
}
|
||||
} else {
|
||||
main_x = resized.left;
|
||||
alt_x = resized.left + half_resized_width;
|
||||
main_y = resized.top;
|
||||
alt_y = resized.top + half_resized_height;
|
||||
}
|
||||
|
||||
#[allow(clippy::if_not_else)]
|
||||
if count == 0 {
|
||||
vec![]
|
||||
} else if count == 1 {
|
||||
vec![Rect {
|
||||
left: resized.left,
|
||||
top: resized.top,
|
||||
right: resized.right,
|
||||
bottom: resized.bottom,
|
||||
}]
|
||||
} else if idx % 2 != 0 {
|
||||
let mut res = vec![Rect {
|
||||
left: resized.left,
|
||||
top: main_y,
|
||||
right: resized.right,
|
||||
bottom: half_resized_height,
|
||||
}];
|
||||
res.append(&mut recursive_fibonacci(
|
||||
idx + 1,
|
||||
count - 1,
|
||||
&Rect {
|
||||
left: area.left,
|
||||
top: alt_y,
|
||||
right: area.right,
|
||||
bottom: area.bottom - half_resized_height,
|
||||
},
|
||||
layout_flip,
|
||||
resize_adjustments,
|
||||
));
|
||||
res
|
||||
} else {
|
||||
let mut res = vec![Rect {
|
||||
left: main_x,
|
||||
top: resized.top,
|
||||
right: half_resized_width,
|
||||
bottom: resized.bottom,
|
||||
}];
|
||||
res.append(&mut recursive_fibonacci(
|
||||
idx + 1,
|
||||
count - 1,
|
||||
&Rect {
|
||||
left: alt_x,
|
||||
top: area.top,
|
||||
right: area.right - half_resized_width,
|
||||
bottom: area.bottom,
|
||||
},
|
||||
layout_flip,
|
||||
resize_adjustments,
|
||||
));
|
||||
res
|
||||
}
|
||||
}
|
||||
|
||||
@@ -11,18 +11,27 @@ use serde::Serialize;
|
||||
use strum::Display;
|
||||
use strum::EnumString;
|
||||
|
||||
pub use arrangement::Arrangement;
|
||||
pub use arrangement::Flip;
|
||||
pub use custom_layout::CustomLayout;
|
||||
pub use cycle_direction::CycleDirection;
|
||||
pub use layout::Flip;
|
||||
pub use default_layout::DefaultLayout;
|
||||
pub use direction::Direction;
|
||||
pub use layout::Layout;
|
||||
pub use operation_direction::OperationDirection;
|
||||
pub use rect::Rect;
|
||||
|
||||
pub mod arrangement;
|
||||
pub mod custom_layout;
|
||||
pub mod cycle_direction;
|
||||
pub mod default_layout;
|
||||
pub mod direction;
|
||||
pub mod layout;
|
||||
pub mod operation_direction;
|
||||
pub mod rect;
|
||||
|
||||
#[derive(Clone, Debug, Serialize, Deserialize, Display)]
|
||||
#[serde(tag = "type", content = "content")]
|
||||
pub enum SocketMessage {
|
||||
// Window / Container Commands
|
||||
FocusWindow(OperationDirection),
|
||||
@@ -46,7 +55,8 @@ pub enum SocketMessage {
|
||||
UnmanageFocusedWindow,
|
||||
AdjustContainerPadding(Sizing, i32),
|
||||
AdjustWorkspacePadding(Sizing, i32),
|
||||
ChangeLayout(Layout),
|
||||
ChangeLayout(DefaultLayout),
|
||||
ChangeLayoutCustom(PathBuf),
|
||||
FlipLayout(Flip),
|
||||
// Monitor and Workspace Commands
|
||||
EnsureWorkspaces(usize, usize),
|
||||
@@ -67,7 +77,8 @@ pub enum SocketMessage {
|
||||
WorkspacePadding(usize, usize, i32),
|
||||
WorkspaceTiling(usize, usize, bool),
|
||||
WorkspaceName(usize, usize, String),
|
||||
WorkspaceLayout(usize, usize, Layout),
|
||||
WorkspaceLayout(usize, usize, DefaultLayout),
|
||||
WorkspaceLayoutCustom(usize, usize, PathBuf),
|
||||
// Configuration
|
||||
ReloadConfiguration,
|
||||
WatchConfiguration(bool),
|
||||
@@ -78,20 +89,20 @@ pub enum SocketMessage {
|
||||
ManageRule(ApplicationIdentifier, String),
|
||||
IdentifyTrayApplication(ApplicationIdentifier, String),
|
||||
IdentifyBorderOverflow(ApplicationIdentifier, String),
|
||||
RemoveTitleBar(ApplicationIdentifier, String),
|
||||
ToggleTitleBars,
|
||||
State,
|
||||
Query(StateQuery),
|
||||
FocusFollowsMouse(FocusFollowsMouseImplementation, bool),
|
||||
ToggleFocusFollowsMouse(FocusFollowsMouseImplementation),
|
||||
AddSubscriber(String),
|
||||
RemoveSubscriber(String),
|
||||
}
|
||||
|
||||
impl SocketMessage {
|
||||
pub fn as_bytes(&self) -> Result<Vec<u8>> {
|
||||
Ok(serde_json::to_string(self)?.as_bytes().to_vec())
|
||||
}
|
||||
|
||||
pub fn from_bytes(bytes: &[u8]) -> Result<Self> {
|
||||
Ok(serde_json::from_slice(bytes)?)
|
||||
}
|
||||
}
|
||||
|
||||
impl FromStr for SocketMessage {
|
||||
|
||||
@@ -1,11 +1,13 @@
|
||||
use std::num::NonZeroUsize;
|
||||
|
||||
use clap::ArgEnum;
|
||||
use serde::Deserialize;
|
||||
use serde::Serialize;
|
||||
use strum::Display;
|
||||
use strum::EnumString;
|
||||
|
||||
use crate::direction::Direction;
|
||||
use crate::Flip;
|
||||
use crate::Layout;
|
||||
|
||||
#[derive(Clone, Copy, Debug, Serialize, Deserialize, Display, EnumString, ArgEnum)]
|
||||
#[strum(serialize_all = "snake_case")]
|
||||
@@ -27,123 +29,35 @@ impl OperationDirection {
|
||||
}
|
||||
}
|
||||
|
||||
fn flip_direction(direction: Self, layout_flip: Option<Flip>) -> Self {
|
||||
layout_flip.map_or(direction, |flip| match direction {
|
||||
fn flip(self, layout_flip: Option<Flip>) -> Self {
|
||||
layout_flip.map_or(self, |flip| match self {
|
||||
Self::Left => match flip {
|
||||
Flip::Horizontal | Flip::HorizontalAndVertical => Self::Right,
|
||||
Flip::Vertical => direction,
|
||||
Flip::Vertical => self,
|
||||
},
|
||||
Self::Right => match flip {
|
||||
Flip::Horizontal | Flip::HorizontalAndVertical => Self::Left,
|
||||
Flip::Vertical => direction,
|
||||
Flip::Vertical => self,
|
||||
},
|
||||
Self::Up => match flip {
|
||||
Flip::Vertical | Flip::HorizontalAndVertical => Self::Down,
|
||||
Flip::Horizontal => direction,
|
||||
Flip::Horizontal => self,
|
||||
},
|
||||
Self::Down => match flip {
|
||||
Flip::Vertical | Flip::HorizontalAndVertical => Self::Up,
|
||||
Flip::Horizontal => direction,
|
||||
Flip::Horizontal => self,
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
#[must_use]
|
||||
pub fn is_valid(
|
||||
pub fn destination(
|
||||
self,
|
||||
layout: Layout,
|
||||
layout: &dyn Direction,
|
||||
layout_flip: Option<Flip>,
|
||||
idx: usize,
|
||||
len: usize,
|
||||
) -> bool {
|
||||
match Self::flip_direction(self, layout_flip) {
|
||||
Self::Up => match layout {
|
||||
Layout::BSP => len > 2 && idx != 0 && idx != 1,
|
||||
Layout::Columns => false,
|
||||
Layout::Rows | Layout::HorizontalStack => idx != 0,
|
||||
Layout::VerticalStack => idx != 0 && idx != 1,
|
||||
Layout::UltrawideVerticalStack => idx > 2,
|
||||
},
|
||||
Self::Down => match layout {
|
||||
Layout::BSP => len > 2 && idx != len - 1 && idx % 2 != 0,
|
||||
Layout::Columns => false,
|
||||
Layout::Rows => idx != len - 1,
|
||||
Layout::VerticalStack => idx != 0 && idx != len - 1,
|
||||
Layout::HorizontalStack => idx == 0,
|
||||
Layout::UltrawideVerticalStack => idx > 1 && idx != len - 1,
|
||||
},
|
||||
Self::Left => match layout {
|
||||
Layout::BSP => len > 1 && idx != 0,
|
||||
Layout::Columns | Layout::VerticalStack => idx != 0,
|
||||
Layout::Rows => false,
|
||||
Layout::HorizontalStack => idx != 0 && idx != 1,
|
||||
Layout::UltrawideVerticalStack => len > 1 && idx != 1,
|
||||
},
|
||||
Self::Right => match layout {
|
||||
Layout::BSP => len > 1 && idx % 2 == 0 && idx != len - 1,
|
||||
Layout::Columns => idx != len - 1,
|
||||
Layout::Rows => false,
|
||||
Layout::VerticalStack => idx == 0,
|
||||
Layout::HorizontalStack => idx != 0 && idx != len - 1,
|
||||
Layout::UltrawideVerticalStack => match len {
|
||||
0 | 1 => false,
|
||||
2 => idx != 0,
|
||||
_ => idx < 2,
|
||||
},
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
#[must_use]
|
||||
pub fn new_idx(self, layout: Layout, layout_flip: Option<Flip>, idx: usize) -> usize {
|
||||
match Self::flip_direction(self, layout_flip) {
|
||||
Self::Up => match layout {
|
||||
Layout::BSP => {
|
||||
if idx % 2 == 0 {
|
||||
idx - 1
|
||||
} else {
|
||||
idx - 2
|
||||
}
|
||||
}
|
||||
Layout::Columns => unreachable!(),
|
||||
Layout::Rows | Layout::VerticalStack | Layout::UltrawideVerticalStack => idx - 1,
|
||||
Layout::HorizontalStack => 0,
|
||||
},
|
||||
Self::Down => match layout {
|
||||
Layout::BSP
|
||||
| Layout::Rows
|
||||
| Layout::VerticalStack
|
||||
| Layout::UltrawideVerticalStack => idx + 1,
|
||||
Layout::Columns => unreachable!(),
|
||||
Layout::HorizontalStack => 1,
|
||||
},
|
||||
Self::Left => match layout {
|
||||
Layout::BSP => {
|
||||
if idx % 2 == 0 {
|
||||
idx - 2
|
||||
} else {
|
||||
idx - 1
|
||||
}
|
||||
}
|
||||
Layout::Columns | Layout::HorizontalStack => idx - 1,
|
||||
Layout::Rows => unreachable!(),
|
||||
Layout::VerticalStack => 0,
|
||||
Layout::UltrawideVerticalStack => match idx {
|
||||
0 => 1,
|
||||
1 => unreachable!(),
|
||||
_ => 0,
|
||||
},
|
||||
},
|
||||
Self::Right => match layout {
|
||||
Layout::BSP | Layout::Columns | Layout::HorizontalStack => idx + 1,
|
||||
Layout::Rows => unreachable!(),
|
||||
Layout::VerticalStack => 1,
|
||||
Layout::UltrawideVerticalStack => match idx {
|
||||
1 => 0,
|
||||
0 => 2,
|
||||
_ => unreachable!(),
|
||||
},
|
||||
},
|
||||
}
|
||||
len: NonZeroUsize,
|
||||
) -> Option<usize> {
|
||||
layout.index_in_direction(self.flip(layout_flip), idx, len.get())
|
||||
}
|
||||
}
|
||||
|
||||
@@ -6,7 +6,7 @@ description = "A tiling window manager for Windows"
|
||||
categories = ["tiling-window-manager", "windows"]
|
||||
repository = "https://github.com/LGUG2Z/komorebi"
|
||||
license = "MIT"
|
||||
edition = "2018"
|
||||
edition = "2021"
|
||||
|
||||
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
|
||||
|
||||
@@ -38,6 +38,7 @@ uds_windows = "1"
|
||||
which = "4"
|
||||
winput = "0.2"
|
||||
winvd = "0.0.20"
|
||||
miow = "0.3"
|
||||
|
||||
[features]
|
||||
deadlock_detection = []
|
||||
|
||||
@@ -2,6 +2,8 @@
|
||||
#![allow(clippy::missing_errors_doc)]
|
||||
|
||||
use std::collections::HashMap;
|
||||
use std::fs::File;
|
||||
use std::io::Write;
|
||||
use std::process::Command;
|
||||
use std::sync::atomic::AtomicBool;
|
||||
use std::sync::atomic::Ordering;
|
||||
@@ -20,15 +22,19 @@ use lazy_static::lazy_static;
|
||||
#[cfg(feature = "deadlock_detection")]
|
||||
use parking_lot::deadlock;
|
||||
use parking_lot::Mutex;
|
||||
use serde::Serialize;
|
||||
use sysinfo::SystemExt;
|
||||
use tracing_appender::non_blocking::WorkerGuard;
|
||||
use tracing_subscriber::layer::SubscriberExt;
|
||||
use tracing_subscriber::EnvFilter;
|
||||
use which::which;
|
||||
|
||||
use komorebi_core::SocketMessage;
|
||||
|
||||
use crate::process_command::listen_for_commands;
|
||||
use crate::process_event::listen_for_events;
|
||||
use crate::process_movement::listen_for_movements;
|
||||
use crate::window_manager::State;
|
||||
use crate::window_manager::WindowManager;
|
||||
use crate::window_manager_event::WindowManagerEvent;
|
||||
use crate::windows_api::WindowsApi;
|
||||
@@ -74,9 +80,20 @@ lazy_static! {
|
||||
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 BORDER_OVERFLOW_IDENTIFIERS: Arc<Mutex<Vec<String>>> = Arc::new(Mutex::new(vec![]));
|
||||
static ref WSL2_UI_PROCESSES: Arc<Mutex<Vec<String>>> = Arc::new(Mutex::new(vec![
|
||||
"X410.exe".to_string(),
|
||||
"mstsc.exe".to_string(),
|
||||
"vcxsrv.exe".to_string(),
|
||||
]));
|
||||
static ref SUBSCRIPTION_PIPES: Arc<Mutex<HashMap<String, File>>> =
|
||||
Arc::new(Mutex::new(HashMap::new()));
|
||||
// Use app-specific titlebar removal options where possible
|
||||
// eg. Windows Terminal, IntelliJ IDEA, Firefox
|
||||
static ref NO_TITLEBAR: Arc<Mutex<Vec<String>>> = Arc::new(Mutex::new(vec![]));
|
||||
}
|
||||
|
||||
pub static CUSTOM_FFM: AtomicBool = AtomicBool::new(false);
|
||||
pub static REMOVE_TITLEBARS: AtomicBool = AtomicBool::new(false);
|
||||
|
||||
fn setup() -> Result<(WorkerGuard, WorkerGuard)> {
|
||||
if std::env::var("RUST_LIB_BACKTRACE").is_err() {
|
||||
@@ -177,6 +194,53 @@ pub fn load_configuration() -> Result<()> {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[derive(Debug, Serialize)]
|
||||
#[serde(untagged)]
|
||||
pub enum NotificationEvent {
|
||||
WindowManager(WindowManagerEvent),
|
||||
Socket(SocketMessage),
|
||||
}
|
||||
|
||||
#[derive(Debug, Serialize)]
|
||||
pub struct Notification {
|
||||
pub event: NotificationEvent,
|
||||
pub state: State,
|
||||
}
|
||||
|
||||
pub fn notify_subscribers(notification: &str) -> Result<()> {
|
||||
let mut stale_subscriptions = vec![];
|
||||
let mut subscriptions = SUBSCRIPTION_PIPES.lock();
|
||||
for (subscriber, pipe) in subscriptions.iter_mut() {
|
||||
match writeln!(pipe, "{}", notification) {
|
||||
Ok(_) => {
|
||||
tracing::debug!("pushed notification to subscriber: {}", subscriber);
|
||||
}
|
||||
Err(error) => {
|
||||
// ERROR_FILE_NOT_FOUND
|
||||
// 2 (0x2)
|
||||
// The system cannot find the file specified.
|
||||
|
||||
// ERROR_NO_DATA
|
||||
// 232 (0xE8)
|
||||
// The pipe is being closed.
|
||||
|
||||
// Remove the subscription; the process will have to subscribe again
|
||||
if let Some(2 | 232) = error.raw_os_error() {
|
||||
let subscriber_cl = subscriber.clone();
|
||||
stale_subscriptions.push(subscriber_cl);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
for subscriber in stale_subscriptions {
|
||||
tracing::warn!("removing stale subscription: {}", subscriber);
|
||||
subscriptions.remove(&subscriber);
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[cfg(feature = "deadlock_detection")]
|
||||
#[tracing::instrument]
|
||||
fn detect_deadlocks() {
|
||||
@@ -267,7 +331,7 @@ fn main() -> Result<()> {
|
||||
|
||||
tracing::error!("received ctrl-c, restoring all hidden windows and terminating process");
|
||||
|
||||
wm.lock().restore_all_windows();
|
||||
wm.lock().restore_all_windows()?;
|
||||
std::process::exit(130);
|
||||
}
|
||||
|
||||
|
||||
@@ -11,6 +11,7 @@ use std::thread;
|
||||
|
||||
use color_eyre::eyre::anyhow;
|
||||
use color_eyre::Result;
|
||||
use miow::pipe::connect;
|
||||
use parking_lot::Mutex;
|
||||
use uds_windows::UnixStream;
|
||||
|
||||
@@ -19,13 +20,19 @@ use komorebi_core::Rect;
|
||||
use komorebi_core::SocketMessage;
|
||||
use komorebi_core::StateQuery;
|
||||
|
||||
use crate::notify_subscribers;
|
||||
use crate::window_manager;
|
||||
use crate::window_manager::WindowManager;
|
||||
use crate::windows_api::WindowsApi;
|
||||
use crate::Notification;
|
||||
use crate::NotificationEvent;
|
||||
use crate::BORDER_OVERFLOW_IDENTIFIERS;
|
||||
use crate::CUSTOM_FFM;
|
||||
use crate::FLOAT_IDENTIFIERS;
|
||||
use crate::MANAGE_IDENTIFIERS;
|
||||
use crate::NO_TITLEBAR;
|
||||
use crate::REMOVE_TITLEBARS;
|
||||
use crate::SUBSCRIPTION_PIPES;
|
||||
use crate::TRAY_AND_MULTI_WINDOW_IDENTIFIERS;
|
||||
use crate::WORKSPACE_RULES;
|
||||
|
||||
@@ -153,12 +160,16 @@ impl WindowManager {
|
||||
}
|
||||
SocketMessage::Retile => self.retile_all()?,
|
||||
SocketMessage::FlipLayout(layout_flip) => self.flip_layout(layout_flip)?,
|
||||
SocketMessage::ChangeLayout(layout) => self.change_workspace_layout(layout)?,
|
||||
SocketMessage::ChangeLayout(layout) => self.change_workspace_layout_default(layout)?,
|
||||
SocketMessage::ChangeLayoutCustom(path) => self.change_workspace_custom_layout(path)?,
|
||||
SocketMessage::WorkspaceLayoutCustom(monitor_idx, workspace_idx, path) => {
|
||||
self.set_workspace_layout_custom(monitor_idx, workspace_idx, path)?;
|
||||
}
|
||||
SocketMessage::WorkspaceTiling(monitor_idx, workspace_idx, tile) => {
|
||||
self.set_workspace_tiling(monitor_idx, workspace_idx, tile)?;
|
||||
}
|
||||
SocketMessage::WorkspaceLayout(monitor_idx, workspace_idx, layout) => {
|
||||
self.set_workspace_layout(monitor_idx, workspace_idx, layout)?;
|
||||
self.set_workspace_layout_default(monitor_idx, workspace_idx, layout)?;
|
||||
}
|
||||
SocketMessage::CycleFocusWorkspace(direction) => {
|
||||
// This is to ensure that even on an empty workspace on a secondary monitor, the
|
||||
@@ -201,7 +212,7 @@ impl WindowManager {
|
||||
tracing::info!(
|
||||
"received stop command, restoring all hidden windows and terminating process"
|
||||
);
|
||||
self.restore_all_windows();
|
||||
self.restore_all_windows()?;
|
||||
std::process::exit(0)
|
||||
}
|
||||
SocketMessage::EnsureWorkspaces(monitor_idx, workspace_count) => {
|
||||
@@ -214,7 +225,7 @@ impl WindowManager {
|
||||
self.set_workspace_name(monitor_idx, workspace_idx, name)?;
|
||||
}
|
||||
SocketMessage::State => {
|
||||
let state = serde_json::to_string_pretty(&window_manager::State::from(self))?;
|
||||
let state = serde_json::to_string_pretty(&window_manager::State::from(&*self))?;
|
||||
let mut socket =
|
||||
dirs::home_dir().ok_or_else(|| anyhow!("there is no home directory"))?;
|
||||
socket.push("komorebic.sock");
|
||||
@@ -431,6 +442,30 @@ impl WindowManager {
|
||||
workspace.set_resize_dimensions(resize);
|
||||
self.update_focused_workspace(false)?;
|
||||
}
|
||||
SocketMessage::AddSubscriber(subscriber) => {
|
||||
let mut pipes = SUBSCRIPTION_PIPES.lock();
|
||||
let pipe_path = format!(r"\\.\pipe\{}", subscriber);
|
||||
let pipe = connect(&pipe_path).map_err(|_| {
|
||||
anyhow!("the named pipe '{}' has not yet been created; please create it before running this command", pipe_path)
|
||||
})?;
|
||||
|
||||
pipes.insert(subscriber, pipe);
|
||||
}
|
||||
SocketMessage::RemoveSubscriber(subscriber) => {
|
||||
let mut pipes = SUBSCRIPTION_PIPES.lock();
|
||||
pipes.remove(&subscriber);
|
||||
}
|
||||
SocketMessage::RemoveTitleBar(_, id) => {
|
||||
let mut identifiers = NO_TITLEBAR.lock();
|
||||
if !identifiers.contains(&id) {
|
||||
identifiers.push(id);
|
||||
}
|
||||
}
|
||||
SocketMessage::ToggleTitleBars => {
|
||||
let current = REMOVE_TITLEBARS.load(Ordering::SeqCst);
|
||||
REMOVE_TITLEBARS.store(!current, Ordering::SeqCst);
|
||||
self.update_focused_workspace(false)?;
|
||||
}
|
||||
};
|
||||
|
||||
tracing::info!("processed");
|
||||
@@ -455,7 +490,11 @@ impl WindowManager {
|
||||
};
|
||||
}
|
||||
|
||||
self.process_command(message)?;
|
||||
self.process_command(message.clone())?;
|
||||
notify_subscribers(&serde_json::to_string(&Notification {
|
||||
event: NotificationEvent::Socket(message.clone()),
|
||||
state: (&*self).into(),
|
||||
})?)?;
|
||||
}
|
||||
|
||||
Ok(())
|
||||
|
||||
@@ -11,9 +11,12 @@ use komorebi_core::OperationDirection;
|
||||
use komorebi_core::Rect;
|
||||
use komorebi_core::Sizing;
|
||||
|
||||
use crate::notify_subscribers;
|
||||
use crate::window_manager::WindowManager;
|
||||
use crate::window_manager_event::WindowManagerEvent;
|
||||
use crate::windows_api::WindowsApi;
|
||||
use crate::Notification;
|
||||
use crate::NotificationEvent;
|
||||
use crate::HIDDEN_HWNDS;
|
||||
use crate::TRAY_AND_MULTI_WINDOW_IDENTIFIERS;
|
||||
|
||||
@@ -120,10 +123,10 @@ impl WindowManager {
|
||||
// they are not on the top of a container stack.
|
||||
let programmatically_hidden_hwnds = HIDDEN_HWNDS.lock();
|
||||
|
||||
if (!window.is_window()
|
||||
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)
|
||||
|| tray_and_multi_window_identifiers.contains(&window.class()?))
|
||||
&& !programmatically_hidden_hwnds.contains(&window.hwnd)
|
||||
{
|
||||
hide = true;
|
||||
}
|
||||
@@ -316,6 +319,10 @@ impl WindowManager {
|
||||
.open(hwnd_json)?;
|
||||
|
||||
serde_json::to_writer_pretty(&file, &known_hwnds)?;
|
||||
notify_subscribers(&serde_json::to_string(&Notification {
|
||||
event: NotificationEvent::WindowManager(*event),
|
||||
state: (&*self).into(),
|
||||
})?)?;
|
||||
|
||||
tracing::info!("processed: {}", event.window().to_string());
|
||||
Ok(())
|
||||
|
||||
@@ -4,6 +4,7 @@ use std::fmt::Formatter;
|
||||
|
||||
use color_eyre::eyre::anyhow;
|
||||
use color_eyre::Result;
|
||||
use serde::ser::Error;
|
||||
use serde::ser::SerializeStruct;
|
||||
use serde::Serialize;
|
||||
use serde::Serializer;
|
||||
@@ -20,6 +21,8 @@ use crate::FLOAT_IDENTIFIERS;
|
||||
use crate::HIDDEN_HWNDS;
|
||||
use crate::LAYERED_EXE_WHITELIST;
|
||||
use crate::MANAGE_IDENTIFIERS;
|
||||
use crate::NO_TITLEBAR;
|
||||
use crate::WSL2_UI_PROCESSES;
|
||||
|
||||
#[derive(Debug, Clone, Copy)]
|
||||
pub struct Window {
|
||||
@@ -55,12 +58,28 @@ impl Serialize for Window {
|
||||
{
|
||||
let mut state = serializer.serialize_struct("Window", 5)?;
|
||||
state.serialize_field("hwnd", &self.hwnd)?;
|
||||
state.serialize_field("title", &self.title().expect("could not get window title"))?;
|
||||
state.serialize_field("exe", &self.exe().expect("could not get window exe"))?;
|
||||
state.serialize_field("class", &self.class().expect("could not get window class"))?;
|
||||
state.serialize_field(
|
||||
"title",
|
||||
&self
|
||||
.title()
|
||||
.map_err(|_| S::Error::custom("could not get window title"))?,
|
||||
)?;
|
||||
state.serialize_field(
|
||||
"exe",
|
||||
&self
|
||||
.exe()
|
||||
.map_err(|_| S::Error::custom("could not get window exe"))?,
|
||||
)?;
|
||||
state.serialize_field(
|
||||
"class",
|
||||
&self
|
||||
.class()
|
||||
.map_err(|_| S::Error::custom("could not get window class"))?,
|
||||
)?;
|
||||
state.serialize_field(
|
||||
"rect",
|
||||
&WindowsApi::window_rect(self.hwnd()).expect("could not get window rect"),
|
||||
&WindowsApi::window_rect(self.hwnd())
|
||||
.map_err(|_| S::Error::custom("could not get window rect"))?,
|
||||
)?;
|
||||
state.end()
|
||||
}
|
||||
@@ -193,6 +212,20 @@ impl Window {
|
||||
WindowsApi::set_focus(self.hwnd())
|
||||
}
|
||||
|
||||
pub fn remove_title_bar(self) -> Result<()> {
|
||||
let mut style = self.style()?;
|
||||
style.remove(GwlStyle::CAPTION);
|
||||
style.remove(GwlStyle::THICKFRAME);
|
||||
self.update_style(style)
|
||||
}
|
||||
|
||||
pub fn add_title_bar(self) -> Result<()> {
|
||||
let mut style = self.style()?;
|
||||
style.insert(GwlStyle::CAPTION);
|
||||
style.insert(GwlStyle::THICKFRAME);
|
||||
self.update_style(style)
|
||||
}
|
||||
|
||||
#[allow(dead_code)]
|
||||
pub fn update_style(self, style: GwlStyle) -> Result<()> {
|
||||
WindowsApi::update_style(self.hwnd(), isize::try_from(style.bits())?)
|
||||
@@ -235,6 +268,7 @@ impl Window {
|
||||
return Ok(true);
|
||||
}
|
||||
|
||||
#[allow(clippy::question_mark)]
|
||||
if self.title().is_err() {
|
||||
return Ok(false);
|
||||
}
|
||||
@@ -271,11 +305,24 @@ impl Window {
|
||||
layered_exe_whitelist.contains(&exe_name)
|
||||
};
|
||||
|
||||
let allow_wsl2_gui = {
|
||||
let wsl2_ui_processes = WSL2_UI_PROCESSES.lock();
|
||||
wsl2_ui_processes.contains(&exe_name)
|
||||
};
|
||||
|
||||
let allow_titlebar_removed = {
|
||||
let titlebars_removed = NO_TITLEBAR.lock();
|
||||
titlebars_removed.contains(&exe_name)
|
||||
};
|
||||
|
||||
let style = self.style()?;
|
||||
let ex_style = self.ex_style()?;
|
||||
|
||||
if style.contains(GwlStyle::CAPTION)
|
||||
&& ex_style.contains(GwlExStyle::WINDOWEDGE)
|
||||
if (
|
||||
allow_wsl2_gui
|
||||
|| allow_titlebar_removed
|
||||
|| style.contains(GwlStyle::CAPTION) && ex_style.contains(GwlExStyle::WINDOWEDGE)
|
||||
)
|
||||
&& !ex_style.contains(GwlExStyle::DLGMODALFRAME)
|
||||
// Get a lot of dupe events coming through that make the redrawing go crazy
|
||||
// on FocusChange events if I don't filter out this one. But, if we are
|
||||
|
||||
@@ -2,6 +2,7 @@ use std::collections::VecDeque;
|
||||
use std::io::ErrorKind;
|
||||
use std::num::NonZeroUsize;
|
||||
use std::path::PathBuf;
|
||||
use std::sync::atomic::Ordering;
|
||||
use std::sync::Arc;
|
||||
use std::thread;
|
||||
|
||||
@@ -14,7 +15,10 @@ use parking_lot::Mutex;
|
||||
use serde::Serialize;
|
||||
use uds_windows::UnixListener;
|
||||
|
||||
use komorebi_core::custom_layout::CustomLayout;
|
||||
use komorebi_core::Arrangement;
|
||||
use komorebi_core::CycleDirection;
|
||||
use komorebi_core::DefaultLayout;
|
||||
use komorebi_core::Flip;
|
||||
use komorebi_core::FocusFollowsMouseImplementation;
|
||||
use komorebi_core::Layout;
|
||||
@@ -35,6 +39,8 @@ use crate::BORDER_OVERFLOW_IDENTIFIERS;
|
||||
use crate::FLOAT_IDENTIFIERS;
|
||||
use crate::LAYERED_EXE_WHITELIST;
|
||||
use crate::MANAGE_IDENTIFIERS;
|
||||
use crate::NO_TITLEBAR;
|
||||
use crate::REMOVE_TITLEBARS;
|
||||
use crate::TRAY_AND_MULTI_WINDOW_IDENTIFIERS;
|
||||
use crate::WORKSPACE_RULES;
|
||||
|
||||
@@ -60,6 +66,7 @@ pub struct State {
|
||||
pub work_area_offset: Option<Rect>,
|
||||
pub focus_follows_mouse: Option<FocusFollowsMouseImplementation>,
|
||||
pub has_pending_raise_op: bool,
|
||||
pub remove_titlebars: bool,
|
||||
pub float_identifiers: Vec<String>,
|
||||
pub manage_identifiers: Vec<String>,
|
||||
pub layered_exe_whitelist: Vec<String>,
|
||||
@@ -67,9 +74,8 @@ pub struct State {
|
||||
pub border_overflow_identifiers: Vec<String>,
|
||||
}
|
||||
|
||||
#[allow(clippy::fallible_impl_from)]
|
||||
impl From<&mut WindowManager> for State {
|
||||
fn from(wm: &mut WindowManager) -> Self {
|
||||
impl From<&WindowManager> for State {
|
||||
fn from(wm: &WindowManager) -> Self {
|
||||
Self {
|
||||
monitors: wm.monitors.clone(),
|
||||
is_paused: wm.is_paused,
|
||||
@@ -77,6 +83,7 @@ impl From<&mut WindowManager> for State {
|
||||
work_area_offset: wm.work_area_offset,
|
||||
focus_follows_mouse: wm.focus_follows_mouse.clone(),
|
||||
has_pending_raise_op: wm.has_pending_raise_op,
|
||||
remove_titlebars: REMOVE_TITLEBARS.load(Ordering::SeqCst),
|
||||
float_identifiers: FLOAT_IDENTIFIERS.lock().clone(),
|
||||
manage_identifiers: MANAGE_IDENTIFIERS.lock().clone(),
|
||||
layered_exe_whitelist: LAYERED_EXE_WHITELIST.lock().clone(),
|
||||
@@ -564,8 +571,12 @@ impl WindowManager {
|
||||
// Calling this directly instead of the window.focus() wrapper because trying to
|
||||
// attach to the thread of the desktop window always seems to result in "Access is
|
||||
// denied (os error 5)"
|
||||
WindowsApi::set_foreground_window(desktop_window.hwnd())
|
||||
.map_err(|error| anyhow!("{} {}:{}", error, file!(), line!()))?;
|
||||
match WindowsApi::set_foreground_window(desktop_window.hwnd()) {
|
||||
Ok(_) => {}
|
||||
Err(error) => {
|
||||
tracing::warn!("{} {}:{}", error, file!(), line!());
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -579,88 +590,105 @@ impl WindowManager {
|
||||
sizing: Sizing,
|
||||
step: Option<i32>,
|
||||
) -> Result<()> {
|
||||
tracing::info!("resizing window");
|
||||
|
||||
let work_area = self.focused_monitor_work_area()?;
|
||||
let workspace = self.focused_workspace_mut()?;
|
||||
let len = workspace.containers().len();
|
||||
let focused_idx = workspace.focused_container_idx();
|
||||
let focused_idx_resize = workspace
|
||||
.resize_dimensions()
|
||||
.get(focused_idx)
|
||||
.ok_or_else(|| anyhow!("there is no resize adjustment for this container"))?;
|
||||
|
||||
if direction.is_valid(
|
||||
workspace.layout(),
|
||||
workspace.layout_flip(),
|
||||
focused_idx,
|
||||
len,
|
||||
) {
|
||||
let unaltered = workspace.layout().calculate(
|
||||
&work_area,
|
||||
NonZeroUsize::new(len).ok_or_else(|| {
|
||||
anyhow!("there must be at least one container to calculate a workspace layout")
|
||||
})?,
|
||||
workspace.container_padding(),
|
||||
workspace.layout_flip(),
|
||||
&[],
|
||||
);
|
||||
|
||||
let mut direction = direction;
|
||||
|
||||
// We only ever want to operate on the unflipped Rect positions when resizing, then we
|
||||
// can flip them however they need to be flipped once the resizing has been done
|
||||
if let Some(flip) = workspace.layout_flip() {
|
||||
match flip {
|
||||
Flip::Horizontal => {
|
||||
if matches!(direction, OperationDirection::Left)
|
||||
|| matches!(direction, OperationDirection::Right)
|
||||
{
|
||||
direction = direction.opposite();
|
||||
}
|
||||
}
|
||||
Flip::Vertical => {
|
||||
if matches!(direction, OperationDirection::Up)
|
||||
|| matches!(direction, OperationDirection::Down)
|
||||
{
|
||||
direction = direction.opposite();
|
||||
}
|
||||
}
|
||||
Flip::HorizontalAndVertical => direction = direction.opposite(),
|
||||
}
|
||||
}
|
||||
|
||||
let resize = workspace.layout().resize(
|
||||
unaltered
|
||||
match workspace.layout() {
|
||||
Layout::Default(layout) => {
|
||||
tracing::info!("resizing window");
|
||||
let len = NonZeroUsize::new(workspace.containers().len())
|
||||
.ok_or_else(|| anyhow!("there must be at least one container"))?;
|
||||
let focused_idx = workspace.focused_container_idx();
|
||||
let focused_idx_resize = workspace
|
||||
.resize_dimensions()
|
||||
.get(focused_idx)
|
||||
.ok_or_else(|| anyhow!("there is no last layout"))?,
|
||||
focused_idx_resize,
|
||||
direction,
|
||||
sizing,
|
||||
step,
|
||||
);
|
||||
.ok_or_else(|| anyhow!("there is no resize adjustment for this container"))?;
|
||||
|
||||
workspace.resize_dimensions_mut()[focused_idx] = resize;
|
||||
self.update_focused_workspace(false)
|
||||
} else {
|
||||
tracing::warn!("cannot resize container in this direction");
|
||||
Ok(())
|
||||
if direction
|
||||
.destination(
|
||||
workspace.layout().as_boxed_direction().as_ref(),
|
||||
workspace.layout_flip(),
|
||||
focused_idx,
|
||||
len,
|
||||
)
|
||||
.is_some()
|
||||
{
|
||||
let unaltered = layout.calculate(
|
||||
&work_area,
|
||||
len,
|
||||
workspace.container_padding(),
|
||||
workspace.layout_flip(),
|
||||
&[],
|
||||
);
|
||||
|
||||
let mut direction = direction;
|
||||
|
||||
// We only ever want to operate on the unflipped Rect positions when resizing, then we
|
||||
// can flip them however they need to be flipped once the resizing has been done
|
||||
if let Some(flip) = workspace.layout_flip() {
|
||||
match flip {
|
||||
Flip::Horizontal => {
|
||||
if matches!(direction, OperationDirection::Left)
|
||||
|| matches!(direction, OperationDirection::Right)
|
||||
{
|
||||
direction = direction.opposite();
|
||||
}
|
||||
}
|
||||
Flip::Vertical => {
|
||||
if matches!(direction, OperationDirection::Up)
|
||||
|| matches!(direction, OperationDirection::Down)
|
||||
{
|
||||
direction = direction.opposite();
|
||||
}
|
||||
}
|
||||
Flip::HorizontalAndVertical => direction = direction.opposite(),
|
||||
}
|
||||
}
|
||||
|
||||
let resize = layout.resize(
|
||||
unaltered
|
||||
.get(focused_idx)
|
||||
.ok_or_else(|| anyhow!("there is no last layout"))?,
|
||||
focused_idx_resize,
|
||||
direction,
|
||||
sizing,
|
||||
step,
|
||||
);
|
||||
|
||||
workspace.resize_dimensions_mut()[focused_idx] = resize;
|
||||
return self.update_focused_workspace(false);
|
||||
}
|
||||
|
||||
tracing::warn!("cannot resize container in this direction");
|
||||
}
|
||||
Layout::Custom(_) => {
|
||||
tracing::warn!("containers cannot be resized when using custom layouts");
|
||||
}
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[tracing::instrument(skip(self))]
|
||||
pub fn restore_all_windows(&mut self) {
|
||||
pub fn restore_all_windows(&mut self) -> Result<()> {
|
||||
tracing::info!("restoring all hidden windows");
|
||||
|
||||
let no_titlebar = NO_TITLEBAR.lock();
|
||||
|
||||
for monitor in self.monitors_mut() {
|
||||
for workspace in monitor.workspaces_mut() {
|
||||
for containers in workspace.containers_mut() {
|
||||
for window in containers.windows_mut() {
|
||||
if no_titlebar.contains(&window.exe()?) {
|
||||
window.add_title_bar()?;
|
||||
}
|
||||
|
||||
window.restore();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[tracing::instrument(skip(self))]
|
||||
@@ -806,14 +834,18 @@ impl WindowManager {
|
||||
tracing::info!("adding window to container");
|
||||
|
||||
let workspace = self.focused_workspace_mut()?;
|
||||
let len = NonZeroUsize::new(workspace.containers_mut().len())
|
||||
.ok_or_else(|| anyhow!("there must be at least one container"))?;
|
||||
let current_container_idx = workspace.focused_container_idx();
|
||||
|
||||
let is_valid = direction.is_valid(
|
||||
workspace.layout(),
|
||||
workspace.layout_flip(),
|
||||
workspace.focused_container_idx(),
|
||||
workspace.containers_mut().len(),
|
||||
);
|
||||
let is_valid = direction
|
||||
.destination(
|
||||
workspace.layout().as_boxed_direction().as_ref(),
|
||||
workspace.layout_flip(),
|
||||
workspace.focused_container_idx(),
|
||||
len,
|
||||
)
|
||||
.is_some();
|
||||
|
||||
if is_valid {
|
||||
let new_idx = workspace.new_idx_for_direction(direction).ok_or_else(|| {
|
||||
@@ -859,7 +891,7 @@ impl WindowManager {
|
||||
#[tracing::instrument(skip(self))]
|
||||
pub fn toggle_tiling(&mut self) -> Result<()> {
|
||||
let workspace = self.focused_workspace_mut()?;
|
||||
workspace.set_tile(!workspace.tile());
|
||||
workspace.set_tile(!*workspace.tile());
|
||||
self.update_focused_workspace(false)
|
||||
}
|
||||
|
||||
@@ -1014,12 +1046,54 @@ impl WindowManager {
|
||||
}
|
||||
|
||||
#[tracing::instrument(skip(self))]
|
||||
pub fn change_workspace_layout(&mut self, layout: Layout) -> Result<()> {
|
||||
pub fn change_workspace_layout_default(&mut self, layout: DefaultLayout) -> Result<()> {
|
||||
tracing::info!("changing layout");
|
||||
|
||||
let workspace = self.focused_workspace_mut()?;
|
||||
workspace.set_layout(layout);
|
||||
self.update_focused_workspace(false)
|
||||
|
||||
match workspace.layout() {
|
||||
Layout::Default(_) => {}
|
||||
Layout::Custom(layout) => {
|
||||
let primary_idx =
|
||||
layout.first_container_idx(layout.primary_idx().ok_or_else(|| {
|
||||
anyhow!("this custom layout does not have a primary column")
|
||||
})?);
|
||||
|
||||
if !workspace.containers().is_empty() && primary_idx < workspace.containers().len()
|
||||
{
|
||||
workspace.swap_containers(0, primary_idx);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
workspace.set_layout(Layout::Default(layout));
|
||||
self.update_focused_workspace(true)
|
||||
}
|
||||
|
||||
#[tracing::instrument(skip(self))]
|
||||
pub fn change_workspace_custom_layout(&mut self, path: PathBuf) -> Result<()> {
|
||||
tracing::info!("changing layout");
|
||||
|
||||
let layout = CustomLayout::from_path_buf(path)?;
|
||||
let workspace = self.focused_workspace_mut()?;
|
||||
|
||||
match workspace.layout() {
|
||||
Layout::Default(_) => {
|
||||
let primary_idx =
|
||||
layout.first_container_idx(layout.primary_idx().ok_or_else(|| {
|
||||
anyhow!("this custom layout does not have a primary column")
|
||||
})?);
|
||||
|
||||
if !workspace.containers().is_empty() && primary_idx < workspace.containers().len()
|
||||
{
|
||||
workspace.swap_containers(0, primary_idx);
|
||||
}
|
||||
}
|
||||
Layout::Custom(_) => {}
|
||||
}
|
||||
|
||||
workspace.set_layout(Layout::Custom(layout));
|
||||
self.update_focused_workspace(true)
|
||||
}
|
||||
|
||||
#[tracing::instrument(skip(self))]
|
||||
@@ -1075,11 +1149,11 @@ impl WindowManager {
|
||||
}
|
||||
|
||||
#[tracing::instrument(skip(self))]
|
||||
pub fn set_workspace_layout(
|
||||
pub fn set_workspace_layout_default(
|
||||
&mut self,
|
||||
monitor_idx: usize,
|
||||
workspace_idx: usize,
|
||||
layout: Layout,
|
||||
layout: DefaultLayout,
|
||||
) -> Result<()> {
|
||||
tracing::info!("setting workspace layout");
|
||||
|
||||
@@ -1100,7 +1174,44 @@ impl WindowManager {
|
||||
.get_mut(workspace_idx)
|
||||
.ok_or_else(|| anyhow!("there is no monitor"))?;
|
||||
|
||||
workspace.set_layout(layout);
|
||||
workspace.set_layout(Layout::Default(layout));
|
||||
|
||||
// If this is the focused workspace on a non-focused screen, let's update it
|
||||
if focused_monitor_idx != monitor_idx && focused_workspace_idx == workspace_idx {
|
||||
workspace.update(&work_area, offset, &invisible_borders)?;
|
||||
Ok(())
|
||||
} else {
|
||||
Ok(self.update_focused_workspace(false)?)
|
||||
}
|
||||
}
|
||||
|
||||
#[tracing::instrument(skip(self))]
|
||||
pub fn set_workspace_layout_custom(
|
||||
&mut self,
|
||||
monitor_idx: usize,
|
||||
workspace_idx: usize,
|
||||
path: PathBuf,
|
||||
) -> Result<()> {
|
||||
tracing::info!("setting workspace layout");
|
||||
let layout = CustomLayout::from_path_buf(path)?;
|
||||
let invisible_borders = self.invisible_borders;
|
||||
let offset = self.work_area_offset;
|
||||
let focused_monitor_idx = self.focused_monitor_idx();
|
||||
|
||||
let monitor = self
|
||||
.monitors_mut()
|
||||
.get_mut(monitor_idx)
|
||||
.ok_or_else(|| anyhow!("there is no monitor"))?;
|
||||
|
||||
let work_area = *monitor.work_area_size();
|
||||
let focused_workspace_idx = monitor.focused_workspace_idx();
|
||||
|
||||
let workspace = monitor
|
||||
.workspaces_mut()
|
||||
.get_mut(workspace_idx)
|
||||
.ok_or_else(|| anyhow!("there is no monitor"))?;
|
||||
|
||||
workspace.set_layout(Layout::Custom(layout));
|
||||
|
||||
// If this is the focused workspace on a non-focused screen, let's update it
|
||||
if focused_monitor_idx != monitor_idx && focused_workspace_idx == workspace_idx {
|
||||
|
||||
@@ -1,11 +1,14 @@
|
||||
use std::fmt::Display;
|
||||
use std::fmt::Formatter;
|
||||
|
||||
use serde::Serialize;
|
||||
|
||||
use crate::window::Window;
|
||||
use crate::winevent::WinEvent;
|
||||
use crate::OBJECT_NAME_CHANGE_ON_LAUNCH;
|
||||
|
||||
#[derive(Debug, Copy, Clone)]
|
||||
#[derive(Debug, Copy, Clone, Serialize)]
|
||||
#[serde(tag = "type", content = "content")]
|
||||
pub enum WindowManagerEvent {
|
||||
Destroy(WinEvent, Window),
|
||||
FocusChange(WinEvent, Window),
|
||||
|
||||
@@ -408,9 +408,9 @@ impl WindowsApi {
|
||||
}
|
||||
|
||||
fn window_long_ptr_w(hwnd: HWND, index: WINDOW_LONG_PTR_INDEX) -> Result<isize> {
|
||||
Result::from(WindowsResult::from(unsafe {
|
||||
GetWindowLongPtrW(hwnd, index)
|
||||
}))
|
||||
// Can return 0, which does not always mean that an error has occurred
|
||||
// https://docs.microsoft.com/en-us/windows/win32/api/winuser/nf-winuser-getwindowlongptrw
|
||||
Result::from(unsafe { WindowsResult::Ok(GetWindowLongPtrW(hwnd, index)) })
|
||||
}
|
||||
|
||||
#[allow(dead_code)]
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
use serde::Serialize;
|
||||
use strum::Display;
|
||||
|
||||
use bindings::Windows::Win32::UI::WindowsAndMessaging::EVENT_AIA_END;
|
||||
@@ -85,7 +86,7 @@ use bindings::Windows::Win32::UI::WindowsAndMessaging::EVENT_UIA_EVENTID_START;
|
||||
use bindings::Windows::Win32::UI::WindowsAndMessaging::EVENT_UIA_PROPID_END;
|
||||
use bindings::Windows::Win32::UI::WindowsAndMessaging::EVENT_UIA_PROPID_START;
|
||||
|
||||
#[derive(Clone, Copy, PartialEq, Debug, Display)]
|
||||
#[derive(Clone, Copy, PartialEq, Debug, Serialize, Display)]
|
||||
#[repr(u32)]
|
||||
#[allow(dead_code)]
|
||||
pub enum WinEvent {
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
use std::collections::VecDeque;
|
||||
use std::num::NonZeroUsize;
|
||||
use std::sync::atomic::Ordering;
|
||||
|
||||
use color_eyre::eyre::anyhow;
|
||||
use color_eyre::Result;
|
||||
@@ -10,6 +11,7 @@ use getset::Setters;
|
||||
use serde::Serialize;
|
||||
|
||||
use komorebi_core::CycleDirection;
|
||||
use komorebi_core::DefaultLayout;
|
||||
use komorebi_core::Flip;
|
||||
use komorebi_core::Layout;
|
||||
use komorebi_core::OperationDirection;
|
||||
@@ -19,6 +21,8 @@ use crate::container::Container;
|
||||
use crate::ring::Ring;
|
||||
use crate::window::Window;
|
||||
use crate::windows_api::WindowsApi;
|
||||
use crate::NO_TITLEBAR;
|
||||
use crate::REMOVE_TITLEBARS;
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Getters, CopyGetters, MutGetters, Setters)]
|
||||
pub struct Workspace {
|
||||
@@ -37,7 +41,7 @@ pub struct Workspace {
|
||||
maximized_window_restore_idx: Option<usize>,
|
||||
#[getset(get = "pub", get_mut = "pub")]
|
||||
floating_windows: Vec<Window>,
|
||||
#[getset(get_copy = "pub", set = "pub")]
|
||||
#[getset(get = "pub", set = "pub")]
|
||||
layout: Layout,
|
||||
#[getset(get_copy = "pub", set = "pub")]
|
||||
layout_flip: Option<Flip>,
|
||||
@@ -66,7 +70,7 @@ impl Default for Workspace {
|
||||
maximized_window_restore_idx: None,
|
||||
monocle_container_restore_idx: None,
|
||||
floating_windows: Vec::default(),
|
||||
layout: Layout::BSP,
|
||||
layout: Layout::Default(DefaultLayout::BSP),
|
||||
layout_flip: None,
|
||||
workspace_padding: Option::from(10),
|
||||
container_padding: Option::from(10),
|
||||
@@ -171,7 +175,7 @@ impl Workspace {
|
||||
} else if let Some(window) = self.maximized_window_mut() {
|
||||
window.maximize();
|
||||
} else if !self.containers().is_empty() {
|
||||
let layouts = self.layout().calculate(
|
||||
let layouts = self.layout().as_boxed_arrangement().calculate(
|
||||
&adjusted_work_area,
|
||||
NonZeroUsize::new(self.containers().len()).ok_or_else(|| {
|
||||
anyhow!(
|
||||
@@ -183,9 +187,18 @@ impl Workspace {
|
||||
self.resize_dimensions(),
|
||||
);
|
||||
|
||||
let should_remove_titlebars = REMOVE_TITLEBARS.load(Ordering::SeqCst);
|
||||
let no_titlebar = { NO_TITLEBAR.lock().clone() };
|
||||
|
||||
let windows = self.visible_windows_mut();
|
||||
for (i, window) in windows.into_iter().enumerate() {
|
||||
if let (Some(window), Some(layout)) = (window, layouts.get(i)) {
|
||||
if should_remove_titlebars && no_titlebar.contains(&window.exe()?) {
|
||||
window.remove_title_bar()?;
|
||||
} else if no_titlebar.contains(&window.exe()?) {
|
||||
window.add_title_bar()?;
|
||||
}
|
||||
|
||||
window.set_position(layout, invisible_borders, false)?;
|
||||
}
|
||||
}
|
||||
@@ -349,10 +362,19 @@ impl Workspace {
|
||||
.remove_focused_container()
|
||||
.ok_or_else(|| anyhow!("there is no container"))?;
|
||||
|
||||
self.containers_mut().push_front(container);
|
||||
self.resize_dimensions_mut().insert(0, resize);
|
||||
let primary_idx = match self.layout() {
|
||||
Layout::Default(_) => 0,
|
||||
Layout::Custom(layout) => layout.first_container_idx(
|
||||
layout
|
||||
.primary_idx()
|
||||
.ok_or_else(|| anyhow!("this custom layout does not have a primary column"))?,
|
||||
),
|
||||
};
|
||||
|
||||
self.focus_container(0);
|
||||
self.containers_mut().insert(primary_idx, container);
|
||||
self.resize_dimensions_mut().insert(primary_idx, resize);
|
||||
|
||||
self.focus_container(primary_idx);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
@@ -462,20 +484,14 @@ impl Workspace {
|
||||
}
|
||||
|
||||
pub fn new_idx_for_direction(&self, direction: OperationDirection) -> Option<usize> {
|
||||
if direction.is_valid(
|
||||
self.layout(),
|
||||
let len = NonZeroUsize::new(self.containers().len())?;
|
||||
|
||||
direction.destination(
|
||||
self.layout().as_boxed_direction().as_ref(),
|
||||
self.layout_flip(),
|
||||
self.focused_container_idx(),
|
||||
self.containers().len(),
|
||||
) {
|
||||
Option::from(direction.new_idx(
|
||||
self.layout(),
|
||||
self.layout_flip(),
|
||||
self.containers.focused_idx(),
|
||||
))
|
||||
} else {
|
||||
None
|
||||
}
|
||||
len,
|
||||
)
|
||||
}
|
||||
pub fn new_idx_for_cycle_direction(&self, direction: CycleDirection) -> Option<usize> {
|
||||
Option::from(direction.next_idx(
|
||||
|
||||
@@ -16,6 +16,14 @@ Query(state_query) {
|
||||
Run, komorebic.exe query %state_query%, , Hide
|
||||
}
|
||||
|
||||
Subscribe(named_pipe) {
|
||||
Run, komorebic.exe subscribe %named_pipe%, , Hide
|
||||
}
|
||||
|
||||
Unsubscribe(named_pipe) {
|
||||
Run, komorebic.exe unsubscribe %named_pipe%, , Hide
|
||||
}
|
||||
|
||||
Log() {
|
||||
Run, komorebic.exe log, , Hide
|
||||
}
|
||||
@@ -120,8 +128,12 @@ AdjustWorkspacePadding(sizing, adjustment) {
|
||||
Run, komorebic.exe adjust-workspace-padding %sizing% %adjustment%, , Hide
|
||||
}
|
||||
|
||||
ChangeLayout(layout) {
|
||||
Run, komorebic.exe change-layout %layout%, , Hide
|
||||
ChangeLayout(default_layout) {
|
||||
Run, komorebic.exe change-layout %default_layout%, , Hide
|
||||
}
|
||||
|
||||
LoadCustomLayout(path) {
|
||||
Run, komorebic.exe load-custom-layout %path%, , Hide
|
||||
}
|
||||
|
||||
FlipLayout(flip) {
|
||||
@@ -152,6 +164,10 @@ WorkspaceLayout(monitor, workspace, value) {
|
||||
Run, komorebic.exe workspace-layout %monitor% %workspace% %value%, , Hide
|
||||
}
|
||||
|
||||
WorkspaceCustomLayout(monitor, workspace, path) {
|
||||
Run, komorebic.exe workspace-custom-layout %monitor% %workspace% %path%, , Hide
|
||||
}
|
||||
|
||||
WorkspaceTiling(monitor, workspace, value) {
|
||||
Run, komorebic.exe workspace-tiling %monitor% %workspace% %value%, , Hide
|
||||
}
|
||||
|
||||
@@ -6,7 +6,7 @@ description = "The command-line interface for Komorebi, a tiling window manager
|
||||
categories = ["cli", "tiling-window-manager", "windows"]
|
||||
repository = "https://github.com/LGUG2Z/komorebi"
|
||||
license = "MIT"
|
||||
edition = "2018"
|
||||
edition = "2021"
|
||||
|
||||
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
|
||||
|
||||
|
||||
@@ -29,9 +29,9 @@ use derive_ahk::AhkFunction;
|
||||
use derive_ahk::AhkLibrary;
|
||||
use komorebi_core::ApplicationIdentifier;
|
||||
use komorebi_core::CycleDirection;
|
||||
use komorebi_core::DefaultLayout;
|
||||
use komorebi_core::Flip;
|
||||
use komorebi_core::FocusFollowsMouseImplementation;
|
||||
use komorebi_core::Layout;
|
||||
use komorebi_core::OperationDirection;
|
||||
use komorebi_core::Rect;
|
||||
use komorebi_core::Sizing;
|
||||
@@ -86,7 +86,7 @@ gen_enum_subcommand_args! {
|
||||
Stack: OperationDirection,
|
||||
CycleStack: CycleDirection,
|
||||
FlipLayout: Flip,
|
||||
ChangeLayout: Layout,
|
||||
ChangeLayout: DefaultLayout,
|
||||
WatchConfiguration: BooleanState,
|
||||
Query: StateQuery,
|
||||
}
|
||||
@@ -132,7 +132,7 @@ macro_rules! gen_workspace_subcommand_args {
|
||||
$(#[clap(arg_enum)] $($arg_enum)?)?
|
||||
#[cfg_attr(
|
||||
all($(FALSE $($arg_enum)?)?),
|
||||
doc = ""$name" of the workspace as a "$value""
|
||||
doc = ""$name " of the workspace as a "$value ""
|
||||
)]
|
||||
value: $value,
|
||||
}
|
||||
@@ -143,10 +143,22 @@ macro_rules! gen_workspace_subcommand_args {
|
||||
|
||||
gen_workspace_subcommand_args! {
|
||||
Name: String,
|
||||
Layout: #[enum] Layout,
|
||||
Layout: #[enum] DefaultLayout,
|
||||
Tiling: #[enum] BooleanState,
|
||||
}
|
||||
|
||||
#[derive(Clap, AhkFunction)]
|
||||
pub struct WorkspaceCustomLayout {
|
||||
/// Monitor index (zero-indexed)
|
||||
monitor: usize,
|
||||
|
||||
/// Workspace index on the specified monitor (zero-indexed)
|
||||
workspace: usize,
|
||||
|
||||
/// JSON or YAML file from which the custom layout definition should be loaded
|
||||
path: String,
|
||||
}
|
||||
|
||||
#[derive(Clap, AhkFunction)]
|
||||
struct Resize {
|
||||
#[clap(arg_enum)]
|
||||
@@ -249,6 +261,7 @@ gen_application_target_subcommand_args! {
|
||||
ManageRule,
|
||||
IdentifyTrayApplication,
|
||||
IdentifyBorderOverflow,
|
||||
RemoveTitleBar,
|
||||
}
|
||||
|
||||
#[derive(Clap, AhkFunction)]
|
||||
@@ -296,6 +309,24 @@ struct Load {
|
||||
path: String,
|
||||
}
|
||||
|
||||
#[derive(Clap, AhkFunction)]
|
||||
struct LoadCustomLayout {
|
||||
/// JSON or YAML file from which the custom layout definition should be loaded
|
||||
path: String,
|
||||
}
|
||||
|
||||
#[derive(Clap, AhkFunction)]
|
||||
struct Subscribe {
|
||||
/// Name of the pipe to send event notifications to (without "\\.\pipe\" prepended)
|
||||
named_pipe: String,
|
||||
}
|
||||
|
||||
#[derive(Clap, AhkFunction)]
|
||||
struct Unsubscribe {
|
||||
/// Name of the pipe to stop sending event notifications to (without "\\.\pipe\" prepended)
|
||||
named_pipe: String,
|
||||
}
|
||||
|
||||
#[derive(Clap)]
|
||||
#[clap(author, about, version, setting = AppSettings::DeriveDisplayOrder)]
|
||||
struct Opts {
|
||||
@@ -314,6 +345,12 @@ enum SubCommand {
|
||||
/// Query the current window manager state
|
||||
#[clap(setting = AppSettings::ArgRequiredElseHelp)]
|
||||
Query(Query),
|
||||
/// Subscribe to komorebi events
|
||||
#[clap(setting = AppSettings::ArgRequiredElseHelp)]
|
||||
Subscribe(Subscribe),
|
||||
/// Unsubscribe from komorebi events
|
||||
#[clap(setting = AppSettings::ArgRequiredElseHelp)]
|
||||
Unsubscribe(Unsubscribe),
|
||||
/// Tail komorebi.exe's process logs (cancel with Ctrl-C)
|
||||
Log,
|
||||
/// Quicksave the current resize layout dimensions
|
||||
@@ -390,6 +427,9 @@ enum SubCommand {
|
||||
/// Set the layout on the focused workspace
|
||||
#[clap(setting = AppSettings::ArgRequiredElseHelp)]
|
||||
ChangeLayout(ChangeLayout),
|
||||
/// Load a custom layout from file for the focused workspace
|
||||
#[clap(setting = AppSettings::ArgRequiredElseHelp)]
|
||||
LoadCustomLayout(LoadCustomLayout),
|
||||
/// Flip the layout on the focused workspace (BSP only)
|
||||
#[clap(setting = AppSettings::ArgRequiredElseHelp)]
|
||||
FlipLayout(FlipLayout),
|
||||
@@ -409,6 +449,9 @@ enum SubCommand {
|
||||
/// Set the layout for the specified workspace
|
||||
#[clap(setting = AppSettings::ArgRequiredElseHelp)]
|
||||
WorkspaceLayout(WorkspaceLayout),
|
||||
/// Set a custom layout for the specified workspace
|
||||
#[clap(setting = AppSettings::ArgRequiredElseHelp)]
|
||||
WorkspaceCustomLayout(WorkspaceCustomLayout),
|
||||
/// Enable or disable window tiling for the specified workspace
|
||||
#[clap(setting = AppSettings::ArgRequiredElseHelp)]
|
||||
WorkspaceTiling(WorkspaceTiling),
|
||||
@@ -451,6 +494,11 @@ enum SubCommand {
|
||||
/// Identify an application that has overflowing borders
|
||||
#[clap(setting = AppSettings::ArgRequiredElseHelp)]
|
||||
IdentifyBorderOverflow(IdentifyBorderOverflow),
|
||||
/// Whitelist an application for title bar removal
|
||||
#[clap(setting = AppSettings::ArgRequiredElseHelp)]
|
||||
RemoveTitleBar(RemoveTitleBar),
|
||||
/// Toggle title bars for whitelisted applications
|
||||
ToggleTitleBars,
|
||||
/// Enable or disable focus follows mouse for the operating system
|
||||
#[clap(setting = AppSettings::ArgRequiredElseHelp)]
|
||||
FocusFollowsMouse(FocusFollowsMouse),
|
||||
@@ -607,6 +655,16 @@ fn main() -> Result<()> {
|
||||
.as_bytes()?,
|
||||
)?;
|
||||
}
|
||||
SubCommand::WorkspaceCustomLayout(arg) => {
|
||||
send_message(
|
||||
&*SocketMessage::WorkspaceLayoutCustom(
|
||||
arg.monitor,
|
||||
arg.workspace,
|
||||
resolve_windows_path(&arg.path)?,
|
||||
)
|
||||
.as_bytes()?,
|
||||
)?;
|
||||
}
|
||||
SubCommand::WorkspaceTiling(arg) => {
|
||||
send_message(
|
||||
&*SocketMessage::WorkspaceTiling(arg.monitor, arg.workspace, arg.value.into())
|
||||
@@ -691,7 +749,12 @@ fn main() -> Result<()> {
|
||||
send_message(&*SocketMessage::CycleStack(arg.cycle_direction).as_bytes()?)?;
|
||||
}
|
||||
SubCommand::ChangeLayout(arg) => {
|
||||
send_message(&*SocketMessage::ChangeLayout(arg.layout).as_bytes()?)?;
|
||||
send_message(&*SocketMessage::ChangeLayout(arg.default_layout).as_bytes()?)?;
|
||||
}
|
||||
SubCommand::LoadCustomLayout(arg) => {
|
||||
send_message(
|
||||
&*SocketMessage::ChangeLayoutCustom(resolve_windows_path(&arg.path)?).as_bytes()?,
|
||||
)?;
|
||||
}
|
||||
SubCommand::FlipLayout(arg) => {
|
||||
send_message(&*SocketMessage::FlipLayout(arg.flip).as_bytes()?)?;
|
||||
@@ -838,6 +901,23 @@ fn main() -> Result<()> {
|
||||
&*SocketMessage::IdentifyBorderOverflow(target.identifier, target.id).as_bytes()?,
|
||||
)?;
|
||||
}
|
||||
SubCommand::RemoveTitleBar(target) => {
|
||||
match target.identifier {
|
||||
ApplicationIdentifier::Exe => {}
|
||||
_ => {
|
||||
return Err(anyhow!(
|
||||
"this command requires applications to be identified by their exe"
|
||||
))
|
||||
}
|
||||
}
|
||||
|
||||
send_message(
|
||||
&*SocketMessage::RemoveTitleBar(target.identifier, target.id).as_bytes()?,
|
||||
)?;
|
||||
}
|
||||
SubCommand::ToggleTitleBars => {
|
||||
send_message(&*SocketMessage::ToggleTitleBars.as_bytes()?)?;
|
||||
}
|
||||
SubCommand::Manage => {
|
||||
send_message(&*SocketMessage::ManageFocusedWindow.as_bytes()?)?;
|
||||
}
|
||||
@@ -856,6 +936,12 @@ fn main() -> Result<()> {
|
||||
SubCommand::Load(arg) => {
|
||||
send_message(&*SocketMessage::Load(resolve_windows_path(&arg.path)?).as_bytes()?)?;
|
||||
}
|
||||
SubCommand::Subscribe(arg) => {
|
||||
send_message(&*SocketMessage::AddSubscriber(arg.named_pipe).as_bytes()?)?;
|
||||
}
|
||||
SubCommand::Unsubscribe(arg) => {
|
||||
send_message(&*SocketMessage::RemoveSubscriber(arg.named_pipe).as_bytes()?)?;
|
||||
}
|
||||
}
|
||||
|
||||
Ok(())
|
||||
|
||||
1
rustfmt.toml
Normal file
1
rustfmt.toml
Normal file
@@ -0,0 +1 @@
|
||||
imports_granularity = "Item"
|
||||
Reference in New Issue
Block a user