Skip to content

Commit

Permalink
feat(wm): dump and load previous instance state
Browse files Browse the repository at this point in the history
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.
  • Loading branch information
LGUG2Z committed Dec 17, 2024
1 parent b49e634 commit 7bf1521
Show file tree
Hide file tree
Showing 4 changed files with 105 additions and 0 deletions.
15 changes: 15 additions & 0 deletions komorebi/src/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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;
Expand Down Expand Up @@ -156,6 +158,9 @@ struct Opts {
/// Path to a static configuration JSON file
#[clap(short, long)]
config: Option<PathBuf>,
/// 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]
Expand Down Expand Up @@ -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());
Expand Down Expand Up @@ -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()?;
Expand Down
7 changes: 7 additions & 0 deletions komorebi/src/process_command.rs
Original file line number Diff line number Diff line change
@@ -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;
Expand Down Expand Up @@ -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()?;
Expand Down
76 changes: 76 additions & 0 deletions komorebi/src/window_manager.rs
Original file line number Diff line number Diff line change
@@ -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;
Expand Down Expand Up @@ -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");
Expand Down
7 changes: 7 additions & 0 deletions komorebic/src/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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)]
Expand Down Expand Up @@ -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",
Expand Down

0 comments on commit 7bf1521

Please sign in to comment.