# Virtual desktops for Hyprland ![hyprico](.github/hyprland.ico) `virtual-desktops` is a plugin for the [Hyprland](https://github.com/hyprwm/Hyprland) compositor. `virtual-desktops` manages multiple screens workspaces as if they were a single virtual desktop. This plugin **only supports official releases of Hyprland** (e.g., v0.39.x, v0.40.x). If you are on `hyprland-git`, please try compiling this plugin from the [dev branch](https://github.com/levnikmyskin/hyprland-virtual-desktops/tree/dev). There is **NO GUARANTEE** that the plugin will compile succesfully on the latest Hyprland commit, but we try our best to keep it updated. Also, always check the [PR section](https://github.com/levnikmyskin/hyprland-virtual-desktops/pulls?q=is%3Apr+is%3Aopen+sort%3Aupdated-desc), as there might be a draft PR for the next Hyprland release, where you can check the status of development. Feel free to join our [matrix room](https://matrix.to/#/#hypr-virtual-desktops:matrix.org)! ## Table of contents - [Virtual desktops for Hyprland ](#virtual-desktops-for-hyprland-) - [Table of contents](#table-of-contents) - [What is this exactly?](#what-is-this-exactly) - [How does this work?](#how-does-this-work) - [It's just workspaces, really](#its-just-workspaces-really) - [Hyprctl dispatchers](#hyprctl-dispatchers) - [Mix with Hyprland native workspaces](#mix-with-hyprland-native-workspaces) - [Hyprctl commands](#hyprctl-commands) - [Hyprland keywords](#hyprland-keywords) - [Syntax](#syntax) - [Examples](#examples) - [Configuration values](#configuration-values) - [Example config](#example-config) - [Layouts](#layouts) - [Example](#example) - [Layouts are cached and restored if you disconnect/reconnect monitors](#layouts-are-cached-and-restored-if-you-disconnectreconnect-monitors) - [Example](#example-1) - [Choosing how to remember, or choosing to forget](#choosing-how-to-remember-or-choosing-to-forget) - [Example](#example-2) - [Install](#install) - [Installing on NixOS with home—manager](#installing-on-nixos-with-homemanager) - [Help, Hyprland is being weird!](#help-hyprland-is-being-weird) - [It's actually the plugin 😱](#its-actually-the-plugin-) - [Thanks to](#thanks-to) ## What is this exactly? In Hyprland, each screen has its own set of workspaces. For instance, say you have two monitors, with workspace 1 on screen 1 and workspace 2 on screen 2: - When you switch from workspace 1 to 2, Hyprland will simply focus your second screen; - If you switch to workspace 3, your active screen will go to workspace 3, whereas the other screen will stay on whichever workspace it is currently on. You may think of a virtual desktop, instead, as a "single" workspace which extends across your screens (even though, internally, you will still have _n_ different workspaces on your _n_ monitors). If you've ever used KDE Plasma (or Gnome, I think) with multiple screens, this plugin basically replicates that functionality. Taking the previous example: - You will be on virtual desktop 1. Let's say you open your web browser on your first screen and an IDE on your second screen; - When you switch to virtual desktop 2, both screens will switch to empty workspaces. Let's say here you open your email client and your favourite chat application; - If you switch back to virtual desktop 1, you will get back your web browser and the IDE on screen 1 and 2; and viceversa when you go back to virtual desktop 2. ## How does this work? ### It's just workspaces, really Internally, this simply ties _n_ workspaces to your _n_ screens, for each virtual desktop. That is, on virtual desktop 1 you will have workspace 1 on screen 1 and workspace 2 on screen 2; on virtual desktop 2, you will have workspace 3 on screen 1 and workspace 4 on screen 2, and so on. However, if you focus another workspace on a given virtual desktop, the plugin will remember this and you will keep this layout (see [Layouts](#Layouts)). **Notice**: screen 1 and screen 2 are not necessarily what you expect your first and second screen to be, e.g., screen 1 is not necessarily your left screen, and screen 2 is not necessarily your right screen. ### Hyprctl dispatchers This plugin exposes a few hyprctl dispatchers: | Dispatcher | description | type | example| |------------|-------------|------|--------| | vdesk [vdesk] | Changes to virtual desktop `vdesk` | see below | `vdesk 3` or `vdesk coding`| | lastdesk | Changes to last visited virtual desktop | `none` | `lastdesk`| | movetodesk vdesk(, window) | Moves the active/selected window to the specified `vdesk` | `vdesk`, optional window, see below | `movetodesk 2` or `movetodesk 2,title:kitty` | | movetodesksilent vdesk(, window) | same as `movetodesk`, but doesn't switch to desk | same as above | same as above | | movetolastdesk (window) | Moves the active/selected window to the last visited `vdesk` | optional window | `movetolastdesk` or `movetolastdesk title:kitty` | | movetolastdesksilent (window) | same as `movetolastdesk`, but doesn't switch to desk | same as above | same as above | | movetoprevdesk (cycleback)(,window) | Moves the active/selected window to the previous `vdesk`. If `cycleback == 1 and vdesk == 1`, moves to vdesk with max id | optional 1 for true, optional window | `movetoprevdesk 1` or `movetodesk ,title:kitty` | | movetoprevdesksilent (cycleback)(,window) | same as `movetoprevdesk`, but doesn't switch to desk | same as above | same as above | | movetonextdesk (cycle)(, window) | Moves the active/selected window to the next `vdesk`. If `cycle == 1 and vdesk == max`, moves to vdesk 1 | optional 1 for true, optional window | `movetonextdesk 1` or `movetonextdesk ,title:kitty` | | movetonextdesksilent (cycle)(, window) | same as `movetonextdesk`, but doesn't switch to desk | same as above | same as above | | vdeskreset (vdesk) | reset layouts on `vdesk` or on all vdesks if no argument is given (see [Layouts](#Layouts)) | optional vdesk, see below | `vdeskreset` or `vdeskreset 2` or `vdeskreset coding` | | prevdesk | go to previous vdesk. If less than 1, will simply execute `vdesk 1` | `none` | `prevdesk` | | nextdesk | go to next vdesk. Creates it if it doesn't exist | `none` | `nextdesk` | | backcyclevdesks | backward cycle between currently existing vdesks. Goes back to vdesk with max id when at vdesk 1 | `none` | `backcyclevdesks` | | cyclevdesks | cycle between currently existing vdesks. Goes back to vdesk 1 if next vdesk does not exist | `none` | `cyclevdesks` | > BREAKING v2.1.0: `prevdesk` dispatcher was renamed to `lastdesk`. `prevdesk` has a new functionality: it goes to the previous desk. If you were using `prevdesk`, please update your config. For `vdesk` names, you can use: - ID: e.g., `1`, `2` etc; - Name: e.g., `coding`, `internet`, `mail and chats` If a `vdesk` with a given ID or name does not exist, it'll be created on the fly. If you give a (non configured, see [below](#configuration-values)) name, it will be assigned to the next available vdesk id: the virtual-desktops plugin will remember this association even if Hyprland kills the related workspaces. The `movetodesk` and `movetodesksilent` dispatchers work similarly to Hyprland's `movetoworkspace` and `movetoworkspacesilent` dispatchers. See [Hyprland's wiki](https://wiki.hyprland.org/Configuring/Dispatchers/#list-of-dispatchers). Of course, make sure to use the `vdesk` syntax above instead of Hyprland's. #### Mix with Hyprland native workspaces You can use `hyprctl dispatch vdesk n`, even if you have no secondary screen connected at the moment (the behaviour would be identical to native workspaces). Also, I would REMOVE any workspace related configuration, such as `wsbind`. If you want to leverage [workspace-specific rules](https://wiki.hyprland.org/Configuring/Workspace-Rules/), you can: workspaces are always assigned to the same vdesk given the same number of monitors, unless you focus (e.g. with hyprctl) another workspace (see [Layouts](#Layouts)). For instance: - Given two monitors: - vdesk 1 has workspaces 1 and 2; - vdesk 2 has workspaces 3 and 4, and so on; - Given three monitors: - vdesk 1 has workspaces 1, 2 and 3; - vdesk 2 has workspaces 4, 5 and 6, and so on. - Given four monitors... The vdesk a workspace will end up to is easily computed by doing `ceil(workspace_id / n_monitors)`. You know where I'm going with this one...you can easily script it. ### Hyprctl commands Since version 2.2, this plugin exposes a couple of `hyprctl` commands. That is, you can use them by calling `hyprctl {command} {args}`. **NOTICE**: some of these used to be dispatchers. | Command | description | args | example| |------------|-------------|------|--------| | printdesk (vdesk)| Prints to Hyprland log the specified vdesk or the currently active vdesk* (if no argument is given) | optional vdesk, see [above](#hyprctl-dispatchers) | `hyprctl printdesk` or `hyprctl printdesk 2` or `hyprctl printdesk coding`| | printstate | Prints state of all vdesks | `none` | `hyprctl printstate` | | printlayout | print to Hyprland logs the current layout | `none` | `hyprctl printlayout` | ### Hyprland keywords Since version 2.2, this plugin exposes one keyword: `stickyrule`. A sticky rule is composed of a window identifier and a vdesk identifier. A window matched by a sticky rule will be moved to the matched vdesk: 1. When the window is created (similar to [Hyprland's `workspace` windowrule](https://wiki.hyprland.org/Configuring/Window-Rules/#window-rules-v2), but with virtual desks); 2. Every time a monitor is connected/disconnected. **BE CAREFUL**: 1. **NOT** to mix this with Hyprland's `workspace` windowrule (it wouldn't make sense right?); 2. This is not a plugin config, but an Hyprland keyword. Place it in the top level of Hyprland's config (i.e., where you'd put windowrules too). #### Syntax ```bash stickyrule = window,vdesk ``` - `window` identifier has the same syntax as [Hyprland's windowrule window](https://wiki.hyprland.org/Configuring/Window-Rules/#window-rules-v2) identifier; - `vdesk` identifier has the same syntax specified above. #### Examples `stickyrule = class:^(kittysticky)$,3` `stickyrule = title:thunderbird,mail` ### Configuration values This plugin exposes a few configuration options, under the `plugin:virtual-desktops:` category, namely: | Name | description | type | example| |------|-------------|------|--------| | names | map a vdesk id with a name | map[int:string], see below| `names = 1:coding, 2:internet, 3:mail and chats`| | cycleworkspaces | if set to 1 and switching to the currently active vdesk, workspaces will be swapped between your monitors (see [swapactiveworkspaces](https://wiki.hyprland.org/Configuring/Dispatchers/#list-of-dispatchers))| `0` or `1`| `cycleworkspaces = 1`| | rememberlayout | chooses how layouts should be remembered (see [Layouts](#Layouts)), defaults to `size` | `none`, `size` or `monitors` | `remember = size` | | notifyinit | chooses whether to display the startup notification, defaults to 1 | `0` or `1` | `notifyinit = 0` | | verbose_logging | whether to log more stuff, defaults to 0 | `0` or `1` | `verbose_logging = 0` | * The `names` config option maps virtual desktop IDs to a name (you can then use this with the hyprctl [dispatchers](#hyprctl-dispatchers)); * `cycleworkspaces`: THIS CURRENTLY DOES NOT WORK WITH MORE THAN 2 MONITORS. If you need this feature, please feel welcome to submit a PR ^^. #### Example config ```ini stickyrule = class:^(kittysticky)$,3 stickyrule = title:thunderbird,mail plugin { virtual-desktops { names = 1:coding, 2:internet, 3:mail and chats cycleworkspaces = 1 rememberlayout = size notifyinit = 0 verbose_logging = 0 } } ``` ## Layouts Version 2.0 of this plugin introduced the concept of a *layout*, with the meaning of "a specific combination of workspaces on a (more or less) specific combination of monitors". In other words, `virtual-desktops` remembers if you focused another workspace on your vdesk, even if you switch to another vdesk and then come back to this one #### Example Say you have 2 monitors A and B, and you're on vdesk 1: - On monitor A you have workspace 1, and on monitor B you have workspace 2; - Now, say you focus workspace 4 with `hyprctl dispatch workspace 4` on monitor B. - If you switch to vdesk 2 and back to vdesk 1, you will see workspace 4 on monitor B instead of workspace 2. **Notice** that, in this case, workspace 4 would also be shown on vdesk 2. ### Layouts are cached and restored if you disconnect/reconnect monitors Internally, every vdesk will cache all the previous layouts. Once a monitor is connected/disconnected, the plugin will try to look for a previously existing layout for this configuration and it will apply it. #### Example Continuing from the previous example, say you now disconnect monitor B and then you reconnect it, while being on vdesk 1: - the previous layout will be matched and you will get back workspace 1 on monitor A and workspace 4 on monitor B. This would work also if instead of disconnecting and reconnecting monitor B, you connected a third monitor C and then disconnect it. ### Choosing how to remember, or choosing to forget Version 2.0 also introduced the `rememberlayout` config option: with this option, we can choose if we want to match a layout by number of monitors (`size`) or by unique monitor descriptions (`monitors`). There is also the third and final option to not remember layouts at all (`none`). #### Example Continuing from the previous example. Say we now disconnect monitor B and connect monitor C: our connected monitors are A and C. - if `rememberlayout = size` (the default), the existing layout will still be matched, we will have workspace 1 on monitor A, workspace 4 on monitor C; - if `rememberlayout = monitors`, a new layout will be created with defaults: workspace 1 on monitor A, workspace 2 on monitor C; - if `rememberlayout = none`, same as above. If we now disconnect monitor C and reconnect monitor B: our connected monitors are A and B. - if `rememberlayout = size` (the default), the existing layout will be matched, we will have workspace 1 on monitor A, workspace 4 on monitor B; - if `rememberlayout = monitors`, the existing layout will be matched, we will have workspace 1 on monitor A, workspace 4 on monitor B; - if `rememberlayout = none`, a new layout will be created with defaults: workspace 1 on monitor A, workspace 2 on monitor C. ## Install In order to use plugins, you should compile Hyprland yourself. See [Hyprland Wiki#Using Plugins](https://wiki.hyprland.org/Plugins/Using-Plugins/). You can use: ```bash HYPRLAND_HEADERS=path/to/hyprlandrepo make all ``` this will compile and copy the compiled `.so` plugin in the `$HOME/.local/share/hyprload/plugins/bin` path (useful if you use [hyprload](https://github.com/Duckonaut/hyprload)). You can also use `make virtual-desktops.so` to output the compiled plugin in the repo directory. Once compiled, you can tell Hyprland to load the plugin as described in the [Hyprland wiki](https://wiki.hyprland.org/Plugins/Using-Plugins/#installing--using-plugins). ### Installing on NixOS with home—manager Here is an example flake that you can modify to add hyprland-virtual-desktops to your configuration ```nix # flake.nix { inputs = { nixpkgs.url = "github:NixOS/nixpkgs/nixos-unstable"; home-manager = { url = "github:nix-community/home-manager"; inputs.nixpkgs.follows = "nixpkgs"; }; hyprland = { url = "git+https://github.com/hyprwm/Hyprland?submodules=1"; follows = "hyprland-virtual-desktops/hyprland"; # To make sure we run the same version of hyprland that the plugin was built against }; hyprland-virtual-desktops.url = "github:levnikmyskin/hyprland-virtual-desktops"; }; outputs = { nixpkgs, home-manager, hyprland, hyprland-virtual-desktops, ... }: let system = "x86_64-linux"; pkgs = nixpkgs.legacyPackages.${system}; in { homeConfigurations."user@hostname" = home-manager.lib.homeManagerConfiguration { pkgs = nixpkgs.legacyPackages.x86_64-linux; # You can optionally move this module to its own .nix file and source it # here if you want to modularise your configuration modules = [ hyprland.homeManagerModules.default { wayland.windowManager.hyprland = { enable = true; package = hyprland.packages.${pkgs.system}.hyprland; plugins = [ hyprland-virtual-desktops.packages.${pkgs.system}.virtual-desktops ]; # extraConfig is a string that becomes hyprland.conf extraConfig = '' stickyrule = class:^(kittysticky)$,3 stickyrule = title:thunderbird,mail plugin { virtual-desktops { names = 1:coding, 2:internet, 3:mail and chats cycleworkspaces = 1 rememberlayout = size notifyinit = 0 verbose_logging = 0 } } '' + '' # your other configuration for hyprland ''; }; } # ... # NOTE: # You will want to enable the Hyprland module in your NixOS configuration # too, since that also enables critical components like xdg-desktop-portal, # xwayland, polkit, etc # # # Have this somewhere in your NixOS configuration # programs.hyprland = { # enabled = true; # package = inputs.hyprland.packages.${pkgs.system}.hyprland; # }; ]; }; }; } ``` ## Help, Hyprland is being weird! I've noticed that, sometimes, when disconnecting or reconnecting monitors, there might be weird artifacts or similar. Try running: `hyprctl reload` ### It's actually the plugin 😱 If instead you're seeing weird behaviour with the plugin itself, remember you can always run: `hyprtl dispatch vdeskreset` ## Thanks to [split-workspaces](https://github.com/Duckonaut/split-monitor-workspaces/), from which I borrowed the Makefile, and the general idea of how to write Hyprland plugins.