swim - draft specification for a simple, extensible window manager
swim proposal v0.0.1
This document is a draft proposal for a new window manager, swim.
swim (the Simple WIndow Manager) is a simple, extensible window manager built for Wayland. It aims to implement only a minimal, unopinionated feature set, with extensible interfaces for custom window layouts and other behaviour. The intention is that swim should provide only those features required by all window managers, upon which arbitrary layout algorithms and custom behaviour can be implemented.
At its core, swim assigns workspaces (a representation of a display) to physical or virtual displays, per seat. The visible windows on each workspace are determined by a system of tags. The position of each window is determined entirely by external layouts that are bound to the workspace. Layouts also expose additional commands to control window position.
A system of event propagation exists to allow input events to be intercepted and trigger associated commmands. Events originate either in the seat, or according to custom program logic, and propate in sequence through the active display, workspace, layouts, and active window.
Event-command bindings exist at the seat and workspace levels to control different aspects of the window manager. Bindings within the seat reference commands within the active display and workspace, whereas bindings within the layout reference only commands exposed by the layout. For example, commands to control the currently active display are bound within the seat, whereas commands to cycle window position may be bound within the workspace.
The user can affect the behaviour of swim according to two kinds of extension:
- layouts: code that exposes a public interface to manage on-screen window positions
- plugins: code that extends the functionality of swim with additional features
These may be implemented either as libraries that link against swim, or instead as wayland clients that communicate with swim via a protocol extension. Implementation details such as these are still to be determined.
Layouts manage on-screen window positions. They must expose the following methods:
- add_window: notifies the layout about the existence of a new window
- remove_window: notifies the layout to forget about a window
- configure_layout(x1, y1, x2, y2): notifies the layout that it must only draw within the given coordinates
- visible_windows: returns a list of (identifier, size, coordinates) corresponding to where each window should be drawn. Where windows overlap, the first in the list takes priority. This method may override the title bars of windows.
The implementation must be according to the following behaviour:
A layout must act as if it contains the windows it is given. It must only use the part of the screen it is told to use. It will be told when it needs to start managing a window, and when it should stop. It must be able to report where all of its visible windows should be rendered. This means that it is possible for a layout to create more layouts and delegate window management to them. Where nested layouts are required, the visible windows of the parent layout should be the visible windows of its visible sublayouts. This approach enables us to re-use logic: e.g. the stack layout could be implemented as nested hsplit layouts.
Alongside the methods that must be implemented, a layout can optionally define additional commands to manipulate the state of the layout. These commands can be bound to events within swim workspaces, to run them in response to events such as key combinations. Examples of layout commands might include re-ordering the internal structure of windows, temporarily hiding windows, etc.
Nested layouts should include sufficient commands to control their sub-layouts.
A workspace is a representation of a display which can be bound to either a physical or a virtual display in the seat, and itself binds events to layout commands.
Workspaces have a given resolution, and call the visible_windows method on its layout to determine window position.
Each workspace maintains a set of tags, where each tag is a string. Each window also maintains a set of tags of size >=1.
The layout bound to the workspace is instructed to draw the set of windows whose tags match those of the layout instance.
Events are signals that result in commands being called. They are propagated through the following chain:
seat -> active_physical_display -> workspace -> layouts -> active_windows
An event may begin as a keyboard/mouse input of a given seat, or instead according to program logic within the workspace.
Within the seat, events can be mapped to commands for switching focus to different displays, etc.
Within the workspace, events can be mapped to the commands implemented by the underlying layout.
At the levels of seat, and workspace, a mapping can be configured between events and commands. The seat implements commands for switching focus to different displays. The layout implements commands, as aforementioned, for changing window order etc.
Plugins allow swim to be augmented with custom behaviour, according to swim's public interface. Plugins are expected to provide non-blocking initialisation and destruction commands
Plugins may implement systems such as emitting events (such as handling keyboard and mouse input), intercepting screen drawing (such as screencasting to a certain URL, or applying a colour filter to a workspace), etc. They can also implement methods for manipulating layou
swim provides a set of hooks that can be used by plugins to respond to changes in state (e.g. the addition/removal of physical displays, the lock signal). swim implements methods to bind/unbind callbacks to the hooks.
Plugins must provide commands for initialisation/destruction, which may be called by default or in response to events. They can define additional commands for custom logic and controlling the state of the plugin at runtime. Plugins may require plugin-specific config, which may be passed to the plugin object on initialisation.
All plugin methods are expected to be non-blocking: they register the user's intention, and hand control back to the main thread before performing the required actions. Initialisation and destruction must be idempotent.
Hooks are handles which plugins can use to be notified of state changes within swim. They come in in three forms: *_added, *_removed, and *_changed. Examples may include workspace_changed, display_added, etc.
Callbacks are registered to hooks using hook/unhook methods. The hook method expects a callback and returns an ID that can be later passed to unhook to detatch the callback.
The implementation of hooks is somewhat subltle, because it needs to deal with cases such as:
- A callback unregistering itself while it still has calls pending.
- Two callbacks are bound to the workspace_changed event, where the first one destroys the workspace.
Still to consider
Desktop backgrounds: a layout, or part of the workspace? Title bars Starting sessions without seats Attaching to remote sessions Adding/removing seats to/from an existing session. This allows locking to be implemented by detatching a workspace from a display and attaching the lockscreen workspace instead. Meanwhile, remote sessions and plugins such as screen recorders keep working.
All core swim methods should be non-blocking. Blocking operations should be performed by notifying the main thread to call a callback soon. The callback handles the blocking operation.