Thanks to visit codestin.com
Credit goes to Github.com

Skip to content

policastro/mondrian

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

Mondrian

Mondrian is a tiling window manager built with Rust for Windows 11.

🌟 Key Features

  • 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.

Getting Started

Usage

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.
  • dumpstateinfo dump the application state into a file (./logs/app_state.txt) at the start of the application;
  • healthcheck enables 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).

Moving windows

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 SHIFT while dragging the window to swap the windows;
  • hold ALT while 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 SHIFT while dragging the window to insert the windows;
  • hold ALT while 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.

Resizing windows

Windows can be resized as usual just by dragging their borders.

Configuration

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.

Configuration options

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.

Keybindings

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 (A to Z, a to z, 0 to 9);
  • arrow keys (up, down, left, right);
  • SPACE key;
  • symbols `, ', ., ,, ;, [, ], -, =, /, \;
  • numpad keys (NUM0 to NUM9);
  • function keys (F1 to F24).

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 the focus-workspace action;
  • 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 the focus-workspace action;
  • 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 the move and then the insert action 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, next is 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.txt file;
  • 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"
]

Custom rules

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:

  • filter field that specifies the window(s) to apply the rule to;
  • behavior or behaviors field 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
   ...

...

Ignore windows

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" }
]

Per-monitor configurations

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 = 12

Workspaces

Workspaces 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)

Configuration precedence

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:

  1. workspaces.*.monitors.*;
  2. workspaces.*;
  3. monitors.*;
  4. 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 = 16

The 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".

FAQ

1. Why another tiling window manager?

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.

2. Are there any alternatives?

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.

3. Are there any options to improve the performance?

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.

4. How can I ignore a window?

You can create a rule in the configuration file (see the core.ignore_rules section or the core.rules section for more info).

5. How do the move/focus actions determine the target window?

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 for left/right directions, or the leftmost window for up/down directions;
  • true: the most recently focused window among the candidates will be selected.

Example

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.

License

This project is licensed under the GPLv3 license. See the LICENSE.md for more information.

Acknowledgments

Footnotes

  1. 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

  2. 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.

About

A tiling window manager for Windows 11, built with Rust.

Resources

License

Stars

Watchers

Forks

Packages

No packages published

Languages