Compare commits

...

6 Commits

Author SHA1 Message Date
LGUG2Z
644f7ee604 chore(release): v0.1.5 2021-09-22 08:43:51 -07:00
LGUG2Z
b9a40924a8 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
2021-09-22 08:31:50 -07:00
LGUG2Z
80bcb51f75 feat(wm): add quicksaving/loading of sizes/layouts
This commit adds two new komorebic commands to quicksave and quickload
BSP layouts with custom resize dimensions. The quicksave file is stored
at ${Env:TEMP}/komorebi.quicksave.json, and is a Vec<Option<Rect>>
serialized to JSON.

If a user tries to quickload without a quicksave file being present, an
error will be logged.

At this point there is only one quicksave file which will always be
overwritten whenever the quicksave command is called. Both commands will
only operate on the focused workspace of the focused monitor.

This means that you can quicksave a layout on one workspace, and then
quickload it onto multiple other workspaces (individually) on the same
or other monitors.

If the number of elements in the deserialized Vec is greater than the
number of containers on a workspace, the Vec will be truncated when
Workspace.update is run, and similarly if the number of elements is less
than the number of containers on a workspace, the Vec will be extended
by the difference using None values.

resolve #39
2021-09-21 17:12:18 -07:00
LGUG2Z
e10e11d1de fix(wm): preserve resize dimensions on promotion
Whatever resize dimensions are at the front of the workspace were
previously being thrown away and overwritten with None whenever a
Promote command was being handled.

This commit preserves any resize dimensions that may already be there
and restores them after the container promotion has been completed.

fix #40
2021-09-21 10:07:15 -07:00
LGUG2Z
2807cafdd0 refactor(windows_api): use handle trait from 0.20
The 0.20.0 release of windows-rs includes a Handle trait which provides
ok() and invalid() fns for implementors, including HWND and HANDLE.

This is pretty cool (and also a big breaking change since the release
takes away is_null() at the same time...), so the code in windows_api.rs
has been updated to make use of this by implementing a
ProcessWindowsCrateResult trait with a process() fn.

When implemented for a windows::Result<T>, it will do any required
processing for T, and ensure that windows::Error is converted to an
eyre-compatible Report.

Switching to this means that I have been able to get rid of some of the
hacky error handling for weird behaviours encountered previously. So
far, they don't seem to be presenting again, but I will run with this
build for a couple of days to see if the false-negative errors are
really gone for good with this update.
2021-09-20 18:41:31 -07:00
LGUG2Z
63cf48daa5 fix(wm): ensure idx < len before container removal
fix #38
2021-09-19 11:05:09 -07:00
15 changed files with 317 additions and 144 deletions

34
Cargo.lock generated
View File

