mirror of
https://github.com/LGUG2Z/komorebi.git
synced 2026-07-17 18:41:11 +02:00
2357 lines
84 KiB
Rust
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"))
|
|
}
|
|
}
|