├── .clang-format ├── .gitignore ├── LICENSE ├── Makefile ├── README.md ├── flake.lock ├── flake.nix ├── hyprpm.toml ├── meson.build └── src ├── globals.hpp └── main.cpp /.clang-format: -------------------------------------------------------------------------------- 1 | --- 2 | Language: Cpp 3 | BasedOnStyle: LLVM 4 | 5 | AccessModifierOffset: -2 6 | AlignAfterOpenBracket: Align 7 | AlignConsecutiveMacros: true 8 | AlignConsecutiveAssignments: true 9 | AlignEscapedNewlines: Right 10 | AlignOperands: false 11 | AlignTrailingComments: true 12 | AllowAllArgumentsOnNextLine: true 13 | AllowAllConstructorInitializersOnNextLine: true 14 | AllowAllParametersOfDeclarationOnNextLine: true 15 | AllowShortBlocksOnASingleLine: true 16 | AllowShortCaseLabelsOnASingleLine: true 17 | AllowShortFunctionsOnASingleLine: Empty 18 | AllowShortIfStatementsOnASingleLine: Never 19 | AllowShortLambdasOnASingleLine: All 20 | AllowShortLoopsOnASingleLine: false 21 | AlwaysBreakAfterDefinitionReturnType: None 22 | AlwaysBreakAfterReturnType: None 23 | AlwaysBreakBeforeMultilineStrings: false 24 | AlwaysBreakTemplateDeclarations: Yes 25 | BreakBeforeBraces: Attach 26 | BreakBeforeTernaryOperators: false 27 | BreakConstructorInitializers: AfterColon 28 | ColumnLimit: 180 29 | CompactNamespaces: false 30 | ConstructorInitializerAllOnOneLineOrOnePerLine: false 31 | ExperimentalAutoDetectBinPacking: false 32 | FixNamespaceComments: false 33 | IncludeBlocks: Preserve 34 | IndentCaseLabels: true 35 | IndentWidth: 4 36 | PointerAlignment: Left 37 | ReflowComments: false 38 | SortIncludes: false 39 | SortUsingDeclarations: false 40 | SpaceAfterCStyleCast: false 41 | SpaceAfterLogicalNot: false 42 | SpaceAfterTemplateKeyword: true 43 | SpaceBeforeCtorInitializerColon: true 44 | SpaceBeforeInheritanceColon: true 45 | SpaceBeforeParens: ControlStatements 46 | SpaceBeforeRangeBasedForLoopColon: true 47 | SpaceInEmptyParentheses: false 48 | SpacesBeforeTrailingComments: 1 49 | SpacesInAngles: false 50 | SpacesInCStyleCastParentheses: false 51 | SpacesInContainerLiterals: false 52 | SpacesInParentheses: false 53 | SpacesInSquareBrackets: false 54 | Standard: Auto 55 | TabWidth: 4 56 | UseTab: Never 57 | 58 | AllowShortEnumsOnASingleLine: false 59 | 60 | BraceWrapping: 61 | AfterEnum: false 62 | 63 | AlignConsecutiveDeclarations: AcrossEmptyLines 64 | 65 | NamespaceIndentation: All -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .vscode/ 2 | hyprsplit.so 3 | result 4 | build 5 | compile_commands.json 6 | .cache 7 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | BSD 3-Clause License 2 | 3 | Copyright (c) 2024, shezdy 4 | 5 | Redistribution and use in source and binary forms, with or without 6 | modification, are permitted provided that the following conditions are met: 7 | 8 | 1. Redistributions of source code must retain the above copyright notice, this 9 | list of conditions and the following disclaimer. 10 | 11 | 2. Redistributions in binary form must reproduce the above copyright notice, 12 | this list of conditions and the following disclaimer in the documentation 13 | and/or other materials provided with the distribution. 14 | 15 | 3. Neither the name of the copyright holder nor the names of its 16 | contributors may be used to endorse or promote products derived from 17 | this software without specific prior written permission. 18 | 19 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" 20 | AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE 21 | IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE 22 | DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE 23 | FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL 24 | DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR 25 | SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER 26 | CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, 27 | OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE 28 | OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 29 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | CXXFLAGS=-shared -fPIC --no-gnu-unique -Wall -g -DWLR_USE_UNSTABLE -std=c++23 -O2 2 | INCLUDES = `pkg-config --cflags pixman-1 libdrm hyprland` 3 | SRC = $(wildcard src/*.cpp) 4 | TARGET = hyprsplit.so 5 | 6 | all: 7 | $(CXX) $(CXXFLAGS) $(INCLUDES) $(SRC) -o $(TARGET) 8 | 9 | clean: 10 | rm ./$(TARGET) 11 | 12 | withhyprpmheaders: export PKG_CONFIG_PATH = $(XDG_DATA_HOME)/hyprpm/headersRoot/share/pkgconfig 13 | withhyprpmheaders: all 14 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # hyprsplit 2 | awesome / dwm like workspaces for [hyprland](https://github.com/hyprwm/hyprland) 3 | 4 | A complete rewrite of [split-monitor-workspaces](https://github.com/Duckonaut/split-monitor-workspaces) that attempts to fix the issues I experienced with it. Improvements include: 5 | - Workspaces on each monitor are determined by that monitor's ID 6 | - Ability to grab windows that get lost in invalid workspaces when disconnecting monitors 7 | - Dispatcher to swap all windows in active workspaces between two monitors 8 | - Better handling of workspace params. For example `empty` will work properly and select an empty workspace on the current monitor. 9 | 10 | ## Installation 11 | Requires Hyprland version >=`v0.36.0` 12 | 13 | ### Hyprpm 14 | Load plugins on startup by putting exec-once = hyprpm reload -n in your hyprland config. 15 | ``` 16 | hyprpm update 17 | hyprpm add https://github.com/shezdy/hyprsplit 18 | hyprpm enable hyprsplit 19 | ``` 20 | 21 | ### Manual 22 | Make sure you have the Hyprland headers installed (see [hyprland wiki](https://wiki.hyprland.org/Plugins/Using-Plugins/#manual)) 23 | 24 | If you are compiling for a numbered version of Hyprland, check for a commit pin in hyprpm.toml, and reset the plugin to the second hash in the pair. 25 | 26 | Compile the plugin: 27 | ``` 28 | make all 29 | ``` 30 | Finally add the following to your config `plugin = /path/to/hyprsplit/hyprsplit.so`, or run `hyprctl plugin load /path/to/hyprsplit/hyprsplit.so` 31 | 32 | ## Configuration 33 | ### Options 34 | 35 | | name | description | type | default | 36 | |---|---|---|---| 37 | | num_workspaces | Number of workspaces on each monitor | int | 10 | 38 | | persistent_workspaces | if true, will make workspaces on each monitor persistent (they will always exist and will not be destroyed when empty) | bool | false | 39 | 40 | ### Dispatchers 41 | | Dispatcher | Description | Params | 42 | | ---------- | ----------- | ------ | 43 | | split:workspace | Replacement for `workspace` | workspace | 44 | | split:movetoworkspace | Replacement for `movetoworkspace` | workspace OR `workspace,window` for a specific window | 45 | | split:movetoworkspacesilent | Replacement for `movetoworkspacesilent` | workspace OR `workspace,window` for a specific window | 46 | | split:swapactiveworkspaces | Swaps all windows in active workspaces between two monitors | two monitors separated by a space | 47 | | split:grabroguewindows | Finds all windows that are in invalid workspaces and moves them to the current workspace. Useful when unplugging monitors. | none | 48 | 49 | Some of Hyprland's workspace parameters are treated differently by the plugin's dispatchers: 50 | - `1`,`2`, or `3`: number on current monitor 51 | - `+1` or `-1`: relative on current monitor, no looping 52 | - `r+1` or `r-1`: relative on current monitor, with looping 53 | - `e+1`, `e-1`: relative on current monitor, excluding empty workspaces, same as m+1 54 | - `empty`: empty workspace on current monitor 55 | 56 | All other workspace params will be treated the same as however Hyprland normally treats them. 57 | 58 | If you are using hy3 you should use `hy3:movetoworkspace` instead of `split:movetoworkspace`, it has compatibility with hyprsplit. 59 | 60 | ### Example Config 61 | ``` 62 | plugin { 63 | hyprsplit { 64 | num_workspaces = 6 65 | } 66 | } 67 | 68 | bind = SUPER, 1, split:workspace, 1 69 | bind = SUPER, 2, split:workspace, 2 70 | bind = SUPER, 3, split:workspace, 3 71 | bind = SUPER, 4, split:workspace, 4 72 | bind = SUPER, 5, split:workspace, 5 73 | bind = SUPER, 6, split:workspace, 6 74 | 75 | bind = SUPER SHIFT, 1, split:movetoworkspacesilent, 1 76 | bind = SUPER SHIFT, 2, split:movetoworkspacesilent, 2 77 | bind = SUPER SHIFT, 3, split:movetoworkspacesilent, 3 78 | bind = SUPER SHIFT, 4, split:movetoworkspacesilent, 4 79 | bind = SUPER SHIFT, 5, split:movetoworkspacesilent, 5 80 | bind = SUPER SHIFT, 6, split:movetoworkspacesilent, 6 81 | 82 | bind = SUPER, D, split:swapactiveworkspaces, current +1 83 | bind = SUPER, G, split:grabroguewindows 84 | ``` 85 | -------------------------------------------------------------------------------- /flake.lock: -------------------------------------------------------------------------------- 1 | { 2 | "nodes": { 3 | "hyprcursor": { 4 | "inputs": { 5 | "hyprlang": "hyprlang", 6 | "nixpkgs": [ 7 | "hyprland", 8 | "nixpkgs" 9 | ], 10 | "systems": [ 11 | "hyprland", 12 | "systems" 13 | ] 14 | }, 15 | "locked": { 16 | "lastModified": 1711466786, 17 | "narHash": "sha256-sArxGyUBiCA1in+q6t0QqT+ZJiZ1PyBp7cNPKLmREM0=", 18 | "owner": "hyprwm", 19 | "repo": "hyprcursor", 20 | "rev": "d3876f34779cc03ee51e4aafc0d00a4f187c7544", 21 | "type": "github" 22 | }, 23 | "original": { 24 | "owner": "hyprwm", 25 | "repo": "hyprcursor", 26 | "type": "github" 27 | } 28 | }, 29 | "hyprland": { 30 | "inputs": { 31 | "hyprcursor": "hyprcursor", 32 | "hyprland-protocols": "hyprland-protocols", 33 | "hyprlang": "hyprlang_2", 34 | "nixpkgs": "nixpkgs", 35 | "systems": "systems_2", 36 | "wlroots": "wlroots", 37 | "xdph": "xdph" 38 | }, 39 | "locked": { 40 | "lastModified": 1712248444, 41 | "narHash": "sha256-ayxuwrzpow3cRoKtCYj3v7GGrDHTbKVDDUxb8wd1cL8=", 42 | "owner": "hyprwm", 43 | "repo": "Hyprland", 44 | "rev": "c4b660a33930a0e60e853b6796c1fd76914b1719", 45 | "type": "github" 46 | }, 47 | "original": { 48 | "owner": "hyprwm", 49 | "repo": "Hyprland", 50 | "type": "github" 51 | } 52 | }, 53 | "hyprland-protocols": { 54 | "inputs": { 55 | "nixpkgs": [ 56 | "hyprland", 57 | "nixpkgs" 58 | ], 59 | "systems": [ 60 | "hyprland", 61 | "systems" 62 | ] 63 | }, 64 | "locked": { 65 | "lastModified": 1691753796, 66 | "narHash": "sha256-zOEwiWoXk3j3+EoF3ySUJmberFewWlagvewDRuWYAso=", 67 | "owner": "hyprwm", 68 | "repo": "hyprland-protocols", 69 | "rev": "0c2ce70625cb30aef199cb388f99e19a61a6ce03", 70 | "type": "github" 71 | }, 72 | "original": { 73 | "owner": "hyprwm", 74 | "repo": "hyprland-protocols", 75 | "type": "github" 76 | } 77 | }, 78 | "hyprlang": { 79 | "inputs": { 80 | "nixpkgs": [ 81 | "hyprland", 82 | "hyprcursor", 83 | "nixpkgs" 84 | ], 85 | "systems": "systems" 86 | }, 87 | "locked": { 88 | "lastModified": 1709914708, 89 | "narHash": "sha256-bR4o3mynoTa1Wi4ZTjbnsZ6iqVcPGriXp56bZh5UFTk=", 90 | "owner": "hyprwm", 91 | "repo": "hyprlang", 92 | "rev": "a685493fdbeec01ca8ccdf1f3655c044a8ce2fe2", 93 | "type": "github" 94 | }, 95 | "original": { 96 | "owner": "hyprwm", 97 | "repo": "hyprlang", 98 | "type": "github" 99 | } 100 | }, 101 | "hyprlang_2": { 102 | "inputs": { 103 | "nixpkgs": [ 104 | "hyprland", 105 | "nixpkgs" 106 | ], 107 | "systems": [ 108 | "hyprland", 109 | "systems" 110 | ] 111 | }, 112 | "locked": { 113 | "lastModified": 1711250455, 114 | "narHash": "sha256-LSq1ZsTpeD7xsqvlsepDEelWRDtAhqwetp6PusHXJRo=", 115 | "owner": "hyprwm", 116 | "repo": "hyprlang", 117 | "rev": "b3e430f81f3364c5dd1a3cc9995706a4799eb3fa", 118 | "type": "github" 119 | }, 120 | "original": { 121 | "owner": "hyprwm", 122 | "repo": "hyprlang", 123 | "type": "github" 124 | } 125 | }, 126 | "nixpkgs": { 127 | "locked": { 128 | "lastModified": 1711523803, 129 | "narHash": "sha256-UKcYiHWHQynzj6CN/vTcix4yd1eCu1uFdsuarupdCQQ=", 130 | "owner": "NixOS", 131 | "repo": "nixpkgs", 132 | "rev": "2726f127c15a4cc9810843b96cad73c7eb39e443", 133 | "type": "github" 134 | }, 135 | "original": { 136 | "owner": "NixOS", 137 | "ref": "nixos-unstable", 138 | "repo": "nixpkgs", 139 | "type": "github" 140 | } 141 | }, 142 | "root": { 143 | "inputs": { 144 | "hyprland": "hyprland" 145 | } 146 | }, 147 | "systems": { 148 | "locked": { 149 | "lastModified": 1689347949, 150 | "narHash": "sha256-12tWmuL2zgBgZkdoB6qXZsgJEH9LR3oUgpaQq2RbI80=", 151 | "owner": "nix-systems", 152 | "repo": "default-linux", 153 | "rev": "31732fcf5e8fea42e59c2488ad31a0e651500f68", 154 | "type": "github" 155 | }, 156 | "original": { 157 | "owner": "nix-systems", 158 | "repo": "default-linux", 159 | "type": "github" 160 | } 161 | }, 162 | "systems_2": { 163 | "locked": { 164 | "lastModified": 1689347949, 165 | "narHash": "sha256-12tWmuL2zgBgZkdoB6qXZsgJEH9LR3oUgpaQq2RbI80=", 166 | "owner": "nix-systems", 167 | "repo": "default-linux", 168 | "rev": "31732fcf5e8fea42e59c2488ad31a0e651500f68", 169 | "type": "github" 170 | }, 171 | "original": { 172 | "owner": "nix-systems", 173 | "repo": "default-linux", 174 | "type": "github" 175 | } 176 | }, 177 | "wlroots": { 178 | "flake": false, 179 | "locked": { 180 | "host": "gitlab.freedesktop.org", 181 | "lastModified": 1709983277, 182 | "narHash": "sha256-wXWIJLd4F2JZeMaihWVDW/yYXCLEC8OpeNJZg9a9ly8=", 183 | "owner": "wlroots", 184 | "repo": "wlroots", 185 | "rev": "50eae512d9cecbf0b3b1898bb1f0b40fa05fe19b", 186 | "type": "gitlab" 187 | }, 188 | "original": { 189 | "host": "gitlab.freedesktop.org", 190 | "owner": "wlroots", 191 | "repo": "wlroots", 192 | "rev": "50eae512d9cecbf0b3b1898bb1f0b40fa05fe19b", 193 | "type": "gitlab" 194 | } 195 | }, 196 | "xdph": { 197 | "inputs": { 198 | "hyprland-protocols": [ 199 | "hyprland", 200 | "hyprland-protocols" 201 | ], 202 | "hyprlang": [ 203 | "hyprland", 204 | "hyprlang" 205 | ], 206 | "nixpkgs": [ 207 | "hyprland", 208 | "nixpkgs" 209 | ], 210 | "systems": [ 211 | "hyprland", 212 | "systems" 213 | ] 214 | }, 215 | "locked": { 216 | "lastModified": 1709299639, 217 | "narHash": "sha256-jYqJM5khksLIbqSxCLUUcqEgI+O2LdlSlcMEBs39CAU=", 218 | "owner": "hyprwm", 219 | "repo": "xdg-desktop-portal-hyprland", 220 | "rev": "2d2fb547178ec025da643db57d40a971507b82fe", 221 | "type": "github" 222 | }, 223 | "original": { 224 | "owner": "hyprwm", 225 | "repo": "xdg-desktop-portal-hyprland", 226 | "type": "github" 227 | } 228 | } 229 | }, 230 | "root": "root", 231 | "version": 7 232 | } 233 | -------------------------------------------------------------------------------- /flake.nix: -------------------------------------------------------------------------------- 1 | { 2 | inputs = { 3 | hyprland.url = "github:hyprwm/Hyprland"; 4 | }; 5 | 6 | outputs = { 7 | self, 8 | hyprland, 9 | ... 10 | }: let 11 | inherit (hyprland.inputs) nixpkgs; 12 | eachSystem = nixpkgs.lib.genAttrs (import hyprland.inputs.systems); 13 | pkgsFor = eachSystem (system: import nixpkgs {localSystem = system;}); 14 | rawCommitPins = (builtins.fromTOML (builtins.readFile ./hyprpm.toml)).repository.commit_pins; 15 | commitPins = builtins.listToAttrs ( 16 | map (p: { 17 | name = builtins.head p; 18 | value = builtins.elemAt p 1; 19 | }) 20 | rawCommitPins 21 | ); 22 | srcRev = "${commitPins.${hyprland.rev} or "git"}"; 23 | srcRevShort = builtins.substring 0 7 srcRev; 24 | in { 25 | packages = eachSystem (system: let 26 | pkgs = pkgsFor.${system}; 27 | in rec { 28 | hyprsplit = pkgs.stdenv.mkDerivation { 29 | pname = "hyprsplit"; 30 | version = "flakeRev=${self.shortRev or "dirty"}_srcRev=${srcRevShort}"; 31 | src = 32 | if (commitPins ? ${hyprland.rev}) && (self ? rev) 33 | then 34 | (builtins.fetchGit { 35 | url = "https://github.com/shezdy/hyprsplit"; 36 | rev = srcRev; 37 | }) 38 | else ./.; 39 | 40 | nativeBuildInputs = with pkgs; [pkg-config meson ninja gcc14]; 41 | buildInputs = with pkgs; 42 | [ 43 | hyprland.packages.${system}.hyprland.dev 44 | pixman 45 | libdrm 46 | ] 47 | ++ hyprland.packages.${system}.hyprland.buildInputs; 48 | 49 | meta = with pkgs.lib; { 50 | homepage = "https://github.com/shezdy/hyprsplit"; 51 | description = "Hyprland plugin for separate sets of workspaces on each monitor"; 52 | license = licenses.bsd3; 53 | platforms = platforms.linux; 54 | }; 55 | }; 56 | 57 | default = hyprsplit; 58 | }); 59 | 60 | devShells = eachSystem (system: let 61 | pkgs = pkgsFor.${system}; 62 | in { 63 | default = pkgs.mkShell.override {stdenv = pkgs.gcc14Stdenv;} { 64 | shellHook = '' 65 | meson setup build --reconfigure 66 | cp ./build/compile_commands.json ./compile_commands.json 67 | ''; 68 | name = "hyprsplit"; 69 | inputsFrom = [self.packages.${system}.hyprsplit]; 70 | }; 71 | }); 72 | }; 73 | } 74 | -------------------------------------------------------------------------------- /hyprpm.toml: -------------------------------------------------------------------------------- 1 | [repository] 2 | name = "hyprsplit" 3 | authors = ["shezdy"] 4 | commit_pins = [ 5 | ["1c460e98f870676b15871fe4e5bfeb1a32a3d6d8", "fd31d08d2f9756ba487541a4d2f5a2cbf0eb16aa"], # 0.36.0 6 | ["19c90048d65a5660384d2fb865926a366696d6be", "fd31d08d2f9756ba487541a4d2f5a2cbf0eb16aa"], # 0.37.0 7 | ["c5e28ebcfe00a510922779b2c568cfa52a317445", "fd31d08d2f9756ba487541a4d2f5a2cbf0eb16aa"], # 0.37.1 8 | ["3875679755014997776e091ff8903acfb311dd2f", "fd31d08d2f9756ba487541a4d2f5a2cbf0eb16aa"], # 0.38.0 9 | ["360ede79d124ffdeebbe8401f1ac4bc0dbec2c91", "fd31d08d2f9756ba487541a4d2f5a2cbf0eb16aa"], # 0.38.1 10 | ["e93fbd7c4f991cb8ef03e433ccc4d43587923e15", "3dd002cc6df3fce7d5a49df91201e849f6e2d8e5"], # 0.39.0 11 | ["fe7b748eb668136dd0558b7c8279bfcd7ab4d759", "6b9db62fab91066bb678f14198771b8e8cc12a8f"], # 0.39.1 12 | ["cba1ade848feac44b2eda677503900639581c3f4", "ed317e19a8a4e46b339b2d28d1380b5ad1c24eaf"], # 0.40.0 13 | ["ea2501d4556f84d3de86a4ae2f4b22a474555b9f", "b9753043dc8c2a385f39777aa17f63ae3c104e36"], # 0.41.0 14 | ["9e781040d9067c2711ec2e9f5b47b76ef70762b3", "38102b8f81e1d48e1c96728513cc663fffe034fc"], # 0.41.1 15 | ["918d8340afd652b011b937d29d5eea0be08467f5", "3051541ebc7f24dd5325cdfa458f29b4ff9648de"], # 0.41.2 16 | ["9a09eac79b85c846e3a865a9078a3f8ff65a9259", "fcf00b770e3b89fd93de2de1bb5e68721090f5fe"], # 0.42.0 17 | ["0f594732b063a90d44df8c5d402d658f27471dfe", "e5c8698c772f7658d1893441ce811ff1eb7c1ba2"], # 0.43.0 18 | ["0c7a7e2d569eeed9d6025f3eef4ea0690d90845d", "7376aaf982e388046872d534e09e6281666fce72"], # 0.44.0 19 | ["4520b30d498daca8079365bdb909a8dea38e8d55", "0b315fb97a0f9825668db9ff7436bec5d7e028cb"], # 0.44.1 20 | ["a425fbebe4cf4238e48a42f724ef2208959d66cf", "b98cc80aab041677cd7648f6e44c18e8f36fa907"], # 0.45.0 21 | ["500d2a3580388afc8b620b0a3624147faa34f98b", "1ac3dc76d7bd9a5dcffa0f6fb75d35258c519454"], # 0.45.1 22 | ["12f9a0d0b93f691d4d9923716557154d74777b0a", "1ac3dc76d7bd9a5dcffa0f6fb75d35258c519454"], # 0.45.2 23 | ["788ae588979c2a1ff8a660f16e3c502ef5796755", "28b1603e7674cc12e0b2b5b384b4dc88b659a62b"], # 0.46.0 24 | ["254fc2bc6000075f660b4b8ed818a6af544d1d64", "28b1603e7674cc12e0b2b5b384b4dc88b659a62b"], # 0.46.1 25 | ["0bd541f2fd902dbfa04c3ea2ccf679395e316887", "28b1603e7674cc12e0b2b5b384b4dc88b659a62b"], # 0.46.2 26 | ["04ac46c54357278fc68f0a95d26347ea0db99496", "36f43793272c0700974477271ef0e0e544b21293"], # 0.47.0 27 | ["75dff7205f6d2bd437abfb4196f700abee92581a", "36f43793272c0700974477271ef0e0e544b21293"], # 0.47.1 28 | ["882f7ad7d2bbfc7440d0ccaef93b1cdd78e8e3ff", "36f43793272c0700974477271ef0e0e544b21293"], # 0.47.2 29 | ["5ee35f914f921e5696030698e74fb5566a804768", "62715b22421a43ba4ac0b5cd13bbc31090b8ec8b"], # 0.48.0 30 | ["29e2e59fdbab8ed2cc23a20e3c6043d5decb5cdc", "62715b22421a43ba4ac0b5cd13bbc31090b8ec8b"], # 0.48.1 31 | ["9958d297641b5c84dcff93f9039d80a5ad37ab00", "9a65a4d33cc86703d2ac1f349de9697c8fc7a4b9"] # 0.49.0 32 | ] 33 | 34 | [hyprsplit] 35 | description = "split monitor workspaces" 36 | authors = ["shezdy"] 37 | output = "hyprsplit.so" 38 | build = [ 39 | "make all", 40 | ] 41 | -------------------------------------------------------------------------------- /meson.build: -------------------------------------------------------------------------------- 1 | project('hyprsplit', 'cpp', 2 | version: '0.1', 3 | default_options: ['buildtype=release', 'cpp_std=c++23', 'warning_level=1'], 4 | ) 5 | 6 | add_project_arguments( 7 | [ 8 | '-DWLR_USE_UNSTABLE' 9 | ], 10 | language: 'cpp') 11 | 12 | globber = run_command('find', '.', '-name', '*.cpp', check: true) 13 | src = globber.stdout().strip().split('\n') 14 | 15 | shared_module(meson.project_name(), src, 16 | dependencies: [ 17 | dependency('hyprland'), 18 | dependency('pixman-1'), 19 | dependency('libdrm'), 20 | ], 21 | install: true, 22 | ) 23 | -------------------------------------------------------------------------------- /src/globals.hpp: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | #include 4 | 5 | inline HANDLE PHANDLE = nullptr; -------------------------------------------------------------------------------- /src/main.cpp: -------------------------------------------------------------------------------- 1 | #include "globals.hpp" 2 | #include 3 | #include 4 | 5 | #define private public 6 | #include 7 | #include 8 | #include 9 | #include 10 | #include 11 | #include 12 | #undef private 13 | 14 | using namespace Hyprutils::String; 15 | 16 | std::string getWorkspaceOnCurrentMonitor(const std::string& workspace) { 17 | if (!g_pCompositor->m_lastMonitor) { 18 | Debug::log(ERR, "[hyprsplit] no monitor in getWorkspaceOnCurrentMonitor?"); 19 | return workspace; 20 | } 21 | 22 | int wsID = 1; 23 | static auto* const NUMWORKSPACES = (Hyprlang::INT* const*)HyprlandAPI::getConfigValue(PHANDLE, "plugin:hyprsplit:num_workspaces")->getDataStaticPtr(); 24 | 25 | if (workspace[0] == '+' || workspace[0] == '-') { 26 | const auto PLUSMINUSRESULT = getPlusMinusKeywordResult(workspace, ((g_pCompositor->m_lastMonitor->activeWorkspaceID() - 1) % **NUMWORKSPACES) + 1); 27 | 28 | if (!PLUSMINUSRESULT.has_value()) 29 | return workspace; 30 | 31 | wsID = std::max((int)PLUSMINUSRESULT.value(), 1); 32 | 33 | if (wsID > **NUMWORKSPACES) 34 | wsID = **NUMWORKSPACES; 35 | } else if (isNumber(workspace)) { 36 | wsID = std::max(std::stoi(workspace), 1); 37 | } else if (workspace[0] == 'r' && (workspace[1] == '-' || workspace[1] == '+') && isNumber(workspace.substr(2))) { 38 | const auto PLUSMINUSRESULT = getPlusMinusKeywordResult(workspace.substr(1), g_pCompositor->m_lastMonitor->activeWorkspaceID()); 39 | 40 | if (!PLUSMINUSRESULT.has_value()) 41 | return workspace; 42 | 43 | wsID = (int)PLUSMINUSRESULT.value(); 44 | 45 | if (wsID <= 0) 46 | wsID = ((((wsID - 1) % **NUMWORKSPACES) + **NUMWORKSPACES) % **NUMWORKSPACES) + 1; 47 | } else if (workspace[0] == 'e' && (workspace[1] == '-' || workspace[1] == '+') && isNumber(workspace.substr(2))) { 48 | return "m" + workspace.substr(1); 49 | } else if (workspace.starts_with("empty")) { 50 | int i = 0; 51 | while (++i <= **NUMWORKSPACES) { 52 | const int id = g_pCompositor->m_lastMonitor->m_id * (**NUMWORKSPACES) + i; 53 | const auto PWORKSPACE = g_pCompositor->getWorkspaceByID(id); 54 | 55 | if (!PWORKSPACE || (PWORKSPACE->getWindows() == 0)) 56 | return std::to_string(id); 57 | } 58 | 59 | Debug::log(LOG, "[hyprsplit] no empty workspace on monitor"); 60 | return std::to_string(g_pCompositor->m_lastMonitor->activeWorkspaceID()); 61 | } else { 62 | return workspace; 63 | } 64 | 65 | if (wsID > **NUMWORKSPACES) 66 | wsID = ((wsID - 1) % **NUMWORKSPACES) + 1; 67 | 68 | return std::to_string(g_pCompositor->m_lastMonitor->m_id * (**NUMWORKSPACES) + wsID); 69 | } 70 | 71 | void ensureGoodWorkspaces() { 72 | if (g_pCompositor->m_unsafeState) 73 | return; 74 | 75 | static auto* const NUMWORKSPACES = (Hyprlang::INT* const*)HyprlandAPI::getConfigValue(PHANDLE, "plugin:hyprsplit:num_workspaces")->getDataStaticPtr(); 76 | static auto* const PERSISTENT = (Hyprlang::INT* const*)HyprlandAPI::getConfigValue(PHANDLE, "plugin:hyprsplit:persistent_workspaces")->getDataStaticPtr(); 77 | 78 | for (auto& m : g_pCompositor->m_monitors) { 79 | if (m->m_id == MONITOR_INVALID || m->isMirror()) 80 | continue; 81 | 82 | const int MIN = m->m_id * (**NUMWORKSPACES) + 1; 83 | const int MAX = (m->m_id + 1) * (**NUMWORKSPACES); 84 | 85 | if (m->activeWorkspaceID() < MIN || m->activeWorkspaceID() > MAX) { 86 | Debug::log(LOG, "[hyprsplit] {} {} active workspace {} out of bounds, changing workspace to {}", m->m_name, m->m_id, m->activeWorkspaceID(), MIN); 87 | auto ws = g_pCompositor->getWorkspaceByID(MIN); 88 | 89 | if (!ws) { 90 | ws = g_pCompositor->createNewWorkspace(MIN, m->m_id); 91 | } else if (ws->monitorID() != m->m_id) { 92 | g_pCompositor->moveWorkspaceToMonitor(ws, m); 93 | } 94 | 95 | m->changeWorkspace(ws, false, true, true); 96 | } 97 | } 98 | 99 | for (auto& m : g_pCompositor->m_monitors) { 100 | if (m->m_id == MONITOR_INVALID || m->isMirror()) 101 | continue; 102 | 103 | const int MIN = m->m_id * (**NUMWORKSPACES) + 1; 104 | const int MAX = (m->m_id + 1) * (**NUMWORKSPACES); 105 | 106 | const auto WSSIZE = g_pCompositor->m_workspaces.size(); 107 | for (size_t i = 0; i < WSSIZE; i++) { 108 | const auto& ws = g_pCompositor->m_workspaces[i]; 109 | if (!valid(ws)) 110 | continue; 111 | 112 | if (!**PERSISTENT || !g_pCompositor->getMonitorFromID((ws->m_id - 1) / **NUMWORKSPACES)) 113 | ws->m_persistent = false; 114 | 115 | if (ws->monitorID() != m->m_id && ws->m_id >= MIN && ws->m_id <= MAX) { 116 | Debug::log(LOG, "[hyprsplit] workspace {} on monitor {} move to {} {}", ws->m_id, ws->monitorID(), m->m_name, m->m_id); 117 | g_pCompositor->moveWorkspaceToMonitor(ws, m); 118 | } 119 | } 120 | 121 | if (**PERSISTENT) { 122 | for (auto i = MIN; i <= MAX; i++) { 123 | auto ws = g_pCompositor->getWorkspaceByID(i); 124 | if (!ws) 125 | ws = g_pCompositor->createNewWorkspace(i, m->m_id); 126 | 127 | ws->m_persistent = true; 128 | } 129 | } 130 | } 131 | } 132 | 133 | SDispatchResult focusWorkspace(std::string args) { 134 | const auto PCURRMONITOR = g_pCompositor->m_lastMonitor; 135 | 136 | if (!PCURRMONITOR) { 137 | Debug::log(ERR, "[hyprsplit] focusWorkspace: monitor doesn't exist"); 138 | return {.success = false, .error = "focusWorkspace: monitor doesn't exist"}; 139 | } 140 | 141 | const int WORKSPACEID = getWorkspaceIDNameFromString(getWorkspaceOnCurrentMonitor(args)).id; 142 | 143 | if (WORKSPACEID == WORKSPACE_INVALID) { 144 | Debug::log(ERR, "[hyprsplit] focusWorkspace: invalid workspace"); 145 | return {.success = false, .error = "focusWorkspace: invalid workspace"}; 146 | } 147 | 148 | auto PWORKSPACE = g_pCompositor->getWorkspaceByID(WORKSPACEID); 149 | if (!PWORKSPACE) { 150 | PWORKSPACE = g_pCompositor->createNewWorkspace(WORKSPACEID, PCURRMONITOR->m_id); 151 | g_pKeybindManager->m_dispatchers["workspace"](PWORKSPACE->getConfigName()); 152 | return {}; 153 | } 154 | 155 | static auto* const NUMWORKSPACES = (Hyprlang::INT* const*)HyprlandAPI::getConfigValue(PHANDLE, "plugin:hyprsplit:num_workspaces")->getDataStaticPtr(); 156 | const int MIN = PCURRMONITOR->m_id * (**NUMWORKSPACES) + 1; 157 | const int MAX = (PCURRMONITOR->m_id + 1) * (**NUMWORKSPACES); 158 | if (PWORKSPACE->monitorID() != PCURRMONITOR->m_id && (WORKSPACEID >= MIN && WORKSPACEID <= MAX)) { 159 | Debug::log(WARN, "[hyprsplit] focusWorkspace: workspace exists but is on the wrong monitor?"); 160 | ensureGoodWorkspaces(); 161 | } 162 | g_pKeybindManager->m_dispatchers["workspace"](PWORKSPACE->getConfigName()); 163 | return {}; 164 | } 165 | 166 | SDispatchResult moveToWorkspace(std::string args) { 167 | if (args.contains(',')) 168 | args = getWorkspaceOnCurrentMonitor(args.substr(0, args.find_last_of(','))) + "," + args.substr(args.find_last_of(',') + 1); 169 | else 170 | args = getWorkspaceOnCurrentMonitor(args); 171 | 172 | g_pKeybindManager->m_dispatchers["movetoworkspace"](args); 173 | return {}; 174 | } 175 | 176 | SDispatchResult moveToWorkspaceSilent(std::string args) { 177 | if (args.contains(',')) 178 | args = getWorkspaceOnCurrentMonitor(args.substr(0, args.find_last_of(','))) + "," + args.substr(args.find_last_of(',') + 1); 179 | else 180 | args = getWorkspaceOnCurrentMonitor(args); 181 | 182 | g_pKeybindManager->m_dispatchers["movetoworkspacesilent"](args); 183 | return {}; 184 | } 185 | 186 | SDispatchResult swapActiveWorkspaces(std::string args) { 187 | const auto MON1 = args.substr(0, args.find_first_of(' ')); 188 | const auto MON2 = args.substr(args.find_first_of(' ') + 1); 189 | 190 | const auto PMON1 = g_pCompositor->getMonitorFromString(MON1); 191 | const auto PMON2 = g_pCompositor->getMonitorFromString(MON2); 192 | 193 | if (!PMON1 || !PMON2 || PMON1 == PMON2) 194 | return {}; 195 | 196 | const auto PWORKSPACEA = PMON1->m_activeWorkspace; 197 | const auto PWORKSPACEB = PMON2->m_activeWorkspace; 198 | 199 | if (!PWORKSPACEA || !valid(PWORKSPACEA) || !PWORKSPACEB || !valid(PWORKSPACEB)) 200 | return {}; 201 | 202 | const auto LAYOUTNAME = g_pLayoutManager->m_layouts[g_pLayoutManager->m_currentLayoutID].first; 203 | 204 | // with known layouts, swap the workspaces between monitors, then fix the layout 205 | // with an unknown layout (eg from a plugin) do a "dumb" swap by moving the windows between the workspaces. 206 | if (LAYOUTNAME == "dwindle" || LAYOUTNAME == "master" || LAYOUTNAME == "hy3") { 207 | // proceed as Hyprland normally would (see CCompositor::swapActiveWorkspaces) 208 | PWORKSPACEA->m_monitor = PMON2; 209 | PWORKSPACEA->moveToMonitor(PMON2->m_id); 210 | 211 | for (auto& w :g_pCompositor->m_windows) { 212 | if (w->m_workspace == PWORKSPACEA) { 213 | if (w->m_pinned) { 214 | w->m_workspace = PWORKSPACEB; 215 | continue; 216 | } 217 | 218 | w->m_monitor = PMON2; 219 | 220 | // additionally, move floating and fs windows manually 221 | if (w->m_isFloating) 222 | *w->m_realPosition= w->m_realPosition->goal() - PMON1->m_position + PMON2->m_position; 223 | 224 | if (w->isFullscreen()) { 225 | *w->m_realPosition= PMON2->m_position; 226 | *w->m_realSize = PMON2->m_size; 227 | } 228 | 229 | w->updateToplevel(); 230 | } 231 | } 232 | 233 | PWORKSPACEB->m_monitor = PMON1; 234 | PWORKSPACEB->moveToMonitor(PMON1->m_id); 235 | 236 | for (auto& w :g_pCompositor->m_windows) { 237 | if (w->m_workspace == PWORKSPACEB) { 238 | if (w->m_pinned) { 239 | w->m_workspace = PWORKSPACEA; 240 | continue; 241 | } 242 | 243 | w->m_monitor = PMON1; 244 | 245 | // additionally, move floating and fs windows manually 246 | if (w->m_isFloating) 247 | *w->m_realPosition= w->m_realPosition->goal() - PMON2->m_position + PMON1->m_position; 248 | 249 | if (w->isFullscreen()) { 250 | *w->m_realPosition= PMON1->m_position; 251 | *w->m_realSize = PMON1->m_size; 252 | } 253 | 254 | w->updateToplevel(); 255 | } 256 | } 257 | 258 | PMON1->m_activeWorkspace = PWORKSPACEB; 259 | PMON2->m_activeWorkspace = PWORKSPACEA; 260 | 261 | // swap workspace ids 262 | const auto TMPID = PWORKSPACEA->m_id; 263 | const auto TMPNAME = PWORKSPACEA->m_name; 264 | PWORKSPACEA->m_id = PWORKSPACEB->m_id; 265 | PWORKSPACEA->m_name = PWORKSPACEB->m_name; 266 | PWORKSPACEB->m_id = TMPID; 267 | PWORKSPACEB->m_name = TMPNAME; 268 | 269 | // swap previous workspaces 270 | const auto TMPPREV = PWORKSPACEA->m_prevWorkspace; 271 | PWORKSPACEA->m_prevWorkspace = PWORKSPACEB->m_prevWorkspace; 272 | PWORKSPACEB->m_prevWorkspace = TMPPREV; 273 | 274 | // fix the layout nodes 275 | if (LAYOUTNAME == "dwindle") { 276 | const auto LAYOUT = (CHyprDwindleLayout*)g_pLayoutManager->getCurrentLayout(); 277 | for (auto& n : LAYOUT->m_dwindleNodesData) { 278 | if (n.workspaceID == PWORKSPACEA->m_id) 279 | n.workspaceID = PWORKSPACEB->m_id; 280 | else if (n.workspaceID == PWORKSPACEB->m_id) 281 | n.workspaceID = PWORKSPACEA->m_id; 282 | } 283 | } else if (LAYOUTNAME == "master") { 284 | const auto LAYOUT = (CHyprMasterLayout*)g_pLayoutManager->getCurrentLayout(); 285 | for (auto& n : LAYOUT->m_masterNodesData) { 286 | if (n.workspaceID == PWORKSPACEA->m_id) 287 | n.workspaceID = PWORKSPACEB->m_id; 288 | else if (n.workspaceID == PWORKSPACEB->m_id) 289 | n.workspaceID = PWORKSPACEA->m_id; 290 | } 291 | 292 | const auto WSDATAA = LAYOUT->getMasterWorkspaceData(PWORKSPACEA->m_id); 293 | const auto WSDATAB = LAYOUT->getMasterWorkspaceData(PWORKSPACEB->m_id); 294 | 295 | WSDATAA->workspaceID = PWORKSPACEB->m_id; 296 | WSDATAB->workspaceID = PWORKSPACEA->m_id; 297 | } 298 | 299 | // recalc layout 300 | g_pLayoutManager->getCurrentLayout()->recalculateMonitor(PMON1->m_id); 301 | g_pLayoutManager->getCurrentLayout()->recalculateMonitor(PMON2->m_id); 302 | 303 | g_pCompositor->updateFullscreenFadeOnWorkspace(PWORKSPACEA); 304 | g_pCompositor->updateFullscreenFadeOnWorkspace(PWORKSPACEB); 305 | 306 | // instead of moveworkspace events, we should send movewindow events 307 | for (auto& w :g_pCompositor->m_windows) { 308 | if (w->workspaceID() == PWORKSPACEA->m_id) { 309 | g_pEventManager->postEvent(SHyprIPCEvent{"movewindow", std::format("{:x},{}", (uintptr_t)w.get(), PWORKSPACEA->m_name)}); 310 | g_pEventManager->postEvent(SHyprIPCEvent{"movewindowv2", std::format("{:x},{},{}", (uintptr_t)w.get(), PWORKSPACEA->m_id, PWORKSPACEA->m_name)}); 311 | EMIT_HOOK_EVENT("moveWindow", (std::vector{w, PWORKSPACEA})); 312 | } else if (w->workspaceID() == PWORKSPACEB->m_id) { 313 | g_pEventManager->postEvent(SHyprIPCEvent{"movewindow", std::format("{:x},{}", (uintptr_t)w.get(), PWORKSPACEB->m_name)}); 314 | g_pEventManager->postEvent(SHyprIPCEvent{"movewindowv2", std::format("{:x},{},{}", (uintptr_t)w.get(), PWORKSPACEB->m_id, PWORKSPACEB->m_name)}); 315 | EMIT_HOOK_EVENT("moveWindow", (std::vector{w, PWORKSPACEB})); 316 | } 317 | } 318 | } else { 319 | // unknown layout. move all windows without preserving layout 320 | std::vector windowsA; 321 | std::vector windowsB; 322 | 323 | for (auto& w :g_pCompositor->m_windows) { 324 | if (w->workspaceID() == PWORKSPACEA->m_id) { 325 | windowsA.push_back(w); 326 | } 327 | if (w->workspaceID() == PWORKSPACEB->m_id) { 328 | windowsB.push_back(w); 329 | } 330 | } 331 | 332 | for (auto& w : windowsA) { 333 | g_pCompositor->moveWindowToWorkspaceSafe(w, PWORKSPACEB); 334 | } 335 | for (auto& w : windowsB) { 336 | g_pCompositor->moveWindowToWorkspaceSafe(w, PWORKSPACEA); 337 | } 338 | } 339 | 340 | g_pInputManager->refocus(); 341 | return {}; 342 | } 343 | 344 | SDispatchResult grabRogueWindows(std::string args) { 345 | const auto PWORKSPACE = g_pCompositor->m_lastMonitor->m_activeWorkspace; 346 | 347 | if (!PWORKSPACE) { 348 | Debug::log(ERR, "[hyprsplit] no active workspace?"); 349 | return {.success = false, .error = "no active workspace?"}; 350 | } 351 | 352 | static auto* const NUMWORKSPACES = (Hyprlang::INT* const*)HyprlandAPI::getConfigValue(PHANDLE, "plugin:hyprsplit:num_workspaces")->getDataStaticPtr(); 353 | 354 | for (auto& w :g_pCompositor->m_windows) { 355 | if (!w->m_isMapped || w->onSpecialWorkspace()) 356 | continue; 357 | 358 | bool inGoodWorkspace = false; 359 | 360 | for (auto& m : g_pCompositor->m_monitors) { 361 | const int MIN = m->m_id * (**NUMWORKSPACES) + 1; 362 | const int MAX = (m->m_id + 1) * (**NUMWORKSPACES); 363 | 364 | if (w->workspaceID() >= MIN && w->workspaceID() <= MAX) { 365 | inGoodWorkspace = true; 366 | break; 367 | } 368 | } 369 | 370 | if (!inGoodWorkspace) { 371 | Debug::log(LOG, "[hyprsplit] moving window {} to workspace {}", w->m_title, PWORKSPACE->m_id); 372 | const auto args = std::format("{},address:0x{:x}", PWORKSPACE->m_id, (uintptr_t)w.get()); 373 | g_pKeybindManager->m_dispatchers["movetoworkspacesilent"](args); 374 | } 375 | } 376 | return {}; 377 | } 378 | 379 | void onMonitorAdded(PHLMONITOR pMonitor) { 380 | Debug::log(LOG, "[hyprsplit] monitor added {}", pMonitor->m_name); 381 | 382 | ensureGoodWorkspaces(); 383 | } 384 | 385 | void onMonitorRemoved(PHLMONITOR pMonitor) { 386 | Debug::log(LOG, "[hyprsplit] monitor removed {}", pMonitor->m_name); 387 | 388 | static auto* const NUMWORKSPACES = (Hyprlang::INT* const*)HyprlandAPI::getConfigValue(PHANDLE, "plugin:hyprsplit:num_workspaces")->getDataStaticPtr(); 389 | static auto* const PERSISTENT = (Hyprlang::INT* const*)HyprlandAPI::getConfigValue(PHANDLE, "plugin:hyprsplit:persistent_workspaces")->getDataStaticPtr(); 390 | 391 | if (**PERSISTENT) { 392 | const int MIN = pMonitor->m_id * (**NUMWORKSPACES) + 1; 393 | const int MAX = (pMonitor->m_id + 1) * (**NUMWORKSPACES); 394 | 395 | const auto WSSIZE = g_pCompositor->m_workspaces.size(); 396 | for (size_t i = 0; i < WSSIZE; i++) { 397 | const auto& ws = g_pCompositor->m_workspaces[i]; 398 | if (!valid(ws)) 399 | continue; 400 | 401 | if (ws->m_id >= MIN && ws->m_id <= MAX) 402 | ws->m_persistent = false; 403 | } 404 | } 405 | } 406 | 407 | // other plugins can use this to convert a regular hyprland workspace string the correct hyprsplit one 408 | APICALL EXPORT std::string hyprsplitGetWorkspace(const std::string& workspace) { 409 | return getWorkspaceOnCurrentMonitor(workspace); 410 | } 411 | 412 | // Do NOT change this function. 413 | APICALL EXPORT std::string PLUGIN_API_VERSION() { 414 | return HYPRLAND_API_VERSION; 415 | } 416 | 417 | APICALL EXPORT PLUGIN_DESCRIPTION_INFO PLUGIN_INIT(HANDLE handle) { 418 | PHANDLE = handle; 419 | 420 | const std::string HASH = __hyprland_api_get_hash(); 421 | 422 | if (HASH != GIT_COMMIT_HASH) { 423 | HyprlandAPI::addNotification(PHANDLE, 424 | "[hyprsplit] Failure in initialization: Version mismatch (headers " 425 | "ver is not equal to running hyprland ver)", 426 | CHyprColor{1.0, 0.2, 0.2, 1.0}, 5000); 427 | throw std::runtime_error("[hyprsplit] Version mismatch"); 428 | } 429 | 430 | HyprlandAPI::addConfigValue(PHANDLE, "plugin:hyprsplit:num_workspaces", Hyprlang::INT{10}); 431 | HyprlandAPI::addConfigValue(PHANDLE, "plugin:hyprsplit:persistent_workspaces", Hyprlang::INT{0}); 432 | 433 | HyprlandAPI::addDispatcherV2(PHANDLE, "split:workspace", focusWorkspace); 434 | HyprlandAPI::addDispatcherV2(PHANDLE, "split:movetoworkspace", moveToWorkspace); 435 | HyprlandAPI::addDispatcherV2(PHANDLE, "split:movetoworkspacesilent", moveToWorkspaceSilent); 436 | HyprlandAPI::addDispatcherV2(PHANDLE, "split:swapactiveworkspaces", swapActiveWorkspaces); 437 | HyprlandAPI::addDispatcherV2(PHANDLE, "split:grabroguewindows", grabRogueWindows); 438 | 439 | static auto monitorAddedHook = 440 | HyprlandAPI::registerCallbackDynamic(PHANDLE, "monitorAdded", [&](void* self, SCallbackInfo& info, std::any data) { onMonitorAdded(std::any_cast(data)); }); 441 | static auto monitorRemovedHook = 442 | HyprlandAPI::registerCallbackDynamic(PHANDLE, "monitorRemoved", [&](void* self, SCallbackInfo& info, std::any data) { onMonitorRemoved(std::any_cast(data)); }); 443 | static auto configReloadedHook = 444 | HyprlandAPI::registerCallbackDynamic(PHANDLE, "configReloaded", [&](void* self, SCallbackInfo& info, std::any data) { ensureGoodWorkspaces(); }); 445 | 446 | HyprlandAPI::reloadConfig(); 447 | 448 | Debug::log(LOG, "[hyprsplit] plugin init"); 449 | return {"hyprsplit", "split monitor workspaces", "shezdy", "1.0"}; 450 | } 451 | 452 | APICALL EXPORT void PLUGIN_EXIT() { 453 | Debug::log(LOG, "[hyprsplit] plugin exit"); 454 | 455 | static auto* const PERSISTENT = (Hyprlang::INT* const*)HyprlandAPI::getConfigValue(PHANDLE, "plugin:hyprsplit:persistent_workspaces")->getDataStaticPtr(); 456 | if (**PERSISTENT) { 457 | const auto WSSIZE = g_pCompositor->m_workspaces.size(); 458 | for (size_t i = 0; i < WSSIZE; i++) { 459 | const auto& ws = g_pCompositor->m_workspaces[i]; 460 | if (!valid(ws)) 461 | continue; 462 | ws->m_persistent = false; 463 | } 464 | } 465 | } 466 | --------------------------------------------------------------------------------