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