Skip to content
/ Whim Public
forked from dalyIsaac/Whim

Pluggable window manager for Windows🏗️🪟

License

Notifications You must be signed in to change notification settings

urob/Whim

 
 

Repository files navigation

Whim

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.

Whim demo

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.

Installation

Alpha builds are available on the releases page.

Customization

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.

Workspaces

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.

Plugins

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.

Commands

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

    // ...
}

Keybinds

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.

Default Keybindings

Keybindings can also be seen in the command palette, when it is activated (Win + Shift + K by default).

Core Commands

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}
Command Palette Plugin Commands

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
Floating Layout Plugin Commands

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
Focus Indicator Plugin Commands

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
Gaps Plugin Commands

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
Tree Layout Plugin Commands

See TreeLayoutCommands.cs.

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

Routing

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;
});

Filtering

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");

Window Manager

The IWindowManager is used by Whim to manage IWindows. 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.

Logging

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;
}

Architecture

Inspiration

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.

Layouts

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

ILayoutEngine Data Structure

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.

Primary Area Support

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.

Directional Support

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

ILayoutEngine Mutability

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.

Contributing

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

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

Visual Studio Code

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.

Unhandled Exception Handling

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.

About

Pluggable window manager for Windows🏗️🪟

Resources

License

Stars

Watchers

Forks

Packages

No packages published

Languages

  • C# 98.9%
  • Other 1.1%