mirror of
https://github.com/LGUG2Z/komorebi.git
synced 2026-03-03 17:30:02 +01:00
Compare commits
19 Commits
feature/lo
...
feature/sp
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
7fed31bc54 | ||
|
|
8fd18048a4 | ||
|
|
5809735024 | ||
|
|
96fdbbd1fb | ||
|
|
de131e9ca5 | ||
|
|
07dba03255 | ||
|
|
acd53dec1b | ||
|
|
a98968d179 | ||
|
|
8a32219867 | ||
|
|
ce4b75cc3c | ||
|
|
e4226ce623 | ||
|
|
4bfd7febb4 | ||
|
|
5cc688dc6b | ||
|
|
d897890032 | ||
|
|
e702d93a8a | ||
|
|
a8c687d3d5 | ||
|
|
30fbc1ae73 | ||
|
|
cb60e91842 | ||
|
|
64d29d606a |
40
CODE_OF_CONDUCT.md
Normal file
40
CODE_OF_CONDUCT.md
Normal file
@@ -0,0 +1,40 @@
|
||||
# The Komorebi Code of Conduct
|
||||
|
||||
This document is based on the [Rust Code of
|
||||
Conduct](https://www.rust-lang.org/policies/code-of-conduct)
|
||||
|
||||
## Conduct
|
||||
|
||||
- We are committed to providing a friendly, safe and welcoming environment for
|
||||
all, regardless of level of experience, gender identity and expression, sexual
|
||||
orientation, disability, personal appearance, body size, race, ethnicity, age,
|
||||
religion, nationality, or other similar characteristic.
|
||||
|
||||
- Please avoid using overtly sexual aliases or other nicknames that might
|
||||
detract from a friendly, safe and welcoming environment for all.
|
||||
|
||||
- Please be kind and courteous. There’s no need to be mean or rude.
|
||||
|
||||
- Respect that people have differences of opinion and that every design or
|
||||
implementation choice carries a trade-off and numerous costs. There is seldom a
|
||||
right answer.
|
||||
|
||||
- Please keep unstructured critique to a minimum. If you have solid ideas you
|
||||
want to experiment with, make a fork and see how it works.
|
||||
|
||||
- We will exclude you from interaction if you insult, demean or harass anyone.
|
||||
That is not welcome behavior. We interpret the term “harassment” as including
|
||||
the definition in the [Citizen Code of
|
||||
Conduct](https://github.com/stumpsyn/policies/blob/master/citizen_code_of_conduct.md);
|
||||
if you have any lack of clarity about what might be included in that concept,
|
||||
please read their definition. In particular, we don’t tolerate behavior that
|
||||
excludes people in socially marginalized groups.
|
||||
|
||||
- Private harassment is also unacceptable. No matter who you are, if you feel
|
||||
you have been or are being harassed or made uncomfortable by a community member,
|
||||
please contact me immediately. Whether you’re a regular contributor or a
|
||||
newcomer, we care about making this community a safe place for you and we’ve got
|
||||
your back.
|
||||
|
||||
- Likewise any spamming, trolling, flaming, baiting or other attention-stealing
|
||||
behavior is not welcome.
|
||||
25
Cargo.lock
generated
25
Cargo.lock
generated
@@ -2745,7 +2745,8 @@ dependencies = [
|
||||
"tracing-subscriber",
|
||||
"windows 0.61.1",
|
||||
"windows-core 0.61.0",
|
||||
"windows-icons",
|
||||
"windows-icons 0.1.0 (git+https://github.com/LGUG2Z/windows-icons?rev=0c9d7ee1b807347c507d3a9862dd007b4d3f4354)",
|
||||
"windows-icons 0.1.0 (git+https://github.com/LGUG2Z/windows-icons?rev=d67cc9920aa9b4883393e411fb4fa2ddd4c498b5)",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -4472,6 +4473,12 @@ dependencies = [
|
||||
"windows-sys 0.52.0",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "roxmltree"
|
||||
version = "0.20.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "6c20b6793b5c2fa6553b250154b78d6d0db37e72700ae35fad9387a46f487c97"
|
||||
|
||||
[[package]]
|
||||
name = "rustc-demangle"
|
||||
version = "0.1.24"
|
||||
@@ -6085,7 +6092,7 @@ dependencies = [
|
||||
[[package]]
|
||||
name = "win32-display-data"
|
||||
version = "0.1.0"
|
||||
source = "git+https://github.com/LGUG2Z/win32-display-data?rev=93949750b1f123fb79827ba4d66ffcab68055654#93949750b1f123fb79827ba4d66ffcab68055654"
|
||||
source = "git+https://github.com/LGUG2Z/win32-display-data?rev=a28c6559a9de2f92c142a714947a9b081776caca#a28c6559a9de2f92c142a714947a9b081776caca"
|
||||
dependencies = [
|
||||
"itertools 0.14.0",
|
||||
"serde",
|
||||
@@ -6270,6 +6277,20 @@ dependencies = [
|
||||
"windows-link",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "windows-icons"
|
||||
version = "0.1.0"
|
||||
source = "git+https://github.com/LGUG2Z/windows-icons?rev=0c9d7ee1b807347c507d3a9862dd007b4d3f4354#0c9d7ee1b807347c507d3a9862dd007b4d3f4354"
|
||||
dependencies = [
|
||||
"base64",
|
||||
"image",
|
||||
"regex",
|
||||
"roxmltree",
|
||||
"sysinfo",
|
||||
"winapi",
|
||||
"windows 0.58.0",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "windows-icons"
|
||||
version = "0.1.0"
|
||||
|
||||
@@ -35,7 +35,7 @@ tracing-subscriber = { version = "0.3", features = ["env-filter"] }
|
||||
paste = "1"
|
||||
sysinfo = "0.33"
|
||||
uds_windows = "1"
|
||||
win32-display-data = { git = "https://github.com/LGUG2Z/win32-display-data", rev = "93949750b1f123fb79827ba4d66ffcab68055654" }
|
||||
win32-display-data = { git = "https://github.com/LGUG2Z/win32-display-data", rev = "a28c6559a9de2f92c142a714947a9b081776caca" }
|
||||
windows-numerics = { version = "0.2" }
|
||||
windows-implement = { version = "0.60" }
|
||||
windows-interface = { version = "0.59" }
|
||||
|
||||
24
LICENSE.md
24
LICENSE.md
@@ -1,6 +1,6 @@
|
||||
# Komorebi License
|
||||
|
||||
Version 1.0.0
|
||||
Version 2.0.0
|
||||
|
||||
## Acceptance
|
||||
|
||||
@@ -13,9 +13,20 @@ your licenses.
|
||||
The licensor grants you a copyright license for the software
|
||||
to do everything you might do with the software that would
|
||||
otherwise infringe the licensor's copyright in it for any
|
||||
permitted purpose. However, you may only make changes according
|
||||
permitted purpose. However, you may only distribute the source
|
||||
code of the software according to the [Distribution License](
|
||||
#distribution-license), you may only make changes according
|
||||
to the [Changes License](#changes-license), and you may not
|
||||
distribute the software or new works based on the software.
|
||||
otherwise distribute the software or new works based on the
|
||||
software.
|
||||
|
||||
## Distribution License
|
||||
|
||||
The licensor grants you an additional copyright license to
|
||||
distribute copies of the source code of the software. Your
|
||||
license to distribute covers distributing the source code of
|
||||
the software with changes permitted by the [Changes License](
|
||||
#changes-license).
|
||||
|
||||
## Changes License
|
||||
|
||||
@@ -45,7 +56,7 @@ law. These terms do not limit them.
|
||||
|
||||
These terms do not allow you to sublicense or transfer any of
|
||||
your licenses to anyone else, or prevent the licensor from
|
||||
granting licenses to anyone else. These terms do not imply
|
||||
granting licenses to anyone else. These terms do not imply
|
||||
any other licenses.
|
||||
|
||||
## Patent Defense
|
||||
@@ -63,7 +74,7 @@ violated any of these terms, or done anything with the software
|
||||
not covered by your licenses, your licenses can nonetheless
|
||||
continue if you come into full compliance with these terms,
|
||||
and take practical steps to correct past violations, within
|
||||
32 days of receiving notice. Otherwise, all your licenses
|
||||
32 days of receiving notice. Otherwise, all your licenses
|
||||
end immediately.
|
||||
|
||||
## No Liability
|
||||
@@ -88,11 +99,10 @@ organizations that have control over, are under the control of,
|
||||
or are under common control with that organization. **Control**
|
||||
means ownership of substantially all the assets of an entity,
|
||||
or the power to direct its management and policies by vote,
|
||||
contract, or otherwise. Control can be direct or indirect.
|
||||
contract, or otherwise. Control can be direct or indirect.
|
||||
|
||||
**Your licenses** are all the licenses granted to you for the
|
||||
software under these terms.
|
||||
|
||||
**Use** means anything you do with the software requiring one
|
||||
of your licenses.
|
||||
|
||||
|
||||
@@ -74,7 +74,7 @@ showcases the many awesome projects that exist in the _komorebi_ ecosystem.
|
||||
`komorebi` is [educational source
|
||||
software](https://lgug2z.com/articles/educational-source-software/).
|
||||
|
||||
`komorebi` is licensed under the [Komorebi 1.0.0
|
||||
`komorebi` is licensed under the [Komorebi 2.0.0
|
||||
license](https://github.com/LGUG2Z/komorebi-license), which is a fork of the
|
||||
[PolyForm Strict 1.0.0
|
||||
license](https://polyformproject.org/licenses/strict/1.0.0). On a high level
|
||||
@@ -85,7 +85,7 @@ hard-forks) based on the software.
|
||||
Anyone is free to make their own fork of `komorebi` with changes intended either
|
||||
for personal use or for integration back upstream via pull requests.
|
||||
|
||||
The [Komorebi 1.0.0 License](https://github.com/LGUG2Z/komorebi-license) does
|
||||
The [Komorebi 2.0.0 License](https://github.com/LGUG2Z/komorebi-license) does
|
||||
not permit any kind of commercial use (i.e. using `komorebi` at work).
|
||||
|
||||
## Sponsorship for Personal Use
|
||||
@@ -146,7 +146,8 @@ video will answer the majority of your questions.
|
||||
|
||||
[@amnweb](https://github.com/amnweb) showing _komorebi_ `v0.1.28` running on Windows 11 with window borders,
|
||||
unfocused window transparency and animations enabled, using a custom status bar integrated using
|
||||
_komorebi_'s [Window Manager Event Subscriptions](https://github.com/LGUG2Z/komorebi?tab=readme-ov-file#window-manager-event-subscriptions).
|
||||
_komorebi_'
|
||||
s [Window Manager Event Subscriptions](https://github.com/LGUG2Z/komorebi?tab=readme-ov-file#window-manager-event-subscriptions).
|
||||
|
||||
https://github.com/LGUG2Z/komorebi/assets/13164844/21be8dc4-fa76-4f70-9b37-1d316f4b40c2
|
||||
|
||||
|
||||
@@ -10,9 +10,9 @@ Options:
|
||||
Desired ease function for animation
|
||||
|
||||
[default: linear]
|
||||
[possible values: linear, ease-in-sine, ease-out-sine, ease-in-out-sine, ease-in-quad, ease-out-quad, ease-in-out-quad, ease-in-cubic, ease-in-out-cubic, ease-in-quart,
|
||||
ease-out-quart, ease-in-out-quart, ease-in-quint, ease-out-quint, ease-in-out-quint, ease-in-expo, ease-out-expo, ease-in-out-expo, ease-in-circ, ease-out-circ, ease-in-out-circ,
|
||||
ease-in-back, ease-out-back, ease-in-out-back, ease-in-elastic, ease-out-elastic, ease-in-out-elastic, ease-in-bounce, ease-out-bounce, ease-in-out-bounce]
|
||||
[possible values: linear, ease-in-sine, ease-out-sine, ease-in-out-sine, ease-in-quad, ease-out-quad, ease-in-out-quad, ease-in-cubic, ease-in-out-cubic, ease-in-quart, ease-out-quart,
|
||||
ease-in-out-quart, ease-in-quint, ease-out-quint, ease-in-out-quint, ease-in-expo, ease-out-expo, ease-in-out-expo, ease-in-circ, ease-out-circ, ease-in-out-circ, ease-in-back, ease-out-back,
|
||||
ease-in-out-back, ease-in-elastic, ease-out-elastic, ease-in-out-elastic, ease-in-bounce, ease-out-bounce, ease-in-out-bounce]
|
||||
|
||||
-a, --animation-type <ANIMATION_TYPE>
|
||||
Animation type to apply the style to. If not specified, sets global style
|
||||
|
||||
@@ -18,7 +18,7 @@ Arguments:
|
||||
Options:
|
||||
-w, --window-kind <WINDOW_KIND>
|
||||
[default: single]
|
||||
[possible values: single, stack, monocle, unfocused, floating]
|
||||
[possible values: single, stack, monocle, unfocused, unfocused-locked, floating]
|
||||
|
||||
-h, --help
|
||||
Print help
|
||||
|
||||
12
docs/cli/move-to-last-workspace.md
Normal file
12
docs/cli/move-to-last-workspace.md
Normal file
@@ -0,0 +1,12 @@
|
||||
# move-to-last-workspace
|
||||
|
||||
```
|
||||
Move the focused window to the last focused monitor workspace
|
||||
|
||||
Usage: komorebic.exe move-to-last-workspace
|
||||
|
||||
Options:
|
||||
-h, --help
|
||||
Print help
|
||||
|
||||
```
|
||||
12
docs/cli/send-to-last-workspace.md
Normal file
12
docs/cli/send-to-last-workspace.md
Normal file
@@ -0,0 +1,12 @@
|
||||
# send-to-last-workspace
|
||||
|
||||
```
|
||||
Send the focused window to the last focused monitor workspace
|
||||
|
||||
Usage: komorebic.exe send-to-last-workspace
|
||||
|
||||
Options:
|
||||
-h, --help
|
||||
Print help
|
||||
|
||||
```
|
||||
12
docs/cli/toggle-lock.md
Normal file
12
docs/cli/toggle-lock.md
Normal file
@@ -0,0 +1,12 @@
|
||||
# toggle-lock
|
||||
|
||||
```
|
||||
Toggle a lock for the focused container, ensuring it will not be displaced by any new windows
|
||||
|
||||
Usage: komorebic.exe toggle-lock
|
||||
|
||||
Options:
|
||||
-h, --help
|
||||
Print help
|
||||
|
||||
```
|
||||
@@ -1,8 +1,8 @@
|
||||
# toggle-workspace-float-override
|
||||
|
||||
```
|
||||
Enable or disable float override, which makes it so every new window opens in floating mode, for the currently focused workspace. If there was no override value set for the workspace
|
||||
previously it takes the opposite of the global value
|
||||
Enable or disable float override, which makes it so every new window opens in floating mode, for the currently focused workspace. If there was no override value set for the workspace previously it takes
|
||||
the opposite of the global value
|
||||
|
||||
Usage: komorebic.exe toggle-workspace-float-override
|
||||
|
||||
|
||||
@@ -1,8 +1,7 @@
|
||||
# toggle-workspace-window-container-behaviour
|
||||
|
||||
```
|
||||
Toggle the behaviour for new windows (stacking or dynamic tiling) for currently focused workspace. If there was no behaviour set for the workspace previously it takes the opposite of the
|
||||
global value
|
||||
Toggle the behaviour for new windows (stacking or dynamic tiling) for currently focused workspace. If there was no behaviour set for the workspace previously it takes the opposite of the global value
|
||||
|
||||
Usage: komorebic.exe toggle-workspace-window-container-behaviour
|
||||
|
||||
|
||||
@@ -34,7 +34,7 @@ showcases the many awesome projects that exist in the `komorebi` ecosystem.
|
||||
|
||||
## Licensing for Personal Use
|
||||
|
||||
`komorebi` is licensed under the [Komorebi 1.0.0 license](https://github.com/LGUG2Z/komorebi-license), which is a fork
|
||||
`komorebi` is licensed under the [Komorebi 2.0.0 license](https://github.com/LGUG2Z/komorebi-license), which is a fork
|
||||
of the [PolyForm Strict 1.0.0 license](https://polyformproject.org/licenses/strict/1.0.0). On a high level this means
|
||||
that you are free to do whatever you want with `komorebi` for personal use other than redistribution, or distribution of
|
||||
new works (i.e. hard-forks) based on the software.
|
||||
@@ -42,7 +42,7 @@ new works (i.e. hard-forks) based on the software.
|
||||
Anyone is free to make their own fork of `komorebi` with changes intended either for personal use or for integration
|
||||
back upstream via pull requests.
|
||||
|
||||
The [Komorebi 1.0.0 License](https://github.com/LGUG2Z/komorebi-license) does not permit any kind of commercial use (
|
||||
The [Komorebi 2.0.0 License](https://github.com/LGUG2Z/komorebi-license) does not permit any kind of commercial use (
|
||||
i.e. using `komorebi` at work).
|
||||
|
||||
## Sponsorship for Personal Use
|
||||
|
||||
@@ -37,7 +37,8 @@ tracing = { workspace = true }
|
||||
tracing-subscriber = { workspace = true }
|
||||
windows = { workspace = true }
|
||||
windows-core = { workspace = true }
|
||||
windows-icons = { git = "https://github.com/LGUG2Z/windows-icons", rev = "d67cc9920aa9b4883393e411fb4fa2ddd4c498b5" }
|
||||
windows-icons = { git = "https://github.com/LGUG2Z/windows-icons", rev = "0c9d7ee1b807347c507d3a9862dd007b4d3f4354" }
|
||||
windows-icons-fallback = { package = "windows-icons", git = "https://github.com/LGUG2Z/windows-icons", rev = "d67cc9920aa9b4883393e411fb4fa2ddd4c498b5" }
|
||||
|
||||
[features]
|
||||
default = ["schemars"]
|
||||
|
||||
@@ -47,7 +47,7 @@ pub static MONITOR_INDEX: AtomicUsize = AtomicUsize::new(0);
|
||||
pub static BAR_HEIGHT: f32 = 50.0;
|
||||
pub static DEFAULT_PADDING: f32 = 10.0;
|
||||
|
||||
pub static ICON_CACHE: LazyLock<Mutex<HashMap<String, RgbaImage>>> =
|
||||
pub static ICON_CACHE: LazyLock<Mutex<HashMap<isize, RgbaImage>>> =
|
||||
LazyLock::new(|| Mutex::new(HashMap::new()));
|
||||
|
||||
#[derive(Parser)]
|
||||
|
||||
@@ -837,11 +837,16 @@ impl From<&Container> for KomorebiNotificationStateContainerInformation {
|
||||
for window in windows {
|
||||
let mut icon_cache = ICON_CACHE.lock().unwrap();
|
||||
let mut update_cache = false;
|
||||
let exe = window.exe().unwrap_or_default();
|
||||
let hwnd = window.hwnd;
|
||||
|
||||
match icon_cache.get(&exe) {
|
||||
match icon_cache.get(&hwnd) {
|
||||
None => {
|
||||
icons.push(windows_icons::get_icon_by_process_id(window.process_id()));
|
||||
let icon = match windows_icons::get_icon_by_hwnd(window.hwnd) {
|
||||
None => windows_icons_fallback::get_icon_by_process_id(window.process_id()),
|
||||
Some(icon) => Some(icon),
|
||||
};
|
||||
|
||||
icons.push(icon);
|
||||
update_cache = true;
|
||||
}
|
||||
Some(icon) => {
|
||||
@@ -851,7 +856,7 @@ impl From<&Container> for KomorebiNotificationStateContainerInformation {
|
||||
|
||||
if update_cache {
|
||||
if let Some(Some(icon)) = icons.last() {
|
||||
icon_cache.insert(exe, icon.clone());
|
||||
icon_cache.insert(hwnd, icon.clone());
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -873,11 +878,16 @@ impl From<&Window> for KomorebiNotificationStateContainerInformation {
|
||||
let mut icon_cache = ICON_CACHE.lock().unwrap();
|
||||
let mut update_cache = false;
|
||||
let mut icons = vec![];
|
||||
let exe = value.exe().unwrap_or_default();
|
||||
let hwnd = value.hwnd;
|
||||
|
||||
match icon_cache.get(&exe) {
|
||||
match icon_cache.get(&hwnd) {
|
||||
None => {
|
||||
icons.push(windows_icons::get_icon_by_process_id(value.process_id()));
|
||||
let icon = match windows_icons::get_icon_by_hwnd(hwnd) {
|
||||
None => windows_icons_fallback::get_icon_by_process_id(value.process_id()),
|
||||
Some(icon) => Some(icon),
|
||||
};
|
||||
|
||||
icons.push(icon);
|
||||
update_cache = true;
|
||||
}
|
||||
Some(icon) => {
|
||||
@@ -887,7 +897,7 @@ impl From<&Window> for KomorebiNotificationStateContainerInformation {
|
||||
|
||||
if update_cache {
|
||||
if let Some(Some(icon)) = icons.last() {
|
||||
icon_cache.insert(exe, icon.clone());
|
||||
icon_cache.insert(hwnd, icon.clone());
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -29,6 +29,7 @@ pub use komorebi::core::CustomLayout;
|
||||
pub use komorebi::core::CycleDirection;
|
||||
pub use komorebi::core::DefaultLayout;
|
||||
pub use komorebi::core::Direction;
|
||||
pub use komorebi::core::FloatingLayerBehaviour;
|
||||
pub use komorebi::core::FocusFollowsMouseImplementation;
|
||||
pub use komorebi::core::HidingBehaviour;
|
||||
pub use komorebi::core::Layout;
|
||||
@@ -46,6 +47,7 @@ pub use komorebi::core::WindowKind;
|
||||
pub use komorebi::monitor::Monitor;
|
||||
pub use komorebi::monitor_reconciliator::MonitorNotification;
|
||||
pub use komorebi::ring::Ring;
|
||||
pub use komorebi::win32_display_data;
|
||||
pub use komorebi::window::Window;
|
||||
pub use komorebi::window_manager_event::WindowManagerEvent;
|
||||
pub use komorebi::workspace::Workspace;
|
||||
|
||||
@@ -7,7 +7,6 @@ use crate::core::WindowKind;
|
||||
use crate::ring::Ring;
|
||||
use crate::windows_api;
|
||||
use crate::workspace::WorkspaceLayer;
|
||||
use crate::workspace_reconciliator::ALT_TAB_HWND;
|
||||
use crate::Colour;
|
||||
use crate::Rgb;
|
||||
use crate::WindowManager;
|
||||
@@ -333,8 +332,6 @@ pub fn handle_notifications(wm: Arc<Mutex<WindowManager>>) -> color_eyre::Result
|
||||
if !BORDER_ENABLED.load_consume()
|
||||
// Or if the wm is paused
|
||||
|| is_paused
|
||||
// Or if we are handling an alt-tab across workspaces
|
||||
|| ALT_TAB_HWND.load().is_some()
|
||||
{
|
||||
// Destroy the borders we know about
|
||||
for (_, border) in borders.drain() {
|
||||
@@ -501,10 +498,10 @@ pub fn handle_notifications(wm: Arc<Mutex<WindowManager>>) -> color_eyre::Result
|
||||
|| focused_window_hwnd != foreground_window
|
||||
{
|
||||
if ws.locked_containers().contains(&idx) {
|
||||
WindowKind::UnfocusedLocked
|
||||
} else {
|
||||
WindowKind::Unfocused
|
||||
}
|
||||
WindowKind::UnfocusedLocked
|
||||
} else {
|
||||
WindowKind::Unfocused
|
||||
}
|
||||
} else if c.windows().len() > 1 {
|
||||
WindowKind::Stack
|
||||
} else {
|
||||
|
||||
@@ -62,6 +62,8 @@ pub enum SocketMessage {
|
||||
UnstackAll,
|
||||
ResizeWindowEdge(OperationDirection, Sizing),
|
||||
ResizeWindowAxis(Axis, Sizing),
|
||||
MoveContainerToLastWorkspace,
|
||||
SendContainerToLastWorkspace,
|
||||
MoveContainerToMonitorNumber(usize),
|
||||
CycleMoveContainerToMonitor(CycleDirection),
|
||||
MoveContainerToWorkspaceNumber(usize),
|
||||
@@ -376,6 +378,18 @@ pub enum WindowContainerBehaviour {
|
||||
Append,
|
||||
}
|
||||
|
||||
#[derive(
|
||||
Clone, Copy, Debug, Default, Serialize, Deserialize, Display, EnumString, ValueEnum, PartialEq,
|
||||
)]
|
||||
#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
|
||||
pub enum FloatingLayerBehaviour {
|
||||
/// Tile new windows (unless they match a float rule)
|
||||
#[default]
|
||||
Tile,
|
||||
/// Float new windows
|
||||
Float,
|
||||
}
|
||||
|
||||
#[derive(Clone, Copy, Debug, PartialEq, Serialize, Deserialize, Display, EnumString, ValueEnum)]
|
||||
#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
|
||||
pub enum MoveBehaviour {
|
||||
|
||||
@@ -30,7 +30,6 @@ pub mod windows_callbacks;
|
||||
pub mod winevent;
|
||||
pub mod winevent_listener;
|
||||
pub mod workspace;
|
||||
pub mod workspace_reconciliator;
|
||||
|
||||
use lazy_static::lazy_static;
|
||||
use monitor_reconciliator::MonitorNotification;
|
||||
@@ -53,6 +52,7 @@ pub use core::*;
|
||||
pub use process_command::*;
|
||||
pub use process_event::*;
|
||||
pub use static_config::*;
|
||||
pub use win32_display_data;
|
||||
pub use window::*;
|
||||
pub use window_manager::*;
|
||||
pub use window_manager_event::*;
|
||||
|
||||
@@ -16,6 +16,7 @@ use std::sync::Arc;
|
||||
use std::time::Duration;
|
||||
|
||||
use clap::Parser;
|
||||
use clap::ValueEnum;
|
||||
use color_eyre::Result;
|
||||
use crossbeam_utils::Backoff;
|
||||
use komorebi::animation::AnimationEngine;
|
||||
@@ -24,6 +25,7 @@ use komorebi::animation::ANIMATION_ENABLED_PER_ANIMATION;
|
||||
#[cfg(feature = "deadlock_detection")]
|
||||
use parking_lot::deadlock;
|
||||
use parking_lot::Mutex;
|
||||
use serde::Deserialize;
|
||||
use sysinfo::Process;
|
||||
use sysinfo::ProcessesToUpdate;
|
||||
use tracing_appender::non_blocking::WorkerGuard;
|
||||
@@ -48,7 +50,6 @@ use komorebi::window_manager::State;
|
||||
use komorebi::window_manager::WindowManager;
|
||||
use komorebi::windows_api::WindowsApi;
|
||||
use komorebi::winevent_listener;
|
||||
use komorebi::workspace_reconciliator;
|
||||
use komorebi::CUSTOM_FFM;
|
||||
use komorebi::DATA_DIR;
|
||||
use komorebi::HOME_DIR;
|
||||
@@ -57,7 +58,7 @@ use komorebi::SESSION_ID;
|
||||
|
||||
shadow_rs::shadow!(build);
|
||||
|
||||
fn setup() -> Result<(WorkerGuard, WorkerGuard)> {
|
||||
fn setup(log_level: LogLevel) -> Result<(WorkerGuard, WorkerGuard)> {
|
||||
if std::env::var("RUST_LIB_BACKTRACE").is_err() {
|
||||
std::env::set_var("RUST_LIB_BACKTRACE", "1");
|
||||
}
|
||||
@@ -65,7 +66,16 @@ fn setup() -> Result<(WorkerGuard, WorkerGuard)> {
|
||||
color_eyre::install()?;
|
||||
|
||||
if std::env::var("RUST_LOG").is_err() {
|
||||
std::env::set_var("RUST_LOG", "info");
|
||||
std::env::set_var(
|
||||
"RUST_LOG",
|
||||
match log_level {
|
||||
LogLevel::Error => "error",
|
||||
LogLevel::Warn => "warn",
|
||||
LogLevel::Info => "info",
|
||||
LogLevel::Debug => "debug",
|
||||
LogLevel::Trace => "trace",
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
let appender = tracing_appender::rolling::daily(std::env::temp_dir(), "komorebi_plaintext.log");
|
||||
@@ -143,6 +153,17 @@ fn detect_deadlocks() {
|
||||
});
|
||||
}
|
||||
|
||||
#[derive(Default, Deserialize, ValueEnum, Clone)]
|
||||
#[serde(rename_all = "snake_case")]
|
||||
enum LogLevel {
|
||||
Error,
|
||||
Warn,
|
||||
#[default]
|
||||
Info,
|
||||
Debug,
|
||||
Trace,
|
||||
}
|
||||
|
||||
#[derive(Parser)]
|
||||
#[clap(author, about, version = build::CLAP_LONG_VERSION)]
|
||||
struct Opts {
|
||||
@@ -161,6 +182,9 @@ struct Opts {
|
||||
/// Do not attempt to auto-apply a dumped state temp file from a previously running instance of komorebi
|
||||
#[clap(long)]
|
||||
clean_state: bool,
|
||||
/// Level of log output verbosity
|
||||
#[clap(long, value_enum, default_value_t=LogLevel::Info)]
|
||||
log_level: LogLevel,
|
||||
}
|
||||
|
||||
#[tracing::instrument]
|
||||
@@ -198,7 +222,7 @@ fn main() -> Result<()> {
|
||||
}
|
||||
|
||||
// File logging worker guard has to have an assignment in the main fn to work
|
||||
let (_guard, _color_guard) = setup()?;
|
||||
let (_guard, _color_guard) = setup(opts.log_level)?;
|
||||
|
||||
WindowsApi::foreground_lock_timeout()?;
|
||||
|
||||
@@ -278,7 +302,6 @@ fn main() -> Result<()> {
|
||||
border_manager::listen_for_notifications(wm.clone());
|
||||
stackbar_manager::listen_for_notifications(wm.clone());
|
||||
transparency_manager::listen_for_notifications(wm.clone());
|
||||
workspace_reconciliator::listen_for_notifications(wm.clone());
|
||||
monitor_reconciliator::listen_for_notifications(wm.clone())?;
|
||||
reaper::listen_for_notifications(wm.clone(), wm.lock().known_hwnds.clone());
|
||||
focus_manager::listen_for_notifications(wm.clone());
|
||||
|
||||
@@ -370,20 +370,20 @@ impl Monitor {
|
||||
.position(|w| w.hwnd == foreground_hwnd);
|
||||
|
||||
if let Some(idx) = floating_window_index {
|
||||
let window = workspace.floating_windows_mut().remove(idx);
|
||||
if let Some(window) = workspace.floating_windows_mut().remove(idx) {
|
||||
let workspaces = self.workspaces_mut();
|
||||
#[allow(clippy::option_if_let_else)]
|
||||
let target_workspace = match workspaces.get_mut(target_workspace_idx) {
|
||||
None => {
|
||||
workspaces.resize(target_workspace_idx + 1, Workspace::default());
|
||||
workspaces.get_mut(target_workspace_idx).unwrap()
|
||||
}
|
||||
Some(workspace) => workspace,
|
||||
};
|
||||
|
||||
let workspaces = self.workspaces_mut();
|
||||
#[allow(clippy::option_if_let_else)]
|
||||
let target_workspace = match workspaces.get_mut(target_workspace_idx) {
|
||||
None => {
|
||||
workspaces.resize(target_workspace_idx + 1, Workspace::default());
|
||||
workspaces.get_mut(target_workspace_idx).unwrap()
|
||||
}
|
||||
Some(workspace) => workspace,
|
||||
};
|
||||
|
||||
target_workspace.floating_windows_mut().push(window);
|
||||
target_workspace.set_layer(WorkspaceLayer::Floating);
|
||||
target_workspace.floating_windows_mut().push_back(window);
|
||||
target_workspace.set_layer(WorkspaceLayer::Floating);
|
||||
}
|
||||
} else {
|
||||
let container = workspace
|
||||
.remove_focused_container()
|
||||
|
||||
@@ -29,7 +29,7 @@ use std::sync::OnceLock;
|
||||
|
||||
pub mod hidden;
|
||||
|
||||
#[derive(Debug, Copy, Clone, Serialize, Deserialize)]
|
||||
#[derive(Debug, Copy, Clone, Serialize, Deserialize, PartialEq)]
|
||||
#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
|
||||
#[serde(tag = "type", content = "content")]
|
||||
pub enum MonitorNotification {
|
||||
@@ -84,10 +84,12 @@ pub fn insert_in_monitor_cache(serial_or_device_id: &str, monitor: Monitor) {
|
||||
monitor_cache.insert(preferred_id, monitor);
|
||||
}
|
||||
|
||||
pub fn attached_display_devices() -> color_eyre::Result<Vec<Monitor>> {
|
||||
let all_displays = win32_display_data::connected_displays_all()
|
||||
.flatten()
|
||||
.collect::<Vec<_>>();
|
||||
pub fn attached_display_devices<F, I>(display_provider: F) -> color_eyre::Result<Vec<Monitor>>
|
||||
where
|
||||
F: Fn() -> I + Copy,
|
||||
I: Iterator<Item = Result<win32_display_data::Device, win32_display_data::Error>>,
|
||||
{
|
||||
let all_displays = display_provider().flatten().collect::<Vec<_>>();
|
||||
|
||||
let mut serial_id_map = HashMap::new();
|
||||
|
||||
@@ -154,7 +156,7 @@ pub fn listen_for_notifications(wm: Arc<Mutex<WindowManager>>) -> color_eyre::Re
|
||||
tracing::info!("created hidden window to listen for monitor-related events");
|
||||
|
||||
std::thread::spawn(move || loop {
|
||||
match handle_notifications(wm.clone()) {
|
||||
match handle_notifications(wm.clone(), win32_display_data::connected_displays_all) {
|
||||
Ok(()) => {
|
||||
tracing::warn!("restarting finished thread");
|
||||
}
|
||||
@@ -171,7 +173,14 @@ pub fn listen_for_notifications(wm: Arc<Mutex<WindowManager>>) -> color_eyre::Re
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn handle_notifications(wm: Arc<Mutex<WindowManager>>) -> color_eyre::Result<()> {
|
||||
pub fn handle_notifications<F, I>(
|
||||
wm: Arc<Mutex<WindowManager>>,
|
||||
display_provider: F,
|
||||
) -> color_eyre::Result<()>
|
||||
where
|
||||
F: Fn() -> I + Copy,
|
||||
I: Iterator<Item = Result<win32_display_data::Device, win32_display_data::Error>>,
|
||||
{
|
||||
tracing::info!("listening");
|
||||
|
||||
let receiver = event_rx();
|
||||
@@ -296,7 +305,7 @@ pub fn handle_notifications(wm: Arc<Mutex<WindowManager>>) -> color_eyre::Result
|
||||
let initial_monitor_count = wm.monitors().len();
|
||||
|
||||
// Get the currently attached display devices
|
||||
let attached_devices = attached_display_devices()?;
|
||||
let attached_devices = attached_display_devices(display_provider)?;
|
||||
|
||||
// Make sure that in our state any attached displays have the latest Win32 data
|
||||
for monitor in wm.monitors_mut() {
|
||||
@@ -718,3 +727,304 @@ pub fn handle_notifications(wm: Arc<Mutex<WindowManager>>) -> color_eyre::Result
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use crate::window_manager_event::WindowManagerEvent;
|
||||
use crossbeam_channel::bounded;
|
||||
use crossbeam_channel::Sender;
|
||||
use std::path::PathBuf;
|
||||
use uuid::Uuid;
|
||||
use windows::Win32::Devices::Display::DISPLAYCONFIG_VIDEO_OUTPUT_TECHNOLOGY;
|
||||
// NOTE: Using RECT instead of RECT since I get a mismatched type error. Can be updated if
|
||||
// needed.
|
||||
use windows::Win32::Foundation::RECT;
|
||||
|
||||
// Creating a Mock Display Provider
|
||||
#[derive(Clone)]
|
||||
struct MockDevice {
|
||||
hmonitor: isize,
|
||||
device_path: String,
|
||||
device_name: String,
|
||||
device_description: String,
|
||||
serial_number_id: Option<String>,
|
||||
size: RECT,
|
||||
work_area_size: RECT,
|
||||
device_key: String,
|
||||
output_technology: Option<DISPLAYCONFIG_VIDEO_OUTPUT_TECHNOLOGY>,
|
||||
}
|
||||
|
||||
impl From<MockDevice> for win32_display_data::Device {
|
||||
fn from(mock: MockDevice) -> Self {
|
||||
win32_display_data::Device {
|
||||
hmonitor: mock.hmonitor,
|
||||
device_path: mock.device_path,
|
||||
device_name: mock.device_name,
|
||||
device_description: mock.device_description,
|
||||
serial_number_id: mock.serial_number_id,
|
||||
size: mock.size,
|
||||
work_area_size: mock.work_area_size,
|
||||
device_key: mock.device_key,
|
||||
output_technology: mock.output_technology,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Creating a Window Manager Instance
|
||||
struct TestContext {
|
||||
socket_path: Option<PathBuf>,
|
||||
}
|
||||
|
||||
impl Drop for TestContext {
|
||||
fn drop(&mut self) {
|
||||
if let Some(socket_path) = &self.socket_path {
|
||||
// Clean up the socket file
|
||||
if let Err(e) = std::fs::remove_file(socket_path) {
|
||||
tracing::warn!("Failed to remove socket file: {}", e);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn setup_window_manager() -> (WindowManager, TestContext) {
|
||||
let (_sender, receiver): (Sender<WindowManagerEvent>, Receiver<WindowManagerEvent>) =
|
||||
bounded(1);
|
||||
|
||||
// Temporary socket path for testing
|
||||
let socket_name = format!("komorebi-test-{}.sock", Uuid::new_v4());
|
||||
let socket_path = PathBuf::from(socket_name);
|
||||
|
||||
// Create a new WindowManager instance
|
||||
let wm = match WindowManager::new(receiver, Some(socket_path.clone())) {
|
||||
Ok(manager) => manager,
|
||||
Err(e) => {
|
||||
panic!("Failed to create WindowManager: {}", e);
|
||||
}
|
||||
};
|
||||
|
||||
(
|
||||
wm,
|
||||
TestContext {
|
||||
socket_path: Some(socket_path),
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_send_notification() {
|
||||
// Create a monitor notification
|
||||
let notification = MonitorNotification::ResolutionScalingChanged;
|
||||
|
||||
// Use the send_notification function to send the notification
|
||||
send_notification(notification);
|
||||
|
||||
// Receive the notification from the channel
|
||||
let received = event_rx().try_recv();
|
||||
|
||||
// Check if we received the notification and if it matches what we sent
|
||||
match received {
|
||||
Ok(notification) => {
|
||||
assert_eq!(notification, MonitorNotification::ResolutionScalingChanged);
|
||||
}
|
||||
Err(e) => panic!("Failed to receive MonitorNotification: {}", e),
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_channel_bounded_capacity() {
|
||||
let (_, receiver) = channel();
|
||||
|
||||
// Fill the channel to its capacity (20 messages)
|
||||
for _ in 0..20 {
|
||||
send_notification(MonitorNotification::WorkAreaChanged);
|
||||
}
|
||||
|
||||
// Attempt to send another message (should be dropped)
|
||||
send_notification(MonitorNotification::ResolutionScalingChanged);
|
||||
|
||||
// Verify the channel contains only the first 20 messages
|
||||
for _ in 0..20 {
|
||||
let notification = match receiver.try_recv() {
|
||||
Ok(notification) => notification,
|
||||
Err(e) => panic!("Failed to receive MonitorNotification: {}", e),
|
||||
};
|
||||
assert_eq!(
|
||||
notification,
|
||||
MonitorNotification::WorkAreaChanged,
|
||||
"Unexpected notification in the channel"
|
||||
);
|
||||
}
|
||||
|
||||
// Verify that no additional messages are in the channel
|
||||
assert!(
|
||||
receiver.try_recv().is_err(),
|
||||
"Channel should be empty after consuming all messages"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_insert_in_monitor_cache() {
|
||||
let m = monitor::new(
|
||||
0,
|
||||
Rect::default(),
|
||||
Rect::default(),
|
||||
"Test Monitor".to_string(),
|
||||
"Test Device".to_string(),
|
||||
"Test Device ID".to_string(),
|
||||
Some("TestMonitorID".to_string()),
|
||||
);
|
||||
|
||||
// Insert the monitor into the cache
|
||||
insert_in_monitor_cache("TestMonitorID", m.clone());
|
||||
|
||||
// Retrieve the monitor from the cache
|
||||
let cache = MONITOR_CACHE
|
||||
.get_or_init(|| Mutex::new(HashMap::new()))
|
||||
.lock();
|
||||
let retrieved_monitor = cache.get("TestMonitorID");
|
||||
|
||||
// Check that the monitor was inserted correctly and matches the expected value
|
||||
assert_eq!(retrieved_monitor, Some(&m));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_insert_two_monitors_cache() {
|
||||
let m1 = monitor::new(
|
||||
0,
|
||||
Rect::default(),
|
||||
Rect::default(),
|
||||
"Test Monitor".to_string(),
|
||||
"Test Device".to_string(),
|
||||
"Test Device ID".to_string(),
|
||||
Some("TestMonitorID".to_string()),
|
||||
);
|
||||
|
||||
let m2 = monitor::new(
|
||||
0,
|
||||
Rect::default(),
|
||||
Rect::default(),
|
||||
"Test Monitor 2".to_string(),
|
||||
"Test Device 2".to_string(),
|
||||
"Test Device ID 2".to_string(),
|
||||
Some("TestMonitorID2".to_string()),
|
||||
);
|
||||
|
||||
// Insert the first monitor into the cache
|
||||
insert_in_monitor_cache("TestMonitorID", m1.clone());
|
||||
|
||||
// Insert the second monitor into the cache
|
||||
insert_in_monitor_cache("TestMonitorID2", m2.clone());
|
||||
|
||||
// Retrieve the cache to check if the first and second monitors are present
|
||||
let cache = MONITOR_CACHE
|
||||
.get_or_init(|| Mutex::new(HashMap::new()))
|
||||
.lock();
|
||||
|
||||
// Check if Monitor 1 was found in the cache
|
||||
assert_eq!(
|
||||
cache.get("TestMonitorID"),
|
||||
Some(&m1),
|
||||
"Monitor cache should contain monitor 1"
|
||||
);
|
||||
|
||||
// Check if Monitor 2 was found in the cache
|
||||
assert_eq!(
|
||||
cache.get("TestMonitorID2"),
|
||||
Some(&m2),
|
||||
"Monitor cache should contain monitor 2"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_listen_for_notifications() {
|
||||
// Create a WindowManager instance for testing
|
||||
let (wm, _test_context) = setup_window_manager();
|
||||
|
||||
// Start the notification listener
|
||||
let result = listen_for_notifications(Arc::new(Mutex::new(wm)));
|
||||
|
||||
// Check if the listener started successfully
|
||||
assert!(result.is_ok(), "Failed to start notification listener");
|
||||
|
||||
// Test sending a notification
|
||||
send_notification(MonitorNotification::DisplayConnectionChange);
|
||||
|
||||
// Receive the notification from the channel
|
||||
let received = event_rx().try_recv();
|
||||
|
||||
// Check if we received the notification and if it matches what we sent
|
||||
match received {
|
||||
Ok(notification) => {
|
||||
assert_eq!(notification, MonitorNotification::DisplayConnectionChange);
|
||||
}
|
||||
Err(e) => panic!("Failed to receive MonitorNotification: {}", e),
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_attached_display_devices() {
|
||||
// Define mock display data
|
||||
let mock_monitor = MockDevice {
|
||||
hmonitor: 1,
|
||||
device_path: String::from(
|
||||
"\\\\?\\DISPLAY#ABC123#4&123456&0&UID0#{saucepackets-4321-5678-2468-abc123456789}",
|
||||
),
|
||||
device_name: String::from("\\\\.\\DISPLAY1"),
|
||||
device_description: String::from("Display description"),
|
||||
serial_number_id: Some(String::from("SaucePackets123")),
|
||||
device_key: String::from("Mock Key"),
|
||||
size: RECT {
|
||||
left: 0,
|
||||
top: 0,
|
||||
right: 1920,
|
||||
bottom: 1080,
|
||||
},
|
||||
work_area_size: RECT {
|
||||
left: 0,
|
||||
top: 0,
|
||||
right: 1920,
|
||||
bottom: 1080,
|
||||
},
|
||||
output_technology: Some(DISPLAYCONFIG_VIDEO_OUTPUT_TECHNOLOGY(0)),
|
||||
};
|
||||
|
||||
// Create a closure to simulate the display provider
|
||||
let display_provider = || {
|
||||
vec![Ok::<win32_display_data::Device, win32_display_data::Error>(
|
||||
win32_display_data::Device::from(mock_monitor.clone()),
|
||||
)]
|
||||
.into_iter()
|
||||
};
|
||||
|
||||
// Should contain the mock monitor
|
||||
let result = attached_display_devices(display_provider).ok();
|
||||
if let Some(monitors) = result {
|
||||
// Check Number of monitors
|
||||
assert_eq!(monitors.len(), 1, "Expected one monitor");
|
||||
|
||||
// hmonitor
|
||||
assert_eq!(monitors[0].id(), 1);
|
||||
|
||||
// device name
|
||||
assert_eq!(monitors[0].name(), &String::from("DISPLAY1"));
|
||||
|
||||
// Device
|
||||
assert_eq!(monitors[0].device(), &String::from("ABC123"));
|
||||
|
||||
// Device ID
|
||||
assert_eq!(
|
||||
monitors[0].device_id(),
|
||||
&String::from("ABC123-4&123456&0&UID0")
|
||||
);
|
||||
|
||||
// Check monitor serial number id
|
||||
assert_eq!(
|
||||
monitors[0].serial_number_id,
|
||||
Some(String::from("SaucePackets123")),
|
||||
);
|
||||
} else {
|
||||
panic!("No monitors found");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -640,6 +640,67 @@ impl WindowManager {
|
||||
SocketMessage::AdjustWorkspacePadding(sizing, adjustment) => {
|
||||
self.adjust_workspace_padding(sizing, adjustment)?;
|
||||
}
|
||||
SocketMessage::MoveContainerToLastWorkspace => {
|
||||
// This is to ensure that even on an empty workspace on a secondary monitor, the
|
||||
// secondary monitor where the cursor is focused will be used as the target for
|
||||
// the workspace switch op
|
||||
if let Some(monitor_idx) = self.monitor_idx_from_current_pos() {
|
||||
if monitor_idx != self.focused_monitor_idx() {
|
||||
if let Some(monitor) = self.monitors().get(monitor_idx) {
|
||||
if let Some(workspace) = monitor.focused_workspace() {
|
||||
if workspace.is_empty() {
|
||||
self.focus_monitor(monitor_idx)?;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
let idx = self
|
||||
.focused_monitor()
|
||||
.ok_or_else(|| anyhow!("there is no monitor"))?
|
||||
.focused_workspace_idx();
|
||||
|
||||
if let Some(monitor) = self.focused_monitor_mut() {
|
||||
if let Some(last_focused_workspace) = monitor.last_focused_workspace() {
|
||||
self.move_container_to_workspace(last_focused_workspace, true, None)?;
|
||||
}
|
||||
}
|
||||
|
||||
self.focused_monitor_mut()
|
||||
.ok_or_else(|| anyhow!("there is no monitor"))?
|
||||
.set_last_focused_workspace(Option::from(idx));
|
||||
}
|
||||
SocketMessage::SendContainerToLastWorkspace => {
|
||||
// This is to ensure that even on an empty workspace on a secondary monitor, the
|
||||
// secondary monitor where the cursor is focused will be used as the target for
|
||||
// the workspace switch op
|
||||
if let Some(monitor_idx) = self.monitor_idx_from_current_pos() {
|
||||
if monitor_idx != self.focused_monitor_idx() {
|
||||
if let Some(monitor) = self.monitors().get(monitor_idx) {
|
||||
if let Some(workspace) = monitor.focused_workspace() {
|
||||
if workspace.is_empty() {
|
||||
self.focus_monitor(monitor_idx)?;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
let idx = self
|
||||
.focused_monitor()
|
||||
.ok_or_else(|| anyhow!("there is no monitor"))?
|
||||
.focused_workspace_idx();
|
||||
|
||||
if let Some(monitor) = self.focused_monitor_mut() {
|
||||
if let Some(last_focused_workspace) = monitor.last_focused_workspace() {
|
||||
self.move_container_to_workspace(last_focused_workspace, false, None)?;
|
||||
}
|
||||
}
|
||||
self.focused_monitor_mut()
|
||||
.ok_or_else(|| anyhow!("there is no monitor"))?
|
||||
.set_last_focused_workspace(Option::from(idx));
|
||||
}
|
||||
SocketMessage::MoveContainerToWorkspaceNumber(workspace_idx) => {
|
||||
self.move_container_to_workspace(workspace_idx, true, None)?;
|
||||
}
|
||||
@@ -1136,11 +1197,33 @@ impl WindowManager {
|
||||
WorkspaceLayer::Tiling => {
|
||||
workspace.set_layer(WorkspaceLayer::Floating);
|
||||
|
||||
for (i, window) in workspace.floating_windows().iter().enumerate() {
|
||||
if i == 0 {
|
||||
let focused_idx = workspace.focused_floating_window_idx();
|
||||
let mut window_idx_pairs = workspace
|
||||
.floating_windows_mut()
|
||||
.make_contiguous()
|
||||
.iter()
|
||||
.enumerate()
|
||||
.collect::<Vec<_>>();
|
||||
|
||||
// Sort by window area
|
||||
window_idx_pairs.sort_by_key(|(_, w)| {
|
||||
let rect = WindowsApi::window_rect(w.hwnd).unwrap_or_default();
|
||||
rect.right * rect.bottom
|
||||
});
|
||||
window_idx_pairs.reverse();
|
||||
|
||||
for (i, window) in window_idx_pairs {
|
||||
if i == focused_idx {
|
||||
to_focus = Some(*window);
|
||||
} else {
|
||||
window.raise()?;
|
||||
}
|
||||
window.raise()?;
|
||||
}
|
||||
|
||||
if let Some(focused_window) = &to_focus {
|
||||
// The focused window should be the last one raised to make sure it is
|
||||
// on top
|
||||
focused_window.raise()?;
|
||||
}
|
||||
|
||||
for container in workspace.containers() {
|
||||
@@ -1162,7 +1245,19 @@ impl WindowManager {
|
||||
}
|
||||
}
|
||||
|
||||
for window in workspace.floating_windows() {
|
||||
let mut window_idx_pairs = workspace
|
||||
.floating_windows_mut()
|
||||
.make_contiguous()
|
||||
.iter()
|
||||
.collect::<Vec<_>>();
|
||||
|
||||
// Sort by window area
|
||||
window_idx_pairs.sort_by_key(|w| {
|
||||
let rect = WindowsApi::window_rect(w.hwnd).unwrap_or_default();
|
||||
rect.right * rect.bottom
|
||||
});
|
||||
|
||||
for window in window_idx_pairs {
|
||||
window.lower()?;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,7 +1,5 @@
|
||||
use std::sync::atomic::Ordering;
|
||||
use std::sync::Arc;
|
||||
use std::time::Duration;
|
||||
use std::time::Instant;
|
||||
|
||||
use color_eyre::eyre::anyhow;
|
||||
use color_eyre::Result;
|
||||
@@ -27,12 +25,10 @@ use crate::window_manager_event::WindowManagerEvent;
|
||||
use crate::windows_api::WindowsApi;
|
||||
use crate::winevent::WinEvent;
|
||||
use crate::workspace::WorkspaceLayer;
|
||||
use crate::workspace_reconciliator;
|
||||
use crate::workspace_reconciliator::ALT_TAB_HWND;
|
||||
use crate::workspace_reconciliator::ALT_TAB_HWND_INSTANT;
|
||||
use crate::Notification;
|
||||
use crate::NotificationEvent;
|
||||
use crate::State;
|
||||
use crate::Window;
|
||||
use crate::FLOATING_APPLICATIONS;
|
||||
use crate::HIDDEN_HWNDS;
|
||||
use crate::REGEX_IDENTIFIERS;
|
||||
@@ -300,38 +296,11 @@ impl WindowManager {
|
||||
tracing::info!("ignoring uncloak after monocle move by mouse across monitors");
|
||||
self.uncloack_to_ignore = self.uncloack_to_ignore.saturating_sub(1);
|
||||
} else {
|
||||
let focused_monitor_idx = self.focused_monitor_idx();
|
||||
let focused_workspace_idx =
|
||||
let mut focused_monitor_idx = self.focused_monitor_idx();
|
||||
let mut focused_workspace_idx =
|
||||
self.focused_workspace_idx_for_monitor_idx(focused_monitor_idx)?;
|
||||
|
||||
let focused_pair = (focused_monitor_idx, focused_workspace_idx);
|
||||
|
||||
let mut needs_reconciliation = false;
|
||||
|
||||
if let Some((m_idx, w_idx)) = self.known_hwnds.get(&window.hwnd) {
|
||||
if focused_pair != (*m_idx, *w_idx) {
|
||||
// At this point we know we are going to send a notification to the workspace reconciliator
|
||||
// So we get the topmost window returned by EnumWindows, which is almost always the window
|
||||
// that has been selected by alt-tab
|
||||
if let Ok(alt_tab_windows) = WindowsApi::alt_tab_windows() {
|
||||
if let Some(first) =
|
||||
alt_tab_windows.iter().find(|w| w.title().is_ok())
|
||||
{
|
||||
// If our record of this HWND hasn't been updated in over a minute
|
||||
let mut instant = ALT_TAB_HWND_INSTANT.lock();
|
||||
if instant.elapsed().gt(&Duration::from_secs(1)) {
|
||||
// Update our record with the HWND we just found
|
||||
ALT_TAB_HWND.store(Some(first.hwnd));
|
||||
// Update the timestamp of our record
|
||||
*instant = Instant::now();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
workspace_reconciliator::send_notification(*m_idx, *w_idx);
|
||||
needs_reconciliation = true;
|
||||
}
|
||||
}
|
||||
let mut needs_reconciliation = None;
|
||||
|
||||
// There are some applications such as Firefox where, if they are focused when a
|
||||
// workspace switch takes place, it will fire an additional Show event, which will
|
||||
@@ -340,6 +309,23 @@ impl WindowManager {
|
||||
// duplicates across multiple workspaces, as it results in ghost layout tiles.
|
||||
let mut proceed = true;
|
||||
|
||||
// Check for potential `alt-tab` event
|
||||
if matches!(
|
||||
event,
|
||||
WindowManagerEvent::Uncloak(_, _) | WindowManagerEvent::Show(_, _)
|
||||
) {
|
||||
needs_reconciliation = self.needs_reconciliation(window)?;
|
||||
|
||||
if let Some((m_idx, ws_idx)) = needs_reconciliation {
|
||||
self.perform_reconciliation(window, (m_idx, ws_idx))?;
|
||||
|
||||
// Since there was a reconciliation after an `alt-tab`, that means this
|
||||
// window is already handled by komorebi so we shouldn't proceed with
|
||||
// adding it as a new window.
|
||||
proceed = false;
|
||||
}
|
||||
}
|
||||
|
||||
if let Some((m_idx, w_idx)) = self.known_hwnds.get(&window.hwnd) {
|
||||
if let Some(focused_workspace_idx) = self
|
||||
.monitors()
|
||||
@@ -360,6 +346,18 @@ impl WindowManager {
|
||||
}
|
||||
|
||||
if proceed {
|
||||
if matches!(event, WindowManagerEvent::Show(_, _)) {
|
||||
let initial_monitor_idx = initial_state.monitors.focused_idx();
|
||||
if focused_monitor_idx != initial_monitor_idx {
|
||||
tracing::info!("assuming focused monitor index should be {initial_monitor_idx} for WindowManagerEvent::Show");
|
||||
self.focus_monitor(initial_monitor_idx)?;
|
||||
}
|
||||
|
||||
focused_monitor_idx = self.focused_monitor_idx();
|
||||
focused_workspace_idx =
|
||||
self.focused_workspace_idx_for_monitor_idx(focused_monitor_idx)?;
|
||||
}
|
||||
|
||||
let mut behaviour = self.window_management_behaviour(
|
||||
focused_monitor_idx,
|
||||
focused_workspace_idx,
|
||||
@@ -368,7 +366,7 @@ impl WindowManager {
|
||||
let workspace_contains_window = workspace.contains_window(window.hwnd);
|
||||
let monocle_container = workspace.monocle_container().clone();
|
||||
|
||||
if !workspace_contains_window && !needs_reconciliation {
|
||||
if !workspace_contains_window && needs_reconciliation.is_none() {
|
||||
let floating_applications = FLOATING_APPLICATIONS.lock();
|
||||
let mut should_float = false;
|
||||
|
||||
@@ -402,7 +400,7 @@ impl WindowManager {
|
||||
matches!(workspace.layer, WorkspaceLayer::Floating)
|
||||
&& !should_float
|
||||
&& workspace.tile;
|
||||
workspace.floating_windows_mut().push(window);
|
||||
workspace.floating_windows_mut().push_back(window);
|
||||
workspace.set_layer(WorkspaceLayer::Floating);
|
||||
if center_spawned_floats {
|
||||
let mut floating_window = window;
|
||||
@@ -630,7 +628,7 @@ impl WindowManager {
|
||||
window.focus(self.mouse_follows_focus)?;
|
||||
}
|
||||
} else if window_management_behaviour.float_override {
|
||||
workspace.floating_windows_mut().push(window);
|
||||
workspace.floating_windows_mut().push_back(window);
|
||||
self.update_focused_workspace(false, false)?;
|
||||
} else {
|
||||
match window_management_behaviour.current_behaviour {
|
||||
@@ -753,4 +751,119 @@ impl WindowManager {
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Checks if this window is from another unfocused workspace or is an unfocused window on a
|
||||
/// stack container. If it is it will return the monitor/workspace index pair of this window so
|
||||
/// that a reconciliation of that monitor/workspace can be done.
|
||||
fn needs_reconciliation(&self, window: Window) -> color_eyre::Result<Option<(usize, usize)>> {
|
||||
let focused_monitor_idx = self.focused_monitor_idx();
|
||||
let focused_workspace_idx =
|
||||
self.focused_workspace_idx_for_monitor_idx(focused_monitor_idx)?;
|
||||
|
||||
let focused_pair = (focused_monitor_idx, focused_workspace_idx);
|
||||
|
||||
let mut needs_reconciliation = None;
|
||||
|
||||
if let Some((m_idx, ws_idx)) = self.known_hwnds.get(&window.hwnd) {
|
||||
if (*m_idx, *ws_idx) == focused_pair {
|
||||
if let Some(target_workspace) = self
|
||||
.monitors()
|
||||
.get(*m_idx)
|
||||
.and_then(|m| m.workspaces().get(*ws_idx))
|
||||
{
|
||||
if let Some(monocle_with_window) = target_workspace
|
||||
.monocle_container()
|
||||
.as_ref()
|
||||
.and_then(|m| m.contains_window(window.hwnd).then_some(m))
|
||||
{
|
||||
if monocle_with_window.focused_window() != Some(&window) {
|
||||
tracing::debug!("Needs reconciliation within a monocled stack");
|
||||
needs_reconciliation = Some((*m_idx, *ws_idx));
|
||||
}
|
||||
} else {
|
||||
let c_idx = target_workspace.container_idx_for_window(window.hwnd);
|
||||
|
||||
if let Some(target_container) =
|
||||
c_idx.and_then(|c_idx| target_workspace.containers().get(c_idx))
|
||||
{
|
||||
if target_container.focused_window() != Some(&window) {
|
||||
tracing::debug!(
|
||||
"Needs reconciliation within a stack on the focused workspace"
|
||||
);
|
||||
needs_reconciliation = Some((*m_idx, *ws_idx));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
} else {
|
||||
tracing::debug!("Needs reconciliation for a different monitor/workspace pair");
|
||||
needs_reconciliation = Some((*m_idx, *ws_idx));
|
||||
}
|
||||
}
|
||||
|
||||
Ok(needs_reconciliation)
|
||||
}
|
||||
|
||||
/// When there was an `alt-tab` to a hidden window we need to perform a reconciliation, meaning
|
||||
/// we need to update the focused monitor, workspace, container and window indices to the ones
|
||||
/// corresponding to the window the user just alt-tabbed into.
|
||||
fn perform_reconciliation(
|
||||
&mut self,
|
||||
window: Window,
|
||||
reconciliation_pair: (usize, usize),
|
||||
) -> color_eyre::Result<()> {
|
||||
let (m_idx, ws_idx) = reconciliation_pair;
|
||||
|
||||
tracing::debug!("performing reconciliation");
|
||||
self.focus_monitor(m_idx)?;
|
||||
let mouse_follows_focus = self.mouse_follows_focus;
|
||||
let offset = self.work_area_offset;
|
||||
|
||||
if let Some(monitor) = self.focused_monitor_mut() {
|
||||
if ws_idx != monitor.focused_workspace_idx() {
|
||||
let previous_idx = monitor.focused_workspace_idx();
|
||||
monitor.set_last_focused_workspace(Option::from(previous_idx));
|
||||
monitor.focus_workspace(ws_idx)?;
|
||||
}
|
||||
if let Some(workspace) = monitor.focused_workspace_mut() {
|
||||
let mut layer = WorkspaceLayer::Tiling;
|
||||
if let Some((monocle, idx)) = workspace
|
||||
.monocle_container_mut()
|
||||
.as_mut()
|
||||
.and_then(|m| m.idx_for_window(window.hwnd).map(|i| (m, i)))
|
||||
{
|
||||
monocle.focus_window(idx);
|
||||
} else if workspace
|
||||
.floating_windows()
|
||||
.iter()
|
||||
.any(|w| w.hwnd == window.hwnd)
|
||||
{
|
||||
layer = WorkspaceLayer::Floating;
|
||||
} else if !workspace
|
||||
.maximized_window()
|
||||
.is_some_and(|w| w.hwnd == window.hwnd)
|
||||
{
|
||||
// If the window is the maximized window do nothing, else we
|
||||
// reintegrate the monocle if it exists and then focus the
|
||||
// container
|
||||
if workspace.monocle_container().is_some() {
|
||||
tracing::info!("disabling monocle");
|
||||
for container in workspace.containers_mut() {
|
||||
container.restore();
|
||||
}
|
||||
for window in workspace.floating_windows_mut() {
|
||||
window.restore();
|
||||
}
|
||||
workspace.reintegrate_monocle_container()?;
|
||||
}
|
||||
workspace.focus_container_by_window(window.hwnd)?;
|
||||
}
|
||||
workspace.set_layer(layer);
|
||||
}
|
||||
monitor.load_focused_workspace(mouse_follows_focus)?;
|
||||
monitor.update_focused_workspace(offset)?;
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
@@ -76,4 +76,36 @@ macro_rules! impl_ring_elements {
|
||||
}
|
||||
}
|
||||
};
|
||||
// This allows passing a different name to be used for the functions. For instance, the
|
||||
// `floating_windows` ring calls this as:
|
||||
// ```rust
|
||||
// impl_ring_elements!(Workspace, Window, "floating_window");
|
||||
// ```
|
||||
// Which allows using the `Window` element but name the functions as `floating_window`
|
||||
($name:ty, $element:ident, $el_name:literal) => {
|
||||
paste::paste! {
|
||||
impl $name {
|
||||
pub const fn [<$el_name:lower s>](&self) -> &VecDeque<$element> {
|
||||
self.[<$el_name:lower s>].elements()
|
||||
}
|
||||
|
||||
pub fn [<$el_name:lower s_mut>](&mut self) -> &mut VecDeque<$element> {
|
||||
self.[<$el_name:lower s>].elements_mut()
|
||||
}
|
||||
|
||||
#[allow(dead_code)]
|
||||
pub fn [<focused_ $el_name:lower>](&self) -> Option<&$element> {
|
||||
self.[<$el_name:lower s>].focused()
|
||||
}
|
||||
|
||||
pub const fn [<focused_ $el_name:lower _idx>](&self) -> usize {
|
||||
self.[<$el_name:lower s>].focused_idx()
|
||||
}
|
||||
|
||||
pub fn [<focused_ $el_name:lower _mut>](&mut self) -> Option<&mut $element> {
|
||||
self.[<$el_name:lower s>].focused_mut()
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
@@ -60,6 +60,7 @@ use crate::workspace::Workspace;
|
||||
use crate::AspectRatio;
|
||||
use crate::Axis;
|
||||
use crate::CrossBoundaryBehaviour;
|
||||
use crate::FloatingLayerBehaviour;
|
||||
use crate::PredefinedAspectRatio;
|
||||
use crate::DATA_DIR;
|
||||
use crate::DEFAULT_CONTAINER_PADDING;
|
||||
@@ -164,6 +165,9 @@ pub struct WorkspaceConfig {
|
||||
/// Specify an axis on which to flip the selected layout (default: None)
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub layout_flip: Option<Axis>,
|
||||
/// Determine what happens to a new window when the Floating workspace layer is active (default: Tile)
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub floating_layer_behaviour: Option<FloatingLayerBehaviour>,
|
||||
}
|
||||
|
||||
impl From<&Workspace> for WorkspaceConfig {
|
||||
@@ -239,6 +243,7 @@ impl From<&Workspace> for WorkspaceConfig {
|
||||
window_container_behaviour_rules: Option::from(window_container_behaviour_rules),
|
||||
float_override: *value.float_override(),
|
||||
layout_flip: value.layout_flip(),
|
||||
floating_layer_behaviour: Option::from(*value.floating_layer_behaviour()),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -80,6 +80,7 @@ use crate::workspace::WorkspaceLayer;
|
||||
use crate::BorderColours;
|
||||
use crate::Colour;
|
||||
use crate::CrossBoundaryBehaviour;
|
||||
use crate::FloatingLayerBehaviour;
|
||||
use crate::Rgb;
|
||||
use crate::CUSTOM_FFM;
|
||||
use crate::DATA_DIR;
|
||||
@@ -341,6 +342,7 @@ impl From<&WindowManager> for State {
|
||||
.clone(),
|
||||
float_override: workspace.float_override,
|
||||
layer: workspace.layer,
|
||||
floating_layer_behaviour: workspace.floating_layer_behaviour,
|
||||
globals: workspace.globals,
|
||||
locked_containers: workspace.locked_containers.clone(),
|
||||
workspace_config: None,
|
||||
@@ -644,10 +646,14 @@ impl WindowManager {
|
||||
self.window_management_behaviour.float_override
|
||||
};
|
||||
|
||||
// If the workspace layer is `Floating`, then consider it as if it had float
|
||||
// override so that new windows spawn as floating
|
||||
float_override =
|
||||
float_override || matches!(workspace.layer, WorkspaceLayer::Floating);
|
||||
// If the workspace layer is `Floating` and the floating layer behaviour is `Float`,
|
||||
// then consider it as if it had float override so that new windows spawn as floating
|
||||
float_override = float_override
|
||||
|| (matches!(workspace.layer, WorkspaceLayer::Floating)
|
||||
&& matches!(
|
||||
workspace.floating_layer_behaviour,
|
||||
FloatingLayerBehaviour::Float
|
||||
));
|
||||
|
||||
return WindowManagementBehaviour {
|
||||
current_behaviour,
|
||||
@@ -968,7 +974,7 @@ impl WindowManager {
|
||||
if op.floating {
|
||||
target_workspace
|
||||
.floating_windows_mut()
|
||||
.push(Window::from(op.hwnd));
|
||||
.push_back(Window::from(op.hwnd));
|
||||
} else {
|
||||
//TODO(alex-ds13): should this take into account the target workspace
|
||||
//`window_container_behaviour`?
|
||||
@@ -1145,18 +1151,18 @@ impl WindowManager {
|
||||
// There is no need to physically move the floating window between areas with
|
||||
// `move_to_area` because the user already did that, so we only need to transfer the
|
||||
// window to the target `floating_windows`
|
||||
let floating_window = origin_workspace.floating_windows_mut().remove(idx);
|
||||
if let Some(floating_window) = origin_workspace.floating_windows_mut().remove(idx) {
|
||||
let target_workspace = self
|
||||
.monitors_mut()
|
||||
.get_mut(target_monitor_idx)
|
||||
.ok_or_else(|| anyhow!("there is no monitor at this idx"))?
|
||||
.focused_workspace_mut()
|
||||
.ok_or_else(|| anyhow!("there is no focused workspace for this monitor"))?;
|
||||
|
||||
let target_workspace = self
|
||||
.monitors_mut()
|
||||
.get_mut(target_monitor_idx)
|
||||
.ok_or_else(|| anyhow!("there is no monitor at this idx"))?
|
||||
.focused_workspace_mut()
|
||||
.ok_or_else(|| anyhow!("there is no focused workspace for this monitor"))?;
|
||||
|
||||
target_workspace
|
||||
.floating_windows_mut()
|
||||
.push(floating_window);
|
||||
target_workspace
|
||||
.floating_windows_mut()
|
||||
.push_back(floating_window);
|
||||
}
|
||||
} else if origin_workspace
|
||||
.monocle_container()
|
||||
.as_ref()
|
||||
@@ -1830,7 +1836,7 @@ impl WindowManager {
|
||||
.position(|w| w.hwnd == foreground_hwnd);
|
||||
|
||||
let floating_window =
|
||||
floating_window_index.map(|idx| workspace.floating_windows_mut().remove(idx));
|
||||
floating_window_index.and_then(|idx| workspace.floating_windows_mut().remove(idx));
|
||||
let container = if floating_window_index.is_none() {
|
||||
Some(
|
||||
workspace
|
||||
@@ -1859,7 +1865,7 @@ impl WindowManager {
|
||||
.ok_or_else(|| anyhow!("there is no focused workspace on target monitor"))?;
|
||||
|
||||
if let Some(window) = floating_window {
|
||||
target_workspace.floating_windows_mut().push(window);
|
||||
target_workspace.floating_windows_mut().push_back(window);
|
||||
target_workspace.set_layer(WorkspaceLayer::Floating);
|
||||
Window::from(window.hwnd)
|
||||
.move_to_area(¤t_area, target_monitor.work_area_size())?;
|
||||
@@ -1978,45 +1984,251 @@ impl WindowManager {
|
||||
direction: OperationDirection,
|
||||
) -> Result<()> {
|
||||
let mouse_follows_focus = self.mouse_follows_focus;
|
||||
let focused_workspace = self.focused_workspace()?;
|
||||
let focused_workspace = self.focused_workspace_mut()?;
|
||||
|
||||
let mut target_idx = None;
|
||||
let len = focused_workspace.floating_windows().len();
|
||||
|
||||
if len > 1 {
|
||||
let focused_hwnd = WindowsApi::foreground_window()?;
|
||||
for (idx, window) in focused_workspace.floating_windows().iter().enumerate() {
|
||||
if window.hwnd == focused_hwnd {
|
||||
match direction {
|
||||
OperationDirection::Left => {}
|
||||
OperationDirection::Right => {}
|
||||
OperationDirection::Up => {
|
||||
if idx == len - 1 {
|
||||
target_idx = Some(0)
|
||||
} else {
|
||||
target_idx = Some(idx + 1)
|
||||
}
|
||||
}
|
||||
OperationDirection::Down => {
|
||||
if idx == 0 {
|
||||
target_idx = Some(len - 1)
|
||||
} else {
|
||||
target_idx = Some(idx - 1)
|
||||
}
|
||||
}
|
||||
let focused_rect = WindowsApi::window_rect(focused_hwnd)?;
|
||||
match direction {
|
||||
OperationDirection::Left => {
|
||||
let mut windows_in_direction = focused_workspace
|
||||
.floating_windows()
|
||||
.iter()
|
||||
.enumerate()
|
||||
.flat_map(|(idx, w)| {
|
||||
(w.hwnd != focused_hwnd)
|
||||
.then_some(WindowsApi::window_rect(w.hwnd).ok().map(|r| (idx, r)))
|
||||
})
|
||||
.flatten()
|
||||
.flat_map(|(idx, r)| {
|
||||
(r.left < focused_rect.left)
|
||||
.then_some((idx, i32::abs(r.left - focused_rect.left)))
|
||||
})
|
||||
.collect::<Vec<_>>();
|
||||
|
||||
// Sort by distance to focused
|
||||
windows_in_direction.sort_by_key(|(_, d)| (*d as f32 * 1000.0).trunc() as i32);
|
||||
|
||||
if let Some((idx, _)) = windows_in_direction.first() {
|
||||
target_idx = Some(*idx);
|
||||
}
|
||||
}
|
||||
}
|
||||
OperationDirection::Right => {
|
||||
let mut windows_in_direction = focused_workspace
|
||||
.floating_windows()
|
||||
.iter()
|
||||
.enumerate()
|
||||
.flat_map(|(idx, w)| {
|
||||
(w.hwnd != focused_hwnd)
|
||||
.then_some(WindowsApi::window_rect(w.hwnd).ok().map(|r| (idx, r)))
|
||||
})
|
||||
.flatten()
|
||||
.flat_map(|(idx, r)| {
|
||||
(r.left > focused_rect.left)
|
||||
.then_some((idx, i32::abs(r.left - focused_rect.left)))
|
||||
})
|
||||
.collect::<Vec<_>>();
|
||||
|
||||
if target_idx.is_none() {
|
||||
target_idx = Some(0);
|
||||
}
|
||||
// Sort by distance to focused
|
||||
windows_in_direction.sort_by_key(|(_, d)| (*d as f32 * 1000.0).trunc() as i32);
|
||||
|
||||
if let Some((idx, _)) = windows_in_direction.first() {
|
||||
target_idx = Some(*idx);
|
||||
}
|
||||
}
|
||||
OperationDirection::Up => {
|
||||
let mut windows_in_direction = focused_workspace
|
||||
.floating_windows()
|
||||
.iter()
|
||||
.enumerate()
|
||||
.flat_map(|(idx, w)| {
|
||||
(w.hwnd != focused_hwnd)
|
||||
.then_some(WindowsApi::window_rect(w.hwnd).ok().map(|r| (idx, r)))
|
||||
})
|
||||
.flatten()
|
||||
.flat_map(|(idx, r)| {
|
||||
(r.top < focused_rect.top)
|
||||
.then_some((idx, i32::abs(r.top - focused_rect.top)))
|
||||
})
|
||||
.collect::<Vec<_>>();
|
||||
|
||||
// Sort by distance to focused
|
||||
windows_in_direction.sort_by_key(|(_, d)| (*d as f32 * 1000.0).trunc() as i32);
|
||||
|
||||
if let Some((idx, _)) = windows_in_direction.first() {
|
||||
target_idx = Some(*idx);
|
||||
}
|
||||
}
|
||||
OperationDirection::Down => {
|
||||
let mut windows_in_direction = focused_workspace
|
||||
.floating_windows()
|
||||
.iter()
|
||||
.enumerate()
|
||||
.flat_map(|(idx, w)| {
|
||||
(w.hwnd != focused_hwnd)
|
||||
.then_some(WindowsApi::window_rect(w.hwnd).ok().map(|r| (idx, r)))
|
||||
})
|
||||
.flatten()
|
||||
.flat_map(|(idx, r)| {
|
||||
(r.top > focused_rect.top)
|
||||
.then_some((idx, i32::abs(r.top - focused_rect.top)))
|
||||
})
|
||||
.collect::<Vec<_>>();
|
||||
|
||||
// Sort by distance to focused
|
||||
windows_in_direction.sort_by_key(|(_, d)| (*d as f32 * 1000.0).trunc() as i32);
|
||||
|
||||
if let Some((idx, _)) = windows_in_direction.first() {
|
||||
target_idx = Some(*idx);
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
if let Some(idx) = target_idx {
|
||||
focused_workspace.floating_windows.focus(idx);
|
||||
if let Some(window) = focused_workspace.floating_windows().get(idx) {
|
||||
window.focus(mouse_follows_focus)?;
|
||||
}
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
let mut cross_monitor_monocle_or_max = false;
|
||||
|
||||
let workspace_idx = self.focused_workspace_idx()?;
|
||||
|
||||
// this is for when we are scrolling across workspaces like PaperWM
|
||||
if matches!(
|
||||
self.cross_boundary_behaviour,
|
||||
CrossBoundaryBehaviour::Workspace
|
||||
) && matches!(
|
||||
direction,
|
||||
OperationDirection::Left | OperationDirection::Right
|
||||
) {
|
||||
let workspace_count = if let Some(monitor) = self.focused_monitor() {
|
||||
monitor.workspaces().len()
|
||||
} else {
|
||||
1
|
||||
};
|
||||
|
||||
let next_idx = match direction {
|
||||
OperationDirection::Left => match workspace_idx {
|
||||
0 => workspace_count - 1,
|
||||
n => n - 1,
|
||||
},
|
||||
OperationDirection::Right => match workspace_idx {
|
||||
n if n == workspace_count - 1 => 0,
|
||||
n => n + 1,
|
||||
},
|
||||
_ => workspace_idx,
|
||||
};
|
||||
|
||||
self.focus_workspace(next_idx)?;
|
||||
|
||||
if let Ok(focused_workspace) = self.focused_workspace_mut() {
|
||||
if focused_workspace.monocle_container().is_none() {
|
||||
match direction {
|
||||
OperationDirection::Left => match focused_workspace.layout() {
|
||||
Layout::Default(layout) => {
|
||||
let target_index =
|
||||
layout.rightmost_index(focused_workspace.containers().len());
|
||||
focused_workspace.focus_container(target_index);
|
||||
}
|
||||
Layout::Custom(_) => {
|
||||
focused_workspace.focus_container(
|
||||
focused_workspace.containers().len().saturating_sub(1),
|
||||
);
|
||||
}
|
||||
},
|
||||
OperationDirection::Right => match focused_workspace.layout() {
|
||||
Layout::Default(layout) => {
|
||||
let target_index =
|
||||
layout.leftmost_index(focused_workspace.containers().len());
|
||||
focused_workspace.focus_container(target_index);
|
||||
}
|
||||
Layout::Custom(_) => {
|
||||
focused_workspace.focus_container(0);
|
||||
}
|
||||
},
|
||||
_ => {}
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
// if there is no floating_window in that direction for this workspace
|
||||
let monitor_idx = self
|
||||
.monitor_idx_in_direction(direction)
|
||||
.ok_or_else(|| anyhow!("there is no container or monitor in this direction"))?;
|
||||
|
||||
self.focus_monitor(monitor_idx)?;
|
||||
let mouse_follows_focus = self.mouse_follows_focus;
|
||||
|
||||
if let Ok(focused_workspace) = self.focused_workspace_mut() {
|
||||
if let Some(window) = focused_workspace.maximized_window() {
|
||||
window.focus(mouse_follows_focus)?;
|
||||
cross_monitor_monocle_or_max = true;
|
||||
} else if let Some(monocle) = focused_workspace.monocle_container() {
|
||||
if let Some(window) = monocle.focused_window() {
|
||||
window.focus(mouse_follows_focus)?;
|
||||
cross_monitor_monocle_or_max = true;
|
||||
}
|
||||
} else if focused_workspace.layer() == &WorkspaceLayer::Tiling {
|
||||
match direction {
|
||||
OperationDirection::Left => match focused_workspace.layout() {
|
||||
Layout::Default(layout) => {
|
||||
let target_index =
|
||||
layout.rightmost_index(focused_workspace.containers().len());
|
||||
focused_workspace.focus_container(target_index);
|
||||
}
|
||||
Layout::Custom(_) => {
|
||||
focused_workspace.focus_container(
|
||||
focused_workspace.containers().len().saturating_sub(1),
|
||||
);
|
||||
}
|
||||
},
|
||||
OperationDirection::Right => match focused_workspace.layout() {
|
||||
Layout::Default(layout) => {
|
||||
let target_index =
|
||||
layout.leftmost_index(focused_workspace.containers().len());
|
||||
focused_workspace.focus_container(target_index);
|
||||
}
|
||||
Layout::Custom(_) => {
|
||||
focused_workspace.focus_container(0);
|
||||
}
|
||||
},
|
||||
_ => {}
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
if !cross_monitor_monocle_or_max {
|
||||
let ws = self.focused_workspace_mut()?;
|
||||
if ws.is_empty() {
|
||||
// This is to remove focus from the previous monitor
|
||||
let desktop_window = Window::from(WindowsApi::desktop_window()?);
|
||||
|
||||
match WindowsApi::raise_and_focus_window(desktop_window.hwnd) {
|
||||
Ok(()) => {}
|
||||
Err(error) => {
|
||||
tracing::warn!("{} {}:{}", error, file!(), line!());
|
||||
}
|
||||
}
|
||||
} else if ws.layer() == &WorkspaceLayer::Floating && !ws.floating_windows().is_empty() {
|
||||
if let Some(window) = ws.focused_floating_window() {
|
||||
window.focus(self.mouse_follows_focus)?;
|
||||
}
|
||||
} else {
|
||||
ws.set_layer(WorkspaceLayer::Tiling);
|
||||
if let Ok(focused_window) = self.focused_window() {
|
||||
focused_window.focus(self.mouse_follows_focus)?;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Ok(())
|
||||
@@ -2123,7 +2335,7 @@ impl WindowManager {
|
||||
window.focus(mouse_follows_focus)?;
|
||||
cross_monitor_monocle_or_max = true;
|
||||
}
|
||||
} else {
|
||||
} else if focused_workspace.layer() == &WorkspaceLayer::Tiling {
|
||||
match direction {
|
||||
OperationDirection::Left => match focused_workspace.layout() {
|
||||
Layout::Default(layout) => {
|
||||
@@ -2159,8 +2371,26 @@ impl WindowManager {
|
||||
}
|
||||
|
||||
if !cross_monitor_monocle_or_max {
|
||||
if let Ok(focused_window) = self.focused_window_mut() {
|
||||
focused_window.focus(self.mouse_follows_focus)?;
|
||||
let ws = self.focused_workspace_mut()?;
|
||||
if ws.is_empty() {
|
||||
// This is to remove focus from the previous monitor
|
||||
let desktop_window = Window::from(WindowsApi::desktop_window()?);
|
||||
|
||||
match WindowsApi::raise_and_focus_window(desktop_window.hwnd) {
|
||||
Ok(()) => {}
|
||||
Err(error) => {
|
||||
tracing::warn!("{} {}:{}", error, file!(), line!());
|
||||
}
|
||||
}
|
||||
} else if ws.layer() == &WorkspaceLayer::Floating && !ws.floating_windows().is_empty() {
|
||||
if let Some(window) = ws.focused_floating_window() {
|
||||
window.focus(self.mouse_follows_focus)?;
|
||||
}
|
||||
} else {
|
||||
ws.set_layer(WorkspaceLayer::Tiling);
|
||||
if let Ok(focused_window) = self.focused_window() {
|
||||
focused_window.focus(self.mouse_follows_focus)?;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2875,7 +3105,7 @@ impl WindowManager {
|
||||
|
||||
let window = workspace
|
||||
.floating_windows_mut()
|
||||
.last_mut()
|
||||
.back_mut()
|
||||
.ok_or_else(|| anyhow!("there is no floating window"))?;
|
||||
|
||||
window.center(&work_area)?;
|
||||
|
||||
@@ -22,6 +22,7 @@ use crate::core::DefaultLayout;
|
||||
use crate::core::Layout;
|
||||
use crate::core::OperationDirection;
|
||||
use crate::core::Rect;
|
||||
use crate::FloatingLayerBehaviour;
|
||||
|
||||
use crate::border_manager::BORDER_OFFSET;
|
||||
use crate::border_manager::BORDER_WIDTH;
|
||||
@@ -62,8 +63,7 @@ pub struct Workspace {
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
#[getset(get_copy = "pub", set = "pub")]
|
||||
pub maximized_window_restore_idx: Option<usize>,
|
||||
#[getset(get = "pub", get_mut = "pub")]
|
||||
pub floating_windows: Vec<Window>,
|
||||
pub floating_windows: Ring<Window>,
|
||||
#[getset(get = "pub", get_mut = "pub", set = "pub")]
|
||||
pub layout: Layout,
|
||||
#[getset(get = "pub", get_mut = "pub", set = "pub")]
|
||||
@@ -93,6 +93,8 @@ pub struct Workspace {
|
||||
#[getset(get = "pub", get_mut = "pub", set = "pub")]
|
||||
pub layer: WorkspaceLayer,
|
||||
#[getset(get = "pub", get_mut = "pub", set = "pub")]
|
||||
pub floating_layer_behaviour: FloatingLayerBehaviour,
|
||||
#[getset(get = "pub", get_mut = "pub", set = "pub")]
|
||||
pub locked_containers: BTreeSet<usize>,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
#[getset(get = "pub", set = "pub")]
|
||||
@@ -117,6 +119,7 @@ impl Display for WorkspaceLayer {
|
||||
}
|
||||
|
||||
impl_ring_elements!(Workspace, Container);
|
||||
impl_ring_elements!(Workspace, Window, "floating_window");
|
||||
|
||||
impl Default for Workspace {
|
||||
fn default() -> Self {
|
||||
@@ -127,7 +130,7 @@ impl Default for Workspace {
|
||||
maximized_window: None,
|
||||
maximized_window_restore_idx: None,
|
||||
monocle_container_restore_idx: None,
|
||||
floating_windows: Vec::default(),
|
||||
floating_windows: Ring::default(),
|
||||
layout: Layout::Default(DefaultLayout::BSP),
|
||||
layout_rules: vec![],
|
||||
layout_flip: None,
|
||||
@@ -141,6 +144,7 @@ impl Default for Workspace {
|
||||
window_container_behaviour_rules: None,
|
||||
float_override: None,
|
||||
layer: Default::default(),
|
||||
floating_layer_behaviour: Default::default(),
|
||||
globals: Default::default(),
|
||||
workspace_config: None,
|
||||
locked_containers: Default::default(),
|
||||
@@ -250,6 +254,7 @@ impl Workspace {
|
||||
|
||||
self.set_float_override(config.float_override);
|
||||
self.set_layout_flip(config.layout_flip);
|
||||
self.set_floating_layer_behaviour(config.floating_layer_behaviour.unwrap_or_default());
|
||||
|
||||
self.set_workspace_config(Some(config.clone()));
|
||||
|
||||
@@ -325,13 +330,13 @@ impl Workspace {
|
||||
} else if let Some(maximized_window) = self.maximized_window() {
|
||||
maximized_window.restore();
|
||||
maximized_window.focus(mouse_follows_focus)?;
|
||||
} else if let Some(floating_window) = self.floating_windows().first() {
|
||||
} else if let Some(floating_window) = self.focused_floating_window() {
|
||||
floating_window.focus(mouse_follows_focus)?;
|
||||
}
|
||||
} else if let Some(maximized_window) = self.maximized_window() {
|
||||
maximized_window.restore();
|
||||
maximized_window.focus(mouse_follows_focus)?;
|
||||
} else if let Some(floating_window) = self.floating_windows().first() {
|
||||
} else if let Some(floating_window) = self.focused_floating_window() {
|
||||
floating_window.focus(mouse_follows_focus)?;
|
||||
}
|
||||
|
||||
@@ -608,6 +613,9 @@ impl Workspace {
|
||||
self.containers().get(self.container_idx_for_window(hwnd)?)
|
||||
}
|
||||
|
||||
/// If there is a container which holds the window with `hwnd` it will focus that container.
|
||||
/// This function will only emit a focus on the window if it isn't the focused window of that
|
||||
/// container already.
|
||||
pub fn focus_container_by_window(&mut self, hwnd: isize) -> Result<()> {
|
||||
let container_idx = self
|
||||
.container_idx_for_window(hwnd)
|
||||
@@ -862,7 +870,7 @@ impl Workspace {
|
||||
container
|
||||
}
|
||||
|
||||
fn container_idx_for_window(&self, hwnd: isize) -> Option<usize> {
|
||||
pub fn container_idx_for_window(&self, hwnd: isize) -> Option<usize> {
|
||||
let mut idx = None;
|
||||
for (i, x) in self.containers().iter().enumerate() {
|
||||
if x.contains_window(hwnd) {
|
||||
@@ -1113,7 +1121,7 @@ impl Workspace {
|
||||
window
|
||||
};
|
||||
|
||||
self.floating_windows_mut().push(window);
|
||||
self.floating_windows_mut().push_back(window);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
@@ -1431,24 +1439,11 @@ impl Workspace {
|
||||
|
||||
pub fn new_maximized_window(&mut self) -> Result<()> {
|
||||
let focused_idx = self.focused_container_idx();
|
||||
let foreground_hwnd = WindowsApi::foreground_window()?;
|
||||
let mut floating_window = None;
|
||||
|
||||
if !self.floating_windows().is_empty() {
|
||||
let mut focused_floating_window_idx = None;
|
||||
for (i, w) in self.floating_windows().iter().enumerate() {
|
||||
if w.hwnd == foreground_hwnd {
|
||||
focused_floating_window_idx = Option::from(i);
|
||||
}
|
||||
}
|
||||
|
||||
if let Some(idx) = focused_floating_window_idx {
|
||||
floating_window = Option::from(self.floating_windows_mut().remove(idx));
|
||||
}
|
||||
}
|
||||
|
||||
if let Some(floating_window) = floating_window {
|
||||
self.set_maximized_window(Option::from(floating_window));
|
||||
if matches!(self.layer, WorkspaceLayer::Floating) {
|
||||
let floating_window_idx = self.focused_floating_window_idx();
|
||||
let floating_window = self.floating_windows_mut().remove(floating_window_idx);
|
||||
self.set_maximized_window(floating_window);
|
||||
self.set_maximized_window_restore_idx(Option::from(focused_idx));
|
||||
if let Some(window) = self.maximized_window() {
|
||||
window.maximize();
|
||||
@@ -1563,7 +1558,7 @@ impl Workspace {
|
||||
let hwnd = WindowsApi::foreground_window().ok()?;
|
||||
|
||||
let mut idx = None;
|
||||
for (i, window) in self.floating_windows.iter().enumerate() {
|
||||
for (i, window) in self.floating_windows().iter().enumerate() {
|
||||
if hwnd == window.hwnd {
|
||||
idx = Option::from(i);
|
||||
}
|
||||
@@ -1572,8 +1567,8 @@ impl Workspace {
|
||||
match idx {
|
||||
None => None,
|
||||
Some(idx) => {
|
||||
if self.floating_windows.get(idx).is_some() {
|
||||
Option::from(self.floating_windows_mut().remove(idx))
|
||||
if self.floating_windows().get(idx).is_some() {
|
||||
self.floating_windows_mut().remove(idx)
|
||||
} else {
|
||||
None
|
||||
}
|
||||
@@ -1760,7 +1755,7 @@ mod tests {
|
||||
|
||||
// unfloat - have to do this semi-manually becuase of calls to WindowsApi in
|
||||
// new_container_for_floating_window which usually handles unfloating
|
||||
let window = ws.floating_windows_mut().pop().unwrap();
|
||||
let window = ws.floating_windows_mut().pop_back().unwrap();
|
||||
let mut container = Container::default();
|
||||
container.add_window(window);
|
||||
ws.insert_container_at_idx(ws.focused_container_idx(), container);
|
||||
@@ -2270,4 +2265,99 @@ mod tests {
|
||||
assert!(workspace.contains_window(0));
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_focus_container_by_window() {
|
||||
let mut workspace = Workspace::default();
|
||||
|
||||
{
|
||||
// Container with 3 windows
|
||||
let mut container = Container::default();
|
||||
for i in 0..3 {
|
||||
container.windows_mut().push_back(Window::from(i));
|
||||
}
|
||||
workspace.add_container_to_back(container);
|
||||
}
|
||||
|
||||
{
|
||||
// Container with 1 window
|
||||
let mut container = Container::default();
|
||||
container.windows_mut().push_back(Window::from(4));
|
||||
workspace.add_container_to_back(container);
|
||||
}
|
||||
|
||||
// Focus container by window
|
||||
workspace.focus_container_by_window(1).unwrap();
|
||||
|
||||
// Should be focused on workspace 0
|
||||
assert_eq!(workspace.focused_container_idx(), 0);
|
||||
|
||||
// Should be focused on window 1 and hwnd should be 1
|
||||
let focused_container = workspace.focused_container_mut().unwrap();
|
||||
assert_eq!(
|
||||
focused_container.focused_window(),
|
||||
Some(&Window { hwnd: 1 })
|
||||
);
|
||||
assert_eq!(focused_container.focused_window_idx(), 1);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_contains_managed_window() {
|
||||
let mut workspace = Workspace::default();
|
||||
|
||||
{
|
||||
// Container with 3 windows
|
||||
let mut container = Container::default();
|
||||
for i in 0..3 {
|
||||
container.windows_mut().push_back(Window::from(i));
|
||||
}
|
||||
workspace.add_container_to_back(container);
|
||||
}
|
||||
|
||||
{
|
||||
// Container with 1 window
|
||||
let mut container = Container::default();
|
||||
container.windows_mut().push_back(Window::from(4));
|
||||
workspace.add_container_to_back(container);
|
||||
}
|
||||
|
||||
// Should return true, window is in container 1
|
||||
assert!(workspace.contains_managed_window(4));
|
||||
|
||||
// Should return true, all the windows are in container 0
|
||||
for i in 0..3 {
|
||||
assert!(workspace.contains_managed_window(i));
|
||||
}
|
||||
|
||||
// Should return false since window was never added
|
||||
assert!(!workspace.contains_managed_window(5));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_new_floating_window() {
|
||||
let mut workspace = Workspace::default();
|
||||
|
||||
{
|
||||
// Container with 3 windows
|
||||
let mut container = Container::default();
|
||||
for i in 0..3 {
|
||||
container.windows_mut().push_back(Window::from(i));
|
||||
}
|
||||
workspace.add_container_to_back(container);
|
||||
}
|
||||
|
||||
// Add window to floating_windows
|
||||
workspace.new_floating_window().ok();
|
||||
|
||||
// Should have 1 floating window
|
||||
assert_eq!(workspace.floating_windows().len(), 1);
|
||||
|
||||
// Should have only 2 windows now
|
||||
let container = workspace.focused_container_mut().unwrap();
|
||||
assert_eq!(container.windows().len(), 2);
|
||||
|
||||
// Should contain hwnd 0 since this is the first window in the container
|
||||
let floating_windows = workspace.floating_windows_mut();
|
||||
assert!(floating_windows.contains(&Window { hwnd: 0 }));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,130 +0,0 @@
|
||||
#![deny(clippy::unwrap_used, clippy::expect_used)]
|
||||
|
||||
use crate::border_manager;
|
||||
use crate::WindowManager;
|
||||
use crossbeam_channel::Receiver;
|
||||
use crossbeam_channel::Sender;
|
||||
use crossbeam_utils::atomic::AtomicCell;
|
||||
use lazy_static::lazy_static;
|
||||
use parking_lot::Mutex;
|
||||
use std::sync::Arc;
|
||||
use std::sync::OnceLock;
|
||||
use std::time::Duration;
|
||||
use std::time::Instant;
|
||||
|
||||
#[derive(Copy, Clone)]
|
||||
pub struct Notification {
|
||||
pub monitor_idx: usize,
|
||||
pub workspace_idx: usize,
|
||||
}
|
||||
|
||||
pub static ALT_TAB_HWND: AtomicCell<Option<isize>> = AtomicCell::new(None);
|
||||
|
||||
lazy_static! {
|
||||
pub static ref ALT_TAB_HWND_INSTANT: Arc<Mutex<Instant>> = Arc::new(Mutex::new(Instant::now()));
|
||||
}
|
||||
|
||||
static CHANNEL: OnceLock<(Sender<Notification>, Receiver<Notification>)> = OnceLock::new();
|
||||
|
||||
pub fn channel() -> &'static (Sender<Notification>, Receiver<Notification>) {
|
||||
CHANNEL.get_or_init(|| crossbeam_channel::bounded(1))
|
||||
}
|
||||
|
||||
fn event_tx() -> Sender<Notification> {
|
||||
channel().0.clone()
|
||||
}
|
||||
|
||||
fn event_rx() -> Receiver<Notification> {
|
||||
channel().1.clone()
|
||||
}
|
||||
|
||||
pub fn send_notification(monitor_idx: usize, workspace_idx: usize) {
|
||||
if event_tx()
|
||||
.try_send(Notification {
|
||||
monitor_idx,
|
||||
workspace_idx,
|
||||
})
|
||||
.is_err()
|
||||
{
|
||||
tracing::warn!("channel is full; dropping notification")
|
||||
}
|
||||
}
|
||||
|
||||
pub fn listen_for_notifications(wm: Arc<Mutex<WindowManager>>) {
|
||||
std::thread::spawn(move || loop {
|
||||
match handle_notifications(wm.clone()) {
|
||||
Ok(()) => {
|
||||
tracing::warn!("restarting finished thread");
|
||||
}
|
||||
Err(error) => {
|
||||
if cfg!(debug_assertions) {
|
||||
tracing::error!("restarting failed thread: {:?}", error)
|
||||
} else {
|
||||
tracing::error!("restarting failed thread: {}", error)
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
pub fn handle_notifications(wm: Arc<Mutex<WindowManager>>) -> color_eyre::Result<()> {
|
||||
tracing::info!("listening");
|
||||
|
||||
let receiver = event_rx();
|
||||
let arc = wm.clone();
|
||||
|
||||
for notification in receiver {
|
||||
tracing::info!("running reconciliation");
|
||||
|
||||
let mut wm = wm.lock();
|
||||
let focused_monitor_idx = wm.focused_monitor_idx();
|
||||
let focused_workspace_idx =
|
||||
wm.focused_workspace_idx_for_monitor_idx(focused_monitor_idx)?;
|
||||
|
||||
let focused_pair = (focused_monitor_idx, focused_workspace_idx);
|
||||
let updated_pair = (notification.monitor_idx, notification.workspace_idx);
|
||||
|
||||
if focused_pair != updated_pair {
|
||||
wm.focus_monitor(notification.monitor_idx)?;
|
||||
let mouse_follows_focus = wm.mouse_follows_focus;
|
||||
|
||||
if let Some(monitor) = wm.focused_monitor_mut() {
|
||||
let previous_idx = monitor.focused_workspace_idx();
|
||||
monitor.set_last_focused_workspace(Option::from(previous_idx));
|
||||
monitor.focus_workspace(notification.workspace_idx)?;
|
||||
monitor.load_focused_workspace(mouse_follows_focus)?;
|
||||
}
|
||||
|
||||
// Drop our lock on the window manager state here to not slow down updates
|
||||
drop(wm);
|
||||
|
||||
// Check if there was an alt-tab across workspaces in the last second
|
||||
if let Some(hwnd) = ALT_TAB_HWND.load() {
|
||||
if ALT_TAB_HWND_INSTANT
|
||||
.lock()
|
||||
.elapsed()
|
||||
.lt(&Duration::from_secs(1))
|
||||
{
|
||||
// Sleep for 100 millis to let other events pass
|
||||
std::thread::sleep(Duration::from_millis(100));
|
||||
tracing::info!("focusing alt-tabbed window");
|
||||
|
||||
// Take a new lock on the wm and try to focus the container with
|
||||
// the recorded HWND from the alt-tab
|
||||
let mut wm = arc.lock();
|
||||
if let Ok(workspace) = wm.focused_workspace_mut() {
|
||||
// Regardless of if this fails, we need to get past this part
|
||||
// to unblock the border manager below
|
||||
let _ = workspace.focus_container_by_window(hwnd);
|
||||
}
|
||||
|
||||
// Unblock the border manager
|
||||
ALT_TAB_HWND.store(None);
|
||||
// Send a notification to the border manager to update the borders
|
||||
border_manager::send_notification(None);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
@@ -1104,6 +1104,10 @@ enum SubCommand {
|
||||
/// Move the focused window to the specified monitor workspace
|
||||
#[clap(arg_required_else_help = true)]
|
||||
MoveToMonitorWorkspace(MoveToMonitorWorkspace),
|
||||
/// Send the focused window to the last focused monitor workspace
|
||||
SendToLastWorkspace,
|
||||
/// Move the focused window to the last focused monitor workspace
|
||||
MoveToLastWorkspace,
|
||||
/// Focus the specified monitor
|
||||
#[clap(arg_required_else_help = true)]
|
||||
FocusMonitor(FocusMonitor),
|
||||
@@ -1857,6 +1861,12 @@ fn main() -> Result<()> {
|
||||
arg.cycle_direction,
|
||||
))?;
|
||||
}
|
||||
SubCommand::MoveToLastWorkspace => {
|
||||
send_message(&SocketMessage::MoveContainerToLastWorkspace)?;
|
||||
}
|
||||
SubCommand::SendToLastWorkspace => {
|
||||
send_message(&SocketMessage::SendContainerToLastWorkspace)?;
|
||||
}
|
||||
SubCommand::SwapWorkspacesWithMonitor(arg) => {
|
||||
send_message(&SocketMessage::SwapWorkspacesToMonitorNumber(arg.target))?;
|
||||
}
|
||||
|
||||
@@ -133,6 +133,8 @@ nav:
|
||||
- cli/cycle-send-to-workspace.md
|
||||
- cli/send-to-monitor-workspace.md
|
||||
- cli/move-to-monitor-workspace.md
|
||||
- cli/send-to-last-workspace.md
|
||||
- cli/move-to-last-workspace.md
|
||||
- cli/focus-monitor.md
|
||||
- cli/focus-monitor-at-cursor.md
|
||||
- cli/focus-last-workspace.md
|
||||
@@ -191,6 +193,7 @@ nav:
|
||||
- cli/toggle-float.md
|
||||
- cli/toggle-monocle.md
|
||||
- cli/toggle-maximize.md
|
||||
- cli/toggle-lock.md
|
||||
- cli/restore-windows.md
|
||||
- cli/manage.md
|
||||
- cli/unmanage.md
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"$schema": "http://json-schema.org/draft-07/schema#",
|
||||
"title": "KomobarConfig",
|
||||
"description": "The `komorebi.bar.json` configuration file reference for `v0.1.35`",
|
||||
"description": "The `komorebi.bar.json` configuration file reference for `v0.1.36`",
|
||||
"type": "object",
|
||||
"required": [
|
||||
"left_widgets",
|
||||
|
||||
21
schema.json
21
schema.json
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"$schema": "http://json-schema.org/draft-07/schema#",
|
||||
"title": "StaticConfig",
|
||||
"description": "The `komorebi.json` static configuration file reference for `v0.1.35`",
|
||||
"description": "The `komorebi.json` static configuration file reference for `v0.1.36`",
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"animation": {
|
||||
@@ -1199,6 +1199,25 @@
|
||||
"description": "Enable or disable float override, which makes it so every new window opens in floating mode (default: false)",
|
||||
"type": "boolean"
|
||||
},
|
||||
"floating_layer_behaviour": {
|
||||
"description": "Determine what happens to a new window when the Floating workspace layer is active (default: Tile)",
|
||||
"oneOf": [
|
||||
{
|
||||
"description": "Tile new windows (unless they match a float rule)",
|
||||
"type": "string",
|
||||
"enum": [
|
||||
"Tile"
|
||||
]
|
||||
},
|
||||
{
|
||||
"description": "Float new windows",
|
||||
"type": "string",
|
||||
"enum": [
|
||||
"Float"
|
||||
]
|
||||
}
|
||||
]
|
||||
},
|
||||
"initial_workspace_rules": {
|
||||
"description": "Initial workspace application rules",
|
||||
"type": "array",
|
||||
|
||||
BIN
wix/License.rtf
BIN
wix/License.rtf
Binary file not shown.
Reference in New Issue
Block a user