Compare commits

..

15 Commits

Author SHA1 Message Date
LGUG2Z
987dc2b8dd feat(wm): add cmds for titlebar removal
This commit introduces some basic commands to read from/add to a
whitelist of applications which that user has determined that are safe
to remove the titlebar from.

Titlebars and removed and added by removing and adding the WS_CAPTION
and WS_THICKFRAME styles (this is the approach that Workspacer also
takes) from the windows.

Wherever possible, the user should prefer native preferences and
settings that remove the titlebar in popular apps (Windows Terminal,
Firefox etc).

re #57
2021-10-27 17:37:57 -07:00
LGUG2Z
29a6c39084 feat(subscriptions): embed latest state
This commit embeds the latest window manager state (as returned from
'komorebic.exe state') as part of the event notifications sent to
subscribers.

Separately, WindowManager.update_focused_workspace has been refactored
to allow a failure to set the foreground window to the default desktop
window on an empty workspace to log a warning instead of returning an
error, allowing messages previously impacted by this to run to
conclusion and be surfaced in the event notifications stream.

resolve #56
2021-10-26 18:49:53 -07:00
LGUG2Z
5d0806a8c9 fix(serde): gracefully handle window ser errors
I came across some panics when trying to run the custom serialization of
the Window struct for windows that were in the process of being
destroyed recently.

This commit replaces all of the expect() calls in the Serialize
implementation for Window with calls to serde::ser::Error::custom()
which should fail gracefully without rendering the thread that
previously panicked as useless.

fix #55
2021-10-26 08:48:53 -07:00
LGUG2Z
6c53fd7830 refactor(subscriptions): ensure consistent naming
This commit renames add-subscriber and remove-subscriber to subscribe
and unsubscribe for more semantic consistency in command names, as well
as improving and fixing the cli documentation for these commands.

@denBot's example of how to create named pipes and subscribe to events
has also been added to the readme.
2021-10-25 12:08:59 -07:00
LGUG2Z
6ae59671a2 feat(subscriptions): add and remove subscribers
This commit adds two new commands to add and remove subscribers to
WindowManagerEvent and SocketMessage notifications after they have been
handled by komorebi.

Interprocess communication is achieved using Named Pipes; the
subscribing process must first create the Named Pipe, and then run the
'add-subscriber' command, specifying the pipe name as the argument
(without the pipe filesystem path prepended).

Whenever a pipe is closing or has been closed, komorebi will flag this
as a stale subscription and remove it automatically.

resolve #54
2021-10-25 09:31:59 -07:00
LGUG2Z
f17bfe267e docs(readme): add link to custom layout generator 2021-10-23 08:10:46 -07:00
LGUG2Z
840af215a0 docs(readme): add section about custom layouts
This commit adds some documentation around custom layouts as well as a
YAML example.

The load-layout command has been renamed to load-custom-layout for
consistency.

resolve #50
2021-10-21 16:38:47 -07:00
LGUG2Z
6981d778a9 feat(custom_layout): add yaml file support
This commit adds support for loading custom layouts from yaml files, and
also moves the custom layout loading and validating logic into the
komorebi-core crate.

re #50
2021-10-21 16:30:41 -07:00
LGUG2Z
5d6351f48d feat(custom_layout): add opt width for primary col
This commit adds a ColumnWidth for Column::Primary which can optionally
be given as a percentage of the total work area of a monitor. The
remaining columns will have their widths calculated by dividing the
remaining work area space evenly.

This commit also fixes a bug with the Promote command, which was not
calculating the primary container index of custom layouts properly, and
was also not using this value to update the focused container index at
the end of the promotion handler.

re #50
2021-10-21 16:30:41 -07:00
LGUG2Z
ac0f33f7ed feat(custom_layout): implement navigation
This commit introduces a number of refactors to layouts in general in
order to enable navigation across custom layouts and integrate both
default and custom layouts cleanly into komorebi and komorebic.

Layout has been renamed to DefaultLayout, and Layout is now an enum with
the variants Default and Custom, both of which implement the new traits
Arrangement (for layout calculation) and Direction (for operation
destination calculation).

CustomLayout has been simplified to wrap Vec<Column> and no longer
requires the primary column index to be explicitly defined as this can
be looked up at runtime for any valid CustomLayout.

Given the focus on ultrawide layouts for this feature, I have disabled
(and have not yet written the logic for) vertical column splits in
custom layouts.

