Whim is a pluggable and modern window manager for Windows 10 and 11, built using WinUI 3 and .NET. It is currently in active development.
Note
Documentation is lacking in some areas, and is a work in progress. If you have any questions, feel free to ask in the Discord server, or raise an issue on GitHub.
Alpha builds are available on the releases page.
When you run Whim for the first time, it will create a .whim
directory in your user profile - for example, C:\Users\Isaac\.whim
. This can be configured with the CLI option --dir
.
This directory will contain a whim.config.csx
file which you can edit to customize Whim. This file is a C# script file, and is reloaded every time Whim starts. To have the best development experience, you should have dotnet tooling installed (Visual Studio Code will prompt you when you open .whim
).
The config contains a pre-filled example which you can use as a starting point. You can also find the config here.
A "workspace" in Whim is a collection of windows. They are displayed on a single monitor. The layouts of workspaces are determined by their layout engines. Each workspace has a single active layout engine, and can cycle through different layout engines. For more, see Inspiration.
The WorkspaceManager
object has a customizable CreateLayoutEngines
property which provides the default layout engines for workspaces. For example, the following config sets up three workspaces, and two layout engines:
// Set up workspaces.
context.WorkspaceManager.Add("Browser");
context.WorkspaceManager.Add("IDE");
context.WorkspaceManager.Add("Alt");
// Set up layout engines.
context.WorkspaceManager.CreateLayoutEngines = () => new CreateLeafLayoutEngine[]
{
(id) => new TreeLayoutEngine(context, treeLayoutPlugin, id),
(id) => new ColumnLayoutEngine(id)
};
It's also possible to customize the layout engines for a specific workspace:
context.WorkspaceManager.Add(
"Alt",
new CreateLeafLayoutEngine[]
{
(id) => new ColumnLayoutEngine(id)
}
);
When Whim exits, it will save the current workspaces and the current positions of each window within them. When Whim is started again, it will attempt to merge the saved workspaces with the workspaces defined in the config.
Whim is build around plugins. Plugins are referenced using #r
and using
statements at the top of the config file. Each plugin generally has a Config
class, and a Plugin
class. For example:
BarConfig barConfig = new(leftComponents, centerComponents, rightComponents);
BarPlugin barPlugin = new(context, barConfig);
context.PluginManager.AddPlugin(barPlugin);
Each plugin needs to be added to the context
object.
Whim stores commands (ICommand
), which are objects with a unique identifier, title, and executable action. Commands expose easy access to functionality from Whim's core, and loaded plugins.
Command identifiers namespaced to the plugin which defines them. For example, the whim.core
namespace is reserved for core commands, and whim.gaps
is used by the GapsPlugin
to define commands. Identifiers are based on the Name
property of the plugin - for example, GapsPlugin.Name
.
Each plugin can provide commands through the PluginCommands
property of the IPlugin
interface.
Custom commands are automatically added to the whim.custom
namespace. For example, the following command minimizes Discord:
// Add to the top.
using System.Linq;
void DoConfig(IConfig context)
{
// ...
// Create the command.
context.CommandManager.Add(
// Automatically namespaced to `whim.custom`.
identifier: "minimize_discord",
title: "Minimize Discord",
callback: () =>
{
// Get the first window with the process name "Discord.exe".
IWindow window = context.WindowManager.FirstOrDefault(w => w.ProcessFileName == "Discord.exe");
if (window != null)
{
// Minimize the window.
window.ShowMinimized();
context.WorkspaceManager.ActiveWorkspace.FocusFirstWindow();
}
}
);
// Create an associated keybind.
context.KeybindManager.SetKeybind("whim.custom.minimize_discord", new Keybind(IKeybind.WinAlt, VIRTUAL_KEY.VK_D));
// ...
}
Commands can be bound to keybinds (IKeybind
).
Each command is bound to a single keybind.
Each keybind can trigger multiple commands.
Keybinds can be overridden and removed in the config. For example:
// Override the default keybind for showing/hiding the command palette.
context.KeybindManager.SetKeybind("whim.command_palette.toggle", new Keybind(IKeybind.WinAlt, VIRTUAL_KEY.VK_P));
// Remove the default keybind for closing the current workspace.
context.KeybindManager.Remove("whim.core.close_current_workspace");
// Remove all keybinds - start from scratch.
context.KeybindManager.Clear();
Warning
When overridding keybinds for plugins, make sure to set the keybind after calling context.PluginManager.AddPlugin(plugin)
.
Otherwise, PluginManager.AddPlugin
will set the default keybinds, overriding custom keybinds set before the plugin is added.
Keybindings can also be seen in the command palette, when it is activated (Win + Shift + K by default).
These are the commands and associated keybindings provided by Whim's core. See CoreCommands.cs
.
Identifier | Title | Keybind |
---|---|---|
whim.core.activate_previous_workspace |
Activate the previous workspace | Win + Ctrl + LEFT |
whim.core.activate_next_workspace |
Activate the next workspace | Win + Ctrl + RIGHT |
whim.core.focus_window_in_direction.left |
Focus the window in the left direction | Win + Alt + LEFT |
whim.core.focus_window_in_direction.right |
Focus the window in the right direction | Win + Alt + RIGHT |
whim.core.focus_window_in_direction.up |
Focus the window in the up direction | Win + Alt + UP |
whim.core.focus_window_in_direction.down |
Focus the window in the down direction | Win + Alt + DOWN |
whim.core.swap_window_in_direction.left |
Swap the window with the window to the left | Win + LEFT |
whim.core.swap_window_in_direction.right |
Swap the window with the window to the right | Win + RIGHT |
whim.core.swap_window_in_direction.up |
Swap the window with the window to the up | Win + UP |
whim.core.swap_window_in_direction.down |
Swap the window with the window to the down | Win + DOWN |
whim.core.move_window_left_edge_left |
Move the current window's left edge to the left | Win + Ctrl + H |
whim.core.move_window_left_edge_right |
Move the current window's left edge to the right | Win + Ctrl + J |
whim.core.move_window_right_edge_left |
Move the current window's right edge to the left | Win + Ctrl + K |
whim.core.move_window_right_edge_right |
Move the current window's right edge to the right | Win + Ctrl + L |
whim.core.move_window_top_edge_up |
Move the current window's top edge up | Win + Ctrl + U |
whim.core.move_window_top_edge_down |
Move the current window's top edge down | Win + Ctrl + I |
whim.core.move_window_bottom_edge_up |
Move the current window's bottom edge up | Win + Ctrl + O |
whim.core.move_window_bottom_edge_down |
Move the current window's bottom edge down | Win + Ctrl + P |
whim.core.move_window_to_previous_monitor |
Move the window to the previous monitor | Win + Shift + LEFT |
whim.core.move_window_to_next_monitor |
Move the window to the next monitor | Win + Shift + RIGHT |
whim.core.focus_previous_monitor |
Focus the previous monitor | No default keybind |
whim.core.focus_next_monitor |
Focus the next monitor | No default keybind |
whim.core.close_current_workspace |
Close the current workspace | Win + Ctrl + W |
whim.core.exit_whim |
Exit Whim | No default keybind |
whim.core.activate_workspace_{idx} |
Activate workspace {idx} (where idx is an int 1, 2, ...9, 0) |
Alt + Shift + {idx} |
See CommandPaletteCommands.cs
.
Identifier | Title | Keybind |
---|---|---|
whim.command_palette.toggle |
Toggle command palette | Win + Shift + K |
whim.command_palette.activate_workspace |
Activate workspace | No default keybind |
whim.command_palette.rename_workspace |
Rename workspace | No default keybind |
whim.command_palette.create_workspace |
Create workspace | No default keybind |
whim.command_palette.move_window_to_workspace |
Move window to workspace | No default keybind |
whim.command_palette.move_multiple_windows_to_workspace |
Move multiple windows to workspace | No default keybind |
whim.command_palette.remove_window |
Select window to remove from Whim | No default keybind |
See FloatingLayoutCommands.cs
.
Identifier | Title | Keybind |
---|---|---|
whim.floating_layout.toggle_window_floating |
Toggle window floating | Win + Shift + F |
whim.floating_layout.mark_window_as_floating |
Mark window as floating | Win + Shift + M |
whim.floating_layout.mark_window_as_docked |
Mark window as docked | Win + Shift + D |
See FocusIndicatorCommands.cs
.
Identifier | Title | Keybind |
---|---|---|
whim.focus_indicator.show |
Show focus indicator | No default keybind |
whim.focus_indicator.toggle |
Toggle focus indicator | No default keybind |
whim.focus_indicator.toggle_fade |
Toggle whether the focus indicator fades | No default keybind |
whim.focus_indicator.toggle_enabled |
Toggle whether the focus indicator is enabled | No default keybind |
See GapsCommands.cs
.
Identifier | Title | Keybind |
---|---|---|
whim.gaps.outer.increase |
Increase outer gap | Win + Ctrl + Shift + L |
whim.gaps.outer.decrease |
Decrease outer gap | Win + Ctrl + Shift + H |
whim.gaps.inner.increase |
Increase inner gap | Win + Ctrl + Shift + K |
whim.gaps.inner.decrease |
Decrease inner gap | Win + Ctrl + Shift + J |
Identifier | Title | Keybind |
---|---|---|
whim.tree_layout.add_tree_direction_left |
Add windows to the left of the current window | Win + Ctrl + Shift + LEFT |
whim.tree_layout.add_tree_direction_right |
Add windows to the right of the current window | Win + Ctrl + Shift + RIGHT |
whim.tree_layout.add_tree_direction_up |
Add windows above the current window | Win + Ctrl + Shift + UP |
whim.tree_layout.add_tree_direction_down |
Add windows below the current window | Win + Ctrl + Shift + DOWN |
IRouterManager
is used by Whim to route windows to specific workspaces. For example, to route Discord to a workspace "Chat", you can do the following:
context.RouterManager.Add((window) =>
{
if (window.ProcessFileName == "Discord.exe")
{
return context.WorkspaceManager.TryGet("Chat");
}
// Continue routing.
return null;
});
IFilterManager
tells Whim to ignore windows based on Filter
delegates. A common use case is for plugins to filter out windows they manage themselves and want Whim to not lay out. For example, the bars and command palette are filtered out.
// Called by the bar plugin.
context.FilterManager.AddTitleMatchFilter("Whim Bar");
The IWindowManager
is used by Whim to manage IWindow
s. It listens to window events from Windows and notifies listeners (Whim core, plugins, etc.).
For example, the WindowFocused
event is used by the Whim.FocusIndicator
and Whim.Bar
plugins to update their indications of the currently focused window.
The IWindowManager
also exposes an IFilterManager
called LocationRestoringFilterManager
. Some applications like to restore their window positions when they start (e.g., Firefox, JetBrains Gateway). As a window manager, this is undesirable. LocationRestoringFilterManager
listens to WindowMoved
events for these windows and will force their parent IWorkspace
to do a layout two seconds after their first WindowMoved
event, attempting to restore the window to its correct position.
If this doesn't work, dragging a window's edge will force a layout, which should fix the window's position. This is an area which could use further improvement.
Whim wraps Serilog to provide logging functionality. It can be configured using the LoggerConfig
class. For example:
// The logger will only log messages with a level of `Debug` or higher.
context.Logger.Config = new LoggerConfig() { BaseMinLogLevel = LogLevel.Debug };
// The logger will log messages with a level of `Debug` or higher to a file.
if (context.Logger.Config.FileSink is FileSinkConfig fileSinkConfig)
{
fileSinkConfig.MinLogLevel = LogLevel.Debug;
}
// The logger will log messages with a level of `Error` or higher to the debug console.
// The debug sink is only available in debug builds, and can slow down Whim.
if (context.Logger.Config.DebugSink is SinkConfig debugSinkConfig)
{
debugSinkConfig.MinLogLevel = LogLevel.Error;
}
Whim is heavily inspired by the workspacer project, to which I've contributed to in the past. However, there are a few key differences:
- Whim is built using WinUI 3 instead of Windows Forms. This makes it easier to have a more modern UI.
- Whim has a more powerful command palette, which supports fuzzy search.
- Whim stores windows internally in a more flexible way. This facilitates more complex window management. For more, see Layouts.
- Whim has a command system with common functionality, which makes it easier to interact with at a higher level.
- Creating subclasses of internal classes is not encouraged in Whim - instead, plugins should suffice to add new functionality.
Whim was not built to be a drop-in replacement for workspacer, but it does have a similar feel and many of the same features. It is not a fork of workspacer, and is built from the ground up.
It should be noted that workspacer is no longer in active development.
I am grateful to the workspacer project for the inspiration and ideas it has provided.
This is one of the key areas where Whim differs from workspacer.
Concept | workspacer | Whim |
---|---|---|
Data structure for storing windows | IEnumerable<IWindow> |
Any |
Primary area support | Yes | Not built in but possible in a custom ILayoutEngine |
Directional support | No | Yes |
ILayoutEngine mutability |
Mutable | Immutable |
Currently, workspacer stores all windows in an IEnumerable<IWindow>
stack which is passed to each ILayout
implementation. Relying so heavily on a stack prevents workspacer from supporting more complex window layouts. For example, Whim's TreeLayoutEngine
uses a n-ary tree structure to store windows in arbitrary grid layouts.
Whim does not have a core concept of a "primary area", as it's an idea which lends itself to a stack-based data structure. However, it is possible to implement this functionality in a custom ILayoutEngine
and plugin.
As Whim supports more novel layouts, it also has functionality to account for directions, like FocusWindowInDirection
, SwapWindowInDirection
, and MoveWindowEdgesInDirection
. For example, it's possible to drag a corner of a window diagonally to resize it (provided the underlying ILayoutEngine
supports it).
Implementations of Whim's ILayoutEngine
should be immutable. This was done to support functionality like previewing changes to layouts before committing them, with the LayoutPreview
plugin. In comparison, workspacer's ILayoutEngine
implementations are mutable.
After cloning, make sure to run in the root Whim directory:
git config core.autocrlf true
Please file an issue if you find any bugs or have any feature requests. Pull requests are welcome.
Work is currently being tracked in the project board.
Before making a pull request, please install the tools specified in .config/dotnet-tools.json
:
dotnet tool restore
# To run the formatters:
dotnet tool run dotnet-csharpier .
dotnet tool run xstyler --recursive --d . --config ./.xamlstylerrc
Tests have not been written for all of Whim's code, but they are encouraged. Tests have not been written for UI code-behind files, as I committed to xUnit before I realized that Windows App SDK isn't easily compatible with xUnit. I'm open to suggestions on how to test UI code-behind files.
To use your existing configuration, make sure to update the #r
directives to point to your newly compiled DLLs. In other words, replace C:\Users\<USERNAME>\AppData\Local\Programs\Whim
with C:\path\to\repo\Whim
:
#r "C:\Users\dalyisaac\Repos\Whim\src\Whim.Runner\bin\x64\Debug\net7.0-windows10.0.19041.0\whim.dll"
#r "C:\Users\dalyisaac\Repos\Whim\src\Whim.Runner\bin\x64\Debug\net7.0-windows10.0.19041.0\plugins\Whim.Bar\Whim.Bar.dll"
#r "C:\Users\dalyisaac\Repos\Whim\src\Whim.Runner\bin\x64\Debug\net7.0-windows10.0.19041.0\plugins\Whim.CommandPalette\Whim.CommandPalette.dll"
#r "C:\Users\dalyisaac\Repos\Whim\src\Whim.Runner\bin\x64\Debug\net7.0-windows10.0.19041.0\plugins\Whim.FloatingLayout\Whim.FloatingLayout.dll"
#r "C:\Users\dalyisaac\Repos\Whim\src\Whim.Runner\bin\x64\Debug\net7.0-windows10.0.19041.0\plugins\Whim.FocusIndicator\Whim.FocusIndicator.dll"
#r "C:\Users\dalyisaac\Repos\Whim\src\Whim.Runner\bin\x64\Debug\net7.0-windows10.0.19041.0\plugins\Whim.Gaps\Whim.Gaps.dll"
#r "C:\Users\dalyisaac\Repos\Whim\src\Whim.Runner\bin\x64\Debug\net7.0-windows10.0.19041.0\plugins\Whim.LayoutPreview\Whim.LayoutPreview.dll"
#r "C:\Users\dalyisaac\Repos\Whim\src\Whim.Runner\bin\x64\Debug\net7.0-windows10.0.19041.0\plugins\Whim.TreeLayout\Whim.TreeLayout.dll"
#r "C:\Users\dalyisaac\Repos\Whim\src\Whim.Runner\bin\x64\Debug\net7.0-windows10.0.19041.0\plugins\Whim.TreeLayout.Bar\Whim.TreeLayout.Bar.dll"
#r "C:\Users\dalyisaac\Repos\Whim\src\Whim.Runner\bin\x64\Debug\net7.0-windows10.0.19041.0\plugins\Whim.TreeLayout.CommandPalette\Whim.TreeLayout.CommandPalette.dll"
// Old references:
// #r "C:\Users\dalyisaac\AppData\Local\Programs\Whim\whim.dll"
// #r "C:\Users\dalyisaac\AppData\Local\Programs\Whim\plugins\Whim.Bar\Whim.Bar.dll"
// #r "C:\Users\dalyisaac\AppData\Local\Programs\Whim\plugins\Whim.CommandPalette\Whim.CommandPalette.dll"
// #r "C:\Users\dalyisaac\AppData\Local\Programs\Whim\plugins\Whim.FloatingLayout\Whim.FloatingLayout.dll"
// #r "C:\Users\dalyisaac\AppData\Local\Programs\Whim\plugins\Whim.FocusIndicator\Whim.FocusIndicator.dll"
// #r "C:\Users\dalyisaac\AppData\Local\Programs\Whim\plugins\Whim.Gaps\Whim.Gaps.dll"
// #r "C:\Users\dalyisaac\AppData\Local\Programs\Whim\plugins\Whim.LayoutPreview\Whim.LayoutPreview.dll"
// #r "C:\Users\dalyisaac\AppData\Local\Programs\Whim\plugins\Whim.TreeLayout\Whim.TreeLayout.dll"
// #r "C:\Users\dalyisaac\AppData\Local\Programs\Whim\plugins\Whim.TreeLayout.Bar\Whim.TreeLayout.Bar.dll"
// #r "C:\Users\dalyisaac\AppData\Local\Programs\Whim\plugins\Whim.TreeLayout.CommandPalette\Whim.TreeLayout.CommandPalette.dll"
Visual Studio 2022 is the easiest way to get started with working on Whim. Check the following:
- The
.NET Desktop Development
workload is installed (see the Visual Studio Installer). - The Configuration Manager is set to
Debug
and your target architecture (e.g.x64
). - Each project's platform matches the current target architecture.
Whim.Runner
is set as the startup project.- The green Start arrow is labeled
Whim.Runner (Unpackaged)
.
Recommended Extensions:
Warning
Windows App SDK 1.4 introduced a bug which causes Visual Studio to crash Whim when debugging. Make sure to apply the workaround from microsoft/microsoft-ui-xaml#9008 (comment).
The Whim repository includes a .vscode
directory with a launch.json
file. This file contains a Launch Whim.Runner
configuration which can be used to debug Whim in Visual Studio Code. Unfortunately tests do not appear in Visual Studio Code's Test Explorer.
Tasks to build, test, and format XAML can be found in tasks.json
.
To see the recommended extensions, open the Command Palette and run Extensions: Show Recommended Extensions
.
IContext
has an UncaughtExceptionHandling
property to specify how to handle uncaught exceptions. When developing, it's recommended to set this to UncaughtExceptionHandling.Shutdown
to shutdown Whim when an uncaught exception occurs. This will make it easier to debug the exception.
All uncaught exceptions will be logged as Fatal
.