diff --git a/README.md b/README.md index 6afc217c..5b0e61b4 100644 --- a/README.md +++ b/README.md @@ -267,6 +267,18 @@ komorebic.exe container-padding 0 komorebic.exe workspace padding 0 ``` +#### Multiple Layout Changes on Startup + +Depending on what is in your configuration, when `komorebi` is started, you may experience the layout rapidly being adjusted +with many retile events. + +If you would like to avoid this, you can start `komorebi` with a flag which tells `komorebi` to wait until all configuration +has been loaded before listening to and responding to window manager events: `komorebic start --await-configuration`. + +If you start `komorebi` with the `--await-configuration` flag, you _must_ send the `komorebic complete-configuration` command +at the end of your `komorebi.ahk` config. The layout will not be updated and `komorebi` will not respond to `komorebic` +commands until this command has been received. + #### Floating Windows Sometimes you will want a specific application to never be tiled, and instead float all the time. You add add rules to diff --git a/komorebi-core/src/lib.rs b/komorebi-core/src/lib.rs index f3c87c06..9dffef12 100644 --- a/komorebi-core/src/lib.rs +++ b/komorebi-core/src/lib.rs @@ -96,6 +96,7 @@ pub enum SocketMessage { // Configuration ReloadConfiguration, WatchConfiguration(bool), + CompleteConfiguration, InvisibleBorders(Rect), WorkAreaOffset(Rect), ResizeDelta(i32), diff --git a/komorebi/src/main.rs b/komorebi/src/main.rs index 9697aadc..fe2d5482 100644 --- a/komorebi/src/main.rs +++ b/komorebi/src/main.rs @@ -11,8 +11,6 @@ use std::sync::atomic::AtomicU32; use std::sync::atomic::Ordering; use std::sync::Arc; #[cfg(feature = "deadlock_detection")] -use std::thread; -#[cfg(feature = "deadlock_detection")] use std::time::Duration; use clap::Parser; @@ -20,6 +18,7 @@ use color_eyre::eyre::anyhow; use color_eyre::Result; use crossbeam_channel::Receiver; use crossbeam_channel::Sender; +use crossbeam_utils::Backoff; use lazy_static::lazy_static; #[cfg(feature = "deadlock_detection")] use parking_lot::deadlock; @@ -143,6 +142,7 @@ lazy_static! { }; } +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); @@ -242,6 +242,8 @@ pub fn load_configuration() -> Result<()> { Command::new("AutoHotkey64.exe") .arg(config_v2.as_os_str()) .output()?; + } else { + INITIAL_CONFIGURATION_LOADED.store(true, Ordering::SeqCst); }; Ok(()) @@ -341,9 +343,9 @@ pub fn notify_subscribers(notification: &str) -> Result<()> { #[tracing::instrument] fn detect_deadlocks() { // Create a background thread which checks for deadlocks every 10s - thread::spawn(move || loop { + std::thread::spawn(move || loop { tracing::info!("running deadlock detector"); - thread::sleep(Duration::from_secs(5)); + std::thread::sleep(Duration::from_secs(5)); let deadlocks = deadlock::check_deadlock(); if deadlocks.is_empty() { continue; @@ -364,8 +366,11 @@ fn detect_deadlocks() { #[clap(author, about, version)] struct Opts { /// Allow the use of komorebi's custom focus-follows-mouse implementation - #[clap(long = "ffm")] + #[clap(action, short, long = "ffm")] focus_follows_mouse: bool, + /// Wait for 'komorebic complete-configuration' to be sent before processing events + #[clap(action, short, long)] + await_configuration: bool, } #[tracing::instrument] @@ -374,7 +379,9 @@ fn main() -> Result<()> { CUSTOM_FFM.store(opts.focus_follows_mouse, Ordering::SeqCst); let arg_count = std::env::args().count(); - let has_valid_args = arg_count == 1 || (arg_count == 2 && CUSTOM_FFM.load(Ordering::SeqCst)); + let has_valid_args = arg_count == 1 + || (arg_count == 2 && (opts.await_configuration || opts.focus_follows_mouse)) + || (arg_count == 3 && opts.await_configuration && opts.focus_follows_mouse); if has_valid_args { let session_id = WindowsApi::process_id_to_session_id()?; @@ -421,14 +428,21 @@ fn main() -> Result<()> { wm.lock().init()?; listen_for_commands(wm.clone()); + std::thread::spawn(|| load_configuration().expect("could not load configuration")); + + if opts.await_configuration { + let backoff = Backoff::new(); + while !INITIAL_CONFIGURATION_LOADED.load(Ordering::SeqCst) { + backoff.snooze(); + } + } + listen_for_events(wm.clone()); if CUSTOM_FFM.load(Ordering::SeqCst) { listen_for_movements(wm.clone()); } - load_configuration()?; - let (ctrlc_sender, ctrlc_receiver) = crossbeam_channel::bounded(1); ctrlc::set_handler(move || { ctrlc_sender diff --git a/komorebi/src/process_command.rs b/komorebi/src/process_command.rs index 980c4068..def0bd6a 100644 --- a/komorebi/src/process_command.rs +++ b/komorebi/src/process_command.rs @@ -7,7 +7,6 @@ use std::num::NonZeroUsize; use std::str::FromStr; use std::sync::atomic::Ordering; use std::sync::Arc; -use std::thread; use color_eyre::eyre::anyhow; use color_eyre::Result; @@ -40,6 +39,7 @@ use crate::CUSTOM_FFM; use crate::FLOAT_IDENTIFIERS; use crate::HIDING_BEHAVIOUR; use crate::HOME_DIR; +use crate::INITIAL_CONFIGURATION_LOADED; use crate::LAYERED_WHITELIST; use crate::MANAGE_IDENTIFIERS; use crate::OBJECT_NAME_CHANGE_ON_LAUNCH; @@ -55,7 +55,7 @@ pub fn listen_for_commands(wm: Arc>) { .try_clone() .expect("could not clone unix listener"); - thread::spawn(move || { + std::thread::spawn(move || { tracing::info!("listening"); for client in listener.incoming() { match client { @@ -574,6 +574,12 @@ impl WindowManager { SocketMessage::ReloadConfiguration => { Self::reload_configuration(); } + SocketMessage::CompleteConfiguration => { + if !INITIAL_CONFIGURATION_LOADED.load(Ordering::SeqCst) { + INITIAL_CONFIGURATION_LOADED.store(true, Ordering::SeqCst); + self.update_focused_workspace(false)?; + } + } SocketMessage::WatchConfiguration(enable) => { self.watch_configuration(enable)?; } diff --git a/komorebi/src/process_event.rs b/komorebi/src/process_event.rs index 5ca67b86..e1d843e2 100644 --- a/komorebi/src/process_event.rs +++ b/komorebi/src/process_event.rs @@ -1,6 +1,5 @@ use std::fs::OpenOptions; use std::sync::Arc; -use std::thread; use color_eyre::eyre::anyhow; use color_eyre::Result; @@ -27,7 +26,7 @@ use crate::TRAY_AND_MULTI_WINDOW_IDENTIFIERS; pub fn listen_for_events(wm: Arc>) { let receiver = wm.lock().incoming_events.lock().clone(); - thread::spawn(move || { + std::thread::spawn(move || { tracing::info!("listening"); loop { select! { diff --git a/komorebi/src/window_manager.rs b/komorebi/src/window_manager.rs index b08c0d39..66b67181 100644 --- a/komorebi/src/window_manager.rs +++ b/komorebi/src/window_manager.rs @@ -3,7 +3,6 @@ use std::io::ErrorKind; use std::num::NonZeroUsize; use std::path::PathBuf; use std::sync::Arc; -use std::thread; use color_eyre::eyre::anyhow; use color_eyre::Result; @@ -191,14 +190,13 @@ impl WindowManager { pub fn init(&mut self) -> Result<()> { tracing::info!("initialising"); WindowsApi::load_monitor_information(&mut self.monitors)?; - WindowsApi::load_workspace_information(&mut self.monitors)?; - self.update_focused_workspace(false) + WindowsApi::load_workspace_information(&mut self.monitors) } #[tracing::instrument] pub fn reload_configuration() { tracing::info!("reloading configuration"); - thread::spawn(|| load_configuration().expect("could not load configuration")); + std::thread::spawn(|| load_configuration().expect("could not load configuration")); } #[tracing::instrument(skip(self))] @@ -247,7 +245,7 @@ impl WindowManager { // 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(_) => { - thread::spawn(|| { + std::thread::spawn(|| { load_configuration().expect("could not load configuration"); }); } diff --git a/komorebi/src/winevent_listener.rs b/komorebi/src/winevent_listener.rs index ce593212..ac1d0df0 100644 --- a/komorebi/src/winevent_listener.rs +++ b/komorebi/src/winevent_listener.rs @@ -1,7 +1,6 @@ use std::sync::atomic::AtomicIsize; use std::sync::atomic::Ordering; use std::sync::Arc; -use std::thread; use std::time::Duration; use crossbeam_channel::Receiver; @@ -44,7 +43,7 @@ impl WinEventListener { let hook = self.hook.clone(); let outgoing = self.outgoing_events.lock().clone(); - thread::spawn(move || unsafe { + std::thread::spawn(move || unsafe { let hook_ref = SetWinEventHook( EVENT_MIN as u32, EVENT_MAX as u32, @@ -96,7 +95,7 @@ impl MessageLoop { } } - thread::sleep(Duration::from_millis(sleep)); + std::thread::sleep(Duration::from_millis(sleep)); if !cb(value) { break; diff --git a/komorebi/src/workspace.rs b/komorebi/src/workspace.rs index 9240e919..8f24d7a4 100644 --- a/komorebi/src/workspace.rs +++ b/komorebi/src/workspace.rs @@ -1,5 +1,6 @@ use std::collections::VecDeque; use std::num::NonZeroUsize; +use std::sync::atomic::Ordering; use color_eyre::eyre::anyhow; use color_eyre::Result; @@ -21,6 +22,7 @@ use crate::container::Container; use crate::ring::Ring; use crate::window::Window; use crate::windows_api::WindowsApi; +use crate::INITIAL_CONFIGURATION_LOADED; #[derive(Debug, Clone, Serialize, Getters, CopyGetters, MutGetters, Setters, JsonSchema)] pub struct Workspace { @@ -149,6 +151,10 @@ impl Workspace { offset: Option, invisible_borders: &Rect, ) -> Result<()> { + if !INITIAL_CONFIGURATION_LOADED.load(Ordering::SeqCst) { + return Ok(()); + } + let container_padding = self.container_padding(); let mut adjusted_work_area = offset.map_or_else( || *work_area, diff --git a/komorebic.lib.sample.ahk b/komorebic.lib.sample.ahk index f0466f14..12f05307 100644 --- a/komorebic.lib.sample.ahk +++ b/komorebic.lib.sample.ahk @@ -1,7 +1,7 @@ ; Generated by komorebic.exe -Start(ffm) { - Run, komorebic.exe start --ffm %ffm%, , Hide +Start(ffm, await_configuration) { + Run, komorebic.exe start %ffm% --await_configuration %await_configuration%, , Hide } Stop() { @@ -252,6 +252,10 @@ WatchConfiguration(boolean_state) { Run, komorebic.exe watch-configuration %boolean_state%, , Hide } +CompleteConfiguration() { + Run, komorebic.exe complete-configuration, , Hide +} + WindowHidingBehaviour(hiding_behaviour) { Run, komorebic.exe window-hiding-behaviour %hiding_behaviour%, , Hide } diff --git a/komorebic/src/main.rs b/komorebic/src/main.rs index cd177841..1b04a964 100644 --- a/komorebic/src/main.rs +++ b/komorebic/src/main.rs @@ -396,8 +396,11 @@ struct FocusFollowsMouse { #[derive(Parser, AhkFunction)] struct Start { /// Allow the use of komorebi's custom focus-follows-mouse implementation - #[clap(long)] + #[clap(action, short, long = "ffm")] ffm: bool, + /// Wait for 'komorebic complete-configuration' to be sent before processing events + #[clap(action, short, long)] + await_configuration: bool, } #[derive(Parser, AhkFunction)] @@ -627,6 +630,8 @@ enum SubCommand { /// Enable or disable watching of ~/komorebi.ahk (if it exists) #[clap(arg_required_else_help = true)] WatchConfiguration(WatchConfiguration), + /// Signal that the final configuration option has been sent + CompleteConfiguration, /// Set the window behaviour when switching workspaces / cycling stacks #[clap(arg_required_else_help = true)] WindowHidingBehaviour(WindowHidingBehaviour), @@ -910,19 +915,33 @@ fn main() -> Result<()> { let script = exec.map_or_else( || { - if arg.ffm { - String::from( - "Start-Process komorebi.exe -ArgumentList '--ffm' -WindowStyle hidden", + if arg.ffm | arg.await_configuration { + format!( + "Start-Process komorebi.exe -ArgumentList {} -WindowStyle hidden", + if arg.ffm && arg.await_configuration { + "'--ffm','--await-configuration'" + } else if arg.ffm { + "'--ffm'" + } else { + "'--await-configuration'" + } ) } else { String::from("Start-Process komorebi.exe -WindowStyle hidden") } }, |exec| { - if arg.ffm { + if arg.ffm | arg.await_configuration { format!( - "Start-Process '{}' -ArgumentList '--ffm' -WindowStyle hidden", - exec + "Start-Process '{}' -ArgumentList {} -WindowStyle hidden", + exec, + if arg.ffm && arg.await_configuration { + "'--ffm','--await-configuration'" + } else if arg.ffm { + "'--ffm'" + } else { + "'--await-configuration'" + } ) } else { format!("Start-Process '{}' -WindowStyle hidden", exec) @@ -1107,6 +1126,9 @@ fn main() -> Result<()> { SubCommand::WatchConfiguration(arg) => { send_message(&SocketMessage::WatchConfiguration(arg.boolean_state.into()).as_bytes()?)?; } + SubCommand::CompleteConfiguration => { + send_message(&SocketMessage::CompleteConfiguration.as_bytes()?)?; + } SubCommand::IdentifyObjectNameChangeApplication(target) => { send_message( &SocketMessage::IdentifyObjectNameChangeApplication(target.identifier, target.id)