mirror of
https://github.com/LGUG2Z/komorebi.git
synced 2026-01-16 06:16:43 +01:00
Compare commits
40 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
0ed732d085 | ||
|
|
fa11594103 | ||
|
|
88aaf1f368 | ||
|
|
c5bf62fac0 | ||
|
|
528a0bf9ff | ||
|
|
f73d1bf0da | ||
|
|
4510cccc3e | ||
|
|
b5035fbb5d | ||
|
|
1a703690c9 | ||
|
|
039f69b70b | ||
|
|
f7dccfe0f5 | ||
|
|
fc7198823b | ||
|
|
c3637665e9 | ||
|
|
45046a0e1e | ||
|
|
63dee21257 | ||
|
|
2675f747bb | ||
|
|
5b9730823e | ||
|
|
84da706d64 | ||
|
|
cf7a01695e | ||
|
|
510650cb94 | ||
|
|
6fe2290d74 | ||
|
|
647f5b7ded | ||
|
|
933122b073 | ||
|
|
9b62d5408d | ||
|
|
087b08612d | ||
|
|
c4be0636f7 | ||
|
|
6df91d7d40 | ||
|
|
b0737ff603 | ||
|
|
8ff0963203 | ||
|
|
ce326f31bc | ||
|
|
2050d9a3fa | ||
|
|
8e47bfdba6 | ||
|
|
9103ce2b2b | ||
|
|
7ba7067c96 | ||
|
|
a51f2387aa | ||
|
|
e4189c19ce | ||
|
|
1a2be3bc02 | ||
|
|
c37ba42825 | ||
|
|
d0c081feae | ||
|
|
0027c7d1de |
2
.github/workflows/windows.yaml
vendored
2
.github/workflows/windows.yaml
vendored
@@ -104,7 +104,7 @@ jobs:
|
||||
if: startsWith(github.ref, 'refs/tags/v')
|
||||
with:
|
||||
version: latest
|
||||
args: release --skip-validate --rm-dist --release-notes=CHANGELOG.md
|
||||
args: release --skip-validate --clean --release-notes=CHANGELOG.md
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
SCOOP_TOKEN: ${{ secrets.SCOOP_TOKEN }}
|
||||
|
||||
@@ -28,11 +28,8 @@ builds:
|
||||
- cp ".\target\x86_64-pc-windows-msvc\release\komorebic.exe" ".\dist\komorebic_windows_amd64_v1\komorebic.exe"
|
||||
|
||||
archives:
|
||||
- replacements:
|
||||
windows: pc-windows-msvc
|
||||
amd64: x86_64
|
||||
- name_template: "{{ .ProjectName }}-{{ .Version }}-x86_64-pc-windows-msvc"
|
||||
format: zip
|
||||
name_template: "{{ .ProjectName }}-{{ .Version }}-{{ .Arch }}-{{ .Os }}"
|
||||
files:
|
||||
- LICENSE
|
||||
- CHANGELOG.md
|
||||
|
||||
1019
Cargo.lock
generated
1019
Cargo.lock
generated
File diff suppressed because it is too large
Load Diff
25
Cargo.toml
25
Cargo.toml
@@ -6,3 +6,28 @@ members = [
|
||||
"komorebi-core",
|
||||
"komorebic",
|
||||
]
|
||||
|
||||
[workspace.dependencies]
|
||||
windows-interface = { version = "0.48" }
|
||||
windows-implement = { version = "0.48" }
|
||||
|
||||
[workspace.dependencies.windows]
|
||||
version = "0.48"
|
||||
features = [
|
||||
"implement",
|
||||
"Win32_System_Com",
|
||||
"Win32_UI_Shell_Common", # for IObjectArray
|
||||
"Win32_Foundation",
|
||||
"Win32_Graphics_Dwm",
|
||||
"Win32_Graphics_Gdi",
|
||||
"Win32_System_LibraryLoader",
|
||||
"Win32_System_RemoteDesktop",
|
||||
"Win32_System_Threading",
|
||||
"Win32_UI_Accessibility",
|
||||
"Win32_UI_HiDpi",
|
||||
"Win32_UI_Input_KeyboardAndMouse",
|
||||
"Win32_UI_Shell",
|
||||
"Win32_UI_Shell_Common",
|
||||
"Win32_UI_WindowsAndMessaging",
|
||||
"Win32_System_SystemServices"
|
||||
]
|
||||
|
||||
72
README.md
72
README.md
@@ -160,8 +160,21 @@ This means that:
|
||||
|
||||
### Quickstart
|
||||
|
||||
It highly recommended that you enable support for long paths in Windows by running the following command in an
|
||||
Administrator Terminal before starting with this quickstart guide:
|
||||
|
||||
```powershell
|
||||
Set-ItemProperty 'HKLM:\SYSTEM\CurrentControlSet\Control\FileSystem' -Name 'LongPathsEnabled' -Value 1
|
||||
```
|
||||
|
||||
Make sure that you have either the [Scoop Package Manager](https://scoop.sh) or WinGet installed, then run the following
|
||||
commands at a PowerShell prompt.
|
||||
commands at a PowerShell prompt. If you are using WinGet, make sure that you open a new terminal window or reload your
|
||||
profile after running the installation steps. Since this is not required when using `scoop`, I personally recommend that
|
||||
you use `scoop` for this process.
|
||||
|
||||
As of v0.1.17, the quickstart recommends the use of a static configuration file. If you would like to see older versions
|
||||
of this quickstart which recommend the use of dynamic configuration scripts, please refer to
|
||||
the [README file of v0.1.16](https://github.com/LGUG2Z/komorebi/tree/v0.1.16).
|
||||
|
||||
```powershell
|
||||
# if using scoop
|
||||
@@ -173,11 +186,11 @@ scoop install komorebi
|
||||
winget install LGUG2Z.whkd
|
||||
winget install LGUG2Z.komorebi
|
||||
|
||||
# save the latest generated app-specific config tweaks and fixes to ~/komorebi.generated.ps1
|
||||
iwr https://raw.githubusercontent.com/LGUG2Z/komorebi/master/komorebi.generated.ps1 -OutFile $Env:USERPROFILE\komorebi.generated.ps1
|
||||
# save the example configuration to ~/komorebi.json
|
||||
iwr https://raw.githubusercontent.com/LGUG2Z/komorebi/master/komorebi.example.json -OutFile $Env:USERPROFILE\komorebi.json
|
||||
|
||||
# save the sample komorebi configuration file to ~/komorebi.ps1
|
||||
iwr https://raw.githubusercontent.com/LGUG2Z/komorebi/master/komorebi.sample.ps1 -OutFile $Env:USERPROFILE\komorebi.ps1
|
||||
# save the latest generated app-specific config tweaks and fixes
|
||||
komorebic fetch-app-specific-configuration
|
||||
|
||||
# ensure the ~/.config folder exists
|
||||
mkdir $Env:USERPROFILE\.config -ea 0
|
||||
@@ -185,8 +198,8 @@ mkdir $Env:USERPROFILE\.config -ea 0
|
||||
# save the sample whkdrc file with key bindings to ~/.config/whkdrc
|
||||
iwr https://raw.githubusercontent.com/LGUG2Z/komorebi/master/whkdrc.sample -OutFile $Env:USERPROFILE\.config\whkdrc
|
||||
|
||||
# start komorebi
|
||||
komorebic start --await-configuration
|
||||
# start komorebi and whkd
|
||||
komorebic start -c $Env:USERPROFILE\komorebi.json --whkd
|
||||
```
|
||||
|
||||
Thanks to [@sitiom](https://github.com/sitiom) for getting _komorebi_ added to both the popular Scoop Extras bucket and
|
||||
@@ -194,7 +207,7 @@ to WinGet.
|
||||
|
||||
You can watch a walkthrough video of this quickstart below on YouTube.
|
||||
|
||||
[](https://www.youtube.com/watch?v=cBnLIwMtv8g)
|
||||
[](https://www.youtube.com/watch?v=hDDxtvpjpHs)
|
||||
|
||||
#### Using Autohotkey
|
||||
|
||||
@@ -204,7 +217,9 @@ Generally, users who opt for AHK will have specific needs that can only be addre
|
||||
and so they are assumed to be able to craft their own configuration files.
|
||||
|
||||
If you would like to try out AHK, a simple sample configuration powered by `komorebic.lib.ahk` is provided as a starting
|
||||
point.
|
||||
point. This sample configuration does not take into account the use of a static configuration file; if you choose to use
|
||||
a static configuration file alongside AHK, you can remove all the configuration options from your `komorebi.ahk` and use
|
||||
it solely to handle hotkey bindings.
|
||||
|
||||
```powershell
|
||||
# save the latest generated komorebic library to ~/komorebic.lib.ahk
|
||||
@@ -239,7 +254,12 @@ cargo install --path komorebic --locked
|
||||
|
||||
### Running
|
||||
|
||||
Run `komorebic start --await-configuration` at a Powershell prompt, and you will see the following output:
|
||||
`komorebi` can be run in two ways, using either a static configuration file or a dynamic configuration script.
|
||||
|
||||
The quickstart covers running with a static configuration file.
|
||||
|
||||
If you would like to use a dynamic configuration script, ensure that you have a `komorebi.ps1` or `komorebi.ahk` file
|
||||
present, run `komorebic start --await-configuration` at a Powershell prompt, and you will see the following output:
|
||||
|
||||
```
|
||||
Start-Process komorebi.exe -ArgumentList '--await-configuration' -WindowStyle hidden
|
||||
@@ -249,15 +269,37 @@ Waiting for komorebi.exe to start...Started!
|
||||
This means that `komorebi` is now running in the background, tiling all your windows, and listening for commands sent to
|
||||
it by `komorebic`. You can similarly stop the process by running `komorebic stop`.
|
||||
|
||||
For further information on running with a dynamic configuration script, please refer to
|
||||
the quickstart section in the [README file of v0.1.16](https://github.com/LGUG2Z/komorebi/tree/v0.1.16)
|
||||
|
||||
### Configuring
|
||||
|
||||
If you followed the quickstart, `komorebi` will find the sample `komorebi.ps1` file in your `$Env:USERPROFILE` directory
|
||||
and automatically load it. This file also starts `whkd` using the sample `whkrc` file in your `$Env:USERPROFILE\.config`
|
||||
directory.
|
||||
If you followed the quickstart, `komorebi.json` will be the single place where you declaratively configure the behaviour
|
||||
of the window manager. There is a [complete JSON Schema for this configuration file](schema.json) available to provide
|
||||
users with auto-completions in their editors.
|
||||
|
||||
If you are running with a dynamic configuration script as recommended in v0.1.16 and earlier, `komorebi` will find the
|
||||
sample `komorebi.ps1` file in your `$Env:USERPROFILE` directory and automatically load it. This file also starts `whkd` using the sample `whkrc` file
|
||||
in your `$Env:USERPROFILE\.config` directory.
|
||||
|
||||
Alternatively, if you have AutoHotKey installed and a `komorebi.ahk` file in `$Env:UserProfile` directory, `komorebi`
|
||||
will automatically try to load it when starting.
|
||||
|
||||
#### Migrating to a Static Configuration File
|
||||
|
||||
If you have been using `komorebi` with a dynamic configuration script and wish to migrate to using a static
|
||||
configuration file, once you have `komorebi` running in the desired configuration state, you can
|
||||
run `komorebic generate-static-config`.
|
||||
|
||||
This will print a static configuration that mostly represents your current configuration to the terminal.
|
||||
|
||||
There are four configuration options that you may need to set yourself, if you make use of them:
|
||||
|
||||
- Custom layouts paths for workspaces
|
||||
- Custom layout rules for workspaces
|
||||
- The applications.yaml path
|
||||
- Any individual application rules you have that are not in applications.yaml
|
||||
|
||||
#### Configuration with `komorebic`
|
||||
|
||||
As previously mentioned, this project does not handle anything related to keybindings and shortcuts directly. I
|
||||
@@ -336,6 +378,8 @@ exist in this folder.
|
||||
|
||||
#### Generating Common Application-Specific Configurations
|
||||
|
||||
❗️**NOTE**: This section is only relevant for people who use dynamic configuration scripts.
|
||||
|
||||
A curated selection of application-specific configurations can be generated to
|
||||
help ease the setup for first-time users.
|
||||
[`komorebi-application-specific-configuration`](https://github.com/LGUG2Z/komorebi-application-specific-configuration)
|
||||
@@ -407,7 +451,7 @@ komorebic.exe workspace-padding <MONITOR_INDEX> <WORKSPACE_INDEX> 0
|
||||
|
||||
#### Multiple Layout Changes on Startup
|
||||
|
||||
❗️**NOTE**: If you followed the quickstart and are using the sample configurations, this is already the default behaviour.
|
||||
❗️**NOTE**: This section is only relevant for people who use dynamic configuration scripts.
|
||||
|
||||
Depending on what is in your configuration, when `komorebi` is started, you may experience the layout rapidly being adjusted
|
||||
with many retile events.
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
[package]
|
||||
name = "komorebi-core"
|
||||
version = "0.1.16"
|
||||
version = "0.1.18"
|
||||
edition = "2021"
|
||||
|
||||
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
|
||||
@@ -11,11 +11,6 @@ color-eyre = "0.6"
|
||||
serde = { version = "1", features = ["derive"] }
|
||||
serde_json = "1"
|
||||
serde_yaml = "0.9"
|
||||
strum = { version = "0.24", features = ["derive"] }
|
||||
strum = { version = "0.25", features = ["derive"] }
|
||||
schemars = "0.8"
|
||||
|
||||
[dependencies.windows]
|
||||
version = "0.48"
|
||||
features = [
|
||||
"Win32_Foundation",
|
||||
]
|
||||
windows = { workspace = true }
|
||||
|
||||
@@ -231,7 +231,7 @@ impl Arrangement for CustomLayout {
|
||||
let mut dimensions = vec![];
|
||||
let container_count = len.get();
|
||||
|
||||
if container_count <= self.len() {
|
||||
if container_count < self.len() {
|
||||
let mut layouts = columns(area, container_count);
|
||||
dimensions.append(&mut layouts);
|
||||
} else {
|
||||
|
||||
@@ -24,31 +24,19 @@ impl ApplicationOptions {
|
||||
pub fn raw_cfgen(&self, kind: &ApplicationIdentifier, id: &str) -> String {
|
||||
match self {
|
||||
ApplicationOptions::ObjectNameChange => {
|
||||
format!(
|
||||
"komorebic.exe identify-object-name-change-application {} \"{}\"",
|
||||
kind, id
|
||||
)
|
||||
format!("komorebic.exe identify-object-name-change-application {kind} \"{id}\"",)
|
||||
}
|
||||
ApplicationOptions::Layered => {
|
||||
format!(
|
||||
"komorebic.exe identify-layered-application {} \"{}\"",
|
||||
kind, id
|
||||
)
|
||||
format!("komorebic.exe identify-layered-application {kind} \"{id}\"",)
|
||||
}
|
||||
ApplicationOptions::BorderOverflow => {
|
||||
format!(
|
||||
"komorebic.exe identify-border-overflow-application {} \"{}\"",
|
||||
kind, id
|
||||
)
|
||||
format!("komorebic.exe identify-border-overflow-application {kind} \"{id}\"",)
|
||||
}
|
||||
ApplicationOptions::TrayAndMultiWindow => {
|
||||
format!(
|
||||
"komorebic.exe identify-tray-application {} \"{}\"",
|
||||
kind, id
|
||||
)
|
||||
format!("komorebic.exe identify-tray-application {kind} \"{id}\"",)
|
||||
}
|
||||
ApplicationOptions::Force => {
|
||||
format!("komorebic.exe manage-rule {} \"{}\"", kind, id)
|
||||
format!("komorebic.exe manage-rule {kind} \"{id}\"")
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -90,7 +78,7 @@ pub struct ApplicationConfiguration {
|
||||
pub struct ApplicationConfigurationGenerator;
|
||||
|
||||
impl ApplicationConfigurationGenerator {
|
||||
fn load(content: &str) -> Result<Vec<ApplicationConfiguration>> {
|
||||
pub fn load(content: &str) -> Result<Vec<ApplicationConfiguration>> {
|
||||
Ok(serde_yaml::from_str(content)?)
|
||||
}
|
||||
|
||||
@@ -143,7 +131,7 @@ impl ApplicationConfigurationGenerator {
|
||||
lines.push(format!("# {}", app.name));
|
||||
if let Some(options) = app.options {
|
||||
for opt in options {
|
||||
if let ApplicationOptions::TrayAndMultiWindow = opt {
|
||||
if matches!(opt, ApplicationOptions::TrayAndMultiWindow) {
|
||||
lines.push(String::from("# If you have disabled minimize/close to tray for this application, you can delete/comment out the next line"));
|
||||
}
|
||||
|
||||
@@ -161,7 +149,7 @@ impl ApplicationConfigurationGenerator {
|
||||
float_rules.push(float_rule.clone());
|
||||
|
||||
if let Some(comment) = float.comment {
|
||||
lines.push(format!("# {}", comment));
|
||||
lines.push(format!("# {comment}"));
|
||||
};
|
||||
|
||||
lines.push(float_rule);
|
||||
@@ -192,7 +180,7 @@ impl ApplicationConfigurationGenerator {
|
||||
lines.push(format!("; {}", app.name));
|
||||
if let Some(options) = app.options {
|
||||
for opt in options {
|
||||
if let ApplicationOptions::TrayAndMultiWindow = opt {
|
||||
if matches!(opt, ApplicationOptions::TrayAndMultiWindow) {
|
||||
lines.push(String::from("; If you have disabled minimize/close to tray for this application, you can delete/comment out the next line"));
|
||||
}
|
||||
|
||||
@@ -212,7 +200,7 @@ impl ApplicationConfigurationGenerator {
|
||||
float_rules.push(float_rule.clone());
|
||||
|
||||
if let Some(comment) = float.comment {
|
||||
lines.push(format!("; {}", comment));
|
||||
lines.push(format!("; {comment}"));
|
||||
};
|
||||
|
||||
lines.push(float_rule);
|
||||
|
||||
@@ -72,7 +72,7 @@ impl CustomLayout {
|
||||
}
|
||||
|
||||
#[must_use]
|
||||
pub fn primary_width_percentage(&self) -> Option<usize> {
|
||||
pub fn primary_width_percentage(&self) -> Option<f32> {
|
||||
for column in self.iter() {
|
||||
if let Column::Primary(Option::Some(ColumnWidth::WidthPercentage(percentage))) = column
|
||||
{
|
||||
@@ -83,7 +83,7 @@ impl CustomLayout {
|
||||
None
|
||||
}
|
||||
|
||||
pub fn set_primary_width_percentage(&mut self, percentage: usize) {
|
||||
pub fn set_primary_width_percentage(&mut self, percentage: f32) {
|
||||
for column in self.iter_mut() {
|
||||
if let Column::Primary(Option::Some(ColumnWidth::WidthPercentage(current))) = column {
|
||||
*current = percentage;
|
||||
@@ -262,7 +262,7 @@ pub enum Column {
|
||||
|
||||
#[derive(Clone, Copy, Debug, Serialize, Deserialize, JsonSchema)]
|
||||
pub enum ColumnWidth {
|
||||
WidthPercentage(usize),
|
||||
WidthPercentage(f32),
|
||||
}
|
||||
|
||||
#[derive(Clone, Copy, Debug, Serialize, Deserialize, JsonSchema)]
|
||||
|
||||
@@ -57,6 +57,7 @@ pub enum SocketMessage {
|
||||
SendContainerToMonitorWorkspaceNumber(usize, usize),
|
||||
SendContainerToNamedWorkspace(String),
|
||||
MoveWorkspaceToMonitorNumber(usize),
|
||||
SwapWorkspacesToMonitorNumber(usize),
|
||||
ForceFocus,
|
||||
Close,
|
||||
Minimize,
|
||||
@@ -95,6 +96,7 @@ pub enum SocketMessage {
|
||||
CycleFocusWorkspace(CycleDirection),
|
||||
FocusMonitorNumber(usize),
|
||||
FocusWorkspaceNumber(usize),
|
||||
FocusWorkspaceNumbers(usize),
|
||||
FocusMonitorWorkspaceNumber(usize, usize),
|
||||
FocusNamedWorkspace(String),
|
||||
ContainerPadding(usize, usize, i32),
|
||||
@@ -116,6 +118,7 @@ pub enum SocketMessage {
|
||||
ClearNamedWorkspaceLayoutRules(String),
|
||||
// Configuration
|
||||
ReloadConfiguration,
|
||||
ReloadStaticConfiguration(PathBuf),
|
||||
WatchConfiguration(bool),
|
||||
CompleteConfiguration,
|
||||
AltFocusHack(bool),
|
||||
@@ -143,10 +146,14 @@ pub enum SocketMessage {
|
||||
ToggleFocusFollowsMouse(FocusFollowsMouseImplementation),
|
||||
MouseFollowsFocus(bool),
|
||||
ToggleMouseFollowsFocus,
|
||||
RemoveTitleBar(ApplicationIdentifier, String),
|
||||
ToggleTitleBars,
|
||||
AddSubscriber(String),
|
||||
RemoveSubscriber(String),
|
||||
NotificationSchema,
|
||||
SocketSchema,
|
||||
StaticConfigSchema,
|
||||
GenerateStaticConfig,
|
||||
}
|
||||
|
||||
impl SocketMessage {
|
||||
@@ -188,10 +195,12 @@ pub enum StateQuery {
|
||||
Copy, Clone, Debug, Serialize, Deserialize, Display, EnumString, ValueEnum, JsonSchema,
|
||||
)]
|
||||
#[strum(serialize_all = "snake_case")]
|
||||
#[serde(rename_all = "snake_case")]
|
||||
pub enum ApplicationIdentifier {
|
||||
#[serde(alias = "exe")]
|
||||
Exe,
|
||||
#[serde(alias = "class")]
|
||||
Class,
|
||||
#[serde(alias = "title")]
|
||||
Title,
|
||||
}
|
||||
|
||||
@@ -200,7 +209,9 @@ pub enum ApplicationIdentifier {
|
||||
)]
|
||||
#[strum(serialize_all = "snake_case")]
|
||||
pub enum FocusFollowsMouseImplementation {
|
||||
/// A custom FFM implementation (slightly more CPU-intensive)
|
||||
Komorebi,
|
||||
/// The native (legacy) Windows FFM implementation
|
||||
Windows,
|
||||
}
|
||||
|
||||
@@ -209,7 +220,9 @@ pub enum FocusFollowsMouseImplementation {
|
||||
)]
|
||||
#[strum(serialize_all = "snake_case")]
|
||||
pub enum WindowContainerBehaviour {
|
||||
/// Create a new container for each new window
|
||||
Create,
|
||||
/// Append new windows to the focused window container
|
||||
Append,
|
||||
}
|
||||
|
||||
@@ -218,7 +231,9 @@ pub enum WindowContainerBehaviour {
|
||||
)]
|
||||
#[strum(serialize_all = "snake_case")]
|
||||
pub enum MoveBehaviour {
|
||||
/// Swap the window container with the window container at the edge of the adjacent monitor
|
||||
Swap,
|
||||
/// Insert the window container into the focused workspace on the adjacent monitor
|
||||
Insert,
|
||||
}
|
||||
|
||||
@@ -227,8 +242,11 @@ pub enum MoveBehaviour {
|
||||
)]
|
||||
#[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,
|
||||
/// Use the SW_MINIMIZE flag to hide windows when switching workspaces (has issues with frequent workspace switching)
|
||||
Minimize,
|
||||
/// Use the undocumented SetCloak Win32 function to hide windows when switching workspaces (has foregrounding issues)
|
||||
Cloak,
|
||||
}
|
||||
|
||||
@@ -237,7 +255,9 @@ pub enum HidingBehaviour {
|
||||
)]
|
||||
#[strum(serialize_all = "snake_case")]
|
||||
pub enum OperationBehaviour {
|
||||
/// Process komorebic commands on temporarily unmanaged/floated windows
|
||||
Op,
|
||||
/// Ignore komorebic commands on temporarily unmanaged/floated windows
|
||||
NoOp,
|
||||
}
|
||||
|
||||
|
||||
@@ -5,9 +5,13 @@ use windows::Win32::Foundation::RECT;
|
||||
|
||||
#[derive(Debug, Default, Clone, Copy, Serialize, Deserialize, Eq, PartialEq, JsonSchema)]
|
||||
pub struct Rect {
|
||||
/// The left point in a Win32 Rect
|
||||
pub left: i32,
|
||||
/// The top point in a Win32 Rect
|
||||
pub top: i32,
|
||||
/// The right point in a Win32 Rect
|
||||
pub right: i32,
|
||||
/// The bottom point in a Win32 Rect
|
||||
pub bottom: i32,
|
||||
}
|
||||
|
||||
|
||||
25
komorebi.example.json
Normal file
25
komorebi.example.json
Normal file
@@ -0,0 +1,25 @@
|
||||
{
|
||||
"app_specific_configuration_path": "$Env:USERPROFILE/applications.yaml",
|
||||
"window_hiding_behaviour": "Cloak",
|
||||
"cross_monitor_move_behaviour": "Insert",
|
||||
"alt_focus_hack": true,
|
||||
"default_workspace_padding": 20,
|
||||
"default_container_padding": 20,
|
||||
"active_window_border": false,
|
||||
"active_window_border_colours": {
|
||||
"single": { "r": 66, "g": 165, "b": 245 },
|
||||
"stack": { "r": 256, "g": 165, "b": 66 },
|
||||
"monocle": { "r": 255, "g": 51, "b": 153 }
|
||||
},
|
||||
"monitors": [
|
||||
{
|
||||
"workspaces": [
|
||||
{ "name": "I", "layout": "BSP" },
|
||||
{ "name": "II", "layout": "VerticalStack" },
|
||||
{ "name": "III", "layout": "HorizontalStack" },
|
||||
{ "name": "IV", "layout": "UltrawideVerticalStack" },
|
||||
{ "name": "V", "layout": "Rows" }
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -16,10 +16,17 @@ RunWait('komorebic.exe identify-tray-application class "CreativeCloudDesktopWind
|
||||
; Adobe Photoshop
|
||||
RunWait('komorebic.exe identify-border-overflow-application class "Photoshop"', , "Hide")
|
||||
|
||||
; Affinity Photo 2
|
||||
RunWait('komorebic.exe manage-rule title "Affinity Photo 2"', , "Hide")
|
||||
RunWait('komorebic.exe float-rule exe "Photo.exe"', , "Hide")
|
||||
|
||||
; Akiflow
|
||||
; If you have disabled minimize/close to tray for this application, you can delete/comment out the next line
|
||||
RunWait('komorebic.exe identify-tray-application exe "Akiflow.exe"', , "Hide")
|
||||
|
||||
; Android Studio
|
||||
RunWait('komorebic.exe identify-object-name-change-application exe "studio64.exe"', , "Hide")
|
||||
|
||||
; ArmCord
|
||||
RunWait('komorebic.exe identify-border-overflow-application exe "ArmCord.exe"', , "Hide")
|
||||
; If you have disabled minimize/close to tray for this application, you can delete/comment out the next line
|
||||
@@ -54,6 +61,10 @@ RunWait('komorebic.exe identify-border-overflow-application exe "Cron.exe"', , "
|
||||
; If you have disabled minimize/close to tray for this application, you can delete/comment out the next line
|
||||
RunWait('komorebic.exe identify-tray-application exe "Cron.exe"', , "Hide")
|
||||
|
||||
; DS4Windows
|
||||
; If you have disabled minimize/close to tray for this application, you can delete/comment out the next line
|
||||
RunWait('komorebic.exe identify-tray-application exe "DS4Windows.exe"', , "Hide")
|
||||
|
||||
; Delphi applications
|
||||
; Target hidden window spawned by Delphi applications
|
||||
RunWait('komorebic.exe float-rule class "TApplication"', , "Hide")
|
||||
@@ -274,6 +285,9 @@ RunWait('komorebic.exe identify-tray-application class "DocEditorsWindowClass"',
|
||||
RunWait('komorebic.exe identify-border-overflow-application exe "Obsidian.exe"', , "Hide")
|
||||
RunWait('komorebic.exe manage-rule exe "Obsidian.exe"', , "Hide")
|
||||
|
||||
; OneDrive
|
||||
RunWait('komorebic.exe float-rule class "OneDriveReactNativeWin32WindowClass"', , "Hide")
|
||||
|
||||
; OpenRGB
|
||||
; If you have disabled minimize/close to tray for this application, you can delete/comment out the next line
|
||||
RunWait('komorebic.exe identify-tray-application exe "OpenRGB.exe"', , "Hide")
|
||||
@@ -289,6 +303,8 @@ RunWait('komorebic.exe identify-border-overflow-application exe "Plexamp.exe"',
|
||||
RunWait('komorebic.exe float-rule exe "PowerToys.ColorPickerUI.exe"', , "Hide")
|
||||
; Target image resizer dialog
|
||||
RunWait('komorebic.exe float-rule exe "PowerToys.ImageResizer.exe"', , "Hide")
|
||||
; Target Peek popup
|
||||
RunWait('komorebic.exe float-rule exe "PowerToys.Peek.UI.exe"', , "Hide")
|
||||
|
||||
; Process Hacker
|
||||
; If you have disabled minimize/close to tray for this application, you can delete/comment out the next line
|
||||
@@ -350,13 +366,11 @@ RunWait('komorebic.exe identify-tray-application exe "sirikali.exe"', , "Hide")
|
||||
|
||||
; Slack
|
||||
RunWait('komorebic.exe identify-border-overflow-application exe "Slack.exe"', , "Hide")
|
||||
RunWait('komorebic.exe manage-rule exe "Slack.exe"', , "Hide")
|
||||
; If you have disabled minimize/close to tray for this application, you can delete/comment out the next line
|
||||
RunWait('komorebic.exe identify-tray-application exe "Slack.exe"', , "Hide")
|
||||
|
||||
; Slack
|
||||
RunWait('komorebic.exe identify-border-overflow-application exe "slack.exe"', , "Hide")
|
||||
RunWait('komorebic.exe manage-rule exe "slack.exe"', , "Hide")
|
||||
; If you have disabled minimize/close to tray for this application, you can delete/comment out the next line
|
||||
RunWait('komorebic.exe identify-tray-application exe "slack.exe"', , "Hide")
|
||||
|
||||
@@ -382,6 +396,8 @@ RunWait('komorebic.exe identify-border-overflow-application class "vguiPopupWind
|
||||
RunWait('komorebic.exe identify-border-overflow-application class "SDL_app"', , "Hide")
|
||||
; If you have disabled minimize/close to tray for this application, you can delete/comment out the next line
|
||||
RunWait('komorebic.exe identify-tray-application class "SDL_app"', , "Hide")
|
||||
; Target notification toast popups
|
||||
RunWait('komorebic.exe float-rule title "notificationtoasts_"', , "Hide")
|
||||
|
||||
; Stremio
|
||||
; If you have disabled minimize/close to tray for this application, you can delete/comment out the next line
|
||||
@@ -420,6 +436,10 @@ RunWait('komorebic.exe float-rule exe "TranslucentTB.exe"', , "Hide")
|
||||
; If you have disabled minimize/close to tray for this application, you can delete/comment out the next line
|
||||
RunWait('komorebic.exe identify-tray-application exe "TranslucentTB.exe"', , "Hide")
|
||||
|
||||
; Unity Hub
|
||||
; If you have disabled minimize/close to tray for this application, you can delete/comment out the next line
|
||||
RunWait('komorebic.exe identify-tray-application exe "Unity Hub.exe"', , "Hide")
|
||||
|
||||
; Unreal Editor
|
||||
RunWait('komorebic.exe identify-border-overflow-application exe "UnrealEditor.exe"', , "Hide")
|
||||
; If you have disabled minimize/close to tray for this application, you can delete/comment out the next line
|
||||
@@ -435,6 +455,9 @@ RunWait('komorebic.exe identify-object-name-change-application exe "devenv.exe"'
|
||||
; Visual Studio Code
|
||||
RunWait('komorebic.exe identify-border-overflow-application exe "Code.exe"', , "Hide")
|
||||
|
||||
; Visual Studio Code - Insiders
|
||||
RunWait('komorebic.exe identify-border-overflow-application exe "Code - Insiders.exe"', , "Hide")
|
||||
|
||||
; Voice.ai
|
||||
RunWait('komorebic.exe identify-border-overflow-application exe "VoiceAI.exe"', , "Hide")
|
||||
; If you have disabled minimize/close to tray for this application, you can delete/comment out the next line
|
||||
|
||||
@@ -16,10 +16,17 @@ komorebic.exe identify-tray-application class "CreativeCloudDesktopWindowClass"
|
||||
# Adobe Photoshop
|
||||
komorebic.exe identify-border-overflow-application class "Photoshop"
|
||||
|
||||
# Affinity Photo 2
|
||||
komorebic.exe manage-rule title "Affinity Photo 2"
|
||||
komorebic.exe float-rule exe "Photo.exe"
|
||||
|
||||
# Akiflow
|
||||
# If you have disabled minimize/close to tray for this application, you can delete/comment out the next line
|
||||
komorebic.exe identify-tray-application exe "Akiflow.exe"
|
||||
|
||||
# Android Studio
|
||||
komorebic.exe identify-object-name-change-application exe "studio64.exe"
|
||||
|
||||
# ArmCord
|
||||
komorebic.exe identify-border-overflow-application exe "ArmCord.exe"
|
||||
# If you have disabled minimize/close to tray for this application, you can delete/comment out the next line
|
||||
@@ -54,6 +61,10 @@ komorebic.exe identify-border-overflow-application exe "Cron.exe"
|
||||
# If you have disabled minimize/close to tray for this application, you can delete/comment out the next line
|
||||
komorebic.exe identify-tray-application exe "Cron.exe"
|
||||
|
||||
# DS4Windows
|
||||
# If you have disabled minimize/close to tray for this application, you can delete/comment out the next line
|
||||
komorebic.exe identify-tray-application exe "DS4Windows.exe"
|
||||
|
||||
# Delphi applications
|
||||
# Target hidden window spawned by Delphi applications
|
||||
komorebic.exe float-rule class "TApplication"
|
||||
@@ -274,6 +285,9 @@ komorebic.exe identify-tray-application class "DocEditorsWindowClass"
|
||||
komorebic.exe identify-border-overflow-application exe "Obsidian.exe"
|
||||
komorebic.exe manage-rule exe "Obsidian.exe"
|
||||
|
||||
# OneDrive
|
||||
komorebic.exe float-rule class "OneDriveReactNativeWin32WindowClass"
|
||||
|
||||
# OpenRGB
|
||||
# If you have disabled minimize/close to tray for this application, you can delete/comment out the next line
|
||||
komorebic.exe identify-tray-application exe "OpenRGB.exe"
|
||||
@@ -289,6 +303,8 @@ komorebic.exe identify-border-overflow-application exe "Plexamp.exe"
|
||||
komorebic.exe float-rule exe "PowerToys.ColorPickerUI.exe"
|
||||
# Target image resizer dialog
|
||||
komorebic.exe float-rule exe "PowerToys.ImageResizer.exe"
|
||||
# Target Peek popup
|
||||
komorebic.exe float-rule exe "PowerToys.Peek.UI.exe"
|
||||
|
||||
# Process Hacker
|
||||
# If you have disabled minimize/close to tray for this application, you can delete/comment out the next line
|
||||
@@ -350,13 +366,11 @@ komorebic.exe identify-tray-application exe "sirikali.exe"
|
||||
|
||||
# Slack
|
||||
komorebic.exe identify-border-overflow-application exe "Slack.exe"
|
||||
komorebic.exe manage-rule exe "Slack.exe"
|
||||
# If you have disabled minimize/close to tray for this application, you can delete/comment out the next line
|
||||
komorebic.exe identify-tray-application exe "Slack.exe"
|
||||
|
||||
# Slack
|
||||
komorebic.exe identify-border-overflow-application exe "slack.exe"
|
||||
komorebic.exe manage-rule exe "slack.exe"
|
||||
# If you have disabled minimize/close to tray for this application, you can delete/comment out the next line
|
||||
komorebic.exe identify-tray-application exe "slack.exe"
|
||||
|
||||
@@ -382,6 +396,8 @@ komorebic.exe identify-border-overflow-application class "vguiPopupWindow"
|
||||
komorebic.exe identify-border-overflow-application class "SDL_app"
|
||||
# If you have disabled minimize/close to tray for this application, you can delete/comment out the next line
|
||||
komorebic.exe identify-tray-application class "SDL_app"
|
||||
# Target notification toast popups
|
||||
komorebic.exe float-rule title "notificationtoasts_"
|
||||
|
||||
# Stremio
|
||||
# If you have disabled minimize/close to tray for this application, you can delete/comment out the next line
|
||||
@@ -420,6 +436,10 @@ komorebic.exe float-rule exe "TranslucentTB.exe"
|
||||
# If you have disabled minimize/close to tray for this application, you can delete/comment out the next line
|
||||
komorebic.exe identify-tray-application exe "TranslucentTB.exe"
|
||||
|
||||
# Unity Hub
|
||||
# If you have disabled minimize/close to tray for this application, you can delete/comment out the next line
|
||||
komorebic.exe identify-tray-application exe "Unity Hub.exe"
|
||||
|
||||
# Unreal Editor
|
||||
komorebic.exe identify-border-overflow-application exe "UnrealEditor.exe"
|
||||
# If you have disabled minimize/close to tray for this application, you can delete/comment out the next line
|
||||
@@ -435,6 +455,9 @@ komorebic.exe identify-object-name-change-application exe "devenv.exe"
|
||||
# Visual Studio Code
|
||||
komorebic.exe identify-border-overflow-application exe "Code.exe"
|
||||
|
||||
# Visual Studio Code - Insiders
|
||||
komorebic.exe identify-border-overflow-application exe "Code - Insiders.exe"
|
||||
|
||||
# Voice.ai
|
||||
komorebic.exe identify-border-overflow-application exe "VoiceAI.exe"
|
||||
# If you have disabled minimize/close to tray for this application, you can delete/comment out the next line
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
[package]
|
||||
name = "komorebi"
|
||||
version = "0.1.16"
|
||||
version = "0.1.18"
|
||||
authors = ["Jade Iqbal <jadeiqbal@fastmail.com>"]
|
||||
description = "A tiling window manager for Windows"
|
||||
categories = ["tiling-window-manager", "windows"]
|
||||
@@ -32,7 +32,7 @@ paste = "1"
|
||||
schemars = "0.8"
|
||||
serde = { version = "1", features = ["derive"] }
|
||||
serde_json = "1"
|
||||
strum = { version = "0.24", features = ["derive"] }
|
||||
strum = { version = "0.25", features = ["derive"] }
|
||||
sysinfo = "0.29"
|
||||
tracing = "0.1"
|
||||
tracing-appender = "0.2"
|
||||
@@ -41,28 +41,9 @@ uds_windows = "1"
|
||||
which = "4"
|
||||
winput = "0.2"
|
||||
winreg = "0.50"
|
||||
windows-interface = { version = "0.48" }
|
||||
windows-implement = { version = "0.48" }
|
||||
[dependencies.windows]
|
||||
version = "0.48"
|
||||
features = [
|
||||
"implement",
|
||||
"Win32_System_Com",
|
||||
"Win32_UI_Shell_Common", # for IObjectArray
|
||||
"Win32_Foundation",
|
||||
"Win32_Graphics_Dwm",
|
||||
"Win32_Graphics_Gdi",
|
||||
"Win32_System_LibraryLoader",
|
||||
"Win32_System_RemoteDesktop",
|
||||
"Win32_System_Threading",
|
||||
"Win32_UI_Accessibility",
|
||||
"Win32_UI_HiDpi",
|
||||
"Win32_UI_Input_KeyboardAndMouse",
|
||||
"Win32_UI_Shell",
|
||||
"Win32_UI_Shell_Common",
|
||||
"Win32_UI_WindowsAndMessaging",
|
||||
"Win32_System_SystemServices"
|
||||
]
|
||||
windows-interface = { workspace = true }
|
||||
windows-implement = { workspace = true }
|
||||
windows = { workspace = true }
|
||||
|
||||
[features]
|
||||
deadlock_detection = []
|
||||
|
||||
@@ -48,6 +48,7 @@ use crate::process_command::listen_for_commands;
|
||||
use crate::process_command::listen_for_commands_tcp;
|
||||
use crate::process_event::listen_for_events;
|
||||
use crate::process_movement::listen_for_movements;
|
||||
use crate::static_config::StaticConfig;
|
||||
use crate::window_manager::State;
|
||||
use crate::window_manager::WindowManager;
|
||||
use crate::window_manager_event::WindowManagerEvent;
|
||||
@@ -65,6 +66,7 @@ mod process_command;
|
||||
mod process_event;
|
||||
mod process_movement;
|
||||
mod set_window_position;
|
||||
mod static_config;
|
||||
mod styles;
|
||||
mod window;
|
||||
mod window_manager;
|
||||
@@ -75,6 +77,8 @@ mod winevent;
|
||||
mod winevent_listener;
|
||||
mod workspace;
|
||||
|
||||
type WorkspaceRule = (usize, usize, bool);
|
||||
|
||||
lazy_static! {
|
||||
static ref HIDDEN_HWNDS: Arc<Mutex<Vec<isize>>> = Arc::new(Mutex::new(vec![]));
|
||||
static ref LAYERED_WHITELIST: Arc<Mutex<Vec<String>>> =
|
||||
@@ -94,7 +98,7 @@ lazy_static! {
|
||||
]));
|
||||
static ref MONITOR_INDEX_PREFERENCES: Arc<Mutex<HashMap<usize, Rect>>> =
|
||||
Arc::new(Mutex::new(HashMap::new()));
|
||||
static ref WORKSPACE_RULES: Arc<Mutex<HashMap<String, (usize, usize, bool)>>> =
|
||||
static ref WORKSPACE_RULES: Arc<Mutex<HashMap<String, WorkspaceRule>>> =
|
||||
Arc::new(Mutex::new(HashMap::new()));
|
||||
static ref MANAGE_IDENTIFIERS: Arc<Mutex<Vec<String>>> = Arc::new(Mutex::new(vec![]));
|
||||
static ref FLOAT_IDENTIFIERS: Arc<Mutex<Vec<String>>> = Arc::new(Mutex::new(vec![
|
||||
@@ -103,6 +107,9 @@ lazy_static! {
|
||||
"OPContainerClass".to_string(),
|
||||
"IHWindowClass".to_string()
|
||||
]));
|
||||
static ref PERMAIGNORE_CLASSES: Arc<Mutex<Vec<String>>> = Arc::new(Mutex::new(vec![
|
||||
"Chrome_RenderWidgetHostHWND".to_string(),
|
||||
]));
|
||||
static ref BORDER_OVERFLOW_IDENTIFIERS: Arc<Mutex<Vec<String>>> = Arc::new(Mutex::new(vec![]));
|
||||
static ref WSL2_UI_PROCESSES: Arc<Mutex<Vec<String>>> = Arc::new(Mutex::new(vec![
|
||||
"X410.exe".to_string(),
|
||||
@@ -122,8 +129,7 @@ lazy_static! {
|
||||
home
|
||||
} else {
|
||||
panic!(
|
||||
"$Env:KOMOREBI_CONFIG_HOME is set to '{}', which is not a valid directory",
|
||||
home_path
|
||||
"$Env:KOMOREBI_CONFIG_HOME is set to '{home_path}', which is not a valid directory",
|
||||
);
|
||||
}
|
||||
})
|
||||
@@ -152,8 +158,15 @@ lazy_static! {
|
||||
|
||||
static ref BORDER_OFFSET: Arc<Mutex<Option<Rect>>> =
|
||||
Arc::new(Mutex::new(None));
|
||||
|
||||
// Use app-specific titlebar removal options where possible
|
||||
// eg. Windows Terminal, IntelliJ IDEA, Firefox
|
||||
static ref NO_TITLEBAR: Arc<Mutex<Vec<String>>> = Arc::new(Mutex::new(vec![]));
|
||||
}
|
||||
|
||||
pub static DEFAULT_WORKSPACE_PADDING: AtomicI32 = AtomicI32::new(10);
|
||||
pub static DEFAULT_CONTAINER_PADDING: AtomicI32 = AtomicI32::new(10);
|
||||
|
||||
pub static INITIAL_CONFIGURATION_LOADED: AtomicBool = AtomicBool::new(false);
|
||||
pub static CUSTOM_FFM: AtomicBool = AtomicBool::new(false);
|
||||
pub static SESSION_ID: AtomicU32 = AtomicU32::new(0);
|
||||
@@ -168,6 +181,7 @@ pub static BORDER_COLOUR_CURRENT: AtomicU32 = AtomicU32::new(0);
|
||||
pub static BORDER_WIDTH: AtomicI32 = AtomicI32::new(20);
|
||||
// 0 0 0 aka pure black, I doubt anyone will want this as a border colour
|
||||
pub const TRANSPARENCY_COLOUR: u32 = 0;
|
||||
pub static REMOVE_TITLEBARS: AtomicBool = AtomicBool::new(false);
|
||||
|
||||
pub static HIDDEN_HWND: AtomicIsize = AtomicIsize::new(0);
|
||||
|
||||
@@ -270,7 +284,7 @@ pub fn load_configuration() -> Result<()> {
|
||||
.ok_or_else(|| anyhow!("cannot convert path to string"))?
|
||||
);
|
||||
|
||||
Command::new("autohotkey.exe")
|
||||
Command::new(&*AHK_EXE)
|
||||
.arg(config_ahk.as_os_str())
|
||||
.output()?;
|
||||
}
|
||||
@@ -339,7 +353,7 @@ pub fn notify_subscribers(notification: &str) -> Result<()> {
|
||||
let mut stale_subscriptions = vec![];
|
||||
let mut subscriptions = SUBSCRIPTION_PIPES.lock();
|
||||
for (subscriber, pipe) in subscriptions.iter_mut() {
|
||||
match writeln!(pipe, "{}", notification) {
|
||||
match writeln!(pipe, "{notification}") {
|
||||
Ok(_) => {
|
||||
tracing::debug!("pushed notification to subscriber: {}", subscriber);
|
||||
}
|
||||
@@ -404,81 +418,89 @@ struct Opts {
|
||||
/// Start a TCP server on the given port to allow the direct sending of SocketMessages
|
||||
#[clap(action, short, long)]
|
||||
tcp_port: Option<usize>,
|
||||
/// Path to a static configuration JSON file
|
||||
#[clap(action, short, long)]
|
||||
config: Option<PathBuf>,
|
||||
}
|
||||
|
||||
#[tracing::instrument]
|
||||
#[allow(clippy::nonminimal_bool)]
|
||||
#[allow(clippy::cognitive_complexity)]
|
||||
fn main() -> Result<()> {
|
||||
let opts: Opts = Opts::parse();
|
||||
CUSTOM_FFM.store(opts.focus_follows_mouse, Ordering::SeqCst);
|
||||
|
||||
let arg_count = std::env::args().count();
|
||||
let process_id = WindowsApi::current_process_id();
|
||||
WindowsApi::allow_set_foreground_window(process_id)?;
|
||||
WindowsApi::set_process_dpi_awareness_context()?;
|
||||
|
||||
let has_valid_args = arg_count == 1
|
||||
|| (arg_count == 2
|
||||
&& (opts.await_configuration || opts.focus_follows_mouse || opts.tcp_port.is_some()))
|
||||
|| (arg_count == 3 && opts.await_configuration && opts.focus_follows_mouse)
|
||||
|| (arg_count == 3 && opts.tcp_port.is_some() && opts.focus_follows_mouse)
|
||||
|| (arg_count == 3 && opts.tcp_port.is_some() && opts.await_configuration)
|
||||
|| (arg_count == 4
|
||||
&& (opts.focus_follows_mouse && opts.await_configuration && opts.tcp_port.is_some()));
|
||||
let session_id = WindowsApi::process_id_to_session_id()?;
|
||||
SESSION_ID.store(session_id, Ordering::SeqCst);
|
||||
|
||||
if has_valid_args {
|
||||
let process_id = WindowsApi::current_process_id();
|
||||
WindowsApi::allow_set_foreground_window(process_id)?;
|
||||
WindowsApi::set_process_dpi_awareness_context()?;
|
||||
let mut system = sysinfo::System::new_all();
|
||||
system.refresh_processes();
|
||||
|
||||
let session_id = WindowsApi::process_id_to_session_id()?;
|
||||
SESSION_ID.store(session_id, Ordering::SeqCst);
|
||||
let matched_procs: Vec<&Process> = system.processes_by_name("komorebi.exe").collect();
|
||||
|
||||
let mut system = sysinfo::System::new_all();
|
||||
system.refresh_processes();
|
||||
|
||||
let matched_procs: Vec<&Process> = system.processes_by_name("komorebi.exe").collect();
|
||||
|
||||
if matched_procs.len() > 1 {
|
||||
let mut len = matched_procs.len();
|
||||
for proc in matched_procs {
|
||||
if proc.root().ends_with("shims") {
|
||||
len -= 1;
|
||||
}
|
||||
}
|
||||
|
||||
if len > 1 {
|
||||
tracing::error!("komorebi.exe is already running, please exit the existing process before starting a new one");
|
||||
std::process::exit(1);
|
||||
if matched_procs.len() > 1 {
|
||||
let mut len = matched_procs.len();
|
||||
for proc in matched_procs {
|
||||
if proc.root().ends_with("shims") {
|
||||
len -= 1;
|
||||
}
|
||||
}
|
||||
|
||||
// File logging worker guard has to have an assignment in the main fn to work
|
||||
let (_guard, _color_guard) = setup()?;
|
||||
if len > 1 {
|
||||
tracing::error!("komorebi.exe is already running, please exit the existing process before starting a new one");
|
||||
std::process::exit(1);
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(feature = "deadlock_detection")]
|
||||
detect_deadlocks();
|
||||
// File logging worker guard has to have an assignment in the main fn to work
|
||||
let (_guard, _color_guard) = setup()?;
|
||||
|
||||
let (outgoing, incoming): (Sender<WindowManagerEvent>, Receiver<WindowManagerEvent>) =
|
||||
crossbeam_channel::unbounded();
|
||||
#[cfg(feature = "deadlock_detection")]
|
||||
detect_deadlocks();
|
||||
|
||||
let winevent_listener = winevent_listener::new(Arc::new(Mutex::new(outgoing)));
|
||||
winevent_listener.start();
|
||||
let (outgoing, incoming): (Sender<WindowManagerEvent>, Receiver<WindowManagerEvent>) =
|
||||
crossbeam_channel::unbounded();
|
||||
|
||||
Hidden::create("komorebi-hidden")?;
|
||||
let winevent_listener = winevent_listener::new(Arc::new(Mutex::new(outgoing)));
|
||||
winevent_listener.start();
|
||||
|
||||
let wm = Arc::new(Mutex::new(WindowManager::new(Arc::new(Mutex::new(
|
||||
Hidden::create("komorebi-hidden")?;
|
||||
|
||||
let wm = if let Some(config) = &opts.config {
|
||||
tracing::info!(
|
||||
"creating window manager from static configuration file: {}",
|
||||
config.as_os_str().to_str().unwrap()
|
||||
);
|
||||
|
||||
Arc::new(Mutex::new(StaticConfig::preload(
|
||||
config,
|
||||
Arc::new(Mutex::new(incoming)),
|
||||
)?))
|
||||
} else {
|
||||
Arc::new(Mutex::new(WindowManager::new(Arc::new(Mutex::new(
|
||||
incoming,
|
||||
)))?));
|
||||
)))?))
|
||||
};
|
||||
|
||||
wm.lock().init()?;
|
||||
listen_for_commands(wm.clone());
|
||||
wm.lock().init()?;
|
||||
if let Some(config) = &opts.config {
|
||||
StaticConfig::postload(config, &wm)?;
|
||||
}
|
||||
|
||||
if !opts.await_configuration && !INITIAL_CONFIGURATION_LOADED.load(Ordering::SeqCst) {
|
||||
INITIAL_CONFIGURATION_LOADED.store(true, Ordering::SeqCst);
|
||||
};
|
||||
listen_for_commands(wm.clone());
|
||||
|
||||
if let Some(port) = opts.tcp_port {
|
||||
listen_for_commands_tcp(wm.clone(), port);
|
||||
}
|
||||
if !opts.await_configuration && !INITIAL_CONFIGURATION_LOADED.load(Ordering::SeqCst) {
|
||||
INITIAL_CONFIGURATION_LOADED.store(true, Ordering::SeqCst);
|
||||
};
|
||||
|
||||
if let Some(port) = opts.tcp_port {
|
||||
listen_for_commands_tcp(wm.clone(), port);
|
||||
}
|
||||
|
||||
if opts.config.is_none() {
|
||||
std::thread::spawn(|| {
|
||||
load_configuration().expect("could not load configuration");
|
||||
});
|
||||
@@ -489,36 +511,34 @@ fn main() -> Result<()> {
|
||||
backoff.snooze();
|
||||
}
|
||||
}
|
||||
|
||||
wm.lock().retile_all(false)?;
|
||||
|
||||
listen_for_events(wm.clone());
|
||||
|
||||
if CUSTOM_FFM.load(Ordering::SeqCst) {
|
||||
listen_for_movements(wm.clone());
|
||||
}
|
||||
|
||||
let (ctrlc_sender, ctrlc_receiver) = crossbeam_channel::bounded(1);
|
||||
ctrlc::set_handler(move || {
|
||||
ctrlc_sender
|
||||
.send(())
|
||||
.expect("could not send signal on ctrl-c channel");
|
||||
})?;
|
||||
|
||||
ctrlc_receiver
|
||||
.recv()
|
||||
.expect("could not receive signal on ctrl-c channel");
|
||||
|
||||
tracing::error!("received ctrl-c, restoring all hidden windows and terminating process");
|
||||
|
||||
wm.lock().restore_all_windows();
|
||||
|
||||
if WindowsApi::focus_follows_mouse()? {
|
||||
WindowsApi::disable_focus_follows_mouse()?;
|
||||
}
|
||||
|
||||
std::process::exit(130);
|
||||
}
|
||||
|
||||
Ok(())
|
||||
wm.lock().retile_all(false)?;
|
||||
|
||||
listen_for_events(wm.clone());
|
||||
|
||||
if CUSTOM_FFM.load(Ordering::SeqCst) {
|
||||
listen_for_movements(wm.clone());
|
||||
}
|
||||
|
||||
let (ctrlc_sender, ctrlc_receiver) = crossbeam_channel::bounded(1);
|
||||
ctrlc::set_handler(move || {
|
||||
ctrlc_sender
|
||||
.send(())
|
||||
.expect("could not send signal on ctrl-c channel");
|
||||
})?;
|
||||
|
||||
ctrlc_receiver
|
||||
.recv()
|
||||
.expect("could not receive signal on ctrl-c channel");
|
||||
|
||||
tracing::error!("received ctrl-c, restoring all hidden windows and terminating process");
|
||||
|
||||
wm.lock().restore_all_windows()?;
|
||||
|
||||
if WindowsApi::focus_follows_mouse()? {
|
||||
WindowsApi::disable_focus_follows_mouse()?;
|
||||
}
|
||||
|
||||
std::process::exit(130);
|
||||
}
|
||||
|
||||
@@ -105,6 +105,10 @@ impl Monitor {
|
||||
}
|
||||
}
|
||||
|
||||
pub fn remove_workspaces(&mut self) -> VecDeque<Workspace> {
|
||||
self.workspaces_mut().drain(..).collect()
|
||||
}
|
||||
|
||||
#[tracing::instrument(skip(self))]
|
||||
pub fn move_container_to_workspace(
|
||||
&mut self,
|
||||
|
||||
@@ -36,6 +36,7 @@ use komorebi_core::WindowKind;
|
||||
use crate::border::Border;
|
||||
use crate::current_virtual_desktop;
|
||||
use crate::notify_subscribers;
|
||||
use crate::static_config::StaticConfig;
|
||||
use crate::window::Window;
|
||||
use crate::window_manager;
|
||||
use crate::window_manager::WindowManager;
|
||||
@@ -61,7 +62,9 @@ use crate::INITIAL_CONFIGURATION_LOADED;
|
||||
use crate::LAYERED_WHITELIST;
|
||||
use crate::MANAGE_IDENTIFIERS;
|
||||
use crate::MONITOR_INDEX_PREFERENCES;
|
||||
use crate::NO_TITLEBAR;
|
||||
use crate::OBJECT_NAME_CHANGE_ON_LAUNCH;
|
||||
use crate::REMOVE_TITLEBARS;
|
||||
use crate::SUBSCRIPTION_PIPES;
|
||||
use crate::TCP_CONNECTIONS;
|
||||
use crate::TRAY_AND_MULTI_WINDOW_IDENTIFIERS;
|
||||
@@ -95,7 +98,7 @@ pub fn listen_for_commands(wm: Arc<Mutex<WindowManager>>) {
|
||||
#[tracing::instrument]
|
||||
pub fn listen_for_commands_tcp(wm: Arc<Mutex<WindowManager>>, port: usize) {
|
||||
let listener =
|
||||
TcpListener::bind(format!("0.0.0.0:{}", port)).expect("could not start tcp server");
|
||||
TcpListener::bind(format!("0.0.0.0:{port}")).expect("could not start tcp server");
|
||||
|
||||
std::thread::spawn(move || {
|
||||
tracing::info!("listening on 0.0.0.0:43663");
|
||||
@@ -322,6 +325,9 @@ impl WindowManager {
|
||||
SocketMessage::MoveContainerToMonitorNumber(monitor_idx) => {
|
||||
self.move_container_to_monitor(monitor_idx, None, true)?;
|
||||
}
|
||||
SocketMessage::SwapWorkspacesToMonitorNumber(monitor_idx) => {
|
||||
self.swap_focused_monitor(monitor_idx)?;
|
||||
}
|
||||
SocketMessage::CycleMoveContainerToMonitor(direction) => {
|
||||
let monitor_idx = direction.next_idx(
|
||||
self.focused_monitor_idx(),
|
||||
@@ -555,6 +561,29 @@ impl WindowManager {
|
||||
self.show_border()?;
|
||||
};
|
||||
}
|
||||
SocketMessage::FocusWorkspaceNumbers(workspace_idx) => {
|
||||
// This is to ensure that even on an empty workspace on a secondary monitor, the
|
||||
// secondary monitor where the cursor is focused will be used as the target for
|
||||
// the workspace switch op
|
||||
if let Some(monitor_idx) = self.monitor_idx_from_current_pos() {
|
||||
self.focus_monitor(monitor_idx)?;
|
||||
}
|
||||
|
||||
let focused_monitor_idx = self.focused_monitor_idx();
|
||||
|
||||
for (i, monitor) in self.monitors_mut().iter_mut().enumerate() {
|
||||
if i != focused_monitor_idx {
|
||||
monitor.focus_workspace(workspace_idx)?;
|
||||
monitor.load_focused_workspace(false)?;
|
||||
}
|
||||
}
|
||||
|
||||
self.focus_workspace(workspace_idx)?;
|
||||
|
||||
if BORDER_ENABLED.load(Ordering::SeqCst) {
|
||||
self.show_border()?;
|
||||
};
|
||||
}
|
||||
SocketMessage::FocusMonitorWorkspaceNumber(monitor_idx, workspace_idx) => {
|
||||
self.focus_monitor(monitor_idx)?;
|
||||
self.focus_workspace(workspace_idx)?;
|
||||
@@ -575,7 +604,7 @@ impl WindowManager {
|
||||
tracing::info!(
|
||||
"received stop command, restoring all hidden windows and terminating process"
|
||||
);
|
||||
self.restore_all_windows();
|
||||
self.restore_all_windows()?;
|
||||
|
||||
if WindowsApi::focus_follows_mouse()? {
|
||||
WindowsApi::disable_focus_follows_mouse()?;
|
||||
@@ -656,17 +685,18 @@ impl WindowManager {
|
||||
|
||||
if let Layout::Custom(ref mut custom) = workspace.layout_mut() {
|
||||
if matches!(axis, Axis::Horizontal) {
|
||||
#[allow(clippy::cast_precision_loss)]
|
||||
let percentage = custom
|
||||
.primary_width_percentage()
|
||||
.unwrap_or(100 / custom.len());
|
||||
.unwrap_or(100.0 / (custom.len() as f32));
|
||||
|
||||
if no_layout_rules {
|
||||
match sizing {
|
||||
Sizing::Increase => {
|
||||
custom.set_primary_width_percentage(percentage + 5);
|
||||
custom.set_primary_width_percentage(percentage + 5.0);
|
||||
}
|
||||
Sizing::Decrease => {
|
||||
custom.set_primary_width_percentage(percentage - 5);
|
||||
custom.set_primary_width_percentage(percentage - 5.0);
|
||||
}
|
||||
}
|
||||
} else {
|
||||
@@ -675,10 +705,12 @@ impl WindowManager {
|
||||
if let Layout::Custom(ref mut custom) = rule.1 {
|
||||
match sizing {
|
||||
Sizing::Increase => {
|
||||
custom.set_primary_width_percentage(percentage + 5);
|
||||
custom
|
||||
.set_primary_width_percentage(percentage + 5.0);
|
||||
}
|
||||
Sizing::Decrease => {
|
||||
custom.set_primary_width_percentage(percentage - 5);
|
||||
custom
|
||||
.set_primary_width_percentage(percentage - 5.0);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -686,8 +718,8 @@ impl WindowManager {
|
||||
}
|
||||
}
|
||||
}
|
||||
// Otherwise proceed with the resizing logic for individual window containers in the
|
||||
// assumed BSP layout
|
||||
// Otherwise proceed with the resizing logic for individual window containers in the
|
||||
// assumed BSP layout
|
||||
} else {
|
||||
match axis {
|
||||
Axis::Horizontal => {
|
||||
@@ -771,9 +803,10 @@ impl WindowManager {
|
||||
}
|
||||
}
|
||||
FocusFollowsMouseImplementation::Windows => {
|
||||
if let Some(FocusFollowsMouseImplementation::Komorebi) =
|
||||
self.focus_follows_mouse
|
||||
{
|
||||
if matches!(
|
||||
self.focus_follows_mouse,
|
||||
Some(FocusFollowsMouseImplementation::Komorebi)
|
||||
) {
|
||||
tracing::warn!(
|
||||
"the windows implementation of focus follows mouse cannot be enabled while the komorebi implementation is enabled"
|
||||
);
|
||||
@@ -818,9 +851,10 @@ impl WindowManager {
|
||||
}
|
||||
}
|
||||
FocusFollowsMouseImplementation::Windows => {
|
||||
if let Some(FocusFollowsMouseImplementation::Komorebi) =
|
||||
self.focus_follows_mouse
|
||||
{
|
||||
if matches!(
|
||||
self.focus_follows_mouse,
|
||||
Some(FocusFollowsMouseImplementation::Komorebi)
|
||||
) {
|
||||
tracing::warn!(
|
||||
"the windows implementation of focus follows mouse cannot be toggled while the komorebi implementation is enabled"
|
||||
);
|
||||
@@ -845,6 +879,9 @@ impl WindowManager {
|
||||
SocketMessage::ReloadConfiguration => {
|
||||
Self::reload_configuration();
|
||||
}
|
||||
SocketMessage::ReloadStaticConfiguration(ref pathbuf) => {
|
||||
self.reload_static_configuration(pathbuf)?;
|
||||
}
|
||||
SocketMessage::CompleteConfiguration => {
|
||||
if !INITIAL_CONFIGURATION_LOADED.load(Ordering::SeqCst) {
|
||||
INITIAL_CONFIGURATION_LOADED.store(true, Ordering::SeqCst);
|
||||
@@ -956,7 +993,7 @@ impl WindowManager {
|
||||
}
|
||||
SocketMessage::AddSubscriber(ref subscriber) => {
|
||||
let mut pipes = SUBSCRIPTION_PIPES.lock();
|
||||
let pipe_path = format!(r"\\.\pipe\{}", subscriber);
|
||||
let pipe_path = format!(r"\\.\pipe\{subscriber}");
|
||||
let pipe = connect(&pipe_path).map_err(|_| {
|
||||
anyhow!("the named pipe '{}' has not yet been created; please create it before running this command", pipe_path)
|
||||
})?;
|
||||
@@ -1076,6 +1113,36 @@ impl WindowManager {
|
||||
let mut stream = UnixStream::connect(socket)?;
|
||||
stream.write_all(schema.as_bytes())?;
|
||||
}
|
||||
SocketMessage::StaticConfigSchema => {
|
||||
let socket_message = schema_for!(StaticConfig);
|
||||
let schema = serde_json::to_string_pretty(&socket_message)?;
|
||||
let mut socket = DATA_DIR.clone();
|
||||
socket.push("komorebic.sock");
|
||||
let socket = socket.as_path();
|
||||
|
||||
let mut stream = UnixStream::connect(socket)?;
|
||||
stream.write_all(schema.as_bytes())?;
|
||||
}
|
||||
SocketMessage::GenerateStaticConfig => {
|
||||
let config = serde_json::to_string_pretty(&StaticConfig::from(&*self))?;
|
||||
let mut socket = DATA_DIR.clone();
|
||||
socket.push("komorebic.sock");
|
||||
let socket = socket.as_path();
|
||||
|
||||
let mut stream = UnixStream::connect(socket)?;
|
||||
stream.write_all(config.as_bytes())?;
|
||||
}
|
||||
SocketMessage::RemoveTitleBar(_, ref id) => {
|
||||
let mut identifiers = NO_TITLEBAR.lock();
|
||||
if !identifiers.contains(id) {
|
||||
identifiers.push(id.clone());
|
||||
}
|
||||
}
|
||||
SocketMessage::ToggleTitleBars => {
|
||||
let current = REMOVE_TITLEBARS.load(Ordering::SeqCst);
|
||||
REMOVE_TITLEBARS.store(!current, Ordering::SeqCst);
|
||||
self.update_focused_workspace(false)?;
|
||||
}
|
||||
};
|
||||
|
||||
match message {
|
||||
@@ -1140,6 +1207,7 @@ impl WindowManager {
|
||||
| SocketMessage::FocusWindow(_)
|
||||
| SocketMessage::InvisibleBorders(_)
|
||||
| SocketMessage::WorkAreaOffset(_)
|
||||
| SocketMessage::CycleMoveWindow(_)
|
||||
| SocketMessage::MoveWindow(_) => {
|
||||
let foreground = WindowsApi::foreground_window()?;
|
||||
let foreground_window = Window { hwnd: foreground };
|
||||
@@ -1206,7 +1274,7 @@ impl WindowManager {
|
||||
}
|
||||
|
||||
#[tracing::instrument(skip(self))]
|
||||
fn handle_workspace_rules(
|
||||
pub fn handle_workspace_rules(
|
||||
&mut self,
|
||||
id: &String,
|
||||
monitor_idx: usize,
|
||||
@@ -1273,11 +1341,7 @@ pub fn read_commands_tcp(
|
||||
break;
|
||||
}
|
||||
Ok(size) => {
|
||||
let message = if let Ok(message) =
|
||||
SocketMessage::from_str(&String::from_utf8_lossy(&buf[..size]))
|
||||
{
|
||||
message
|
||||
} else {
|
||||
let Ok(message) = SocketMessage::from_str(&String::from_utf8_lossy(&buf[..size])) else {
|
||||
tracing::warn!("client sent an invalid message, disconnecting: {addr}");
|
||||
let mut connections = TCP_CONNECTIONS.lock();
|
||||
connections.remove(addr);
|
||||
|
||||
@@ -18,7 +18,10 @@ pub fn listen_for_movements(wm: Arc<Mutex<WindowManager>>) {
|
||||
|
||||
loop {
|
||||
let focus_follows_mouse = wm.lock().focus_follows_mouse;
|
||||
if let Some(FocusFollowsMouseImplementation::Komorebi) = focus_follows_mouse {
|
||||
if matches!(
|
||||
focus_follows_mouse,
|
||||
Some(FocusFollowsMouseImplementation::Komorebi)
|
||||
) {
|
||||
match receiver.next_event() {
|
||||
// Don't want to send any raise events while we are dragging or resizing
|
||||
Event::MouseButton { action, .. } => match action {
|
||||
|
||||
754
komorebi/src/static_config.rs
Normal file
754
komorebi/src/static_config.rs
Normal file
@@ -0,0 +1,754 @@
|
||||
use crate::border::Border;
|
||||
use crate::current_virtual_desktop;
|
||||
use crate::monitor::Monitor;
|
||||
use crate::ring::Ring;
|
||||
use crate::window_manager::WindowManager;
|
||||
use crate::window_manager_event::WindowManagerEvent;
|
||||
use crate::workspace::Workspace;
|
||||
use crate::ALT_FOCUS_HACK;
|
||||
use crate::BORDER_COLOUR_CURRENT;
|
||||
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_OVERFLOW_IDENTIFIERS;
|
||||
use crate::BORDER_WIDTH;
|
||||
use crate::DATA_DIR;
|
||||
use crate::DEFAULT_CONTAINER_PADDING;
|
||||
use crate::DEFAULT_WORKSPACE_PADDING;
|
||||
use crate::FLOAT_IDENTIFIERS;
|
||||
use crate::HIDING_BEHAVIOUR;
|
||||
use crate::LAYERED_WHITELIST;
|
||||
use crate::MANAGE_IDENTIFIERS;
|
||||
use crate::OBJECT_NAME_CHANGE_ON_LAUNCH;
|
||||
use crate::TRAY_AND_MULTI_WINDOW_IDENTIFIERS;
|
||||
use crate::WORKSPACE_RULES;
|
||||
use color_eyre::Result;
|
||||
use crossbeam_channel::Receiver;
|
||||
use dirs::home_dir;
|
||||
use hotwatch::notify::DebouncedEvent;
|
||||
use hotwatch::Hotwatch;
|
||||
use komorebi_core::config_generation::ApplicationConfigurationGenerator;
|
||||
use komorebi_core::config_generation::ApplicationOptions;
|
||||
use komorebi_core::config_generation::IdWithIdentifier;
|
||||
use komorebi_core::ApplicationIdentifier;
|
||||
use komorebi_core::DefaultLayout;
|
||||
use komorebi_core::FocusFollowsMouseImplementation;
|
||||
use komorebi_core::HidingBehaviour;
|
||||
use komorebi_core::Layout;
|
||||
use komorebi_core::MoveBehaviour;
|
||||
use komorebi_core::OperationBehaviour;
|
||||
use komorebi_core::Rect;
|
||||
use komorebi_core::SocketMessage;
|
||||
use komorebi_core::WindowContainerBehaviour;
|
||||
use parking_lot::Mutex;
|
||||
use schemars::JsonSchema;
|
||||
use serde::Deserialize;
|
||||
use serde::Serialize;
|
||||
use std::collections::HashMap;
|
||||
use std::collections::HashSet;
|
||||
use std::io::ErrorKind;
|
||||
use std::io::Write;
|
||||
use std::path::PathBuf;
|
||||
use std::sync::atomic::Ordering;
|
||||
use std::sync::Arc;
|
||||
use uds_windows::UnixListener;
|
||||
use uds_windows::UnixStream;
|
||||
|
||||
#[derive(Debug, Serialize, Deserialize, JsonSchema)]
|
||||
pub struct Rgb {
|
||||
/// Red
|
||||
pub r: u32,
|
||||
/// Green
|
||||
pub g: u32,
|
||||
/// Blue
|
||||
pub b: u32,
|
||||
}
|
||||
|
||||
#[derive(Debug, Serialize, Deserialize, JsonSchema)]
|
||||
pub struct ActiveWindowBorderColours {
|
||||
/// Border colour when the container contains a single window
|
||||
pub single: Rgb,
|
||||
/// Border colour when the container contains multiple windows
|
||||
pub stack: Rgb,
|
||||
/// Border colour when the container is in monocle mode
|
||||
pub monocle: Rgb,
|
||||
}
|
||||
|
||||
#[derive(Debug, Serialize, Deserialize, JsonSchema)]
|
||||
pub struct WorkspaceConfig {
|
||||
/// Name
|
||||
pub name: String,
|
||||
/// Layout (default: BSP)
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub layout: Option<DefaultLayout>,
|
||||
/// Custom Layout (default: None)
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub custom_layout: Option<PathBuf>,
|
||||
/// Layout rules (default: None)
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub layout_rules: Option<HashMap<usize, DefaultLayout>>,
|
||||
/// Layout rules (default: None)
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub custom_layout_rules: Option<HashMap<usize, PathBuf>>,
|
||||
/// Container padding (default: global)
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub container_padding: Option<i32>,
|
||||
/// Container padding (default: global)
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub workspace_padding: Option<i32>,
|
||||
/// Initial workspace application rules
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub initial_workspace_rules: Option<Vec<IdWithIdentifier>>,
|
||||
/// Permanent workspace application rules
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub workspace_rules: Option<Vec<IdWithIdentifier>>,
|
||||
}
|
||||
|
||||
impl From<&Workspace> for WorkspaceConfig {
|
||||
fn from(value: &Workspace) -> Self {
|
||||
let mut layout_rules = HashMap::new();
|
||||
for (threshold, layout) in value.layout_rules() {
|
||||
match layout {
|
||||
Layout::Default(value) => {
|
||||
layout_rules.insert(*threshold, *value);
|
||||
}
|
||||
Layout::Custom(_) => {}
|
||||
}
|
||||
}
|
||||
|
||||
let workspace_rules = WORKSPACE_RULES.lock();
|
||||
let mut initial_ws_rules = vec![];
|
||||
let mut ws_rules = vec![];
|
||||
|
||||
for (identifier, (_, _, is_initial)) in &*workspace_rules {
|
||||
if identifier.ends_with("exe") {
|
||||
let rule = IdWithIdentifier {
|
||||
kind: ApplicationIdentifier::Exe,
|
||||
id: identifier.clone(),
|
||||
};
|
||||
|
||||
if *is_initial {
|
||||
initial_ws_rules.push(rule);
|
||||
} else {
|
||||
ws_rules.push(rule);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
let initial_ws_rules = if initial_ws_rules.is_empty() {
|
||||
None
|
||||
} else {
|
||||
Option::from(initial_ws_rules)
|
||||
};
|
||||
let ws_rules = if ws_rules.is_empty() {
|
||||
None
|
||||
} else {
|
||||
Option::from(ws_rules)
|
||||
};
|
||||
|
||||
let default_container_padding = DEFAULT_CONTAINER_PADDING.load(Ordering::SeqCst);
|
||||
let default_workspace_padding = DEFAULT_WORKSPACE_PADDING.load(Ordering::SeqCst);
|
||||
|
||||
let container_padding = value.container_padding().and_then(|container_padding| {
|
||||
if container_padding == default_container_padding {
|
||||
None
|
||||
} else {
|
||||
Option::from(container_padding)
|
||||
}
|
||||
});
|
||||
|
||||
let workspace_padding = value.workspace_padding().and_then(|workspace_padding| {
|
||||
if workspace_padding == default_workspace_padding {
|
||||
None
|
||||
} else {
|
||||
Option::from(workspace_padding)
|
||||
}
|
||||
});
|
||||
|
||||
Self {
|
||||
name: value
|
||||
.name()
|
||||
.clone()
|
||||
.unwrap_or_else(|| String::from("unnamed")),
|
||||
layout: match value.layout() {
|
||||
Layout::Default(layout) => Option::from(*layout),
|
||||
// TODO: figure out how we might resolve file references in the future
|
||||
Layout::Custom(_) => None,
|
||||
},
|
||||
custom_layout: None,
|
||||
layout_rules: Option::from(layout_rules),
|
||||
// TODO: figure out how we might resolve file references in the future
|
||||
custom_layout_rules: None,
|
||||
container_padding,
|
||||
workspace_padding,
|
||||
initial_workspace_rules: initial_ws_rules,
|
||||
workspace_rules: ws_rules,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Serialize, Deserialize, JsonSchema)]
|
||||
pub struct MonitorConfig {
|
||||
/// Workspace configurations
|
||||
pub workspaces: Vec<WorkspaceConfig>,
|
||||
/// Monitor-specific work area offset (default: None)
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub work_area_offset: Option<Rect>,
|
||||
}
|
||||
|
||||
impl From<&Monitor> for MonitorConfig {
|
||||
fn from(value: &Monitor) -> Self {
|
||||
let mut workspaces = vec![];
|
||||
for w in value.workspaces() {
|
||||
workspaces.push(WorkspaceConfig::from(w));
|
||||
}
|
||||
|
||||
Self {
|
||||
workspaces,
|
||||
work_area_offset: value.work_area_offset(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Serialize, Deserialize, JsonSchema)]
|
||||
pub struct StaticConfig {
|
||||
/// Dimensions of Windows' own invisible borders; don't set these yourself unless you are told to
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub invisible_borders: Option<Rect>,
|
||||
/// Delta to resize windows by (default 50)
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub resize_delta: Option<i32>,
|
||||
/// Determine what happens when a new window is opened (default: Create)
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub window_container_behaviour: Option<WindowContainerBehaviour>,
|
||||
/// Determine what happens when a window is moved across a monitor boundary (default: Swap)
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub cross_monitor_move_behaviour: Option<MoveBehaviour>,
|
||||
/// Determine what happens when commands are sent while an unmanaged window is in the foreground (default: Op)
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub unmanaged_window_operation_behaviour: Option<OperationBehaviour>,
|
||||
/// Determine focus follows mouse implementation (default: None)
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub focus_follows_mouse: Option<FocusFollowsMouseImplementation>,
|
||||
/// Enable or disable mouse follows focus (default: true)
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub mouse_follows_focus: Option<bool>,
|
||||
/// Path to applications.yaml from komorebi-application-specific-configurations (default: None)
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub app_specific_configuration_path: Option<PathBuf>,
|
||||
/// Width of the active window border (default: 20)
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub border_width: Option<i32>,
|
||||
/// Offset of the active window border (default: None)
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub border_offset: Option<Rect>,
|
||||
/// Display an active window border (default: false)
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub active_window_border: Option<bool>,
|
||||
/// Active window border colours for different container types
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub active_window_border_colours: Option<ActiveWindowBorderColours>,
|
||||
/// Global default workspace padding (default: 10)
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub default_workspace_padding: Option<i32>,
|
||||
/// Global default container padding (default: 10)
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub default_container_padding: Option<i32>,
|
||||
/// Monitor and workspace configurations
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub monitors: Option<Vec<MonitorConfig>>,
|
||||
/// Always send the ALT key when using focus commands (default: false)
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub alt_focus_hack: Option<bool>,
|
||||
/// Which Windows signal to use when hiding windows (default: minimize)
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub window_hiding_behaviour: Option<HidingBehaviour>,
|
||||
/// Global work area (space used for tiling) offset (default: None)
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub global_work_area_offset: Option<Rect>,
|
||||
/// Individual window floating rules
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub float_rules: Option<Vec<IdWithIdentifier>>,
|
||||
/// Individual window force-manage rules
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub manage_rules: Option<Vec<IdWithIdentifier>>,
|
||||
/// Identify border overflow applications
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub border_overflow_applications: Option<Vec<IdWithIdentifier>>,
|
||||
/// Identify tray and multi-window applications
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub tray_and_multi_window_applications: Option<Vec<IdWithIdentifier>>,
|
||||
/// Identify applications that have the WS_EX_LAYERED extended window style
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub layered_applications: Option<Vec<IdWithIdentifier>>,
|
||||
/// Identify applications that send EVENT_OBJECT_NAMECHANGE on launch (very rare)
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub object_name_change_applications: Option<Vec<IdWithIdentifier>>,
|
||||
}
|
||||
|
||||
impl From<&WindowManager> for StaticConfig {
|
||||
fn from(value: &WindowManager) -> Self {
|
||||
let default_invisible_borders = Rect {
|
||||
left: 7,
|
||||
top: 0,
|
||||
right: 14,
|
||||
bottom: 7,
|
||||
};
|
||||
|
||||
let mut monitors = vec![];
|
||||
for m in value.monitors() {
|
||||
monitors.push(MonitorConfig::from(m));
|
||||
}
|
||||
|
||||
let mut to_remove = vec![];
|
||||
|
||||
let workspace_rules = WORKSPACE_RULES.lock();
|
||||
for (m_idx, m) in monitors.iter().enumerate() {
|
||||
for (w_idx, w) in m.workspaces.iter().enumerate() {
|
||||
if let Some(rules) = &w.initial_workspace_rules {
|
||||
for iwsr in rules {
|
||||
for (identifier, (monitor_idx, workspace_idx, _)) in &*workspace_rules {
|
||||
if iwsr.id.eq(identifier)
|
||||
&& (*monitor_idx != m_idx || *workspace_idx != w_idx)
|
||||
{
|
||||
to_remove.push((m_idx, w_idx, iwsr.id.clone()));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if let Some(rules) = &w.workspace_rules {
|
||||
for wsr in rules {
|
||||
for (identifier, (monitor_idx, workspace_idx, _)) in &*workspace_rules {
|
||||
if wsr.id.eq(identifier)
|
||||
&& (*monitor_idx != m_idx || *workspace_idx != w_idx)
|
||||
{
|
||||
to_remove.push((m_idx, w_idx, wsr.id.clone()));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
for (m_idx, w_idx, id) in to_remove {
|
||||
if let Some(monitor) = monitors.get_mut(m_idx) {
|
||||
if let Some(workspace) = monitor.workspaces.get_mut(w_idx) {
|
||||
if let Some(rules) = &mut workspace.workspace_rules {
|
||||
rules.retain(|r| r.id != id);
|
||||
}
|
||||
|
||||
if let Some(rules) = &mut workspace.initial_workspace_rules {
|
||||
rules.retain(|r| r.id != id);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Self {
|
||||
invisible_borders: if value.invisible_borders == default_invisible_borders {
|
||||
None
|
||||
} else {
|
||||
Option::from(value.invisible_borders)
|
||||
},
|
||||
resize_delta: Option::from(value.resize_delta),
|
||||
window_container_behaviour: Option::from(value.window_container_behaviour),
|
||||
cross_monitor_move_behaviour: Option::from(value.cross_monitor_move_behaviour),
|
||||
unmanaged_window_operation_behaviour: Option::from(
|
||||
value.unmanaged_window_operation_behaviour,
|
||||
),
|
||||
focus_follows_mouse: value.focus_follows_mouse,
|
||||
mouse_follows_focus: Option::from(value.mouse_follows_focus),
|
||||
app_specific_configuration_path: None,
|
||||
border_width: Option::from(BORDER_WIDTH.load(Ordering::SeqCst)),
|
||||
border_offset: *BORDER_OFFSET.lock(),
|
||||
active_window_border: Option::from(BORDER_ENABLED.load(Ordering::SeqCst)),
|
||||
active_window_border_colours: None,
|
||||
default_workspace_padding: Option::from(
|
||||
DEFAULT_WORKSPACE_PADDING.load(Ordering::SeqCst),
|
||||
),
|
||||
default_container_padding: Option::from(
|
||||
DEFAULT_CONTAINER_PADDING.load(Ordering::SeqCst),
|
||||
),
|
||||
monitors: Option::from(monitors),
|
||||
alt_focus_hack: Option::from(ALT_FOCUS_HACK.load(Ordering::SeqCst)),
|
||||
window_hiding_behaviour: Option::from(*HIDING_BEHAVIOUR.lock()),
|
||||
global_work_area_offset: value.work_area_offset,
|
||||
float_rules: None,
|
||||
manage_rules: None,
|
||||
border_overflow_applications: None,
|
||||
tray_and_multi_window_applications: None,
|
||||
layered_applications: None,
|
||||
object_name_change_applications: None,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl StaticConfig {
|
||||
#[allow(clippy::cognitive_complexity, clippy::too_many_lines)]
|
||||
fn apply_globals(&self) -> Result<()> {
|
||||
if let Some(behaviour) = self.window_hiding_behaviour {
|
||||
let mut window_hiding_behaviour = HIDING_BEHAVIOUR.lock();
|
||||
*window_hiding_behaviour = behaviour;
|
||||
}
|
||||
|
||||
if let Some(hack) = self.alt_focus_hack {
|
||||
ALT_FOCUS_HACK.store(hack, Ordering::SeqCst);
|
||||
}
|
||||
|
||||
if let Some(container) = self.default_container_padding {
|
||||
DEFAULT_CONTAINER_PADDING.store(container, Ordering::SeqCst);
|
||||
}
|
||||
|
||||
if let Some(workspace) = self.default_workspace_padding {
|
||||
DEFAULT_WORKSPACE_PADDING.store(workspace, Ordering::SeqCst);
|
||||
}
|
||||
|
||||
if let Some(width) = self.border_width {
|
||||
BORDER_WIDTH.store(width, Ordering::SeqCst);
|
||||
}
|
||||
|
||||
if let Some(offset) = self.border_offset {
|
||||
let mut border_offset = BORDER_OFFSET.lock();
|
||||
*border_offset = Some(offset);
|
||||
}
|
||||
|
||||
if let Some(colours) = &self.active_window_border_colours {
|
||||
BORDER_COLOUR_SINGLE.store(
|
||||
colours.single.r | (colours.single.g << 8) | (colours.single.b << 16),
|
||||
Ordering::SeqCst,
|
||||
);
|
||||
BORDER_COLOUR_CURRENT.store(
|
||||
colours.single.r | (colours.single.g << 8) | (colours.single.b << 16),
|
||||
Ordering::SeqCst,
|
||||
);
|
||||
BORDER_COLOUR_STACK.store(
|
||||
colours.stack.r | (colours.stack.g << 8) | (colours.stack.b << 16),
|
||||
Ordering::SeqCst,
|
||||
);
|
||||
BORDER_COLOUR_MONOCLE.store(
|
||||
colours.monocle.r | (colours.monocle.g << 8) | (colours.monocle.b << 16),
|
||||
Ordering::SeqCst,
|
||||
);
|
||||
}
|
||||
|
||||
let mut float_identifiers = FLOAT_IDENTIFIERS.lock();
|
||||
let mut manage_identifiers = MANAGE_IDENTIFIERS.lock();
|
||||
let mut tray_and_multi_window_identifiers = TRAY_AND_MULTI_WINDOW_IDENTIFIERS.lock();
|
||||
let mut border_overflow_identifiers = BORDER_OVERFLOW_IDENTIFIERS.lock();
|
||||
let mut object_name_change_identifiers = OBJECT_NAME_CHANGE_ON_LAUNCH.lock();
|
||||
let mut layered_identifiers = LAYERED_WHITELIST.lock();
|
||||
|
||||
if let Some(float) = &self.float_rules {
|
||||
for identifier in float {
|
||||
if !float_identifiers.contains(&identifier.id) {
|
||||
float_identifiers.push(identifier.id.clone());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if let Some(float) = &self.manage_rules {
|
||||
for identifier in float {
|
||||
if !manage_identifiers.contains(&identifier.id) {
|
||||
manage_identifiers.push(identifier.id.clone());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if let Some(identifiers) = &self.object_name_change_applications {
|
||||
for identifier in identifiers {
|
||||
if !object_name_change_identifiers.contains(&identifier.id) {
|
||||
object_name_change_identifiers.push(identifier.id.clone());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if let Some(identifiers) = &self.layered_applications {
|
||||
for identifier in identifiers {
|
||||
if !layered_identifiers.contains(&identifier.id) {
|
||||
layered_identifiers.push(identifier.id.clone());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if let Some(identifiers) = &self.border_overflow_applications {
|
||||
for identifier in identifiers {
|
||||
if !border_overflow_identifiers.contains(&identifier.id) {
|
||||
border_overflow_identifiers.push(identifier.id.clone());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if let Some(identifiers) = &self.tray_and_multi_window_applications {
|
||||
for identifier in identifiers {
|
||||
if !tray_and_multi_window_identifiers.contains(&identifier.id) {
|
||||
tray_and_multi_window_identifiers.push(identifier.id.clone());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if let Some(path) = &self.app_specific_configuration_path {
|
||||
let stringified = path.to_string_lossy();
|
||||
let stringified = stringified.replace(
|
||||
"$Env:USERPROFILE",
|
||||
&home_dir().expect("no home dir").to_string_lossy(),
|
||||
);
|
||||
|
||||
let content = std::fs::read_to_string(stringified)?;
|
||||
let asc = ApplicationConfigurationGenerator::load(&content)?;
|
||||
|
||||
for entry in asc {
|
||||
if let Some(float) = entry.float_identifiers {
|
||||
for f in float {
|
||||
if !float_identifiers.contains(&f.id) {
|
||||
float_identifiers.push(f.id.clone());
|
||||
}
|
||||
}
|
||||
}
|
||||
if let Some(options) = entry.options {
|
||||
for o in options {
|
||||
match o {
|
||||
ApplicationOptions::ObjectNameChange => {
|
||||
if !object_name_change_identifiers.contains(&entry.identifier.id) {
|
||||
object_name_change_identifiers
|
||||
.push(entry.identifier.id.clone());
|
||||
}
|
||||
}
|
||||
ApplicationOptions::Layered => {
|
||||
if !layered_identifiers.contains(&entry.identifier.id) {
|
||||
layered_identifiers.push(entry.identifier.id.clone());
|
||||
}
|
||||
}
|
||||
ApplicationOptions::BorderOverflow => {
|
||||
if !border_overflow_identifiers.contains(&entry.identifier.id) {
|
||||
border_overflow_identifiers.push(entry.identifier.id.clone());
|
||||
}
|
||||
}
|
||||
ApplicationOptions::TrayAndMultiWindow => {
|
||||
if !tray_and_multi_window_identifiers.contains(&entry.identifier.id)
|
||||
{
|
||||
tray_and_multi_window_identifiers
|
||||
.push(entry.identifier.id.clone());
|
||||
}
|
||||
}
|
||||
ApplicationOptions::Force => {
|
||||
if !manage_identifiers.contains(&entry.identifier.id) {
|
||||
manage_identifiers.push(entry.identifier.id.clone());
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[allow(clippy::too_many_lines)]
|
||||
pub fn preload(
|
||||
path: &PathBuf,
|
||||
incoming: Arc<Mutex<Receiver<WindowManagerEvent>>>,
|
||||
) -> Result<WindowManager> {
|
||||
let content = std::fs::read_to_string(path)?;
|
||||
let value: Self = serde_json::from_str(&content)?;
|
||||
value.apply_globals()?;
|
||||
|
||||
let socket = DATA_DIR.join("komorebi.sock");
|
||||
|
||||
match std::fs::remove_file(&socket) {
|
||||
Ok(_) => {}
|
||||
Err(error) => match error.kind() {
|
||||
// Doing this because ::exists() doesn't work reliably on Windows via IntelliJ
|
||||
ErrorKind::NotFound => {}
|
||||
_ => {
|
||||
return Err(error.into());
|
||||
}
|
||||
},
|
||||
};
|
||||
|
||||
let listener = UnixListener::bind(&socket)?;
|
||||
|
||||
let mut wm = WindowManager {
|
||||
monitors: Ring::default(),
|
||||
monitor_cache: HashMap::new(),
|
||||
incoming_events: incoming,
|
||||
command_listener: listener,
|
||||
is_paused: false,
|
||||
invisible_borders: value.invisible_borders.unwrap_or(Rect {
|
||||
left: 7,
|
||||
top: 0,
|
||||
right: 14,
|
||||
bottom: 7,
|
||||
}),
|
||||
virtual_desktop_id: current_virtual_desktop(),
|
||||
work_area_offset: value.global_work_area_offset,
|
||||
window_container_behaviour: value
|
||||
.window_container_behaviour
|
||||
.unwrap_or(WindowContainerBehaviour::Create),
|
||||
cross_monitor_move_behaviour: value
|
||||
.cross_monitor_move_behaviour
|
||||
.unwrap_or(MoveBehaviour::Swap),
|
||||
unmanaged_window_operation_behaviour: value
|
||||
.unmanaged_window_operation_behaviour
|
||||
.unwrap_or(OperationBehaviour::Op),
|
||||
resize_delta: value.resize_delta.unwrap_or(50),
|
||||
focus_follows_mouse: value.focus_follows_mouse,
|
||||
mouse_follows_focus: value.mouse_follows_focus.unwrap_or(true),
|
||||
hotwatch: Hotwatch::new()?,
|
||||
has_pending_raise_op: false,
|
||||
pending_move_op: None,
|
||||
already_moved_window_handles: Arc::new(Mutex::new(HashSet::new())),
|
||||
};
|
||||
|
||||
let bytes = SocketMessage::ReloadStaticConfiguration(path.clone()).as_bytes()?;
|
||||
|
||||
wm.hotwatch.watch(path, move |event| match event {
|
||||
// Editing in Notepad sends a NoticeWrite while editing in (Neo)Vim sends
|
||||
// a NoticeRemove, presumably because of the use of swap files?
|
||||
DebouncedEvent::NoticeWrite(_) | DebouncedEvent::NoticeRemove(_) => {
|
||||
let socket = DATA_DIR.join("komorebi.sock");
|
||||
let mut stream =
|
||||
UnixStream::connect(socket).expect("could not connect to komorebi.sock");
|
||||
stream
|
||||
.write_all(&bytes)
|
||||
.expect("could not write to komorebi.sock");
|
||||
}
|
||||
_ => {}
|
||||
})?;
|
||||
|
||||
Ok(wm)
|
||||
}
|
||||
|
||||
pub fn postload(path: &PathBuf, wm: &Arc<Mutex<WindowManager>>) -> Result<()> {
|
||||
let content = std::fs::read_to_string(path)?;
|
||||
let value: Self = serde_json::from_str(&content)?;
|
||||
let mut wm = wm.lock();
|
||||
|
||||
if let Some(monitors) = value.monitors {
|
||||
for (i, monitor) in monitors.iter().enumerate() {
|
||||
if let Some(m) = wm.monitors_mut().get_mut(i) {
|
||||
m.ensure_workspace_count(monitor.workspaces.len());
|
||||
m.set_work_area_offset(monitor.work_area_offset);
|
||||
|
||||
for (j, ws) in m.workspaces_mut().iter_mut().enumerate() {
|
||||
ws.load_static_config(
|
||||
monitor
|
||||
.workspaces
|
||||
.get(j)
|
||||
.expect("no static workspace config"),
|
||||
)?;
|
||||
}
|
||||
}
|
||||
|
||||
for (j, ws) in monitor.workspaces.iter().enumerate() {
|
||||
if let Some(rules) = &ws.workspace_rules {
|
||||
for r in rules {
|
||||
wm.handle_workspace_rules(&r.id, i, j, false)?;
|
||||
}
|
||||
}
|
||||
|
||||
if let Some(rules) = &ws.initial_workspace_rules {
|
||||
for r in rules {
|
||||
wm.handle_workspace_rules(&r.id, i, j, true)?;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if value.active_window_border == Some(true) {
|
||||
if BORDER_HWND.load(Ordering::SeqCst) == 0 {
|
||||
Border::create("komorebi-border-window")?;
|
||||
}
|
||||
|
||||
BORDER_ENABLED.store(true, Ordering::SeqCst);
|
||||
wm.show_border()?;
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn reload(path: &PathBuf, wm: &mut WindowManager) -> Result<()> {
|
||||
let content = std::fs::read_to_string(path)?;
|
||||
let value: Self = serde_json::from_str(&content)?;
|
||||
|
||||
value.apply_globals()?;
|
||||
|
||||
if let Some(monitors) = value.monitors {
|
||||
for (i, monitor) in monitors.iter().enumerate() {
|
||||
if let Some(m) = wm.monitors_mut().get_mut(i) {
|
||||
m.ensure_workspace_count(monitor.workspaces.len());
|
||||
m.set_work_area_offset(monitor.work_area_offset);
|
||||
|
||||
for (j, ws) in m.workspaces_mut().iter_mut().enumerate() {
|
||||
ws.load_static_config(
|
||||
monitor
|
||||
.workspaces
|
||||
.get(j)
|
||||
.expect("no static workspace config"),
|
||||
)?;
|
||||
}
|
||||
}
|
||||
|
||||
for (j, ws) in monitor.workspaces.iter().enumerate() {
|
||||
if let Some(rules) = &ws.workspace_rules {
|
||||
for r in rules {
|
||||
wm.handle_workspace_rules(&r.id, i, j, false)?;
|
||||
}
|
||||
}
|
||||
|
||||
if let Some(rules) = &ws.initial_workspace_rules {
|
||||
for r in rules {
|
||||
wm.handle_workspace_rules(&r.id, i, j, true)?;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if value.active_window_border == Some(true) {
|
||||
if BORDER_HWND.load(Ordering::SeqCst) == 0 {
|
||||
Border::create("komorebi-border-window")?;
|
||||
}
|
||||
|
||||
BORDER_ENABLED.store(true, Ordering::SeqCst);
|
||||
wm.show_border()?;
|
||||
} else {
|
||||
BORDER_ENABLED.store(false, Ordering::SeqCst);
|
||||
wm.hide_border()?;
|
||||
}
|
||||
|
||||
if let Some(val) = value.invisible_borders {
|
||||
wm.invisible_borders = val;
|
||||
}
|
||||
|
||||
if let Some(val) = value.window_container_behaviour {
|
||||
wm.window_container_behaviour = val;
|
||||
}
|
||||
|
||||
if let Some(val) = value.cross_monitor_move_behaviour {
|
||||
wm.cross_monitor_move_behaviour = val;
|
||||
}
|
||||
|
||||
if let Some(val) = value.unmanaged_window_operation_behaviour {
|
||||
wm.unmanaged_window_operation_behaviour = val;
|
||||
}
|
||||
|
||||
if let Some(val) = value.resize_delta {
|
||||
wm.resize_delta = val;
|
||||
}
|
||||
|
||||
if let Some(val) = value.mouse_follows_focus {
|
||||
wm.mouse_follows_focus = val;
|
||||
}
|
||||
|
||||
wm.work_area_offset = value.global_work_area_offset;
|
||||
wm.focus_follows_mouse = value.focus_follows_mouse;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
@@ -17,7 +17,6 @@ use winput::press;
|
||||
use winput::release;
|
||||
use winput::Vk;
|
||||
|
||||
use komorebi_core::ApplicationIdentifier;
|
||||
use komorebi_core::HidingBehaviour;
|
||||
use komorebi_core::Rect;
|
||||
|
||||
@@ -32,6 +31,8 @@ use crate::HIDDEN_HWNDS;
|
||||
use crate::HIDING_BEHAVIOUR;
|
||||
use crate::LAYERED_WHITELIST;
|
||||
use crate::MANAGE_IDENTIFIERS;
|
||||
use crate::NO_TITLEBAR;
|
||||
use crate::PERMAIGNORE_CLASSES;
|
||||
use crate::WSL2_UI_PROCESSES;
|
||||
|
||||
#[derive(Debug, Clone, Copy, JsonSchema)]
|
||||
@@ -44,20 +45,20 @@ impl Display for Window {
|
||||
let mut display = format!("(hwnd: {}", self.hwnd);
|
||||
|
||||
if let Ok(title) = self.title() {
|
||||
write!(display, ", title: {}", title)?;
|
||||
write!(display, ", title: {title}")?;
|
||||
}
|
||||
|
||||
if let Ok(exe) = self.exe() {
|
||||
write!(display, ", exe: {}", exe)?;
|
||||
write!(display, ", exe: {exe}")?;
|
||||
}
|
||||
|
||||
if let Ok(class) = self.class() {
|
||||
write!(display, ", class: {}", class)?;
|
||||
write!(display, ", class: {class}")?;
|
||||
}
|
||||
|
||||
write!(display, ")")?;
|
||||
|
||||
write!(f, "{}", display)
|
||||
write!(f, "{display}")
|
||||
}
|
||||
}
|
||||
|
||||
@@ -346,7 +347,7 @@ impl Window {
|
||||
pub fn transparent(self) -> Result<()> {
|
||||
let mut ex_style = self.ex_style()?;
|
||||
ex_style.insert(ExtendedWindowStyle::LAYERED);
|
||||
self.update_ex_style(ex_style)?;
|
||||
self.update_ex_style(&ex_style)?;
|
||||
WindowsApi::set_transparent(self.hwnd());
|
||||
Ok(())
|
||||
}
|
||||
@@ -354,15 +355,15 @@ impl Window {
|
||||
pub fn opaque(self) -> Result<()> {
|
||||
let mut ex_style = self.ex_style()?;
|
||||
ex_style.remove(ExtendedWindowStyle::LAYERED);
|
||||
self.update_ex_style(ex_style)
|
||||
self.update_ex_style(&ex_style)
|
||||
}
|
||||
|
||||
#[allow(dead_code)]
|
||||
pub fn update_style(self, style: WindowStyle) -> Result<()> {
|
||||
pub fn update_style(self, style: &WindowStyle) -> Result<()> {
|
||||
WindowsApi::update_style(self.hwnd(), isize::try_from(style.bits())?)
|
||||
}
|
||||
|
||||
pub fn update_ex_style(self, style: ExtendedWindowStyle) -> Result<()> {
|
||||
pub fn update_ex_style(self, style: &ExtendedWindowStyle) -> Result<()> {
|
||||
WindowsApi::update_ex_style(self.hwnd(), isize::try_from(style.bits())?)
|
||||
}
|
||||
|
||||
@@ -382,7 +383,10 @@ impl Window {
|
||||
|
||||
pub fn exe(self) -> Result<String> {
|
||||
let (process_id, _) = WindowsApi::window_thread_process_id(self.hwnd());
|
||||
WindowsApi::exe(WindowsApi::process_handle(process_id)?)
|
||||
let handle = WindowsApi::process_handle(process_id)?;
|
||||
let exe = WindowsApi::exe(handle);
|
||||
WindowsApi::close_process(handle)?;
|
||||
exe
|
||||
}
|
||||
|
||||
pub fn class(self) -> Result<String> {
|
||||
@@ -397,6 +401,20 @@ impl Window {
|
||||
WindowsApi::is_window(self.hwnd())
|
||||
}
|
||||
|
||||
pub fn remove_title_bar(self) -> Result<()> {
|
||||
let mut style = self.style()?;
|
||||
style.remove(WindowStyle::CAPTION);
|
||||
style.remove(WindowStyle::THICKFRAME);
|
||||
self.update_style(&style)
|
||||
}
|
||||
|
||||
pub fn add_title_bar(self) -> Result<()> {
|
||||
let mut style = self.style()?;
|
||||
style.insert(WindowStyle::CAPTION);
|
||||
style.insert(WindowStyle::THICKFRAME);
|
||||
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 {
|
||||
@@ -427,7 +445,7 @@ impl Window {
|
||||
// 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)) = (self.title(), self.exe(), self.class()) {
|
||||
return Ok(window_is_eligible(&title, &exe_name, &class, self.style()?, self.ex_style()?, event));
|
||||
return Ok(window_is_eligible(&title, &exe_name, &class, &self.style()?, &self.ex_style()?, event));
|
||||
}
|
||||
}
|
||||
_ => {}
|
||||
@@ -441,47 +459,41 @@ fn window_is_eligible(
|
||||
title: &String,
|
||||
exe_name: &String,
|
||||
class: &String,
|
||||
style: WindowStyle,
|
||||
ex_style: ExtendedWindowStyle,
|
||||
style: &WindowStyle,
|
||||
ex_style: &ExtendedWindowStyle,
|
||||
event: Option<WindowManagerEvent>,
|
||||
) -> bool {
|
||||
{
|
||||
let permaignore_classes = PERMAIGNORE_CLASSES.lock();
|
||||
if permaignore_classes.contains(class) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
let mut should_float = false;
|
||||
let mut matched_identifier = None;
|
||||
|
||||
{
|
||||
let float_identifiers = FLOAT_IDENTIFIERS.lock();
|
||||
for identifier in float_identifiers.iter() {
|
||||
if title.starts_with(identifier) || title.ends_with(identifier) {
|
||||
should_float = true;
|
||||
matched_identifier = Option::from(ApplicationIdentifier::Title);
|
||||
}
|
||||
|
||||
if class.starts_with(identifier) || class.ends_with(identifier) {
|
||||
should_float = true;
|
||||
matched_identifier = Option::from(ApplicationIdentifier::Class);
|
||||
}
|
||||
|
||||
if identifier == exe_name {
|
||||
should_float = true;
|
||||
matched_identifier = Option::from(ApplicationIdentifier::Exe);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
let managed_override = {
|
||||
let manage_identifiers = MANAGE_IDENTIFIERS.lock();
|
||||
matched_identifier.map_or_else(
|
||||
|| {
|
||||
manage_identifiers.contains(exe_name)
|
||||
|| manage_identifiers.contains(class)
|
||||
|| manage_identifiers.contains(title)
|
||||
},
|
||||
|matched_identifier| match matched_identifier {
|
||||
ApplicationIdentifier::Exe => manage_identifiers.contains(exe_name),
|
||||
ApplicationIdentifier::Class => manage_identifiers.contains(class),
|
||||
ApplicationIdentifier::Title => manage_identifiers.contains(title),
|
||||
},
|
||||
)
|
||||
manage_identifiers.contains(exe_name)
|
||||
|| manage_identifiers.contains(class)
|
||||
|| manage_identifiers.contains(title)
|
||||
};
|
||||
|
||||
if should_float && !managed_override {
|
||||
@@ -503,7 +515,12 @@ fn window_is_eligible(
|
||||
wsl2_ui_processes.contains(exe_name)
|
||||
};
|
||||
|
||||
if (allow_wsl2_gui || style.contains(WindowStyle::CAPTION) && ex_style.contains(ExtendedWindowStyle::WINDOWEDGE))
|
||||
let allow_titlebar_removed = {
|
||||
let titlebars_removed = NO_TITLEBAR.lock();
|
||||
titlebars_removed.contains(exe_name)
|
||||
};
|
||||
|
||||
if (allow_wsl2_gui || allow_titlebar_removed || style.contains(WindowStyle::CAPTION) && ex_style.contains(ExtendedWindowStyle::WINDOWEDGE))
|
||||
&& !ex_style.contains(ExtendedWindowStyle::DLGMODALFRAME)
|
||||
// Get a lot of dupe events coming through that make the redrawing go crazy
|
||||
// on FocusChange events if I don't filter out this one. But, if we are
|
||||
|
||||
@@ -37,6 +37,7 @@ use crate::current_virtual_desktop;
|
||||
use crate::load_configuration;
|
||||
use crate::monitor::Monitor;
|
||||
use crate::ring::Ring;
|
||||
use crate::static_config::StaticConfig;
|
||||
use crate::window::Window;
|
||||
use crate::window_manager_event::WindowManagerEvent;
|
||||
use crate::windows_api::WindowsApi;
|
||||
@@ -49,7 +50,9 @@ use crate::FLOAT_IDENTIFIERS;
|
||||
use crate::HOME_DIR;
|
||||
use crate::LAYERED_WHITELIST;
|
||||
use crate::MANAGE_IDENTIFIERS;
|
||||
use crate::NO_TITLEBAR;
|
||||
use crate::OBJECT_NAME_CHANGE_ON_LAUNCH;
|
||||
use crate::REMOVE_TITLEBARS;
|
||||
use crate::TRAY_AND_MULTI_WINDOW_IDENTIFIERS;
|
||||
use crate::WORKSPACE_RULES;
|
||||
|
||||
@@ -75,6 +78,7 @@ pub struct WindowManager {
|
||||
pub already_moved_window_handles: Arc<Mutex<HashSet<isize>>>,
|
||||
}
|
||||
|
||||
#[allow(clippy::struct_excessive_bools)]
|
||||
#[derive(Debug, Serialize, JsonSchema)]
|
||||
pub struct State {
|
||||
pub monitors: Ring<Monitor>,
|
||||
@@ -87,6 +91,7 @@ pub struct State {
|
||||
pub focus_follows_mouse: Option<FocusFollowsMouseImplementation>,
|
||||
pub mouse_follows_focus: bool,
|
||||
pub has_pending_raise_op: bool,
|
||||
pub remove_titlebars: bool,
|
||||
pub float_identifiers: Vec<String>,
|
||||
pub manage_identifiers: Vec<String>,
|
||||
pub layered_whitelist: Vec<String>,
|
||||
@@ -114,6 +119,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(),
|
||||
@@ -229,6 +235,12 @@ impl WindowManager {
|
||||
std::thread::spawn(|| load_configuration().expect("could not load configuration"));
|
||||
}
|
||||
|
||||
#[tracing::instrument(skip(self))]
|
||||
pub fn reload_static_configuration(&mut self, pathbuf: &PathBuf) -> Result<()> {
|
||||
tracing::info!("reloading static configuration");
|
||||
StaticConfig::reload(pathbuf, self)
|
||||
}
|
||||
|
||||
#[tracing::instrument(skip(self))]
|
||||
pub fn watch_configuration(&mut self, enable: bool) -> Result<()> {
|
||||
let home = HOME_DIR.clone();
|
||||
@@ -480,6 +492,7 @@ impl WindowManager {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[allow(clippy::too_many_arguments)]
|
||||
#[tracing::instrument(skip(self))]
|
||||
fn add_window_handle_to_move_based_on_workspace_rule(
|
||||
&self,
|
||||
@@ -954,23 +967,34 @@ impl WindowManager {
|
||||
}
|
||||
|
||||
#[tracing::instrument(skip(self))]
|
||||
pub fn restore_all_windows(&mut self) {
|
||||
pub fn restore_all_windows(&mut self) -> Result<()> {
|
||||
tracing::info!("restoring all hidden windows");
|
||||
|
||||
let no_titlebar = NO_TITLEBAR.lock();
|
||||
|
||||
for monitor in self.monitors_mut() {
|
||||
for workspace in monitor.workspaces_mut() {
|
||||
for containers in workspace.containers_mut() {
|
||||
for window in containers.windows_mut() {
|
||||
if no_titlebar.contains(&window.exe()?) {
|
||||
window.add_title_bar()?;
|
||||
}
|
||||
|
||||
window.restore();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[tracing::instrument(skip(self))]
|
||||
fn handle_unmanaged_window_behaviour(&self) -> Result<()> {
|
||||
if let OperationBehaviour::NoOp = self.unmanaged_window_operation_behaviour {
|
||||
if matches!(
|
||||
self.unmanaged_window_operation_behaviour,
|
||||
OperationBehaviour::NoOp
|
||||
) {
|
||||
let workspace = self.focused_workspace()?;
|
||||
let focused_hwnd = WindowsApi::foreground_window()?;
|
||||
if !workspace.contains_managed_window(focused_hwnd) {
|
||||
@@ -983,6 +1007,92 @@ impl WindowManager {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn update_focused_workspace_by_monitor_idx(&mut self, idx: usize) -> Result<()> {
|
||||
let invisible_borders = self.invisible_borders;
|
||||
let offset = self.work_area_offset;
|
||||
|
||||
self.monitors_mut()
|
||||
.get_mut(idx)
|
||||
.ok_or_else(|| anyhow!("there is no monitor"))?
|
||||
.update_focused_workspace(offset, &invisible_borders)
|
||||
}
|
||||
|
||||
#[tracing::instrument(skip(self))]
|
||||
pub fn swap_monitor_workspaces(&mut self, first_idx: usize, second_idx: usize) -> Result<()> {
|
||||
tracing::info!("swaping monitors");
|
||||
if first_idx == second_idx {
|
||||
return Ok(());
|
||||
}
|
||||
let mouse_follows_focus = self.mouse_follows_focus;
|
||||
let first_focused_workspace = {
|
||||
let first_monitor = self
|
||||
.monitors()
|
||||
.get(first_idx)
|
||||
.ok_or_else(|| anyhow!("There is no monitor"))?;
|
||||
first_monitor.focused_workspace_idx()
|
||||
};
|
||||
|
||||
let second_focused_workspace = {
|
||||
let second_monitor = self
|
||||
.monitors()
|
||||
.get(second_idx)
|
||||
.ok_or_else(|| anyhow!("There is no monitor"))?;
|
||||
second_monitor.focused_workspace_idx()
|
||||
};
|
||||
|
||||
// Swap workspaces between the first and second monitors
|
||||
|
||||
let first_workspaces = self
|
||||
.monitors_mut()
|
||||
.get_mut(first_idx)
|
||||
.ok_or_else(|| anyhow!("There is no monitor"))?
|
||||
.remove_workspaces();
|
||||
|
||||
let second_workspaces = self
|
||||
.monitors_mut()
|
||||
.get_mut(second_idx)
|
||||
.ok_or_else(|| anyhow!("There is no monitor"))?
|
||||
.remove_workspaces();
|
||||
|
||||
self.monitors_mut()
|
||||
.get_mut(first_idx)
|
||||
.ok_or_else(|| anyhow!("There is no monitor"))?
|
||||
.workspaces_mut()
|
||||
.extend(second_workspaces);
|
||||
|
||||
self.monitors_mut()
|
||||
.get_mut(second_idx)
|
||||
.ok_or_else(|| anyhow!("There is no monitor"))?
|
||||
.workspaces_mut()
|
||||
.extend(first_workspaces);
|
||||
|
||||
// Set the focused workspaces for the first and second monitors
|
||||
if let Some(first_monitor) = self.monitors_mut().get_mut(first_idx) {
|
||||
first_monitor.focus_workspace(second_focused_workspace)?;
|
||||
first_monitor.load_focused_workspace(mouse_follows_focus)?;
|
||||
}
|
||||
|
||||
if let Some(second_monitor) = self.monitors_mut().get_mut(second_idx) {
|
||||
second_monitor.focus_workspace(first_focused_workspace)?;
|
||||
second_monitor.load_focused_workspace(mouse_follows_focus)?;
|
||||
}
|
||||
|
||||
self.update_focused_workspace_by_monitor_idx(second_idx)?;
|
||||
self.update_focused_workspace_by_monitor_idx(first_idx)
|
||||
}
|
||||
|
||||
#[tracing::instrument(skip(self))]
|
||||
pub fn swap_focused_monitor(&mut self, idx: usize) -> Result<()> {
|
||||
tracing::info!("swapping focused monitor");
|
||||
|
||||
let focused_monitor_idx = self.focused_monitor_idx();
|
||||
let mouse_follows_focus = self.mouse_follows_focus;
|
||||
|
||||
self.swap_monitor_workspaces(focused_monitor_idx, idx)?;
|
||||
|
||||
self.update_focused_workspace(mouse_follows_focus)
|
||||
}
|
||||
|
||||
#[tracing::instrument(skip(self))]
|
||||
pub fn move_container_to_monitor(
|
||||
&mut self,
|
||||
|
||||
@@ -31,62 +31,49 @@ impl Display for WindowManagerEvent {
|
||||
fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
|
||||
match self {
|
||||
Self::Manage(window) => {
|
||||
write!(f, "Manage (Window: {})", window)
|
||||
write!(f, "Manage (Window: {window})")
|
||||
}
|
||||
Self::Unmanage(window) => {
|
||||
write!(f, "Unmanage (Window: {})", window)
|
||||
write!(f, "Unmanage (Window: {window})")
|
||||
}
|
||||
Self::Destroy(winevent, window) => {
|
||||
write!(f, "Destroy (WinEvent: {}, Window: {})", winevent, window)
|
||||
write!(f, "Destroy (WinEvent: {winevent}, Window: {window})")
|
||||
}
|
||||
Self::FocusChange(winevent, window) => {
|
||||
write!(
|
||||
f,
|
||||
"FocusChange (WinEvent: {}, Window: {})",
|
||||
winevent, window
|
||||
)
|
||||
write!(f, "FocusChange (WinEvent: {winevent}, Window: {window})",)
|
||||
}
|
||||
Self::Hide(winevent, window) => {
|
||||
write!(f, "Hide (WinEvent: {}, Window: {})", winevent, window)
|
||||
write!(f, "Hide (WinEvent: {winevent}, Window: {window})")
|
||||
}
|
||||
Self::Cloak(winevent, window) => {
|
||||
write!(f, "Cloak (WinEvent: {}, Window: {})", winevent, window)
|
||||
write!(f, "Cloak (WinEvent: {winevent}, Window: {window})")
|
||||
}
|
||||
Self::Minimize(winevent, window) => {
|
||||
write!(f, "Minimize (WinEvent: {}, Window: {})", winevent, window)
|
||||
write!(f, "Minimize (WinEvent: {winevent}, Window: {window})")
|
||||
}
|
||||
Self::Show(winevent, window) => {
|
||||
write!(f, "Show (WinEvent: {}, Window: {})", winevent, window)
|
||||
write!(f, "Show (WinEvent: {winevent}, Window: {window})")
|
||||
}
|
||||
Self::Uncloak(winevent, window) => {
|
||||
write!(f, "Uncloak (WinEvent: {}, Window: {})", winevent, window)
|
||||
write!(f, "Uncloak (WinEvent: {winevent}, Window: {window})")
|
||||
}
|
||||
Self::MoveResizeStart(winevent, window) => {
|
||||
write!(
|
||||
f,
|
||||
"MoveResizeStart (WinEvent: {}, Window: {})",
|
||||
winevent, window
|
||||
"MoveResizeStart (WinEvent: {winevent}, Window: {window})",
|
||||
)
|
||||
}
|
||||
Self::MoveResizeEnd(winevent, window) => {
|
||||
write!(
|
||||
f,
|
||||
"MoveResizeEnd (WinEvent: {}, Window: {})",
|
||||
winevent, window
|
||||
)
|
||||
write!(f, "MoveResizeEnd (WinEvent: {winevent}, Window: {window})",)
|
||||
}
|
||||
Self::MouseCapture(winevent, window) => {
|
||||
write!(
|
||||
f,
|
||||
"MouseCapture (WinEvent: {}, Window: {})",
|
||||
winevent, window
|
||||
)
|
||||
write!(f, "MouseCapture (WinEvent: {winevent}, Window: {window})",)
|
||||
}
|
||||
Self::Raise(window) => {
|
||||
write!(f, "Raise (Window: {})", window)
|
||||
write!(f, "Raise (Window: {window})")
|
||||
}
|
||||
Self::DisplayChange(window) => {
|
||||
write!(f, "DisplayChange (Window: {})", window)
|
||||
write!(f, "DisplayChange (Window: {window})")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -11,6 +11,7 @@ use color_eyre::Result;
|
||||
use windows::core::Result as WindowsCrateResult;
|
||||
use windows::core::PCSTR;
|
||||
use windows::core::PWSTR;
|
||||
use windows::Win32::Foundation::CloseHandle;
|
||||
use windows::Win32::Foundation::BOOL;
|
||||
use windows::Win32::Foundation::COLORREF;
|
||||
use windows::Win32::Foundation::HANDLE;
|
||||
@@ -302,7 +303,10 @@ impl WindowsApi {
|
||||
}
|
||||
|
||||
pub fn position_window(hwnd: HWND, layout: &Rect, top: bool) -> Result<()> {
|
||||
let flags = SetWindowPosition::NO_ACTIVATE;
|
||||
let flags = SetWindowPosition::NO_ACTIVATE
|
||||
| SetWindowPosition::NO_SEND_CHANGING
|
||||
| SetWindowPosition::NO_COPY_BITS
|
||||
| SetWindowPosition::FRAME_CHANGED;
|
||||
|
||||
let position = if top { HWND_TOPMOST } else { HWND_BOTTOM };
|
||||
Self::set_window_pos(hwnd, layout, position, flags.bits())
|
||||
@@ -524,6 +528,8 @@ impl WindowsApi {
|
||||
}
|
||||
|
||||
fn window_long_ptr_w(hwnd: HWND, index: WINDOW_LONG_PTR_INDEX) -> Result<isize> {
|
||||
// Can return 0, which does not always mean that an error has occurred
|
||||
// https://docs.microsoft.com/en-us/windows/win32/api/winuser/nf-winuser-getwindowlongptrw
|
||||
Result::from(WindowsResult::from(unsafe {
|
||||
GetWindowLongPtrW(hwnd, index)
|
||||
}))
|
||||
@@ -558,6 +564,10 @@ impl WindowsApi {
|
||||
unsafe { OpenProcess(access_rights, inherit_handle, process_id) }.process()
|
||||
}
|
||||
|
||||
pub fn close_process(handle: HANDLE) -> Result<()> {
|
||||
unsafe { CloseHandle(handle) }.ok().process()
|
||||
}
|
||||
|
||||
pub fn process_handle(process_id: u32) -> Result<HANDLE> {
|
||||
Self::open_process(PROCESS_QUERY_INFORMATION, false, process_id)
|
||||
}
|
||||
|
||||
@@ -158,7 +158,7 @@ pub extern "system" fn border_window(
|
||||
lparam: LPARAM,
|
||||
) -> LRESULT {
|
||||
unsafe {
|
||||
match message as u32 {
|
||||
match message {
|
||||
WM_PAINT => {
|
||||
let border_rect = *BORDER_RECT.lock();
|
||||
let mut ps = PAINTSTRUCT::default();
|
||||
@@ -194,7 +194,7 @@ pub extern "system" fn hidden_window(
|
||||
lparam: LPARAM,
|
||||
) -> LRESULT {
|
||||
unsafe {
|
||||
match message as u32 {
|
||||
match message {
|
||||
WM_DISPLAYCHANGE => {
|
||||
let event_type = WindowManagerEvent::DisplayChange(Window { hwnd: window.0 });
|
||||
WINEVENT_CALLBACK_CHANNEL
|
||||
|
||||
@@ -12,6 +12,7 @@ use schemars::JsonSchema;
|
||||
use serde::Serialize;
|
||||
|
||||
use komorebi_core::Axis;
|
||||
use komorebi_core::CustomLayout;
|
||||
use komorebi_core::CycleDirection;
|
||||
use komorebi_core::DefaultLayout;
|
||||
use komorebi_core::Layout;
|
||||
@@ -20,9 +21,14 @@ use komorebi_core::Rect;
|
||||
|
||||
use crate::container::Container;
|
||||
use crate::ring::Ring;
|
||||
use crate::static_config::WorkspaceConfig;
|
||||
use crate::window::Window;
|
||||
use crate::windows_api::WindowsApi;
|
||||
use crate::DEFAULT_CONTAINER_PADDING;
|
||||
use crate::DEFAULT_WORKSPACE_PADDING;
|
||||
use crate::INITIAL_CONFIGURATION_LOADED;
|
||||
use crate::NO_TITLEBAR;
|
||||
use crate::REMOVE_TITLEBARS;
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Getters, CopyGetters, MutGetters, Setters, JsonSchema)]
|
||||
pub struct Workspace {
|
||||
@@ -75,8 +81,8 @@ impl Default for Workspace {
|
||||
layout: Layout::Default(DefaultLayout::BSP),
|
||||
layout_rules: vec![],
|
||||
layout_flip: None,
|
||||
workspace_padding: Option::from(10),
|
||||
container_padding: Option::from(10),
|
||||
workspace_padding: Option::from(DEFAULT_WORKSPACE_PADDING.load(Ordering::SeqCst)),
|
||||
container_padding: Option::from(DEFAULT_CONTAINER_PADDING.load(Ordering::SeqCst)),
|
||||
latest_layout: vec![],
|
||||
resize_dimensions: vec![],
|
||||
tile: true,
|
||||
@@ -85,6 +91,46 @@ impl Default for Workspace {
|
||||
}
|
||||
|
||||
impl Workspace {
|
||||
pub fn load_static_config(&mut self, config: &WorkspaceConfig) -> Result<()> {
|
||||
self.name = Option::from(config.name.clone());
|
||||
|
||||
if config.container_padding.is_some() {
|
||||
self.set_container_padding(config.container_padding);
|
||||
}
|
||||
|
||||
if config.workspace_padding.is_some() {
|
||||
self.set_workspace_padding(config.workspace_padding);
|
||||
}
|
||||
|
||||
if let Some(layout) = &config.layout {
|
||||
self.layout = Layout::Default(*layout);
|
||||
}
|
||||
|
||||
if let Some(pathbuf) = &config.custom_layout {
|
||||
let layout = CustomLayout::from_path_buf(pathbuf.clone())?;
|
||||
self.layout = Layout::Custom(layout);
|
||||
}
|
||||
|
||||
if let Some(layout_rules) = &config.layout_rules {
|
||||
let mut all_rules = vec![];
|
||||
for (count, rule) in layout_rules {
|
||||
all_rules.push((*count, Layout::Default(*rule)));
|
||||
}
|
||||
|
||||
self.set_layout_rules(all_rules);
|
||||
}
|
||||
|
||||
if let Some(layout_rules) = &config.custom_layout_rules {
|
||||
let rules = self.layout_rules_mut();
|
||||
for (count, pathbuf) in layout_rules {
|
||||
let rule = CustomLayout::from_path_buf(pathbuf.clone())?;
|
||||
rules.push((*count, Layout::Custom(rule)));
|
||||
}
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn hide(&mut self) {
|
||||
for container in self.containers_mut() {
|
||||
for window in container.windows_mut() {
|
||||
@@ -212,9 +258,18 @@ impl Workspace {
|
||||
self.resize_dimensions(),
|
||||
);
|
||||
|
||||
let should_remove_titlebars = REMOVE_TITLEBARS.load(Ordering::SeqCst);
|
||||
let no_titlebar = NO_TITLEBAR.lock().clone();
|
||||
|
||||
let windows = self.visible_windows_mut();
|
||||
for (i, window) in windows.into_iter().enumerate() {
|
||||
if let (Some(window), Some(layout)) = (window, layouts.get(i)) {
|
||||
if should_remove_titlebars && no_titlebar.contains(&window.exe()?) {
|
||||
window.remove_title_bar()?;
|
||||
} else if no_titlebar.contains(&window.exe()?) {
|
||||
window.add_title_bar()?;
|
||||
}
|
||||
|
||||
window.set_position(layout, invisible_borders, false)?;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
[package]
|
||||
name = "komorebic"
|
||||
version = "0.1.16"
|
||||
version = "0.1.18"
|
||||
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"]
|
||||
@@ -22,15 +22,11 @@ heck = "0.4"
|
||||
lazy_static = "1"
|
||||
paste = "1"
|
||||
powershell_script = "1.0"
|
||||
reqwest = { version = "0.11", features = ["blocking"] }
|
||||
serde = { version = "1", features = ["derive"] }
|
||||
serde_json = "1"
|
||||
serde_yaml = "0.9"
|
||||
sysinfo = "0.29"
|
||||
uds_windows = "1"
|
||||
|
||||
[dependencies.windows]
|
||||
version = "0.48"
|
||||
features = [
|
||||
"Win32_Foundation",
|
||||
"Win32_UI_WindowsAndMessaging"
|
||||
]
|
||||
which = "4"
|
||||
windows = { workspace = true }
|
||||
|
||||
@@ -24,6 +24,7 @@ use paste::paste;
|
||||
use sysinfo::SystemExt;
|
||||
use uds_windows::UnixListener;
|
||||
use uds_windows::UnixStream;
|
||||
use which::which;
|
||||
use windows::Win32::Foundation::HWND;
|
||||
use windows::Win32::UI::WindowsAndMessaging::ShowWindow;
|
||||
use windows::Win32::UI::WindowsAndMessaging::SHOW_WINDOW_CMD;
|
||||
@@ -60,8 +61,7 @@ lazy_static! {
|
||||
home
|
||||
} else {
|
||||
panic!(
|
||||
"$Env:KOMOREBI_CONFIG_HOME is set to '{}', which is not a valid directory",
|
||||
home_path
|
||||
"$Env:KOMOREBI_CONFIG_HOME is set to '{home_path}', which is not a valid directory",
|
||||
);
|
||||
}
|
||||
},
|
||||
@@ -153,7 +153,9 @@ gen_target_subcommand_args! {
|
||||
SendToWorkspace,
|
||||
FocusMonitor,
|
||||
FocusWorkspace,
|
||||
FocusWorkspaces,
|
||||
MoveWorkspaceToMonitor,
|
||||
SwapWorkspacesWithMonitor,
|
||||
}
|
||||
|
||||
macro_rules! gen_named_target_subcommand_args {
|
||||
@@ -513,6 +515,7 @@ gen_application_target_subcommand_args! {
|
||||
IdentifyLayeredApplication,
|
||||
IdentifyObjectNameChangeApplication,
|
||||
IdentifyBorderOverflowApplication,
|
||||
RemoveTitleBar,
|
||||
}
|
||||
|
||||
#[derive(Parser, AhkFunction)]
|
||||
@@ -608,12 +611,18 @@ struct Start {
|
||||
/// Allow the use of komorebi's custom focus-follows-mouse implementation
|
||||
#[clap(action, short, long = "ffm")]
|
||||
ffm: bool,
|
||||
/// Path to a static configuration JSON file
|
||||
#[clap(action, short, long)]
|
||||
config: Option<PathBuf>,
|
||||
/// Wait for 'komorebic complete-configuration' to be sent before processing events
|
||||
#[clap(action, short, long)]
|
||||
await_configuration: bool,
|
||||
/// Start a TCP server on the given port to allow the direct sending of SocketMessages
|
||||
#[clap(action, short, long)]
|
||||
tcp_port: Option<usize>,
|
||||
/// Start whkd in a background process
|
||||
#[clap(action, long)]
|
||||
whkd: bool,
|
||||
}
|
||||
|
||||
#[derive(Parser, AhkFunction)]
|
||||
@@ -788,6 +797,9 @@ enum SubCommand {
|
||||
/// Focus the specified workspace on the focused monitor
|
||||
#[clap(arg_required_else_help = true)]
|
||||
FocusWorkspace(FocusWorkspace),
|
||||
/// Focus the specified workspace on all monitors
|
||||
#[clap(arg_required_else_help = true)]
|
||||
FocusWorkspaces(FocusWorkspaces),
|
||||
/// Focus the specified workspace on the target monitor
|
||||
#[clap(arg_required_else_help = true)]
|
||||
FocusMonitorWorkspace(FocusMonitorWorkspace),
|
||||
@@ -803,6 +815,9 @@ enum SubCommand {
|
||||
/// Move the focused workspace to the specified monitor
|
||||
#[clap(arg_required_else_help = true)]
|
||||
MoveWorkspaceToMonitor(MoveWorkspaceToMonitor),
|
||||
/// Swap focused monitor workspaces with specified monitor
|
||||
#[clap(arg_required_else_help = true)]
|
||||
SwapWorkspacesWithMonitor(SwapWorkspacesWithMonitor),
|
||||
/// Create and append a new workspace on the focused monitor
|
||||
NewWorkspace,
|
||||
/// Set the resize delta (used by resize-edge and resize-axis)
|
||||
@@ -964,6 +979,11 @@ enum SubCommand {
|
||||
/// Identify an application that has WS_EX_LAYERED, but should still be managed
|
||||
#[clap(arg_required_else_help = true)]
|
||||
IdentifyLayeredApplication(IdentifyLayeredApplication),
|
||||
/// Whitelist an application for title bar removal
|
||||
#[clap(arg_required_else_help = true)]
|
||||
RemoveTitleBar(RemoveTitleBar),
|
||||
/// Toggle title bars for whitelisted applications
|
||||
ToggleTitleBars,
|
||||
/// Identify an application that has overflowing borders
|
||||
#[clap(arg_required_else_help = true)]
|
||||
#[clap(alias = "identify-border-overflow")]
|
||||
@@ -1005,19 +1025,26 @@ enum SubCommand {
|
||||
#[clap(arg_required_else_help = true)]
|
||||
#[clap(alias = "fmt-asc")]
|
||||
FormatAppSpecificConfiguration(FormatAppSpecificConfiguration),
|
||||
/// Fetch the latest version of applications.yaml from komorebi-application-specific-configuration
|
||||
#[clap(alias = "fetch-asc")]
|
||||
FetchAppSpecificConfiguration,
|
||||
/// Generate a JSON Schema of subscription notifications
|
||||
NotificationSchema,
|
||||
/// Generate a JSON Schema of socket messages
|
||||
SocketSchema,
|
||||
/// Generate a JSON Schema of the static configuration file
|
||||
StaticConfigSchema,
|
||||
/// Generates a static configuration JSON file based on the current window manager state
|
||||
GenerateStaticConfig,
|
||||
}
|
||||
|
||||
pub fn send_message(bytes: &[u8]) -> Result<()> {
|
||||
let socket = DATA_DIR.join("komorebi.sock");
|
||||
let mut stream = UnixStream::connect(&socket)?;
|
||||
let mut stream = UnixStream::connect(socket)?;
|
||||
Ok(stream.write_all(bytes)?)
|
||||
}
|
||||
|
||||
#[allow(clippy::too_many_lines)]
|
||||
#[allow(clippy::too_many_lines, clippy::cognitive_complexity)]
|
||||
fn main() -> Result<()> {
|
||||
let opts: Opts = Opts::parse();
|
||||
|
||||
@@ -1097,7 +1124,7 @@ fn main() -> Result<()> {
|
||||
let locked = file.lock();
|
||||
#[allow(clippy::significant_drop_in_scrutinee)]
|
||||
for line in locked.lines().flatten() {
|
||||
println!("{}", line);
|
||||
println!("{line}");
|
||||
}
|
||||
}
|
||||
SubCommand::Focus(arg) => {
|
||||
@@ -1183,6 +1210,9 @@ fn main() -> Result<()> {
|
||||
SubCommand::MoveWorkspaceToMonitor(arg) => {
|
||||
send_message(&SocketMessage::MoveWorkspaceToMonitorNumber(arg.target).as_bytes()?)?;
|
||||
}
|
||||
SubCommand::SwapWorkspacesWithMonitor(arg) => {
|
||||
send_message(&SocketMessage::SwapWorkspacesToMonitorNumber(arg.target).as_bytes()?)?;
|
||||
}
|
||||
SubCommand::InvisibleBorders(arg) => {
|
||||
send_message(
|
||||
&SocketMessage::InvisibleBorders(Rect {
|
||||
@@ -1361,6 +1391,10 @@ fn main() -> Result<()> {
|
||||
)?;
|
||||
}
|
||||
SubCommand::Start(arg) => {
|
||||
if arg.whkd && which("whkd").is_err() {
|
||||
return Err(anyhow!("could not find whkd, please make sure it is installed before using the --whkd flag"));
|
||||
}
|
||||
|
||||
let mut buf: PathBuf;
|
||||
|
||||
// The komorebi.ps1 shim will only exist in the Path if installed by Scoop
|
||||
@@ -1384,73 +1418,50 @@ fn main() -> Result<()> {
|
||||
None
|
||||
};
|
||||
|
||||
let script = exec.map_or_else(
|
||||
|| {
|
||||
if arg.ffm | arg.await_configuration | arg.tcp_port.is_some() {
|
||||
format!(
|
||||
"Start-Process komorebi.exe -ArgumentList {} -WindowStyle hidden",
|
||||
arg.tcp_port.map_or_else(
|
||||
|| if arg.ffm && arg.await_configuration {
|
||||
"'--ffm','--await-configuration'".to_string()
|
||||
} else if arg.ffm {
|
||||
"'--ffm'".to_string()
|
||||
} else {
|
||||
"'--await-configuration'".to_string()
|
||||
},
|
||||
|port| if arg.ffm {
|
||||
format!("'--ffm','--tcp-port={}'", port)
|
||||
} else if arg.await_configuration {
|
||||
format!("'--await-configuration','--tcp-port={}'", port)
|
||||
} else if arg.ffm && arg.await_configuration {
|
||||
format!("'--ffm','--await-configuration','--tcp-port={}'", port)
|
||||
} else {
|
||||
format!("'--tcp-port={}'", port)
|
||||
}
|
||||
)
|
||||
)
|
||||
} else {
|
||||
String::from("Start-Process komorebi.exe -WindowStyle hidden")
|
||||
}
|
||||
},
|
||||
|exec| {
|
||||
if arg.ffm | arg.await_configuration {
|
||||
format!(
|
||||
"Start-Process '{}' -ArgumentList {} -WindowStyle hidden",
|
||||
exec,
|
||||
arg.tcp_port.map_or_else(
|
||||
|| if arg.ffm && arg.await_configuration {
|
||||
"'--ffm','--await-configuration'".to_string()
|
||||
} else if arg.ffm {
|
||||
"'--ffm'".to_string()
|
||||
} else {
|
||||
"'--await-configuration'".to_string()
|
||||
},
|
||||
|port| if arg.ffm {
|
||||
format!("'--ffm','--tcp-port={}'", port)
|
||||
} else if arg.await_configuration {
|
||||
format!("'--await-configuration','--tcp-port={}'", port)
|
||||
} else if arg.ffm && arg.await_configuration {
|
||||
format!("'--ffm','--await-configuration','--tcp-port={}'", port)
|
||||
} else {
|
||||
format!("'--tcp-port={}'", port)
|
||||
}
|
||||
)
|
||||
)
|
||||
} else {
|
||||
format!("Start-Process '{}' -WindowStyle hidden", exec)
|
||||
}
|
||||
},
|
||||
);
|
||||
let mut flags = vec![];
|
||||
if let Some(config) = arg.config {
|
||||
let path = resolve_windows_path(config.as_os_str().to_str().unwrap())?;
|
||||
if !path.is_file() {
|
||||
return Err(anyhow!("could not find file: {}", path.to_string_lossy()));
|
||||
}
|
||||
|
||||
flags.push(format!(
|
||||
"'--config=\"{}\"'",
|
||||
path.as_os_str()
|
||||
.to_string_lossy()
|
||||
.trim_start_matches(r#"\\?\"#),
|
||||
));
|
||||
}
|
||||
|
||||
if arg.ffm {
|
||||
flags.push("'--ffm'".to_string());
|
||||
}
|
||||
|
||||
if arg.await_configuration {
|
||||
flags.push("'--await-configuration'".to_string());
|
||||
}
|
||||
|
||||
if let Some(port) = arg.tcp_port {
|
||||
flags.push(format!("'--tcp-port={port}'"));
|
||||
}
|
||||
|
||||
let argument_list = flags.join(",");
|
||||
let script = {
|
||||
format!(
|
||||
"Start-Process '{}' -ArgumentList {argument_list} -WindowStyle hidden",
|
||||
exec.unwrap_or("komorebi.exe")
|
||||
)
|
||||
};
|
||||
|
||||
let mut running = false;
|
||||
|
||||
while !running {
|
||||
match powershell_script::run(&script) {
|
||||
Ok(_) => {
|
||||
println!("{}", script);
|
||||
println!("{script}");
|
||||
}
|
||||
Err(error) => {
|
||||
println!("Error: {}", error);
|
||||
println!("Error: {error}");
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1467,6 +1478,23 @@ fn main() -> Result<()> {
|
||||
println!("komorebi.exe did not start... Trying again");
|
||||
}
|
||||
}
|
||||
|
||||
if arg.whkd {
|
||||
let script = r#"
|
||||
if (!(Get-Process whkd -ErrorAction SilentlyContinue))
|
||||
{
|
||||
Start-Process whkd -WindowStyle hidden
|
||||
}
|
||||
"#;
|
||||
match powershell_script::run(script) {
|
||||
Ok(_) => {
|
||||
println!("{script}");
|
||||
}
|
||||
Err(error) => {
|
||||
println!("Error: {error}");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
SubCommand::Stop => {
|
||||
send_message(&SocketMessage::Stop.as_bytes()?)?;
|
||||
@@ -1532,6 +1560,9 @@ fn main() -> Result<()> {
|
||||
SubCommand::FocusWorkspace(arg) => {
|
||||
send_message(&SocketMessage::FocusWorkspaceNumber(arg.target).as_bytes()?)?;
|
||||
}
|
||||
SubCommand::FocusWorkspaces(arg) => {
|
||||
send_message(&SocketMessage::FocusWorkspaceNumbers(arg.target).as_bytes()?)?;
|
||||
}
|
||||
SubCommand::FocusMonitorWorkspace(arg) => {
|
||||
send_message(
|
||||
&SocketMessage::FocusMonitorWorkspaceNumber(
|
||||
@@ -1710,6 +1741,21 @@ fn main() -> Result<()> {
|
||||
.as_bytes()?,
|
||||
)?;
|
||||
}
|
||||
SubCommand::RemoveTitleBar(target) => {
|
||||
match target.identifier {
|
||||
ApplicationIdentifier::Exe => {}
|
||||
_ => {
|
||||
return Err(anyhow!(
|
||||
"this command requires applications to be identified by their exe"
|
||||
));
|
||||
}
|
||||
}
|
||||
|
||||
send_message(&SocketMessage::RemoveTitleBar(target.identifier, target.id).as_bytes()?)?;
|
||||
}
|
||||
SubCommand::ToggleTitleBars => {
|
||||
send_message(&SocketMessage::ToggleTitleBars.as_bytes()?)?;
|
||||
}
|
||||
SubCommand::Manage => {
|
||||
send_message(&SocketMessage::ManageFocusedWindow.as_bytes()?)?;
|
||||
}
|
||||
@@ -1855,6 +1901,29 @@ fn main() -> Result<()> {
|
||||
|
||||
println!("File successfully formatted for PRs to https://github.com/LGUG2Z/komorebi-application-specific-configuration");
|
||||
}
|
||||
SubCommand::FetchAppSpecificConfiguration => {
|
||||
let content = reqwest::blocking::get("https://raw.githubusercontent.com/LGUG2Z/komorebi-application-specific-configuration/master/applications.yaml")?
|
||||
.text()?;
|
||||
|
||||
let mut output_file = HOME_DIR.clone();
|
||||
output_file.push("applications.yaml");
|
||||
|
||||
let mut file = OpenOptions::new()
|
||||
.write(true)
|
||||
.create(true)
|
||||
.truncate(true)
|
||||
.open(&output_file)?;
|
||||
|
||||
file.write_all(content.as_bytes())?;
|
||||
|
||||
let output_path = output_file.to_str().unwrap().to_string();
|
||||
let output_path = output_path.replace('\\', "/");
|
||||
|
||||
println!("Latest version of applications.yaml from https://github.com/LGUG2Z/komorebi-application-specific-configuration downloaded\n");
|
||||
println!(
|
||||
"You can add this to your komorebi.json static configuration file like this: \n\n\"app_specific_configuration_path\": \"{output_path}\"",
|
||||
);
|
||||
}
|
||||
SubCommand::NotificationSchema => {
|
||||
let home = DATA_DIR.clone();
|
||||
let mut socket = home;
|
||||
@@ -1908,6 +1977,74 @@ fn main() -> Result<()> {
|
||||
|
||||
send_message(&SocketMessage::SocketSchema.as_bytes()?)?;
|
||||
|
||||
let listener = UnixListener::bind(socket)?;
|
||||
match listener.accept() {
|
||||
Ok(incoming) => {
|
||||
let stream = BufReader::new(incoming.0);
|
||||
for line in stream.lines() {
|
||||
println!("{}", line?);
|
||||
}
|
||||
|
||||
return Ok(());
|
||||
}
|
||||
Err(error) => {
|
||||
panic!("{}", error);
|
||||
}
|
||||
}
|
||||
}
|
||||
SubCommand::StaticConfigSchema => {
|
||||
let home = DATA_DIR.clone();
|
||||
let mut socket = home;
|
||||
socket.push("komorebic.sock");
|
||||
let socket = socket.as_path();
|
||||
|
||||
match std::fs::remove_file(socket) {
|
||||
Ok(_) => {}
|
||||
Err(error) => match error.kind() {
|
||||
// Doing this because ::exists() doesn't work reliably on Windows via IntelliJ
|
||||
ErrorKind::NotFound => {}
|
||||
_ => {
|
||||
return Err(error.into());
|
||||
}
|
||||
},
|
||||
};
|
||||
|
||||
send_message(&SocketMessage::StaticConfigSchema.as_bytes()?)?;
|
||||
|
||||
let listener = UnixListener::bind(socket)?;
|
||||
match listener.accept() {
|
||||
Ok(incoming) => {
|
||||
let stream = BufReader::new(incoming.0);
|
||||
for line in stream.lines() {
|
||||
println!("{}", line?);
|
||||
}
|
||||
|
||||
return Ok(());
|
||||
}
|
||||
Err(error) => {
|
||||
panic!("{}", error);
|
||||
}
|
||||
}
|
||||
}
|
||||
SubCommand::GenerateStaticConfig => {
|
||||
let home = DATA_DIR.clone();
|
||||
let mut socket = home;
|
||||
socket.push("komorebic.sock");
|
||||
let socket = socket.as_path();
|
||||
|
||||
match std::fs::remove_file(socket) {
|
||||
Ok(_) => {}
|
||||
Err(error) => match error.kind() {
|
||||
// Doing this because ::exists() doesn't work reliably on Windows via IntelliJ
|
||||
ErrorKind::NotFound => {}
|
||||
_ => {
|
||||
return Err(error.into());
|
||||
}
|
||||
},
|
||||
};
|
||||
|
||||
send_message(&SocketMessage::GenerateStaticConfig.as_bytes()?)?;
|
||||
|
||||
let listener = UnixListener::bind(socket)?;
|
||||
match listener.accept() {
|
||||
Ok(incoming) => {
|
||||
|
||||
574
schema.json
Normal file
574
schema.json
Normal file
@@ -0,0 +1,574 @@
|
||||
{
|
||||
"$schema": "http://json-schema.org/draft-07/schema#",
|
||||
"title": "StaticConfig",
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"active_window_border": {
|
||||
"description": "Display an active window border (default: false)",
|
||||
"type": [
|
||||
"boolean",
|
||||
"null"
|
||||
]
|
||||
},
|
||||
"active_window_border_colours": {
|
||||
"description": "Active window border colours for different container types",
|
||||
"anyOf": [
|
||||
{
|
||||
"$ref": "#/definitions/ActiveWindowBorderColours"
|
||||
},
|
||||
{
|
||||
"type": "null"
|
||||
}
|
||||
]
|
||||
},
|
||||
"alt_focus_hack": {
|
||||
"description": "Always send the ALT key when using focus commands (default: false)",
|
||||
"type": [
|
||||
"boolean",
|
||||
"null"
|
||||
]
|
||||
},
|
||||
"app_specific_configuration_path": {
|
||||
"description": "Path to applications.yaml from komorebi-application-specific-configurations (default: None)",
|
||||
"type": [
|
||||
"string",
|
||||
"null"
|
||||
]
|
||||
},
|
||||
"border_offset": {
|
||||
"description": "Offset of the active window border (default: None)",
|
||||
"anyOf": [
|
||||
{
|
||||
"$ref": "#/definitions/Rect"
|
||||
},
|
||||
{
|
||||
"type": "null"
|
||||
}
|
||||
]
|
||||
},
|
||||
"border_overflow_applications": {
|
||||
"description": "Identify border overflow applications",
|
||||
"type": [
|
||||
"array",
|
||||
"null"
|
||||
],
|
||||
"items": {
|
||||
"$ref": "#/definitions/IdWithIdentifier"
|
||||
}
|
||||
},
|
||||
"border_width": {
|
||||
"description": "Width of the active window border (default: 20)",
|
||||
"type": [
|
||||
"integer",
|
||||
"null"
|
||||
],
|
||||
"format": "int32"
|
||||
},
|
||||
"cross_monitor_move_behaviour": {
|
||||
"description": "Determine what happens when a window is moved across a monitor boundary (default: Swap)",
|
||||
"anyOf": [
|
||||
{
|
||||
"$ref": "#/definitions/MoveBehaviour"
|
||||
},
|
||||
{
|
||||
"type": "null"
|
||||
}
|
||||
]
|
||||
},
|
||||
"default_container_padding": {
|
||||
"description": "Global default container padding (default: 10)",
|
||||
"type": [
|
||||
"integer",
|
||||
"null"
|
||||
],
|
||||
"format": "int32"
|
||||
},
|
||||
"default_workspace_padding": {
|
||||
"description": "Global default workspace padding (default: 10)",
|
||||
"type": [
|
||||
"integer",
|
||||
"null"
|
||||
],
|
||||
"format": "int32"
|
||||
},
|
||||
"float_rules": {
|
||||
"description": "Individual window floating rules",
|
||||
"type": [
|
||||
"array",
|
||||
"null"
|
||||
],
|
||||
"items": {
|
||||
"$ref": "#/definitions/IdWithIdentifier"
|
||||
}
|
||||
},
|
||||
"focus_follows_mouse": {
|
||||
"description": "Determine focus follows mouse implementation (default: None)",
|
||||
"anyOf": [
|
||||
{
|
||||
"$ref": "#/definitions/FocusFollowsMouseImplementation"
|
||||
},
|
||||
{
|
||||
"type": "null"
|
||||
}
|
||||
]
|
||||
},
|
||||
"global_work_area_offset": {
|
||||
"description": "Global work area (space used for tiling) offset (default: None)",
|
||||
"anyOf": [
|
||||
{
|
||||
"$ref": "#/definitions/Rect"
|
||||
},
|
||||
{
|
||||
"type": "null"
|
||||
}
|
||||
]
|
||||
},
|
||||
"invisible_borders": {
|
||||
"description": "Dimensions of Windows' own invisible borders; don't set these yourself unless you are told to",
|
||||
"anyOf": [
|
||||
{
|
||||
"$ref": "#/definitions/Rect"
|
||||
},
|
||||
{
|
||||
"type": "null"
|
||||
}
|
||||
]
|
||||
},
|
||||
"layered_applications": {
|
||||
"description": "Identify applications that have the WS_EX_LAYERED extended window style",
|
||||
"type": [
|
||||
"array",
|
||||
"null"
|
||||
],
|
||||
"items": {
|
||||
"$ref": "#/definitions/IdWithIdentifier"
|
||||
}
|
||||
},
|
||||
"manage_rules": {
|
||||
"description": "Individual window force-manage rules",
|
||||
"type": [
|
||||
"array",
|
||||
"null"
|
||||
],
|
||||
"items": {
|
||||
"$ref": "#/definitions/IdWithIdentifier"
|
||||
}
|
||||
},
|
||||
"monitors": {
|
||||
"description": "Monitor and workspace configurations",
|
||||
"type": [
|
||||
"array",
|
||||
"null"
|
||||
],
|
||||
"items": {
|
||||
"$ref": "#/definitions/MonitorConfig"
|
||||
}
|
||||
},
|
||||
"mouse_follows_focus": {
|
||||
"description": "Enable or disable mouse follows focus (default: true)",
|
||||
"type": [
|
||||
"boolean",
|
||||
"null"
|
||||
]
|
||||
},
|
||||
"object_name_change_applications": {
|
||||
"description": "Identify applications that send EVENT_OBJECT_NAMECHANGE on launch (very rare)",
|
||||
"type": [
|
||||
"array",
|
||||
"null"
|
||||
],
|
||||
"items": {
|
||||
"$ref": "#/definitions/IdWithIdentifier"
|
||||
}
|
||||
},
|
||||
"resize_delta": {
|
||||
"description": "Delta to resize windows by (default 50)",
|
||||
"type": [
|
||||
"integer",
|
||||
"null"
|
||||
],
|
||||
"format": "int32"
|
||||
},
|
||||
"tray_and_multi_window_applications": {
|
||||
"description": "Identify tray and multi-window applications",
|
||||
"type": [
|
||||
"array",
|
||||
"null"
|
||||
],
|
||||
"items": {
|
||||
"$ref": "#/definitions/IdWithIdentifier"
|
||||
}
|
||||
},
|
||||
"unmanaged_window_operation_behaviour": {
|
||||
"description": "Determine what happens when commands are sent while an unmanaged window is in the foreground (default: Op)",
|
||||
"anyOf": [
|
||||
{
|
||||
"$ref": "#/definitions/OperationBehaviour"
|
||||
},
|
||||
{
|
||||
"type": "null"
|
||||
}
|
||||
]
|
||||
},
|
||||
"window_container_behaviour": {
|
||||
"description": "Determine what happens when a new window is opened (default: Create)",
|
||||
"anyOf": [
|
||||
{
|
||||
"$ref": "#/definitions/WindowContainerBehaviour"
|
||||
},
|
||||
{
|
||||
"type": "null"
|
||||
}
|
||||
]
|
||||
},
|
||||
"window_hiding_behaviour": {
|
||||
"description": "Which Windows signal to use when hiding windows (default: minimize)",
|
||||
"anyOf": [
|
||||
{
|
||||
"$ref": "#/definitions/HidingBehaviour"
|
||||
},
|
||||
{
|
||||
"type": "null"
|
||||
}
|
||||
]
|
||||
}
|
||||
},
|
||||
"definitions": {
|
||||
"ActiveWindowBorderColours": {
|
||||
"type": "object",
|
||||
"required": [
|
||||
"monocle",
|
||||
"single",
|
||||
"stack"
|
||||
],
|
||||
"properties": {
|
||||
"monocle": {
|
||||
"description": "Border colour when the container is in monocle mode",
|
||||
"allOf": [
|
||||
{
|
||||
"$ref": "#/definitions/Rgb"
|
||||
}
|
||||
]
|
||||
},
|
||||
"single": {
|
||||
"description": "Border colour when the container contains a single window",
|
||||
"allOf": [
|
||||
{
|
||||
"$ref": "#/definitions/Rgb"
|
||||
}
|
||||
]
|
||||
},
|
||||
"stack": {
|
||||
"description": "Border colour when the container contains multiple windows",
|
||||
"allOf": [
|
||||
{
|
||||
"$ref": "#/definitions/Rgb"
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
},
|
||||
"ApplicationIdentifier": {
|
||||
"type": "string",
|
||||
"enum": [
|
||||
"Exe",
|
||||
"Class",
|
||||
"Title"
|
||||
]
|
||||
},
|
||||
"DefaultLayout": {
|
||||
"type": "string",
|
||||
"enum": [
|
||||
"BSP",
|
||||
"Columns",
|
||||
"Rows",
|
||||
"VerticalStack",
|
||||
"HorizontalStack",
|
||||
"UltrawideVerticalStack"
|
||||
]
|
||||
},
|
||||
"FocusFollowsMouseImplementation": {
|
||||
"oneOf": [
|
||||
{
|
||||
"description": "A custom FFM implementation (slightly more CPU-intensive)",
|
||||
"type": "string",
|
||||
"enum": [
|
||||
"Komorebi"
|
||||
]
|
||||
},
|
||||
{
|
||||
"description": "The native (legacy) Windows FFM implementation",
|
||||
"type": "string",
|
||||
"enum": [
|
||||
"Windows"
|
||||
]
|
||||
}
|
||||
]
|
||||
},
|
||||
"HidingBehaviour": {
|
||||
"oneOf": [
|
||||
{
|
||||
"description": "Use the SW_HIDE flag to hide windows when switching workspaces (has issues with Electron apps)",
|
||||
"type": "string",
|
||||
"enum": [
|
||||
"Hide"
|
||||
]
|
||||
},
|
||||
{
|
||||
"description": "Use the SW_MINIMIZE flag to hide windows when switching workspaces (has issues with frequent workspace switching)",
|
||||
"type": "string",
|
||||
"enum": [
|
||||
"Minimize"
|
||||
]
|
||||
},
|
||||
{
|
||||
"description": "Use the undocumented SetCloak Win32 function to hide windows when switching workspaces (has foregrounding issues)",
|
||||
"type": "string",
|
||||
"enum": [
|
||||
"Cloak"
|
||||
]
|
||||
}
|
||||
]
|
||||
},
|
||||
"IdWithIdentifier": {
|
||||
"type": "object",
|
||||
"required": [
|
||||
"id",
|
||||
"kind"
|
||||
],
|
||||
"properties": {
|
||||
"id": {
|
||||
"type": "string"
|
||||
},
|
||||
"kind": {
|
||||
"$ref": "#/definitions/ApplicationIdentifier"
|
||||
}
|
||||
}
|
||||
},
|
||||
"MonitorConfig": {
|
||||
"type": "object",
|
||||
"required": [
|
||||
"workspaces"
|
||||
],
|
||||
"properties": {
|
||||
"work_area_offset": {
|
||||
"description": "Monitor-specific work area offset (default: None)",
|
||||
"anyOf": [
|
||||
{
|
||||
"$ref": "#/definitions/Rect"
|
||||
},
|
||||
{
|
||||
"type": "null"
|
||||
}
|
||||
]
|
||||
},
|
||||
"workspaces": {
|
||||
"description": "Workspace configurations",
|
||||
"type": "array",
|
||||
"items": {
|
||||
"$ref": "#/definitions/WorkspaceConfig"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"MoveBehaviour": {
|
||||
"oneOf": [
|
||||
{
|
||||
"description": "Swap the window container with the window container at the edge of the adjacent monitor",
|
||||
"type": "string",
|
||||
"enum": [
|
||||
"Swap"
|
||||
]
|
||||
},
|
||||
{
|
||||
"description": "Insert the window container into the focused workspace on the adjacent monitor",
|
||||
"type": "string",
|
||||
"enum": [
|
||||
"Insert"
|
||||
]
|
||||
}
|
||||
]
|
||||
},
|
||||
"OperationBehaviour": {
|
||||
"oneOf": [
|
||||
{
|
||||
"description": "Process komorebic commands on temporarily unmanaged/floated windows",
|
||||
"type": "string",
|
||||
"enum": [
|
||||
"Op"
|
||||
]
|
||||
},
|
||||
{
|
||||
"description": "Ignore komorebic commands on temporarily unmanaged/floated windows",
|
||||
"type": "string",
|
||||
"enum": [
|
||||
"NoOp"
|
||||
]
|
||||
}
|
||||
]
|
||||
},
|
||||
"Rect": {
|
||||
"type": "object",
|
||||
"required": [
|
||||
"bottom",
|
||||
"left",
|
||||
"right",
|
||||
"top"
|
||||
],
|
||||
"properties": {
|
||||
"bottom": {
|
||||
"description": "The bottom point in a Win32 Rect",
|
||||
"type": "integer",
|
||||
"format": "int32"
|
||||
},
|
||||
"left": {
|
||||
"description": "The left point in a Win32 Rect",
|
||||
"type": "integer",
|
||||
"format": "int32"
|
||||
},
|
||||
"right": {
|
||||
"description": "The right point in a Win32 Rect",
|
||||
"type": "integer",
|
||||
"format": "int32"
|
||||
},
|
||||
"top": {
|
||||
"description": "The top point in a Win32 Rect",
|
||||
"type": "integer",
|
||||
"format": "int32"
|
||||
}
|
||||
}
|
||||
},
|
||||
"Rgb": {
|
||||
"type": "object",
|
||||
"required": [
|
||||
"b",
|
||||
"g",
|
||||
"r"
|
||||
],
|
||||
"properties": {
|
||||
"b": {
|
||||
"description": "Blue",
|
||||
"type": "integer",
|
||||
"format": "uint32",
|
||||
"minimum": 0.0
|
||||
},
|
||||
"g": {
|
||||
"description": "Green",
|
||||
"type": "integer",
|
||||
"format": "uint32",
|
||||
"minimum": 0.0
|
||||
},
|
||||
"r": {
|
||||
"description": "Red",
|
||||
"type": "integer",
|
||||
"format": "uint32",
|
||||
"minimum": 0.0
|
||||
}
|
||||
}
|
||||
},
|
||||
"WindowContainerBehaviour": {
|
||||
"oneOf": [
|
||||
{
|
||||
"description": "Create a new container for each new window",
|
||||
"type": "string",
|
||||
"enum": [
|
||||
"Create"
|
||||
]
|
||||
},
|
||||
{
|
||||
"description": "Append new windows to the focused window container",
|
||||
"type": "string",
|
||||
"enum": [
|
||||
"Append"
|
||||
]
|
||||
}
|
||||
]
|
||||
},
|
||||
"WorkspaceConfig": {
|
||||
"type": "object",
|
||||
"required": [
|
||||
"name"
|
||||
],
|
||||
"properties": {
|
||||
"container_padding": {
|
||||
"description": "Container padding (default: global)",
|
||||
"type": [
|
||||
"integer",
|
||||
"null"
|
||||
],
|
||||
"format": "int32"
|
||||
},
|
||||
"custom_layout": {
|
||||
"description": "Custom Layout (default: None)",
|
||||
"type": [
|
||||
"string",
|
||||
"null"
|
||||
]
|
||||
},
|
||||
"custom_layout_rules": {
|
||||
"description": "Layout rules (default: None)",
|
||||
"type": [
|
||||
"object",
|
||||
"null"
|
||||
],
|
||||
"additionalProperties": {
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"initial_workspace_rules": {
|
||||
"description": "Initial workspace application rules",
|
||||
"type": [
|
||||
"array",
|
||||
"null"
|
||||
],
|
||||
"items": {
|
||||
"$ref": "#/definitions/IdWithIdentifier"
|
||||
}
|
||||
},
|
||||
"layout": {
|
||||
"description": "Layout (default: BSP)",
|
||||
"anyOf": [
|
||||
{
|
||||
"$ref": "#/definitions/DefaultLayout"
|
||||
},
|
||||
{
|
||||
"type": "null"
|
||||
}
|
||||
]
|
||||
},
|
||||
"layout_rules": {
|
||||
"description": "Layout rules (default: None)",
|
||||
"type": [
|
||||
"object",
|
||||
"null"
|
||||
],
|
||||
"additionalProperties": {
|
||||
"$ref": "#/definitions/DefaultLayout"
|
||||
}
|
||||
},
|
||||
"name": {
|
||||
"description": "Name",
|
||||
"type": "string"
|
||||
},
|
||||
"workspace_padding": {
|
||||
"description": "Container padding (default: global)",
|
||||
"type": [
|
||||
"integer",
|
||||
"null"
|
||||
],
|
||||
"format": "int32"
|
||||
},
|
||||
"workspace_rules": {
|
||||
"description": "Permanent workspace application rules",
|
||||
"type": [
|
||||
"array",
|
||||
"null"
|
||||
],
|
||||
"items": {
|
||||
"$ref": "#/definitions/IdWithIdentifier"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user