From b9a40924a8a78f9d5c9ef8325b415edeacacf802 Mon Sep 17 00:00:00 2001 From: LGUG2Z Date: Wed, 22 Sep 2021 08:27:04 -0700 Subject: [PATCH] feat(wm): add saving/loading of layouts to file This commit expands on the autosave/load functionality to allow saving and loading layouts from any file. Handling relative paths and paths with ~ on Windows is a little tricky so I added a helper fn to komorebic to deal with this, ensuring all the processing happens in komorebic before the messages get sent to komorebi for processing. There will still some lingering uses of ContextCompat around the codebase which I also took the opportunity to clean up and replace with ok_or_else + anyhow!(). windows-rs is also updated to 0.20.1 in the lockfile. resolve #41 --- Cargo.lock | 20 ++++---- README.md | 27 +++++++++- komorebi-core/src/lib.rs | 3 ++ komorebi/src/process_command.rs | 23 +++++++++ komorebi/src/window_manager.rs | 7 ++- komorebi/src/workspace.rs | 9 ++-- komorebic.lib.sample.ahk | 16 ++++-- komorebic/src/main.rs | 90 ++++++++++++++++++++++++++------- 8 files changed, 154 insertions(+), 41 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index a74e19cc..c889f847 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1452,9 +1452,9 @@ checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f" [[package]] name = "windows" -version = "0.20.0" +version = "0.20.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0a0b63f34b1cf0fcb7a2e387189936a7c9822123ef124a95da2b8a0b493bc69d" +checksum = "d7524f6f9074f6326a1c167cd3dc2ed4e6916648a1a55116d029620af9b65fb1" dependencies = [ "const-sha1", "windows_gen", @@ -1463,9 +1463,9 @@ dependencies = [ [[package]] name = "windows_gen" -version = "0.20.0" +version = "0.20.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7213e17fead412ec608804cbe190988db6f40b2a946ef58dd67fd9cdf39da144" +checksum = "4be44a189bde96fc0e0cdd5b152b2d21c635c0c94c7d256aab4425477b2a2f37" dependencies = [ "windows_quote", "windows_reader", @@ -1473,9 +1473,9 @@ dependencies = [ [[package]] name = "windows_macros" -version = "0.20.0" +version = "0.20.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "661a56e1edb9f9d466a9cb59c392edfad0d273b66bb20b1f5f4aea6db5ad35d6" +checksum = "cc1d78ce8a43d45b8da282383a2cb2ffcd5587cc3a9c341125d3181d2b701ede" dependencies = [ "syn", "windows_gen", @@ -1485,15 +1485,15 @@ dependencies = [ [[package]] name = "windows_quote" -version = "0.20.0" +version = "0.20.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6d16ae0ecb5b0a365ff465ca9b9780e70986f951b4e06a95f87ac54a421d3767" +checksum = "51fa2185b18a6164a3fa3ea2b6c92ebc1b60f532ae5a85c57408ba6a5a064913" [[package]] name = "windows_reader" -version = "0.20.0" +version = "0.20.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c75040b326c26dda15a9c18970a7a15bf503dc22597d55dd559df16435f4a550" +checksum = "3daa5bd758f2f8f20cd93a79aedca20759779f43785fc77b08a4e8e1e5876bbb" [[package]] name = "winput" diff --git a/README.md b/README.md index 401075ff..005696cb 100644 --- a/README.md +++ b/README.md @@ -184,6 +184,26 @@ passing it as an argument to the `--implementation` flag: komorebic.exe toggle-focus-follows-mouse --implementation komorebi ``` +#### Saving and Loading Resized Layouts + +If you create a BSP layout through various resize adjustments that you want to be able to restore easily in the future, +it is possible to "quicksave" that layout to the system's temporary folder and load it later in the same session, or +alternatively, you may save it to a specific file to be loaded again at any point in the future. + +```powershell +komorebic.exe quick-save # saves the focused workspace to $Env:TEMP\komorebi.quicksave.json +komorebic.exe quick-load # loads $Env:TEMP\komorebi.quicksave.json on the focused workspace + +komorebic.exe save ~/layouts/primary.json # saves the focused workspace to $Env:USERPROFILE\layouts\primary.json +komorebic.exe load ~/layouts/secondary.json # loads $Env:USERPROFILE\layouts\secondary.json on the focused workspace +``` + +These layouts can be applied to arbitrary collections of windows on any workspace, as they only track the layout +dimensions and are not coupled to the applications that were running at the time of saving. + +When layouts that expect more or less windows than the number currently on the focused workspace are loaded, `komorebi` +will automatically reconcile the difference. + ## Configuration with `komorebic` As previously mentioned, this project does not handle anything related to keybindings and shortcuts directly. I @@ -198,10 +218,12 @@ each command. start Start komorebi.exe as a background process stop Stop the komorebi.exe process and restore all hidden windows state Show a JSON representation of the current window manager state -quick-save Quicksave the current resize layout dimensions -quick-load Load the last quicksaved resize layout dimensions query Query the current window manager state log Tail komorebi.exe's process logs (cancel with Ctrl-C) +quick-save Quicksave the current resize layout dimensions +quick-load Load the last quicksaved resize layout dimensions +save Save the current resize layout dimensions to a file +load Load the resize layout dimensions from a file focus Change focus to the window in the specified direction move Move the focused window in the specified direction stack Stack the focused window in the specified direction @@ -275,6 +297,7 @@ used [is available here](komorebi.sample.with.lib.ahk). - [x] Resize window container in direction - [ ] Resize child window containers by split ratio - [x] Quicksave and quickload layouts with resize dimensions +- [x] Save and load layouts with resize dimensions to/from specific files - [x] Mouse drag to swap window container position - [x] Mouse drag to resize window container - [x] Configurable workspace and container gaps diff --git a/komorebi-core/src/lib.rs b/komorebi-core/src/lib.rs index dbe001df..32968226 100644 --- a/komorebi-core/src/lib.rs +++ b/komorebi-core/src/lib.rs @@ -1,6 +1,7 @@ #![warn(clippy::all, clippy::nursery, clippy::pedantic)] #![allow(clippy::missing_errors_doc)] +use std::path::PathBuf; use std::str::FromStr; use clap::ArgEnum; @@ -54,6 +55,8 @@ pub enum SocketMessage { Retile, QuickSave, QuickLoad, + Save(PathBuf), + Load(PathBuf), FocusMonitorNumber(usize), FocusWorkspaceNumber(usize), ContainerPadding(usize, usize, i32), diff --git a/komorebi/src/process_command.rs b/komorebi/src/process_command.rs index 6f1f4ad6..8df7d627 100644 --- a/komorebi/src/process_command.rs +++ b/komorebi/src/process_command.rs @@ -359,6 +359,29 @@ impl WindowManager { let resize: Vec> = serde_json::from_reader(file)?; + workspace.set_resize_dimensions(resize); + self.update_focused_workspace(false)?; + } + SocketMessage::Save(path) => { + let workspace = self.focused_workspace_mut()?; + let resize = workspace.resize_dimensions(); + + let file = OpenOptions::new() + .write(true) + .truncate(true) + .create(true) + .open(path)?; + + serde_json::to_writer_pretty(&file, &resize)?; + } + SocketMessage::Load(path) => { + let workspace = self.focused_workspace_mut()?; + + let file = File::open(&path) + .map_err(|_| anyhow!("no file found at {}", path.display().to_string()))?; + + let resize: Vec> = serde_json::from_reader(file)?; + workspace.set_resize_dimensions(resize); self.update_focused_workspace(false)?; } diff --git a/komorebi/src/window_manager.rs b/komorebi/src/window_manager.rs index 99ed6011..38a81b32 100644 --- a/komorebi/src/window_manager.rs +++ b/komorebi/src/window_manager.rs @@ -6,7 +6,6 @@ use std::sync::Arc; use std::thread; use color_eyre::eyre::anyhow; -use color_eyre::eyre::ContextCompat; use color_eyre::Result; use crossbeam_channel::Receiver; use hotwatch::notify::DebouncedEvent; @@ -591,9 +590,9 @@ impl WindowManager { ) { let unaltered = workspace.layout().calculate( &work_area, - NonZeroUsize::new(len).context( - "there must be at least one container to calculate a workspace layout", - )?, + NonZeroUsize::new(len).ok_or_else(|| { + anyhow!("there must be at least one container to calculate a workspace layout") + })?, workspace.container_padding(), workspace.layout_flip(), &[], diff --git a/komorebi/src/workspace.rs b/komorebi/src/workspace.rs index f4c6a2f1..4b6b559f 100644 --- a/komorebi/src/workspace.rs +++ b/komorebi/src/workspace.rs @@ -2,7 +2,6 @@ use std::collections::VecDeque; use std::num::NonZeroUsize; use color_eyre::eyre::anyhow; -use color_eyre::eyre::ContextCompat; use color_eyre::Result; use getset::CopyGetters; use getset::Getters; @@ -154,9 +153,11 @@ impl Workspace { } else if !self.containers().is_empty() { let layouts = self.layout().calculate( &adjusted_work_area, - NonZeroUsize::new(self.containers().len()).context( - "there must be at least one container to calculate a workspace layout", - )?, + NonZeroUsize::new(self.containers().len()).ok_or_else(|| { + anyhow!( + "there must be at least one container to calculate a workspace layout" + ) + })?, self.container_padding(), self.layout_flip(), self.resize_dimensions(), diff --git a/komorebic.lib.sample.ahk b/komorebic.lib.sample.ahk index f7f0ea42..97d978d4 100644 --- a/komorebic.lib.sample.ahk +++ b/komorebic.lib.sample.ahk @@ -12,6 +12,14 @@ State() { Run, komorebic.exe state, , Hide } +Query(state_query) { + Run, komorebic.exe query %state_query%, , Hide +} + +Log() { + Run, komorebic.exe log, , Hide +} + QuickSave() { Run, komorebic.exe quick-save, , Hide } @@ -20,12 +28,12 @@ QuickLoad() { Run, komorebic.exe quick-load, , Hide } -Query(state_query) { - Run, komorebic.exe query %state_query%, , Hide +Save(path) { + Run, komorebic.exe save %path%, , Hide } -Log() { - Run, komorebic.exe log, , Hide +Load(path) { + Run, komorebic.exe load %path%, , Hide } Focus(operation_direction) { diff --git a/komorebic/src/main.rs b/komorebic/src/main.rs index 658270c6..68dff9db 100644 --- a/komorebic/src/main.rs +++ b/komorebic/src/main.rs @@ -13,7 +13,7 @@ use std::process::Command; use clap::AppSettings; use clap::ArgEnum; use clap::Clap; -use color_eyre::eyre::ContextCompat; +use color_eyre::eyre::anyhow; use color_eyre::Result; use fs_tail::TailedFile; use heck::KebabCase; @@ -268,6 +268,18 @@ struct Start { ffm: bool, } +#[derive(Clap, AhkFunction)] +struct Save { + /// File to which the resize layout dimensions should be saved + path: String, +} + +#[derive(Clap, AhkFunction)] +struct Load { + /// File from which the resize layout dimensions should be loaded + path: String, +} + #[derive(Clap)] #[clap(author, about, version, setting = AppSettings::DeriveDisplayOrder)] struct Opts { @@ -283,15 +295,21 @@ enum SubCommand { Stop, /// Show a JSON representation of the current window manager state State, - /// Quicksave the current resize layout dimensions - QuickSave, - /// Load the last quicksaved resize layout dimensions - QuickLoad, /// Query the current window manager state #[clap(setting = AppSettings::ArgRequiredElseHelp)] Query(Query), /// Tail komorebi.exe's process logs (cancel with Ctrl-C) Log, + /// Quicksave the current resize layout dimensions + QuickSave, + /// Load the last quicksaved resize layout dimensions + QuickLoad, + /// Save the current resize layout dimensions to a file + #[clap(setting = AppSettings::ArgRequiredElseHelp)] + Save(Save), + /// Load the resize layout dimensions from a file + #[clap(setting = AppSettings::ArgRequiredElseHelp)] + Load(Load), /// Change focus to the window in the specified direction #[clap(setting = AppSettings::ArgRequiredElseHelp)] Focus(Focus), @@ -413,7 +431,7 @@ enum SubCommand { } pub fn send_message(bytes: &[u8]) -> Result<()> { - let mut socket = dirs::home_dir().context("there is no home directory")?; + let mut socket = dirs::home_dir().ok_or_else(|| anyhow!("there is no home directory"))?; socket.push("komorebi.sock"); let socket = socket.as_path(); @@ -427,7 +445,8 @@ fn main() -> Result<()> { match opts.subcmd { SubCommand::AhkLibrary => { - let mut library = dirs::home_dir().context("there is no home directory")?; + let mut library = + dirs::home_dir().ok_or_else(|| anyhow!("there is no home directory"))?; library.push("komorebic.lib.ahk"); let mut file = OpenOptions::new() .write(true) @@ -439,9 +458,9 @@ fn main() -> Result<()> { println!( "\nAHK helper library for komorebic written to {}", - library - .to_str() - .context("could not find the path to the generated ahk lib file")? + library.to_str().ok_or_else(|| anyhow!( + "could not find the path to the generated ahk lib file" + ))? ); println!( @@ -559,10 +578,9 @@ fn main() -> Result<()> { buf.pop(); // %USERPROFILE%\scoop\shims buf.pop(); // %USERPROFILE%\scoop buf.push("apps\\komorebi\\current\\komorebi.exe"); //%USERPROFILE%\scoop\komorebi\current\komorebi.exe - Option::from( - buf.to_str() - .context("cannot create a string from the scoop komorebi path")?, - ) + Option::from(buf.to_str().ok_or_else(|| { + anyhow!("cannot create a string from the scoop komorebi path") + })?) } } } else { @@ -652,7 +670,7 @@ fn main() -> Result<()> { )?; } SubCommand::State => { - let home = dirs::home_dir().context("there is no home directory")?; + let home = dirs::home_dir().ok_or_else(|| anyhow!("there is no home directory"))?; let mut socket = home; socket.push("komorebic.sock"); let socket = socket.as_path(); @@ -686,7 +704,7 @@ fn main() -> Result<()> { } } SubCommand::Query(arg) => { - let home = dirs::home_dir().context("there is no home directory")?; + let home = dirs::home_dir().ok_or_else(|| anyhow!("there is no home directory"))?; let mut socket = home; socket.push("komorebic.sock"); let socket = socket.as_path(); @@ -720,7 +738,8 @@ fn main() -> Result<()> { } } SubCommand::RestoreWindows => { - let mut hwnd_json = dirs::home_dir().context("there is no home directory")?; + let mut hwnd_json = + dirs::home_dir().ok_or_else(|| anyhow!("there is no home directory"))?; hwnd_json.push("komorebi.hwnd.json"); let file = File::open(hwnd_json)?; @@ -777,11 +796,48 @@ fn main() -> Result<()> { SubCommand::QuickLoad => { send_message(&*SocketMessage::QuickLoad.as_bytes()?)?; } + SubCommand::Save(arg) => { + send_message(&*SocketMessage::Save(resolve_windows_path(&arg.path)?).as_bytes()?)?; + } + SubCommand::Load(arg) => { + send_message(&*SocketMessage::Load(resolve_windows_path(&arg.path)?).as_bytes()?)?; + } } Ok(()) } +fn resolve_windows_path(raw_path: &str) -> Result { + let path = if raw_path.starts_with('~') { + raw_path.replacen( + "~", + &dirs::home_dir() + .ok_or_else(|| anyhow!("there is no home directory"))? + .display() + .to_string(), + 1, + ) + } else { + raw_path.to_string() + }; + + let full_path = PathBuf::from(path); + + let parent = full_path + .parent() + .ok_or_else(|| anyhow!("cannot parse directory"))?; + + let file = full_path + .components() + .last() + .ok_or_else(|| anyhow!("cannot parse filename"))?; + + let mut canonicalized = std::fs::canonicalize(parent)?; + canonicalized.push(file); + + Ok(canonicalized) +} + fn show_window(hwnd: HWND, command: SHOW_WINDOW_CMD) { // BOOL is returned but does not signify whether or not the operation was succesful // https://docs.microsoft.com/en-us/windows/win32/api/winuser/nf-winuser-showwindow