Compare commits

..

32 Commits

Author SHA1 Message Date
LGUG2Z
6c6b5d41a8 wip 2024-04-21 12:35:49 -07:00
LGUG2Z
6fce630be5 fix(wm): restore stackback on monocle off
This commit ensures that if a container stack has a stackbar, when
toggling off monocle mode, the stackbar will be restored as expected.
This requires an additional retile which it would be nice to avoid in
the future.
2024-04-21 12:35:23 -07:00
LGUG2Z
dfd0d604aa feat(cli): add move-to-monitor-workspace command
This commit adds a move-to-monitor-workspace command, which, following
the existing convention, does the same action as
send-to-monitor-workspace, but sets the focused monitor, workspace and
container to the window container once it is inserted into the target
monitor and workspace indices.
2024-04-20 14:58:57 -07:00
LGUG2Z
aff1081ccd feat(cli): detailed start failure error feedback
This commit ensures that after 3 failures to start komorebi with
'komorebic start', 'komorebi.exe' will be run directly in order to show
the detailed error output to the end user.
2024-04-20 14:37:27 -07:00
LGUG2Z
793e81d43d fix(cli): use force-quit if stop signal fails
This commit ensures that the komorebic stop command will force-quit
komorebi if the Stop SocketMessage handler fails.
2024-04-20 14:37:23 -07:00
LGUG2Z
eac4c8e9b1 fix(wm): add regex ws rule matching support
This commit ensures that enforce_workspace_rules() will try to match
exact exe names, classes and titles, and then against any potential
regexes that may have been used as keys to index matching rules against
the current window's exe, title and class.

