Compare commits

...

1 Commits

Author SHA1 Message Date
LGUG2Z
41e7b4a9a0 feat(config): add static json loader and whkd flag
This commit is an implementation of a static JSON configuration loader.

An example komorebi.json configuration file has been added.

The application-specific configurations can be loaded directly from a
file, and workspace configuration can be defined declaratively in the
JSON. Individual rules etc. can also be added directly in the static
configuration as one-offs.

A JSONSchema can be generated using komorebic's static-config-schema
command. This should be added to something like SchemaStore later.

Loading from static configuration is significantly faster on startup, as
the lock does not have to be reacquired for every command that is sent
over the socket.

When loading configuration from a static JSON file, a hotwatch instance
will automatically be created to listen to file changes and apply any
updates to both the global and window manager configuration state.

A new --whkd flag has been added to the komorebic start command to
optionally start whkd in a background process.

A new komorebic command 'generate-static-config' has been added to help
existing users migrate to a static JSON config file. Currently, custom
layout file path information can not be automatically populated in the
output of this command and must be added manually by the user if
required.

A new komorebic command 'fetch-asc' has been added to help users update
to the latest version of the application-specific configurations
in-place.

re #427
2023-07-13 08:11:21 -07:00
17 changed files with 2576 additions and 358 deletions

944
Cargo.lock generated

File diff suppressed because it is too large Load Diff

View File

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

View File

@@ -161,7 +161,13 @@ This means that:
### Quickstart
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 +179,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.example.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,16 +191,16 @@ 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
to WinGet.
You can watch a walkthrough video of this quickstart below on YouTube.
<!-- You can watch a walkthrough video of this quickstart below on YouTube. -->
[![Watch the quickstart walkthrough video](https://img.youtube.com/vi/cBnLIwMtv8g/hqdefault.jpg)](https://www.youtube.com/watch?v=cBnLIwMtv8g)
<!-- [![Watch the quickstart walkthrough video](https://img.youtube.com/vi/cBnLIwMtv8g/hqdefault.jpg)](https://www.youtube.com/watch?v=cBnLIwMtv8g) -->
#### Using Autohotkey
@@ -204,7 +210,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 +247,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 +262,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 +371,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 +444,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.

View File

@@ -13,9 +13,4 @@ serde_json = "1"
serde_yaml = "0.9"
strum = { version = "0.25", features = ["derive"] }
schemars = "0.8"
[dependencies.windows]
version = "0.48"
features = [
"Win32_Foundation",
]
windows = { workspace = true }

View File

@@ -78,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)?)
}

View File

@@ -118,6 +118,7 @@ pub enum SocketMessage {
ClearNamedWorkspaceLayoutRules(String),
// Configuration
ReloadConfiguration,
ReloadStaticConfiguration(PathBuf),
WatchConfiguration(bool),
CompleteConfiguration,
AltFocusHack(bool),
@@ -151,6 +152,8 @@ pub enum SocketMessage {
RemoveSubscriber(String),
NotificationSchema,
SocketSchema,
StaticConfigSchema,
GenerateStaticConfig,
}
impl SocketMessage {
@@ -192,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,
}
@@ -204,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,
}
@@ -213,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,
}
@@ -222,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,
}
@@ -231,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,
}
@@ -241,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,
}

View File

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

View File

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

View File

@@ -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![
@@ -160,6 +164,9 @@ lazy_static! {
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);
@@ -411,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");
});
@@ -496,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);
}

View File

@@ -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;
@@ -684,6 +685,7 @@ 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.0 / (custom.len() as f32));
@@ -877,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);
@@ -1108,6 +1113,25 @@ 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) {
@@ -1250,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,

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

View File

@@ -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;
@@ -234,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();

View File

@@ -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,8 +21,11 @@ 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;
@@ -77,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,
@@ -87,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() {

View File

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

View File

@@ -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;
@@ -610,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)]
@@ -1018,10 +1025,17 @@ 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<()> {
@@ -1030,7 +1044,7 @@ pub fn send_message(bytes: &[u8]) -> Result<()> {
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();
@@ -1377,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
@@ -1400,63 +1418,40 @@ 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 '{exec}' -WindowStyle hidden")
}
},
);
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;
@@ -1483,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()?)?;
@@ -1735,7 +1747,7 @@ fn main() -> Result<()> {
_ => {
return Err(anyhow!(
"this command requires applications to be identified by their exe"
))
));
}
}
@@ -1889,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;
@@ -1942,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
View 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"
}
}
}
}
}
}