mirror of
https://github.com/LGUG2Z/komorebi.git
synced 2026-11-14 04:43:42 +01:00
komorebi/ -> komorebi-windows/ komorebi-for-kde/komorebi/ -> komorebi-linux/ komorebi-for-mac/komorebi/ -> komorebi-macos/ komorebi-for-kde/komorebi-hotkeys/ -> komorebi-hotkeys/ The directories now carry the names of their packages (komorebi-windows, komorebi-linux, komorebi-macos). Renames only: every file is a 100% rename (270 files); nothing is edited, so the workspace does not build at this revision. The next revision fixes the paths.
2404 lines
108 KiB
Rust
2404 lines
108 KiB
Rust
//! How the daemon answers the messages on its socket: [`listen_for_commands`] and
|
|
//! [`WindowManager::process_command`].
|
|
//!
|
|
//! A client connects to `komorebi.sock` in [`struct@DATA_DIR`](crate::DATA_DIR) and writes one
|
|
//! [`SocketMessage`] as JSON per line. Each connection is read on a separate thread, and each
|
|
//! message is handled with the window manager locked. A message that asks a question is answered
|
|
//! on the same connection, and the answer ends when the daemon closes it:
|
|
//!
|
|
//! - **State**: the window manager's [`State`], as JSON.
|
|
//! - **MonitorInformation**: the id, the name and the bounds of each monitor, as JSON.
|
|
//! - **VisibleWindows**: what the compositor says about each window that can be seen on the
|
|
//! workspace each monitor shows, by the id of the monitor, as JSON.
|
|
//! - **Query**: one value as text, the answer to a [`StateQuery`].
|
|
//! - **GlobalState**: the ignore and the manage rules, the transparency settings and the data
|
|
//! directory ([`GlobalState`]), as JSON.
|
|
//! - **SessionFloatRules**: the float rules **SessionFloatRule** added, as JSON.
|
|
//! - **DebugWindow**: what [`Window::should_manage`] read of a window and which rules matched it
|
|
//! ([`RuleDebug`]), as JSON, or nothing for a window that is not there.
|
|
//! - **GenerateStaticConfig**: the window manager's settings as a `komorebi.json`
|
|
//! ([`StaticConfig`]), as JSON.
|
|
//! - **StaticConfigSchema**, **ApplicationSpecificConfigurationSchema**, **NotificationSchema**
|
|
//! and **SocketSchema**: the JSON Schema of `komorebi.json`, of a list of applications files,
|
|
//! of a [`Notification`] and of a [`SocketMessage`]. They answer nothing when the daemon is
|
|
//! built without the `schemars` feature.
|
|
//!
|
|
//! The others answer nothing:
|
|
//!
|
|
//! - **FocusWindow** and **CycleFocusWindow** give the focus to the container on one side of the
|
|
//! focused one (or of the monitor on that side, at the edge of a monitor), or to the next or the
|
|
//! previous one in the workspace
|
|
//! ([`WindowManager::focus_container_in_direction`],
|
|
//! [`WindowManager::focus_container_in_cycle_direction`]). On the floating layer they give it
|
|
//! to a floating window instead ([`WindowManager::focus_floating_window_in_direction`],
|
|
//! [`WindowManager::focus_floating_window_in_cycle_direction`]).
|
|
//! - **MoveWindow** and **CycleMoveWindow** swap the focused container with the one on one side
|
|
//! of it (or take it to the monitor on that side, at the edge of a monitor, as
|
|
//! **CrossMonitorMoveBehaviour** and **ToggleCrossMonitorMoveBehaviour** set), or with the next or
|
|
//! the previous one, and tile the workspace again
|
|
//! ([`WindowManager::move_container_in_direction`],
|
|
//! [`WindowManager::move_container_in_cycle_direction`]). On the floating layer **MoveWindow**
|
|
//! moves the focused floating window instead
|
|
//! ([`WindowManager::move_floating_window_in_direction`]).
|
|
//! - **StackWindow** puts the focused window and the window in the container on one side of it in
|
|
//! one container, a stack that shows the window that moved and hides the other, and
|
|
//! **UnstackWindow** takes the focused window out of its stack again
|
|
//! ([`WindowManager::add_window_to_container`],
|
|
//! [`WindowManager::remove_window_from_container`]). **StackAll** puts every window of the
|
|
//! workspace in one stack, and **UnstackAll** takes every window out of the focused one
|
|
//! ([`WindowManager::stack_all`], [`WindowManager::unstack_all`]). **CycleStack** brings the next
|
|
//! or the previous window of the focused stack to the front, **CycleStackIndex** moves the front
|
|
//! window along the stack, and **FocusStackWindow** brings the window at an index to the front,
|
|
//! on the monitor the pointer is on ([`WindowManager::cycle_container_window_in_direction`],
|
|
//! [`WindowManager::cycle_container_window_index_in_direction`],
|
|
//! [`WindowManager::focus_container_window`]).
|
|
//! - **ToggleWindowContainerBehaviour** and **ToggleWorkspaceWindowContainerBehaviour** set whether
|
|
//! a window that opens gets a container of its own or joins the focused container, for every
|
|
//! workspace or for the focused one, and **ToggleFloatOverride** and
|
|
//! **ToggleWorkspaceFloatOverride** whether it floats
|
|
//! ([`WindowManager::window_management_behaviour`]).
|
|
//! - **Promote** moves the focused container to the largest tile, **PromoteSwap** swaps it with
|
|
//! the container there (and back), **PromoteFocus** gives that container the focus, and
|
|
//! **PromoteWindow** promotes the container on one side of the focused one
|
|
//! ([`WindowManager::promote_container_to_front`], [`WindowManager::promote_container_swap`],
|
|
//! [`WindowManager::promote_focus_to_front`]).
|
|
//! - **ChangeLayout**, **CycleLayout** and **FlipLayout** give the focused workspace another
|
|
//! layout, or mirror it, and **ToggleTiling** turns its tiling off and on
|
|
//! ([`WindowManager::change_workspace_layout_default`], [`WindowManager::cycle_layout`],
|
|
//! [`WindowManager::flip_layout`], [`WindowManager::toggle_tiling`]).
|
|
//! - **ScrollingLayoutColumns** sets how many columns the scrolling layout shows on the focused
|
|
//! workspace, and **LayoutRatios** the share of the work area its first columns and rows take,
|
|
//! and both tile the workspace again.
|
|
//! - **ScrollingLayoutFullWidth** makes the focused column of the scrolling layout as wide as the
|
|
//! screen, or puts it back, **CenterFocus** scrolls the strip so that the focused column is in
|
|
//! the middle, once, and **Expand** makes the focused column fill the room that the columns in
|
|
//! view leave.
|
|
//! - **Consume** takes the first window of the column on the right, or the last of the one on the
|
|
//! left, into the focused column, **Expel** takes the focused window out of its column into one
|
|
//! of its own on the right, and **ToggleColumnStack** stacks the windows of the focused column
|
|
//! or spreads the focused stack out into a column. They do nothing in the other layouts.
|
|
//! - **Pan** moves the view of the scrolling layout a column to the left or the right, without
|
|
//! moving the focus first, and lets it come to rest with a column against an edge of the
|
|
//! screen ([`WindowManager::pan`]).
|
|
//! - **InitialWorkspaceRule**, **InitialNamedWorkspaceRule**, **WorkspaceRule** and
|
|
//! **NamedWorkspaceRule** add a rule that sends the windows of an application to a workspace,
|
|
//! given by its indices or by its name: when they open for the first time, or always;
|
|
//! **ClearWorkspaceRules**, **ClearNamedWorkspaceRules** and **ClearAllWorkspaceRules** take
|
|
//! rules away, and **EnforceWorkspaceRules** applies all of them to the windows that are open
|
|
//! ([`WindowManager::enforce_workspace_rules`]).
|
|
//! - **WorkspaceLayoutRule** and **NamedWorkspaceLayoutRule** make a workspace change to a layout
|
|
//! once it has so many containers, and **ClearWorkspaceLayoutRules** and
|
|
//! **ClearNamedWorkspaceLayoutRules** take its rules away
|
|
//! ([`WindowManager::add_workspace_layout_default_rule`],
|
|
//! [`WindowManager::clear_workspace_layout_rules`]).
|
|
//! - **ReloadStaticConfiguration** applies a `komorebi.json` to the window manager
|
|
//! ([`WindowManager::reload_static_configuration`]). The daemon sends itself this message when
|
|
//! the file it was started with changes. **ReplaceConfiguration** makes the window manager
|
|
//! again from a `komorebi.json`, which loads the monitors and the windows from the backend as
|
|
//! the daemon does when it starts, and watches that file instead of the one it had. The old
|
|
//! window manager is paused and brings back the windows it hid (the back of a stack), so that
|
|
//! every window is in view for the new one to find; a file that is not a `komorebi.json` is
|
|
//! logged and changes nothing.
|
|
//! - **PreselectDirection** reserves the place on one side of the focused container for the next
|
|
//! window ([`WindowManager::preselect_container_in_direction`]), unless a place is reserved
|
|
//! already, and **CancelPreselect** gives the reserved place up.
|
|
//! - **MouseFollowsFocus** and **ToggleMouseFollowsFocus** set whether the pointer is moved to
|
|
//! the window that is given the focus.
|
|
//! - **ToggleLock** locks the focused container at its index, or unlocks it
|
|
//! ([`WindowManager::toggle_lock`]), and **LockMonitorWorkspaceContainer** and
|
|
//! **UnlockMonitorWorkspaceContainer** do the same for a container given by the index of its
|
|
//! monitor, its workspace and itself.
|
|
//! - **ToggleFloat** takes the focused window out of the layout into the floating windows of the
|
|
//! workspace, which komorebi does not move, or puts a floating window back
|
|
//! ([`WindowManager::toggle_float`]).
|
|
//! - **ToggleWorkspaceLayer** switches the focused workspace between its tiling layer and its
|
|
//! floating layer, which brings the windows of that layer to the front and gives the focus to
|
|
//! one of them.
|
|
//! - **ToggleMonocle** gives the focused container the whole work area, with the others out of
|
|
//! the layout and out of sight, or puts it back ([`WindowManager::toggle_monocle`]).
|
|
//! **MonocleFocusBehaviour** sets what focusing in a direction does meanwhile, cycle the
|
|
//! monocle or nothing, and **ToggleMonocleFocusBehaviour** switches between the two.
|
|
//! - **ToggleMaximize** has the compositor maximize the focused window, which leaves the layout,
|
|
//! or restores it and puts it back in its tile ([`WindowManager::toggle_maximize`]). A window
|
|
//! the user maximizes leaves the layout the same way
|
|
//! ([`WindowManager::adopt_maximize_change`]). While a window is maximized, by either, the
|
|
//! messages about the tiles (**MoveWindow**, **CycleMoveWindow**, the unstack and cycle messages,
|
|
//! the resize, promote and preselect messages, **ToggleLock** and the ones that send a container
|
|
//! to another monitor or workspace) are ignored. **ToggleFloat**, **StackWindow** and
|
|
//! **ToggleMonocle** are not: see `is_layout_command`.
|
|
//! - **WorkspacePadding** and **ContainerPadding** set the paddings of a workspace given by the
|
|
//! index of its monitor and its own, **NamedWorkspacePadding** and
|
|
//! **NamedWorkspaceContainerPadding** those of the workspace with a name
|
|
//! ([`WindowManager::monitor_workspace_index_by_name`]; a name no workspace has changes
|
|
//! nothing), **FocusedWorkspacePadding** and **FocusedWorkspaceContainerPadding** those of the
|
|
//! focused workspace, and **AdjustWorkspacePadding** and **AdjustContainerPadding** make the
|
|
//! focused workspace's larger or smaller ([`WindowManager::set_workspace_padding`],
|
|
//! [`WindowManager::set_container_padding`], [`WindowManager::adjust_workspace_padding`],
|
|
//! [`WindowManager::adjust_container_padding`]).
|
|
//! - **WorkAreaOffset**, **MonitorWorkAreaOffset** and **WorkspaceWorkAreaOffset** set the work
|
|
//! area offset of every monitor, of one monitor or of one workspace, and tile every monitor again
|
|
//! (a monitor or a workspace that is not there is ignored), and
|
|
//! **ToggleWindowBasedWorkAreaOffset** sets whether the focused workspace takes its monitor's
|
|
//! window-based offset.
|
|
//! - **Minimize** and **Close** ask the compositor to minimize or close the window that has the
|
|
//! focus. The window leaves the layout when the compositor reports that it did.
|
|
//! - **UnmanageFocusedWindow** takes the window that has the focus out of the layout and leaves it
|
|
//! alone until it closes, and **ManageFocusedWindow** takes it back
|
|
//! ([`WindowManager::unmanage_focused_window`], [`WindowManager::manage_focused_window`]).
|
|
//! - **UnmanagedWindowOperationBehaviour** sets whether a message is carried out while the window
|
|
//! that has the focus is not tiled ([`WindowManager::handle_unmanaged_window_behaviour`]). With
|
|
//! `NoOp`, every message is refused then, this one included.
|
|
//! - **IgnoreRule** adds a rule that keeps the windows of an application out of the layout, and
|
|
//! takes the ones it names exactly out of the workspaces in front; **ManageRule** adds a rule
|
|
//! that has them tiled whatever the ignore rules say. **SessionFloatRule** floats the window
|
|
//! that has the focus and adds a rule that floats the windows that open with its executable,
|
|
//! title and class, and **ClearSessionFloatRules** removes those rules.
|
|
//! - **TogglePause** pauses the window manager, or resumes it: while it is paused, it handles no
|
|
//! event, and of the messages only **TogglePause**, **State**, **GlobalState** and **Stop**
|
|
//! ([`read_commands_uds`]).
|
|
//! - **ResizeWindowEdge** and **ResizeWindowAxis** resize the focused container by the
|
|
//! [`WindowManager::resize_delta`] that **ResizeDelta** sets ([`WindowManager::resize_window`]),
|
|
//! or on the floating layer the focused floating window, and **QuickSave**, **QuickLoad**,
|
|
//! **Save** and **Load** write the resize adjustments of the focused workspace's containers to
|
|
//! a file and read them back.
|
|
//! - **FocusWorkspaceNumber** shows a workspace of the focused monitor, by its index
|
|
//! ([`WindowManager::focus_workspace`]): the compositor is asked to show its desktop on that
|
|
//! monitor. **MoveContainerToWorkspaceNumber** moves the focused container to a workspace and
|
|
//! shows it, and **SendContainerToWorkspaceNumber** moves it there and stays
|
|
//! ([`WindowManager::move_container_to_workspace`]). A workspace that does not exist yet is
|
|
//! made, with the ones before it.
|
|
//! - **CycleFocusWorkspace**, **CycleMoveContainerToWorkspace** and
|
|
//! **CycleSendContainerToWorkspace** do the same for the next or the previous workspace, and
|
|
//! **FocusLastWorkspace**, **MoveContainerToLastWorkspace** and
|
|
//! **SendContainerToLastWorkspace** for the workspace that was in front before.
|
|
//! **CycleFocusEmptyWorkspace** shows the next or the previous workspace with no window, and
|
|
//! **FocusWorkspaceNumbers** shows a workspace on every monitor.
|
|
//! - **WorkspaceName** names a workspace and **EnsureNamedWorkspaces** gives a monitor named
|
|
//! workspaces; **FocusNamedWorkspace** shows the workspace with a name, on its monitor
|
|
//! ([`WindowManager::monitor_workspace_index_by_name`]), and **NamedWorkspaceLayout** and
|
|
//! **NamedWorkspaceTiling** set its layout and its tiling, which **WorkspaceLayout** and
|
|
//! **WorkspaceTiling** do by index. **EnsureWorkspaces** gives a monitor workspaces up to a
|
|
//! number, **NewWorkspace** adds one and shows it, and **CloseWorkspace** removes the workspace
|
|
//! in front when it has no window and no name and is not the monitor's last.
|
|
//! - **FocusMonitorNumber**, **CycleFocusMonitor** and **FocusMonitorAtCursor** focus another
|
|
//! monitor ([`WindowManager::focus_monitor`]), and **FocusMonitorWorkspaceNumber** a workspace
|
|
//! of it. **MoveContainerToMonitorNumber**, **SendContainerToMonitorNumber**, their cycle and
|
|
//! monitor-workspace forms, **MoveContainerToNamedWorkspace** and
|
|
//! **SendContainerToNamedWorkspace** take the focused container to another monitor
|
|
//! ([`WindowManager::move_container_to_monitor`]); **MoveWorkspaceToMonitorNumber**,
|
|
//! **CycleMoveWorkspaceToMonitor** and **SwapWorkspacesToMonitorNumber** move workspaces
|
|
//! between monitors. **DisplayIndexPreference** gives a monitor an index, and **EagerFocus**
|
|
//! focuses the first window of an executable on any workspace of any monitor.
|
|
//! - **FocusFollowsMouse** and **ToggleFocusFollowsMouse** set whether the compositor gives the
|
|
//! focus to the window under the pointer, as the pointer moves: KWin's own focus policy, whichever
|
|
//! of the two implementations is named, put back when it is turned off and when komorebi exits
|
|
//! ([`Backend::set_focus_follows_mouse`](komorebi_backend::Backend::set_focus_follows_mouse)).
|
|
//! - **Border** sets whether the compositor draws a border around each window, and **BorderWidth**
|
|
//! and **BorderOffset** the room the layout leaves for it: each asks the backend for the look
|
|
//! again ([`border_manager::send_notification`]), as does every message once it has been
|
|
//! handled. **BorderColour** sets the color of the border of one kind of window
|
|
//! ([`WindowKind`]), and every message, once handled, tells the backend the color of each
|
|
//! window's border ([`border_manager::update_colours`]).
|
|
//! - **Transparency** and **ToggleTransparency** set whether the windows that do not have the
|
|
//! focus are see-through, and **TransparencyAlpha** how opaque they are, 0 to 255; every message,
|
|
//! once handled, has [`transparency_manager::apply`] make the windows as see-through as that
|
|
//! says.
|
|
//! - **Animation** sets whether windows are animated going to their places, **AnimationDuration**
|
|
//! for how long, **AnimationStyle** the easing curve, and **AnimationFps** the frame rate that is
|
|
//! stored and has nothing to map to (the compositor animates at the refresh rate of the
|
|
//! screen); each is for every kind of animation or, given an
|
|
//! [`AnimationPrefix`](crate::animation::AnimationPrefix), for one. The look asked of the
|
|
//! backend after the message has the animation.
|
|
//! - **Retile** tiles the focused workspace of every monitor again, without the resize
|
|
//! adjustments, and **RetileWithResizeDimensions** with them.
|
|
//! - **AddSubscriberSocket** and **AddSubscriberSocketWithOptions** add a subscriber by the name
|
|
//! of its socket in the data directory, and **RemoveSubscriberSocket** removes one.
|
|
//! - **Stop** stops the daemon, after it has brought back the windows it hid, and
|
|
//! **StopIgnoreRestore** stops it without ([`WindowManager::stop`]).
|
|
//!
|
|
//! Once a message has been handled, every subscriber is sent a notification of it
|
|
//! ([`notify_subscribers`]).
|
|
|
|
use crate::DATA_DIR;
|
|
use crate::FLOATING_APPLICATIONS;
|
|
use crate::IGNORE_IDENTIFIERS;
|
|
use crate::MANAGE_IDENTIFIERS;
|
|
use crate::Notification;
|
|
use crate::NotificationEvent;
|
|
use crate::SESSION_FLOATING_APPLICATIONS;
|
|
use crate::SUBSCRIPTION_SOCKET_OPTIONS;
|
|
use crate::SUBSCRIPTION_SOCKETS;
|
|
use crate::WORKSPACE_MATCHING_RULES;
|
|
use crate::backend::backend;
|
|
use crate::border_manager;
|
|
use crate::core::ApplicationIdentifier;
|
|
use crate::core::Axis;
|
|
use crate::core::CycleDirection;
|
|
use crate::core::Layout;
|
|
use crate::core::LayoutOptions;
|
|
use crate::core::OperationDirection;
|
|
use crate::core::Rect;
|
|
use crate::core::Sizing;
|
|
use crate::core::SocketMessage;
|
|
use crate::core::StateQuery;
|
|
use crate::core::WindowContainerBehaviour;
|
|
use crate::core::WindowKind;
|
|
use crate::core::config_generation::IdWithIdentifier;
|
|
use crate::core::config_generation::MatchingRule;
|
|
use crate::core::config_generation::MatchingStrategy;
|
|
use crate::core::config_generation::WorkspaceMatchingRule;
|
|
use crate::monitor::MonitorExt;
|
|
use crate::monitor::MonitorInformation;
|
|
use crate::notify_subscribers;
|
|
use crate::scrolling::WorkspaceScrolling;
|
|
use crate::settings::animation_off;
|
|
use crate::state::GlobalState;
|
|
use crate::state::State;
|
|
use crate::static_config::StaticConfig;
|
|
use crate::static_config::StaticConfigExt;
|
|
use crate::theme_manager;
|
|
use crate::transparency_manager;
|
|
use crate::window::RuleDebug;
|
|
use crate::window::Window;
|
|
use crate::window_manager::WindowManager;
|
|
use crate::window_manager_event_listener;
|
|
use crate::workspace::WorkspaceExt;
|
|
use crate::workspace::WorkspaceLayer;
|
|
use crate::workspace::WorkspaceWindowLocation;
|
|
use color_eyre::eyre;
|
|
use color_eyre::eyre::Context;
|
|
use color_eyre::eyre::OptionExt;
|
|
use komorebi_config::matching::push_simple_rule;
|
|
use komorebi_themes::colour::Rgb;
|
|
use komorebi_transport::CommandTarget;
|
|
use komorebi_transport::NoPolicy;
|
|
use parking_lot::Mutex;
|
|
use std::collections::HashMap;
|
|
use std::fs::File;
|
|
use std::fs::OpenOptions;
|
|
use std::num::NonZeroUsize;
|
|
use std::os::unix::net::UnixListener;
|
|
use std::os::unix::net::UnixStream;
|
|
use std::sync::Arc;
|
|
use std::sync::atomic::Ordering;
|
|
use std::time::Duration;
|
|
use version::build;
|
|
|
|
/// Starts the thread that accepts connections on the window manager's
|
|
/// [`command_listener`](WindowManager::command_listener), and reads each connection on a separate
|
|
/// thread ([`read_commands_uds`])
|
|
///
|
|
/// When accepting fails, the error is logged and the listening thread is started again.
|
|
#[tracing::instrument]
|
|
pub fn listen_for_commands(wm: Arc<Mutex<WindowManager>>) {
|
|
komorebi_transport::listen_for_commands::<WindowManager, _>(wm, NoPolicy);
|
|
}
|
|
|
|
/// What the generic command listener needs of the window manager
|
|
impl CommandTarget for WindowManager {
|
|
type Listener = UnixListener;
|
|
type Shared = Arc<Mutex<WindowManager>>;
|
|
|
|
fn command_listener(shared: &Self::Shared) -> std::io::Result<UnixListener> {
|
|
shared.lock().command_listener.try_clone()
|
|
}
|
|
|
|
fn try_lock_for<R>(
|
|
shared: &Self::Shared,
|
|
timeout: Duration,
|
|
f: impl FnOnce(&mut Self) -> R,
|
|
) -> Option<R> {
|
|
shared.try_lock_for(timeout).map(|mut wm| f(&mut wm))
|
|
}
|
|
|
|
fn is_paused(&self) -> bool {
|
|
self.is_paused
|
|
}
|
|
|
|
fn process_command(
|
|
&mut self,
|
|
message: SocketMessage,
|
|
reply: &mut UnixStream,
|
|
) -> eyre::Result<()> {
|
|
WindowManager::process_command(self, message, reply)
|
|
}
|
|
}
|
|
|
|
/// Commands that are about the tiles of the layout (moving, stacking, resizing, promoting,
|
|
/// locking, the monocle, and sending containers to other monitors and workspaces), which a
|
|
/// maximized window has left
|
|
///
|
|
/// Three commands that look like them are not here, because komorebi for Windows does not ignore
|
|
/// them either (`maximizing.user` in the end-to-end suite has them as what both ports do):
|
|
/// `toggle-float` floats the maximized window, which ends the maximize
|
|
/// ([`Workspace::new_floating_window`](crate::workspace::Workspace::new_floating_window)),
|
|
/// `stack left` and `stack right` stack the tiles that are left, and `toggle-monocle` turns the
|
|
/// monocle of the tiles that are left on, with the window staying maximized.
|
|
fn is_layout_command(message: &SocketMessage) -> bool {
|
|
matches!(
|
|
message,
|
|
SocketMessage::MoveWindow(_)
|
|
| SocketMessage::CycleMoveWindow(_)
|
|
| SocketMessage::UnstackWindow
|
|
| SocketMessage::CycleStack(_)
|
|
| SocketMessage::CycleStackIndex(_)
|
|
| SocketMessage::FocusStackWindow(_)
|
|
| SocketMessage::StackAll
|
|
| SocketMessage::UnstackAll
|
|
| SocketMessage::ResizeWindowEdge(..)
|
|
| SocketMessage::ResizeWindowAxis(..)
|
|
| SocketMessage::PreselectDirection(_)
|
|
| SocketMessage::Promote
|
|
| SocketMessage::PromoteSwap
|
|
| SocketMessage::PromoteFocus
|
|
| SocketMessage::PromoteWindow(_)
|
|
| SocketMessage::ToggleLock
|
|
| SocketMessage::MoveContainerToLastWorkspace
|
|
| SocketMessage::SendContainerToLastWorkspace
|
|
| SocketMessage::MoveContainerToMonitorNumber(_)
|
|
| SocketMessage::CycleMoveContainerToMonitor(_)
|
|
| SocketMessage::MoveContainerToWorkspaceNumber(_)
|
|
| SocketMessage::MoveContainerToNamedWorkspace(_)
|
|
| SocketMessage::CycleMoveContainerToWorkspace(_)
|
|
| SocketMessage::SendContainerToMonitorNumber(_)
|
|
| SocketMessage::CycleSendContainerToMonitor(_)
|
|
| SocketMessage::SendContainerToWorkspaceNumber(_)
|
|
| SocketMessage::CycleSendContainerToWorkspace(_)
|
|
| SocketMessage::SendContainerToMonitorWorkspaceNumber(..)
|
|
| SocketMessage::MoveContainerToMonitorWorkspaceNumber(..)
|
|
| SocketMessage::SendContainerToNamedWorkspace(_)
|
|
)
|
|
}
|
|
|
|
impl WindowManager {
|
|
/// Sets the border colour of one kind of window
|
|
pub fn set_border_colour(&mut self, kind: WindowKind, r: u32, g: u32, b: u32) {
|
|
match kind {
|
|
WindowKind::Single => {
|
|
border_manager::FOCUSED.store(Rgb::new(r, g, b).into(), Ordering::SeqCst);
|
|
}
|
|
WindowKind::Stack => {
|
|
border_manager::STACK.store(Rgb::new(r, g, b).into(), Ordering::SeqCst);
|
|
}
|
|
WindowKind::Monocle => {
|
|
border_manager::MONOCLE.store(Rgb::new(r, g, b).into(), Ordering::SeqCst);
|
|
}
|
|
WindowKind::Unfocused => {
|
|
border_manager::UNFOCUSED.store(Rgb::new(r, g, b).into(), Ordering::SeqCst);
|
|
}
|
|
WindowKind::UnfocusedLocked => {
|
|
border_manager::UNFOCUSED_LOCKED.store(Rgb::new(r, g, b).into(), Ordering::SeqCst);
|
|
}
|
|
WindowKind::Floating => {
|
|
border_manager::FLOATING.store(Rgb::new(r, g, b).into(), Ordering::SeqCst);
|
|
}
|
|
}
|
|
}
|
|
|
|
/// Writes the answer to a state query to `reply`
|
|
pub fn answer_query(
|
|
&mut self,
|
|
query: StateQuery,
|
|
reply: &mut impl std::io::Write,
|
|
) -> eyre::Result<()> {
|
|
let response = match query {
|
|
StateQuery::FocusedMonitorIndex => self.focused_monitor_idx().to_string(),
|
|
StateQuery::FocusedWorkspaceIndex => self
|
|
.focused_monitor()
|
|
.ok_or_eyre("there is no monitor")?
|
|
.focused_workspace_idx()
|
|
.to_string(),
|
|
StateQuery::FocusedContainerIndex => self
|
|
.focused_workspace()?
|
|
.focused_container_idx()
|
|
.to_string(),
|
|
StateQuery::FocusedWindowIndex => {
|
|
self.focused_container()?.focused_window_idx().to_string()
|
|
}
|
|
StateQuery::FocusedWorkspaceName => {
|
|
let focused_monitor = self.focused_monitor().ok_or_eyre("there is no monitor")?;
|
|
|
|
focused_monitor
|
|
.focused_workspace_name()
|
|
.unwrap_or_else(|| focused_monitor.focused_workspace_idx().to_string())
|
|
}
|
|
StateQuery::Version => build::VERSION.to_string(),
|
|
StateQuery::FocusedWorkspaceLayout => {
|
|
let focused_monitor = self.focused_monitor().ok_or_eyre("there is no monitor")?;
|
|
|
|
focused_monitor.focused_workspace_layout().map_or_else(
|
|
|| "None".to_string(),
|
|
|layout| match layout {
|
|
Layout::Default(default_layout) => default_layout.to_string(),
|
|
},
|
|
)
|
|
}
|
|
StateQuery::FocusedContainerKind => {
|
|
match self.focused_workspace()?.focused_container() {
|
|
None => "None".to_string(),
|
|
Some(container) => {
|
|
if container.windows().len() > 1 {
|
|
"Stack".to_string()
|
|
} else {
|
|
"Single".to_string()
|
|
}
|
|
}
|
|
}
|
|
}
|
|
};
|
|
|
|
reply.write_all(response.as_bytes())?;
|
|
Ok(())
|
|
}
|
|
|
|
/// Replaces the running configuration with the one in the file
|
|
pub fn replace_configuration(&mut self, config: &std::path::Path) -> eyre::Result<()> {
|
|
// Check that this is a valid static config file first
|
|
if StaticConfig::read(config).is_ok() {
|
|
// Clear workspace rules; these will need to be replaced
|
|
WORKSPACE_MATCHING_RULES.lock().clear();
|
|
// Pause so that restored windows come to the foreground from all workspaces
|
|
self.is_paused = true;
|
|
// Bring all windows to the foreground
|
|
self.restore_all_windows(false)?;
|
|
|
|
// Create a new wm from the config path
|
|
let mut wm = StaticConfig::preload(
|
|
config,
|
|
window_manager_event_listener::event_rx(),
|
|
self.command_listener.try_clone().ok(),
|
|
)?;
|
|
|
|
// Initialize the new wm
|
|
wm.init()?;
|
|
|
|
wm.restore_all_windows(true)?;
|
|
|
|
// This is equivalent to StaticConfig::postload for this use case
|
|
StaticConfig::reload(config, &mut wm)?;
|
|
|
|
// Set self to the new wm instance
|
|
*self = wm;
|
|
} else {
|
|
tracing::warn!(
|
|
"not replacing the configuration: {} is not a komorebi.json",
|
|
config.display()
|
|
);
|
|
}
|
|
Ok(())
|
|
}
|
|
|
|
/// Adds a float rule for the focused window (its executable, title and class), until the daemon stops
|
|
pub fn add_session_float_rule(&mut self) -> eyre::Result<()> {
|
|
let foreground_window = Window::from(
|
|
backend()
|
|
.foreground_window()
|
|
.ok_or_eyre("there is no foreground window")?,
|
|
);
|
|
let (exe, title, class) = (
|
|
foreground_window.exe(),
|
|
foreground_window.title(),
|
|
foreground_window.class(),
|
|
);
|
|
|
|
if let (Some(exe), Some(title), Some(class)) = (exe, title, class) {
|
|
let rule = MatchingRule::Composite(vec![
|
|
IdWithIdentifier {
|
|
kind: ApplicationIdentifier::Exe,
|
|
id: exe,
|
|
matching_strategy: Option::from(MatchingStrategy::Equals),
|
|
},
|
|
IdWithIdentifier {
|
|
kind: ApplicationIdentifier::Title,
|
|
id: title,
|
|
matching_strategy: Option::from(MatchingStrategy::Equals),
|
|
},
|
|
IdWithIdentifier {
|
|
kind: ApplicationIdentifier::Class,
|
|
id: class,
|
|
matching_strategy: Option::from(MatchingStrategy::Equals),
|
|
},
|
|
]);
|
|
|
|
let mut floating_applications = FLOATING_APPLICATIONS.lock();
|
|
floating_applications.push(rule.clone());
|
|
let mut session_floating_applications = SESSION_FLOATING_APPLICATIONS.lock();
|
|
session_floating_applications.push(rule.clone());
|
|
|
|
self.toggle_float(true)?;
|
|
}
|
|
Ok(())
|
|
}
|
|
|
|
/// Adds a rule that ignores the windows matching `id`, and takes the ones it matches out of the workspaces
|
|
pub fn add_ignore_rule(
|
|
&mut self,
|
|
identifier: ApplicationIdentifier,
|
|
id: &str,
|
|
) -> eyre::Result<()> {
|
|
let mut ignore_identifiers = IGNORE_IDENTIFIERS.lock();
|
|
|
|
push_simple_rule(&mut ignore_identifiers, identifier, id);
|
|
|
|
let offset = self.work_area_offset;
|
|
|
|
let mut window_ids_to_purge = vec![];
|
|
for (i, monitor) in self.monitors().iter().enumerate() {
|
|
for container in monitor
|
|
.focused_workspace()
|
|
.ok_or_eyre("there is no workspace")?
|
|
.containers()
|
|
{
|
|
for window in container.windows() {
|
|
match identifier {
|
|
ApplicationIdentifier::Path => {
|
|
if window.path().unwrap_or_default() == *id {
|
|
window_ids_to_purge.push((i, window.id));
|
|
}
|
|
}
|
|
ApplicationIdentifier::Exe => {
|
|
if window.exe().unwrap_or_default() == *id {
|
|
window_ids_to_purge.push((i, window.id));
|
|
}
|
|
}
|
|
ApplicationIdentifier::Class => {
|
|
if window.class().unwrap_or_default() == *id {
|
|
window_ids_to_purge.push((i, window.id));
|
|
}
|
|
}
|
|
ApplicationIdentifier::Title => {
|
|
if window.title().unwrap_or_default() == *id {
|
|
window_ids_to_purge.push((i, window.id));
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
for (monitor_idx, id) in window_ids_to_purge {
|
|
let monitor = self
|
|
.monitors_mut()
|
|
.get_mut(monitor_idx)
|
|
.ok_or_eyre("there is no monitor")?;
|
|
|
|
monitor
|
|
.focused_workspace_mut()
|
|
.ok_or_eyre("there is no focused workspace")?
|
|
.remove_window(id)?;
|
|
|
|
monitor.update_focused_workspace(offset)?;
|
|
}
|
|
Ok(())
|
|
}
|
|
|
|
/// Adds a rule that puts a window matching `id` on a named workspace when it first appears, if the workspace exists
|
|
pub fn add_initial_named_workspace_rule(
|
|
&mut self,
|
|
identifier: ApplicationIdentifier,
|
|
id: &str,
|
|
workspace: &str,
|
|
) -> eyre::Result<()> {
|
|
if let Some((monitor_idx, workspace_idx)) = self.monitor_workspace_index_by_name(workspace)
|
|
{
|
|
let mut workspace_rules = WORKSPACE_MATCHING_RULES.lock();
|
|
let workspace_matching_rule = WorkspaceMatchingRule {
|
|
monitor_index: monitor_idx,
|
|
workspace_index: workspace_idx,
|
|
matching_rule: MatchingRule::Simple(IdWithIdentifier {
|
|
kind: identifier,
|
|
id: id.to_string(),
|
|
matching_strategy: Some(MatchingStrategy::Legacy),
|
|
}),
|
|
initial_only: true,
|
|
};
|
|
|
|
if !workspace_rules.contains(&workspace_matching_rule) {
|
|
workspace_rules.push(workspace_matching_rule);
|
|
}
|
|
}
|
|
Ok(())
|
|
}
|
|
|
|
/// Adds a rule that puts the windows matching `id` on a named workspace, if the workspace exists
|
|
pub fn add_named_workspace_rule(
|
|
&mut self,
|
|
identifier: ApplicationIdentifier,
|
|
id: &str,
|
|
workspace: &str,
|
|
) -> eyre::Result<()> {
|
|
if let Some((monitor_idx, workspace_idx)) = self.monitor_workspace_index_by_name(workspace)
|
|
{
|
|
let mut workspace_rules = WORKSPACE_MATCHING_RULES.lock();
|
|
let workspace_matching_rule = WorkspaceMatchingRule {
|
|
monitor_index: monitor_idx,
|
|
workspace_index: workspace_idx,
|
|
matching_rule: MatchingRule::Simple(IdWithIdentifier {
|
|
kind: identifier,
|
|
id: id.to_string(),
|
|
matching_strategy: Some(MatchingStrategy::Legacy),
|
|
}),
|
|
initial_only: false,
|
|
};
|
|
|
|
if !workspace_rules.contains(&workspace_matching_rule) {
|
|
workspace_rules.push(workspace_matching_rule);
|
|
}
|
|
}
|
|
Ok(())
|
|
}
|
|
|
|
/// Resizes the focused window along one axis
|
|
pub fn resize_window_axis(&mut self, axis: Axis, sizing: Sizing) -> eyre::Result<()> {
|
|
match axis {
|
|
Axis::Horizontal => {
|
|
self.resize_window(OperationDirection::Left, sizing, self.resize_delta, false)?;
|
|
self.resize_window(OperationDirection::Right, sizing, self.resize_delta, false)?;
|
|
}
|
|
Axis::Vertical => {
|
|
self.resize_window(OperationDirection::Up, sizing, self.resize_delta, false)?;
|
|
self.resize_window(OperationDirection::Down, sizing, self.resize_delta, false)?;
|
|
}
|
|
Axis::HorizontalAndVertical => {
|
|
self.resize_window(OperationDirection::Left, sizing, self.resize_delta, false)?;
|
|
self.resize_window(OperationDirection::Right, sizing, self.resize_delta, false)?;
|
|
self.resize_window(OperationDirection::Up, sizing, self.resize_delta, false)?;
|
|
self.resize_window(OperationDirection::Down, sizing, self.resize_delta, false)?;
|
|
}
|
|
}
|
|
|
|
self.update_focused_workspace(false, false)?;
|
|
Ok(())
|
|
}
|
|
|
|
/// Sets the column and row ratios of the focused workspace's layout
|
|
pub fn set_layout_ratios(
|
|
&mut self,
|
|
columns: &Option<Vec<f32>>,
|
|
rows: &Option<Vec<f32>>,
|
|
) -> eyre::Result<()> {
|
|
use crate::core::validate_ratios;
|
|
|
|
let focused_workspace = self.focused_workspace_mut()?;
|
|
|
|
let mut options = focused_workspace.layout_options.unwrap_or(LayoutOptions {
|
|
scrolling: None,
|
|
grid: None,
|
|
column_ratios: None,
|
|
row_ratios: None,
|
|
});
|
|
|
|
if let Some(cols) = columns {
|
|
options.column_ratios = Some(validate_ratios(cols));
|
|
}
|
|
|
|
if let Some(rws) = rows {
|
|
options.row_ratios = Some(validate_ratios(rws));
|
|
}
|
|
|
|
focused_workspace.layout_options = Some(options);
|
|
self.update_focused_workspace(false, false)?;
|
|
Ok(())
|
|
}
|
|
|
|
/// Switches the focused workspace between creating a container for a new window and adding it to the focused one
|
|
pub fn toggle_workspace_window_container_behaviour(&mut self) -> eyre::Result<()> {
|
|
let current_global_behaviour = self.window_management_behaviour.current_behaviour;
|
|
if let Some(behaviour) = &mut self.focused_workspace_mut()?.window_container_behaviour {
|
|
match behaviour {
|
|
WindowContainerBehaviour::Create => *behaviour = WindowContainerBehaviour::Append,
|
|
WindowContainerBehaviour::Append => *behaviour = WindowContainerBehaviour::Create,
|
|
}
|
|
} else {
|
|
self.focused_workspace_mut()?.window_container_behaviour =
|
|
Some(match current_global_behaviour {
|
|
WindowContainerBehaviour::Create => WindowContainerBehaviour::Append,
|
|
WindowContainerBehaviour::Append => WindowContainerBehaviour::Create,
|
|
});
|
|
};
|
|
Ok(())
|
|
}
|
|
|
|
/// Looks for a window of the executable `exe` on every monitor and workspace, and focuses it
|
|
pub fn eager_focus(&mut self, exe: &str) -> eyre::Result<()> {
|
|
let focused_monitor_idx = self.focused_monitor_idx();
|
|
let mouse_follows_focus = self.mouse_follows_focus;
|
|
|
|
let mut window_location = None;
|
|
let mut monitor_to_focus = None;
|
|
let mut needs_workspace_loading = false;
|
|
|
|
'search: for (monitor_idx, monitor) in self.monitors_mut().iter_mut().enumerate() {
|
|
for (workspace_idx, workspace) in monitor.workspaces().iter().enumerate() {
|
|
if let Some(location) = workspace.location_from_exe(exe) {
|
|
window_location = Some(location);
|
|
|
|
if monitor_idx != focused_monitor_idx {
|
|
monitor_to_focus = Some(monitor_idx);
|
|
}
|
|
|
|
// Focus workspace if it is not already the focused one, without
|
|
// loading it so that we don't give focus to the wrong window, we will
|
|
// load it later after focusing the wanted window
|
|
let focused_ws_idx = monitor.focused_workspace_idx();
|
|
if focused_ws_idx != workspace_idx {
|
|
monitor.last_focused_workspace = Option::from(focused_ws_idx);
|
|
monitor.focus_workspace(workspace_idx)?;
|
|
needs_workspace_loading = true;
|
|
}
|
|
|
|
break 'search;
|
|
}
|
|
}
|
|
}
|
|
|
|
if let Some(monitor_idx) = monitor_to_focus {
|
|
self.focus_monitor(monitor_idx)?;
|
|
}
|
|
|
|
if let Some(location) = window_location {
|
|
match location {
|
|
WorkspaceWindowLocation::Monocle(window_idx) => {
|
|
self.focus_container_window(window_idx)?;
|
|
}
|
|
WorkspaceWindowLocation::Maximized => {
|
|
if let Some(window) = &mut self.focused_workspace_mut()?.maximized_window {
|
|
window.focus(mouse_follows_focus)?;
|
|
}
|
|
}
|
|
WorkspaceWindowLocation::Container(container_idx, window_idx) => {
|
|
let focused_container_idx = self.focused_container_idx()?;
|
|
if container_idx != focused_container_idx {
|
|
self.focused_workspace_mut()?.focus_container(container_idx);
|
|
}
|
|
|
|
self.focus_container_window(window_idx)?;
|
|
}
|
|
WorkspaceWindowLocation::Floating(window_idx) => {
|
|
if let Some(window) = self
|
|
.focused_workspace_mut()?
|
|
.floating_windows_mut()
|
|
.get_mut(window_idx)
|
|
{
|
|
window.focus(mouse_follows_focus)?;
|
|
}
|
|
}
|
|
}
|
|
|
|
if needs_workspace_loading {
|
|
let mouse_follows_focus = self.mouse_follows_focus;
|
|
if let Some(monitor) = self.focused_monitor_mut() {
|
|
monitor.load_focused_workspace(mouse_follows_focus)?;
|
|
}
|
|
}
|
|
}
|
|
Ok(())
|
|
}
|
|
|
|
/// Brings the other layer of the focused workspace, the floating windows or the tiled ones, in front, and focuses a window of it
|
|
pub fn toggle_workspace_layer(&mut self) -> eyre::Result<()> {
|
|
let mouse_follows_focus = self.mouse_follows_focus;
|
|
let workspace = self.focused_workspace_mut()?;
|
|
|
|
let mut to_focus = None;
|
|
match workspace.layer {
|
|
WorkspaceLayer::Tiling => {
|
|
workspace.layer = WorkspaceLayer::Floating;
|
|
tracing::info!("WorkspaceLayer is now Floating");
|
|
|
|
let focused_idx = workspace.focused_floating_window_idx();
|
|
let mut window_idx_pairs = workspace
|
|
.floating_windows_mut()
|
|
.make_contiguous()
|
|
.iter_mut()
|
|
.enumerate()
|
|
.collect::<Vec<_>>();
|
|
|
|
// Sort by window area
|
|
window_idx_pairs.sort_by_key(|(_, w)| {
|
|
let rect = w.rect().unwrap_or_default();
|
|
rect.right * rect.bottom
|
|
});
|
|
window_idx_pairs.reverse();
|
|
|
|
for (i, window) in window_idx_pairs {
|
|
if i == focused_idx {
|
|
to_focus = Some(*window);
|
|
} else {
|
|
window.restore();
|
|
window.raise()?;
|
|
}
|
|
}
|
|
|
|
if let Some(focused_window) = &mut to_focus {
|
|
// The focused window should be the last one raised to make sure it is
|
|
// on top
|
|
focused_window.restore();
|
|
focused_window.raise()?;
|
|
}
|
|
}
|
|
WorkspaceLayer::Floating => {
|
|
workspace.layer = WorkspaceLayer::Tiling;
|
|
tracing::info!("WorkspaceLayer is now Tiling");
|
|
|
|
if let Some(monocle) = &workspace.monocle_container {
|
|
if let Some(window) = monocle.focused_window() {
|
|
to_focus = Some(*window);
|
|
window.raise()?;
|
|
}
|
|
|
|
for window in workspace.floating_windows_mut() {
|
|
window.hide();
|
|
}
|
|
} else {
|
|
let focused_container_idx = workspace.focused_container_idx();
|
|
for (i, container) in workspace.containers_mut().iter_mut().enumerate() {
|
|
if let Some(window) = container.focused_window() {
|
|
if i == focused_container_idx {
|
|
to_focus = Some(*window);
|
|
}
|
|
|
|
window.raise()?;
|
|
}
|
|
}
|
|
|
|
let mut window_idx_pairs = workspace
|
|
.floating_windows_mut()
|
|
.make_contiguous()
|
|
.iter_mut()
|
|
.collect::<Vec<_>>();
|
|
|
|
// Sort by window area
|
|
window_idx_pairs.sort_by_key(|w| {
|
|
let rect = w.rect().unwrap_or_default();
|
|
rect.right * rect.bottom
|
|
});
|
|
|
|
for window in window_idx_pairs {
|
|
window.hide();
|
|
}
|
|
}
|
|
}
|
|
};
|
|
|
|
if let Some(window) = to_focus {
|
|
window.focus(mouse_follows_focus)?;
|
|
}
|
|
Ok(())
|
|
}
|
|
|
|
/// Focuses the next or previous workspace of the monitor that is focused, or of the monitor the pointer is on when its workspace is empty
|
|
pub fn cycle_focus_workspace(&mut self, direction: CycleDirection) -> eyre::Result<()> {
|
|
komorebi_core::cycle_focus_workspace(self, direction)
|
|
}
|
|
|
|
/// Focuses the next or previous empty workspace of the focused monitor, if it has one
|
|
pub fn cycle_focus_empty_workspace(&mut self, direction: CycleDirection) -> eyre::Result<()> {
|
|
komorebi_core::cycle_focus_empty_workspace(self, direction)
|
|
}
|
|
|
|
/// Removes the focused workspace when it is empty, unnamed and not the monitor's only one, and focuses the one before it
|
|
pub fn close_workspace(&mut self) -> eyre::Result<()> {
|
|
komorebi_core::close_workspace(self)
|
|
}
|
|
|
|
/// Moves the focused container to the workspace that was focused before this one, and follows it when `follow` is set
|
|
pub fn move_container_to_last_workspace(&mut self, follow: bool) -> eyre::Result<()> {
|
|
komorebi_core::move_container_to_last_workspace(self, follow)
|
|
}
|
|
|
|
/// Focuses the workspace that was focused before this one
|
|
pub fn focus_last_workspace(&mut self) -> eyre::Result<()> {
|
|
komorebi_core::focus_last_workspace(self)
|
|
}
|
|
|
|
/// Handles one message, writes the answer to a question to `reply`, and sends the subscribers
|
|
/// a notification of the message
|
|
///
|
|
/// A message that fails is not notified.
|
|
///
|
|
/// # Errors
|
|
///
|
|
/// - The unmanaged window operation behavior is `NoOp` and the window that has the focus is not
|
|
/// tiled, for one of the reasons [`WindowManager::handle_unmanaged_window_behaviour`] gives.
|
|
/// - A query about the focused monitor, workspace or container when there is none.
|
|
/// - The focus cannot be moved, for one of the reasons
|
|
/// [`WindowManager::focus_container_in_direction`] and
|
|
/// [`WindowManager::focus_container_in_cycle_direction`] give.
|
|
/// - The focused container cannot be moved, for one of the reasons
|
|
/// [`WindowManager::move_container_in_direction`] and
|
|
/// [`WindowManager::move_container_in_cycle_direction`] give.
|
|
/// - There is no focused workspace to change the layout or the tiling of, to preselect a
|
|
/// place in, or to resize or promote a container of, or it cannot be tiled again.
|
|
/// - The monitor or the workspace of `LockMonitorWorkspaceContainer` or
|
|
/// `UnlockMonitorWorkspaceContainer` is not there (a container that is not there is ignored).
|
|
/// - The focused window cannot be floated or put back, for one of the reasons
|
|
/// [`WindowManager::toggle_float`] gives.
|
|
/// - The monocle cannot be turned on or off, for one of the reasons
|
|
/// [`WindowManager::toggle_monocle`] gives.
|
|
/// - The focused window cannot be maximized or restored, for one of the reasons
|
|
/// [`WindowManager::toggle_maximize`] gives.
|
|
/// - The focused window cannot be stacked, unstacked or cycled, for one of the reasons
|
|
/// [`WindowManager::add_window_to_container`], [`WindowManager::remove_window_from_container`],
|
|
/// [`WindowManager::stack_all`], [`WindowManager::unstack_all`],
|
|
/// [`WindowManager::cycle_container_window_in_direction`],
|
|
/// [`WindowManager::cycle_container_window_index_in_direction`] and
|
|
/// [`WindowManager::focus_container_window`] give, or the focused
|
|
/// workspace is not there for a behavior to be set on (`ToggleWorkspaceWindowContainerBehaviour`,
|
|
/// `ToggleWorkspaceFloatOverride`).
|
|
/// - The monitor a monitor command names is not there, or the compositor reports no window
|
|
/// with the focus to take to it ([`WindowManager::move_container_to_monitor`]).
|
|
/// - The monitor or the workspace a message names by its index is not there
|
|
/// (`WorkspaceName`, `WorkspaceLayout`, `WorkspaceTiling`, `EnsureWorkspaces`,
|
|
/// `EnsureNamedWorkspaces`). A name no workspace has is ignored.
|
|
/// - The focused monitor has no workspace or no container to move to another workspace, or
|
|
/// the compositor reports no window with the focus (`MoveContainerToWorkspaceNumber`,
|
|
/// `SendContainerToWorkspaceNumber`), or the focused window of the workspace that is shown
|
|
/// cannot be focused.
|
|
/// - The monitor or the workspace of `WorkspacePadding` or `ContainerPadding` is not there, or
|
|
/// the focused workspace has no padding of its own for `AdjustWorkspacePadding` or
|
|
/// `AdjustContainerPadding` to change.
|
|
/// - `Minimize`, `Close`, `ManageFocusedWindow`, `UnmanageFocusedWindow` or `SessionFloatRule`
|
|
/// when the compositor reports no focused window, or the backend refuses to minimize, close or
|
|
/// float it.
|
|
/// - `IgnoreRule` cannot take a window out of a workspace, or tile the workspace again.
|
|
/// - The file of `QuickSave` or `Save` cannot be written, or the file of `QuickLoad` or
|
|
/// `Load` cannot be read or is not a list of resize adjustments.
|
|
/// - The window manager fails to retile, for one of the reasons
|
|
/// [`WindowManager::retile_all`] gives.
|
|
/// - The answer cannot be written to `reply`.
|
|
/// - `Stop` or `StopIgnoreRestore` fails, for the reason [`WindowManager::stop`] gives.
|
|
/// - `ReloadHotkeys` fails, for one of the reasons [`crate::kwin::hotkeys::reload`] gives.
|
|
/// - The notification cannot be sent, for one of the reasons [`notify_subscribers`] gives.
|
|
#[tracing::instrument(skip(self, reply, message))]
|
|
pub fn process_command(
|
|
&mut self,
|
|
message: SocketMessage,
|
|
mut reply: impl std::io::Write,
|
|
) -> eyre::Result<()> {
|
|
if matches!(message, SocketMessage::Theme(_)) {
|
|
tracing::trace!("processing command: {message}");
|
|
} else {
|
|
tracing::info!("processing command: {message}");
|
|
}
|
|
|
|
// a window that is maximized (by `toggle-maximize`, or by the user with its title bar) is
|
|
// not in the layout, and what is asked of the layout is not asked of it: nothing happens
|
|
// until it is restored, by `toggle-maximize` or by the user
|
|
if is_layout_command(&message)
|
|
&& self
|
|
.focused_workspace()
|
|
.is_ok_and(|workspace| workspace.maximized_window.is_some())
|
|
{
|
|
tracing::info!("ignoring {message}: the window in front is maximized");
|
|
return Ok(());
|
|
}
|
|
|
|
#[allow(clippy::useless_asref)]
|
|
// We don't have From implemented for &mut WindowManager
|
|
let initial_state = State::from(self.as_ref());
|
|
|
|
komorebi_core::CommandHost::gate_message(self, &message)?;
|
|
|
|
match message {
|
|
SocketMessage::Promote => self.promote_container_to_front()?,
|
|
SocketMessage::PromoteSwap => self.promote_container_swap()?,
|
|
SocketMessage::PromoteFocus => self.promote_focus_to_front()?,
|
|
SocketMessage::PromoteWindow(direction) => {
|
|
komorebi_core::promote_window(self, direction)?
|
|
}
|
|
SocketMessage::EagerFocus(ref exe) => self.eager_focus(exe)?,
|
|
SocketMessage::FocusWindow(direction) => komorebi_core::focus_window(self, direction)?,
|
|
SocketMessage::PreselectDirection(direction) => {
|
|
komorebi_core::preselect_direction(self, direction)?
|
|
}
|
|
SocketMessage::CancelPreselect => komorebi_core::cancel_preselect(self)?,
|
|
SocketMessage::MoveWindow(direction) => komorebi_core::move_window(self, direction)?,
|
|
SocketMessage::CycleFocusWindow(direction) => {
|
|
komorebi_core::cycle_focus_window(self, direction)?
|
|
}
|
|
SocketMessage::CycleMoveWindow(direction) => {
|
|
self.move_container_in_cycle_direction(direction)?;
|
|
}
|
|
SocketMessage::StackWindow(direction) => {
|
|
let _animation_off = animation_off();
|
|
self.add_window_to_container(direction)?;
|
|
}
|
|
SocketMessage::UnstackWindow => {
|
|
let _animation_off = animation_off();
|
|
self.remove_window_from_container()?;
|
|
border_manager::destroy_all_borders()?;
|
|
}
|
|
SocketMessage::StackAll => {
|
|
let _animation_off = animation_off();
|
|
self.stack_all()?;
|
|
}
|
|
SocketMessage::UnstackAll => {
|
|
let _animation_off = animation_off();
|
|
self.unstack_all(true)?;
|
|
border_manager::destroy_all_borders()?;
|
|
}
|
|
SocketMessage::CycleStack(direction) => {
|
|
let _animation_off = animation_off();
|
|
self.cycle_container_window_in_direction(direction)?;
|
|
}
|
|
SocketMessage::CycleStackIndex(direction) => {
|
|
let _animation_off = animation_off();
|
|
self.cycle_container_window_index_in_direction(direction)?;
|
|
}
|
|
SocketMessage::FocusStackWindow(idx) => {
|
|
// In case you are using this command on a bar on a monitor
|
|
// different from the currently focused one, you'd want that
|
|
// monitor to be focused so that the FocusStackWindow happens
|
|
// on the monitor with the bar you just pressed.
|
|
if let Some(monitor_idx) = self.monitor_idx_from_current_pos() {
|
|
self.focus_monitor(monitor_idx)?;
|
|
}
|
|
let _animation_off = animation_off();
|
|
self.focus_container_window(idx)?;
|
|
}
|
|
SocketMessage::CycleFocusMonitor(direction) => {
|
|
komorebi_core::cycle_focus_monitor(self, direction)?
|
|
}
|
|
SocketMessage::CycleFocusWorkspace(direction) => {
|
|
self.cycle_focus_workspace(direction)?
|
|
}
|
|
SocketMessage::CycleFocusEmptyWorkspace(direction) => {
|
|
self.cycle_focus_empty_workspace(direction)?
|
|
}
|
|
SocketMessage::FocusMonitorNumber(monitor_idx) => {
|
|
self.focus_monitor(monitor_idx)?;
|
|
self.update_focused_workspace(self.mouse_follows_focus, true)?;
|
|
}
|
|
SocketMessage::FocusMonitorAtCursor => {
|
|
if let Some(monitor_idx) = self.monitor_idx_from_current_pos() {
|
|
self.focus_monitor(monitor_idx)?;
|
|
}
|
|
}
|
|
SocketMessage::FocusLastWorkspace => self.focus_last_workspace()?,
|
|
SocketMessage::FocusWorkspaceNumber(workspace_idx) => {
|
|
komorebi_core::focus_workspace_number(self, workspace_idx)?
|
|
}
|
|
SocketMessage::FocusWorkspaceNumbers(workspace_idx) => {
|
|
komorebi_core::focus_workspace_numbers(self, workspace_idx)?
|
|
}
|
|
SocketMessage::FocusMonitorWorkspaceNumber(monitor_idx, workspace_idx) => {
|
|
komorebi_core::focus_monitor_workspace_number(self, monitor_idx, workspace_idx)?
|
|
}
|
|
SocketMessage::FocusNamedWorkspace(ref name) => {
|
|
komorebi_core::focus_named_workspace(self, name)?
|
|
}
|
|
SocketMessage::CloseWorkspace => self.close_workspace()?,
|
|
|
|
SocketMessage::MoveContainerToWorkspaceNumber(workspace_idx) => {
|
|
self.move_container_to_workspace(workspace_idx, true, None)?;
|
|
}
|
|
SocketMessage::SendContainerToWorkspaceNumber(workspace_idx) => {
|
|
self.move_container_to_workspace(workspace_idx, false, None)?;
|
|
}
|
|
SocketMessage::MoveContainerToLastWorkspace => {
|
|
self.move_container_to_last_workspace(true)?
|
|
}
|
|
SocketMessage::SendContainerToLastWorkspace => {
|
|
self.move_container_to_last_workspace(false)?;
|
|
}
|
|
SocketMessage::CycleMoveContainerToWorkspace(direction) => {
|
|
komorebi_core::cycle_move_container_to_workspace(self, direction, true)?
|
|
}
|
|
SocketMessage::MoveContainerToMonitorNumber(monitor_idx) => {
|
|
let direction = self.direction_from_monitor_idx(monitor_idx);
|
|
self.move_container_to_monitor(monitor_idx, None, true, direction)?;
|
|
}
|
|
SocketMessage::SwapWorkspacesToMonitorNumber(monitor_idx) => {
|
|
self.swap_focused_monitor(monitor_idx)?;
|
|
}
|
|
SocketMessage::CycleMoveContainerToMonitor(direction) => {
|
|
komorebi_core::cycle_move_container_to_monitor(self, direction, true)?
|
|
}
|
|
SocketMessage::CycleSendContainerToWorkspace(direction) => {
|
|
komorebi_core::cycle_move_container_to_workspace(self, direction, false)?
|
|
}
|
|
SocketMessage::SendContainerToMonitorNumber(monitor_idx) => {
|
|
let direction = self.direction_from_monitor_idx(monitor_idx);
|
|
self.move_container_to_monitor(monitor_idx, None, false, direction)?;
|
|
}
|
|
SocketMessage::CycleSendContainerToMonitor(direction) => {
|
|
komorebi_core::cycle_move_container_to_monitor(self, direction, false)?
|
|
}
|
|
SocketMessage::SendContainerToMonitorWorkspaceNumber(monitor_idx, workspace_idx) => {
|
|
komorebi_core::move_container_to_monitor_workspace_number(
|
|
self,
|
|
monitor_idx,
|
|
workspace_idx,
|
|
false,
|
|
)?
|
|
}
|
|
SocketMessage::MoveContainerToMonitorWorkspaceNumber(monitor_idx, workspace_idx) => {
|
|
komorebi_core::move_container_to_monitor_workspace_number(
|
|
self,
|
|
monitor_idx,
|
|
workspace_idx,
|
|
true,
|
|
)?
|
|
}
|
|
SocketMessage::SendContainerToNamedWorkspace(ref workspace) => {
|
|
komorebi_core::move_container_to_named_workspace(self, workspace, false)?
|
|
}
|
|
SocketMessage::MoveContainerToNamedWorkspace(ref workspace) => {
|
|
komorebi_core::move_container_to_named_workspace(self, workspace, true)?
|
|
}
|
|
|
|
SocketMessage::MoveWorkspaceToMonitorNumber(monitor_idx) => {
|
|
self.move_workspace_to_monitor(monitor_idx)?;
|
|
}
|
|
SocketMessage::CycleMoveWorkspaceToMonitor(direction) => {
|
|
let monitor_idx = direction.next_idx(
|
|
self.focused_monitor_idx(),
|
|
NonZeroUsize::new(self.monitors().len())
|
|
.ok_or_eyre("there must be at least one monitor")?,
|
|
);
|
|
|
|
self.move_workspace_to_monitor(monitor_idx)?;
|
|
}
|
|
SocketMessage::ReloadStaticConfiguration(ref pathbuf) => {
|
|
self.reload_static_configuration(pathbuf)?;
|
|
}
|
|
SocketMessage::ToggleCrossMonitorMoveBehaviour => {
|
|
komorebi_core::toggle_cross_monitor_move_behaviour(self)
|
|
}
|
|
SocketMessage::CrossMonitorMoveBehaviour(behaviour) => {
|
|
self.cross_monitor_move_behaviour = behaviour;
|
|
}
|
|
SocketMessage::DisplayIndexPreference(index_preference, ref display) => {
|
|
crate::settings::settings().update(|settings| {
|
|
settings
|
|
.shared
|
|
.display_index_preferences
|
|
.insert(index_preference, display.clone());
|
|
});
|
|
}
|
|
SocketMessage::ReplaceConfiguration(ref config) => {
|
|
self.replace_configuration(config)?
|
|
}
|
|
SocketMessage::NewWorkspace => {
|
|
self.new_workspace()?;
|
|
}
|
|
SocketMessage::WorkspaceTiling(monitor_idx, workspace_idx, tile) => {
|
|
self.set_workspace_tiling(monitor_idx, workspace_idx, tile)?;
|
|
}
|
|
SocketMessage::WorkspaceLayout(monitor_idx, workspace_idx, layout) => {
|
|
self.set_workspace_layout_default(monitor_idx, workspace_idx, layout)?;
|
|
}
|
|
SocketMessage::WorkspaceLayoutRule(
|
|
monitor_idx,
|
|
workspace_idx,
|
|
at_container_count,
|
|
layout,
|
|
) => {
|
|
self.add_workspace_layout_default_rule(
|
|
monitor_idx,
|
|
workspace_idx,
|
|
at_container_count,
|
|
layout,
|
|
)?;
|
|
}
|
|
SocketMessage::NamedWorkspaceTiling(ref workspace, tile) => {
|
|
if let Some((monitor_idx, workspace_idx)) =
|
|
self.monitor_workspace_index_by_name(workspace)
|
|
{
|
|
self.set_workspace_tiling(monitor_idx, workspace_idx, tile)?;
|
|
}
|
|
}
|
|
SocketMessage::WorkspaceName(monitor_idx, workspace_idx, ref name) => {
|
|
self.set_workspace_name(monitor_idx, workspace_idx, name.to_string())?;
|
|
}
|
|
SocketMessage::NamedWorkspaceLayout(ref workspace, layout) => {
|
|
if let Some((monitor_idx, workspace_idx)) =
|
|
self.monitor_workspace_index_by_name(workspace)
|
|
{
|
|
self.set_workspace_layout_default(monitor_idx, workspace_idx, layout)?;
|
|
}
|
|
}
|
|
SocketMessage::ClearWorkspaceLayoutRules(monitor_idx, workspace_idx) => {
|
|
self.clear_workspace_layout_rules(monitor_idx, workspace_idx)?;
|
|
}
|
|
SocketMessage::EnsureWorkspaces(monitor_idx, workspace_count) => {
|
|
self.ensure_workspaces_for_monitor(monitor_idx, workspace_count)?;
|
|
}
|
|
SocketMessage::EnsureNamedWorkspaces(monitor_idx, ref names) => {
|
|
self.ensure_named_workspaces_for_monitor(monitor_idx, names)?;
|
|
}
|
|
SocketMessage::FocusFollowsMouse(implementation, enable) => {
|
|
// KWin does this, whichever of the two was asked for
|
|
self.focus_follows_mouse = enable.then_some(implementation);
|
|
backend().set_focus_follows_mouse(enable)?;
|
|
}
|
|
SocketMessage::ToggleFocusFollowsMouse(implementation) => {
|
|
let enable = self.focus_follows_mouse.is_none();
|
|
self.focus_follows_mouse = enable.then_some(implementation);
|
|
backend().set_focus_follows_mouse(enable)?;
|
|
}
|
|
SocketMessage::ToggleMonocle => self.toggle_monocle()?,
|
|
SocketMessage::ToggleMaximize => self.toggle_maximize()?,
|
|
SocketMessage::ToggleFloat => self.toggle_float(false)?,
|
|
SocketMessage::ToggleWorkspaceLayer => self.toggle_workspace_layer()?,
|
|
SocketMessage::ResizeWindowEdge(direction, sizing) => {
|
|
self.resize_window(direction, sizing, self.resize_delta, true)?;
|
|
}
|
|
SocketMessage::ResizeWindowAxis(axis, sizing) => {
|
|
self.resize_window_axis(axis, sizing)?
|
|
}
|
|
SocketMessage::Minimize => self.minimize_foreground_window()?,
|
|
SocketMessage::Close => self.close_foreground_window()?,
|
|
SocketMessage::ManageFocusedWindow => {
|
|
self.manage_focused_window()?;
|
|
}
|
|
SocketMessage::UnmanageFocusedWindow => {
|
|
self.unmanage_focused_window()?;
|
|
}
|
|
SocketMessage::LockMonitorWorkspaceContainer(
|
|
monitor_idx,
|
|
workspace_idx,
|
|
container_idx,
|
|
) => komorebi_core::set_container_locked(
|
|
self,
|
|
monitor_idx,
|
|
workspace_idx,
|
|
container_idx,
|
|
true,
|
|
)?,
|
|
SocketMessage::UnlockMonitorWorkspaceContainer(
|
|
monitor_idx,
|
|
workspace_idx,
|
|
container_idx,
|
|
) => komorebi_core::set_container_locked(
|
|
self,
|
|
monitor_idx,
|
|
workspace_idx,
|
|
container_idx,
|
|
false,
|
|
)?,
|
|
SocketMessage::FlipLayout(layout_flip) => self.flip_layout(layout_flip)?,
|
|
SocketMessage::ChangeLayout(layout) => self.change_workspace_layout_default(layout)?,
|
|
SocketMessage::CycleLayout(direction) => self.cycle_layout(direction)?,
|
|
SocketMessage::TogglePause => {
|
|
if self.is_paused {
|
|
tracing::info!("resuming");
|
|
} else {
|
|
tracing::info!("pausing");
|
|
}
|
|
|
|
self.is_paused = !self.is_paused;
|
|
}
|
|
SocketMessage::Retile => self.retile_all(false)?,
|
|
SocketMessage::RetileWithResizeDimensions => self.retile_all(true)?,
|
|
SocketMessage::ToggleWorkspaceWindowContainerBehaviour => {
|
|
self.toggle_workspace_window_container_behaviour()?
|
|
}
|
|
SocketMessage::ToggleWorkspaceFloatOverride => {
|
|
let current_global_override = self.window_management_behaviour.float_override;
|
|
if let Some(float_override) = &mut self.focused_workspace_mut()?.float_override {
|
|
*float_override = !*float_override;
|
|
} else {
|
|
self.focused_workspace_mut()?.float_override = Some(!current_global_override);
|
|
};
|
|
}
|
|
SocketMessage::ToggleLock => self.toggle_lock()?,
|
|
SocketMessage::ToggleWindowContainerBehaviour => {
|
|
komorebi_core::toggle_window_container_behaviour(self)
|
|
}
|
|
SocketMessage::ToggleFloatOverride => {
|
|
self.window_management_behaviour.float_override =
|
|
!self.window_management_behaviour.float_override;
|
|
}
|
|
SocketMessage::ToggleWindowBasedWorkAreaOffset => {
|
|
let workspace = self.focused_workspace_mut()?;
|
|
workspace.apply_window_based_work_area_offset =
|
|
!workspace.apply_window_based_work_area_offset;
|
|
|
|
self.retile_all(true)?;
|
|
}
|
|
SocketMessage::ToggleTiling => {
|
|
self.toggle_tiling()?;
|
|
}
|
|
SocketMessage::State => {
|
|
let state = match serde_json::to_string_pretty(&State::from(&*self)) {
|
|
Ok(state) => state,
|
|
Err(error) => error.to_string(),
|
|
};
|
|
|
|
tracing::info!("replying to state");
|
|
|
|
reply.write_all(state.as_bytes())?;
|
|
|
|
tracing::info!("replying to state done");
|
|
}
|
|
SocketMessage::GlobalState => {
|
|
let state = match serde_json::to_string_pretty(&GlobalState::default()) {
|
|
Ok(state) => state,
|
|
Err(error) => error.to_string(),
|
|
};
|
|
|
|
tracing::info!("replying to global state");
|
|
|
|
reply.write_all(state.as_bytes())?;
|
|
|
|
tracing::info!("replying to global state done");
|
|
}
|
|
SocketMessage::VisibleWindows => {
|
|
let mut monitor_visible_windows = HashMap::new();
|
|
|
|
for monitor in self.monitors() {
|
|
if let Some(ws) = monitor.focused_workspace() {
|
|
monitor_visible_windows
|
|
.insert(monitor.id, ws.visible_window_details().clone());
|
|
}
|
|
}
|
|
|
|
let visible_windows_state = serde_json::to_string_pretty(&monitor_visible_windows)
|
|
.unwrap_or_else(|error| error.to_string());
|
|
|
|
reply.write_all(visible_windows_state.as_bytes())?;
|
|
}
|
|
SocketMessage::MonitorInformation => {
|
|
let mut monitors = vec![];
|
|
for monitor in self.monitors() {
|
|
monitors.push(MonitorInformation::from(monitor));
|
|
}
|
|
|
|
let monitors_state = serde_json::to_string_pretty(&monitors)
|
|
.unwrap_or_else(|error| error.to_string());
|
|
|
|
reply.write_all(monitors_state.as_bytes())?;
|
|
}
|
|
SocketMessage::Query(query) => self.answer_query(query, &mut reply)?,
|
|
SocketMessage::SessionFloatRule => self.add_session_float_rule()?,
|
|
SocketMessage::SessionFloatRules => {
|
|
let session_floating_applications = SESSION_FLOATING_APPLICATIONS.lock();
|
|
let rules = serde_json::to_string_pretty(&*session_floating_applications)
|
|
.unwrap_or_else(|error| error.to_string());
|
|
|
|
reply.write_all(rules.as_bytes())?;
|
|
}
|
|
SocketMessage::ClearSessionFloatRules => {
|
|
let mut floating_applications = FLOATING_APPLICATIONS.lock();
|
|
let mut session_floating_applications = SESSION_FLOATING_APPLICATIONS.lock();
|
|
floating_applications.retain(|r| !session_floating_applications.contains(r));
|
|
session_floating_applications.clear()
|
|
}
|
|
SocketMessage::InitialWorkspaceRule(identifier, ref id, monitor_idx, workspace_idx) => {
|
|
let mut workspace_rules = WORKSPACE_MATCHING_RULES.lock();
|
|
let workspace_matching_rule = WorkspaceMatchingRule {
|
|
monitor_index: monitor_idx,
|
|
workspace_index: workspace_idx,
|
|
matching_rule: MatchingRule::Simple(IdWithIdentifier {
|
|
kind: identifier,
|
|
id: id.to_string(),
|
|
matching_strategy: Some(MatchingStrategy::Legacy),
|
|
}),
|
|
initial_only: true,
|
|
};
|
|
|
|
if !workspace_rules.contains(&workspace_matching_rule) {
|
|
workspace_rules.push(workspace_matching_rule);
|
|
}
|
|
}
|
|
SocketMessage::InitialNamedWorkspaceRule(identifier, ref id, ref workspace) => {
|
|
self.add_initial_named_workspace_rule(identifier, id, workspace)?
|
|
}
|
|
SocketMessage::WorkspaceRule(identifier, ref id, monitor_idx, workspace_idx) => {
|
|
let mut workspace_rules = WORKSPACE_MATCHING_RULES.lock();
|
|
let workspace_matching_rule = WorkspaceMatchingRule {
|
|
monitor_index: monitor_idx,
|
|
workspace_index: workspace_idx,
|
|
matching_rule: MatchingRule::Simple(IdWithIdentifier {
|
|
kind: identifier,
|
|
id: id.to_string(),
|
|
matching_strategy: Some(MatchingStrategy::Legacy),
|
|
}),
|
|
initial_only: false,
|
|
};
|
|
|
|
if !workspace_rules.contains(&workspace_matching_rule) {
|
|
workspace_rules.push(workspace_matching_rule);
|
|
}
|
|
}
|
|
SocketMessage::NamedWorkspaceRule(identifier, ref id, ref workspace) => {
|
|
self.add_named_workspace_rule(identifier, id, workspace)?
|
|
}
|
|
SocketMessage::ClearWorkspaceRules(monitor_idx, workspace_idx) => {
|
|
let mut workspace_rules = WORKSPACE_MATCHING_RULES.lock();
|
|
|
|
workspace_rules.retain(|r| {
|
|
r.monitor_index != monitor_idx && r.workspace_index != workspace_idx
|
|
});
|
|
}
|
|
SocketMessage::ClearNamedWorkspaceRules(ref workspace) => {
|
|
if let Some((monitor_idx, workspace_idx)) =
|
|
self.monitor_workspace_index_by_name(workspace)
|
|
{
|
|
let mut workspace_rules = WORKSPACE_MATCHING_RULES.lock();
|
|
workspace_rules.retain(|r| {
|
|
r.monitor_index != monitor_idx && r.workspace_index != workspace_idx
|
|
});
|
|
}
|
|
}
|
|
SocketMessage::ClearAllWorkspaceRules => {
|
|
let mut workspace_rules = WORKSPACE_MATCHING_RULES.lock();
|
|
workspace_rules.clear();
|
|
}
|
|
SocketMessage::EnforceWorkspaceRules => {
|
|
{
|
|
let mut already_moved = self.already_moved_window_handles.lock();
|
|
already_moved.clear();
|
|
}
|
|
self.enforce_workspace_rules()?;
|
|
}
|
|
SocketMessage::IgnoreRule(identifier, ref id) => {
|
|
self.add_ignore_rule(identifier, id)?
|
|
}
|
|
SocketMessage::ManageRule(identifier, ref id) => {
|
|
let mut manage_identifiers = MANAGE_IDENTIFIERS.lock();
|
|
|
|
push_simple_rule(&mut manage_identifiers, identifier, id);
|
|
}
|
|
SocketMessage::ContainerPadding(monitor_idx, workspace_idx, size) => {
|
|
self.set_container_padding(monitor_idx, workspace_idx, size)?;
|
|
}
|
|
SocketMessage::NamedWorkspaceContainerPadding(ref workspace, size) => {
|
|
if let Some((monitor_idx, workspace_idx)) =
|
|
self.monitor_workspace_index_by_name(workspace)
|
|
{
|
|
self.set_container_padding(monitor_idx, workspace_idx, size)?;
|
|
}
|
|
}
|
|
SocketMessage::WorkspacePadding(monitor_idx, workspace_idx, size) => {
|
|
self.set_workspace_padding(monitor_idx, workspace_idx, size)?;
|
|
}
|
|
SocketMessage::NamedWorkspacePadding(ref workspace, size) => {
|
|
if let Some((monitor_idx, workspace_idx)) =
|
|
self.monitor_workspace_index_by_name(workspace)
|
|
{
|
|
self.set_workspace_padding(monitor_idx, workspace_idx, size)?;
|
|
}
|
|
}
|
|
SocketMessage::FocusedWorkspaceContainerPadding(adjustment) => {
|
|
let focused_monitor_idx = self.focused_monitor_idx();
|
|
|
|
let focused_monitor = self.focused_monitor().ok_or_eyre("there is no monitor")?;
|
|
|
|
let focused_workspace_idx = focused_monitor.focused_workspace_idx();
|
|
|
|
self.set_container_padding(focused_monitor_idx, focused_workspace_idx, adjustment)?;
|
|
}
|
|
SocketMessage::FocusedWorkspacePadding(adjustment) => {
|
|
let focused_monitor_idx = self.focused_monitor_idx();
|
|
|
|
let focused_monitor = self.focused_monitor().ok_or_eyre("there is no monitor")?;
|
|
|
|
let focused_workspace_idx = focused_monitor.focused_workspace_idx();
|
|
|
|
self.set_workspace_padding(focused_monitor_idx, focused_workspace_idx, adjustment)?;
|
|
}
|
|
SocketMessage::WorkAreaOffset(rect) => {
|
|
self.work_area_offset = Option::from(rect);
|
|
self.retile_all(false)?;
|
|
}
|
|
SocketMessage::MonitorWorkAreaOffset(monitor_idx, rect) => {
|
|
if let Some(monitor) = self.monitors_mut().get_mut(monitor_idx) {
|
|
monitor.work_area_offset = Option::from(rect);
|
|
self.retile_all(false)?;
|
|
}
|
|
}
|
|
SocketMessage::WorkspaceWorkAreaOffset(monitor_idx, workspace_idx, rect) => {
|
|
if let Some(monitor) = self.monitors_mut().get_mut(monitor_idx)
|
|
&& let Some(workspace) = monitor.workspaces_mut().get_mut(workspace_idx)
|
|
{
|
|
workspace.work_area_offset = Option::from(rect);
|
|
self.retile_all(false)?
|
|
}
|
|
}
|
|
SocketMessage::ResizeDelta(delta) => {
|
|
self.resize_delta = delta;
|
|
}
|
|
SocketMessage::AdjustContainerPadding(sizing, adjustment) => {
|
|
self.adjust_container_padding(sizing, adjustment)?;
|
|
}
|
|
SocketMessage::AdjustWorkspacePadding(sizing, adjustment) => {
|
|
self.adjust_workspace_padding(sizing, adjustment)?;
|
|
}
|
|
SocketMessage::ScrollingLayoutColumns(count) => {
|
|
self.focused_workspace_mut()?
|
|
.set_scrolling_columns(count.into());
|
|
self.update_focused_workspace(false, false)?;
|
|
}
|
|
SocketMessage::ScrollingLayoutFullWidth => {
|
|
self.focused_workspace_mut()?.toggle_scrolling_full_width();
|
|
self.update_focused_workspace(false, false)?;
|
|
}
|
|
SocketMessage::CenterFocus => {
|
|
self.focused_workspace_mut()?.center_scrolling_column();
|
|
self.update_focused_workspace(false, false)?;
|
|
}
|
|
SocketMessage::Expand => {
|
|
self.focused_workspace_mut()?.expand_scrolling_column();
|
|
self.update_focused_workspace(false, false)?;
|
|
}
|
|
SocketMessage::Consume(direction) => {
|
|
self.focused_workspace_mut()?.consume_scrolling(direction);
|
|
self.update_focused_workspace(false, false)?;
|
|
}
|
|
SocketMessage::Expel => {
|
|
self.focused_workspace_mut()?.expel_scrolling();
|
|
self.update_focused_workspace(false, false)?;
|
|
}
|
|
SocketMessage::ToggleColumnStack => {
|
|
self.focused_workspace_mut()?
|
|
.toggle_scrolling_column_stack()?;
|
|
self.update_focused_workspace(false, false)?;
|
|
}
|
|
SocketMessage::Pan(direction) => self.pan(direction)?,
|
|
SocketMessage::LayoutRatios(ref columns, ref rows) => {
|
|
self.set_layout_ratios(columns, rows)?
|
|
}
|
|
SocketMessage::NamedWorkspaceLayoutRule(ref workspace, at_container_count, layout) => {
|
|
if let Some((monitor_idx, workspace_idx)) =
|
|
self.monitor_workspace_index_by_name(workspace)
|
|
{
|
|
self.add_workspace_layout_default_rule(
|
|
monitor_idx,
|
|
workspace_idx,
|
|
at_container_count,
|
|
layout,
|
|
)?;
|
|
}
|
|
}
|
|
SocketMessage::ClearNamedWorkspaceLayoutRules(ref workspace) => {
|
|
if let Some((monitor_idx, workspace_idx)) =
|
|
self.monitor_workspace_index_by_name(workspace)
|
|
{
|
|
self.clear_workspace_layout_rules(monitor_idx, workspace_idx)?;
|
|
}
|
|
}
|
|
SocketMessage::MouseFollowsFocus(enable) => {
|
|
self.mouse_follows_focus = enable;
|
|
}
|
|
SocketMessage::ToggleMouseFollowsFocus => {
|
|
self.mouse_follows_focus = !self.mouse_follows_focus;
|
|
}
|
|
SocketMessage::ToggleMonocleFocusBehaviour => {
|
|
komorebi_core::toggle_monocle_focus_behaviour(self)
|
|
}
|
|
SocketMessage::MonocleFocusBehaviour(behaviour) => {
|
|
self.monocle_focus_behaviour = behaviour;
|
|
}
|
|
SocketMessage::UnmanagedWindowOperationBehaviour(behaviour) => {
|
|
self.unmanaged_window_operation_behaviour = behaviour;
|
|
}
|
|
SocketMessage::ApplicationSpecificConfigurationSchema => {
|
|
#[cfg(feature = "schemars")]
|
|
{
|
|
let schema = komorebi_config::schema::view::<
|
|
Vec<crate::core::asc::ApplicationSpecificConfiguration>,
|
|
>(komorebi_config::schema::Platform::CURRENT);
|
|
let schema = schema.trim_end();
|
|
|
|
reply.write_all(schema.as_bytes())?;
|
|
}
|
|
}
|
|
SocketMessage::NotificationSchema => {
|
|
#[cfg(feature = "schemars")]
|
|
{
|
|
let notification = schemars::schema_for!(Notification);
|
|
let schema = serde_json::to_string_pretty(¬ification)?;
|
|
|
|
reply.write_all(schema.as_bytes())?;
|
|
}
|
|
}
|
|
SocketMessage::SocketSchema => {
|
|
#[cfg(feature = "schemars")]
|
|
{
|
|
let socket_message = schemars::schema_for!(SocketMessage);
|
|
let schema = serde_json::to_string_pretty(&socket_message)?;
|
|
|
|
reply.write_all(schema.as_bytes())?;
|
|
}
|
|
}
|
|
SocketMessage::StaticConfigSchema => {
|
|
#[cfg(feature = "schemars")]
|
|
{
|
|
let schema = komorebi_config::schema::view::<
|
|
komorebi_config::static_config::StaticConfig,
|
|
>(komorebi_config::schema::Platform::CURRENT);
|
|
let schema = schema.trim_end();
|
|
|
|
reply.write_all(schema.as_bytes())?;
|
|
}
|
|
}
|
|
SocketMessage::AddSubscriberSocket(ref socket) => {
|
|
let mut sockets = SUBSCRIPTION_SOCKETS.lock();
|
|
let socket_path = DATA_DIR.join(socket);
|
|
sockets.insert(socket.clone(), socket_path);
|
|
}
|
|
SocketMessage::AddSubscriberSocketWithOptions(ref socket, options) => {
|
|
let mut sockets = SUBSCRIPTION_SOCKETS.lock();
|
|
let socket_path = DATA_DIR.join(socket);
|
|
sockets.insert(socket.clone(), socket_path);
|
|
|
|
let mut socket_options = SUBSCRIPTION_SOCKET_OPTIONS.lock();
|
|
socket_options.insert(socket.clone(), options);
|
|
}
|
|
SocketMessage::RemoveSubscriberSocket(ref socket) => {
|
|
let mut sockets = SUBSCRIPTION_SOCKETS.lock();
|
|
sockets.remove(socket);
|
|
}
|
|
SocketMessage::Stop => {
|
|
self.stop(false)?;
|
|
}
|
|
SocketMessage::StopIgnoreRestore => {
|
|
self.stop(true)?;
|
|
}
|
|
SocketMessage::GenerateStaticConfig => {
|
|
let config = serde_json::to_string_pretty(&StaticConfig::from(&*self))?;
|
|
|
|
reply.write_all(config.as_bytes())?;
|
|
}
|
|
SocketMessage::QuickSave => {
|
|
let workspace = self.focused_workspace()?;
|
|
let resize = &workspace.resize_dimensions;
|
|
|
|
let quicksave_json = std::env::temp_dir().join("komorebi.quicksave.json");
|
|
|
|
let file = OpenOptions::new()
|
|
.write(true)
|
|
.truncate(true)
|
|
.create(true)
|
|
.open(quicksave_json)?;
|
|
|
|
serde_json::to_writer_pretty(&file, &resize)?;
|
|
}
|
|
SocketMessage::QuickLoad => {
|
|
let workspace = self.focused_workspace_mut()?;
|
|
|
|
let quicksave_json = std::env::temp_dir().join("komorebi.quicksave.json");
|
|
|
|
let file = File::open(&quicksave_json).wrap_err(format!(
|
|
"no quicksave found at {}",
|
|
quicksave_json.display()
|
|
))?;
|
|
|
|
let resize: Vec<Option<Rect>> = serde_json::from_reader(file)?;
|
|
|
|
workspace.resize_dimensions = resize;
|
|
self.update_focused_workspace(false, false)?;
|
|
}
|
|
SocketMessage::Save(ref path) => {
|
|
let workspace = self.focused_workspace_mut()?;
|
|
let resize = &workspace.resize_dimensions;
|
|
|
|
let file = OpenOptions::new()
|
|
.write(true)
|
|
.truncate(true)
|
|
.create(true)
|
|
.open(path)?;
|
|
|
|
serde_json::to_writer_pretty(&file, &resize)?;
|
|
}
|
|
SocketMessage::Load(ref path) => {
|
|
let workspace = self.focused_workspace_mut()?;
|
|
|
|
let file =
|
|
File::open(path).wrap_err(format!("no file found at {}", path.display()))?;
|
|
|
|
let resize: Vec<Option<Rect>> = serde_json::from_reader(file)?;
|
|
|
|
workspace.resize_dimensions = resize;
|
|
self.update_focused_workspace(false, false)?;
|
|
}
|
|
SocketMessage::DebugWindow(window_id) => self.debug_window(window_id, &mut reply)?,
|
|
SocketMessage::Border(enable) => {
|
|
border_manager::BORDER_ENABLED.store(enable, Ordering::SeqCst);
|
|
if !enable {
|
|
border_manager::destroy_all_borders()?;
|
|
}
|
|
}
|
|
SocketMessage::Theme(ref theme) => {
|
|
theme_manager::send_notification(*theme.clone());
|
|
}
|
|
SocketMessage::Transparency(enable) => {
|
|
crate::settings::settings()
|
|
.update(|settings| settings.shared.transparency.enabled = enable);
|
|
}
|
|
SocketMessage::ToggleTransparency => {
|
|
crate::settings::settings().update(|settings| {
|
|
settings.shared.transparency.enabled = !settings.shared.transparency.enabled;
|
|
});
|
|
}
|
|
SocketMessage::TransparencyAlpha(alpha) => {
|
|
crate::settings::settings()
|
|
.update(|settings| settings.shared.transparency.alpha = alpha);
|
|
}
|
|
SocketMessage::BorderColour(kind, r, g, b) => self.set_border_colour(kind, r, g, b),
|
|
SocketMessage::BorderWidth(width) => {
|
|
border_manager::BORDER_WIDTH.store(width, Ordering::SeqCst);
|
|
border_manager::destroy_all_borders()?;
|
|
}
|
|
SocketMessage::BorderOffset(offset) => {
|
|
border_manager::BORDER_OFFSET.store(offset, Ordering::SeqCst);
|
|
border_manager::destroy_all_borders()?;
|
|
}
|
|
SocketMessage::Animation(enable, prefix) => {
|
|
crate::settings::settings().update(|settings| {
|
|
let animation = &mut settings.shared.animation;
|
|
match prefix {
|
|
Some(prefix) => {
|
|
animation.enabled_per_animation.insert(prefix, enable);
|
|
}
|
|
None => {
|
|
animation.enabled = enable;
|
|
animation.enabled_per_animation.clear();
|
|
}
|
|
}
|
|
});
|
|
}
|
|
SocketMessage::AnimationDuration(duration, prefix) => {
|
|
crate::settings::settings().update(|settings| {
|
|
let animation = &mut settings.shared.animation;
|
|
match prefix {
|
|
Some(prefix) => {
|
|
animation.duration_per_animation.insert(prefix, duration);
|
|
}
|
|
None => {
|
|
animation.duration = duration;
|
|
animation.duration_per_animation.clear();
|
|
}
|
|
}
|
|
});
|
|
}
|
|
SocketMessage::AnimationFps(fps) => {
|
|
crate::settings::settings().update(|settings| settings.shared.animation.fps = fps);
|
|
}
|
|
SocketMessage::AnimationStyle(style, prefix) => {
|
|
crate::settings::settings().update(|settings| {
|
|
let animation = &mut settings.shared.animation;
|
|
match prefix {
|
|
Some(prefix) => {
|
|
animation.style_per_animation.insert(prefix, style);
|
|
}
|
|
None => {
|
|
animation.style = style;
|
|
animation.style_per_animation.clear();
|
|
}
|
|
}
|
|
});
|
|
}
|
|
SocketMessage::ReloadHotkeys => crate::kwin::hotkeys::reload()?,
|
|
SocketMessage::HotkeysState => {
|
|
let state = serde_json::to_string_pretty(&crate::kwin::hotkeys::state())?;
|
|
reply.write_all(state.as_bytes())?;
|
|
}
|
|
// Messages of other platforms: answered, and nothing else happens
|
|
SocketMessage::AddSubscriberPipe(..)
|
|
| SocketMessage::AltFocusHack(..)
|
|
| SocketMessage::ApplyState(..)
|
|
| SocketMessage::BorderImplementation(..)
|
|
| SocketMessage::BorderStyle(..)
|
|
| SocketMessage::ChangeLayoutCustom(..)
|
|
| SocketMessage::CompleteConfiguration
|
|
| SocketMessage::ForceFocus
|
|
| SocketMessage::IdentifyBorderOverflowApplication(..)
|
|
| SocketMessage::IdentifyLayeredApplication(..)
|
|
| SocketMessage::IdentifyObjectNameChangeApplication(..)
|
|
| SocketMessage::IdentifyTrayApplication(..)
|
|
| SocketMessage::InvisibleBorders(..)
|
|
| SocketMessage::MonitorIndexPreference(..)
|
|
| SocketMessage::NamedWorkspaceLayoutCustom(..)
|
|
| SocketMessage::NamedWorkspaceLayoutCustomRule(..)
|
|
| SocketMessage::PanGesture(..)
|
|
| SocketMessage::ReloadConfiguration
|
|
| SocketMessage::RemoveSubscriberPipe(..)
|
|
| SocketMessage::RemoveTitleBar(..)
|
|
| SocketMessage::StackbarBackgroundColour(..)
|
|
| SocketMessage::StackbarFocusedTextColour(..)
|
|
| SocketMessage::StackbarFontFamily(..)
|
|
| SocketMessage::StackbarFontSize(..)
|
|
| SocketMessage::StackbarHeight(..)
|
|
| SocketMessage::StackbarLabel(..)
|
|
| SocketMessage::StackbarMode(..)
|
|
| SocketMessage::StackbarTabWidth(..)
|
|
| SocketMessage::StackbarUnfocusedTextColour(..)
|
|
| SocketMessage::ToggleTitleBars
|
|
| SocketMessage::WatchConfiguration(..)
|
|
| SocketMessage::WindowHidingBehaviour(..)
|
|
| SocketMessage::WorkspaceLayoutCustom(..)
|
|
| SocketMessage::WorkspaceLayoutCustomRule(..) => {
|
|
reply.write_all(message.not_supported_reply().as_bytes())?;
|
|
return Ok(());
|
|
}
|
|
}
|
|
|
|
self.update_known_window_ids();
|
|
|
|
notify_subscribers(
|
|
Notification {
|
|
event: NotificationEvent::Socket(message.clone()),
|
|
state: self.as_ref().into(),
|
|
},
|
|
initial_state.has_been_modified(self.as_ref()),
|
|
)?;
|
|
|
|
border_manager::send_notification();
|
|
border_manager::update_colours(self);
|
|
transparency_manager::apply(self);
|
|
|
|
if matches!(message, SocketMessage::Theme(_)) {
|
|
tracing::trace!("processed command: {message}");
|
|
} else {
|
|
tracing::info!("processed command: {message}");
|
|
}
|
|
|
|
Ok(())
|
|
}
|
|
}
|
|
|
|
/// Reads the messages on one connection, one per line, and hands each to
|
|
/// [`WindowManager::process_command`] with the connection to answer on
|
|
///
|
|
/// A message that arrives while the window manager has been locked for a second is logged and
|
|
/// dropped. While the window manager is paused, the first message that is not `TogglePause`,
|
|
/// `State`, `GlobalState` or `Stop` is dropped, and the first that is one of them is handled;
|
|
/// either way, the lines after it are not read.
|
|
///
|
|
/// # Errors
|
|
///
|
|
/// - The connection cannot be read, or cloned to be read and written at once.
|
|
/// - A line is not a [`SocketMessage`]: the lines after it are not read.
|
|
/// - [`WindowManager::process_command`] fails: the lines after it are not read.
|
|
pub fn read_commands_uds(wm: &Arc<Mutex<WindowManager>>, stream: UnixStream) -> eyre::Result<()> {
|
|
komorebi_transport::read_commands::<WindowManager, _>(wm, stream, &NoPolicy)
|
|
}
|
|
|
|
impl WindowManager {
|
|
/// Minimizes the window that has the focus
|
|
pub fn minimize_foreground_window(&mut self) -> eyre::Result<()> {
|
|
let foreground_window = backend()
|
|
.foreground_window()
|
|
.ok_or_eyre("there is no foreground window")?;
|
|
Window::from(foreground_window).minimize()?;
|
|
Ok(())
|
|
}
|
|
|
|
/// Closes the window that has the focus
|
|
pub fn close_foreground_window(&mut self) -> eyre::Result<()> {
|
|
let foreground_window = backend()
|
|
.foreground_window()
|
|
.ok_or_eyre("there is no foreground window")?;
|
|
Window::from(foreground_window).close()?;
|
|
Ok(())
|
|
}
|
|
|
|
/// Answers with why the window of this id is, or is not, managed
|
|
pub fn debug_window(
|
|
&self,
|
|
window_id: i64,
|
|
reply: &mut impl std::io::Write,
|
|
) -> eyre::Result<()> {
|
|
// an id that is not a u32 is not a window
|
|
let window = u32::try_from(window_id).map(Window::from);
|
|
if let Ok(window) = window
|
|
&& window.is_valid()
|
|
{
|
|
let mut rule_debug = RuleDebug::default();
|
|
let _ = window.should_manage(None, &mut rule_debug);
|
|
let schema = serde_json::to_string_pretty(&rule_debug)?;
|
|
|
|
reply.write_all(schema.as_bytes())?;
|
|
}
|
|
Ok(())
|
|
}
|
|
}
|
|
|
|
/// The window manager as the shared commands of `komorebi_core::command` see it
|
|
impl komorebi_core::CommandHost for WindowManager {
|
|
type P = crate::workspace::KdePlatform;
|
|
|
|
fn gate_message(
|
|
&mut self,
|
|
_message: &komorebi_protocol::message::SocketMessage,
|
|
) -> eyre::Result<()> {
|
|
self.handle_unmanaged_window_behaviour()
|
|
}
|
|
|
|
fn monitors(&self) -> &std::collections::VecDeque<crate::monitor::Monitor> {
|
|
WindowManager::monitors(self)
|
|
}
|
|
|
|
fn focused_monitor(&self) -> Option<&crate::monitor::Monitor> {
|
|
WindowManager::focused_monitor(self)
|
|
}
|
|
|
|
fn focused_monitor_mut(&mut self) -> Option<&mut crate::monitor::Monitor> {
|
|
WindowManager::focused_monitor_mut(self)
|
|
}
|
|
|
|
fn focused_monitor_idx(&self) -> usize {
|
|
WindowManager::focused_monitor_idx(self)
|
|
}
|
|
|
|
fn monitor_idx_from_current_pos(&mut self) -> Option<usize> {
|
|
WindowManager::monitor_idx_from_current_pos(self)
|
|
}
|
|
|
|
fn focus_monitor(&mut self, idx: usize) -> eyre::Result<()> {
|
|
WindowManager::focus_monitor(self, idx)
|
|
}
|
|
|
|
fn focus_workspace(&mut self, idx: usize) -> eyre::Result<()> {
|
|
WindowManager::focus_workspace(self, idx)
|
|
}
|
|
|
|
fn show_workspace_on(&mut self, monitor_idx: usize, workspace_idx: usize) -> eyre::Result<()> {
|
|
let monitor = self
|
|
.monitors_mut()
|
|
.get_mut(monitor_idx)
|
|
.ok_or_eyre("there is no monitor at this index")?;
|
|
monitor.focus_workspace(workspace_idx)?;
|
|
monitor.load_focused_workspace(false)?;
|
|
Ok(())
|
|
}
|
|
|
|
fn move_container_to_workspace(
|
|
&mut self,
|
|
idx: usize,
|
|
follow: bool,
|
|
direction: Option<OperationDirection>,
|
|
) -> eyre::Result<()> {
|
|
WindowManager::move_container_to_workspace(self, idx, follow, direction)
|
|
}
|
|
}
|
|
|
|
impl komorebi_core::MonitorCommandHost for WindowManager {
|
|
fn mouse_follows_focus(&self) -> bool {
|
|
self.mouse_follows_focus
|
|
}
|
|
|
|
fn update_focused_workspace(
|
|
&mut self,
|
|
follow_focus: bool,
|
|
trigger_focus: bool,
|
|
) -> eyre::Result<()> {
|
|
WindowManager::update_focused_workspace(self, follow_focus, trigger_focus)
|
|
}
|
|
|
|
fn direction_from_monitor_idx(&self, idx: usize) -> Option<OperationDirection> {
|
|
WindowManager::direction_from_monitor_idx(self, idx)
|
|
}
|
|
|
|
fn move_container_to_monitor(
|
|
&mut self,
|
|
idx: usize,
|
|
workspace_idx: Option<usize>,
|
|
follow: bool,
|
|
direction: Option<OperationDirection>,
|
|
) -> eyre::Result<()> {
|
|
WindowManager::move_container_to_monitor(self, idx, workspace_idx, follow, direction)
|
|
}
|
|
|
|
fn focused_workspace_idx(&self) -> eyre::Result<usize> {
|
|
WindowManager::focused_workspace_idx(self)
|
|
}
|
|
}
|
|
|
|
impl komorebi_core::ContainerCommandHost for WindowManager {
|
|
fn monitors_mut(&mut self) -> &mut std::collections::VecDeque<crate::monitor::Monitor> {
|
|
WindowManager::monitors_mut(self)
|
|
}
|
|
}
|
|
|
|
/// The window manager as the window commands of `komorebi_core::command` see it
|
|
impl komorebi_core::WindowCommandHost for WindowManager {
|
|
fn move_container_in_direction(&mut self, direction: OperationDirection) -> eyre::Result<()> {
|
|
WindowManager::move_container_in_direction(self, direction)
|
|
}
|
|
|
|
fn move_floating_window_in_direction(
|
|
&mut self,
|
|
direction: OperationDirection,
|
|
) -> eyre::Result<()> {
|
|
WindowManager::move_floating_window_in_direction(self, direction)
|
|
}
|
|
|
|
fn focus_container_in_direction(&mut self, direction: OperationDirection) -> eyre::Result<()> {
|
|
WindowManager::focus_container_in_direction(self, direction)
|
|
}
|
|
|
|
fn focus_floating_window_in_direction(
|
|
&mut self,
|
|
direction: OperationDirection,
|
|
) -> eyre::Result<()> {
|
|
WindowManager::focus_floating_window_in_direction(self, direction)
|
|
}
|
|
|
|
fn focus_container_in_cycle_direction(
|
|
&mut self,
|
|
direction: CycleDirection,
|
|
) -> eyre::Result<()> {
|
|
WindowManager::focus_container_in_cycle_direction(self, direction)
|
|
}
|
|
|
|
fn focus_floating_window_in_cycle_direction(
|
|
&mut self,
|
|
direction: CycleDirection,
|
|
) -> eyre::Result<()> {
|
|
WindowManager::focus_floating_window_in_cycle_direction(self, direction)
|
|
}
|
|
|
|
fn preselect_container_in_direction(
|
|
&mut self,
|
|
direction: OperationDirection,
|
|
) -> eyre::Result<()> {
|
|
WindowManager::preselect_container_in_direction(self, direction)
|
|
}
|
|
|
|
fn promote_container_to_front(&mut self) -> eyre::Result<()> {
|
|
WindowManager::promote_container_to_front(self)
|
|
}
|
|
|
|
fn update_focused_workspace_layout(&mut self) -> eyre::Result<()> {
|
|
self.focused_workspace_mut()?.update()
|
|
}
|
|
}
|
|
|
|
/// The window manager as the behaviour toggles of `komorebi_core::command` see it
|
|
impl komorebi_core::BehaviourCommandHost for WindowManager {
|
|
fn window_container_behaviour(&self) -> komorebi_config::behaviour::WindowContainerBehaviour {
|
|
self.window_management_behaviour.current_behaviour
|
|
}
|
|
|
|
fn set_window_container_behaviour(
|
|
&mut self,
|
|
behaviour: komorebi_config::behaviour::WindowContainerBehaviour,
|
|
) {
|
|
self.window_management_behaviour.current_behaviour = behaviour;
|
|
}
|
|
|
|
fn cross_monitor_move_behaviour(&self) -> komorebi_protocol::types::MoveBehaviour {
|
|
self.cross_monitor_move_behaviour
|
|
}
|
|
|
|
fn set_cross_monitor_move_behaviour(
|
|
&mut self,
|
|
behaviour: komorebi_protocol::types::MoveBehaviour,
|
|
) {
|
|
self.cross_monitor_move_behaviour = behaviour;
|
|
}
|
|
|
|
fn monocle_focus_behaviour(&self) -> komorebi_protocol::types::MonocleFocusBehaviour {
|
|
self.monocle_focus_behaviour
|
|
}
|
|
|
|
fn set_monocle_focus_behaviour(
|
|
&mut self,
|
|
behaviour: komorebi_protocol::types::MonocleFocusBehaviour,
|
|
) {
|
|
self.monocle_focus_behaviour = behaviour;
|
|
}
|
|
}
|
|
|
|
#[cfg(test)]
|
|
mod tests {
|
|
use super::*;
|
|
use crate::backend::fake::FakeBackend;
|
|
use crate::backend::with_backend;
|
|
use crate::core::Rect;
|
|
use crate::monitor;
|
|
use crate::window_manager_event::WindowManagerEvent;
|
|
use crossbeam_channel::Receiver;
|
|
use crossbeam_channel::Sender;
|
|
use crossbeam_channel::bounded;
|
|
use std::io::BufRead;
|
|
use std::io::BufReader;
|
|
use std::io::Read;
|
|
use std::io::Write;
|
|
use std::net::Shutdown;
|
|
use std::path::PathBuf;
|
|
use std::str::FromStr;
|
|
use uuid::Uuid;
|
|
|
|
/// A window manager over `fake`, listening on a socket for this test alone in /tmp, which is
|
|
/// short enough for a unix socket path on macOS. The socket is removed when the test is done.
|
|
struct Daemon {
|
|
fake: Arc<FakeBackend>,
|
|
wm: Arc<Mutex<WindowManager>>,
|
|
socket: PathBuf,
|
|
}
|
|
|
|
impl Drop for Daemon {
|
|
fn drop(&mut self) {
|
|
let _ = std::fs::remove_file(&self.socket);
|
|
}
|
|
}
|
|
|
|
impl Daemon {
|
|
fn new(fake: FakeBackend) -> Self {
|
|
let fake = Arc::new(fake);
|
|
let socket = PathBuf::from(format!("/tmp/komorebi-test-{}.sock", Uuid::new_v4()));
|
|
|
|
let (_sender, events) = crossbeam_channel::bounded(1);
|
|
|
|
let wm = with_backend(fake.clone(), || {
|
|
let mut wm = WindowManager::new(events, Some(socket.clone())).unwrap();
|
|
wm.init().unwrap();
|
|
wm
|
|
});
|
|
|
|
Self {
|
|
fake,
|
|
wm: Arc::new(Mutex::new(wm)),
|
|
socket,
|
|
}
|
|
}
|
|
|
|
/// Writes `lines` on a new connection, has the window manager read the connection the
|
|
/// way the daemon does, and returns what it wrote back
|
|
fn send(&self, lines: &str) -> eyre::Result<String> {
|
|
let mut client = UnixStream::connect(&self.socket)?;
|
|
client.write_all(lines.as_bytes())?;
|
|
client.shutdown(Shutdown::Write)?;
|
|
|
|
// the client reads on another thread, so that an answer bigger than the
|
|
// socket's buffer does not block the window manager writing it
|
|
let reading = std::thread::spawn(move || {
|
|
let mut reply = String::new();
|
|
client.read_to_string(&mut reply).map(|_| reply)
|
|
});
|
|
|
|
let (stream, _) = self.wm.lock().command_listener.accept()?;
|
|
let handled = with_backend(self.fake.clone(), || read_commands_uds(&self.wm, stream));
|
|
|
|
let reply = reading.join().expect("the client does not panic")?;
|
|
handled.map(|()| reply)
|
|
}
|
|
|
|
fn ask(&self, message: SocketMessage) -> eyre::Result<String> {
|
|
self.send(&serde_json::to_string(&message)?)
|
|
}
|
|
}
|
|
|
|
fn rect(left: i32, top: i32, right: i32, bottom: i32) -> Rect {
|
|
Rect {
|
|
left,
|
|
top,
|
|
right,
|
|
bottom,
|
|
}
|
|
}
|
|
|
|
fn two_windows(first: u32) -> FakeBackend {
|
|
let fake = FakeBackend::with_one_monitor();
|
|
fake.open_window(first, "foot", "foot", "first");
|
|
fake.open_window(first + 1, "foot", "foot", "second");
|
|
fake
|
|
}
|
|
|
|
#[test]
|
|
fn the_state_has_every_window_of_every_workspace() -> eyre::Result<()> {
|
|
let daemon = Daemon::new(two_windows(701));
|
|
|
|
let state: serde_json::Value = serde_json::from_str(&daemon.ask(SocketMessage::State)?)?;
|
|
|
|
let windows = &state["monitors"]["elements"][0]["workspaces"]["elements"][0]["containers"]
|
|
["elements"];
|
|
assert_eq!(windows[0]["windows"]["elements"][0]["id"], 701);
|
|
assert_eq!(windows[1]["windows"]["elements"][0]["id"], 702);
|
|
assert_eq!(
|
|
windows[1]["windows"]["elements"][0]["details"]["title"],
|
|
"second"
|
|
);
|
|
assert_eq!(state["mouse_follows_focus"], true);
|
|
Ok(())
|
|
}
|
|
|
|
#[test]
|
|
fn monitor_information_names_each_monitor_and_its_bounds() -> eyre::Result<()> {
|
|
let fake = FakeBackend::with_one_monitor();
|
|
fake.add_monitor(
|
|
2,
|
|
"Virtual-1",
|
|
rect(1920, 0, 1920, 1080),
|
|
rect(1920, 0, 1920, 1080),
|
|
);
|
|
let daemon = Daemon::new(fake);
|
|
|
|
let monitors: serde_json::Value =
|
|
serde_json::from_str(&daemon.ask(SocketMessage::MonitorInformation)?)?;
|
|
|
|
assert_eq!(monitors.as_array().map(Vec::len), Some(2));
|
|
assert_eq!(monitors[1]["id"], 2);
|
|
assert_eq!(monitors[1]["device"], "Virtual-1");
|
|
assert_eq!(monitors[1]["size"]["left"], 1920);
|
|
assert_eq!(monitors[0]["size"]["right"], 1920);
|
|
Ok(())
|
|
}
|
|
|
|
#[test]
|
|
fn without_key_bindings_there_is_no_engine_to_show_or_reload() -> eyre::Result<()> {
|
|
let daemon = Daemon::new(two_windows(751));
|
|
|
|
assert_eq!(daemon.ask(SocketMessage::HotkeysState)?, "null");
|
|
let error = daemon.ask(SocketMessage::ReloadHotkeys).unwrap_err();
|
|
assert!(
|
|
error.to_string().contains("started without key bindings"),
|
|
"{error}"
|
|
);
|
|
Ok(())
|
|
}
|
|
|
|
#[test]
|
|
fn a_query_is_answered_with_one_value() -> eyre::Result<()> {
|
|
let daemon = Daemon::new(two_windows(711));
|
|
let query = |query| daemon.ask(SocketMessage::Query(query));
|
|
|
|
assert_eq!(query(StateQuery::FocusedMonitorIndex)?, "0");
|
|
assert_eq!(query(StateQuery::FocusedWorkspaceIndex)?, "0");
|
|
assert_eq!(query(StateQuery::FocusedContainerIndex)?, "0");
|
|
assert_eq!(query(StateQuery::FocusedWindowIndex)?, "0");
|
|
// a workspace with no name is named by its index
|
|
assert_eq!(query(StateQuery::FocusedWorkspaceName)?, "0");
|
|
assert_eq!(query(StateQuery::FocusedWorkspaceLayout)?, "BSP");
|
|
assert_eq!(query(StateQuery::FocusedContainerKind)?, "Single");
|
|
// the version is shadow-rs's description of the build, the package version first
|
|
let version = format!("pkg_version:{}", env!("CARGO_PKG_VERSION"));
|
|
assert!(query(StateQuery::Version)?.contains(&version));
|
|
Ok(())
|
|
}
|
|
|
|
#[test]
|
|
fn a_query_about_a_container_on_an_empty_workspace_is_an_error() {
|
|
let daemon = Daemon::new(FakeBackend::with_one_monitor());
|
|
|
|
let answer = daemon.ask(SocketMessage::Query(StateQuery::FocusedWindowIndex));
|
|
|
|
assert_eq!(
|
|
answer.map_err(|error| error.to_string()),
|
|
Err(String::from("there is no container"))
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
fn retile_puts_a_window_moved_by_hand_back_on_its_tile() -> eyre::Result<()> {
|
|
let daemon = Daemon::new(two_windows(721));
|
|
daemon.ask(SocketMessage::Retile)?;
|
|
let tile = daemon.fake.rect_of(721);
|
|
|
|
daemon
|
|
.fake
|
|
.move_window_by_user(721, rect(300, 300, 400, 400));
|
|
assert_ne!(daemon.fake.rect_of(721), tile);
|
|
|
|
assert_eq!(daemon.ask(SocketMessage::Retile)?, "");
|
|
assert_eq!(daemon.fake.rect_of(721), tile);
|
|
Ok(())
|
|
}
|
|
|
|
#[test]
|
|
fn the_messages_on_one_connection_are_handled_in_order() -> eyre::Result<()> {
|
|
let daemon = Daemon::new(two_windows(731));
|
|
daemon
|
|
.fake
|
|
.move_window_by_user(731, rect(300, 300, 400, 400));
|
|
|
|
let reply = daemon.send(
|
|
"{\"type\":\"Retile\"}\n{\"type\":\"Query\",\"content\":\"FocusedContainerKind\"}\n",
|
|
)?;
|
|
|
|
assert_eq!(reply, "Single");
|
|
assert_ne!(daemon.fake.rect_of(731), Some(rect(300, 300, 400, 400)));
|
|
Ok(())
|
|
}
|
|
|
|
#[test]
|
|
fn while_paused_only_toggle_pause_state_and_stop_are_handled() -> eyre::Result<()> {
|
|
let daemon = Daemon::new(two_windows(741));
|
|
daemon.ask(SocketMessage::Retile)?;
|
|
let tile = daemon.fake.rect_of(741);
|
|
daemon.ask(SocketMessage::TogglePause)?;
|
|
daemon
|
|
.fake
|
|
.move_window_by_user(741, rect(300, 300, 400, 400));
|
|
|
|
assert_eq!(daemon.ask(SocketMessage::Retile)?, "");
|
|
assert_eq!(daemon.fake.rect_of(741), Some(rect(300, 300, 400, 400)));
|
|
// the message that is dropped is the last one read on its connection
|
|
assert_eq!(
|
|
daemon.send("{\"type\":\"Retile\"}\n{\"type\":\"State\"}\n")?,
|
|
""
|
|
);
|
|
let state: serde_json::Value = serde_json::from_str(&daemon.ask(SocketMessage::State)?)?;
|
|
assert_eq!(state["is_paused"], true);
|
|
|
|
daemon.ask(SocketMessage::TogglePause)?;
|
|
daemon.ask(SocketMessage::Retile)?;
|
|
assert_eq!(daemon.fake.rect_of(741), tile);
|
|
Ok(())
|
|
}
|
|
|
|
#[test]
|
|
fn a_line_that_is_not_a_message_is_an_error() {
|
|
let daemon = Daemon::new(FakeBackend::with_one_monitor());
|
|
|
|
let reply = daemon.send("{\"type\":\"Nonsense\"}\n");
|
|
|
|
assert!(reply.is_err());
|
|
}
|
|
|
|
fn send_socket_message(socket: &PathBuf, message: SocketMessage) {
|
|
let mut stream = UnixStream::connect(socket).unwrap();
|
|
stream
|
|
.set_write_timeout(Some(Duration::from_secs(1)))
|
|
.unwrap();
|
|
stream
|
|
.write_all(serde_json::to_string(&message).unwrap().as_bytes())
|
|
.unwrap();
|
|
}
|
|
|
|
#[test]
|
|
fn test_receive_socket_message() {
|
|
let (_sender, receiver): (Sender<WindowManagerEvent>, Receiver<WindowManagerEvent>) =
|
|
bounded(1);
|
|
// /tmp: short enough for a unix socket path on macOS, and writable wherever the tests run
|
|
let socket_name = format!("/tmp/komorebi-test-{}.sock", Uuid::new_v4());
|
|
let socket_path = PathBuf::from(&socket_name);
|
|
let mut wm = WindowManager::new(receiver, Some(socket_path.clone())).unwrap();
|
|
let m = monitor::new(
|
|
0,
|
|
Rect::default(),
|
|
Rect::default(),
|
|
"TestMonitor".to_string(),
|
|
"TestDeviceID".to_string(),
|
|
);
|
|
|
|
wm.monitors_mut().push_back(m);
|
|
|
|
// send a message
|
|
send_socket_message(&socket_path, SocketMessage::FocusWorkspaceNumber(5));
|
|
|
|
let (stream, _) = wm.command_listener.accept().unwrap();
|
|
let reader = BufReader::new(stream.try_clone().unwrap());
|
|
let next = reader.lines().next();
|
|
|
|
// read and deserialize the message
|
|
let message_string = next.unwrap().unwrap();
|
|
let message = SocketMessage::from_str(&message_string).unwrap();
|
|
assert!(matches!(message, SocketMessage::FocusWorkspaceNumber(5)));
|
|
|
|
// process the message
|
|
wm.process_command(message, stream).unwrap();
|
|
|
|
// check the updated window manager state
|
|
assert_eq!(wm.focused_workspace_idx().unwrap(), 5);
|
|
|
|
std::fs::remove_file(socket_path).unwrap();
|
|
}
|
|
}
|