Mondrian is a tiling window manager built with Rust for Windows 11.
- Automatic/manual window placement with different tiling layouts;
- Keybindings;
- Multi-monitor support;
- Mouse movements support (moving/resizing windows);
- Compatible with Virtual Desktops;
- Workspaces;
- System tray application;
- Multiple animations;
- Highly customizable.
To start Mondrian, just download the mondrian.exe executable from the latest release and run it.
The application takes the following arguments (all of them are optional):
./mondrian.exe --log <LOG_TYPE> --loglevel <LOGLEVEL> --dumpstateinfo --healthcheck
Where:
<LOG_TYPE>can be 0 (no log file is created), 1 (error log files is created) or 2 (all log files are created). By default, it is set to 1.<LOG_LEVEL>can be 0 (off), 1 (trace), 2 (debug), 3 (info), 4 (warn) or 5 (error). By default, it is set to 3.dumpstateinfodump the application state into a file (./logs/app_state.txt) at the start of the application;healthcheckenables health checks to detect freezes.
All the log files will be stored in the application directory under the logs subfolder. When a log file reaches 10MB, it will be archived in a .gz file (up to three previous versions).
You can swap two windows in the same monitor just by dragging one of them into the other. While dragging, you can:
- hold
ALT, to swap the windows and to invert the direction of the tiles;
When the window is dragged to another monitor, by default it will be inserted. In this case, you can:
- hold
SHIFTwhile dragging the window to swap the windows; - hold
ALTwhile dragging the window to insert the windows and to invert the direction of the tiles.
By changing the insert_in_monitor configuration option to false, the window will be swapped in the other monitor by default. In this case, you can:
- hold
SHIFTwhile dragging the window to insert the windows; - hold
ALTwhile dragging the window to insert the window and to invert the direction of the tiles.
If you drag a window while holding CTRL, you can place the window freely based on the cursor position relative to an other window.
In particular:
- if the cursor is at the top of an other window (i.e. <=20% of its height), the moving window will be placed above it;
- if the cursor is at the bottom of an other window (i.e. >=80% of its height), the moving window will be placed below it;
- if the cursor is to the left of an other window (i.e. <=50% of its width), the moving window will be placed to the left of it;
- if the cursor is to the right of an other window (i.e. >50% of its width), the moving window will be placed to the right of it.
Holding CTRL has the same effect when dragging the window to another monitor (by default).
You can set the free_move_in_monitor configuration option to true if you want to place the window freely in another monitor without holding CTRL (in this case, holding CTRL will position the window automatically).
Below a table that shows the keybindings for moving/swapping windows in different monitors, depending on the values of the insert_in_monitor and free_move_in_monitor configuration options:
insert_in_monitor |
free_move_in_monitor |
No key | CTRL |
SHIFT |
ALT |
|---|---|---|---|---|---|
false |
false/true |
swaps | inserts freely | inserts auto | inserts auto + inverts tiles |
true |
false |
inserts auto | inserts freely | swaps | inserts auto + inverts tiles |
true |
true |
inserts freely | inserts auto | swaps | inserts auto + inverts tiles |
If more than one modifier is held, the precedence order is as follows: ALT > CTRL > SHIFT.
Windows can be resized as usual just by dragging their borders.
Warning
The application is still evolving and changes between versions may introduce breaking changes. Be sure to check the release notes before updating.
Mondrian can be configured by editing the mondrian.toml file located in the ~/.config/mondrian directory.
If the configuration file does not exist, it will be created automatically when the application starts. The configuration generated by the application can be found here.
| Option | Description | Values | Default |
|---|---|---|---|
layout.tiling_strategy |
Tiling strategy | "golden_ratio", "horizontal", "vertical", "twostep", "squared" |
"golden_ratio" |
layout.paddings.tiles |
Padding between tiles (in px) | 0 - 100 | 12 |
layout.paddings.borders |
Padding between border and tiles (in px) | A number, a 2-tuple ([vertical, horizontal]) or a 4-tuple ([top, right, bottom, left]). All values must be between 0 and 140. | 18 |
layout.half_focalized_paddings.tiles |
Padding between tiles for half-focalized windows (in px) | 0 - 100 | 12 |
layout.half_focalized_paddings.borders |
Padding between border and tiles for half-focalized windows (in px) | A number, a 2-tuple ([vertical, horizontal]) or a 4-tuple ([top, right, bottom, left]). All values must be between 0 and 140. | 18 |
layout.focalized_padding |
Padding between border and focalized window (in px) | A number, a 2-tuple ([vertical, horizontal]) or a 4-tuple ([top, right, bottom, left]). All values must be between 0 and 140. | 8 |
layout.strategy.golden_ratio.ratio |
The ratio of the first split | 10 - 90 | 50 |
layout.strategy.golden_ratio.clockwise |
Places the windows clockwise or counterclockwise | true, false |
true |
layout.strategy.golden_ratio.vertical |
If true, the layout will be vertical | true, false |
false |
layout.strategy.twostep.first_step |
First insertion direction | "right", "left", "up", "down" |
"right" |
layout.strategy.twostep.second_step |
Second insertion direction | "right", "left", "up", "down" |
"down" |
layout.strategy.twostep.ratio |
Ratio of the first split | 10 - 90 | 50 |
layout.strategy.horizontal.grow_right |
If true, the layout will grow on the right side | true, false |
true |
layout.strategy.vertical.grow_down |
If true, the layout will grow on the bottom side | true, false |
true |
general.history_based_navigation |
If true, navigation will prioritize the most recently focused window in the given direction | true, false |
false |
general.insert_in_monitor |
If true, moving the window to a new monitor inserts it rather than swapping | true, false |
true |
general.free_move_in_monitor |
If true, free moving the window to a new monitor is enabled by default | true, false |
false |
general.detect_maximized_windows |
Prevents maximized windows from being managed | true, false |
true |
general.move_cursor_on_focus |
Moves the mouse cursor to the center of the focused window (when using focus/move/insert/moveinsert/amplify actions) |
true, false |
false |
general.auto_reload_configs |
Reloads the configuration on changes | true, false |
true |
general.animations.type |
Animation type | "linear"/any of the easings functions from https://easings.net/ (in snake_case) |
"linear" |
general.animations.enabled |
Enables/disables the animations | true, false |
true |
general.animations.duration |
Duration of the animations in ms | 100 - 10000 | 300 |
general.animations.framerate |
Framerate of the animations | 10 - 240 | 60 |
general.floating_wins.topmost |
If true, floating windows will always be on top of other windows | true, false |
true |
general.floating_wins.centered |
If true, floating windows will be centered in the monitor when released | true, false |
true |
general.floating_wins.size |
How floating windows should be resized | "preserve" (keep previous size)"relative" (resize based on monitor resolution)"fixed" (fixed pixel values) |
"relative" |
general.floating_wins.size_ratio |
The ratio of the floating window's size relative to the monitor (used only if size is "relative") |
[0.1 - 1.0, 0.1 - 1.0] | [0.5, 0.5] |
general.floating_wins.size_fixed |
The fixed pixel values of the floating window's size (used only if size is "fixed") |
[100 - 10000, 100 - 10000] | [700, 400] |
general.default_workspace |
Active workspace on startup. | A string with the workspace name | "1" |
general.allow_focus_on_empty_monitor |
The focus action will also consider empty monitors |
true, false |
true |
modules.keybindings.enabled |
Enables/disables the keybindings module | true, false |
false |
modules.keybindings.bindings |
Custom keybindings | check the relative section for more info. | - |
modules.overlays.enabled |
Enables/disables the overlays module | true, false |
true |
modules.overlays.update_while_dragging |
Updates the overlays while dragging the window | true, false |
true |
modules.overlays.update_while_animating |
Updates the overlays while the animations are running | true, false |
true |
modules.overlays.thickness |
Thickness of the border (in px) | 1 - 100 | 4 |
modules.overlays.padding |
Padding between the overlay and the window (in px) | 0 - 30 | 0 |
modules.overlays.border_radius |
Border radius of the overlay | 0 - 100 | 15 |
modules.overlays.active.enabled |
Enables/disables the overlay for the window in focus | true, false |
true |
modules.overlays.active.color |
Color of the overlay | [r, g, b]/[r, g, b, a] or as hex string ("#rrggbb"/"#rrggbbaa") |
[155, 209, 229] (or "#9BD1E5") |
modules.overlays.inactive.enabled |
Enables/disables the overlays for the windows not in focus | true, false |
true |
modules.overlays.inactive.color |
Color of the overlay | [r, g, b]/[r, g, b, a] or as hex string ("#rrggbb"/"#rrggbbaa") |
[156, 156, 156] (or "#9C9C9C) |
modules.overlays.half_focalized.enabled |
Enables/disables the overlay for the half-focalized windows in focused | true,false |
true |
modules.overlays.half_focalized.color |
Color of the overlay | [r, g, b]/[r, g, b, a] or as hex string ("#rrggbb"/"#rrggbbaa") |
[220, 242, 215] (or "#DCF2D7") |
modules.overlays.focalized.enabled |
Enables/disables the overlay for the focalized windows in focused | true,false |
true |
modules.overlays.focalized.color |
Color of the overlay | [r, g, b]/[r, g, b, a] or as hex string ("#rrggbb"/"#rrggbbaa") |
[234, 153, 153] (or "#EA9999") |
modules.overlays.floating.enabled |
Enables/disables the overlay for the floating windows in focused | true,false |
true |
modules.overlays.floating.color |
Color of the overlay | [r, g, b]/[r, g, b, a] or as hex string ("#rrggbb"/"#rrggbbaa") |
[220, 198, 224] (or "#DCC6E0") |
core.rules |
Custom rules to control the behavior of specific windows | check the relative section for more info. | - |
core.ignore_rules |
Custom rules to exclude windows from being managed | check the relative section for more info. | - |
monitors.* |
Per-monitor configurations | check the relative section for more info. | - |
workspaces.* |
Workspaces configurations | check the relative section for more info. | - |
All the options are optional and if not specified, the default values will be used.
You can specify custom keybindings with the modules.keybindings.bindings option.
Each binding has the following format:
bindings = [
{ modifiers = "MODIFIERS", key = "KEY", action = "ACTION" } # "modifiers" can be also spelled as "modifier" or "mod"
]The available modifiers are LALT/RALT, LCTRL/RCTRL, LSHIFT/RSHIFT, LWIN/RWIN or any combination of them joined by + (e.g. LALT+LSHIFT).
If you omit the left/right prefix (e.g. use ALT instead of LALT or RALT), the binding will be created for both the left and right versions: for instance, ALT+LSHIFT is equivalent to creating a binding with LALT+LSHIFT and one with RALT+LSHIFT.
Without the prefix you use both the left and right versions of the modifier (e.g. ALT instead of LALT and RALT).
This parameter is required, except when the key is a function key, in which case it can be omitted.
The available keys are:
- alphanumeric keys (
AtoZ,atoz,0to9); - arrow keys (
up,down,left,right); SPACEkey;- symbols
`,',.,,,;,[,],-,=,/,\; - numpad keys (
NUM0toNUM9); - function keys (
F1toF24).
The keys and modifiers are case-insensitive.
The available actions are:
refresh-config: reloads the configuration and restarts the application;open-config: opens the configuration file in the default editor;retile: re-tiles the windows;minimize: minimizes the focused window. This action also works with unmanaged windows;close: closes the focused window. This action also works with unmanaged windows;toggle-topmost: toggles the topmost state of the focused window. This action only works with floating windows;focus <left|right|up|down>: focuses the window in the specified direction;focus-monitor <left|right|up|down>: focuses the monitor in the specified direction;focus-workspace <WORKSPACE_NAME> [MONITOR_NAME]: focuses the workspace1 on the specified monitor (if provided, otherwise it will be focused on the current monitor);move-to-workspace <WORKSPACE_NAME> [MONITOR_NAME]: moves the focused window into the workspace1 and focuses it. The[MONITOR_NAME]behaves the same way as in thefocus-workspaceaction;move-to-workspace-silent <WORKSPACE_NAME> [MONITOR_NAME]: moves the focused window into the workspace1 without changing the focused workspace. The[MONITOR_NAME]behaves the same way as in thefocus-workspaceaction;switch-focus: switches focus between tiled and floating windows;move <left|right|up|down> [40-1000]: if applied to a tiled window, swaps the focused window with the window in the specified direction. If applied to a floating window, moves the window in the specified direction by the amount in pixels defined in the third parameter (which defaults to 200 if not specified);insert <left|right|up|down>: adds the focused window in the monitor in the specified direction;moveinsert <left|right|up|down> [40-1000]: first tries themoveand then theinsertaction if no window is found in the specified direction;resize <left|right|up|down> <40-500> [40-500]: if applied to a tiled window, resizes the focused window in the specified direction by the amount defined in the third parameter (in pixels). If applied to a floating window, increases (right/down) or decreases (left/up) the size of the window by the amount defined in fourth parameter (which defaults to the previous one if not specified);peek <left|right|up|down> <10-90>: restricts tiling, keeping a percentage of the screen free in the specified direction;invert: inverts the orientation of the focused window and the neighboring windows;release: removes the focused window from the tiling manager, or adds it back;focalize: focalizes the focused window (i.e. hides the neighboring windows) or unfocalizes it (i.e. restores the neighboring windows);half-focalize: hides all the windows except the focused and largest one on the same monitor. Running the action again restores the previous layout;cycle-focalized [next|prev]: swaps the currently focalized/half-focalized window with the next/previous window in the same monitor. If no parameter is specified,nextis used;amplify: swaps the focused window with the biggest one in the same monitor;dumpstateinfo: dumps the current application state info to the./logs/app_state.txtfile;pause [keybindings|overlays]: if no parameter is specified, pauses/unpauses the application. Otherwise, pauses/unpauses the specified module;quit: closes the application.
The syntax of the actions is as follows:
action <v1|v2>means "action v1" or "action v2" (i.e. required parameter);action [v1|v2]means "action", "action v1" or "action v2" (i.e. optional parameter);
Some examples:
[modules.keybindings]
enabled = true
bindings = [
{ key = "F4", action = "quit" }, # F4 to "quit"
{ modifiers = "WIN+ALT", key = "F4", action = "release" }, # WIN+ALT+F4 to "release"
{ modifiers = "CTRL+ALT", key = "left", action = "focus left" } # CTRL+ALT+Left to "focus left"
]You can create custom rules with the core.rules option to control the behavior of specific windows.
Each rule has the following format (or any equivalent format allowed by the TOML specification):
[core]
rules = [
# Single behavior with no parameters
{ filter = { title = "TITLE", exename = "EXENAME", classname = "CLASSNAME", style = "STYLE" }, behavior = "behavior1" },
# Single behavior with parameters
{ filter = { title = "TITLE", exename = "EXENAME", classname = "CLASSNAME", style = "STYLE" }, behavior.behavior2 = {param = "value" } },
# Multiple behaviors
{ filter = { title = "TITLE", exename = "EXENAME", classname = "CLASSNAME", style = "STYLE" }, behaviors = ["behavior1", { behavior2 = { param = "value" } }] },
# Invalid rules
# { filter = { title = "TITLE", exename = "EXENAME", classname = "CLASSNAME", style = "STYLE" } } # missing behavior/behaviors
# { filter = { title = "TITLE", exename = "EXENAME", classname = "CLASSNAME", style = "STYLE" }, behavior = "behavior1", behaviors = ["behavior2", "behavior3"] } # only one of behavior/behaviors must be specified
]Eache rule has a:
filterfield that specifies the window(s) to apply the rule to;behaviororbehaviorsfield that specifies the action to apply to the window(s);
You can specify at least one or more parameters in the filter field and the rule will be matched if all the parameters match the corresponding window property.
Each parameter can be either a string or a regex (enclosed in slashes).
The following table shows the available behavior/behaviors values:
| Behavior | Parameters | Description |
|---|---|---|
float |
topmost (overrides general.floating_wins.topmost, optional)centered (overrides general.floating_wins.centered, optional)size (overrides general.floating_wins.size, optional)size_ratio (overrides general.floating_wins.size_ratio, optional)size_fixed (overrides general.floating_wins.size_fixed, optional) |
Make the corresponding window floating. |
ignore |
- | Ignore the corresponding window. |
insert |
monitor (string, required if workspace is not specified)workspace (string, required if monitor is not specified)silent (if false, the corresponding workspace will be focused. It is false by default.) |
Always insert the corresponding window on the specified monitor and/or workspace. |
delayinsert |
delay (integer, in milliseconds, defaults to 500) |
Reposition the window within the tile layout after the specified delay upon opening.2 |
Some example:
[core]
rules = [
# Match any window with a title="Title" and exename="app.exe" and classname="ApplicationWindow" and style="00000000"
{ filter = { title = "Title", exename = "app.exe", classname = "ApplicationWindow", style = "00000000" }, behavior = "ignore" },
# Match any window with a title="Title"
{ filter = { title = "Title" }, behavior.insert = { monitor = "MONITOR1" } },
# Match any window with a title that matches the regex "Title[0-9]"
# For the `float` behavior, `topmost` and `size` are inherited from the global options
{ filter = { title = "/Title[0-9]/" }, behaviors = ["float", { insert = { monitor = "MONITOR2" } }] },
# Match any window with a title="Title"
# overrides `general.floating_wins.topmost` and `general.floating_wins.size`
{ filter = { title = "Title" }, behavior.float = { topmost = true, size = "preserve"} },
]
To understand how to match specific windows, you can trigger the dumpstateinfo action, then open the ./logs/app_state.txt file and look for the Currently managed windows subsection, which will look like this:
--------------------------------------------------------------------------------
[ 🔲 Tiles Manager ]
--------------------------------------------------------------------------------
...
🗔 Currently managed windows
▸ Window { hwnd: 12345, exe: "app1.exe", class: "ClassName1", style: "00000000", ... }
▸ Monitor: MONITOR1
▸ State: Normal
▸ Window { hwnd: 54321, exe: "app2.exe", class: "ClassName2", style: "00000000", ... }
▸ Monitor: MONITOR2
▸ State: Floating
...
...
You can ignore windows with the core.ignore_rules option.
Each rule has the following format:
[core]
ignore_rules = [
{ title = "TITLE", exename = "EXENAME", classname = "CLASSNAME", style = "STYLE" }
]These rules are equivalent to the core.rules option with the ignore behavior:
[core]
ignore_rules = [
{ title = "TITLE"}
]
# is equivalent to
[core]
rules = [
{ filter = { title = "TITLE" }, behavior = "ignore" }
]You can override some configuration for each monitor with the monitors option:
# with this syntax
[monitors."Monitor 1 name"]
# ...
[monitors."Monitor 2 name"]
# ...
# or with this one
[monitors]
"Monitor 1 name" = { ... }
"Monitor 2 name" = { ... }The following options are available:
| Option | Description |
|---|---|
monitors.*.default_workspace |
check the default_workspace option |
monitors.*.layout.tiling_strategy |
check the layout.tiling_strategy option |
monitors.*.layout.paddings.tiles |
check the layout.paddings.tiles option |
monitors.*.layout.paddings.borders |
check the layout.paddings.borders option |
monitors.*.layout.half_focalized_paddings.tiles |
check the layout.half_focalized_paddings.tiles option |
monitors.*.layout.half_focalized_paddings.borders |
check the layout.half_focalized_paddings.borders option |
monitors.*.layout.focalized_padding |
check the layout.focalized_padding option |
To find the name of the monitors, you can start the application with the --dumpstateinfo flag (or you can trigger the dumpstateinfo action), then open the ./logs/app_state.txt file and look for the Monitors subsection under the Tiles Manager section. The section looks like this:
--------------------------------------------------------------------------------
[ 🔲 Tiles Manager ]
--------------------------------------------------------------------------------
...
🖥️ Monitors
▸ Monitor { handle: 1234567, id: "MONITOR1", primary: true, ... }
▸ Monitor { handle: 7654321, id: "MONITOR2", primary: false, ... }
...
The id field is the name of the monitor, which you can use in the monitors option:
[monitors."MONITOR1"]
layout.tiling_strategy = "horizontal"
[monitors."MONITOR2"]
layout.paddings.borders = 12Workspaces are created automatically when the corresponding actions are triggered (see the actions section).
Using the workspaces option, you can override some default configurations for each workspace:
# with this syntax
[workspaces."workspace-name1"]
# ...
[workspaces."workspace-name2"]
# ...
# or with this one
[workspaces]
"workspace-name1" = { ... }
"workspace-name2" = { ... }The following options are available:
| Option | Description |
|---|---|
workspaces.*.bind_to_monitor |
bind the workspace to a specific monitor |
workspaces.*.layout.tiling_strategy |
check the layout.tiling_strategy option |
workspaces.*.layout.paddings.tiles |
check the layout.paddings.tiles option |
workspaces.*.layout.paddings.borders |
check the layout.paddings.borders option |
workspaces.*.layout.half_focalized_paddings.tiles |
check the layout.half_focalized_paddings.tiles option |
workspaces.*.layout.half_focalized_paddings.borders |
check the layout.half_focalized_paddings.borders option |
workspaces.*.layout.focalized_padding |
check the layout.focalized_padding option |
workspaces.*.monitors.*.layout.* |
monitor-specific configurations (see the per-monitor configurations guide) |
The monitors and workspaces options allow you to override the default configuration for specific monitors and workspaces. Configuration precedence is applied in the following order, from highest to lowest:
workspaces.*.monitors.*;workspaces.*;monitors.*;- default configurations.
For example, with the following configuration:
layout.paddings.borders = 8
[monitors."MONITOR1"]
layout.paddings.borders = 12
[workspaces."1"]
layout.paddings.borders = 14
monitors."MONITOR1".layout.paddings.borders = 16The resulting borders padding will be:
- 16 when on workspace "1" and monitor "MONITOR1";
- 14 when on workspace "1" but not on monitor "MONITOR1";
- 12 when not on workspace "1" but on monitor "MONITOR1";
- 8 when not on workspace "1" and not on monitor "MONITOR1".
It sounded like a fun project to build, and I used it to learn Rust and the Win32 API.
In the beginning, I just wanted to build a simple tiling window manager for Windows, which allowed me to:
- automatically place windows in the correct positions, in multiple monitors;
- use the mouse to move and resize windows.
Then, I started working on it and new features were added. In any case, the main idea is to have an application that "just works" out-of-box, without any special configuration.
Yes, there are others tiling window managers for Windows out there. In particular, I used komorebi and GlazeWM before building this project. Both of them are really good and with great features, and they are in active development. If you need a more mature and established TWM, I recommend trying them.
There are different configurations options that can improve the performances. Here the most important ones:
general.animations.enabled = false: disables the animations;general.animations.framerate: you can set this option to reduce the framerate of the animations;modules.overlays.enabled = false: disables the overlays;modules.overlays.update_while_dragging = false: the overlays will be updated only when the window move/resize operation is done;modules.overlays.update_while_animating = false: the overlays will be updated only when the animations are completed;general.detect_maximized_windows = false: disables the detection of maximized windows. Disabling this feature may cause issues when overlays are enabled.
You can create a rule in the configuration file (see the core.ignore_rules section or the core.rules section for more info).
The target window is selected from those touching the currently focused window in the specified direction.
If there are multiple candidates, the selection depends on the value of the general.history_based_navigation config option:
false: the window at the top is chosen forleft/rightdirections, or the leftmost window forup/downdirections;true: the most recently focused window among the candidates will be selected.
Imagine a layout like this:
+-----------------+
| | C |
| A |-------+
| | |
|---------| D |
| | |
| B |-------+
| | E |
+----+------------+
If the currently focused window is B, and you use the focus right command, windows D and E will be considered as adjacent in the right direction:
- If
general.history_based_navigation = false, D will be selected, as it is the window at the top; - If
general.history_based_navigation = true, E will be selected if it was the most recently focused window.
This project is licensed under the GPLv3 license. See the LICENSE.md for more information.
- Andrey Sitnik for easings.net, which I used as reference for implementing the animations.
Footnotes
-
The workspace name is case-insensitive and can only contain a-z, A-Z, 0-9, "_", ".", "-" or ":" characters. The maximum length is 32 characters. ↩ ↩2 ↩3
-
This behavior is useful when an application, upon opening, is placed incorrectly initially (e.g. Firefox, Zen Browser). Note that this issue usually doesn't happen when the animation duration is reasonably long. ↩