Files
komorebi/komorebi-linux/src/process_command.rs
T
LGUG2Z cd3892de76 refactor(repo): move the platform libraries to their final directories (pure move)
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.
2026-11-05 15:03:27 -08:00

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(&notification)?;
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();
}
}