@@ -505,7 +505,7 @@ dependencies = [
[[package]]
name = "komorebi"
version = "0.1.4"
version = "0.1.5"
dependencies = [
"bindings",
"bitflags",
@@ -537,7 +537,7 @@ dependencies = [
[[package]]
name = "komorebi-core"
version = "0.1.4"
version = "0.1.5"
dependencies = [
"bindings",
"clap",
@@ -549,7 +549,7 @@ dependencies = [
[[package]]
name = "komorebic"
version = "0.1.4"
version = "0.1.5"
dependencies = [
"bindings",
"clap",
@@ -1245,9 +1245,9 @@ dependencies = [
[[package]]
name = "tracing"
version = "0.1.27"
version = "0.1.28"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c2ba9ab62b7d6497a8638dfda5e5c4fb3b2d5a7fca4118f2b96151c8ef1a437e"
checksum = "84f96e095c0c82419687c20ddf5cb3eadb61f4e1405923c9dc8e53a1adacbda8"
dependencies = [
"cfg-if 1.0.0",
"pin-project-lite",
@@ -1319,9 +1319,9 @@ dependencies = [
[[package]]
name = "tracing-subscriber"
version = "0.2.23"
version = "0.2.24"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "56c42e73a9d277d4d2b6a88389a137ccf3c58599660b17e8f5fc39305e490669"
checksum = "fdd0568dbfe3baf7048b7908d2b32bca0d81cd56bec6d2a8f894b01d74f86be3"
dependencies = [
"ansi_term",
"chrono",
@@ -1452,9 +1452,9 @@ checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f"
[[package]]
name = "windows"
version = "0.19.0"
version = "0.20.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ef84dd25f4c69a271b1bba394532bf400523b43169de21dfc715e8f8e491053d"
checksum = "d7524f6f9074f6326a1c167cd3dc2ed4e6916648a1a55116d029620af9b65fb1"
dependencies = [
"const-sha1",
"windows_gen",
@@ -1463,9 +1463,9 @@ dependencies = [
[[package]]
name = "windows_gen"
version = "0.19.0"
version = "0.20.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ac7bb21b8ff5e801232b72a6ff554b4cc0cef9ed9238188c3ca78fe3968a7e5d"
checksum = "4be44a189bde96fc0e0cdd5b152b2d21c635c0c94c7d256aab4425477b2a2f37"
dependencies = [
"windows_quote",
"windows_reader",
@@ -1473,9 +1473,9 @@ dependencies = [
[[package]]
name = "windows_macros"
version = "0.19.0"
version = "0.20.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5566b8c51118769e4a9094a688bf1233a3f36aacbfc78f3b15817fe0b6e0442f"
checksum = "cc1d78ce8a43d45b8da282383a2cb2ffcd5587cc3a9c341125d3181d2b701ede"
dependencies = [
"syn",
"windows_gen",
@@ -1485,15 +1485,15 @@ dependencies = [
[[package]]
name = "windows_quote"
version = "0.19.0"
version = "0.20.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "4af8236a9493c38855f95cdd11b38b342512a5df4ee7473cffa828b5ebb0e39c"
checksum = "51fa2185b18a6164a3fa3ea2b6c92ebc1b60f532ae5a85c57408ba6a5a064913"
[[package]]
name = "windows_reader"
version = "0.19.0"
version = "0.20.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2c8d5cf83fb08083438c5c46723e6206b2970da57ce314f80b57724439aaacab"
checksum = "3daa5bd758f2f8f20cd93a79aedca20759779f43785fc77b08a4e8e1e5876bbb"
[[package]]
name = "winput"

View File

@@ -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
@@ -200,6 +220,10 @@ stop Stop the komorebi.exe process and restore all hidd
state Show a JSON representation of the current window manager state
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
@@ -272,6 +296,8 @@ used [is available here](komorebi.sample.with.lib.ahk).
- [x] Mouse follows focused container
- [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

View File

@@ -7,7 +7,7 @@ edition = "2018"
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
[dependencies]
windows = "0.19"
windows = "0.20"
[build-dependencies]
windows = "0.19"
windows = "0.20"

View File

@@ -1 +1,4 @@
pub use windows::Handle;
pub use windows::Result;
::windows::include_bindings!();

View File

@@ -1,6 +1,6 @@
[package]
name = "komorebi-core"
version = "0.1.4"
version = "0.1.5"
edition = "2018"
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html

View File

@@ -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;
@@ -52,6 +53,10 @@ pub enum SocketMessage {
Stop,
TogglePause,
Retile,
QuickSave,
QuickLoad,
Save(PathBuf),
Load(PathBuf),
FocusMonitorNumber(usize),
FocusWorkspaceNumber(usize),
ContainerPadding(usize, usize, i32),

View File

@@ -40,16 +40,24 @@ FloatRule("exe", "Wally.exe")
FloatRule("exe", "wincompose.exe")
FloatRule("exe", "1Password.exe")
FloatRule("exe", "Wox.exe")
FloatRule("exe", "ddm.exe")
FloatRule("class", "Chrome_RenderWidgetHostHWND") ; GOG Electron invisible overlay
FloatRule("class", "CEFCLIENT")
; Identify Minimize-to-Tray Applications
IdentifyTrayApplication("exe", "Discord.exe")
IdentifyTrayApplication("exe", "Spotify.exe")
IdentifyTrayApplication("exe", "GalaxyClient.exe")
; Identify Electron applications with overflowing borders
IdentifyBorderOverflow("exe", "Discord.exe")
IdentifyBorderOverflow("exe", "Spotify.exe")
IdentifyBorderOverflow("exe", "GalaxyClient.exe")
IdentifyBorderOverflow("class", "ZPFTEWndClass")
; Identify applications to be forcibly managed
ManageRule("exe", "GalaxyClient.exe")
; Change the focused window, Alt + Vim direction keys
!h::
Focus("left")

View File

@@ -1,6 +1,6 @@
[package]
name = "komorebi"
version = "0.1.4"
version = "0.1.5"
authors = ["Jade Iqbal <jadeiqbal@fastmail.com>"]
description = "A tiling window manager for Windows"
categories = ["tiling-window-manager", "windows"]

View File

@@ -1,3 +1,5 @@
use std::fs::File;
use std::fs::OpenOptions;
use std::io::BufRead;
use std::io::BufReader;
use std::io::Write;
@@ -12,6 +14,7 @@ use parking_lot::Mutex;
use uds_windows::UnixStream;
use komorebi_core::FocusFollowsMouseImplementation;
use komorebi_core::Rect;
use komorebi_core::SocketMessage;
use komorebi_core::StateQuery;
@@ -326,6 +329,62 @@ impl WindowManager {
self.invisible_borders = rect;
self.retile_all()?;
}
SocketMessage::QuickSave => {
let workspace = self.focused_workspace()?;
let resize = workspace.resize_dimensions();
let mut quicksave_json = std::env::temp_dir();
quicksave_json.push("komorebi.quicksave.json");
let file = OpenOptions::new()
.write(true)
.truncate(true)
.create(true)
.open(quicksave_json)?;
serde_json::to_writer_pretty(&file, &resize)?;
}
SocketMessage::QuickLoad => {
let workspace = self.focused_workspace_mut()?;
let mut quicksave_json = std::env::temp_dir();
quicksave_json.push("komorebi.quicksave.json");
let file = File::open(&quicksave_json).map_err(|_| {
anyhow!(
"no quicksave found at {}",
quicksave_json.display().to_string()
)
})?;
let resize: Vec<Option<Rect>> = 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<Option<Rect>> = serde_json::from_reader(file)?;
workspace.set_resize_dimensions(resize);
self.update_focused_workspace(false)?;
}
};
tracing::info!("processed");

View File

@@ -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(),
&[],

View File

@@ -7,6 +7,8 @@ use color_eyre::eyre::anyhow;
use color_eyre::eyre::Error;
use color_eyre::Result;
use bindings::Handle;
use bindings::Result as WindowsCrateResult;
use bindings::Windows::Win32::Foundation::BOOL;
use bindings::Windows::Win32::Foundation::HANDLE;
use bindings::Windows::Win32::Foundation::HWND;
@@ -92,36 +94,6 @@ pub enum WindowsResult<T, E> {
Ok(T),
}
impl From<BOOL> for WindowsResult<(), Error> {
fn from(return_value: BOOL) -> Self {
if return_value.as_bool() {
Self::Ok(())
} else {
Self::Err(std::io::Error::last_os_error().into())
}
}
}
impl From<HWND> for WindowsResult<isize, Error> {
fn from(return_value: HWND) -> Self {
if return_value.is_null() {
Self::Err(std::io::Error::last_os_error().into())
} else {
Self::Ok(return_value.0)
}
}
}
impl From<HANDLE> for WindowsResult<HANDLE, Error> {
fn from(return_value: HANDLE) -> Self {
if return_value.is_null() {
Self::Err(std::io::Error::last_os_error().into())
} else {
Self::Ok(return_value)
}
}
}
macro_rules! impl_from_integer_for_windows_result {
( $( $integer_type:ty ),+ ) => {
$(
@@ -148,6 +120,40 @@ impl<T, E> From<WindowsResult<T, E>> for Result<T, E> {
}
}
pub trait ProcessWindowsCrateResult<T> {
fn process(self) -> Result<T>;
}
macro_rules! impl_process_windows_crate_result {
( $($input:ty => $deref:ty),+ $(,)? ) => (
paste::paste! {
$(
impl ProcessWindowsCrateResult<$deref> for WindowsCrateResult<$input> {
fn process(self) -> Result<$deref> {
match self {
Ok(value) => Ok(value.0),
Err(error) => Err(error.into()),
}
}
}
)+
}
);
}
impl_process_windows_crate_result!(
HWND => isize,
);
impl<T> ProcessWindowsCrateResult<T> for WindowsCrateResult<T> {
fn process(self) -> Result<T> {
match self {
Ok(value) => Ok(value),
Err(error) => Err(error.into()),
}
}
}
pub struct WindowsApi;
impl WindowsApi {
@@ -155,14 +161,16 @@ impl WindowsApi {
callback: MONITORENUMPROC,
callback_data_address: isize,
) -> Result<()> {
Result::from(WindowsResult::from(unsafe {
unsafe {
EnumDisplayMonitors(
HDC(0),
std::ptr::null_mut(),
Option::from(callback),
LPARAM(callback_data_address),
)
}))
}
.ok()
.process()
}
pub fn valid_hmonitors() -> Result<Vec<isize>> {
@@ -184,9 +192,9 @@ impl WindowsApi {
}
pub fn enum_windows(callback: WNDENUMPROC, callback_data_address: isize) -> Result<()> {
Result::from(WindowsResult::from(unsafe {
EnumWindows(Option::from(callback), LPARAM(callback_data_address))
}))
unsafe { EnumWindows(Option::from(callback), LPARAM(callback_data_address)) }
.ok()
.process()
}
pub fn load_workspace_information(monitors: &mut Ring<Monitor>) -> Result<()> {
@@ -225,9 +233,9 @@ impl WindowsApi {
}
pub fn allow_set_foreground_window(process_id: u32) -> Result<()> {
Result::from(WindowsResult::from(unsafe {
AllowSetForegroundWindow(process_id)
}))
unsafe { AllowSetForegroundWindow(process_id) }
.ok()
.process()
}
pub fn monitor_from_window(hwnd: HWND) -> isize {
@@ -250,7 +258,7 @@ impl WindowsApi {
}
pub fn set_window_pos(hwnd: HWND, layout: &Rect, position: HWND, flags: u32) -> Result<()> {
Result::from(WindowsResult::from(unsafe {
unsafe {
SetWindowPos(
hwnd,
position,
@@ -260,7 +268,9 @@ impl WindowsApi {
layout.bottom,
SET_WINDOW_POS_FLAGS(flags),
)
}))
}
.ok()
.process()
}
fn show_window(hwnd: HWND, command: SHOW_WINDOW_CMD) {
@@ -282,39 +292,25 @@ impl WindowsApi {
}
pub fn foreground_window() -> Result<isize> {
Result::from(WindowsResult::from(unsafe { GetForegroundWindow() }))
unsafe { GetForegroundWindow() }.ok().process()
}
pub fn set_foreground_window(hwnd: HWND) -> Result<()> {
match WindowsResult::from(unsafe { SetForegroundWindow(hwnd) }) {
WindowsResult::Ok(_) => Ok(()),
WindowsResult::Err(error) => {
// TODO: Figure out the odd behaviour here, docs state that a zero value means
// TODO: that the window was not brought to the foreground, but this contradicts
// TODO: the behaviour that I have observed which resulted in this check
if error.to_string() == "The operation completed successfully. (os error 0)" {
Ok(())
} else {
Err(error)
}
}
}
unsafe { SetForegroundWindow(hwnd) }.ok().process()
}
#[allow(dead_code)]
pub fn top_window() -> Result<isize> {
Result::from(WindowsResult::from(unsafe { GetTopWindow(HWND::NULL).0 }))
unsafe { GetTopWindow(HWND::default()) }.ok().process()
}
pub fn desktop_window() -> Result<isize> {
Result::from(WindowsResult::from(unsafe { GetDesktopWindow() }))
unsafe { GetDesktopWindow() }.ok().process()
}
#[allow(dead_code)]
pub fn next_window(hwnd: HWND) -> Result<isize> {
Result::from(WindowsResult::from(unsafe {
GetWindow(hwnd, GW_HWNDNEXT).0
}))
unsafe { GetWindow(hwnd, GW_HWNDNEXT) }.ok().process()
}
#[allow(dead_code)]
@@ -335,30 +331,24 @@ impl WindowsApi {
pub fn window_rect(hwnd: HWND) -> Result<Rect> {
let mut rect = unsafe { std::mem::zeroed() };
Result::from(WindowsResult::from(unsafe {
GetWindowRect(hwnd, &mut rect)
}))?;
unsafe { GetWindowRect(hwnd, &mut rect) }.ok().process()?;
Ok(Rect::from(rect))
}
fn set_cursor_pos(x: i32, y: i32) -> Result<()> {
Result::from(WindowsResult::from(unsafe { SetCursorPos(x, y) }))
unsafe { SetCursorPos(x, y) }.ok().process()
}
pub fn cursor_pos() -> Result<POINT> {
let mut cursor_pos: POINT = unsafe { std::mem::zeroed() };
Result::from(WindowsResult::from(unsafe {
GetCursorPos(&mut cursor_pos)
}))?;
let mut cursor_pos = POINT::default();
unsafe { GetCursorPos(&mut cursor_pos) }.ok().process()?;
Ok(cursor_pos)
}
pub fn window_from_point(point: POINT) -> Result<isize> {
Result::from(WindowsResult::from(unsafe { WindowFromPoint(point) }))
unsafe { WindowFromPoint(point) }.ok().process()
}
pub fn window_at_cursor_pos() -> Result<isize> {
@@ -388,24 +378,13 @@ impl WindowsApi {
}
pub fn attach_thread_input(thread_id: u32, target_thread_id: u32, attach: bool) -> Result<()> {
Result::from(WindowsResult::from(unsafe {
AttachThreadInput(thread_id, target_thread_id, attach)
}))
unsafe { AttachThreadInput(thread_id, target_thread_id, attach) }
.ok()
.process()
}
pub fn set_focus(hwnd: HWND) -> Result<()> {
match WindowsResult::from(unsafe { SetFocus(hwnd) }) {
WindowsResult::Ok(_) => Ok(()),
WindowsResult::Err(error) => {
// If the window is not attached to the calling thread's message queue, the return value is NULL
// https://docs.microsoft.com/en-us/windows/win32/api/winuser/nf-winuser-setfocus
if error.to_string() == "The operation completed successfully. (os error 0)" {
Ok(())
} else {
Err(error)
}
}
}
unsafe { SetFocus(hwnd) }.ok().map(|_| ()).process()
}
#[allow(dead_code)]
@@ -457,9 +436,9 @@ impl WindowsApi {
inherit_handle: bool,
process_id: u32,
) -> Result<HANDLE> {
Result::from(WindowsResult::from(unsafe {
OpenProcess(access_rights, inherit_handle, process_id)
}))
unsafe { OpenProcess(access_rights, inherit_handle, process_id) }
.ok()
.process()
}
pub fn process_handle(process_id: u32) -> Result<HANDLE> {
@@ -471,14 +450,16 @@ impl WindowsApi {
let mut path: Vec<u16> = vec![0; len as usize];
let text_ptr = path.as_mut_ptr();
Result::from(WindowsResult::from(unsafe {
unsafe {
QueryFullProcessImageNameW(
handle,
PROCESS_NAME_FORMAT(0),
PWSTR(text_ptr),
&mut len as *mut u32,
)
}))?;
}
.ok()
.process()?;
Ok(String::from_utf16(&path[..len as usize])?)
}
@@ -553,9 +534,9 @@ impl WindowsApi {
let mut monitor_info: MONITORINFO = unsafe { std::mem::zeroed() };
monitor_info.cbSize = u32::try_from(std::mem::size_of::<MONITORINFO>())?;
Result::from(WindowsResult::from(unsafe {
GetMonitorInfoW(hmonitor, (&mut monitor_info as *mut MONITORINFO).cast())
}))?;
unsafe { GetMonitorInfoW(hmonitor, (&mut monitor_info as *mut MONITORINFO).cast()) }
.ok()
.process()?;
Ok(monitor_info)
}
@@ -577,9 +558,9 @@ impl WindowsApi {
pv_param: *mut c_void,
update_flags: SYSTEM_PARAMETERS_INFO_UPDATE_FLAGS,
) -> Result<()> {
Result::from(WindowsResult::from(unsafe {
SystemParametersInfoW(action, ui_param, pv_param, update_flags)
}))
unsafe { SystemParametersInfoW(action, ui_param, pv_param, update_flags) }
.ok()
.process()
}
#[allow(dead_code)]

View File

@@ -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;
@@ -48,8 +47,7 @@ pub struct Workspace {
#[serde(skip_serializing)]
#[getset(get = "pub", set = "pub")]
latest_layout: Vec<Rect>,
#[serde(skip_serializing)]
#[getset(get = "pub", get_mut = "pub")]
#[getset(get = "pub", get_mut = "pub", set = "pub")]
resize_dimensions: Vec<Option<Rect>>,
#[getset(get = "pub", set = "pub")]
tile: bool,
@@ -155,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(),
@@ -324,11 +324,14 @@ impl Workspace {
}
pub fn promote_container(&mut self) -> Result<()> {
let resize = self.resize_dimensions_mut().remove(0);
let container = self
.remove_focused_container()
.ok_or_else(|| anyhow!("there is no container"))?;
self.containers_mut().push_front(container);
self.resize_dimensions_mut().insert(0, None);
self.resize_dimensions_mut().insert(0, resize);
self.focus_container(0);
Ok(())
@@ -340,8 +343,15 @@ impl Workspace {
}
fn remove_container_by_idx(&mut self, idx: usize) -> Option<Container> {
self.resize_dimensions_mut().remove(idx);
self.containers_mut().remove(idx)
if idx < self.resize_dimensions().len() {
self.resize_dimensions_mut().remove(idx);
}
if idx < self.containers().len() {
return self.containers_mut().remove(idx);
}
None
}
fn container_idx_for_window(&self, hwnd: isize) -> Option<usize> {

View File

@@ -20,6 +20,22 @@ Log() {
Run, komorebic.exe log, , Hide
}
QuickSave() {
Run, komorebic.exe quick-save, , Hide
}
QuickLoad() {
Run, komorebic.exe quick-load, , Hide
}
Save(path) {
Run, komorebic.exe save %path%, , Hide
}
Load(path) {
Run, komorebic.exe load %path%, , Hide
}
Focus(operation_direction) {
Run, komorebic.exe focus %operation_direction%, , Hide
}

View File

@@ -1,6 +1,6 @@
[package]
name = "komorebic"
version = "0.1.4"
version = "0.1.5"
authors = ["Jade Iqbal <jadeiqbal@fastmail.com>"]
description = "The command-line interface for Komorebi, a tiling window manager for Windows"
categories = ["cli", "tiling-window-manager", "windows"]

View File

@@ -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 {
@@ -288,6 +300,16 @@ enum SubCommand {
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),
@@ -409,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();
@@ -423,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)
@@ -435,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!(
@@ -555,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 {
@@ -648,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();
@@ -682,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();
@@ -716,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)?;
@@ -767,11 +790,54 @@ fn main() -> Result<()> {
SubCommand::Unmanage => {
send_message(&*SocketMessage::UnmanageFocusedWindow.as_bytes()?)?;
}
SubCommand::QuickSave => {
send_message(&*SocketMessage::QuickSave.as_bytes()?)?;
}
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<PathBuf> {
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