From 7bf152136353dd988246c10af1fd9430fb7bf858 Mon Sep 17 00:00:00 2001 From: LGUG2Z Date: Tue, 17 Dec 2024 08:24:39 -0800 Subject: [PATCH] feat(wm): dump and load previous instance state This commit adds changes to the main wm process to dump a state file to temp_dir() when the process is exited either via komorebic stop or ctrl-c, and to automatically try to reload that dumped state file if it exists on the next run. A new flag "--clean-state" has been added to both komorebi.exe and the komorebic start command to override this behaviour. The dumped state file can only be applied if the number of connected monitors matches the number of monitors recorded in the state, and if every HWND listed in the state file still exists. This is validated by calling Window.exe(), which under the hood checks for the continued existence of the process associated with the HWND. Only the "workspace" subsection of the state for each matching connecting monitor will be applied. --- komorebi/src/main.rs | 15 +++++++ komorebi/src/process_command.rs | 7 +++ komorebi/src/window_manager.rs | 76 +++++++++++++++++++++++++++++++++ komorebic/src/main.rs | 7 +++ 4 files changed, 105 insertions(+) diff --git a/komorebi/src/main.rs b/komorebi/src/main.rs index 4f601975a..a2a642621 100644 --- a/komorebi/src/main.rs +++ b/komorebi/src/main.rs @@ -7,6 +7,7 @@ clippy::doc_markdown )] +use std::env::temp_dir; use std::net::Shutdown; use std::path::PathBuf; use std::sync::atomic::Ordering; @@ -43,6 +44,7 @@ use komorebi::stackbar_manager; use komorebi::static_config::StaticConfig; use komorebi::theme_manager; use komorebi::transparency_manager; +use komorebi::window_manager::State; use komorebi::window_manager::WindowManager; use komorebi::windows_api::WindowsApi; use komorebi::winevent_listener; @@ -156,6 +158,9 @@ struct Opts { /// Path to a static configuration JSON file #[clap(short, long)] config: Option, + /// Do not attempt to auto-apply a dumped state temp file from a previously running instance of komorebi + #[clap(long)] + clean_state: bool, } #[tracing::instrument] @@ -260,6 +265,13 @@ fn main() -> Result<()> { } } + let dumped_state = temp_dir().join("komorebi.state.json"); + + if !opts.clean_state && dumped_state.is_file() { + let state: State = serde_json::from_str(&std::fs::read_to_string(&dumped_state)?)?; + wm.lock().apply_state(state); + } + wm.lock().retile_all(false)?; listen_for_events(wm.clone()); @@ -290,6 +302,9 @@ fn main() -> Result<()> { tracing::error!("received ctrl-c, restoring all hidden windows and terminating process"); + let state = State::from(&*wm.lock()); + std::fs::write(dumped_state, serde_json::to_string_pretty(&state)?)?; + ANIMATION_ENABLED_PER_ANIMATION.lock().clear(); ANIMATION_ENABLED_GLOBAL.store(false, Ordering::SeqCst); wm.lock().restore_all_windows()?; diff --git a/komorebi/src/process_command.rs b/komorebi/src/process_command.rs index 456154138..da5b89b59 100644 --- a/komorebi/src/process_command.rs +++ b/komorebi/src/process_command.rs @@ -1,4 +1,5 @@ use std::collections::HashMap; +use std::env::temp_dir; use std::fs::File; use std::fs::OpenOptions; use std::io::BufRead; @@ -916,6 +917,12 @@ impl WindowManager { "received stop command, restoring all hidden windows and terminating process" ); + let state = &window_manager::State::from(&*self); + std::fs::write( + temp_dir().join("komorebi.state.json"), + serde_json::to_string_pretty(&state)?, + )?; + ANIMATION_ENABLED_PER_ANIMATION.lock().clear(); ANIMATION_ENABLED_GLOBAL.store(false, Ordering::SeqCst); self.restore_all_windows()?; diff --git a/komorebi/src/window_manager.rs b/komorebi/src/window_manager.rs index 0ee3172c2..60d2f2654 100644 --- a/komorebi/src/window_manager.rs +++ b/komorebi/src/window_manager.rs @@ -1,6 +1,7 @@ use std::collections::HashMap; use std::collections::HashSet; use std::collections::VecDeque; +use std::env::temp_dir; use std::io::ErrorKind; use std::num::NonZeroUsize; use std::path::Path; @@ -362,6 +363,81 @@ impl WindowManager { WindowsApi::load_workspace_information(&mut self.monitors) } + #[tracing::instrument(skip(self, state))] + pub fn apply_state(&mut self, state: State) { + let mut can_apply = true; + + let state_monitors_len = state.monitors.elements().len(); + let current_monitors_len = self.monitors.elements().len(); + if state_monitors_len != current_monitors_len { + tracing::warn!( + "cannot apply state from {}; state file has {state_monitors_len} monitors, but only {current_monitors_len} are currently connected", + temp_dir().join("komorebi.state.json").to_string_lossy() + ); + + return; + } + + for monitor in state.monitors.elements() { + for workspace in monitor.workspaces() { + for container in workspace.containers() { + for window in container.windows() { + if window.exe().is_err() { + can_apply = false; + break; + } + } + } + + if let Some(window) = workspace.maximized_window() { + if window.exe().is_err() { + can_apply = false; + break; + } + } + + if let Some(container) = workspace.monocle_container() { + for window in container.windows() { + if window.exe().is_err() { + can_apply = false; + break; + } + } + } + + for window in workspace.floating_windows() { + if window.exe().is_err() { + can_apply = false; + break; + } + } + } + } + + if can_apply { + tracing::info!( + "applying state from {}", + temp_dir().join("komorebi.state.json").to_string_lossy() + ); + + for (monitor_idx, monitor) in self.monitors_mut().iter_mut().enumerate() { + for (workspace_idx, workspace) in monitor.workspaces_mut().iter_mut().enumerate() { + if let Some(state_monitor) = state.monitors.elements().get(monitor_idx) { + if let Some(state_workspace) = state_monitor.workspaces().get(workspace_idx) + { + *workspace = state_workspace.clone(); + } + } + } + } + } else { + tracing::warn!( + "cannot apply state from {}; some windows referenced in the state file no longer exist", + temp_dir().join("komorebi.state.json").to_string_lossy() + ); + } + } + #[tracing::instrument] pub fn reload_configuration() { tracing::info!("reloading configuration"); diff --git a/komorebic/src/main.rs b/komorebic/src/main.rs index 572a9f356..309204a50 100644 --- a/komorebic/src/main.rs +++ b/komorebic/src/main.rs @@ -782,6 +782,9 @@ struct Start { /// Start masir in a background process for focus-follows-mouse #[clap(long)] masir: bool, + /// Do not attempt to auto-apply a dumped state temp file from a previously running instance of komorebi + #[clap(long)] + clean_state: bool, } #[derive(Parser)] @@ -2012,6 +2015,10 @@ fn main() -> Result<()> { flags.push(format!("'--tcp-port={port}'")); } + if arg.clean_state { + flags.push("'--clean-state'".to_string()); + } + let script = if flags.is_empty() { format!( "Start-Process '{}' -WindowStyle hidden",