Skip to content

Commit 7bf1521

Browse files
committed
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.
1 parent b49e634 commit 7bf1521

File tree

4 files changed

+105
-0
lines changed

4 files changed

+105
-0
lines changed

komorebi/src/main.rs

+15
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@
77
clippy::doc_markdown
88
)]
99

10+
use std::env::temp_dir;
1011
use std::net::Shutdown;
1112
use std::path::PathBuf;
1213
use std::sync::atomic::Ordering;
@@ -43,6 +44,7 @@ use komorebi::stackbar_manager;
4344
use komorebi::static_config::StaticConfig;
4445
use komorebi::theme_manager;
4546
use komorebi::transparency_manager;
47+
use komorebi::window_manager::State;
4648
use komorebi::window_manager::WindowManager;
4749
use komorebi::windows_api::WindowsApi;
4850
use komorebi::winevent_listener;
@@ -156,6 +158,9 @@ struct Opts {
156158
/// Path to a static configuration JSON file
157159
#[clap(short, long)]
158160
config: Option<PathBuf>,
161+
/// Do not attempt to auto-apply a dumped state temp file from a previously running instance of komorebi
162+
#[clap(long)]
163+
clean_state: bool,
159164
}
160165

161166
#[tracing::instrument]
@@ -260,6 +265,13 @@ fn main() -> Result<()> {
260265
}
261266
}
262267

268+
let dumped_state = temp_dir().join("komorebi.state.json");
269+
270+
if !opts.clean_state && dumped_state.is_file() {
271+
let state: State = serde_json::from_str(&std::fs::read_to_string(&dumped_state)?)?;
272+
wm.lock().apply_state(state);
273+
}
274+
263275
wm.lock().retile_all(false)?;
264276

265277
listen_for_events(wm.clone());
@@ -290,6 +302,9 @@ fn main() -> Result<()> {
290302

291303
tracing::error!("received ctrl-c, restoring all hidden windows and terminating process");
292304

305+
let state = State::from(&*wm.lock());
306+
std::fs::write(dumped_state, serde_json::to_string_pretty(&state)?)?;
307+
293308
ANIMATION_ENABLED_PER_ANIMATION.lock().clear();
294309
ANIMATION_ENABLED_GLOBAL.store(false, Ordering::SeqCst);
295310
wm.lock().restore_all_windows()?;

komorebi/src/process_command.rs

+7
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
use std::collections::HashMap;
2+
use std::env::temp_dir;
23
use std::fs::File;
34
use std::fs::OpenOptions;
45
use std::io::BufRead;
@@ -916,6 +917,12 @@ impl WindowManager {
916917
"received stop command, restoring all hidden windows and terminating process"
917918
);
918919

920+
let state = &window_manager::State::from(&*self);
921+
std::fs::write(
922+
temp_dir().join("komorebi.state.json"),
923+
serde_json::to_string_pretty(&state)?,
924+
)?;
925+
919926
ANIMATION_ENABLED_PER_ANIMATION.lock().clear();
920927
ANIMATION_ENABLED_GLOBAL.store(false, Ordering::SeqCst);
921928
self.restore_all_windows()?;

komorebi/src/window_manager.rs

+76
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
use std::collections::HashMap;
22
use std::collections::HashSet;
33
use std::collections::VecDeque;
4+
use std::env::temp_dir;
45
use std::io::ErrorKind;
56
use std::num::NonZeroUsize;
67
use std::path::Path;
@@ -362,6 +363,81 @@ impl WindowManager {
362363
WindowsApi::load_workspace_information(&mut self.monitors)
363364
}
364365

366+
#[tracing::instrument(skip(self, state))]
367+
pub fn apply_state(&mut self, state: State) {
368+
let mut can_apply = true;
369+
370+
let state_monitors_len = state.monitors.elements().len();
371+
let current_monitors_len = self.monitors.elements().len();
372+
if state_monitors_len != current_monitors_len {
373+
tracing::warn!(
374+
"cannot apply state from {}; state file has {state_monitors_len} monitors, but only {current_monitors_len} are currently connected",
375+
temp_dir().join("komorebi.state.json").to_string_lossy()
376+
);
377+
378+
return;
379+
}
380+
381+
for monitor in state.monitors.elements() {
382+
for workspace in monitor.workspaces() {
383+
for container in workspace.containers() {
384+
for window in container.windows() {
385+
if window.exe().is_err() {
386+
can_apply = false;
387+
break;
388+
}
389+
}
390+
}
391+
392+
if let Some(window) = workspace.maximized_window() {
393+
if window.exe().is_err() {
394+
can_apply = false;
395+
break;
396+
}
397+
}
398+
399+
if let Some(container) = workspace.monocle_container() {
400+
for window in container.windows() {
401+
if window.exe().is_err() {
402+
can_apply = false;
403+
break;
404+
}
405+
}
406+
}
407+
408+
for window in workspace.floating_windows() {
409+
if window.exe().is_err() {
410+
can_apply = false;
411+
break;
412+
}
413+
}
414+
}
415+
}
416+
417+
if can_apply {
418+
tracing::info!(
419+
"applying state from {}",
420+
temp_dir().join("komorebi.state.json").to_string_lossy()
421+
);
422+
423+
for (monitor_idx, monitor) in self.monitors_mut().iter_mut().enumerate() {
424+
for (workspace_idx, workspace) in monitor.workspaces_mut().iter_mut().enumerate() {
425+
if let Some(state_monitor) = state.monitors.elements().get(monitor_idx) {
426+
if let Some(state_workspace) = state_monitor.workspaces().get(workspace_idx)
427+
{
428+
*workspace = state_workspace.clone();
429+
}
430+
}
431+
}
432+
}
433+
} else {
434+
tracing::warn!(
435+
"cannot apply state from {}; some windows referenced in the state file no longer exist",
436+
temp_dir().join("komorebi.state.json").to_string_lossy()
437+
);
438+
}
439+
}
440+
365441
#[tracing::instrument]
366442
pub fn reload_configuration() {
367443
tracing::info!("reloading configuration");

komorebic/src/main.rs

+7
Original file line numberDiff line numberDiff line change
@@ -782,6 +782,9 @@ struct Start {
782782
/// Start masir in a background process for focus-follows-mouse
783783
#[clap(long)]
784784
masir: bool,
785+
/// Do not attempt to auto-apply a dumped state temp file from a previously running instance of komorebi
786+
#[clap(long)]
787+
clean_state: bool,
785788
}
786789

787790
#[derive(Parser)]
@@ -2012,6 +2015,10 @@ fn main() -> Result<()> {
20122015
flags.push(format!("'--tcp-port={port}'"));
20132016
}
20142017

2018+
if arg.clean_state {
2019+
flags.push("'--clean-state'".to_string());
2020+
}
2021+
20152022
let script = if flags.is_empty() {
20162023
format!(
20172024
"Start-Process '{}' -WindowStyle hidden",

0 commit comments

Comments
 (0)