Since CustomLayouts will be loaded from a file path, a bunch of
clap-related code generation stuff has been removed from the related
enums and structs.

Layout flipping has not yet been worked on for custom layouts.

When switching between Default and Custom layout variants, the primary
column index and the 0 element are swapped to ensure that the same
window container is always at the focal point of every layout.

Resizing/dragging to resize is in a bit of weird spot at the moment
because the logic is only implemented for DefaultLayout::BSP right now
and nothing else. I think eventually this will need to be extracted to a
Resize trait and implemented on everything.
2021-10-21 16:30:41 -07:00
LGUG2Z
f19bd3032b feat(custom_layout): calculate layouts adaptively
This commit introduces a new Trait, Dimensions, which requires the
implementation of a fn calculate() -> Vec<Rect>, a fn that was
previously limited to the Layout struct.

Dimensions is now implemented both for Layout and the new CustomLayout
struct, the latter being a general adaptive fn which employs a number of
fallbacks to sane defaults when the the layout does not have the minimum
number of required windows on the screen.

The CustomLayout is mainly intended for use on ultra and superultrawide
monitors, and as such uses columns as a basic building block. There are
three Column variants: Primary, Secondary and Tertiary.

The Primary column will typically be somewhere in the middle of the
layout, and will be where a window is placed when promoted using the
komorebic command.

The Secondary column is optional, and can be used one or more times in a
layout, either splitting to accomodate a certain number of windows
horizontally or vertically, or not splitting at all.

The Tertiary window is the final window, which will typically be on the
right of a layout, which must be split either horizontally or vertically
to accomodate as many windows as necessary.

The Tertiary column will only be rendered when the threshold of windows
required to enable it has been met. Until then, the rightmost Primary or
Secondary column will expand to take its place.

If there are less windows than (or a number equal to the) columns
defined in the layout, the windows will be arranged in a basic columnar
layout until the number of windows is greater than the number of columns
defined in the layout.

At this point, although the calculation logic has been completed, work
must be done on the navigation logic before a SocketMessage variant can
be added for loading custom layouts from files.
2021-10-21 16:30:41 -07:00
LGUG2Z
3f3c2815da feature(wm): manage linux gui apps by default
This commit introduces an allow_wsl2_gui override in
Window.should_manage() which ensures that Linux GUI apps being run
through WSLg, VcXsrv or X410 will be automatically tiled.

For now the exes that trigger this override are kept in a static Vec in
the codebase, but this could be made configurable in the future if there
is a specific feature request.

resolve #52, resolve #53
2021-10-21 14:30:58 -07:00
LGUG2Z
7070878f4a chore(rust): migrate to edition 2021
This commit applies 'cargo fix --edition' to safely migrate the project
to Edition 2021 of Rust.

A rustfmt.toml has also be added to enforce the flattening of use
statements when running 'cargo fmt'.
2021-10-21 12:08:10 -07:00
LGUG2Z
d3cb9e07f7 fix(wm): keep multi-window app hwnds when stacking
This commit fixes a boolean logic error with an extra pair of parens to
ensure that apps like Firefox don't end up with their HWNDs reaped by
Workspace.remove_window() when another window is stocked on top of them.

fix #51
2021-10-18 13:44:01 -07:00
LGUG2Z
6f6181625f refactor(layouts): compose row and column fns
This commit extracts independent functions for calculating row and
column layouts in an arbitrary work area. This should be useful in the
future for some ideas I have around custom serializable layouts.
2021-10-15 10:49:04 -07:00
26 changed files with 1970 additions and 787 deletions

46
Cargo.lock generated
View File

@@ -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",
]

View File

@@ -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).

View File

@@ -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

View File

@@ -1,7 +1,7 @@
[package]
name = "derive-ahk"
version = "0.1.0"
edition = "2018"
edition = "2021"
[lib]
proc-macro = true

View File

@@ -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"] }

View 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
}
}

View 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),
}

View 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)
}
}
}

View 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)
}
}

View File

@@ -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
}
}

View File

@@ -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 {

View File

@@ -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())
}
}

View File

@@ -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 = []

View File

@@ -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);
}

View File

@@ -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(())

View File

@@ -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(())

View File

@@ -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

View File

@@ -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 {

View File

@@ -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),

View File

@@ -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)]

View File

@@ -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 {

View File

@@ -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(

View File

@@ -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
}

View File

@@ -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

View File

@@ -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
View File

@@ -0,0 +1 @@
imports_granularity = "Item"