Support for other matchers isn't implemented yet. Not sure it's worth
adding while using a HashMap to store the workspace rules, probably need
to rethink the data structure first.
2024-04-18 19:44:08 -07:00
جاد
6b42587af4 docs(ahk): add #requires line, remove asc ref
* re-add the #requires line that was removed in commit e044a5a
* remove the "generated by komorebic" comment which is incorrect. komorebic only generates an AHK v1 lib
* update common-workflows/autohotkey.md to remove line about `komorebi.generated.ahk file which no longer exists
2024-04-16 08:04:15 -07:00
LGUG2Z
46152621c0 feat(wm): immediate stackbar mode updates via ipc
This commit ensures that when the stackbar mode is updated via a
SocketMessage or static config update, any visible stackbars will have
their mode updated immediately without having to wait for user
interaction.
2024-04-16 07:54:31 -07:00
mmikeww
b78693118b Update autohotkey.md
komorebi.generated.ahk is no longer in the repo, it was deleted in commit 52122c401d, so remove the instruction to download it
2024-04-15 21:29:31 -04:00
mmikeww
7caa839a00 Update komorebic.lib.ahk
re-add the #Requires v2 line
remove the "generated by komorebic" comment
2024-04-15 21:24:11 -04:00
LGUG2Z
1671f31e3e feat(wm): add window debugging
This commit adds support for debugging windows and emitting information
about how they go through komorebi's decision making pipeline and rules
engines which ultimately decide how they are or aren't managed.
2024-04-15 15:05:20 -07:00
LGUG2Z
6fe46610fc refactor(wm): ensure configurability via ipc
This commit ensures that new features such as stackbar, particularly
where the configuration is located in the global state, can be
configured via SocketMessages.

A few structs had to be moved to komorebi-core to make this possible.
I've also cleaned up a bunch of strum snake_case attrs which seemed to
be unused.

A new GlobalState SocketMessage has been introduced, and going forward
we should make sure that this can send all global state to a requester,
and move global state out of the State handler, which should only handle
window manager state.
2024-04-15 08:36:28 -07:00
LGUG2Z
4ba3125dde fix(wm): ensure window restore on stackbar click
This commit ensures that a window is restored before a focus call is
made on it when switching windows in a stack by using a window's tab in
the stackbar.

This handles the issue where two clicks are required to trigger a layout
update the first time a window that is not currently at the top of the
stack is brought to the front via a stackbar click.
2024-04-13 22:38:41 -07:00
James Tucker
efa562de5c fix(wm): avoid out of range subtraction
This fixes the panic, but not the underlying problem.

Updates #757
Updates #758
2024-04-13 22:17:57 -07:00
James Tucker
b476bee1d8 fix(wm): fix focus changes with stackbar enabled
Notify all stackbars on focus change, and they now respond to changes,
but do not create focus changes themselves just from an update.
2024-04-13 22:07:53 -07:00
James Tucker
86b07f28dd chore(deps): update deps, including miow
Fixes #696
2024-04-13 21:59:48 -07:00
James Tucker
311e37c8a2 fix(wm): reduce errors from non-window events
As we have been working down some bugs from earlier changes, we
introduced some additional error conditions in the logs. Now that the
new focus approach is available, switching the stackbar to that means we
can avoid needing to pass down ForceUpdate and FocusChange events for
non-windows, which removes many of these cases.

In addition we do a check in should_manage that the target object is
actually a window, ignoring the event if it is not.
2024-04-13 21:14:59 -07:00
James Tucker
15c3b32608 feat(wm): report full errors in debug mode
This gives us a stack trace style error report in the log when in debug
mode, which makes it much quicker to track down the origin of an error.
2024-04-13 17:28:49 -07:00
James Tucker
28b46c54da fix(wm): correct use of z-order flags
We had been setting managed windows to HWND_TOPMOST which is a sticky
and viral parameter. This was also the cause of the border window ending
up behind other windows in an undesirable fashion, as even though it was
marked WS_EX_TOPMOST, we were then having to mark it HWND_NOTOPTMOST
when raising it to avoid it ending up drawing over other windows.

Since we've fixed the border window to no longer be visible when
unmanaged windows are focused, we can now set the border window to
HWND_TOP when we reposition, which will ensure it's drawing in the order
that we want.

Now we also set managed windows only to HWND_TOP, rather than
HWND_TOPMOST which stops us from incorrectly reordering internal
concerns vs. child windows and owned windows that we're not managing.
Windows are still brought to the foreground as expected/desired, but
they're no longer 'sticking' there, nor are they drawing over the border
window.

This change does have a slight transition behavior as it initially rolls
out, as prior versions of the Komorebi have been setting HWND_TOPMOST,
which as a sticky parameter won't be cleared until the application or
host system removes that flag. This means that the final z-order
behavior will come good eventually.

To immediately see the correct results, restarting affected apps or
logging out / in will do. Unfortuantely we can't just set
HWND_NOTTOPMOST, as similarly to setting HWND_TOPMOST, this can cause
issues with an applications intended owned-window Z-Ordering - mostly
affecting toolwindows and child windows, such as file dialogs, toolbars
and so on, most of which we do not manage.
2024-04-13 17:28:45 -07:00
James Tucker
732aca77b5 refactor(wm): use a new method to focus windows
Use the same method as FancyZones to enable setting the foreground
window. This makes it possible then to remove the thread attachment
behaviors that have a number of other complex side effects, and aren't
always allowed.

In addition, cleanup old focus/raise methods some, in particular the
border window is now explicitly not activated when it is raised, as it
should never be activated.
2024-04-13 16:51:01 -07:00
James Tucker
f56fc36557 refactor(wm): remove mutability from window and events
We had a mut requirement on some of the Window functions which may have
been vestigial or in preparation for more state on Window objects, but
presently unused. I removed that, as the Window struct is currently just
carrying an HWND value that's essentially always immutable - there's no
advantage to ever reusing a Window struct vs. making a new one for
another HWND.

In doing so we then no longer needed to be passing in mutable events, so
I applied a little simplification of the event receiver / dispatcher to
process_event. After that it became obvious that we could just pass the
owned event directly into process_event instead, which substantially
simplifies the ownership model and lifetime for those objects.

This is small, and shouldn't create any meaningful behavioral change.
2024-04-13 16:50:26 -07:00
LGUG2Z
5334e1944e fix(wm): ensure stackbar tab clicks trigger
This commit fixes a small regression that prevented events emitted from
clicks on stackbar tabs from being handled.
2024-04-13 12:02:46 -07:00
LGUG2Z
d8d087e621 fix(wm): ensure borders are drawn w/ stackbar
This commit fixes a small regression and ensures that the active window
border, when enabled, will be drawn as expected when a container stack
has a stackbar active.
2024-04-13 11:02:14 -07:00
James Tucker
b61146ead4 fix(wm): hide border when unmanged windows are focused
Previously we were dropping events that don't pertain to managed
windows, with one exception in should_manage that could probably do with
further cleanup (DisplayChange).

This first step fixes the latent border window problem, where we would
retain a border window when the last managed window was closed and focus
transitioned to an unmanaged window.
2024-04-13 10:22:03 -07:00
LGUG2Z
16cb811aa9 docs(license): switch to polyform, add contributing.md 2024-04-12 14:46:36 -07:00
LGUG2Z
862219b9a3 chore(dev): begin v0.1.25-dev 2024-04-11 16:59:01 -07:00
LGUG2Z
0fc75afb00 chore(release): v0.1.24 2024-04-09 17:57:41 -07:00
LGUG2Z
d67f355a17 refactor(logs): reduce noise on info level 2024-04-09 17:34:14 -07:00
LGUG2Z
3d0ed4cfc4 fix(wm): address regression from 5e714ca
This commit fixes a regression introduced in 5e714ca which broke the
cross-monitor mouse move behaviour.
2024-04-09 16:30:58 -07:00
Carlos Regis
21be01b9aa docs(mkdocs): show default loc for whkdrc (#739) 2024-04-08 16:48:52 -07:00
LGUG2Z
648ba672e3 docs(config): update schema version in docstring 2024-04-07 14:26:27 -07:00
LGUG2Z
af6529851e chore(dev): begin v0.1.24-dev 2024-04-07 14:18:29 -07:00
38 changed files with 4614 additions and 467 deletions

45
CONTRIBUTING.md Normal file
View File

@@ -0,0 +1,45 @@
# Contributing to the Project
The project is a collection of contributions from both the project leaders and
community members. There are many ways to contribute, this can include content
in the project repositories, as well as contributing in public and private
conversation, assisting users, writing blog posts, and many other ways.
## How contributions are made
Contributions to the project primarily happen in the project source
repositories, but may also occur in other places, such as discussion forums and
public and private discourse.
## Contributing content to the Project
In order for the project leaders to manage sustained progress toward the
project goals and maintain project velocity, focus and quality, the project may
adjust the license terms over time.
Content contributed to the project must therefore be provided under
sufficiently liberal terms to allow these operations to proceed unimpeded. As
such contributions are accepted with the following understanding:
* Contributed content is licensed under the terms of the 0-BSD license
* Contributors accept the terms of the project license at the time of
contribution
By making a contribution, you accept both the current project license terms,
and that all contributions that you have made are provided under the terms of
the 0-BSD license.
## Zero-Clause BSD
```
Permission to use, copy, modify, and/or distribute this software for
any purpose with or without fee is hereby granted.
THE SOFTWARE IS PROVIDED “AS IS” AND THE AUTHOR DISCLAIMS ALL
WARRANTIES WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES
OF MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE
FOR ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY
DAMAGES WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN
AN ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT
OF OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
```

2945
Cargo.lock generated

File diff suppressed because it is too large Load Diff

View File

@@ -6,6 +6,7 @@ members = [
"komorebi",
"komorebi-client",
"komorebi-core",
"komorebi-egui",
"komorebic",
"komorebic-no-console",
]
@@ -17,6 +18,7 @@ dunce = "1"
dirs = "5"
color-eyre = "0.6"
serde_json = { package = "serde_json_lenient", version = "0.1" }
sysinfo = "0.30"
[workspace.dependencies.windows]
version = "0.54"

21
LICENSE
View File

@@ -1,21 +0,0 @@
MIT License
Copyright (c) 2021 Jade Iqbal
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.

105
LICENSE.md Normal file
View File

@@ -0,0 +1,105 @@
# PolyForm Strict License 1.0.0
<https://polyformproject.org/licenses/strict/1.0.0>
## Acceptance
In order to get any license under these terms, you must agree
to them as both strict obligations and conditions to all
your licenses.
## Copyright License
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, other than distributing the software or
making changes or new works based on the software.
## Patent License
The licensor grants you a patent license for the software that
covers patent claims the licensor can license, or becomes able
to license, that you would infringe by using the software.
## Noncommercial Purposes
Any noncommercial purpose is a permitted purpose.
## Personal Uses
Personal use for research, experiment, and testing for
the benefit of public knowledge, personal study, private
entertainment, hobby projects, amateur pursuits, or religious
observance, without any anticipated commercial application,
is use for a permitted purpose.
## Noncommercial Organizations
Use by any charitable organization, educational institution,
public research organization, public safety or health
organization, environmental protection organization,
or government institution is use for a permitted purpose
regardless of the source of funding or obligations resulting
from the funding.
## Fair Use
You may have "fair use" rights for the software under the
law. These terms do not limit them.
## No Other Rights
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
any other licenses.
## Patent Defense
If you make any written claim that the software infringes or
contributes to infringement of any patent, your patent license
for the software granted under these terms ends immediately. If
your company makes such a claim, your patent license ends
immediately for work on behalf of your company.
## Violations
The first time you are notified in writing that you have
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
end immediately.
## No Liability
***As far as the law allows, the software comes as is, without
any warranty or condition, and the licensor will not be liable
to you for any damages arising out of these terms or the use
or nature of the software, under any kind of legal claim.***
## Definitions
The **licensor** is the individual or entity offering these
terms, and the **software** is the software the licensor makes
available under these terms.
**You** refers to the individual or entity agreeing to these
terms.
**Your company** is any legal entity, sole proprietorship,
or other kind of organization that you work for, plus all
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.
**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.

View File

@@ -169,6 +169,20 @@ ability for users to specify colours in `komorebi.json` in Hex format alongside
There is also a process in place for graceful, non-breaking, deprecation of configuration options that are no longer
required.
## License
`komorebi` is licensed under 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` other than
redistribution, or distribution of new works (ie. 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.
Please see [CONTRIBUTING.md](./CONTRIBUTING.md) for more information about how
code contributions to `komorebi` are licensed.
# Development
If you use IntelliJ, you should enable the following settings to ensure that code generated by macros is recognised by
@@ -324,7 +338,7 @@ every `WindowManagerEvent` and `SocketMessage` handled by `komorebi` in a Rust c
Below is a simple example of how to use `komorebi-client` in a basic Rust application.
```rust
// komorebi-client = { git = "https://github.com/LGUG2Z/komorebi", tag = "v0.1.23"}
// komorebi-client = { git = "https://github.com/LGUG2Z/komorebi", tag = "v0.1.24"}
use anyhow::Result;
use komorebi_client::Notification;

View File

@@ -21,9 +21,6 @@ hotkey bindings.
# save the latest generated komorebic library to ~/komorebic.lib.ahk
iwr https://raw.githubusercontent.com/LGUG2Z/komorebi/v0.1.20/komorebic.lib.ahk -OutFile $Env:USERPROFILE\komorebic.lib.ahk
# save the latest generated app-specific config tweaks and fixes to ~/komorebi.generated.ahk
iwr https://raw.githubusercontent.com/LGUG2Z/komorebi/v0.1.20/komorebi.generated.ahk -OutFile $Env:USERPROFILE\komorebi.generated.ahk
# save the sample komorebi configuration file to ~/komorebi.ahk
iwr https://raw.githubusercontent.com/LGUG2Z/komorebi/v0.1.20/komorebi.sample.ahk -OutFile $Env:USERPROFILE\komorebi.ahk
```

View File

@@ -162,8 +162,8 @@ If you like the `grid` layout in [LeftWM](https://github.com/leftwm/leftwm-layou
## whkdrc
`whkd` is a fairly basic piece of software with a simple configuration format:
key bindings go to the left of the, and shell commands go to the right of the
colon.
key bindings go to the left of the colon, and shell commands go to the right of the
colon. By default, the `whkdrc` file should be located in the `$Env:USERPROFILE/.config/` directory.
Please remember that `whkd` does not support overriding Microsoft's limitations
on hotkey bindings that include the `Windows` key. If this is important to you,

View File

@@ -1,5 +1,5 @@
{
"$schema": "https://raw.githubusercontent.com/LGUG2Z/komorebi/v0.1.23/schema.json",
"$schema": "https://raw.githubusercontent.com/LGUG2Z/komorebi/v0.1.24/schema.json",
"app_specific_configuration_path": "$Env:USERPROFILE/applications.yaml",
"window_hiding_behaviour": "Cloak",
"cross_monitor_move_behaviour": "Insert",

View File

@@ -26,6 +26,7 @@ install:
just install-target komorebic
just install-target komorebic-no-console
just install-target komorebi
just install-target komorebi-egui
run:
just install-target komorebic

View File

@@ -1,6 +1,6 @@
[package]
name = "komorebi-client"
version = "0.1.23"
version = "0.1.25-dev.0"
edition = "2021"
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html

View File

@@ -1,15 +1,24 @@
#![warn(clippy::all, clippy::nursery, clippy::pedantic)]
#![allow(clippy::missing_errors_doc)]
pub use komorebi::colour::Colour;
pub use komorebi::colour::Rgb;
pub use komorebi::container::Container;
pub use komorebi::monitor::Monitor;
pub use komorebi::ring::Ring;
pub use komorebi::window::Window;
pub use komorebi::window_manager_event::WindowManagerEvent;
pub use komorebi::workspace::Workspace;
pub use komorebi::ActiveWindowBorderColours;
pub use komorebi::GlobalState;
pub use komorebi::Notification;
pub use komorebi::NotificationEvent;
pub use komorebi::RuleDebug;
pub use komorebi::StackbarConfig;
pub use komorebi::State;
pub use komorebi::StaticConfig;
pub use komorebi::TabsConfig;
pub use komorebi_core::ActiveWindowBorderStyle;
pub use komorebi_core::Arrangement;
pub use komorebi_core::Axis;
pub use komorebi_core::CustomLayout;
@@ -20,6 +29,8 @@ pub use komorebi_core::Layout;
pub use komorebi_core::OperationDirection;
pub use komorebi_core::Rect;
pub use komorebi_core::SocketMessage;
pub use komorebi_core::StackbarMode;
pub use komorebi_core::WindowKind;
use komorebi::DATA_DIR;

View File

@@ -1,6 +1,6 @@
[package]
name = "komorebi-core"
version = "0.1.23"
version = "0.1.25-dev.0"
edition = "2021"
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html

View File

@@ -321,7 +321,6 @@ impl Arrangement for CustomLayout {
#[derive(
Clone, Copy, Debug, Serialize, Deserialize, Display, EnumString, ValueEnum, JsonSchema,
)]
#[strum(serialize_all = "snake_case")]
pub enum Axis {
Horizontal,
Vertical,

View File

@@ -10,7 +10,6 @@ use strum::EnumString;
#[derive(
Clone, Copy, Debug, Serialize, Deserialize, Display, EnumString, ValueEnum, JsonSchema,
)]
#[strum(serialize_all = "snake_case")]
pub enum CycleDirection {
Previous,
Next,

View File

@@ -10,9 +10,18 @@ use crate::Rect;
use crate::Sizing;
#[derive(
Clone, Copy, Debug, Serialize, Deserialize, Display, EnumString, ValueEnum, JsonSchema,
Clone,
Copy,
Debug,
Serialize,
Deserialize,
Eq,
PartialEq,
Display,
EnumString,
ValueEnum,
JsonSchema,
)]
#[strum(serialize_all = "snake_case")]
pub enum DefaultLayout {
BSP,
Columns,

View File

@@ -57,6 +57,7 @@ pub enum SocketMessage {
SendContainerToWorkspaceNumber(usize),
CycleSendContainerToWorkspace(CycleDirection),
SendContainerToMonitorWorkspaceNumber(usize, usize),
MoveContainerToMonitorWorkspaceNumber(usize, usize),
SendContainerToNamedWorkspace(String),
MoveWorkspaceToMonitorNumber(usize),
SwapWorkspacesToMonitorNumber(usize),
@@ -131,9 +132,16 @@ pub enum SocketMessage {
AltFocusHack(bool),
ActiveWindowBorder(bool),
ActiveWindowBorderColour(WindowKind, u32, u32, u32),
ActiveWindowBorderWidth(i32),
ActiveWindowBorderOffset(i32),
ActiveWindowBorderStyle(ActiveWindowBorderStyle),
BorderWidth(i32),
BorderOffset(i32),
InvisibleBorders(Rect),
StackbarMode(StackbarMode),
StackbarFocusedTextColour(u32, u32, u32),
StackbarUnfocusedTextColour(u32, u32, u32),
StackbarBackgroundColour(u32, u32, u32),
StackbarHeight(i32),
StackbarTabWidth(i32),
WorkAreaOffset(Rect),
MonitorWorkAreaOffset(usize, Rect),
ResizeDelta(i32),
@@ -148,6 +156,7 @@ pub enum SocketMessage {
IdentifyLayeredApplication(ApplicationIdentifier, String),
IdentifyBorderOverflowApplication(ApplicationIdentifier, String),
State,
GlobalState,
VisibleWindows,
Query(StateQuery),
FocusFollowsMouse(FocusFollowsMouseImplementation, bool),
@@ -165,6 +174,7 @@ pub enum SocketMessage {
SocketSchema,
StaticConfigSchema,
GenerateStaticConfig,
DebugWindow(isize),
}
impl SocketMessage {
@@ -181,10 +191,29 @@ impl FromStr for SocketMessage {
}
}
#[derive(Debug, Copy, Clone, Eq, PartialEq, Display, Serialize, Deserialize, JsonSchema)]
pub enum StackbarMode {
Always,
Never,
OnStack,
}
#[derive(
Default, Copy, Clone, Debug, Eq, PartialEq, Display, Serialize, Deserialize, JsonSchema,
)]
pub enum ActiveWindowBorderStyle {
#[default]
/// Use the system border style
System,
/// Use the Windows 11-style rounded borders
Rounded,
/// Use the Windows 10-style square borders
Square,
}
#[derive(
Copy, Clone, Debug, Serialize, Deserialize, Display, EnumString, ValueEnum, JsonSchema,
)]
#[strum(serialize_all = "snake_case")]
pub enum WindowKind {
Single,
Stack,
@@ -194,7 +223,6 @@ pub enum WindowKind {
#[derive(
Copy, Clone, Debug, Serialize, Deserialize, Display, EnumString, ValueEnum, JsonSchema,
)]
#[strum(serialize_all = "snake_case")]
pub enum StateQuery {
FocusedMonitorIndex,
FocusedWorkspaceIndex,
@@ -215,7 +243,6 @@ pub enum StateQuery {
ValueEnum,
JsonSchema,
)]
#[strum(serialize_all = "snake_case")]
pub enum ApplicationIdentifier {
#[serde(alias = "exe")]
Exe,
@@ -230,7 +257,6 @@ pub enum ApplicationIdentifier {
#[derive(
Copy, Clone, Debug, Serialize, Deserialize, Display, EnumString, ValueEnum, JsonSchema,
)]
#[strum(serialize_all = "snake_case")]
pub enum FocusFollowsMouseImplementation {
/// A custom FFM implementation (slightly more CPU-intensive)
Komorebi,
@@ -241,7 +267,6 @@ pub enum FocusFollowsMouseImplementation {
#[derive(
Clone, Copy, Debug, Serialize, Deserialize, Display, EnumString, ValueEnum, JsonSchema,
)]
#[strum(serialize_all = "snake_case")]
pub enum WindowContainerBehaviour {
/// Create a new container for each new window
Create,
@@ -252,7 +277,6 @@ pub enum WindowContainerBehaviour {
#[derive(
Clone, Copy, Debug, Serialize, Deserialize, Display, EnumString, ValueEnum, JsonSchema,
)]
#[strum(serialize_all = "snake_case")]
pub enum MoveBehaviour {
/// Swap the window container with the window container at the edge of the adjacent monitor
Swap,
@@ -263,7 +287,6 @@ pub enum MoveBehaviour {
#[derive(
Copy, Clone, Debug, Serialize, Deserialize, Display, EnumString, ValueEnum, JsonSchema,
)]
#[strum(serialize_all = "snake_case")]
pub enum HidingBehaviour {
/// Use the SW_HIDE flag to hide windows when switching workspaces (has issues with Electron apps)
Hide,
@@ -276,7 +299,6 @@ pub enum HidingBehaviour {
#[derive(
Clone, Copy, Debug, Serialize, Deserialize, Display, EnumString, ValueEnum, JsonSchema,
)]
#[strum(serialize_all = "snake_case")]
pub enum OperationBehaviour {
/// Process komorebic commands on temporarily unmanaged/floated windows
Op,
@@ -287,7 +309,6 @@ pub enum OperationBehaviour {
#[derive(
Clone, Copy, Debug, Serialize, Deserialize, Display, EnumString, ValueEnum, JsonSchema,
)]
#[strum(serialize_all = "snake_case")]
pub enum Sizing {
Increase,
Decrease,

View File

@@ -13,7 +13,6 @@ use crate::Axis;
#[derive(
Clone, Copy, Debug, Serialize, Deserialize, Display, EnumString, ValueEnum, JsonSchema,
)]
#[strum(serialize_all = "snake_case")]
pub enum OperationDirection {
Left,
Right,

14
komorebi-egui/Cargo.toml Normal file
View File

@@ -0,0 +1,14 @@
[package]
name = "komorebi-egui"
version = "0.1.0"
edition = "2021"
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
[dependencies]
eframe = { version = "0.27" }
komorebi-client = { path = "../komorebi-client" }
serde_json = { workspace = true }
random_word = { version = "0.4.3", features = ["en"] }
windows = { workspace = true }
egui_extras = { version = "0.27" }

760
komorebi-egui/src/main.rs Normal file
View File

@@ -0,0 +1,760 @@
use eframe::egui;
use eframe::egui::color_picker::Alpha;
use eframe::egui::Color32;
use eframe::egui::WindowLevel;
use komorebi_client::ActiveWindowBorderStyle;
use komorebi_client::Colour;
use komorebi_client::DefaultLayout;
use komorebi_client::Layout;
use komorebi_client::Monitor;
use komorebi_client::Rect;
use komorebi_client::Rgb;
use komorebi_client::RuleDebug;
use komorebi_client::SocketMessage;
use komorebi_client::StackbarMode;
use komorebi_client::Window;
use komorebi_client::WindowKind;
use komorebi_client::Workspace;
use random_word::Lang;
use windows::Win32::UI::WindowsAndMessaging::EnumWindows;
fn main() {
let native_options = eframe::NativeOptions {
viewport: egui::ViewportBuilder::default()
.with_window_level(WindowLevel::AlwaysOnTop)
.with_inner_size([320.0, 500.0]),
follow_system_theme: true,
..Default::default()
};
eframe::run_native(
"komorebi-egui",
native_options,
Box::new(|cc| Box::new(KomorebiEgui::new(cc))),
)
.unwrap();
}
struct KomorebiEgui {
monitors: Vec<MonitorConfig>,
border_config: BorderConfig,
stackbar_config: StackbarConfig,
mouse_follows_focus: bool,
hwnd_lookup: isize,
hwnd_lookup_windows: Vec<Window>,
hwnd_rule_debug: Option<RuleDebug>,
}
fn colour32(colour: Colour) -> Color32 {
match colour {
Colour::Rgb(rgb) => Color32::from_rgb(rgb.r as u8, rgb.g as u8, rgb.b as u8),
Colour::Hex(hex) => {
let rgb = Rgb::from(hex);
Color32::from_rgb(rgb.r as u8, rgb.g as u8, rgb.b as u8)
}
}
}
impl KomorebiEgui {
fn new(_cc: &eframe::CreationContext<'_>) -> Self {
// Customize egui here with cc.egui_ctx.set_fonts and cc.egui_ctx.set_visuals.
// Restore app state using cc.storage (requires the "persistence" feature).
// Use the cc.gl (a glow::Context) to create graphics shaders and buffers that you can use
// for e.g. egui::PaintCallback.
let mut state = serde_json::from_str::<komorebi_client::State>(
&komorebi_client::send_query(&SocketMessage::State).unwrap(),
)
.unwrap();
let global_state = serde_json::from_str::<komorebi_client::GlobalState>(
&komorebi_client::send_query(&SocketMessage::GlobalState).unwrap(),
)
.unwrap();
let mut monitors = vec![];
for m in state.monitors.elements_mut() {
monitors.push(MonitorConfig::from(m.clone()));
}
let border_config = BorderConfig {
active_window_border_enabled: global_state.active_window_border_enabled,
active_window_border_style: global_state.active_window_border_style,
border_width: global_state.border_width,
border_offset: global_state.border_offset,
single: colour32(global_state.active_window_border_colours.single),
stack: colour32(global_state.active_window_border_colours.stack),
monocle: colour32(global_state.active_window_border_colours.monocle),
};
let stackbar_config = StackbarConfig {
stackbar_mode: global_state.stackbar_mode,
stackbar_focused_text_colour: colour32(global_state.stackbar_focused_text_colour),
stackbar_unfocused_text_colour: colour32(global_state.stackbar_unfocused_text_colour),
stackbar_tab_background_colour: colour32(global_state.stackbar_tab_background_colour),
stackbar_tab_width: global_state.stackbar_tab_width,
stackbar_height: global_state.stackbar_height,
};
let mut hwnd_lookup_windows = vec![];
unsafe {
EnumWindows(
Some(enum_window),
windows::Win32::Foundation::LPARAM(
&mut hwnd_lookup_windows as *mut Vec<Window> as isize,
),
)
.unwrap();
};
Self {
monitors,
border_config,
stackbar_config,
mouse_follows_focus: state.mouse_follows_focus,
hwnd_lookup: 0,
hwnd_lookup_windows,
hwnd_rule_debug: None,
}
}
}
struct BorderConfig {
active_window_border_enabled: bool,
active_window_border_style: ActiveWindowBorderStyle,
border_width: i32,
border_offset: i32,
single: Color32,
monocle: Color32,
stack: Color32,
}
struct StackbarConfig {
stackbar_mode: StackbarMode,
stackbar_focused_text_colour: Color32,
stackbar_unfocused_text_colour: Color32,
stackbar_tab_background_colour: Color32,
stackbar_tab_width: i32,
stackbar_height: i32,
}
#[derive(Clone)]
struct MonitorConfig {
work_area_offset: Rect,
size: Rect,
workspaces: Vec<WorkspaceConfig>,
}
impl From<Monitor> for MonitorConfig {
fn from(value: Monitor) -> Self {
let mut workspaces = vec![];
for ws in value.workspaces() {
workspaces.push(WorkspaceConfig::from(ws.clone()));
}
Self {
work_area_offset: value.work_area_offset().unwrap_or_default(),
size: *value.size(),
workspaces,
}
}
}
#[derive(Clone)]
struct WorkspaceConfig {
container_padding: i32,
workspace_padding: i32,
layout: DefaultLayout,
name: String,
}
impl From<Workspace> for WorkspaceConfig {
fn from(value: Workspace) -> Self {
Self {
container_padding: value.container_padding().unwrap_or(20),
workspace_padding: value.workspace_padding().unwrap_or(20),
layout: match value.layout() {
Layout::Default(layout) => *layout,
Layout::Custom(_) => DefaultLayout::BSP,
},
name: value
.name()
.clone()
.unwrap_or_else(|| random_word::gen(Lang::En).to_string()),
}
}
}
extern "system" fn enum_window(
hwnd: windows::Win32::Foundation::HWND,
lparam: windows::Win32::Foundation::LPARAM,
) -> windows::Win32::Foundation::BOOL {
let windows = unsafe { &mut *(lparam.0 as *mut Vec<Window>) };
let window = Window { hwnd: hwnd.0 };
if window.is_window()
&& !window.is_miminized()
&& window.is_visible()
&& window.title().is_ok()
&& window.exe().is_ok()
{
windows.push(window);
}
true.into()
}
fn json_view_ui(ui: &mut egui::Ui, code: &str) {
let language = "json";
let theme = egui_extras::syntax_highlighting::CodeTheme::from_memory(ui.ctx());
egui_extras::syntax_highlighting::code_view_ui(ui, &theme, code, language);
}
impl eframe::App for KomorebiEgui {
fn update(&mut self, ctx: &egui::Context, _frame: &mut eframe::Frame) {
ctx.set_pixels_per_point(1.5);
egui::CentralPanel::default().show(ctx, |ui| {
egui::ScrollArea::vertical().show(ui, |ui| {
ui.vertical_centered_justified(|ui| {
ui.set_width(ctx.input(|i| i.viewport().inner_rect.unwrap().width()));
ui.collapsing("Debug Windows and Rules", |ui| {
let window = Window {
hwnd: self.hwnd_lookup,
};
let title = if let (Ok(title), Ok(exe)) = (window.title(), window.exe()) {
format!("{} - {:?} - {:?}", window.hwnd, exe, title)
} else {
String::from("Select a Window")
};
if ui.button("Refresh Window List").clicked() {
let mut windows = vec![];
unsafe {
EnumWindows(
Some(enum_window),
windows::Win32::Foundation::LPARAM(
&mut windows as *mut Vec<Window> as isize,
),
)
.unwrap();
};
self.hwnd_lookup_windows = windows;
}
egui::ComboBox::from_label("Select one!")
.selected_text(format!("{:?}", title))
.show_ui(ui, |ui| {
for w in &self.hwnd_lookup_windows {
if ui
.selectable_value(
&mut self.hwnd_lookup,
w.hwnd,
format!(
"{} - {:?} - {:?}",
w.hwnd,
w.exe().unwrap(),
w.title().unwrap()
),
)
.changed()
{
let response = komorebi_client::send_query(
&SocketMessage::DebugWindow(w.hwnd),
)
.unwrap();
let debug: RuleDebug =
serde_json::from_str(&response).unwrap();
self.hwnd_rule_debug = Some(debug);
};
}
});
});
if let Some(debug) = &self.hwnd_rule_debug {
ui.horizontal(|ui| {
json_view_ui(ui, &serde_json::to_string_pretty(&debug).unwrap());
});
}
ui.separator();
ui.collapsing("Mouse", |ui| {
if ui
.toggle_value(&mut self.mouse_follows_focus, "Mouse Follows Focus")
.changed()
{
komorebi_client::send_message(&SocketMessage::MouseFollowsFocus(
self.mouse_follows_focus,
))
.unwrap();
}
});
ui.collapsing("Borders", |ui| {
if ui
.toggle_value(
&mut self.border_config.active_window_border_enabled,
"Active Window Border",
)
.changed()
{
komorebi_client::send_message(&SocketMessage::ActiveWindowBorder(
self.border_config.active_window_border_enabled,
))
.unwrap();
}
ui.collapsing("Style", |ui| {
for option in [
ActiveWindowBorderStyle::System,
ActiveWindowBorderStyle::Rounded,
ActiveWindowBorderStyle::Square,
] {
if ui
.add(egui::SelectableLabel::new(
option == self.border_config.active_window_border_style,
option.to_string(),
))
.clicked()
{
komorebi_client::send_message(
&SocketMessage::ActiveWindowBorderStyle(option),
)
.unwrap();
self.border_config.active_window_border_style = option;
}
}
});
ui.collapsing("Width", |ui| {
if ui
.add(egui::Slider::new(
&mut self.border_config.border_width,
-10..=30,
))
.drag_stopped()
{
komorebi_client::send_message(&SocketMessage::BorderWidth(
self.border_config.border_width,
))
.unwrap();
};
});
ui.collapsing("Offset", |ui| {
if ui
.add(egui::Slider::new(
&mut self.border_config.border_offset,
-10..=30,
))
.drag_stopped()
{
komorebi_client::send_message(&SocketMessage::BorderOffset(
self.border_config.border_offset,
))
.unwrap();
};
});
ui.collapsing("Colours", |ui| {
ui.collapsing("Single", |ui| {
if egui::color_picker::color_picker_color32(
ui,
&mut self.border_config.single,
Alpha::Opaque,
) {
komorebi_client::send_message(
&SocketMessage::ActiveWindowBorderColour(
WindowKind::Single,
self.border_config.single.r() as u32,
self.border_config.single.g() as u32,
self.border_config.single.b() as u32,
),
)
.unwrap();
}
});
ui.collapsing("Monocle", |ui| {
if egui::color_picker::color_picker_color32(
ui,
&mut self.border_config.monocle,
Alpha::Opaque,
) {
komorebi_client::send_message(
&SocketMessage::ActiveWindowBorderColour(
WindowKind::Single,
self.border_config.monocle.r() as u32,
self.border_config.monocle.g() as u32,
self.border_config.monocle.b() as u32,
),
)
.unwrap();
}
});
ui.collapsing("Stack", |ui| {
if egui::color_picker::color_picker_color32(
ui,
&mut self.border_config.stack,
Alpha::Opaque,
) {
komorebi_client::send_message(
&SocketMessage::ActiveWindowBorderColour(
WindowKind::Single,
self.border_config.stack.r() as u32,
self.border_config.stack.g() as u32,
self.border_config.stack.b() as u32,
),
)
.unwrap();
}
});
});
});
ui.collapsing("Stackbar", |ui| {
for option in [
StackbarMode::Never,
StackbarMode::OnStack,
StackbarMode::Always,
] {
if ui
.add(egui::SelectableLabel::new(
option == self.stackbar_config.stackbar_mode,
option.to_string(),
))
.clicked()
{
komorebi_client::send_message(&SocketMessage::StackbarMode(option))
.unwrap();
self.stackbar_config.stackbar_mode = option;
komorebi_client::send_message(&SocketMessage::Retile).unwrap();
}
}
ui.collapsing("Width", |ui| {
if ui
.add(egui::Slider::new(
&mut self.stackbar_config.stackbar_tab_width,
0..=600,
))
.drag_stopped()
{
komorebi_client::send_message(&SocketMessage::StackbarTabWidth(
self.stackbar_config.stackbar_tab_width,
))
.unwrap();
};
});
ui.collapsing("Height", |ui| {
if ui
.add(egui::Slider::new(
&mut self.stackbar_config.stackbar_height,
0..=50,
))
.drag_stopped()
{
komorebi_client::send_message(&SocketMessage::StackbarHeight(
self.stackbar_config.stackbar_height,
))
.unwrap();
};
});
ui.collapsing("Colours", |ui| {
ui.collapsing("Focused Text", |ui| {
if egui::color_picker::color_picker_color32(
ui,
&mut self.stackbar_config.stackbar_focused_text_colour,
Alpha::Opaque,
) {
komorebi_client::send_message(
&SocketMessage::StackbarFocusedTextColour(
self.stackbar_config.stackbar_focused_text_colour.r()
as u32,
self.stackbar_config.stackbar_focused_text_colour.g()
as u32,
self.stackbar_config.stackbar_focused_text_colour.b()
as u32,
),
)
.unwrap();
}
});
ui.collapsing("Unfocused Text", |ui| {
if egui::color_picker::color_picker_color32(
ui,
&mut self.stackbar_config.stackbar_unfocused_text_colour,
Alpha::Opaque,
) {
komorebi_client::send_message(
&SocketMessage::StackbarUnfocusedTextColour(
self.stackbar_config.stackbar_unfocused_text_colour.r()
as u32,
self.stackbar_config.stackbar_unfocused_text_colour.g()
as u32,
self.stackbar_config.stackbar_unfocused_text_colour.b()
as u32,
),
)
.unwrap();
}
});
ui.collapsing("Tab Background", |ui| {
if egui::color_picker::color_picker_color32(
ui,
&mut self.stackbar_config.stackbar_tab_background_colour,
Alpha::Opaque,
) {
komorebi_client::send_message(
&SocketMessage::StackbarBackgroundColour(
self.stackbar_config.stackbar_tab_background_colour.r()
as u32,
self.stackbar_config.stackbar_tab_background_colour.g()
as u32,
self.stackbar_config.stackbar_tab_background_colour.b()
as u32,
),
)
.unwrap();
}
});
});
});
for (monitor_idx, monitor) in self.monitors.iter_mut().enumerate() {
ui.collapsing(
format!(
"Monitor {monitor_idx} ({} x {})",
monitor.size.right, monitor.size.bottom
),
|ui| {
ui.collapsing("Work Area Offset", |ui| {
if ui
.add(
egui::Slider::new(
&mut monitor.work_area_offset.left,
0..=1000,
)
.text("Left"),
)
.drag_stopped()
{
komorebi_client::send_message(
&SocketMessage::MonitorWorkAreaOffset(
monitor_idx,
monitor.work_area_offset,
),
)
.unwrap();
};
if ui
.add(
egui::Slider::new(
&mut monitor.work_area_offset.top,
0..=1000,
)
.text("Top"),
)
.drag_stopped()
{
komorebi_client::send_message(
&SocketMessage::MonitorWorkAreaOffset(
monitor_idx,
monitor.work_area_offset,
),
)
.unwrap();
};
if ui
.add(
egui::Slider::new(
&mut monitor.work_area_offset.right,
0..=1000,
)
.text("Right"),
)
.drag_stopped()
{
komorebi_client::send_message(
&SocketMessage::MonitorWorkAreaOffset(
monitor_idx,
monitor.work_area_offset,
),
)
.unwrap();
};
if ui
.add(
egui::Slider::new(
&mut monitor.work_area_offset.bottom,
0..=1000,
)
.text("Bottom"),
)
.drag_stopped()
{
komorebi_client::send_message(
&SocketMessage::MonitorWorkAreaOffset(
monitor_idx,
monitor.work_area_offset,
),
)
.unwrap();
};
});
for (workspace_idx, workspace) in
monitor.workspaces.iter_mut().enumerate()
{
ui.collapsing(
format!("Workspace {workspace_idx} ({})", workspace.name),
|ui| {
if ui.button("Focus").clicked() {
komorebi_client::send_message(
&SocketMessage::MouseFollowsFocus(false),
)
.unwrap();
komorebi_client::send_message(
&SocketMessage::FocusMonitorWorkspaceNumber(
monitor_idx,
workspace_idx,
),
)
.unwrap();
komorebi_client::send_message(
&SocketMessage::MouseFollowsFocus(
self.mouse_follows_focus,
),
)
.unwrap();
}
ui.collapsing("Name", |ui| {
if ui
.text_edit_singleline(&mut workspace.name)
.lost_focus()
{
komorebi_client::send_message(
&SocketMessage::WorkspaceName(
monitor_idx,
workspace_idx,
workspace.name.clone(),
),
)
.unwrap();
}
});
ui.collapsing("Layout", |ui| {
for option in [
DefaultLayout::BSP,
DefaultLayout::Columns,
DefaultLayout::Rows,
DefaultLayout::VerticalStack,
DefaultLayout::HorizontalStack,
DefaultLayout::UltrawideVerticalStack,
DefaultLayout::Grid,
] {
if ui
.add(egui::SelectableLabel::new(
option == workspace.layout,
option.to_string(),
))
.clicked()
{
komorebi_client::send_message(
&SocketMessage::WorkspaceLayout(
monitor_idx,
workspace_idx,
option,
),
)
.unwrap();
workspace.layout = option;
}
}
});
ui.collapsing("Container Padding", |ui| {
if ui
.add(
egui::Slider::new(
&mut workspace.container_padding,
-100..=100,
)
.text("Container Padding"),
)
.drag_stopped()
{
komorebi_client::send_message(
&SocketMessage::ContainerPadding(
monitor_idx,
workspace_idx,
workspace.container_padding,
),
)
.unwrap();
komorebi_client::send_message(
&SocketMessage::Retile,
)
.unwrap();
};
});
ui.collapsing("Workspace Padding", |ui| {
if ui
.add(
egui::Slider::new(
&mut workspace.workspace_padding,
-100..=100,
)
.text("Workspace Padding"),
)
.drag_stopped()
{
komorebi_client::send_message(
&SocketMessage::WorkspacePadding(
monitor_idx,
workspace_idx,
workspace.workspace_padding,
),
)
.unwrap();
komorebi_client::send_message(
&SocketMessage::Retile,
)
.unwrap();
};
});
},
);
}
},
);
}
});
});
});
}
}

View File

@@ -1,6 +1,6 @@
[package]
name = "komorebi"
version = "0.1.23"
version = "0.1.25-dev.0"
authors = ["Jade Iqbal <jadeiqbal@fastmail.com>"]
description = "A tiling window manager for Windows"
categories = ["tiling-window-manager", "windows"]
@@ -13,7 +13,7 @@ edition = "2021"
[dependencies]
komorebi-core = { path = "../komorebi-core" }
bitflags = "2"
bitflags = { version = "2", features = ["serde"] }
clap = { version = "4", features = ["derive"] }
color-eyre = { workspace = true }
crossbeam-channel = "0.5"
@@ -24,7 +24,7 @@ getset = "0.1"
hex_color = { version = "3", features = ["serde"] }
hotwatch = "0.5"
lazy_static = "1"
miow = "0.5"
miow = "0.6"
nanoid = "0.4"
net2 = "0.2"
os_info = "3.8"
@@ -35,7 +35,7 @@ schemars = "0.8"
serde = { version = "1", features = ["derive"] }
serde_json = { workspace = true }
strum = { version = "0.26", features = ["derive"] }
sysinfo = "0.30"
sysinfo = { workspace = true }
tracing = "0.1"
tracing-appender = "0.2"
tracing-subscriber = { version = "0.3", features = ["env-filter"] }

View File

@@ -9,8 +9,9 @@ use serde::Serialize;
use crate::ring::Ring;
use crate::stackbar::Stackbar;
use crate::window::Window;
use crate::StackbarMode;
use crate::WindowsApi;
use crate::STACKBAR_MODE;
use komorebi_core::StackbarMode;
#[derive(Debug, Clone, Serialize, Deserialize, Getters, JsonSchema)]
pub struct Container {
@@ -156,4 +157,29 @@ impl Container {
tracing::info!("focusing window");
self.windows.focus(idx);
}
pub fn set_stackbar_mode(&mut self, mode: StackbarMode) {
self.stackbar = match mode {
StackbarMode::Always => Stackbar::create().ok(),
StackbarMode::Never => None,
StackbarMode::OnStack => {
if self.windows().len() > 1 && self.stackbar().is_none() {
Stackbar::create().ok()
} else {
None
}
}
};
}
pub fn renew_stackbar(&mut self) {
match &self.stackbar {
None => {}
Some(stackbar) => {
if !WindowsApi::is_window(stackbar.hwnd()) {
self.stackbar = Stackbar::create().ok()
}
}
}
}
}

View File

@@ -37,11 +37,13 @@ use std::sync::atomic::AtomicU32;
use std::sync::atomic::Ordering;
use std::sync::Arc;
pub use colour::*;
pub use hidden::*;
pub use process_command::*;
pub use process_event::*;
pub use stackbar::*;
pub use static_config::*;
pub use window::*;
pub use window_manager::*;
pub use window_manager_event::*;
pub use windows_api::WindowsApi;
@@ -51,10 +53,12 @@ use color_eyre::Result;
use komorebi_core::config_generation::IdWithIdentifier;
use komorebi_core::config_generation::MatchingRule;
use komorebi_core::config_generation::MatchingStrategy;
use komorebi_core::ActiveWindowBorderStyle;
use komorebi_core::ApplicationIdentifier;
use komorebi_core::HidingBehaviour;
use komorebi_core::Rect;
use komorebi_core::SocketMessage;
use komorebi_core::StackbarMode;
use os_info::Version;
use parking_lot::Mutex;
use regex::Regex;

View File

@@ -13,6 +13,7 @@ use std::sync::Arc;
use std::time::Duration;
use color_eyre::eyre::anyhow;
use color_eyre::eyre::bail;
use color_eyre::Result;
use miow::pipe::connect;
use net2::TcpStreamExt;
@@ -43,12 +44,15 @@ use crate::colour::Rgb;
use crate::current_virtual_desktop;
use crate::notify_subscribers;
use crate::static_config::StaticConfig;
use crate::window::RuleDebug;
use crate::window::Window;
use crate::window_manager;
use crate::window_manager::WindowManager;
use crate::windows_api::WindowsApi;
use crate::GlobalState;
use crate::Notification;
use crate::NotificationEvent;
use crate::ACTIVE_WINDOW_BORDER_STYLE;
use crate::BORDER_COLOUR_CURRENT;
use crate::BORDER_COLOUR_MONOCLE;
use crate::BORDER_COLOUR_SINGLE;
@@ -70,6 +74,12 @@ use crate::MONITOR_INDEX_PREFERENCES;
use crate::NO_TITLEBAR;
use crate::OBJECT_NAME_CHANGE_ON_LAUNCH;
use crate::REMOVE_TITLEBARS;
use crate::STACKBAR_FOCUSED_TEXT_COLOUR;
use crate::STACKBAR_MODE;
use crate::STACKBAR_TAB_BACKGROUND_COLOUR;
use crate::STACKBAR_TAB_HEIGHT;
use crate::STACKBAR_TAB_WIDTH;
use crate::STACKBAR_UNFOCUSED_TEXT_COLOUR;
use crate::SUBSCRIPTION_PIPES;
use crate::SUBSCRIPTION_SOCKETS;
use crate::TCP_CONNECTIONS;
@@ -455,6 +465,9 @@ impl WindowManager {
SocketMessage::SendContainerToMonitorWorkspaceNumber(monitor_idx, workspace_idx) => {
self.move_container_to_monitor(monitor_idx, Option::from(workspace_idx), false)?;
}
SocketMessage::MoveContainerToMonitorWorkspaceNumber(monitor_idx, workspace_idx) => {
self.move_container_to_monitor(monitor_idx, Option::from(workspace_idx), true)?;
}
SocketMessage::SendContainerToNamedWorkspace(ref workspace) => {
if let Some((monitor_idx, workspace_idx)) =
self.monitor_workspace_index_by_name(workspace)
@@ -498,11 +511,11 @@ impl WindowManager {
);
self.focus_monitor(monitor_idx)?;
self.update_focused_workspace(self.mouse_follows_focus)?;
self.update_focused_workspace(self.mouse_follows_focus, true)?;
}
SocketMessage::FocusMonitorNumber(monitor_idx) => {
self.focus_monitor(monitor_idx)?;
self.update_focused_workspace(self.mouse_follows_focus)?;
self.update_focused_workspace(self.mouse_follows_focus, true)?;
}
SocketMessage::Retile => self.retile_all(false)?,
SocketMessage::FlipLayout(layout_flip) => self.flip_layout(layout_flip)?,
@@ -765,6 +778,18 @@ impl WindowManager {
tracing::info!("replying to state done");
}
SocketMessage::GlobalState => {
let state = match serde_json::to_string_pretty(&GlobalState::default()) {
Ok(state) => state,
Err(error) => error.to_string(),
};
tracing::info!("replying to global state");
reply.write_all(state.as_bytes())?;
tracing::info!("replying to global state done");
}
SocketMessage::VisibleWindows => {
let mut monitor_visible_windows = HashMap::new();
@@ -913,7 +938,7 @@ impl WindowManager {
}
}
self.update_focused_workspace(false)?;
self.update_focused_workspace(false, false)?;
}
SocketMessage::FocusFollowsMouse(mut implementation, enable) => {
if !CUSTOM_FFM.load(Ordering::SeqCst) {
@@ -1019,7 +1044,7 @@ impl WindowManager {
SocketMessage::CompleteConfiguration => {
if !INITIAL_CONFIGURATION_LOADED.load(Ordering::SeqCst) {
INITIAL_CONFIGURATION_LOADED.store(true, Ordering::SeqCst);
self.update_focused_workspace(false)?;
self.update_focused_workspace(false, false)?;
}
}
SocketMessage::WatchConfiguration(enable) => {
@@ -1126,7 +1151,7 @@ impl WindowManager {
let resize: Vec<Option<Rect>> = serde_json::from_reader(file)?;
workspace.set_resize_dimensions(resize);
self.update_focused_workspace(false)?;
self.update_focused_workspace(false, false)?;
}
SocketMessage::Save(ref path) => {
let workspace = self.focused_workspace_mut()?;
@@ -1149,7 +1174,7 @@ impl WindowManager {
let resize: Vec<Option<Rect>> = serde_json::from_reader(file)?;
workspace.set_resize_dimensions(resize);
self.update_focused_workspace(false)?;
self.update_focused_workspace(false, false)?;
}
SocketMessage::AddSubscriberSocket(ref socket) => {
let mut sockets = SUBSCRIPTION_SOCKETS.lock();
@@ -1241,14 +1266,50 @@ impl WindowManager {
WindowsApi::invalidate_border_rect()?;
}
SocketMessage::ActiveWindowBorderWidth(width) => {
SocketMessage::ActiveWindowBorderStyle(style) => {
let mut active_window_border_style = ACTIVE_WINDOW_BORDER_STYLE.lock();
*active_window_border_style = style;
WindowsApi::invalidate_border_rect()?;
}
SocketMessage::BorderWidth(width) => {
BORDER_WIDTH.store(width, Ordering::SeqCst);
WindowsApi::invalidate_border_rect()?;
}
SocketMessage::ActiveWindowBorderOffset(offset) => {
SocketMessage::BorderOffset(offset) => {
BORDER_OFFSET.store(offset, Ordering::SeqCst);
WindowsApi::invalidate_border_rect()?;
}
SocketMessage::StackbarMode(mode) => {
let mut stackbar_mode = STACKBAR_MODE.lock();
*stackbar_mode = mode;
for m in self.monitors_mut() {
for w in m.workspaces_mut() {
for c in w.containers_mut() {
c.set_stackbar_mode(mode);
}
}
}
}
SocketMessage::StackbarFocusedTextColour(r, g, b) => {
let rgb = Rgb::new(r, g, b);
STACKBAR_FOCUSED_TEXT_COLOUR.store(rgb.into(), Ordering::SeqCst);
}
SocketMessage::StackbarUnfocusedTextColour(r, g, b) => {
let rgb = Rgb::new(r, g, b);
STACKBAR_UNFOCUSED_TEXT_COLOUR.store(rgb.into(), Ordering::SeqCst);
}
SocketMessage::StackbarBackgroundColour(r, g, b) => {
let rgb = Rgb::new(r, g, b);
STACKBAR_TAB_BACKGROUND_COLOUR.store(rgb.into(), Ordering::SeqCst);
}
SocketMessage::StackbarHeight(height) => {
STACKBAR_TAB_HEIGHT.store(height, Ordering::SeqCst);
}
SocketMessage::StackbarTabWidth(width) => {
STACKBAR_TAB_WIDTH.store(width, Ordering::SeqCst);
}
SocketMessage::ApplicationSpecificConfigurationSchema => {
let asc = schema_for!(Vec<ApplicationConfiguration>);
let schema = serde_json::to_string_pretty(&asc)?;
@@ -1294,7 +1355,15 @@ impl WindowManager {
SocketMessage::ToggleTitleBars => {
let current = REMOVE_TITLEBARS.load(Ordering::SeqCst);
REMOVE_TITLEBARS.store(!current, Ordering::SeqCst);
self.update_focused_workspace(false)?;
self.update_focused_workspace(false, false)?;
}
SocketMessage::DebugWindow(hwnd) => {
let window = Window { hwnd };
let mut rule_debug = RuleDebug::default();
let _ = window.should_manage(None, &mut rule_debug);
let schema = serde_json::to_string_pretty(&rule_debug)?;
reply.write_all(schema.as_bytes())?;
}
// Deprecated commands
SocketMessage::AltFocusHack(_)
@@ -1345,6 +1414,8 @@ impl WindowManager {
| SocketMessage::MoveWorkspaceToMonitorNumber(_)
| SocketMessage::MoveContainerToMonitorNumber(_)
| SocketMessage::MoveContainerToWorkspaceNumber(_)
| SocketMessage::MoveContainerToMonitorWorkspaceNumber(_, _)
| SocketMessage::MoveContainerToNamedWorkspace(_)
| SocketMessage::ResizeWindowEdge(_, _)
| SocketMessage::ResizeWindowAxis(_, _)
| SocketMessage::ToggleFloat
@@ -1357,7 +1428,7 @@ impl WindowManager {
| SocketMessage::Retile
// Adding this one so that changes can be seen instantly after
// modifying the active window border offset
| SocketMessage::ActiveWindowBorderOffset(_)
| SocketMessage::BorderOffset(_)
// Adding this one because sometimes EVENT_SYSTEM_FOREGROUND isn't
// getting sent on FocusWindow, meaning the border won't be set
// when processing events
@@ -1371,7 +1442,33 @@ impl WindowManager {
| SocketMessage::FocusMonitorNumber(_)
| SocketMessage::FocusMonitorWorkspaceNumber(_, _)
| SocketMessage::FocusWorkspaceNumber(_) => {
let foreground = WindowsApi::foreground_window()?;
// The foreground window might be de-activating if we've just
// set it as a result of our own actions, so wait until the new
// one returns. This particularly happens when switching monitors.
//
// TODO(raggi): re-evaluate this branch. I checked the
// suggestion from the comment above, that we don't get
// EVENT_SYSTEM_FOREGROUND, but if I print out trace events I
// see that we do.
// XXX(raggi) We drop FocusChange events though for windows that
// we're not managing, so that's one of the ways that the border
// window gets stuck. We should stop overloading `should_manage`
// as an event filter, and separately filter events that we want
// to handle, and windows that we want to handle, as some events
// must be handled even if we're not managing the target window.
let mut attempts = 0;
let foreground = loop {
match WindowsApi::foreground_window() {
Ok(foreground) => break foreground,
Err(_) => {
std::thread::sleep(std::time::Duration::from_millis(10));
attempts+=1;
if attempts == 10 {
bail!("failed to get foreground window after 100ms")
}
}
};
};
let foreground_window = Window { hwnd: foreground };
let monocle = BORDER_COLOUR_MONOCLE.load(Ordering::SeqCst);
@@ -1426,7 +1523,7 @@ impl WindowManager {
Ok(())
}
#[tracing::instrument(skip(self))]
#[tracing::instrument(skip(self), level = "debug")]
fn handle_initial_workspace_rules(
&mut self,
id: &String,
@@ -1438,7 +1535,7 @@ impl WindowManager {
Ok(())
}
#[tracing::instrument(skip(self))]
#[tracing::instrument(skip(self), level = "debug")]
fn handle_definitive_workspace_rules(
&mut self,
id: &String,
@@ -1450,7 +1547,7 @@ impl WindowManager {
Ok(())
}
#[tracing::instrument(skip(self))]
#[tracing::instrument(skip(self), level = "debug")]
pub fn handle_workspace_rules(
&mut self,
id: &String,
@@ -1485,9 +1582,10 @@ pub fn read_commands_uds(wm: &Arc<Mutex<WindowManager>>, mut stream: UnixStream)
if wm.is_paused {
return match message {
SocketMessage::TogglePause | SocketMessage::State | SocketMessage::Stop => {
Ok(wm.process_command(message, &mut stream)?)
}
SocketMessage::TogglePause
| SocketMessage::State
| SocketMessage::GlobalState
| SocketMessage::Stop => Ok(wm.process_command(message, &mut stream)?),
_ => {
tracing::trace!("ignoring while paused");
Ok(())
@@ -1534,9 +1632,10 @@ pub fn read_commands_tcp(
if wm.is_paused {
return match message {
SocketMessage::TogglePause | SocketMessage::State | SocketMessage::Stop => {
Ok(wm.process_command(message, stream)?)
}
SocketMessage::TogglePause
| SocketMessage::State
| SocketMessage::GlobalState
| SocketMessage::Stop => Ok(wm.process_command(message, stream)?),
_ => {
tracing::trace!("ignoring while paused");
Ok(())

View File

@@ -4,7 +4,6 @@ use std::sync::Arc;
use color_eyre::eyre::anyhow;
use color_eyre::Result;
use crossbeam_channel::select;
use parking_lot::Mutex;
use komorebi_core::OperationDirection;
@@ -16,6 +15,7 @@ use crate::border::Border;
use crate::current_virtual_desktop;
use crate::notify_subscribers;
use crate::window::should_act;
use crate::window::RuleDebug;
use crate::window_manager::WindowManager;
use crate::window_manager_event::WindowManagerEvent;
use crate::windows_api::WindowsApi;
@@ -42,12 +42,14 @@ pub fn listen_for_events(wm: Arc<Mutex<WindowManager>>) {
std::thread::spawn(move || {
tracing::info!("listening");
loop {
select! {
recv(receiver) -> mut maybe_event => {
if let Ok(event) = maybe_event.as_mut() {
match wm.lock().process_event(event) {
Ok(()) => {},
Err(error) => tracing::error!("{}", error)
if let Ok(event) = receiver.recv() {
match wm.lock().process_event(event) {
Ok(()) => {}
Err(error) => {
if cfg!(debug_assertions) {
tracing::error!("{:?}", error)
} else {
tracing::error!("{}", error)
}
}
}
@@ -59,12 +61,44 @@ pub fn listen_for_events(wm: Arc<Mutex<WindowManager>>) {
impl WindowManager {
#[allow(clippy::too_many_lines, clippy::cognitive_complexity)]
#[tracing::instrument(skip(self))]
pub fn process_event(&mut self, event: &mut WindowManagerEvent) -> Result<()> {
pub fn process_event(&mut self, event: WindowManagerEvent) -> Result<()> {
if self.is_paused {
tracing::trace!("ignoring while paused");
return Ok(());
}
let mut rule_debug = RuleDebug::default();
let should_manage = event.window().should_manage(Some(event), &mut rule_debug)?;
// Hide or reposition the window based on whether the target is managed.
if BORDER_ENABLED.load(Ordering::SeqCst) {
if let WindowManagerEvent::FocusChange(_, window) = event {
let border_window = Border::from(BORDER_HWND.load(Ordering::SeqCst));
if should_manage {
border_window.set_position(window, true)?;
} else {
let mut stackbar = false;
if let Ok(class) = window.class() {
if class == "komorebi_stackbar" {
stackbar = true;
}
}
if !stackbar {
border_window.hide()?;
}
}
}
}
// All event handlers below this point should only be processed if the event is
// related to a window that should be managed by the WindowManager.
if !should_manage && !matches!(event, WindowManagerEvent::DisplayChange(_)) {
return Ok(());
}
if let Some(virtual_desktop_id) = &self.virtual_desktop_id {
if let Some(id) = current_virtual_desktop() {
if id != *virtual_desktop_id {
@@ -85,7 +119,7 @@ impl WindowManager {
| WindowManagerEvent::MoveResizeEnd(_, window) => {
self.reconcile_monitors()?;
let monitor_idx = self.monitor_idx_from_window(*window)
let monitor_idx = self.monitor_idx_from_window(window)
.ok_or_else(|| anyhow!("there is no monitor associated with this window, it may have already been destroyed"))?;
// This is a hidden window apparently associated with COM support mechanisms (based
@@ -122,6 +156,10 @@ impl WindowManager {
};
for (j, workspace) in monitor.workspaces_mut().iter_mut().enumerate() {
if let WindowManagerEvent::FocusChange(_, window) = event {
let _ = workspace.focus_changed(window.hwnd);
}
let reaped_orphans = workspace.reap_orphans()?;
if reaped_orphans.0 > 0 || reaped_orphans.1 > 0 {
workspace.update(&work_area, offset)?;
@@ -146,17 +184,18 @@ impl WindowManager {
}
match event {
WindowManagerEvent::Raise(window) => {
window.focus(false)?;
WindowManagerEvent::Raise(_window) => {
self.has_pending_raise_op = false;
}
WindowManagerEvent::Destroy(_, window) | WindowManagerEvent::Unmanage(window) => {
self.focused_workspace_mut()?.remove_window(window.hwnd)?;
self.update_focused_workspace(false)?;
if self.focused_workspace()?.contains_window(window.hwnd) {
self.focused_workspace_mut()?.remove_window(window.hwnd)?;
self.update_focused_workspace(false, false)?;
let mut already_moved_window_handles = self.already_moved_window_handles.lock();
let mut already_moved_window_handles = self.already_moved_window_handles.lock();
already_moved_window_handles.remove(&window.hwnd);
already_moved_window_handles.remove(&window.hwnd);
}
}
WindowManagerEvent::Minimize(_, window) => {
let mut hide = false;
@@ -170,7 +209,7 @@ impl WindowManager {
if hide {
self.focused_workspace_mut()?.remove_window(window.hwnd)?;
self.update_focused_workspace(false)?;
self.update_focused_workspace(false, false)?;
}
}
WindowManagerEvent::Hide(_, window) => {
@@ -199,7 +238,8 @@ impl WindowManager {
path,
&tray_and_multi_window_identifiers,
&regex_identifiers,
);
)
.is_some();
if !window.is_window()
|| should_act
@@ -211,7 +251,7 @@ impl WindowManager {
if hide {
self.focused_workspace_mut()?.remove_window(window.hwnd)?;
self.update_focused_workspace(false)?;
self.update_focused_workspace(false, false)?;
}
let mut already_moved_window_handles = self.already_moved_window_handles.lock();
@@ -219,6 +259,8 @@ impl WindowManager {
already_moved_window_handles.remove(&window.hwnd);
}
WindowManagerEvent::FocusChange(_, window) => {
self.update_focused_workspace(true, false)?;
let workspace = self.focused_workspace_mut()?;
if !workspace
.floating_windows()
@@ -292,15 +334,15 @@ impl WindowManager {
if !workspace.contains_window(window.hwnd) {
match behaviour {
WindowContainerBehaviour::Create => {
workspace.new_container_for_window(*window);
self.update_focused_workspace(false)?;
workspace.new_container_for_window(window);
self.update_focused_workspace(false, false)?;
}
WindowContainerBehaviour::Append => {
workspace
.focused_container_mut()
.ok_or_else(|| anyhow!("there is no focused container"))?
.add_window(*window);
self.update_focused_workspace(true)?;
.add_window(window);
self.update_focused_workspace(true, false)?;
}
}
}
@@ -340,34 +382,52 @@ impl WindowManager {
let workspace = self.focused_workspace_mut()?;
if workspace.contains_managed_window(window.hwnd) {
let focused_container_idx = workspace.focused_container_idx();
let focused_container_idx = workspace.focused_container_idx();
let new_position = WindowsApi::window_rect(window.hwnd())?;
let old_position = *workspace
.latest_layout()
.get(focused_container_idx)
// If the move was to another monitor with an empty workspace, the
// workspace here will refer to that empty workspace, which won't
// have any latest layout set. We fall back to a Default for Rect
// which allows us to make a reasonable guess that the drag has taken
// place across a monitor boundary to an empty workspace
.unwrap_or(&Rect::default());
let new_position = WindowsApi::window_rect(window.hwnd())?;
// This will be true if we have moved to an empty workspace on another monitor
let mut moved_across_monitors = old_position == Rect::default();
if let Some((origin_monitor_idx, origin_workspace_idx, _)) = pending {
// If we didn't move to another monitor with an empty workspace, it is
// still possible that we moved to another monitor with a populated workspace
if !moved_across_monitors {
// So we'll check if the origin monitor index and the target monitor index
// are different, if they are, we can set the override
moved_across_monitors = origin_monitor_idx != target_monitor_idx;
let old_position = *workspace
.latest_layout()
.get(focused_container_idx)
// If the move was to another monitor with an empty workspace, the
// workspace here will refer to that empty workspace, which won't
// have any latest layout set. We fall back to a Default for Rect
// which allows us to make a reasonable guess that the drag has taken
// place across a monitor boundary to an empty workspace
.unwrap_or(&Rect::default());
if moved_across_monitors {
// Want to make sure that we exclude unmanaged windows from cross-monitor
// moves with a mouse, otherwise the currently focused idx container will
// be moved when we just want to drag an unmanaged window
let origin_workspace = self
.monitors()
.get(origin_monitor_idx)
.ok_or_else(|| anyhow!("cannot get monitor idx"))?
.workspaces()
.get(origin_workspace_idx)
.ok_or_else(|| anyhow!("cannot get workspace idx"))?;
// This will be true if we have moved to an empty workspace on another monitor
let mut moved_across_monitors = old_position == Rect::default();
let managed_window =
origin_workspace.contains_managed_window(window.hwnd);
if let Some((origin_monitor_idx, _, _)) = pending {
// If we didn't move to another monitor with an empty workspace, it is
// still possible that we moved to another monitor with a populated workspace
if !moved_across_monitors {
// So we'll check if the origin monitor index and the target monitor index
// are different, if they are, we can set the override
moved_across_monitors = origin_monitor_idx != target_monitor_idx;
if !managed_window {
moved_across_monitors = false;
}
}
}
}
let workspace = self.focused_workspace_mut()?;
if workspace.contains_managed_window(window.hwnd) || moved_across_monitors {
let resize = Rect {
left: new_position.left - old_position.left,
top: new_position.top - old_position.top,
@@ -432,11 +492,11 @@ impl WindowManager {
// the origin monitor's focused workspace
self.focus_monitor(origin_monitor_idx)?;
self.focus_workspace(origin_workspace_idx)?;
self.update_focused_workspace(false)?;
self.update_focused_workspace(false, false)?;
self.focus_monitor(target_monitor_idx)?;
self.focus_workspace(target_workspace_idx)?;
self.update_focused_workspace(false)?;
self.update_focused_workspace(false, false)?;
}
// Here we handle a simple move on the same monitor which is treated as
// a container swap
@@ -447,11 +507,12 @@ impl WindowManager {
Some(target_idx) => {
workspace
.swap_containers(focused_container_idx, target_idx);
self.update_focused_workspace(false)?;
self.update_focused_workspace(false, false)?;
}
None => {
self.update_focused_workspace(
self.mouse_follows_focus,
false,
)?;
}
}
@@ -460,11 +521,12 @@ impl WindowManager {
match workspace.container_idx_from_current_point() {
Some(target_idx) => {
workspace.move_window_to_container(target_idx)?;
self.update_focused_workspace(false)?;
self.update_focused_workspace(false, false)?;
}
None => {
self.update_focused_workspace(
self.mouse_follows_focus,
false,
)?;
}
}
@@ -511,12 +573,12 @@ impl WindowManager {
self.resize_window(edge, sizing, delta, true)?;
}
self.update_focused_workspace(false)?;
self.update_focused_workspace(false, false)?;
}
}
}
WindowManagerEvent::ForceUpdate(_) => {
self.update_focused_workspace(false)?;
self.update_focused_workspace(false, true)?;
}
WindowManagerEvent::DisplayChange(..)
| WindowManagerEvent::MouseCapture(..)
@@ -552,7 +614,7 @@ impl WindowManager {
.iter()
.any(|w| w.hwnd == window.hwnd)
{
target_window = Option::from(*window);
target_window = Option::from(window);
WindowsApi::raise_window(border.hwnd())?;
};
@@ -645,7 +707,7 @@ impl WindowManager {
serde_json::to_writer_pretty(&file, &known_hwnds)?;
notify_subscribers(&serde_json::to_string(&Notification {
event: NotificationEvent::WindowManager(*event),
event: NotificationEvent::WindowManager(event),
state: self.as_ref().into(),
})?)?;

View File

@@ -55,9 +55,6 @@ use komorebi_core::Rect;
use crate::window::Window;
use crate::windows_api::WindowsApi;
use crate::winevent::WinEvent;
use crate::winevent_listener;
use crate::WindowManagerEvent;
use crate::DEFAULT_CONTAINER_PADDING;
use crate::STACKBAR_FOCUSED_TEXT_COLOUR;
use crate::STACKBAR_TAB_BACKGROUND_COLOUR;
@@ -116,12 +113,10 @@ impl Stackbar {
if x >= left && x <= right && y >= top && y <= bottom {
let window = Window { hwnd: *win_hwnd };
let event_sender = winevent_listener::event_tx();
let _ = event_sender.send(WindowManagerEvent::FocusChange(
WinEvent::ObjectFocus,
window,
));
let _ = event_sender.send(WindowManagerEvent::ForceUpdate(window));
window.restore();
if let Err(err) = window.focus(false) {
tracing::error!("Stackbar focus error: HWND:{} {}", *win_hwnd, err);
}
}
}
}
@@ -240,8 +235,6 @@ impl Stackbar {
for (i, window) in windows.iter().enumerate() {
if window.hwnd == focused_hwnd {
SetTextColor(hdc, COLORREF(focused_text_colour));
window.focus(false)?;
} else {
SetTextColor(hdc, COLORREF(unfocused_text_colour));
}

View File

@@ -35,6 +35,7 @@ use crate::STACKBAR_TAB_WIDTH;
use crate::STACKBAR_UNFOCUSED_TEXT_COLOUR;
use crate::TRAY_AND_MULTI_WINDOW_IDENTIFIERS;
use crate::WORKSPACE_RULES;
use komorebi_core::StackbarMode;
use color_eyre::Result;
use crossbeam_channel::Receiver;
@@ -47,6 +48,7 @@ use komorebi_core::config_generation::IdWithIdentifier;
use komorebi_core::config_generation::MatchingRule;
use komorebi_core::config_generation::MatchingStrategy;
use komorebi_core::resolve_home_path;
use komorebi_core::ActiveWindowBorderStyle;
use komorebi_core::ApplicationIdentifier;
use komorebi_core::DefaultLayout;
use komorebi_core::FocusFollowsMouseImplementation;
@@ -82,17 +84,6 @@ pub struct ActiveWindowBorderColours {
pub monocle: Colour,
}
#[derive(Default, Copy, Clone, Debug, Serialize, Deserialize, JsonSchema)]
pub enum ActiveWindowBorderStyle {
#[default]
/// Use the system border style
System,
/// Use the Windows 11-style rounded borders
Rounded,
/// Use the Windows 10-style square borders
Square,
}
#[derive(Debug, Serialize, Deserialize, JsonSchema)]
pub struct WorkspaceConfig {
/// Name
@@ -231,7 +222,7 @@ impl From<&Monitor> for MonitorConfig {
}
#[derive(Debug, Serialize, Deserialize, JsonSchema)]
/// The `komorebi.json` static configuration file reference for `v0.1.20`
/// The `komorebi.json` static configuration file reference for `v0.1.24`
pub struct StaticConfig {
/// DEPRECATED from v0.1.22: no longer required
#[serde(skip_serializing_if = "Option::is_none")]
@@ -317,13 +308,6 @@ pub struct StaticConfig {
pub stackbar: Option<StackbarConfig>,
}
#[derive(Debug, Copy, Clone, Serialize, Deserialize, JsonSchema)]
pub enum StackbarMode {
Always,
Never,
OnStack,
}
#[derive(Debug, Serialize, Deserialize, JsonSchema)]
pub struct TabsConfig {
width: Option<i32>,
@@ -334,9 +318,9 @@ pub struct TabsConfig {
#[derive(Debug, Serialize, Deserialize, JsonSchema)]
pub struct StackbarConfig {
height: Option<i32>,
mode: Option<StackbarMode>,
tabs: Option<TabsConfig>,
pub height: Option<i32>,
pub mode: Option<StackbarMode>,
pub tabs: Option<TabsConfig>,
}
impl From<&WindowManager> for StaticConfig {
@@ -534,10 +518,12 @@ impl StaticConfig {
if let Some(height) = &stackbar.height {
STACKBAR_TAB_HEIGHT.store(*height, Ordering::SeqCst);
}
if let Some(mode) = &stackbar.mode {
let mut stackbar_mode = STACKBAR_MODE.lock();
*stackbar_mode = *mode;
}
if let Some(tabs) = &stackbar.tabs {
if let Some(background) = &tabs.background {
STACKBAR_TAB_BACKGROUND_COLOUR.store((*background).into(), Ordering::SeqCst);
@@ -741,6 +727,16 @@ impl StaticConfig {
value.apply_globals()?;
let stackbar_mode = *STACKBAR_MODE.lock();
for m in wm.monitors_mut() {
for w in m.workspaces_mut() {
for c in w.containers_mut() {
c.set_stackbar_mode(stackbar_mode);
}
}
}
if let Some(monitors) = value.monitors {
for (i, monitor) in monitors.iter().enumerate() {
if let Some(m) = wm.monitors_mut().get_mut(i) {

View File

@@ -1,4 +1,6 @@
use bitflags::bitflags;
use serde::Deserialize;
use serde::Serialize;
use windows::Win32::UI::WindowsAndMessaging::WS_BORDER;
use windows::Win32::UI::WindowsAndMessaging::WS_CAPTION;
use windows::Win32::UI::WindowsAndMessaging::WS_CHILD;
@@ -56,7 +58,7 @@ use windows::Win32::UI::WindowsAndMessaging::WS_VSCROLL;
// https://docs.microsoft.com/en-us/windows/win32/winmsg/window-styles
bitflags! {
#[derive(Default)]
#[derive(Default, Debug, Copy, Clone, Serialize, Deserialize)]
pub struct WindowStyle: u32 {
const BORDER = WS_BORDER.0;
const CAPTION = WS_CAPTION.0;
@@ -90,7 +92,7 @@ bitflags! {
// https://docs.microsoft.com/en-us/windows/win32/winmsg/extended-window-styles
bitflags! {
#[derive(Default)]
#[derive(Default, Debug, Copy, Clone, Serialize, Deserialize)]
pub struct ExtendedWindowStyle: u32 {
const ACCEPTFILES = WS_EX_ACCEPTFILES.0;
const APPWINDOW = WS_EX_APPWINDOW.0;

View File

@@ -41,7 +41,7 @@ use crate::WSL2_UI_PROCESSES;
#[derive(Debug, Clone, Copy, Deserialize, JsonSchema)]
pub struct Window {
pub(crate) hwnd: isize,
pub hwnd: isize,
}
#[allow(clippy::module_name_repetitions)]
@@ -125,7 +125,7 @@ impl Window {
HWND(self.hwnd)
}
pub fn center(&mut self, work_area: &Rect) -> Result<()> {
pub fn center(&self, work_area: &Rect) -> Result<()> {
let half_width = work_area.right / 2;
let half_weight = work_area.bottom / 2;
@@ -140,7 +140,7 @@ impl Window {
)
}
pub fn set_position(&mut self, layout: &Rect, top: bool) -> Result<()> {
pub fn set_position(&self, layout: &Rect, top: bool) -> Result<()> {
let rect = *layout;
WindowsApi::position_window(self.hwnd(), &rect, top)
}
@@ -153,6 +153,10 @@ impl Window {
WindowsApi::is_iconic(self.hwnd())
}
pub fn is_visible(self) -> bool {
WindowsApi::is_window_visible(self.hwnd())
}
pub fn hide(self) {
let mut programmatically_hidden_hwnds = HIDDEN_HWNDS.lock();
if !programmatically_hidden_hwnds.contains(&self.hwnd) {
@@ -218,77 +222,20 @@ impl Window {
}
pub fn focus(self, mouse_follows_focus: bool) -> Result<()> {
// Attach komorebi thread to Window thread
let (_, window_thread_id) = WindowsApi::window_thread_process_id(self.hwnd());
let current_thread_id = WindowsApi::current_thread_id();
// This can be allowed to fail if a window doesn't have a message queue or if a journal record
// hook has been installed
// https://docs.microsoft.com/en-us/windows/win32/api/winuser/nf-winuser-attachthreadinput#remarks
match WindowsApi::attach_thread_input(current_thread_id, window_thread_id, true) {
Ok(()) => {}
Err(error) => {
tracing::error!(
"could not attach to window thread input processing mechanism, but continuing execution of focus(): {}",
error
);
// If the target window is already focused, do nothing.
if let Ok(ihwnd) = WindowsApi::foreground_window() {
if HWND(ihwnd) == self.hwnd() {
return Ok(());
}
};
// Raise Window to foreground
let mut foregrounded = false;
let mut tried_resetting_foreground_access = false;
let mut max_attempts = 10;
while !foregrounded && max_attempts > 0 {
match WindowsApi::set_foreground_window(self.hwnd()) {
Ok(()) => {
foregrounded = true;
}
Err(error) => {
max_attempts -= 1;
tracing::error!(
"could not set as foreground window, but continuing execution of focus(): {}",
error
);
// If this still doesn't work then maybe try https://learn.microsoft.com/en-us/windows/win32/api/winuser/nf-winuser-locksetforegroundwindow
if !tried_resetting_foreground_access {
let process_id = WindowsApi::current_process_id();
if WindowsApi::allow_set_foreground_window(process_id).is_ok() {
tried_resetting_foreground_access = true;
}
}
}
};
}
WindowsApi::raise_and_focus_window(self.hwnd())?;
// Center cursor in Window
if mouse_follows_focus {
WindowsApi::center_cursor_in_rect(&WindowsApi::window_rect(self.hwnd())?)?;
}
// This isn't really needed when the above command works as expected via AHK
match WindowsApi::set_focus(self.hwnd()) {
Ok(()) => {}
Err(error) => {
tracing::error!(
"could not set focus, but continuing execution of focus(): {}",
error
);
}
};
match WindowsApi::attach_thread_input(current_thread_id, window_thread_id, false) {
Ok(()) => {}
Err(error) => {
tracing::error!(
"could not detach from window thread input processing mechanism, but continuing execution of focus(): {}",
error
);
}
};
Ok(())
}
@@ -370,18 +317,27 @@ impl Window {
self.update_style(&style)
}
#[tracing::instrument(fields(exe, title))]
pub fn should_manage(self, event: Option<WindowManagerEvent>) -> Result<bool> {
if let Some(WindowManagerEvent::DisplayChange(_)) = event {
return Ok(true);
#[tracing::instrument(fields(exe, title), skip(debug))]
pub fn should_manage(
self,
event: Option<WindowManagerEvent>,
debug: &mut RuleDebug,
) -> Result<bool> {
if !self.is_window() {
return Ok(false);
}
#[allow(clippy::question_mark)]
debug.is_window = true;
if self.title().is_err() {
return Ok(false);
}
let is_cloaked = self.is_cloaked()?;
debug.has_title = true;
let is_cloaked = self.is_cloaked().unwrap_or_default();
debug.is_cloaked = is_cloaked;
let mut allow_cloaked = false;
@@ -394,13 +350,28 @@ impl Window {
}
}
debug.allow_cloaked = allow_cloaked;
match (allow_cloaked, is_cloaked) {
// If allowing cloaked windows, we don't need to check the cloaked status
(true, _) |
// If not allowing cloaked windows, we need to ensure the window is not cloaked
(false, false) => {
if let (Ok(title), Ok(exe_name), Ok(class), Ok(path)) = (self.title(), self.exe(), self.class(), self.path()) {
return Ok(window_is_eligible(&title, &exe_name, &class, &path, &self.style()?, &self.ex_style()?, event));
debug.title = Some(title.clone());
debug.exe_name = Some(exe_name.clone());
debug.class = Some(class.clone());
debug.path = Some(path.clone());
// calls for styles can fail quite often for events with windows that aren't really "windows"
// since we have moved up calls of should_manage to the beginning of the process_event handler,
// we should handle failures here gracefully to be able to continue the execution of process_event
if let (Ok(style), Ok(ex_style)) = (&self.style(), &self.ex_style()) {
debug.window_style = Some(*style);
debug.extended_window_style = Some(*ex_style);
let eligible = window_is_eligible(&title, &exe_name, &class, &path, style, ex_style, event, debug);
debug.should_manage = eligible;
return Ok(eligible);
}
}
}
_ => {}
@@ -410,6 +381,28 @@ impl Window {
}
}
#[derive(Debug, Default, Serialize, Deserialize)]
pub struct RuleDebug {
pub should_manage: bool,
pub is_window: bool,
pub has_title: bool,
pub is_cloaked: bool,
pub allow_cloaked: bool,
pub window_style: Option<WindowStyle>,
pub extended_window_style: Option<ExtendedWindowStyle>,
pub title: Option<String>,
pub exe_name: Option<String>,
pub class: Option<String>,
pub path: Option<String>,
pub matches_permaignore_class: Option<String>,
pub matches_float_identifier: Option<MatchingRule>,
pub matches_managed_override: Option<MatchingRule>,
pub matches_layered_whitelist: Option<MatchingRule>,
pub matches_wsl2_gui: Option<String>,
pub matches_no_titlebar: Option<String>,
}
#[allow(clippy::too_many_arguments)]
fn window_is_eligible(
title: &String,
exe_name: &String,
@@ -418,10 +411,12 @@ fn window_is_eligible(
style: &WindowStyle,
ex_style: &ExtendedWindowStyle,
event: Option<WindowManagerEvent>,
debug: &mut RuleDebug,
) -> bool {
{
let permaignore_classes = PERMAIGNORE_CLASSES.lock();
if permaignore_classes.contains(class) {
debug.matches_permaignore_class = Some(class.clone());
return false;
}
}
@@ -429,45 +424,65 @@ fn window_is_eligible(
let regex_identifiers = REGEX_IDENTIFIERS.lock();
let float_identifiers = FLOAT_IDENTIFIERS.lock();
let should_float = should_act(
let should_float = if let Some(rule) = should_act(
title,
exe_name,
class,
path,
&float_identifiers,
&regex_identifiers,
);
) {
debug.matches_float_identifier = Some(rule);
true
} else {
false
};
let manage_identifiers = MANAGE_IDENTIFIERS.lock();
let managed_override = should_act(
let managed_override = if let Some(rule) = should_act(
title,
exe_name,
class,
path,
&manage_identifiers,
&regex_identifiers,
);
) {
debug.matches_managed_override = Some(rule);
true
} else {
false
};
if should_float && !managed_override {
return false;
}
let layered_whitelist = LAYERED_WHITELIST.lock();
let allow_layered = should_act(
let allow_layered = if let Some(rule) = should_act(
title,
exe_name,
class,
path,
&layered_whitelist,
&regex_identifiers,
);
) {
debug.matches_layered_whitelist = Some(rule);
true
} else {
false
};
// TODO: might need this for transparency
// let allow_layered = true;
let allow_wsl2_gui = {
let wsl2_ui_processes = WSL2_UI_PROCESSES.lock();
wsl2_ui_processes.contains(exe_name)
let allow = wsl2_ui_processes.contains(exe_name);
if allow {
debug.matches_wsl2_gui = Some(exe_name.clone())
}
allow
};
let allow_titlebar_removed = {
@@ -509,10 +524,10 @@ pub fn should_act(
path: &str,
identifiers: &[MatchingRule],
regex_identifiers: &HashMap<String, Regex>,
) -> bool {
let mut should_act = false;
for identifier in identifiers {
match identifier {
) -> Option<MatchingRule> {
let mut matching_rule = None;
for rule in identifiers {
match rule {
MatchingRule::Simple(identifier) => {
if should_act_individual(
title,
@@ -522,7 +537,7 @@ pub fn should_act(
identifier,
regex_identifiers,
) {
should_act = true
matching_rule = Some(rule.clone());
};
}
MatchingRule::Composite(identifiers) => {
@@ -539,13 +554,13 @@ pub fn should_act(
}
if composite_results.iter().all(|&x| x) {
should_act = true;
matching_rule = Some(rule.clone());
}
}
}
}
should_act
matching_rule
}
pub fn should_act_individual(

View File

@@ -16,6 +16,7 @@ use hotwatch::notify::ErrorKind as NotifyErrorKind;
use hotwatch::EventKind;
use hotwatch::Hotwatch;
use parking_lot::Mutex;
use regex::Regex;
use schemars::JsonSchema;
use serde::Deserialize;
use serde::Serialize;
@@ -23,11 +24,13 @@ use uds_windows::UnixListener;
use komorebi_core::config_generation::MatchingRule;
use komorebi_core::custom_layout::CustomLayout;
use komorebi_core::ActiveWindowBorderStyle;
use komorebi_core::Arrangement;
use komorebi_core::Axis;
use komorebi_core::CycleDirection;
use komorebi_core::DefaultLayout;
use komorebi_core::FocusFollowsMouseImplementation;
use komorebi_core::HidingBehaviour;
use komorebi_core::Layout;
use komorebi_core::MoveBehaviour;
use komorebi_core::OperationBehaviour;
@@ -48,10 +51,23 @@ use crate::window_manager_event::WindowManagerEvent;
use crate::windows_api::WindowsApi;
use crate::winevent_listener;
use crate::workspace::Workspace;
use crate::ActiveWindowBorderColours;
use crate::Colour;
use crate::Rgb;
use crate::WorkspaceRule;
use crate::ACTIVE_WINDOW_BORDER_STYLE;
use crate::BORDER_COLOUR_MONOCLE;
use crate::BORDER_COLOUR_SINGLE;
use crate::BORDER_COLOUR_STACK;
use crate::BORDER_ENABLED;
use crate::BORDER_HWND;
use crate::BORDER_OFFSET;
use crate::BORDER_WIDTH;
use crate::CUSTOM_FFM;
use crate::DATA_DIR;
use crate::DISPLAY_INDEX_PREFERENCES;
use crate::FLOAT_IDENTIFIERS;
use crate::HIDING_BEHAVIOUR;
use crate::HOME_DIR;
use crate::LAYERED_WHITELIST;
use crate::MANAGE_IDENTIFIERS;
@@ -59,8 +75,15 @@ use crate::MONITOR_INDEX_PREFERENCES;
use crate::NO_TITLEBAR;
use crate::OBJECT_NAME_CHANGE_ON_LAUNCH;
use crate::REMOVE_TITLEBARS;
use crate::STACKBAR_FOCUSED_TEXT_COLOUR;
use crate::STACKBAR_MODE;
use crate::STACKBAR_TAB_BACKGROUND_COLOUR;
use crate::STACKBAR_TAB_HEIGHT;
use crate::STACKBAR_TAB_WIDTH;
use crate::STACKBAR_UNFOCUSED_TEXT_COLOUR;
use crate::TRAY_AND_MULTI_WINDOW_IDENTIFIERS;
use crate::WORKSPACE_RULES;
use komorebi_core::StackbarMode;
#[derive(Debug)]
pub struct WindowManager {
@@ -91,10 +114,27 @@ pub struct State {
pub resize_delta: i32,
pub new_window_behaviour: WindowContainerBehaviour,
pub cross_monitor_move_behaviour: MoveBehaviour,
pub unmanaged_window_operation_behaviour: OperationBehaviour,
pub work_area_offset: Option<Rect>,
pub focus_follows_mouse: Option<FocusFollowsMouseImplementation>,
pub mouse_follows_focus: bool,
pub has_pending_raise_op: bool,
}
#[allow(clippy::struct_excessive_bools)]
#[derive(Debug, Serialize, Deserialize, JsonSchema)]
pub struct GlobalState {
pub active_window_border_enabled: bool,
pub active_window_border_colours: ActiveWindowBorderColours,
pub active_window_border_style: ActiveWindowBorderStyle,
pub border_offset: i32,
pub border_width: i32,
pub stackbar_mode: StackbarMode,
pub stackbar_focused_text_colour: Colour,
pub stackbar_unfocused_text_colour: Colour,
pub stackbar_tab_background_colour: Colour,
pub stackbar_tab_width: i32,
pub stackbar_height: i32,
pub remove_titlebars: bool,
pub float_identifiers: Vec<MatchingRule>,
pub manage_identifiers: Vec<MatchingRule>,
@@ -103,6 +143,52 @@ pub struct State {
pub name_change_on_launch_identifiers: Vec<MatchingRule>,
pub monitor_index_preferences: HashMap<usize, Rect>,
pub display_index_preferences: HashMap<usize, String>,
pub workspace_rules: HashMap<String, WorkspaceRule>,
pub window_hiding_behaviour: HidingBehaviour,
pub configuration_dir: PathBuf,
pub data_dir: PathBuf,
pub custom_ffm: bool,
}
impl Default for GlobalState {
fn default() -> Self {
Self {
active_window_border_enabled: BORDER_ENABLED.load(Ordering::SeqCst),
active_window_border_colours: ActiveWindowBorderColours {
single: Colour::Rgb(Rgb::from(BORDER_COLOUR_SINGLE.load(Ordering::SeqCst))),
stack: Colour::Rgb(Rgb::from(BORDER_COLOUR_STACK.load(Ordering::SeqCst))),
monocle: Colour::Rgb(Rgb::from(BORDER_COLOUR_MONOCLE.load(Ordering::SeqCst))),
},
active_window_border_style: *ACTIVE_WINDOW_BORDER_STYLE.lock(),
border_offset: BORDER_OFFSET.load(Ordering::SeqCst),
border_width: BORDER_WIDTH.load(Ordering::SeqCst),
stackbar_mode: *STACKBAR_MODE.lock(),
stackbar_focused_text_colour: Colour::Rgb(Rgb::from(
STACKBAR_FOCUSED_TEXT_COLOUR.load(Ordering::SeqCst),
)),
stackbar_unfocused_text_colour: Colour::Rgb(Rgb::from(
STACKBAR_UNFOCUSED_TEXT_COLOUR.load(Ordering::SeqCst),
)),
stackbar_tab_background_colour: Colour::Rgb(Rgb::from(
STACKBAR_TAB_BACKGROUND_COLOUR.load(Ordering::SeqCst),
)),
stackbar_tab_width: STACKBAR_TAB_WIDTH.load(Ordering::SeqCst),
stackbar_height: STACKBAR_TAB_HEIGHT.load(Ordering::SeqCst),
remove_titlebars: REMOVE_TITLEBARS.load(Ordering::SeqCst),
float_identifiers: FLOAT_IDENTIFIERS.lock().clone(),
manage_identifiers: MANAGE_IDENTIFIERS.lock().clone(),
layered_whitelist: LAYERED_WHITELIST.lock().clone(),
tray_and_multi_window_identifiers: TRAY_AND_MULTI_WINDOW_IDENTIFIERS.lock().clone(),
name_change_on_launch_identifiers: OBJECT_NAME_CHANGE_ON_LAUNCH.lock().clone(),
monitor_index_preferences: MONITOR_INDEX_PREFERENCES.lock().clone(),
display_index_preferences: DISPLAY_INDEX_PREFERENCES.lock().clone(),
workspace_rules: WORKSPACE_RULES.lock().clone(),
window_hiding_behaviour: *HIDING_BEHAVIOUR.lock(),
configuration_dir: HOME_DIR.clone(),
data_dir: DATA_DIR.clone(),
custom_ffm: CUSTOM_FFM.load(Ordering::SeqCst),
}
}
}
impl AsRef<Self> for WindowManager {
@@ -123,14 +209,7 @@ impl From<&WindowManager> for State {
focus_follows_mouse: wm.focus_follows_mouse,
mouse_follows_focus: wm.mouse_follows_focus,
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_whitelist: LAYERED_WHITELIST.lock().clone(),
tray_and_multi_window_identifiers: TRAY_AND_MULTI_WINDOW_IDENTIFIERS.lock().clone(),
name_change_on_launch_identifiers: OBJECT_NAME_CHANGE_ON_LAUNCH.lock().clone(),
monitor_index_preferences: MONITOR_INDEX_PREFERENCES.lock().clone(),
display_index_preferences: DISPLAY_INDEX_PREFERENCES.lock().clone(),
unmanaged_window_operation_behaviour: wm.unmanaged_window_operation_behaviour,
}
}
}
@@ -476,7 +555,7 @@ impl WindowManager {
}
#[allow(clippy::too_many_arguments)]
#[tracing::instrument(skip(self))]
#[tracing::instrument(skip(self), level = "debug")]
fn add_window_handle_to_move_based_on_workspace_rule(
&self,
window_title: &String,
@@ -504,7 +583,7 @@ impl WindowManager {
});
}
#[tracing::instrument(skip(self))]
#[tracing::instrument(skip(self), level = "debug")]
pub fn enforce_workspace_rules(&mut self) -> Result<()> {
let mut to_move = vec![];
@@ -522,11 +601,36 @@ impl WindowManager {
// And all the visible windows (at the top of a container)
for window in workspace.visible_windows().into_iter().flatten() {
let mut already_moved_window_handles = self.already_moved_window_handles.lock();
let exe_name = window.exe()?;
let title = window.title()?;
let class = window.class()?;
let mut found_workspace_rule = workspace_rules.get(&window.exe()?);
let mut found_workspace_rule = workspace_rules.get(&exe_name);
if found_workspace_rule.is_none() {
found_workspace_rule = workspace_rules.get(&window.title()?);
found_workspace_rule = workspace_rules.get(&title);
}
if found_workspace_rule.is_none() {
found_workspace_rule = workspace_rules.get(&class);
}
if found_workspace_rule.is_none() {
for (k, v) in workspace_rules.iter() {
if let Ok(re) = Regex::new(k) {
if re.is_match(&exe_name) {
found_workspace_rule = Some(v);
}
if re.is_match(&title) {
found_workspace_rule = Some(v);
}
if re.is_match(&class) {
found_workspace_rule = Some(v);
}
}
}
}
// If the executable names or titles of any of those windows are in our rules map
@@ -617,7 +721,7 @@ impl WindowManager {
// Only re-tile the focused workspace if we need to
if should_update_focused_workspace {
self.update_focused_workspace(false)?;
self.update_focused_workspace(false, false)?;
}
Ok(())
@@ -796,7 +900,11 @@ impl WindowManager {
}
#[tracing::instrument(skip(self))]
pub fn update_focused_workspace(&mut self, follow_focus: bool) -> Result<()> {
pub fn update_focused_workspace(
&mut self,
follow_focus: bool,
trigger_focus: bool,
) -> Result<()> {
tracing::info!("updating");
let offset = self.work_area_offset;
@@ -807,13 +915,19 @@ impl WindowManager {
if follow_focus {
if let Some(window) = self.focused_workspace()?.maximized_window() {
window.focus(self.mouse_follows_focus)?;
} else if let Some(container) = self.focused_workspace()?.monocle_container() {
if let Some(window) = container.focused_window() {
if trigger_focus {
window.focus(self.mouse_follows_focus)?;
}
} else if let Some(container) = self.focused_workspace()?.monocle_container() {
if let Some(window) = container.focused_window() {
if trigger_focus {
window.focus(self.mouse_follows_focus)?;
}
}
} else if let Ok(window) = self.focused_window_mut() {
window.focus(self.mouse_follows_focus)?;
if trigger_focus {
window.focus(self.mouse_follows_focus)?;
}
} else {
let desktop_window = Window {
hwnd: WindowsApi::desktop_window()?,
@@ -822,10 +936,7 @@ impl WindowManager {
let rect = self.focused_monitor_size()?;
WindowsApi::center_cursor_in_rect(&rect)?;
// 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)"
match WindowsApi::set_foreground_window(desktop_window.hwnd()) {
match WindowsApi::raise_and_focus_window(desktop_window.hwnd()) {
Ok(()) => {}
Err(error) => {
tracing::warn!("{} {}:{}", error, file!(), line!());
@@ -844,7 +955,9 @@ impl WindowManager {
&& self.focused_workspace()?.monocle_container().is_none()
{
if let Ok(window) = self.focused_window_mut() {
window.focus(self.mouse_follows_focus)?;
if trigger_focus {
window.focus(self.mouse_follows_focus)?;
}
}
}
}
@@ -854,7 +967,9 @@ impl WindowManager {
if !follow_focus {
if let Some(window) = self.focused_workspace()?.maximized_window() {
window.restore();
window.focus(self.mouse_follows_focus)?;
if trigger_focus {
window.focus(self.mouse_follows_focus)?;
}
}
}
@@ -937,7 +1052,7 @@ impl WindowManager {
workspace.resize_dimensions_mut()[focused_idx] = resize;
return if update {
self.update_focused_workspace(false)
self.update_focused_workspace(false, false)
} else {
Ok(())
};
@@ -1073,7 +1188,7 @@ impl WindowManager {
self.swap_monitor_workspaces(focused_monitor_idx, idx)?;
self.update_focused_workspace(mouse_follows_focus)
self.update_focused_workspace(mouse_follows_focus, true)
}
#[tracing::instrument(skip(self))]
@@ -1120,7 +1235,7 @@ impl WindowManager {
self.focus_monitor(monitor_idx)?;
}
self.update_focused_workspace(self.mouse_follows_focus)
self.update_focused_workspace(self.mouse_follows_focus, true)
}
#[tracing::instrument(skip(self))]
@@ -1137,7 +1252,7 @@ impl WindowManager {
monitor.move_container_to_workspace(idx, follow)?;
monitor.load_focused_workspace(mouse_follows_focus)?;
self.update_focused_workspace(mouse_follows_focus)
self.update_focused_workspace(mouse_follows_focus, true)
}
pub fn remove_focused_workspace(&mut self) -> Option<Workspace> {
@@ -1166,7 +1281,7 @@ impl WindowManager {
}
self.focus_monitor(idx)?;
self.update_focused_workspace(mouse_follows_focus)
self.update_focused_workspace(mouse_follows_focus, true)
}
#[tracing::instrument(skip(self))]
@@ -1327,7 +1442,7 @@ impl WindowManager {
.id();
if !WindowsApi::monitors_have_same_dpi(a, b)? {
self.update_focused_workspace(self.mouse_follows_focus)?;
self.update_focused_workspace(self.mouse_follows_focus, true)?;
}
}
Some(new_idx) => {
@@ -1337,7 +1452,7 @@ impl WindowManager {
}
}
self.update_focused_workspace(self.mouse_follows_focus)
self.update_focused_workspace(self.mouse_follows_focus, true)
}
#[tracing::instrument(skip(self))]
@@ -1395,7 +1510,7 @@ impl WindowManager {
workspace.swap_containers(current_idx, new_idx);
workspace.focus_container(new_idx);
self.update_focused_workspace(self.mouse_follows_focus)
self.update_focused_workspace(self.mouse_follows_focus, true)
}
#[tracing::instrument(skip(self))]
@@ -1419,7 +1534,7 @@ impl WindowManager {
container.focus_window(next_idx);
container.load_focused_window();
self.update_focused_workspace(self.mouse_follows_focus)
self.update_focused_workspace(self.mouse_follows_focus, true)
}
#[tracing::instrument(skip(self))]
@@ -1456,7 +1571,7 @@ impl WindowManager {
};
workspace.move_window_to_container(adjusted_new_index)?;
self.update_focused_workspace(self.mouse_follows_focus)?;
self.update_focused_workspace(self.mouse_follows_focus, false)?;
}
Ok(())
@@ -1476,7 +1591,7 @@ impl WindowManager {
tracing::info!("promoting container");
workspace.promote_container()?;
self.update_focused_workspace(self.mouse_follows_focus)
self.update_focused_workspace(self.mouse_follows_focus, true)
}
#[tracing::instrument(skip(self))]
@@ -1499,7 +1614,7 @@ impl WindowManager {
};
workspace.focus_container(target_idx);
self.update_focused_workspace(self.mouse_follows_focus)
self.update_focused_workspace(self.mouse_follows_focus, true)
}
#[tracing::instrument(skip(self))]
@@ -1515,14 +1630,14 @@ impl WindowManager {
let workspace = self.focused_workspace_mut()?;
workspace.new_container_for_focused_window()?;
self.update_focused_workspace(self.mouse_follows_focus)
self.update_focused_workspace(self.mouse_follows_focus, false)
}
#[tracing::instrument(skip(self))]
pub fn toggle_tiling(&mut self) -> Result<()> {
let workspace = self.focused_workspace_mut()?;
workspace.set_tile(!*workspace.tile());
self.update_focused_workspace(false)
self.update_focused_workspace(false, false)
}
#[tracing::instrument(skip(self))]
@@ -1544,7 +1659,7 @@ impl WindowManager {
self.float_window()?;
}
self.update_focused_workspace(is_floating_window)
self.update_focused_workspace(is_floating_window, true)
}
#[tracing::instrument(skip(self))]
@@ -1579,14 +1694,25 @@ impl WindowManager {
pub fn toggle_monocle(&mut self) -> Result<()> {
self.handle_unmanaged_window_behaviour()?;
let workspace = self.focused_workspace_mut()?;
let workspace = self.focused_workspace()?;
match workspace.monocle_container() {
None => self.monocle_on()?,
Some(_) => self.monocle_off()?,
}
self.update_focused_workspace(true)
self.update_focused_workspace(true, true)?;
// TODO: fix this ugly hack to restore stackbar after monocle is toggled off
let workspace = self.focused_workspace()?;
if workspace.monocle_container().is_none() {
if let Some(container) = workspace.focused_container() {
if container.stackbar().is_some() {
self.retile_all(true)?;
};
}
};
Ok(())
}
#[tracing::instrument(skip(self))]
@@ -1616,7 +1742,7 @@ impl WindowManager {
Some(_) => self.unmaximize_window()?,
}
self.update_focused_workspace(true)
self.update_focused_workspace(true, false)
}
#[tracing::instrument(skip(self))]
@@ -1675,7 +1801,7 @@ impl WindowManager {
}
}
self.update_focused_workspace(false)
self.update_focused_workspace(false, false)
}
#[tracing::instrument(skip(self))]
@@ -1700,7 +1826,7 @@ impl WindowManager {
}
workspace.set_layout(Layout::Default(layout));
self.update_focused_workspace(self.mouse_follows_focus)
self.update_focused_workspace(self.mouse_follows_focus, false)
}
#[tracing::instrument(skip(self))]
@@ -1723,7 +1849,7 @@ impl WindowManager {
Layout::Custom(_) => {}
}
self.update_focused_workspace(self.mouse_follows_focus)
self.update_focused_workspace(self.mouse_follows_focus, false)
}
#[tracing::instrument(skip(self))]
@@ -1753,7 +1879,7 @@ impl WindowManager {
workspace.set_layout(Layout::Custom(layout));
workspace.set_layout_flip(None);
self.update_focused_workspace(self.mouse_follows_focus)
self.update_focused_workspace(self.mouse_follows_focus, false)
}
#[tracing::instrument(skip(self))]
@@ -1768,7 +1894,7 @@ impl WindowManager {
workspace.set_workspace_padding(Option::from(sizing.adjust_by(padding, adjustment)));
self.update_focused_workspace(false)
self.update_focused_workspace(false, false)
}
#[tracing::instrument(skip(self))]
@@ -1783,7 +1909,7 @@ impl WindowManager {
workspace.set_container_padding(Option::from(sizing.adjust_by(padding, adjustment)));
self.update_focused_workspace(false)
self.update_focused_workspace(false, false)
}
#[tracing::instrument(skip(self))]
@@ -1805,7 +1931,7 @@ impl WindowManager {
workspace.set_tile(tile);
self.update_focused_workspace(false)
self.update_focused_workspace(false, false)
}
#[tracing::instrument(skip(self))]
@@ -1849,7 +1975,7 @@ impl WindowManager {
workspace.update(&work_area, offset)?;
Ok(())
} else {
Ok(self.update_focused_workspace(false)?)
Ok(self.update_focused_workspace(false, false)?)
}
}
@@ -1899,7 +2025,7 @@ impl WindowManager {
workspace.update(&work_area, offset)?;
Ok(())
} else {
Ok(self.update_focused_workspace(false)?)
Ok(self.update_focused_workspace(false, false)?)
}
}
@@ -1940,7 +2066,7 @@ impl WindowManager {
workspace.update(&work_area, offset)?;
Ok(())
} else {
Ok(self.update_focused_workspace(false)?)
Ok(self.update_focused_workspace(false, false)?)
}
}
@@ -1981,7 +2107,7 @@ impl WindowManager {
workspace.update(&work_area, offset)?;
Ok(())
} else {
Ok(self.update_focused_workspace(false)?)
Ok(self.update_focused_workspace(false, false)?)
}
}
@@ -2026,7 +2152,7 @@ impl WindowManager {
workspace.update(&work_area, offset)?;
Ok(())
} else {
Ok(self.update_focused_workspace(false)?)
Ok(self.update_focused_workspace(false, false)?)
}
}
@@ -2093,7 +2219,7 @@ impl WindowManager {
workspace.set_workspace_padding(Option::from(size));
self.update_focused_workspace(false)
self.update_focused_workspace(false, false)
}
#[tracing::instrument(skip(self))]
@@ -2142,7 +2268,7 @@ impl WindowManager {
workspace.set_container_padding(Option::from(size));
self.update_focused_workspace(false)
self.update_focused_workspace(false, false)
}
pub fn focused_monitor_size(&self) -> Result<Rect> {
@@ -2246,7 +2372,7 @@ impl WindowManager {
monitor.focus_workspace(idx)?;
monitor.load_focused_workspace(mouse_follows_focus)?;
self.update_focused_workspace(false)
self.update_focused_workspace(false, true)
}
#[tracing::instrument(skip(self))]
@@ -2278,7 +2404,7 @@ impl WindowManager {
monitor.focus_workspace(monitor.new_workspace_idx())?;
monitor.load_focused_workspace(mouse_follows_focus)?;
self.update_focused_workspace(self.mouse_follows_focus)
self.update_focused_workspace(self.mouse_follows_focus, false)
}
pub fn focused_container(&self) -> Result<&Container> {

View File

@@ -153,7 +153,8 @@ impl WindowManagerEvent {
path,
&object_name_change_on_launch,
&regex_identifiers,
);
)
.is_some();
if should_trigger {
Option::from(Self::Show(winevent, window))

View File

@@ -1,6 +1,7 @@
use std::collections::VecDeque;
use std::convert::TryFrom;
use std::ffi::c_void;
use std::mem::size_of;
use std::sync::atomic::Ordering;
use color_eyre::eyre::anyhow;
@@ -47,9 +48,7 @@ use windows::Win32::Graphics::Gdi::MONITORINFOEXW;
use windows::Win32::Graphics::Gdi::MONITOR_DEFAULTTONEAREST;
use windows::Win32::System::LibraryLoader::GetModuleHandleW;
use windows::Win32::System::RemoteDesktop::ProcessIdToSessionId;
use windows::Win32::System::Threading::AttachThreadInput;
use windows::Win32::System::Threading::GetCurrentProcessId;
use windows::Win32::System::Threading::GetCurrentThreadId;
use windows::Win32::System::Threading::OpenProcess;
use windows::Win32::System::Threading::QueryFullProcessImageNameW;
use windows::Win32::System::Threading::PROCESS_ACCESS_RIGHTS;
@@ -61,7 +60,6 @@ use windows::Win32::UI::HiDpi::DPI_AWARENESS_CONTEXT_PER_MONITOR_AWARE_V2;
use windows::Win32::UI::HiDpi::MDT_EFFECTIVE_DPI;
use windows::Win32::UI::Input::KeyboardAndMouse::GetKeyState;
use windows::Win32::UI::Input::KeyboardAndMouse::SendInput;
use windows::Win32::UI::Input::KeyboardAndMouse::SetFocus;
use windows::Win32::UI::Input::KeyboardAndMouse::INPUT;
use windows::Win32::UI::Input::KeyboardAndMouse::INPUT_0;
use windows::Win32::UI::Input::KeyboardAndMouse::INPUT_MOUSE;
@@ -104,7 +102,6 @@ use windows::Win32::UI::WindowsAndMessaging::GWL_EXSTYLE;
use windows::Win32::UI::WindowsAndMessaging::GWL_STYLE;
use windows::Win32::UI::WindowsAndMessaging::GW_HWNDNEXT;
use windows::Win32::UI::WindowsAndMessaging::HWND_BOTTOM;
use windows::Win32::UI::WindowsAndMessaging::HWND_NOTOPMOST;
use windows::Win32::UI::WindowsAndMessaging::HWND_TOP;
use windows::Win32::UI::WindowsAndMessaging::LWA_ALPHA;
use windows::Win32::UI::WindowsAndMessaging::LWA_COLORKEY;
@@ -115,6 +112,9 @@ use windows::Win32::UI::WindowsAndMessaging::SPI_GETACTIVEWINDOWTRACKING;
use windows::Win32::UI::WindowsAndMessaging::SPI_GETFOREGROUNDLOCKTIMEOUT;
use windows::Win32::UI::WindowsAndMessaging::SPI_SETACTIVEWINDOWTRACKING;
use windows::Win32::UI::WindowsAndMessaging::SPI_SETFOREGROUNDLOCKTIMEOUT;
use windows::Win32::UI::WindowsAndMessaging::SWP_NOMOVE;
use windows::Win32::UI::WindowsAndMessaging::SWP_NOSIZE;
use windows::Win32::UI::WindowsAndMessaging::SWP_SHOWWINDOW;
use windows::Win32::UI::WindowsAndMessaging::SW_HIDE;
use windows::Win32::UI::WindowsAndMessaging::SW_MAXIMIZE;
use windows::Win32::UI::WindowsAndMessaging::SW_MINIMIZE;
@@ -347,12 +347,19 @@ impl WindowsApi {
/// the layout to account for any window shadow borders (the window painted
/// region will match layout on completion).
pub fn position_window(hwnd: HWND, layout: &Rect, top: bool) -> Result<()> {
let flags = SetWindowPosition::NO_ACTIVATE
let mut flags = SetWindowPosition::NO_ACTIVATE
| SetWindowPosition::NO_SEND_CHANGING
| SetWindowPosition::NO_COPY_BITS
| SetWindowPosition::FRAME_CHANGED;
let shadow_rect = Self::shadow_rect(hwnd)?;
// If the request is to place the window on top, then HWND_TOP will take
// effect, otherwise pass NO_Z_ORDER that will cause set_window_pos to
// ignore the z-order paramter.
if !top {
flags |= SetWindowPosition::NO_Z_ORDER;
}
let shadow_rect = Self::shadow_rect(hwnd).unwrap_or_default();
let rect = Rect {
left: layout.left + shadow_rect.left,
top: layout.top + shadow_rect.top,
@@ -360,16 +367,29 @@ impl WindowsApi {
bottom: layout.bottom + shadow_rect.bottom,
};
let position = if top { HWND_TOP } else { HWND_NOTOPMOST };
Self::set_window_pos(hwnd, &rect, position, flags.bits())
// Note: earlier code had set HWND_TOPMOST here, but we should not do
// that. HWND_TOPMOST is a sticky z-order change, rather than a regular
// z-order reordering. Programs will use TOPMOST themselves to do things
// such as making sure that their tool windows or dialog pop-ups are
// above their main window. If any such windows are unmanaged, they must
// still remian topmost, so we set HWND_TOP here, which will cause the
// managed window to come to the front, but if the managed window has a
// child that is TOPMOST it will still be rendered above, in the proper
// order expected by the application. It's also important to understand
// that TOPMOST is somewhat viral, in that when you set a window to
// TOPMOST all of its owned windows are also made TOPMOST.
// See https://learn.microsoft.com/en-us/windows/win32/api/winuser/nf-winuser-setwindowpos#remarks
Self::set_window_pos(hwnd, &rect, HWND_TOP, flags.bits())
}
pub fn bring_window_to_top(hwnd: HWND) -> Result<()> {
unsafe { BringWindowToTop(hwnd) }.process()
}
// Raise the window to the top of the Z order, but do not activate or focus
// it. Use raise_and_focus_window to activate and focus a window.
pub fn raise_window(hwnd: HWND) -> Result<()> {
let flags = SetWindowPosition::NO_MOVE;
let flags = SetWindowPosition::NO_MOVE | SetWindowPosition::NO_ACTIVATE;
let position = HWND_TOP;
Self::set_window_pos(hwnd, &Rect::default(), position, flags.bits())
@@ -394,8 +414,7 @@ impl WindowsApi {
// top of other pop-up dialogs such as a file picker dialog from
// Firefox. When adjusting this in the future, it's important to check
// those dialog cases.
let position = HWND_NOTOPMOST;
Self::set_window_pos(hwnd, layout, position, flags.bits())
Self::set_window_pos(hwnd, layout, HWND_TOP, flags.bits())
}
pub fn hide_border_window(hwnd: HWND) -> Result<()> {
@@ -462,8 +481,31 @@ impl WindowsApi {
unsafe { GetForegroundWindow() }.process()
}
pub fn set_foreground_window(hwnd: HWND) -> Result<()> {
unsafe { SetForegroundWindow(hwnd) }.ok().process()
pub fn raise_and_focus_window(hwnd: HWND) -> Result<()> {
let event = [INPUT {
r#type: INPUT_MOUSE,
..Default::default()
}];
unsafe {
// Send an input event to our own process first so that we pass the
// foreground lock check
SendInput(&event, size_of::<INPUT>() as i32);
// Error ignored, as the operation is not always necessary.
let _ = SetWindowPos(
hwnd,
HWND_TOP,
0,
0,
0,
0,
SWP_NOMOVE | SWP_NOSIZE | SWP_SHOWWINDOW,
)
.process();
SetForegroundWindow(hwnd)
}
.ok()
.process()
}
#[allow(dead_code)]
@@ -583,10 +625,6 @@ impl WindowsApi {
(process_id, thread_id)
}
pub fn current_thread_id() -> u32 {
unsafe { GetCurrentThreadId() }
}
pub fn current_process_id() -> u32 {
unsafe { GetCurrentProcessId() }
}
@@ -604,16 +642,6 @@ impl WindowsApi {
}
}
pub fn attach_thread_input(thread_id: u32, target_thread_id: u32, attach: bool) -> Result<()> {
unsafe { AttachThreadInput(thread_id, target_thread_id, attach) }
.ok()
.process()
}
pub fn set_focus(hwnd: HWND) -> Result<()> {
unsafe { SetFocus(hwnd) }.process().map(|_| ())
}
#[allow(dead_code)]
fn set_window_long_ptr_w(
hwnd: HWND,

View File

@@ -36,12 +36,12 @@ use windows::Win32::UI::WindowsAndMessaging::WM_SETTINGCHANGE;
use crate::container::Container;
use crate::monitor::Monitor;
use crate::ring::Ring;
use crate::window::RuleDebug;
use crate::window::Window;
use crate::window_manager_event::WindowManagerEvent;
use crate::windows_api::WindowsApi;
use crate::winevent::WinEvent;
use crate::winevent_listener;
use crate::ActiveWindowBorderStyle;
use crate::ACTIVE_WINDOW_BORDER_STYLE;
use crate::BORDER_COLOUR_CURRENT;
use crate::BORDER_RECT;
@@ -50,6 +50,7 @@ use crate::DISPLAY_INDEX_PREFERENCES;
use crate::MONITOR_INDEX_PREFERENCES;
use crate::TRANSPARENCY_COLOUR;
use crate::WINDOWS_11;
use komorebi_core::ActiveWindowBorderStyle;
pub extern "system" fn valid_display_monitors(
hmonitor: HMONITOR,
@@ -157,7 +158,7 @@ pub extern "system" fn enum_window(hwnd: HWND, lparam: LPARAM) -> BOOL {
if is_visible && is_window && !is_minimized {
let window = Window { hwnd: hwnd.0 };
if let Ok(should_manage) = window.should_manage(None) {
if let Ok(should_manage) = window.should_manage(None, &mut RuleDebug::default()) {
if should_manage {
if is_maximized {
WindowsApi::restore_window(hwnd);
@@ -198,13 +199,9 @@ pub extern "system" fn win_event_hook(
Some(event) => event,
};
if let Ok(should_manage) = window.should_manage(Option::from(event_type)) {
if should_manage {
winevent_listener::event_tx()
.send(event_type)
.expect("could not send message on winevent_listener::event_tx");
}
}
winevent_listener::event_tx()
.send(event_type)
.expect("could not send message on winevent_listener::event_tx");
}
pub extern "system" fn border_window(

View File

@@ -299,8 +299,10 @@ impl Workspace {
let containers = self.containers_mut();
for (i, container) in containers.iter_mut().enumerate() {
container.renew_stackbar();
let container_windows = container.windows().clone();
let container_topbar = container.stackbar().clone();
let container_stackbar = container.stackbar().clone();
if let (Some(window), Some(layout)) =
(container.focused_window_mut(), layouts.get(i))
@@ -326,18 +328,21 @@ impl Workspace {
rect.add_padding(width);
}
if let Some(stackbar) = container_topbar {
stackbar.set_position(
&stackbar.get_position_from_container_layout(layout),
false,
)?;
if let Some(stackbar) = container_stackbar {
if stackbar
.set_position(
&stackbar.get_position_from_container_layout(layout),
false,
)
.is_ok()
{
stackbar.update(&container_windows, focused_hwnd)?;
let tab_height = STACKBAR_TAB_HEIGHT.load(Ordering::SeqCst);
let total_height = tab_height + container_padding;
stackbar.update(&container_windows, focused_hwnd)?;
let tab_height = STACKBAR_TAB_HEIGHT.load(Ordering::SeqCst);
let total_height = tab_height + container_padding;
rect.top += total_height;
rect.bottom -= total_height;
rect.top += total_height;
rect.bottom -= total_height;
}
}
window.set_position(&rect, false)?;
@@ -357,6 +362,33 @@ impl Workspace {
Ok(())
}
// focus_changed performs updates in response to the fact that a focus
// change event has occurred. The focus change is assumed to be valid, and
// should not result in a new focus change - the intent here is to update
// focus-reactive elements, such as the stackbar.
pub fn focus_changed(&mut self, hwnd: isize) -> Result<()> {
if !self.tile() {
return Ok(());
}
let containers = self.containers_mut();
for container in containers.iter_mut() {
let container_windows = container.windows().clone();
let container_topbar = container.stackbar().clone();
if let Some(idx) = container.idx_for_window(hwnd) {
container.focus_window(idx);
container.restore();
}
if let Some(stackbar) = container_topbar {
stackbar.update(&container_windows, hwnd)?;
}
}
Ok(())
}
pub fn reap_orphans(&mut self) -> Result<(usize, usize)> {
let mut hwnds = vec![];
let mut floating_hwnds = vec![];
@@ -995,7 +1027,7 @@ impl Workspace {
.ok_or_else(|| anyhow!("there is no monocle container"))?;
let container = container.clone();
if restore_idx > self.containers().len() - 1 {
if restore_idx >= self.containers().len() {
self.containers_mut()
.resize(restore_idx, Container::default());
}

View File

@@ -1,6 +1,6 @@
[package]
name = "komorebic-no-console"
version = "0.1.23"
version = "0.1.25-dev.0"
authors = ["Jade Iqbal <jadeiqbal@fastmail.com>"]
description = "The command-line interface (without a console) for Komorebi, a tiling window manager for Windows"
categories = ["cli", "tiling-window-manager", "windows"]

View File

@@ -1,4 +1,4 @@
; Generated by komorebic.exe
#Requires AutoHotkey v2.0.2
Start(ffm, await_configuration, tcp_port) {
RunWait("komorebic.exe start " ffm " --await-configuration " await_configuration " --tcp-port " tcp_port, , "Hide")

View File

@@ -1,6 +1,6 @@
[package]
name = "komorebic"
version = "0.1.23"
version = "0.1.25-dev.0"
authors = ["Jade Iqbal <jadeiqbal@fastmail.com>"]
description = "The command-line interface for Komorebi, a tiling window manager for Windows"
categories = ["cli", "tiling-window-manager", "windows"]
@@ -13,6 +13,7 @@ edition = "2021"
[dependencies]
derive-ahk = { path = "../derive-ahk" }
komorebi-core = { path = "../komorebi-core" }
komorebi-client = { path = "../komorebi-client" }
clap = { version = "4", features = ["derive", "wrap_help"] }
color-eyre = { workspace = true }
@@ -28,7 +29,7 @@ reqwest = { version = "0.12", features = ["blocking"] }
serde = { version = "1", features = ["derive"] }
serde_json = { workspace = true }
serde_yaml = "0.9"
sysinfo = "0.30"
sysinfo = { workspace = true }
thiserror = "1"
uds_windows = "1"
which = "6"

View File

@@ -478,6 +478,14 @@ pub struct SendToMonitorWorkspace {
target_workspace: usize,
}
#[derive(Parser, AhkFunction)]
pub struct MoveToMonitorWorkspace {
/// Target monitor index (zero-indexed)
target_monitor: usize,
/// Workspace index on the target monitor (zero-indexed)
target_workspace: usize,
}
macro_rules! gen_focused_workspace_padding_subcommand_args {
// SubCommand Pattern
( $( $name:ident ),+ $(,)? ) => {
@@ -815,6 +823,8 @@ enum SubCommand {
Whkdrc,
/// Show a JSON representation of the current window manager state
State,
/// Show a JSON representation of the current global state
GlobalState,
/// Show a JSON representation of visible windows
VisibleWindows,
/// Query the current window manager state
@@ -916,6 +926,9 @@ enum SubCommand {
/// Send the focused window to the specified monitor workspace
#[clap(arg_required_else_help = true)]
SendToMonitorWorkspace(SendToMonitorWorkspace),
/// Move the focused window to the specified monitor workspace
#[clap(arg_required_else_help = true)]
MoveToMonitorWorkspace(MoveToMonitorWorkspace),
/// Focus the specified monitor
#[clap(arg_required_else_help = true)]
FocusMonitor(FocusMonitor),
@@ -1389,6 +1402,11 @@ fn main() -> Result<()> {
}
}
// Check that this file adheres to the schema static config schema as the last step,
// so that more basic errors above can be shown to the error before schema-specific
// errors
let _ = serde_json::from_str::<komorebi_client::StaticConfig>(&config_source)?;
if config_whkd.exists() {
println!("Found {}; key bindings will be loaded from here when whkd is started, and you can start it automatically using the --whkd flag\n", config_whkd.to_string_lossy());
} else {
@@ -1543,6 +1561,15 @@ fn main() -> Result<()> {
.as_bytes()?,
)?;
}
SubCommand::MoveToMonitorWorkspace(arg) => {
send_message(
&SocketMessage::MoveContainerToMonitorWorkspaceNumber(
arg.target_monitor,
arg.target_workspace,
)
.as_bytes()?,
)?;
}
SubCommand::MoveWorkspaceToMonitor(arg) => {
send_message(&SocketMessage::MoveWorkspaceToMonitorNumber(arg.target).as_bytes()?)?;
}
@@ -1773,7 +1800,7 @@ fn main() -> Result<()> {
};
let mut flags = vec![];
if let Some(config) = arg.config {
if let Some(config) = &arg.config {
let path = resolve_home_path(config)?;
if !path.is_file() {
bail!("could not find file: {}", path.display());
@@ -1808,9 +1835,10 @@ fn main() -> Result<()> {
)
};
let mut attempts = 0;
let mut running = false;
while !running {
while !running && attempts <= 2 {
match powershell_script::run(&script) {
Ok(_) => {
println!("{script}");
@@ -1831,9 +1859,27 @@ fn main() -> Result<()> {
running = true;
} else {
println!("komorebi.exe did not start... Trying again");
attempts += 1;
}
}
if !running {
println!("\nRunning komorebi.exe directly for detailed error output\n");
if let Some(config) = arg.config {
let path = resolve_home_path(config)?;
if let Ok(output) = Command::new("komorebi.exe")
.arg(format!("'--config=\"{}\"'", path.display()))
.output()
{
println!("{}", String::from_utf8(output.stderr)?);
}
} else if let Ok(output) = Command::new("komorebi.exe").output() {
println!("{}", String::from_utf8(output.stderr)?);
}
return Ok(());
}
if arg.whkd {
let script = r"
if (!(Get-Process whkd -ErrorAction SilentlyContinue))
@@ -1895,6 +1941,34 @@ Stop-Process -Name:whkd -ErrorAction SilentlyContinue
}
send_message(&SocketMessage::Stop.as_bytes()?)?;
let mut system = sysinfo::System::new_all();
system.refresh_processes();
if system.processes_by_name("komorebi.exe").count() >= 1 {
println!("komorebi is still running, attempting to force-quit");
let script = r"
Stop-Process -Name:komorebi -ErrorAction SilentlyContinue
";
match powershell_script::run(script) {
Ok(_) => {
println!("{script}");
let hwnd_json = DATA_DIR.join("komorebi.hwnd.json");
let file = File::open(hwnd_json)?;
let reader = BufReader::new(file);
let hwnds: Vec<isize> = serde_json::from_reader(reader)?;
for hwnd in hwnds {
restore_window(HWND(hwnd));
}
}
Err(error) => {
println!("Error: {error}");
}
}
}
}
SubCommand::FloatRule(arg) => {
send_message(&SocketMessage::FloatRule(arg.identifier, arg.id).as_bytes()?)?;
@@ -2025,6 +2099,9 @@ Stop-Process -Name:whkd -ErrorAction SilentlyContinue
SubCommand::State => {
print_query(&SocketMessage::State.as_bytes()?);
}
SubCommand::GlobalState => {
print_query(&SocketMessage::GlobalState.as_bytes()?);
}
SubCommand::VisibleWindows => {
print_query(&SocketMessage::VisibleWindows.as_bytes()?);
}
@@ -2139,10 +2216,10 @@ Stop-Process -Name:whkd -ErrorAction SilentlyContinue
)?;
}
SubCommand::ActiveWindowBorderWidth(arg) => {
send_message(&SocketMessage::ActiveWindowBorderWidth(arg.width).as_bytes()?)?;
send_message(&SocketMessage::BorderWidth(arg.width).as_bytes()?)?;
}
SubCommand::ActiveWindowBorderOffset(arg) => {
send_message(&SocketMessage::ActiveWindowBorderOffset(arg.offset).as_bytes()?)?;
send_message(&SocketMessage::BorderOffset(arg.offset).as_bytes()?)?;
}
SubCommand::ResizeDelta(arg) => {
send_message(&SocketMessage::ResizeDelta(arg.pixels).as_bytes()?)?;