Files
komorebi/komorebi/src/window_manager.rs
T
LGUG2Z 57825db886 wip
2024-05-19 11:51:10 -07:00

2357 lines
84 KiB
Rust

use std::collections::HashMap;
use std::collections::HashSet;
use std::collections::VecDeque;
use std::io::ErrorKind;
use std::num::NonZeroUsize;
use std::path::Path;
use std::path::PathBuf;
use std::sync::atomic::Ordering;
use std::sync::Arc;
use color_eyre::eyre::anyhow;
use color_eyre::eyre::bail;
use color_eyre::Result;
use crossbeam_channel::Receiver;
use hotwatch::notify::ErrorKind as NotifyErrorKind;
use hotwatch::EventKind;
use hotwatch::Hotwatch;
use parking_lot::Mutex;
use regex::Regex;
use schemars::JsonSchema;
use serde::Deserialize;
use serde::Serialize;
use uds_windows::UnixListener;
use komorebi_core::config_generation::MatchingRule;
use komorebi_core::custom_layout::CustomLayout;
use komorebi_core::Arrangement;
use komorebi_core::Axis;
use komorebi_core::BorderStyle;
use komorebi_core::CycleDirection;
use komorebi_core::DefaultLayout;
use komorebi_core::FocusFollowsMouseImplementation;
use komorebi_core::HidingBehaviour;
use komorebi_core::Layout;
use komorebi_core::MoveBehaviour;
use komorebi_core::OperationBehaviour;
use komorebi_core::OperationDirection;
use komorebi_core::Rect;
use komorebi_core::Sizing;
use komorebi_core::WindowContainerBehaviour;
use crate::border_manager;
use crate::border_manager::STYLE;
use crate::container::Container;
use crate::current_virtual_desktop;
use crate::load_configuration;
use crate::monitor::Monitor;
use crate::ring::Ring;
use crate::stackbar_manager::STACKBAR_FOCUSED_TEXT_COLOUR;
use crate::stackbar_manager::STACKBAR_MODE;
use crate::stackbar_manager::STACKBAR_TAB_BACKGROUND_COLOUR;
use crate::stackbar_manager::STACKBAR_TAB_HEIGHT;
use crate::stackbar_manager::STACKBAR_TAB_WIDTH;
use crate::stackbar_manager::STACKBAR_UNFOCUSED_TEXT_COLOUR;
use crate::static_config::StaticConfig;
use crate::window::Window;
use crate::window_manager_event::WindowManagerEvent;
use crate::windows_api::WindowsApi;
use crate::winevent_listener;
use crate::workspace::Workspace;
use crate::BorderColours;
use crate::Colour;
use crate::Rgb;
use crate::WorkspaceRule;
use crate::CUSTOM_FFM;
use crate::DATA_DIR;
use crate::DISPLAY_INDEX_PREFERENCES;
use crate::FLOAT_IDENTIFIERS;
use crate::HIDING_BEHAVIOUR;
use crate::HOME_DIR;
use crate::LAYERED_WHITELIST;
use crate::MANAGE_IDENTIFIERS;
use crate::MONITOR_INDEX_PREFERENCES;
use crate::NO_TITLEBAR;
use crate::OBJECT_NAME_CHANGE_ON_LAUNCH;
use crate::REMOVE_TITLEBARS;
use crate::TRAY_AND_MULTI_WINDOW_IDENTIFIERS;
use crate::WORKSPACE_RULES;
use komorebi_core::StackbarMode;
#[derive(Debug)]
pub struct WindowManager {
pub monitors: Ring<Monitor>,
pub incoming_events: Receiver<WindowManagerEvent>,
pub command_listener: UnixListener,
pub is_paused: bool,
pub work_area_offset: Option<Rect>,
pub resize_delta: i32,
pub window_container_behaviour: WindowContainerBehaviour,
pub cross_monitor_move_behaviour: MoveBehaviour,
pub unmanaged_window_operation_behaviour: OperationBehaviour,
pub focus_follows_mouse: Option<FocusFollowsMouseImplementation>,
pub mouse_follows_focus: bool,
pub hotwatch: Hotwatch,
pub virtual_desktop_id: Option<Vec<u8>>,
pub has_pending_raise_op: bool,
pub pending_move_op: Option<(usize, usize, usize)>,
pub already_moved_window_handles: Arc<Mutex<HashSet<isize>>>,
}
#[allow(clippy::struct_excessive_bools)]
#[derive(Debug, Serialize, Deserialize, JsonSchema)]
pub struct State {
pub monitors: Ring<Monitor>,
pub is_paused: bool,
pub resize_delta: i32,
pub new_window_behaviour: WindowContainerBehaviour,
pub cross_monitor_move_behaviour: MoveBehaviour,
pub unmanaged_window_operation_behaviour: OperationBehaviour,
pub work_area_offset: Option<Rect>,
pub focus_follows_mouse: Option<FocusFollowsMouseImplementation>,
pub mouse_follows_focus: bool,
pub has_pending_raise_op: bool,
}
#[allow(clippy::struct_excessive_bools)]
#[derive(Debug, Serialize, Deserialize, JsonSchema)]
pub struct GlobalState {
pub border_enabled: bool,
pub border_colours: BorderColours,
pub border_style: BorderStyle,
pub border_offset: i32,
pub border_width: i32,
pub stackbar_mode: StackbarMode,
pub stackbar_focused_text_colour: Colour,
pub stackbar_unfocused_text_colour: Colour,
pub stackbar_tab_background_colour: Colour,
pub stackbar_tab_width: i32,
pub stackbar_height: i32,
pub remove_titlebars: bool,
pub float_identifiers: Vec<MatchingRule>,
pub manage_identifiers: Vec<MatchingRule>,
pub layered_whitelist: Vec<MatchingRule>,
pub tray_and_multi_window_identifiers: Vec<MatchingRule>,
pub name_change_on_launch_identifiers: Vec<MatchingRule>,
pub monitor_index_preferences: HashMap<usize, Rect>,
pub display_index_preferences: HashMap<usize, String>,
pub workspace_rules: HashMap<String, WorkspaceRule>,
pub window_hiding_behaviour: HidingBehaviour,
pub configuration_dir: PathBuf,
pub data_dir: PathBuf,
pub custom_ffm: bool,
}
impl Default for GlobalState {
fn default() -> Self {
Self {
border_enabled: border_manager::BORDER_ENABLED.load(Ordering::SeqCst),
border_colours: BorderColours {
single: Option::from(Colour::Rgb(Rgb::from(
border_manager::FOCUSED.load(Ordering::SeqCst),
))),
stack: Option::from(Colour::Rgb(Rgb::from(
border_manager::STACK.load(Ordering::SeqCst),
))),
monocle: Option::from(Colour::Rgb(Rgb::from(
border_manager::MONOCLE.load(Ordering::SeqCst),
))),
unfocused: Option::from(Colour::Rgb(Rgb::from(
border_manager::UNFOCUSED.load(Ordering::SeqCst),
))),
},
border_style: *STYLE.lock(),
border_offset: border_manager::BORDER_OFFSET.load(Ordering::SeqCst),
border_width: border_manager::BORDER_WIDTH.load(Ordering::SeqCst),
stackbar_mode: STACKBAR_MODE.load(),
stackbar_focused_text_colour: Colour::Rgb(Rgb::from(
STACKBAR_FOCUSED_TEXT_COLOUR.load(Ordering::SeqCst),
)),
stackbar_unfocused_text_colour: Colour::Rgb(Rgb::from(
STACKBAR_UNFOCUSED_TEXT_COLOUR.load(Ordering::SeqCst),
)),
stackbar_tab_background_colour: Colour::Rgb(Rgb::from(
STACKBAR_TAB_BACKGROUND_COLOUR.load(Ordering::SeqCst),
)),
stackbar_tab_width: STACKBAR_TAB_WIDTH.load(Ordering::SeqCst),
stackbar_height: STACKBAR_TAB_HEIGHT.load(Ordering::SeqCst),
remove_titlebars: REMOVE_TITLEBARS.load(Ordering::SeqCst),
float_identifiers: FLOAT_IDENTIFIERS.lock().clone(),
manage_identifiers: MANAGE_IDENTIFIERS.lock().clone(),
layered_whitelist: LAYERED_WHITELIST.lock().clone(),
tray_and_multi_window_identifiers: TRAY_AND_MULTI_WINDOW_IDENTIFIERS.lock().clone(),
name_change_on_launch_identifiers: OBJECT_NAME_CHANGE_ON_LAUNCH.lock().clone(),
monitor_index_preferences: MONITOR_INDEX_PREFERENCES.lock().clone(),
display_index_preferences: DISPLAY_INDEX_PREFERENCES.lock().clone(),
workspace_rules: WORKSPACE_RULES.lock().clone(),
window_hiding_behaviour: *HIDING_BEHAVIOUR.lock(),
configuration_dir: HOME_DIR.clone(),
data_dir: DATA_DIR.clone(),
custom_ffm: CUSTOM_FFM.load(Ordering::SeqCst),
}
}
}
impl AsRef<Self> for WindowManager {
fn as_ref(&self) -> &Self {
self
}
}
impl From<&WindowManager> for State {
fn from(wm: &WindowManager) -> Self {
Self {
monitors: wm.monitors.clone(),
is_paused: wm.is_paused,
work_area_offset: wm.work_area_offset,
resize_delta: wm.resize_delta,
new_window_behaviour: wm.window_container_behaviour,
cross_monitor_move_behaviour: wm.cross_monitor_move_behaviour,
focus_follows_mouse: wm.focus_follows_mouse,
mouse_follows_focus: wm.mouse_follows_focus,
has_pending_raise_op: wm.has_pending_raise_op,
unmanaged_window_operation_behaviour: wm.unmanaged_window_operation_behaviour,
}
}
}
impl_ring_elements!(WindowManager, Monitor);
#[derive(Debug, Clone, Copy)]
struct EnforceWorkspaceRuleOp {
hwnd: isize,
origin_monitor_idx: usize,
origin_workspace_idx: usize,
target_monitor_idx: usize,
target_workspace_idx: usize,
}
impl EnforceWorkspaceRuleOp {
const fn is_origin(&self, monitor_idx: usize, workspace_idx: usize) -> bool {
self.origin_monitor_idx == monitor_idx && self.origin_workspace_idx == workspace_idx
}
const fn is_target(&self, monitor_idx: usize, workspace_idx: usize) -> bool {
self.target_monitor_idx == monitor_idx && self.target_workspace_idx == workspace_idx
}
const fn is_enforced(&self) -> bool {
(self.origin_monitor_idx == self.target_monitor_idx)
&& (self.origin_workspace_idx == self.target_workspace_idx)
}
}
impl WindowManager {
#[tracing::instrument]
pub fn new(incoming: Receiver<WindowManagerEvent>) -> Result<Self> {
let socket = DATA_DIR.join("komorebi.sock");
match std::fs::remove_file(&socket) {
Ok(()) => {}
Err(error) => match error.kind() {
// Doing this because ::exists() doesn't work reliably on Windows via IntelliJ
ErrorKind::NotFound => {}
_ => {
return Err(error.into());
}
},
};
let listener = UnixListener::bind(&socket)?;
Ok(Self {
monitors: Ring::default(),
incoming_events: incoming,
command_listener: listener,
is_paused: false,
virtual_desktop_id: current_virtual_desktop(),
work_area_offset: None,
window_container_behaviour: WindowContainerBehaviour::Create,
cross_monitor_move_behaviour: MoveBehaviour::Swap,
unmanaged_window_operation_behaviour: OperationBehaviour::Op,
resize_delta: 50,
focus_follows_mouse: None,
mouse_follows_focus: true,
hotwatch: Hotwatch::new()?,
has_pending_raise_op: false,
pending_move_op: None,
already_moved_window_handles: Arc::new(Mutex::new(HashSet::new())),
})
}
#[tracing::instrument(skip(self))]
pub fn init(&mut self) -> Result<()> {
tracing::info!("initialising");
WindowsApi::load_monitor_information(&mut self.monitors)?;
WindowsApi::load_workspace_information(&mut self.monitors)
}
#[tracing::instrument]
pub fn reload_configuration() {
tracing::info!("reloading configuration");
std::thread::spawn(|| load_configuration().expect("could not load configuration"));
}
#[tracing::instrument(skip(self))]
pub fn reload_static_configuration(&mut self, pathbuf: &PathBuf) -> Result<()> {
tracing::info!("reloading static configuration");
StaticConfig::reload(pathbuf, self)
}
#[tracing::instrument(skip(self))]
pub fn watch_configuration(&mut self, enable: bool) -> Result<()> {
let config_pwsh = HOME_DIR.join("komorebi.ps1");
let config_ahk = HOME_DIR.join("komorebi.ahk");
if config_pwsh.exists() {
self.configure_watcher(enable, config_pwsh)?;
} else if config_ahk.exists() {
self.configure_watcher(enable, config_ahk)?;
}
Ok(())
}
fn configure_watcher(&mut self, enable: bool, config: PathBuf) -> Result<()> {
if enable {
tracing::info!("watching configuration for changes: {}", config.display());
// Always make absolutely sure that there isn't an already existing watch, because
// hotwatch allows multiple watches to be registered for the same path
match self.hotwatch.unwatch(&config) {
Ok(()) => {}
Err(error) => match error {
hotwatch::Error::Notify(ref notify_error) => match notify_error.kind {
NotifyErrorKind::WatchNotFound => {}
_ => return Err(error.into()),
},
error @ hotwatch::Error::Io(_) => return Err(error.into()),
},
}
self.hotwatch.watch(config, |event| match event.kind {
// Editing in Notepad sends a NoticeWrite while editing in (Neo)Vim sends
// a NoticeRemove, presumably because of the use of swap files?
EventKind::Modify(_) | EventKind::Remove(_) => {
std::thread::spawn(|| {
load_configuration().expect("could not load configuration");
});
}
_ => {}
})?;
} else {
tracing::info!(
"no longer watching configuration for changes: {}",
config.display()
);
self.hotwatch.unwatch(config)?;
};
Ok(())
}
pub fn monitor_idx_in_direction(&self, direction: OperationDirection) -> Option<usize> {
let current_monitor_size = self.focused_monitor_size().ok()?;
for (idx, monitor) in self.monitors.elements().iter().enumerate() {
match direction {
OperationDirection::Left => {
if monitor.size().left + monitor.size().right == current_monitor_size.left {
return Option::from(idx);
}
}
OperationDirection::Right => {
if current_monitor_size.right + current_monitor_size.left == monitor.size().left
{
return Option::from(idx);
}
}
OperationDirection::Up => {
if monitor.size().top + monitor.size().bottom == current_monitor_size.top {
return Option::from(idx);
}
}
OperationDirection::Down => {
if current_monitor_size.top + current_monitor_size.bottom == monitor.size().top
{
return Option::from(idx);
}
}
}
}
None
}
#[allow(clippy::too_many_arguments)]
#[tracing::instrument(skip(self), level = "debug")]
fn add_window_handle_to_move_based_on_workspace_rule(
&self,
window_title: &String,
hwnd: isize,
origin_monitor_idx: usize,
origin_workspace_idx: usize,
target_monitor_idx: usize,
target_workspace_idx: usize,
to_move: &mut Vec<EnforceWorkspaceRuleOp>,
) -> () {
tracing::trace!(
"{} should be on monitor {}, workspace {}",
window_title,
target_monitor_idx,
target_workspace_idx
);
// Create an operation outline and save it for later in the fn
to_move.push(EnforceWorkspaceRuleOp {
hwnd,
origin_monitor_idx,
origin_workspace_idx,
target_monitor_idx,
target_workspace_idx,
});
}
#[tracing::instrument(skip(self), level = "debug")]
pub fn enforce_workspace_rules(&mut self) -> Result<()> {
let mut to_move = vec![];
let focused_monitor_idx = self.focused_monitor_idx();
let focused_workspace_idx = self
.monitors()
.get(focused_monitor_idx)
.ok_or_else(|| anyhow!("there is no monitor with that index"))?
.focused_workspace_idx();
let workspace_rules = WORKSPACE_RULES.lock();
// Go through all the monitors and workspaces
for (i, monitor) in self.monitors().iter().enumerate() {
for (j, workspace) in monitor.workspaces().iter().enumerate() {
// And all the visible windows (at the top of a container)
for window in workspace.visible_windows().into_iter().flatten() {
let mut already_moved_window_handles = self.already_moved_window_handles.lock();
let exe_name = window.exe()?;
let title = window.title()?;
let class = window.class()?;
let mut found_workspace_rule = workspace_rules.get(&exe_name);
if found_workspace_rule.is_none() {
found_workspace_rule = workspace_rules.get(&title);
}
if found_workspace_rule.is_none() {
found_workspace_rule = workspace_rules.get(&class);
}
if found_workspace_rule.is_none() {
for (k, v) in workspace_rules.iter() {
if let Ok(re) = Regex::new(k) {
if re.is_match(&exe_name) {
found_workspace_rule = Some(v);
}
if re.is_match(&title) {
found_workspace_rule = Some(v);
}
if re.is_match(&class) {
found_workspace_rule = Some(v);
}
}
}
}
// If the executable names or titles of any of those windows are in our rules map
if let Some((monitor_idx, workspace_idx, apply_on_first_show_only)) =
found_workspace_rule
{
if *apply_on_first_show_only {
if !already_moved_window_handles.contains(&window.hwnd) {
already_moved_window_handles.insert(window.hwnd);
self.add_window_handle_to_move_based_on_workspace_rule(
&window.title()?,
window.hwnd,
i,
j,
*monitor_idx,
*workspace_idx,
&mut to_move,
);
}
} else {
self.add_window_handle_to_move_based_on_workspace_rule(
&window.title()?,
window.hwnd,
i,
j,
*monitor_idx,
*workspace_idx,
&mut to_move,
);
}
}
}
}
}
// Only retain operations where the target is not the current workspace
to_move.retain(|op| !op.is_target(focused_monitor_idx, focused_workspace_idx));
// Only retain operations where the rule has not already been enforced
to_move.retain(|op| !op.is_enforced());
let mut should_update_focused_workspace = false;
// Parse the operation and remove any windows that are not placed according to their rules
for op in &to_move {
let origin_workspace = self
.monitors_mut()
.get_mut(op.origin_monitor_idx)
.ok_or_else(|| anyhow!("there is no monitor with that index"))?
.workspaces_mut()
.get_mut(op.origin_workspace_idx)
.ok_or_else(|| anyhow!("there is no workspace with that index"))?;
// Hide the window we are about to remove if it is on the currently focused workspace
if op.is_origin(focused_monitor_idx, focused_workspace_idx) {
Window { hwnd: op.hwnd }.hide();
should_update_focused_workspace = true;
}
origin_workspace.remove_window(op.hwnd)?;
}
// Parse the operation again and associate those removed windows with the workspace that
// their rules have defined for them
for op in &to_move {
let target_monitor = self
.monitors_mut()
.get_mut(op.target_monitor_idx)
.ok_or_else(|| anyhow!("there is no monitor with that index"))?;
// The very first time this fn is called, the workspace might not even exist yet
if target_monitor
.workspaces()
.get(op.target_workspace_idx)
.is_none()
{
// If it doesn't, let's make sure it does for the next step
target_monitor.ensure_workspace_count(op.target_workspace_idx + 1);
}
let target_workspace = target_monitor
.workspaces_mut()
.get_mut(op.target_workspace_idx)
.ok_or_else(|| anyhow!("there is no workspace with that index"))?;
target_workspace.new_container_for_window(Window { hwnd: op.hwnd });
}
// Only re-tile the focused workspace if we need to
if should_update_focused_workspace {
self.update_focused_workspace(false, false)?;
}
Ok(())
}
#[tracing::instrument(skip(self))]
pub fn retile_all(&mut self, preserve_resize_dimensions: bool) -> Result<()> {
let offset = self.work_area_offset;
for monitor in self.monitors_mut() {
let work_area = *monitor.work_area_size();
let window_based_work_area_offset = (
monitor.window_based_work_area_offset_limit(),
monitor.window_based_work_area_offset(),
);
let offset = if monitor.work_area_offset().is_some() {
monitor.work_area_offset()
} else {
offset
};
let workspace = monitor
.focused_workspace_mut()
.ok_or_else(|| anyhow!("there is no workspace"))?;
// Reset any resize adjustments if we want to force a retile
if !preserve_resize_dimensions {
for resize in workspace.resize_dimensions_mut() {
*resize = None;
}
}
workspace.update(&work_area, offset, window_based_work_area_offset)?;
}
Ok(())
}
#[tracing::instrument(skip(self))]
pub fn manage_focused_window(&mut self) -> Result<()> {
let hwnd = WindowsApi::foreground_window()?;
let event = WindowManagerEvent::Manage(Window { hwnd });
Ok(winevent_listener::event_tx().send(event)?)
}
#[tracing::instrument(skip(self))]
pub fn unmanage_focused_window(&mut self) -> Result<()> {
let hwnd = WindowsApi::foreground_window()?;
let event = WindowManagerEvent::Unmanage(Window { hwnd });
Ok(winevent_listener::event_tx().send(event)?)
}
#[tracing::instrument(skip(self))]
pub fn raise_window_at_cursor_pos(&mut self) -> Result<()> {
let mut hwnd = None;
let workspace = self.focused_workspace()?;
if let Some(container_idx) = workspace.container_idx_from_current_point() {
if let Some(container) = workspace.containers().get(container_idx) {
if let Some(window) = container.focused_window() {
hwnd = Some(window.hwnd);
}
}
}
if let Some(hwnd) = hwnd {
if self.has_pending_raise_op
|| self.focused_window()?.hwnd == hwnd
// Sometimes we need this check, because the focus may have been given by a click
// to a non-window such as the taskbar or system tray, and komorebi doesn't know that
// the focused window of the workspace is not actually focused by the OS at that point
|| WindowsApi::foreground_window()? == hwnd
{
return Ok(());
}
let event = WindowManagerEvent::Raise(Window { hwnd });
self.has_pending_raise_op = true;
winevent_listener::event_tx().send(event)?;
} else {
tracing::debug!(
"not raising unknown window: {}",
Window {
hwnd: WindowsApi::window_at_cursor_pos()?
}
);
}
Ok(())
}
#[tracing::instrument(skip(self))]
pub fn transfer_container(
&mut self,
origin: (usize, usize, usize),
target: (usize, usize, usize),
) -> Result<()> {
let (origin_monitor_idx, origin_workspace_idx, origin_container_idx) = origin;
let (target_monitor_idx, target_workspace_idx, target_container_idx) = target;
let origin_container = self
.monitors_mut()
.get_mut(origin_monitor_idx)
.ok_or_else(|| anyhow!("there is no monitor at this index"))?
.workspaces_mut()
.get_mut(origin_workspace_idx)
.ok_or_else(|| anyhow!("there is no workspace at this index"))?
.remove_container(origin_container_idx)
.ok_or_else(|| anyhow!("there is no container at this index"))?;
let target_workspace = self
.monitors_mut()
.get_mut(target_monitor_idx)
.ok_or_else(|| anyhow!("there is no monitor at this index"))?
.workspaces_mut()
.get_mut(target_workspace_idx)
.ok_or_else(|| anyhow!("there is no workspace at this index"))?;
target_workspace
.containers_mut()
.insert(target_container_idx, origin_container);
target_workspace.focus_container(target_container_idx);
Ok(())
}
#[tracing::instrument(skip(self))]
pub fn swap_containers(
&mut self,
origin: (usize, usize, usize),
target: (usize, usize, usize),
) -> Result<()> {
let (origin_monitor_idx, origin_workspace_idx, origin_container_idx) = origin;
let (target_monitor_idx, target_workspace_idx, target_container_idx) = target;
let origin_container = self
.monitors_mut()
.get_mut(origin_monitor_idx)
.ok_or_else(|| anyhow!("there is no monitor at this index"))?
.workspaces_mut()
.get_mut(origin_workspace_idx)
.ok_or_else(|| anyhow!("there is no workspace at this index"))?
.remove_container(origin_container_idx)
.ok_or_else(|| anyhow!("there is no container at this index"))?;
let target_container = self
.monitors_mut()
.get_mut(target_monitor_idx)
.ok_or_else(|| anyhow!("there is no monitor at this index"))?
.workspaces_mut()
.get_mut(target_workspace_idx)
.ok_or_else(|| anyhow!("there is no workspace at this index"))?
.remove_container(target_container_idx);
self.monitors_mut()
.get_mut(target_monitor_idx)
.ok_or_else(|| anyhow!("there is no monitor at this index"))?
.workspaces_mut()
.get_mut(target_workspace_idx)
.ok_or_else(|| anyhow!("there is no workspace at this index"))?
.containers_mut()
.insert(target_container_idx, origin_container);
if let Some(target_container) = target_container {
self.monitors_mut()
.get_mut(origin_monitor_idx)
.ok_or_else(|| anyhow!("there is no monitor at this index"))?
.workspaces_mut()
.get_mut(origin_workspace_idx)
.ok_or_else(|| anyhow!("there is no workspace at this index"))?
.containers_mut()
.insert(origin_container_idx, target_container);
}
Ok(())
}
#[tracing::instrument(skip(self))]
pub fn update_focused_workspace(
&mut self,
follow_focus: bool,
trigger_focus: bool,
) -> Result<()> {
tracing::info!("updating");
let offset = self.work_area_offset;
self.focused_monitor_mut()
.ok_or_else(|| anyhow!("there is no monitor"))?
.update_focused_workspace(offset)?;
if follow_focus {
if let Some(window) = self.focused_workspace()?.maximized_window() {
if trigger_focus {
window.focus(self.mouse_follows_focus)?;
}
} else if let Some(container) = self.focused_workspace()?.monocle_container() {
if let Some(window) = container.focused_window() {
if trigger_focus {
window.focus(self.mouse_follows_focus)?;
}
}
} else if let Ok(window) = self.focused_window_mut() {
if trigger_focus {
window.focus(self.mouse_follows_focus)?;
}
} else {
let desktop_window = Window {
hwnd: WindowsApi::desktop_window()?,
};
let rect = self.focused_monitor_size()?;
WindowsApi::center_cursor_in_rect(&rect)?;
match WindowsApi::raise_and_focus_window(desktop_window.hwnd()) {
Ok(()) => {}
Err(error) => {
tracing::warn!("{} {}:{}", error, file!(), line!());
}
}
}
}
// if we passed false for follow_focus and there is a container on the workspace
if !follow_focus && self.focused_container_mut().is_ok() {
// and we have a stack with >1 windows
if self.focused_container_mut()?.windows().len() > 1
// and we don't have a maxed window
&& self.focused_workspace()?.maximized_window().is_none()
// and we don't have a monocle container
&& self.focused_workspace()?.monocle_container().is_none()
{
if let Ok(window) = self.focused_window_mut() {
if trigger_focus {
window.focus(self.mouse_follows_focus)?;
}
}
}
}
// This is to correctly restore and focus when switching to a workspace which
// contains a managed maximized window
if !follow_focus {
if let Some(window) = self.focused_workspace()?.maximized_window() {
window.restore();
if trigger_focus {
window.focus(self.mouse_follows_focus)?;
}
}
}
Ok(())
}
#[tracing::instrument(skip(self))]
pub fn resize_window(
&mut self,
direction: OperationDirection,
sizing: Sizing,
delta: i32,
update: bool,
) -> Result<()> {
let work_area = self.focused_monitor_work_area()?;
let workspace = self.focused_workspace_mut()?;
match workspace.layout() {
Layout::Default(layout) => {
tracing::info!("resizing window");
let len = NonZeroUsize::new(workspace.containers().len())
.ok_or_else(|| anyhow!("there must be at least one container"))?;
let focused_idx = workspace.focused_container_idx();
let focused_idx_resize = workspace
.resize_dimensions()
.get(focused_idx)
.ok_or_else(|| anyhow!("there is no resize adjustment for this container"))?;
if direction
.destination(
workspace.layout().as_boxed_direction().as_ref(),
workspace.layout_flip(),
focused_idx,
len,
)
.is_some()
{
let unaltered = layout.calculate(
&work_area,
len,
workspace.container_padding(),
workspace.layout_flip(),
&[],
);
let mut direction = direction;
// We only ever want to operate on the unflipped Rect positions when resizing, then we
// can flip them however they need to be flipped once the resizing has been done
if let Some(flip) = workspace.layout_flip() {
match flip {
Axis::Horizontal => {
if matches!(direction, OperationDirection::Left)
|| matches!(direction, OperationDirection::Right)
{
direction = direction.opposite();
}
}
Axis::Vertical => {
if matches!(direction, OperationDirection::Up)
|| matches!(direction, OperationDirection::Down)
{
direction = direction.opposite();
}
}
Axis::HorizontalAndVertical => direction = direction.opposite(),
}
}
let resize = layout.resize(
unaltered
.get(focused_idx)
.ok_or_else(|| anyhow!("there is no last layout"))?,
focused_idx_resize,
direction,
sizing,
delta,
);
workspace.resize_dimensions_mut()[focused_idx] = resize;
return if update {
self.update_focused_workspace(false, false)
} else {
Ok(())
};
}
tracing::warn!("cannot resize container in this direction");
}
Layout::Custom(_) => {
tracing::warn!("containers cannot be resized when using custom layouts");
}
}
Ok(())
}
#[tracing::instrument(skip(self))]
pub fn restore_all_windows(&mut self) -> Result<()> {
tracing::info!("restoring all hidden windows");
let no_titlebar = NO_TITLEBAR.lock();
for monitor in self.monitors_mut() {
for workspace in monitor.workspaces_mut() {
for containers in workspace.containers_mut() {
for window in containers.windows_mut() {
if no_titlebar.contains(&window.exe()?) {
window.add_title_bar()?;
}
window.restore();
}
}
}
}
Ok(())
}
#[tracing::instrument(skip(self))]
fn handle_unmanaged_window_behaviour(&self) -> Result<()> {
if matches!(
self.unmanaged_window_operation_behaviour,
OperationBehaviour::NoOp
) {
let workspace = self.focused_workspace()?;
let focused_hwnd = WindowsApi::foreground_window()?;
if !workspace.contains_managed_window(focused_hwnd) {
bail!("ignoring commands while active window is not managed by komorebi");
}
}
Ok(())
}
pub fn update_focused_workspace_by_monitor_idx(&mut self, idx: usize) -> Result<()> {
let offset = self.work_area_offset;
self.monitors_mut()
.get_mut(idx)
.ok_or_else(|| anyhow!("there is no monitor"))?
.update_focused_workspace(offset)
}
#[tracing::instrument(skip(self))]
pub fn swap_monitor_workspaces(&mut self, first_idx: usize, second_idx: usize) -> Result<()> {
tracing::info!("swaping monitors");
if first_idx == second_idx {
return Ok(());
}
let mouse_follows_focus = self.mouse_follows_focus;
let first_focused_workspace = {
let first_monitor = self
.monitors()
.get(first_idx)
.ok_or_else(|| anyhow!("There is no monitor"))?;
first_monitor.focused_workspace_idx()
};
let second_focused_workspace = {
let second_monitor = self
.monitors()
.get(second_idx)
.ok_or_else(|| anyhow!("There is no monitor"))?;
second_monitor.focused_workspace_idx()
};
// Swap workspaces between the first and second monitors
let first_workspaces = self
.monitors_mut()
.get_mut(first_idx)
.ok_or_else(|| anyhow!("There is no monitor"))?
.remove_workspaces();
let second_workspaces = self
.monitors_mut()
.get_mut(second_idx)
.ok_or_else(|| anyhow!("There is no monitor"))?
.remove_workspaces();
self.monitors_mut()
.get_mut(first_idx)
.ok_or_else(|| anyhow!("There is no monitor"))?
.workspaces_mut()
.extend(second_workspaces);
self.monitors_mut()
.get_mut(second_idx)
.ok_or_else(|| anyhow!("There is no monitor"))?
.workspaces_mut()
.extend(first_workspaces);
// Set the focused workspaces for the first and second monitors
if let Some(first_monitor) = self.monitors_mut().get_mut(first_idx) {
first_monitor.focus_workspace(second_focused_workspace)?;
first_monitor.load_focused_workspace(mouse_follows_focus)?;
}
if let Some(second_monitor) = self.monitors_mut().get_mut(second_idx) {
second_monitor.focus_workspace(first_focused_workspace)?;
second_monitor.load_focused_workspace(mouse_follows_focus)?;
}
self.update_focused_workspace_by_monitor_idx(second_idx)?;
self.update_focused_workspace_by_monitor_idx(first_idx)
}
#[tracing::instrument(skip(self))]
pub fn swap_focused_monitor(&mut self, idx: usize) -> Result<()> {
tracing::info!("swapping focused monitor");
let focused_monitor_idx = self.focused_monitor_idx();
let mouse_follows_focus = self.mouse_follows_focus;
self.swap_monitor_workspaces(focused_monitor_idx, idx)?;
self.update_focused_workspace(mouse_follows_focus, true)
}
#[tracing::instrument(skip(self))]
pub fn move_container_to_monitor(
&mut self,
monitor_idx: usize,
workspace_idx: Option<usize>,
follow: bool,
) -> Result<()> {
self.handle_unmanaged_window_behaviour()?;
tracing::info!("moving container");
let offset = self.work_area_offset;
let mouse_follows_focus = self.mouse_follows_focus;
let monitor = self
.focused_monitor_mut()
.ok_or_else(|| anyhow!("there is no monitor"))?;
let workspace = monitor
.focused_workspace_mut()
.ok_or_else(|| anyhow!("there is no workspace"))?;
if workspace.maximized_window().is_some() {
bail!("cannot move native maximized window to another monitor or workspace");
}
let container = workspace
.remove_focused_container()
.ok_or_else(|| anyhow!("there is no container"))?;
monitor.update_focused_workspace(offset)?;
let target_monitor = self
.monitors_mut()
.get_mut(monitor_idx)
.ok_or_else(|| anyhow!("there is no monitor"))?;
target_monitor.add_container(container, workspace_idx)?;
if let Some(workspace_idx) = workspace_idx {
target_monitor.focus_workspace(workspace_idx)?;
}
target_monitor.load_focused_workspace(mouse_follows_focus)?;
target_monitor.update_focused_workspace(offset)?;
if follow {
self.focus_monitor(monitor_idx)?;
}
self.update_focused_workspace(self.mouse_follows_focus, true)
}
#[tracing::instrument(skip(self))]
pub fn move_container_to_workspace(&mut self, idx: usize, follow: bool) -> Result<()> {
self.handle_unmanaged_window_behaviour()?;
tracing::info!("moving container");
let mouse_follows_focus = self.mouse_follows_focus;
let monitor = self
.focused_monitor_mut()
.ok_or_else(|| anyhow!("there is no monitor"))?;
monitor.move_container_to_workspace(idx, follow)?;
monitor.load_focused_workspace(mouse_follows_focus)?;
self.update_focused_workspace(mouse_follows_focus, true)
}
pub fn remove_focused_workspace(&mut self) -> Option<Workspace> {
let focused_monitor: &mut Monitor = self.focused_monitor_mut()?;
let focused_workspace_idx = focused_monitor.focused_workspace_idx();
focused_monitor.remove_workspace_by_idx(focused_workspace_idx)
}
#[tracing::instrument(skip(self))]
pub fn move_workspace_to_monitor(&mut self, idx: usize) -> Result<()> {
tracing::info!("moving workspace");
let mouse_follows_focus = self.mouse_follows_focus;
let workspace = self
.remove_focused_workspace()
.ok_or_else(|| anyhow!("there is no workspace"))?;
{
let target_monitor: &mut Monitor = self
.monitors_mut()
.get_mut(idx)
.ok_or_else(|| anyhow!("there is no monitor"))?;
target_monitor.workspaces_mut().push_back(workspace);
target_monitor.focus_workspace(target_monitor.workspaces().len() - 1)?;
target_monitor.load_focused_workspace(mouse_follows_focus)?;
}
self.focus_monitor(idx)?;
self.update_focused_workspace(mouse_follows_focus, true)
}
#[tracing::instrument(skip(self))]
pub fn focus_container_in_direction(&mut self, direction: OperationDirection) -> Result<()> {
self.handle_unmanaged_window_behaviour()?;
let workspace = self.focused_workspace()?;
tracing::info!("focusing container");
let new_idx = if workspace.monocle_container().is_some() {
None
} else {
workspace.new_idx_for_direction(direction)
};
let mut cross_monitor_monocle = false;
// if there is no container in that direction for this workspace
match new_idx {
None => {
let monitor_idx = self
.monitor_idx_in_direction(direction)
.ok_or_else(|| anyhow!("there is no container or monitor in this direction"))?;
self.focus_monitor(monitor_idx)?;
if let Ok(focused_workspace) = self.focused_workspace() {
if let Some(monocle) = focused_workspace.monocle_container() {
if let Some(window) = monocle.focused_window() {
window.focus(self.mouse_follows_focus)?;
WindowsApi::center_cursor_in_rect(&WindowsApi::window_rect(
window.hwnd(),
)?)?;
cross_monitor_monocle = true;
}
}
}
}
Some(idx) => {
let workspace = self.focused_workspace_mut()?;
workspace.focus_container(idx);
}
}
if !cross_monitor_monocle {
if let Ok(focused_window) = self.focused_window_mut() {
focused_window.focus(self.mouse_follows_focus)?;
}
}
Ok(())
}
#[tracing::instrument(skip(self))]
pub fn move_container_in_direction(&mut self, direction: OperationDirection) -> Result<()> {
self.handle_unmanaged_window_behaviour()?;
let workspace = self.focused_workspace()?;
// removing this messes up the monitor / container / window index somewhere
// and results in the wrong window getting moved across the monitor boundary
if workspace.is_focused_window_monocle_or_maximized()? {
bail!("ignoring command while active window is in monocle mode or maximized");
}
tracing::info!("moving container");
let origin_container_idx = workspace.focused_container_idx();
let origin_monitor_idx = self.focused_monitor_idx();
let target_container_idx = workspace.new_idx_for_direction(direction);
match target_container_idx {
// If there is nowhere to move on the current workspace, try to move it onto the monitor
// in that direction if there is one
None => {
// Don't do anything if the user has set the MoveBehaviour to NoOp
if matches!(self.cross_monitor_move_behaviour, MoveBehaviour::NoOp) {
return Ok(());
}
let target_monitor_idx = self
.monitor_idx_in_direction(direction)
.ok_or_else(|| anyhow!("there is no container or monitor in this direction"))?;
{
// remove the container from the origin monitor workspace
let origin_container = self
.focused_workspace_mut()?
.remove_container_by_idx(origin_container_idx)
.ok_or_else(|| {
anyhow!("could not remove container at given origin index")
})?;
self.focused_workspace_mut()?.focus_previous_container();
// focus the target monitor
self.focus_monitor(target_monitor_idx)?;
// unset monocle container on target workspace if there is one
let mut target_workspace_has_monocle = false;
if let Ok(target_workspace) = self.focused_workspace() {
if target_workspace.monocle_container().is_some() {
target_workspace_has_monocle = true;
}
}
if target_workspace_has_monocle {
self.toggle_monocle()?;
}
// get a mutable ref to the focused workspace on the target monitor
let target_workspace = self.focused_workspace_mut()?;
// insert the origin container into the focused workspace on the target monitor
// at the position where the currently focused container on that workspace is
target_workspace.insert_container_at_idx(
target_workspace.focused_container_idx(),
origin_container,
);
// if there is only one container on the target workspace after the insertion
// it means that there won't be one swapped back, so we have to decrement the
// focused position
if target_workspace.containers().len() == 1 {
let origin_workspace =
self.focused_workspace_for_monitor_idx_mut(origin_monitor_idx)?;
if origin_workspace.focused_container_idx() != 0 {
origin_workspace
.focus_container(origin_workspace.focused_container_idx() - 1);
}
}
}
// if our MoveBehaviour is Swap, let's try to send back the window container
// whose position which just took over
if matches!(self.cross_monitor_move_behaviour, MoveBehaviour::Swap) {
{
let target_workspace = self.focused_workspace_mut()?;
// if the target workspace doesn't have more than one container, this means it
// was previously empty, by only doing the second part of the swap when there is
// more than one container, we can fall back to a "move" if there is nothing to
// swap with on the target monitor
if target_workspace.containers().len() > 1 {
// remove the container from the target monitor workspace
let target_container = target_workspace
// this is now focused_container_idx + 1 because we have inserted our origin container
.remove_container_by_idx(
target_workspace.focused_container_idx() + 1,
)
.ok_or_else(|| {
anyhow!("could not remove container at given target index")
})?;
let origin_workspace =
self.focused_workspace_for_monitor_idx_mut(origin_monitor_idx)?;
// insert the container from the target monitor workspace into the origin monitor workspace
// at the same position from which our origin container was removed
origin_workspace
.insert_container_at_idx(origin_container_idx, target_container);
}
}
}
// make sure to update the origin monitor workspace layout because it is no
// longer focused so it won't get updated at the end of this fn
let offset = self.work_area_offset;
self.monitors_mut()
.get_mut(origin_monitor_idx)
.ok_or_else(|| anyhow!("there is no monitor at this index"))?
.update_focused_workspace(offset)?;
let a = self
.focused_monitor()
.ok_or_else(|| anyhow!("there is no monitor focused monitor"))?
.id();
let b = self
.monitors_mut()
.get_mut(origin_monitor_idx)
.ok_or_else(|| anyhow!("there is no monitor at this index"))?
.id();
if !WindowsApi::monitors_have_same_dpi(a, b)? {
self.update_focused_workspace(self.mouse_follows_focus, true)?;
}
}
Some(new_idx) => {
let workspace = self.focused_workspace_mut()?;
workspace.swap_containers(origin_container_idx, new_idx);
workspace.focus_container(new_idx);
}
}
self.update_focused_workspace(self.mouse_follows_focus, true)
}
#[tracing::instrument(skip(self))]
pub fn focus_container_in_cycle_direction(&mut self, direction: CycleDirection) -> Result<()> {
self.handle_unmanaged_window_behaviour()?;
tracing::info!("focusing container");
let mut maximize_next = false;
let mut monocle_next = false;
if self.focused_workspace_mut()?.maximized_window().is_some() {
maximize_next = true;
self.unmaximize_window()?;
}
if self.focused_workspace_mut()?.monocle_container().is_some() {
monocle_next = true;
self.monocle_off()?;
}
let workspace = self.focused_workspace_mut()?;
let new_idx = workspace
.new_idx_for_cycle_direction(direction)
.ok_or_else(|| anyhow!("this is not a valid direction from the current position"))?;
workspace.focus_container(new_idx);
if maximize_next {
self.toggle_maximize()?;
} else if monocle_next {
self.toggle_monocle()?;
} else {
self.focused_window_mut()?.focus(self.mouse_follows_focus)?;
}
Ok(())
}
#[tracing::instrument(skip(self))]
pub fn move_container_in_cycle_direction(&mut self, direction: CycleDirection) -> Result<()> {
self.handle_unmanaged_window_behaviour()?;
let workspace = self.focused_workspace_mut()?;
if workspace.is_focused_window_monocle_or_maximized()? {
bail!("ignoring command while active window is in monocle mode or maximized");
}
tracing::info!("moving container");
let current_idx = workspace.focused_container_idx();
let new_idx = workspace
.new_idx_for_cycle_direction(direction)
.ok_or_else(|| anyhow!("this is not a valid direction from the current position"))?;
workspace.swap_containers(current_idx, new_idx);
workspace.focus_container(new_idx);
self.update_focused_workspace(self.mouse_follows_focus, true)
}
#[tracing::instrument(skip(self))]
pub fn cycle_container_window_in_direction(&mut self, direction: CycleDirection) -> Result<()> {
self.handle_unmanaged_window_behaviour()?;
tracing::info!("cycling container windows");
let container = self.focused_container_mut()?;
let len = NonZeroUsize::new(container.windows().len())
.ok_or_else(|| anyhow!("there must be at least one window in a container"))?;
if len.get() == 1 {
bail!("there is only one window in this container");
}
let current_idx = container.focused_window_idx();
let next_idx = direction.next_idx(current_idx, len);
container.focus_window(next_idx);
container.load_focused_window();
self.update_focused_workspace(self.mouse_follows_focus, true)
}
#[tracing::instrument(skip(self))]
pub fn add_window_to_container(&mut self, direction: OperationDirection) -> Result<()> {
self.handle_unmanaged_window_behaviour()?;
tracing::info!("adding window to container");
let workspace = self.focused_workspace_mut()?;
let len = NonZeroUsize::new(workspace.containers_mut().len())
.ok_or_else(|| anyhow!("there must be at least one container"))?;
let current_container_idx = workspace.focused_container_idx();
let is_valid = direction
.destination(
workspace.layout().as_boxed_direction().as_ref(),
workspace.layout_flip(),
workspace.focused_container_idx(),
len,
)
.is_some();
if is_valid {
let new_idx = workspace.new_idx_for_direction(direction).ok_or_else(|| {
anyhow!("this is not a valid direction from the current position")
})?;
let adjusted_new_index = if new_idx > current_container_idx
&& !matches!(
workspace.layout(),
Layout::Default(DefaultLayout::Grid)
| Layout::Default(DefaultLayout::UltrawideVerticalStack)
) {
new_idx - 1
} else {
new_idx
};
workspace.move_window_to_container(adjusted_new_index)?;
self.update_focused_workspace(self.mouse_follows_focus, false)?;
}
Ok(())
}
#[tracing::instrument(skip(self))]
pub fn promote_container_to_front(&mut self) -> Result<()> {
self.handle_unmanaged_window_behaviour()?;
let workspace = self.focused_workspace_mut()?;
if matches!(workspace.layout(), Layout::Default(DefaultLayout::Grid)) {
tracing::debug!("ignoring promote command for grid layout");
return Ok(());
}
tracing::info!("promoting container");
workspace.promote_container()?;
self.update_focused_workspace(self.mouse_follows_focus, true)
}
#[tracing::instrument(skip(self))]
pub fn promote_focus_to_front(&mut self) -> Result<()> {
self.handle_unmanaged_window_behaviour()?;
let workspace = self.focused_workspace_mut()?;
if matches!(workspace.layout(), Layout::Default(DefaultLayout::Grid)) {
tracing::info!("ignoring promote focus command for grid layout");
return Ok(());
}
tracing::info!("promoting focus");
let target_idx = match workspace.layout() {
Layout::Default(_) => 0,
Layout::Custom(custom) => custom
.first_container_idx(custom.primary_idx().map_or(0, |primary_idx| primary_idx)),
};
workspace.focus_container(target_idx);
self.update_focused_workspace(self.mouse_follows_focus, true)
}
#[tracing::instrument(skip(self))]
pub fn remove_window_from_container(&mut self) -> Result<()> {
self.handle_unmanaged_window_behaviour()?;
tracing::info!("removing window");
if self.focused_container()?.windows().len() == 1 {
bail!("a container must have at least one window");
}
let workspace = self.focused_workspace_mut()?;
workspace.new_container_for_focused_window()?;
self.update_focused_workspace(self.mouse_follows_focus, false)
}
#[tracing::instrument(skip(self))]
pub fn toggle_tiling(&mut self) -> Result<()> {
let workspace = self.focused_workspace_mut()?;
workspace.set_tile(!*workspace.tile());
self.update_focused_workspace(false, false)
}
#[tracing::instrument(skip(self))]
pub fn toggle_float(&mut self) -> Result<()> {
let hwnd = WindowsApi::foreground_window()?;
let workspace = self.focused_workspace_mut()?;
let mut is_floating_window = false;
for window in workspace.floating_windows() {
if window.hwnd == hwnd {
is_floating_window = true;
}
}
if is_floating_window {
self.unfloat_window()?;
} else {
self.float_window()?;
}
self.update_focused_workspace(is_floating_window, true)
}
#[tracing::instrument(skip(self))]
pub fn float_window(&mut self) -> Result<()> {
tracing::info!("floating window");
let work_area = self.focused_monitor_work_area()?;
let workspace = self.focused_workspace_mut()?;
workspace.new_floating_window()?;
let window = workspace
.floating_windows_mut()
.last_mut()
.ok_or_else(|| anyhow!("there is no floating window"))?;
window.center(&work_area)?;
window.focus(self.mouse_follows_focus)?;
Ok(())
}
#[tracing::instrument(skip(self))]
pub fn unfloat_window(&mut self) -> Result<()> {
tracing::info!("unfloating window");
let workspace = self.focused_workspace_mut()?;
workspace.new_container_for_floating_window()
}
#[tracing::instrument(skip(self))]
pub fn toggle_monocle(&mut self) -> Result<()> {
self.handle_unmanaged_window_behaviour()?;
let workspace = self.focused_workspace()?;
match workspace.monocle_container() {
None => self.monocle_on()?,
Some(_) => self.monocle_off()?,
}
self.update_focused_workspace(true, true)?;
Ok(())
}
#[tracing::instrument(skip(self))]
pub fn monocle_on(&mut self) -> Result<()> {
tracing::info!("enabling monocle");
let workspace = self.focused_workspace_mut()?;
workspace.new_monocle_container()?;
for container in workspace.containers_mut() {
container.hide(None);
}
Ok(())
}
#[tracing::instrument(skip(self))]
pub fn monocle_off(&mut self) -> Result<()> {
tracing::info!("disabling monocle");
let workspace = self.focused_workspace_mut()?;
for container in workspace.containers_mut() {
container.restore();
}
workspace.reintegrate_monocle_container()
}
#[tracing::instrument(skip(self))]
pub fn toggle_maximize(&mut self) -> Result<()> {
self.handle_unmanaged_window_behaviour()?;
let workspace = self.focused_workspace_mut()?;
match workspace.maximized_window() {
None => self.maximize_window()?,
Some(_) => self.unmaximize_window()?,
}
self.update_focused_workspace(true, false)
}
#[tracing::instrument(skip(self))]
pub fn maximize_window(&mut self) -> Result<()> {
tracing::info!("maximizing windowj");
let workspace = self.focused_workspace_mut()?;
workspace.new_maximized_window()
}
#[tracing::instrument(skip(self))]
pub fn unmaximize_window(&mut self) -> Result<()> {
tracing::info!("unmaximizing window");
let workspace = self.focused_workspace_mut()?;
workspace.reintegrate_maximized_window()
}
#[tracing::instrument(skip(self))]
pub fn flip_layout(&mut self, layout_flip: Axis) -> Result<()> {
let workspace = self.focused_workspace_mut()?;
tracing::info!("flipping layout");
#[allow(clippy::match_same_arms)]
match workspace.layout_flip() {
None => {
workspace.set_layout_flip(Option::from(layout_flip));
}
Some(current_layout_flip) => {
match current_layout_flip {
Axis::Horizontal => match layout_flip {
Axis::Horizontal => workspace.set_layout_flip(None),
Axis::Vertical => {
workspace.set_layout_flip(Option::from(Axis::HorizontalAndVertical))
}
Axis::HorizontalAndVertical => {
workspace.set_layout_flip(Option::from(Axis::HorizontalAndVertical))
}
},
Axis::Vertical => match layout_flip {
Axis::Horizontal => {
workspace.set_layout_flip(Option::from(Axis::HorizontalAndVertical))
}
Axis::Vertical => workspace.set_layout_flip(None),
Axis::HorizontalAndVertical => {
workspace.set_layout_flip(Option::from(Axis::HorizontalAndVertical))
}
},
Axis::HorizontalAndVertical => match layout_flip {
Axis::Horizontal => workspace.set_layout_flip(Option::from(Axis::Vertical)),
Axis::Vertical => workspace.set_layout_flip(Option::from(Axis::Horizontal)),
Axis::HorizontalAndVertical => workspace.set_layout_flip(None),
},
};
}
}
self.update_focused_workspace(false, false)
}
#[tracing::instrument(skip(self))]
pub fn change_workspace_layout_default(&mut self, layout: DefaultLayout) -> Result<()> {
tracing::info!("changing layout");
let workspace = self.focused_workspace_mut()?;
match workspace.layout() {
Layout::Default(_) => {}
Layout::Custom(layout) => {
let primary_idx =
layout.first_container_idx(layout.primary_idx().ok_or_else(|| {
anyhow!("this custom layout does not have a primary column")
})?);
if !workspace.containers().is_empty() && primary_idx < workspace.containers().len()
{
workspace.swap_containers(0, primary_idx);
}
}
}
workspace.set_layout(Layout::Default(layout));
self.update_focused_workspace(self.mouse_follows_focus, false)
}
#[tracing::instrument(skip(self))]
pub fn cycle_layout(&mut self, direction: CycleDirection) -> Result<()> {
tracing::info!("cycling layout");
let workspace = self.focused_workspace_mut()?;
let current_layout = workspace.layout();
match current_layout {
Layout::Default(current) => {
let new_layout = match direction {
CycleDirection::Previous => current.cycle_previous(),
CycleDirection::Next => current.cycle_next(),
};
tracing::info!("next layout: {new_layout}");
workspace.set_layout(Layout::Default(new_layout));
}
Layout::Custom(_) => {}
}
self.update_focused_workspace(self.mouse_follows_focus, false)
}
#[tracing::instrument(skip(self))]
pub fn change_workspace_custom_layout<P>(&mut self, path: P) -> Result<()>
where
P: AsRef<Path> + std::fmt::Debug,
{
tracing::info!("changing layout");
let layout = CustomLayout::from_path(path)?;
let workspace = self.focused_workspace_mut()?;
match workspace.layout() {
Layout::Default(_) => {
let primary_idx =
layout.first_container_idx(layout.primary_idx().ok_or_else(|| {
anyhow!("this custom layout does not have a primary column")
})?);
if !workspace.containers().is_empty() && primary_idx < workspace.containers().len()
{
workspace.swap_containers(0, primary_idx);
}
}
Layout::Custom(_) => {}
}
workspace.set_layout(Layout::Custom(layout));
workspace.set_layout_flip(None);
self.update_focused_workspace(self.mouse_follows_focus, false)
}
#[tracing::instrument(skip(self))]
pub fn adjust_workspace_padding(&mut self, sizing: Sizing, adjustment: i32) -> Result<()> {
tracing::info!("adjusting workspace padding");
let workspace = self.focused_workspace_mut()?;
let padding = workspace
.workspace_padding()
.ok_or_else(|| anyhow!("there is no workspace padding"))?;
workspace.set_workspace_padding(Option::from(sizing.adjust_by(padding, adjustment)));
self.update_focused_workspace(false, false)
}
#[tracing::instrument(skip(self))]
pub fn adjust_container_padding(&mut self, sizing: Sizing, adjustment: i32) -> Result<()> {
tracing::info!("adjusting container padding");
let workspace = self.focused_workspace_mut()?;
let padding = workspace
.container_padding()
.ok_or_else(|| anyhow!("there is no container padding"))?;
workspace.set_container_padding(Option::from(sizing.adjust_by(padding, adjustment)));
self.update_focused_workspace(false, false)
}
#[tracing::instrument(skip(self))]
pub fn set_workspace_tiling(
&mut self,
monitor_idx: usize,
workspace_idx: usize,
tile: bool,
) -> Result<()> {
let monitor = self
.monitors_mut()
.get_mut(monitor_idx)
.ok_or_else(|| anyhow!("there is no monitor"))?;
let workspace = monitor
.workspaces_mut()
.get_mut(workspace_idx)
.ok_or_else(|| anyhow!("there is no monitor"))?;
workspace.set_tile(tile);
self.update_focused_workspace(false, false)
}
#[tracing::instrument(skip(self))]
pub fn add_workspace_layout_default_rule(
&mut self,
monitor_idx: usize,
workspace_idx: usize,
at_container_count: usize,
layout: DefaultLayout,
) -> Result<()> {
tracing::info!("setting workspace layout");
let offset = self.work_area_offset;
let focused_monitor_idx = self.focused_monitor_idx();
let monitor = self
.monitors_mut()
.get_mut(monitor_idx)
.ok_or_else(|| anyhow!("there is no monitor"))?;
let work_area = *monitor.work_area_size();
let window_based_work_area_offset = (
monitor.window_based_work_area_offset_limit(),
monitor.window_based_work_area_offset(),
);
let focused_workspace_idx = monitor.focused_workspace_idx();
let offset = if monitor.work_area_offset().is_some() {
monitor.work_area_offset()
} else {
offset
};
let workspace = monitor
.workspaces_mut()
.get_mut(workspace_idx)
.ok_or_else(|| anyhow!("there is no monitor"))?;
let rules: &mut Vec<(usize, Layout)> = workspace.layout_rules_mut();
rules.retain(|pair| pair.0 != at_container_count);
rules.push((at_container_count, Layout::Default(layout)));
rules.sort_by(|a, b| a.0.cmp(&b.0));
// If this is the focused workspace on a non-focused screen, let's update it
if focused_monitor_idx != monitor_idx && focused_workspace_idx == workspace_idx {
workspace.update(&work_area, offset, window_based_work_area_offset)?;
Ok(())
} else {
Ok(self.update_focused_workspace(false, false)?)
}
}
#[tracing::instrument(skip(self))]
pub fn add_workspace_layout_custom_rule<P>(
&mut self,
monitor_idx: usize,
workspace_idx: usize,
at_container_count: usize,
path: P,
) -> Result<()>
where
P: AsRef<Path> + std::fmt::Debug,
{
tracing::info!("setting workspace layout");
let offset = self.work_area_offset;
let focused_monitor_idx = self.focused_monitor_idx();
let monitor = self
.monitors_mut()
.get_mut(monitor_idx)
.ok_or_else(|| anyhow!("there is no monitor"))?;
let work_area = *monitor.work_area_size();
let window_based_work_area_offset = (
monitor.window_based_work_area_offset_limit(),
monitor.window_based_work_area_offset(),
);
let focused_workspace_idx = monitor.focused_workspace_idx();
let offset = if monitor.work_area_offset().is_some() {
monitor.work_area_offset()
} else {
offset
};
let workspace = monitor
.workspaces_mut()
.get_mut(workspace_idx)
.ok_or_else(|| anyhow!("there is no monitor"))?;
let layout = CustomLayout::from_path(path)?;
let rules: &mut Vec<(usize, Layout)> = workspace.layout_rules_mut();
rules.retain(|pair| pair.0 != at_container_count);
rules.push((at_container_count, Layout::Custom(layout)));
rules.sort_by(|a, b| a.0.cmp(&b.0));
// If this is the focused workspace on a non-focused screen, let's update it
if focused_monitor_idx != monitor_idx && focused_workspace_idx == workspace_idx {
workspace.update(&work_area, offset, window_based_work_area_offset)?;
Ok(())
} else {
Ok(self.update_focused_workspace(false, false)?)
}
}
#[tracing::instrument(skip(self))]
pub fn clear_workspace_layout_rules(
&mut self,
monitor_idx: usize,
workspace_idx: usize,
) -> Result<()> {
tracing::info!("setting workspace layout");
let offset = self.work_area_offset;
let focused_monitor_idx = self.focused_monitor_idx();
let monitor = self
.monitors_mut()
.get_mut(monitor_idx)
.ok_or_else(|| anyhow!("there is no monitor"))?;
let work_area = *monitor.work_area_size();
let window_based_work_area_offset = (
monitor.window_based_work_area_offset_limit(),
monitor.window_based_work_area_offset(),
);
let focused_workspace_idx = monitor.focused_workspace_idx();
let offset = if monitor.work_area_offset().is_some() {
monitor.work_area_offset()
} else {
offset
};
let workspace = monitor
.workspaces_mut()
.get_mut(workspace_idx)
.ok_or_else(|| anyhow!("there is no monitor"))?;
let rules: &mut Vec<(usize, Layout)> = workspace.layout_rules_mut();
rules.clear();
// If this is the focused workspace on a non-focused screen, let's update it
if focused_monitor_idx != monitor_idx && focused_workspace_idx == workspace_idx {
workspace.update(&work_area, offset, window_based_work_area_offset)?;
Ok(())
} else {
Ok(self.update_focused_workspace(false, false)?)
}
}
#[tracing::instrument(skip(self))]
pub fn set_workspace_layout_default(
&mut self,
monitor_idx: usize,
workspace_idx: usize,
layout: DefaultLayout,
) -> Result<()> {
tracing::info!("setting workspace layout");
let offset = self.work_area_offset;
let focused_monitor_idx = self.focused_monitor_idx();
let monitor = self
.monitors_mut()
.get_mut(monitor_idx)
.ok_or_else(|| anyhow!("there is no monitor"))?;
let work_area = *monitor.work_area_size();
let window_based_work_area_offset = (
monitor.window_based_work_area_offset_limit(),
monitor.window_based_work_area_offset(),
);
let focused_workspace_idx = monitor.focused_workspace_idx();
let offset = if monitor.work_area_offset().is_some() {
monitor.work_area_offset()
} else {
offset
};
let workspace = monitor
.workspaces_mut()
.get_mut(workspace_idx)
.ok_or_else(|| anyhow!("there is no monitor"))?;
workspace.set_layout(Layout::Default(layout));
// If this is the focused workspace on a non-focused screen, let's update it
if focused_monitor_idx != monitor_idx && focused_workspace_idx == workspace_idx {
workspace.update(&work_area, offset, window_based_work_area_offset)?;
Ok(())
} else {
Ok(self.update_focused_workspace(false, false)?)
}
}
#[tracing::instrument(skip(self))]
pub fn set_workspace_layout_custom<P>(
&mut self,
monitor_idx: usize,
workspace_idx: usize,
path: P,
) -> Result<()>
where
P: AsRef<Path> + std::fmt::Debug,
{
tracing::info!("setting workspace layout");
let layout = CustomLayout::from_path(path)?;
let offset = self.work_area_offset;
let focused_monitor_idx = self.focused_monitor_idx();
let monitor = self
.monitors_mut()
.get_mut(monitor_idx)
.ok_or_else(|| anyhow!("there is no monitor"))?;
let work_area = *monitor.work_area_size();
let window_based_work_area_offset = (
monitor.window_based_work_area_offset_limit(),
monitor.window_based_work_area_offset(),
);
let focused_workspace_idx = monitor.focused_workspace_idx();
let offset = if monitor.work_area_offset().is_some() {
monitor.work_area_offset()
} else {
offset
};
let workspace = monitor
.workspaces_mut()
.get_mut(workspace_idx)
.ok_or_else(|| anyhow!("there is no monitor"))?;
workspace.set_layout(Layout::Custom(layout));
workspace.set_layout_flip(None);
// If this is the focused workspace on a non-focused screen, let's update it
if focused_monitor_idx != monitor_idx && focused_workspace_idx == workspace_idx {
workspace.update(&work_area, offset, window_based_work_area_offset)?;
Ok(())
} else {
Ok(self.update_focused_workspace(false, false)?)
}
}
#[tracing::instrument(skip(self))]
pub fn ensure_workspaces_for_monitor(
&mut self,
monitor_idx: usize,
workspace_count: usize,
) -> Result<()> {
tracing::info!("ensuring workspace count");
let monitor = self
.monitors_mut()
.get_mut(monitor_idx)
.ok_or_else(|| anyhow!("there is no monitor"))?;
monitor.ensure_workspace_count(workspace_count);
Ok(())
}
#[tracing::instrument(skip(self))]
pub fn ensure_named_workspaces_for_monitor(
&mut self,
monitor_idx: usize,
names: &Vec<String>,
) -> Result<()> {
tracing::info!("ensuring workspace count");
let monitor = self
.monitors_mut()
.get_mut(monitor_idx)
.ok_or_else(|| anyhow!("there is no monitor"))?;
monitor.ensure_workspace_count(names.len());
for (workspace_idx, name) in names.iter().enumerate() {
if let Some(workspace) = monitor.workspaces_mut().get_mut(workspace_idx) {
workspace.set_name(Option::from(name.clone()));
}
}
Ok(())
}
#[tracing::instrument(skip(self))]
pub fn set_workspace_padding(
&mut self,
monitor_idx: usize,
workspace_idx: usize,
size: i32,
) -> Result<()> {
tracing::info!("setting workspace padding");
let monitor = self
.monitors_mut()
.get_mut(monitor_idx)
.ok_or_else(|| anyhow!("there is no monitor"))?;
let workspace = monitor
.workspaces_mut()
.get_mut(workspace_idx)
.ok_or_else(|| anyhow!("there is no monitor"))?;
workspace.set_workspace_padding(Option::from(size));
self.update_focused_workspace(false, false)
}
#[tracing::instrument(skip(self))]
pub fn set_workspace_name(
&mut self,
monitor_idx: usize,
workspace_idx: usize,
name: String,
) -> Result<()> {
tracing::info!("setting workspace name");
let monitor = self
.monitors_mut()
.get_mut(monitor_idx)
.ok_or_else(|| anyhow!("there is no monitor"))?;
let workspace = monitor
.workspaces_mut()
.get_mut(workspace_idx)
.ok_or_else(|| anyhow!("there is no monitor"))?;
workspace.set_name(Option::from(name.clone()));
monitor.workspace_names_mut().insert(workspace_idx, name);
Ok(())
}
#[tracing::instrument(skip(self))]
pub fn set_container_padding(
&mut self,
monitor_idx: usize,
workspace_idx: usize,
size: i32,
) -> Result<()> {
tracing::info!("setting container padding");
let monitor = self
.monitors_mut()
.get_mut(monitor_idx)
.ok_or_else(|| anyhow!("there is no monitor"))?;
let workspace = monitor
.workspaces_mut()
.get_mut(workspace_idx)
.ok_or_else(|| anyhow!("there is no monitor"))?;
workspace.set_container_padding(Option::from(size));
self.update_focused_workspace(false, false)
}
pub fn focused_monitor_size(&self) -> Result<Rect> {
Ok(*self
.focused_monitor()
.ok_or_else(|| anyhow!("there is no monitor"))?
.size())
}
pub fn focused_monitor_work_area(&self) -> Result<Rect> {
Ok(*self
.focused_monitor()
.ok_or_else(|| anyhow!("there is no monitor"))?
.work_area_size())
}
#[tracing::instrument(skip(self))]
pub fn focus_monitor(&mut self, idx: usize) -> Result<()> {
tracing::info!("focusing monitor");
if self.monitors().get(idx).is_some() {
self.monitors.focus(idx);
} else {
bail!("this is not a valid monitor index");
}
Ok(())
}
pub fn monitor_idx_from_window(&mut self, window: Window) -> Option<usize> {
let hmonitor = WindowsApi::monitor_from_window(window.hwnd());
for (i, monitor) in self.monitors().iter().enumerate() {
if monitor.id() == hmonitor {
return Option::from(i);
}
}
// our hmonitor might be stale, so if we didn't return above, try querying via the latest
// info taken from win32_display_data and update our hmonitor while we're at it
if let Ok(latest) = WindowsApi::monitor(hmonitor) {
for (i, monitor) in self.monitors_mut().iter_mut().enumerate() {
if monitor.device_id() == latest.device_id() {
monitor.set_id(latest.id());
return Option::from(i);
}
}
}
None
}
pub fn monitor_idx_from_current_pos(&mut self) -> Option<usize> {
let hmonitor = WindowsApi::monitor_from_point(WindowsApi::cursor_pos().ok()?);
for (i, monitor) in self.monitors().iter().enumerate() {
if monitor.id() == hmonitor {
return Option::from(i);
}
}
// our hmonitor might be stale, so if we didn't return above, try querying via the latest
// info taken from win32_display_data and update our hmonitor while we're at it
if let Ok(latest) = WindowsApi::monitor(hmonitor) {
for (i, monitor) in self.monitors_mut().iter_mut().enumerate() {
if monitor.device_id() == latest.device_id() {
monitor.set_id(latest.id());
return Option::from(i);
}
}
}
None
}
pub fn focused_workspace_idx(&self) -> Result<usize> {
Ok(self
.focused_monitor()
.ok_or_else(|| anyhow!("there is no monitor"))?
.focused_workspace_idx())
}
pub fn focused_workspace(&self) -> Result<&Workspace> {
self.focused_monitor()
.ok_or_else(|| anyhow!("there is no monitor"))?
.focused_workspace()
.ok_or_else(|| anyhow!("there is no workspace"))
}
pub fn focused_workspace_mut(&mut self) -> Result<&mut Workspace> {
self.focused_monitor_mut()
.ok_or_else(|| anyhow!("there is no monitor"))?
.focused_workspace_mut()
.ok_or_else(|| anyhow!("there is no workspace"))
}
pub fn focused_workspace_idx_for_monitor_idx(&self, idx: usize) -> Result<usize> {
Ok(self
.monitors()
.get(idx)
.ok_or_else(|| anyhow!("there is no monitor at this index"))?
.focused_workspace_idx())
}
pub fn focused_workspace_for_monitor_idx(&self, idx: usize) -> Result<&Workspace> {
self.monitors()
.get(idx)
.ok_or_else(|| anyhow!("there is no monitor at this index"))?
.focused_workspace()
.ok_or_else(|| anyhow!("there is no workspace"))
}
pub fn focused_workspace_for_monitor_idx_mut(&mut self, idx: usize) -> Result<&mut Workspace> {
self.monitors_mut()
.get_mut(idx)
.ok_or_else(|| anyhow!("there is no monitor at this index"))?
.focused_workspace_mut()
.ok_or_else(|| anyhow!("there is no workspace"))
}
#[tracing::instrument(skip(self))]
pub fn focus_workspace(&mut self, idx: usize) -> Result<()> {
tracing::info!("focusing workspace");
let mouse_follows_focus = self.mouse_follows_focus;
let monitor = self
.focused_monitor_mut()
.ok_or_else(|| anyhow!("there is no workspace"))?;
monitor.focus_workspace(idx)?;
monitor.load_focused_workspace(mouse_follows_focus)?;
self.update_focused_workspace(false, true)
}
#[tracing::instrument(skip(self))]
pub fn monitor_workspace_index_by_name(&mut self, name: &str) -> Option<(usize, usize)> {
tracing::info!("looking up workspace by name");
for (monitor_idx, monitor) in self.monitors().iter().enumerate() {
for (workspace_idx, workspace) in monitor.workspaces().iter().enumerate() {
if let Some(workspace_name) = workspace.name() {
if workspace_name == name {
return Option::from((monitor_idx, workspace_idx));
}
}
}
}
None
}
#[tracing::instrument(skip(self))]
pub fn new_workspace(&mut self) -> Result<()> {
tracing::info!("adding new workspace");
let mouse_follows_focus = self.mouse_follows_focus;
let monitor = self
.focused_monitor_mut()
.ok_or_else(|| anyhow!("there is no workspace"))?;
monitor.focus_workspace(monitor.new_workspace_idx())?;
monitor.load_focused_workspace(mouse_follows_focus)?;
self.update_focused_workspace(self.mouse_follows_focus, false)
}
pub fn focused_container(&self) -> Result<&Container> {
self.focused_workspace()?
.focused_container()
.ok_or_else(|| anyhow!("there is no container"))
}
pub fn focused_container_mut(&mut self) -> Result<&mut Container> {
self.focused_workspace_mut()?
.focused_container_mut()
.ok_or_else(|| anyhow!("there is no container"))
}
pub fn focused_window(&self) -> Result<&Window> {
self.focused_container()?
.focused_window()
.ok_or_else(|| anyhow!("there is no window"))
}
fn focused_window_mut(&mut self) -> Result<&mut Window> {
self.focused_container_mut()?
.focused_window_mut()
.ok_or_else(|| anyhow!("there is no window"))
}
}