diff --git a/README.md b/README.md index d8f5e9d..e499002 100644 --- a/README.md +++ b/README.md @@ -13,6 +13,7 @@ plugin { layout { orientation=left new_on_top=0 + new_near_focused=1 new_is_master=1 no_gaps_when_only=0 special_scale_factor=0.8 @@ -38,9 +39,10 @@ All configuration variables are also usable as workspace rule layout options. Ju # Dispatchers -Two new dispatchers +New dispatchers * `resetsplits` Reset all the window splits to default sizes. * `setstackcount` Change the number of stacks for the current workspace. Windows will be re-tiled to fit the new stack count. + * `swapdirection [noloop]` Swap the focused tiled window directionally, wrapping at workspace edges unless `noloop` is set. Two new-ish orientations * `orientationhcenter` Master is horizontally centered with stacks to the left and right. diff --git a/flake.lock b/flake.lock new file mode 100644 index 0000000..80c4bc6 --- /dev/null +++ b/flake.lock @@ -0,0 +1,501 @@ +{ + "nodes": { + "aquamarine": { + "inputs": { + "hyprutils": [ + "hyprland", + "hyprutils" + ], + "hyprwayland-scanner": [ + "hyprland", + "hyprwayland-scanner" + ], + "nixpkgs": [ + "hyprland", + "nixpkgs" + ], + "systems": [ + "hyprland", + "systems" + ] + }, + "locked": { + "lastModified": 1776876344, + "narHash": "sha256-Ubqb/agkuMJK+k19gjQgHux/eOYRc1sRGoOZOho8+VY=", + "owner": "hyprwm", + "repo": "aquamarine", + "rev": "648a13d0ee1e03a843b3e145b8ece15393058701", + "type": "github" + }, + "original": { + "owner": "hyprwm", + "repo": "aquamarine", + "type": "github" + } + }, + "flake-compat": { + "flake": false, + "locked": { + "lastModified": 1767039857, + "narHash": "sha256-vNpUSpF5Nuw8xvDLj2KCwwksIbjua2LZCqhV1LNRDns=", + "owner": "NixOS", + "repo": "flake-compat", + "rev": "5edf11c44bc78a0d334f6334cdaf7d60d732daab", + "type": "github" + }, + "original": { + "owner": "NixOS", + "repo": "flake-compat", + "type": "github" + } + }, + "gitignore": { + "inputs": { + "nixpkgs": [ + "hyprland", + "pre-commit-hooks", + "nixpkgs" + ] + }, + "locked": { + "lastModified": 1709087332, + "narHash": "sha256-HG2cCnktfHsKV0s4XW83gU3F57gaTljL9KNSuG6bnQs=", + "owner": "hercules-ci", + "repo": "gitignore.nix", + "rev": "637db329424fd7e46cf4185293b9cc8c88c95394", + "type": "github" + }, + "original": { + "owner": "hercules-ci", + "repo": "gitignore.nix", + "type": "github" + } + }, + "hyprcursor": { + "inputs": { + "hyprlang": [ + "hyprland", + "hyprlang" + ], + "nixpkgs": [ + "hyprland", + "nixpkgs" + ], + "systems": [ + "hyprland", + "systems" + ] + }, + "locked": { + "lastModified": 1776511930, + "narHash": "sha256-fCpwFiTW0rT7oKJqr3cqHMnkwypSwQKpbtUEtxdkgrM=", + "owner": "hyprwm", + "repo": "hyprcursor", + "rev": "39435900785d0c560c6ae8777d29f28617d031ef", + "type": "github" + }, + "original": { + "owner": "hyprwm", + "repo": "hyprcursor", + "type": "github" + } + }, + "hyprgraphics": { + "inputs": { + "hyprutils": [ + "hyprland", + "hyprutils" + ], + "nixpkgs": [ + "hyprland", + "nixpkgs" + ], + "systems": [ + "hyprland", + "systems" + ] + }, + "locked": { + "lastModified": 1776426399, + "narHash": "sha256-RUESLKNikIeEq9ymGJ6nmcDXiSFQpUW1IhJ245nL3xM=", + "owner": "hyprwm", + "repo": "hyprgraphics", + "rev": "68d064434787cf1ed4a2fe257c03c5f52f33cf84", + "type": "github" + }, + "original": { + "owner": "hyprwm", + "repo": "hyprgraphics", + "type": "github" + } + }, + "hyprland": { + "inputs": { + "aquamarine": "aquamarine", + "hyprcursor": "hyprcursor", + "hyprgraphics": "hyprgraphics", + "hyprland-guiutils": "hyprland-guiutils", + "hyprland-protocols": "hyprland-protocols", + "hyprlang": "hyprlang", + "hyprutils": "hyprutils", + "hyprwayland-scanner": "hyprwayland-scanner", + "hyprwire": "hyprwire", + "nixpkgs": [ + "nixpkgs" + ], + "pre-commit-hooks": "pre-commit-hooks", + "systems": "systems", + "xdph": "xdph" + }, + "locked": { + "lastModified": 1777497195, + "narHash": "sha256-I9z3somAUJ5jGwv4ZBjADco8CmdglEUqYs0AgJ5LSDs=", + "ref": "refs/heads/main", + "rev": "56d7a43102b53f79a2311662783d5dd94cb2f1a5", + "revCount": 7215, + "submodules": true, + "type": "git", + "url": "https://github.com/hyprwm/Hyprland" + }, + "original": { + "submodules": true, + "type": "git", + "url": "https://github.com/hyprwm/Hyprland" + } + }, + "hyprland-guiutils": { + "inputs": { + "aquamarine": [ + "hyprland", + "aquamarine" + ], + "hyprgraphics": [ + "hyprland", + "hyprgraphics" + ], + "hyprlang": [ + "hyprland", + "hyprlang" + ], + "hyprtoolkit": "hyprtoolkit", + "hyprutils": [ + "hyprland", + "hyprutils" + ], + "hyprwayland-scanner": [ + "hyprland", + "hyprwayland-scanner" + ], + "nixpkgs": [ + "hyprland", + "nixpkgs" + ], + "systems": [ + "hyprland", + "systems" + ] + }, + "locked": { + "lastModified": 1776426575, + "narHash": "sha256-KI6nIfVihn/DPaeB5Et46Xg3dkNHrrEtUd5LBBVomB0=", + "owner": "hyprwm", + "repo": "hyprland-guiutils", + "rev": "a968d211048e3ed538e47b84cb3649299578f19d", + "type": "github" + }, + "original": { + "owner": "hyprwm", + "repo": "hyprland-guiutils", + "type": "github" + } + }, + "hyprland-protocols": { + "inputs": { + "nixpkgs": [ + "hyprland", + "nixpkgs" + ], + "systems": [ + "hyprland", + "systems" + ] + }, + "locked": { + "lastModified": 1772460177, + "narHash": "sha256-/6G/MsPvtn7bc4Y32pserBT/Z4SUUdBd4XYJpOEKVR4=", + "owner": "hyprwm", + "repo": "hyprland-protocols", + "rev": "1cb6db5fd6bb8aee419f4457402fa18293ace917", + "type": "github" + }, + "original": { + "owner": "hyprwm", + "repo": "hyprland-protocols", + "type": "github" + } + }, + "hyprlang": { + "inputs": { + "hyprutils": [ + "hyprland", + "hyprutils" + ], + "nixpkgs": [ + "hyprland", + "nixpkgs" + ], + "systems": [ + "hyprland", + "systems" + ] + }, + "locked": { + "lastModified": 1776426736, + "narHash": "sha256-rl7i4aY+9p8LysJp7o8uRWahCkpFznCgGHXszlTw7b0=", + "owner": "hyprwm", + "repo": "hyprlang", + "rev": "7833ff33b2e82d3406337b5dcf0d1cec595d83e9", + "type": "github" + }, + "original": { + "owner": "hyprwm", + "repo": "hyprlang", + "type": "github" + } + }, + "hyprtoolkit": { + "inputs": { + "aquamarine": [ + "hyprland", + "hyprland-guiutils", + "aquamarine" + ], + "hyprgraphics": [ + "hyprland", + "hyprland-guiutils", + "hyprgraphics" + ], + "hyprlang": [ + "hyprland", + "hyprland-guiutils", + "hyprlang" + ], + "hyprutils": [ + "hyprland", + "hyprland-guiutils", + "hyprutils" + ], + "hyprwayland-scanner": [ + "hyprland", + "hyprland-guiutils", + "hyprwayland-scanner" + ], + "nixpkgs": [ + "hyprland", + "hyprland-guiutils", + "nixpkgs" + ], + "systems": [ + "hyprland", + "hyprland-guiutils", + "systems" + ] + }, + "locked": { + "lastModified": 1772462885, + "narHash": "sha256-5pHXrQK9zasMnIo6yME6EOXmWGFMSnCITcfKshhKJ9I=", + "owner": "hyprwm", + "repo": "hyprtoolkit", + "rev": "9af245a69fa6b286b88ddfc340afd288e00a6998", + "type": "github" + }, + "original": { + "owner": "hyprwm", + "repo": "hyprtoolkit", + "type": "github" + } + }, + "hyprutils": { + "inputs": { + "nixpkgs": [ + "hyprland", + "nixpkgs" + ], + "systems": [ + "hyprland", + "systems" + ] + }, + "locked": { + "lastModified": 1777492286, + "narHash": "sha256-PwuoEJQcjSKJNP5T55qhfDwIP0tw5zxEhfu8GDfKfeg=", + "owner": "hyprwm", + "repo": "hyprutils", + "rev": "ec5c0c709706bad5b82f667fd8758eae442577ce", + "type": "github" + }, + "original": { + "owner": "hyprwm", + "repo": "hyprutils", + "type": "github" + } + }, + "hyprwayland-scanner": { + "inputs": { + "nixpkgs": [ + "hyprland", + "nixpkgs" + ], + "systems": [ + "hyprland", + "systems" + ] + }, + "locked": { + "lastModified": 1777148232, + "narHash": "sha256-Uv0WZLhu89SafuSOmYDA7akrPt4wBRmsa1ucasO5aXg=", + "owner": "hyprwm", + "repo": "hyprwayland-scanner", + "rev": "fec9cf1abcc1011e46f0a0986f46bf93c6bf8b92", + "type": "github" + }, + "original": { + "owner": "hyprwm", + "repo": "hyprwayland-scanner", + "type": "github" + } + }, + "hyprwire": { + "inputs": { + "hyprutils": [ + "hyprland", + "hyprutils" + ], + "nixpkgs": [ + "hyprland", + "nixpkgs" + ], + "systems": [ + "hyprland", + "systems" + ] + }, + "locked": { + "lastModified": 1776728575, + "narHash": "sha256-z9eGphrArEBpl1O/GCH0wlY6z4K9vA6yWh2gAS6qytU=", + "owner": "hyprwm", + "repo": "hyprwire", + "rev": "f3a80888783702a39691b684d099e16b83ed4702", + "type": "github" + }, + "original": { + "owner": "hyprwm", + "repo": "hyprwire", + "type": "github" + } + }, + "nixpkgs": { + "locked": { + "lastModified": 1776877367, + "narHash": "sha256-EHq1/OX139R1RvBzOJ0aMRT3xnWyqtHBRUBuO1gFzjI=", + "owner": "NixOS", + "repo": "nixpkgs", + "rev": "0726a0ecb6d4e08f6adced58726b95db924cef57", + "type": "github" + }, + "original": { + "owner": "NixOS", + "ref": "nixos-unstable", + "repo": "nixpkgs", + "type": "github" + } + }, + "pre-commit-hooks": { + "inputs": { + "flake-compat": "flake-compat", + "gitignore": "gitignore", + "nixpkgs": [ + "hyprland", + "nixpkgs" + ] + }, + "locked": { + "lastModified": 1776796298, + "narHash": "sha256-PcRvlWayisPSjd0UcRQbhG8Oqw78AcPE6x872cPRHN8=", + "owner": "cachix", + "repo": "git-hooks.nix", + "rev": "3cfd774b0a530725a077e17354fbdb87ea1c4aad", + "type": "github" + }, + "original": { + "owner": "cachix", + "repo": "git-hooks.nix", + "type": "github" + } + }, + "root": { + "inputs": { + "hyprland": "hyprland", + "nixpkgs": "nixpkgs" + } + }, + "systems": { + "locked": { + "lastModified": 1689347949, + "narHash": "sha256-12tWmuL2zgBgZkdoB6qXZsgJEH9LR3oUgpaQq2RbI80=", + "owner": "nix-systems", + "repo": "default-linux", + "rev": "31732fcf5e8fea42e59c2488ad31a0e651500f68", + "type": "github" + }, + "original": { + "owner": "nix-systems", + "repo": "default-linux", + "type": "github" + } + }, + "xdph": { + "inputs": { + "hyprland-protocols": [ + "hyprland", + "hyprland-protocols" + ], + "hyprlang": [ + "hyprland", + "hyprlang" + ], + "hyprutils": [ + "hyprland", + "hyprutils" + ], + "hyprwayland-scanner": [ + "hyprland", + "hyprwayland-scanner" + ], + "nixpkgs": [ + "hyprland", + "nixpkgs" + ], + "systems": [ + "hyprland", + "systems" + ] + }, + "locked": { + "lastModified": 1777035886, + "narHash": "sha256-m1TNuBoSXUBSKhD9UVMkU90M0wFTPTfvIOOltO8IM8A=", + "owner": "hyprwm", + "repo": "xdg-desktop-portal-hyprland", + "rev": "ecfcdcc781f48821d83e1e2a0e30d7beca0eeb5e", + "type": "github" + }, + "original": { + "owner": "hyprwm", + "repo": "xdg-desktop-portal-hyprland", + "type": "github" + } + } + }, + "root": "root", + "version": 7 +} diff --git a/flake.nix b/flake.nix new file mode 100644 index 0000000..5cbbb66 --- /dev/null +++ b/flake.nix @@ -0,0 +1,81 @@ +{ + description = "N-stack layout plugin for Hyprland"; + + inputs = { + nixpkgs.url = "github:NixOS/nixpkgs/nixos-unstable"; + hyprland = { + url = "git+https://github.com/hyprwm/Hyprland?submodules=1"; + inputs.nixpkgs.follows = "nixpkgs"; + }; + }; + + outputs = { + self, + nixpkgs, + hyprland, + }: + let + systems = [ + "aarch64-linux" + "x86_64-linux" + ]; + + forAllSystems = nixpkgs.lib.genAttrs systems; + in { + packages = forAllSystems (system: + let + pkgs = nixpkgs.legacyPackages.${system}; + hyprlandPkg = hyprland.packages.${system}.hyprland; + hyprNStack = pkgs.stdenv.mkDerivation { + pname = "hyprNStack"; + version = "0-unstable-${self.shortRev or "dirty"}"; + + src = self; + + strictDeps = true; + nativeBuildInputs = [ + pkgs.pkg-config + ]; + buildInputs = [ + hyprlandPkg + ] ++ hyprlandPkg.buildInputs; + + dontStrip = true; + + installPhase = '' + runHook preInstall + + install -Dm755 nstackLayoutPlugin.so \ + "$out/lib/libhyprNStack.so" + + runHook postInstall + ''; + + meta = { + description = "N-stack layout plugin for Hyprland"; + homepage = "https://github.com/zakk4223/hyprNStack"; + license = pkgs.lib.licenses.bsd3; + platforms = hyprlandPkg.meta.platforms; + }; + }; + in { + default = hyprNStack; + hyprNStack = hyprNStack; + }); + + devShells = forAllSystems (system: + let + pkgs = nixpkgs.legacyPackages.${system}; + hyprlandPkg = hyprland.packages.${system}.hyprland; + in { + default = pkgs.mkShell { + nativeBuildInputs = [ + pkgs.pkg-config + ]; + buildInputs = [ + hyprlandPkg + ] ++ hyprlandPkg.buildInputs; + }; + }); + }; +} diff --git a/main.cpp b/main.cpp index f523aa2..bb4bf1f 100644 --- a/main.cpp +++ b/main.cpp @@ -1,5 +1,8 @@ #include "globals.hpp" #include +#include +#include +#include #include #include #include "nstackLayout.hpp" @@ -16,21 +19,21 @@ APICALL EXPORT std::string PLUGIN_API_VERSION() { APICALL EXPORT PLUGIN_DESCRIPTION_INFO PLUGIN_INIT(HANDLE handle) { PHANDLE = handle; - HyprlandAPI::addConfigValue(PHANDLE, "plugin:nstack:layout:orientation", Hyprlang::STRING{"left"}); - HyprlandAPI::addConfigValue(PHANDLE, "plugin:nstack:layout:new_on_top", Hyprlang::INT{0}); - HyprlandAPI::addConfigValue(PHANDLE, "plugin:nstack:layout:new_is_master", Hyprlang::INT{1}); - HyprlandAPI::addConfigValue(PHANDLE, "plugin:nstack:layout:no_gaps_when_only", Hyprlang::INT{0}); - HyprlandAPI::addConfigValue(PHANDLE, "plugin:nstack:layout:special_scale_factor", Hyprlang::FLOAT{0.8f}); - HyprlandAPI::addConfigValue(PHANDLE, "plugin:nstack:layout:inherit_fullscreen", Hyprlang::INT{1}); - HyprlandAPI::addConfigValue(PHANDLE, "plugin:nstack:layout:stacks", Hyprlang::INT{2}); - HyprlandAPI::addConfigValue(PHANDLE, "plugin:nstack:layout:center_single_master", Hyprlang::INT{0}); - HyprlandAPI::addConfigValue(PHANDLE, "plugin:nstack:layout:mfact", Hyprlang::FLOAT{0.5f}); - HyprlandAPI::addConfigValue(PHANDLE, "plugin:nstack:layout:single_mfact", Hyprlang::FLOAT{0.5f}); + HyprlandAPI::addConfigValueV2(PHANDLE, makeShared("plugin:nstack:layout:orientation", "orientation", "left")); + HyprlandAPI::addConfigValueV2(PHANDLE, makeShared("plugin:nstack:layout:new_on_top", "new on top", 0)); + HyprlandAPI::addConfigValueV2(PHANDLE, makeShared("plugin:nstack:layout:new_near_focused", "new near focused", 1)); + HyprlandAPI::addConfigValueV2(PHANDLE, makeShared("plugin:nstack:layout:new_is_master", "new is master", 0)); + HyprlandAPI::addConfigValueV2(PHANDLE, makeShared("plugin:nstack:layout:no_gaps_when_only", "no gaps when only", 1)); + HyprlandAPI::addConfigValueV2(PHANDLE, makeShared("plugin:nstack:layout:special_scale_factor", "special scale factor", 0.8f)); + HyprlandAPI::addConfigValueV2(PHANDLE, makeShared("plugin:nstack:layout:inherit_fullscreen", "inherit fullscreen", 1)); + HyprlandAPI::addConfigValueV2(PHANDLE, makeShared("plugin:nstack:layout:stacks", "stacks", 2)); + HyprlandAPI::addConfigValueV2(PHANDLE, makeShared("plugin:nstack:layout:center_single_master", "center single master", 0)); + HyprlandAPI::addConfigValueV2(PHANDLE, makeShared("plugin:nstack:layout:mfact", "master factor", 0.0f)); + HyprlandAPI::addConfigValueV2(PHANDLE, makeShared("plugin:nstack:layout:single_mfact", "single master factor", 1.0f)); g_pNstackLayout = std::make_unique(); if (!HyprlandAPI::addTiledAlgo(PHANDLE, "nStack", &typeid(Layout::Tiled::CHyprNstackAlgorithm), [] { return makeUnique(); })) { HyprlandAPI::addNotification(PHANDLE, "[hyprgollum] addTiledAlgo failed! Can't proceed.", CHyprColor{1.0, 0.2, 0.2, 1.0}, 5000); } - HyprlandAPI::reloadConfig(); return {"hyprNStack", "Plugin for column layout", "Zakk", "1.0"}; } diff --git a/nstackLayout.cpp b/nstackLayout.cpp index 066da8b..0c77765 100644 --- a/nstackLayout.cpp +++ b/nstackLayout.cpp @@ -1,4 +1,6 @@ #include "nstackLayout.hpp" +#include +#include #include #include #include @@ -18,6 +20,7 @@ #include #include #include +#include @@ -25,6 +28,75 @@ using namespace Layout; using namespace Layout::Tiled; +static std::optional nstackConfigStringToInt(const std::string& value) { + if (value == "true" || value == "on" || value == "yes") + return 1; + if (value == "false" || value == "off" || value == "no") + return 0; + + try { + return std::stoll(value); + } catch (std::exception& e) { + Log::logger->log(Log::ERR, "Nstack layoutopt integer format error for '{}': {}", value, e.what()); + return std::nullopt; + } +} + +static const CConfigValue& nstackOrientation() { + static const CConfigValue value("plugin:nstack:layout:orientation"); + return value; +} + +static const CConfigValue& nstackStacks() { + static const CConfigValue value("plugin:nstack:layout:stacks"); + return value; +} + +static const CConfigValue& nstackMasterFactor() { + static const CConfigValue value("plugin:nstack:layout:mfact"); + return value; +} + +static const CConfigValue& nstackSingleMasterFactor() { + static const CConfigValue value("plugin:nstack:layout:single_mfact"); + return value; +} + +static const CConfigValue& nstackSpecialScaleFactor() { + static const CConfigValue value("plugin:nstack:layout:special_scale_factor"); + return value; +} + +static const CConfigValue& nstackNewOnTop() { + static const CConfigValue value("plugin:nstack:layout:new_on_top"); + return value; +} + +static const CConfigValue& nstackNewNearFocused() { + static const CConfigValue value("plugin:nstack:layout:new_near_focused"); + return value; +} + +static const CConfigValue& nstackNewIsMaster() { + static const CConfigValue value("plugin:nstack:layout:new_is_master"); + return value; +} + +static const CConfigValue& nstackNoGapsWhenOnly() { + static const CConfigValue value("plugin:nstack:layout:no_gaps_when_only"); + return value; +} + +static const CConfigValue& nstackInheritFullscreen() { + static const CConfigValue value("plugin:nstack:layout:inherit_fullscreen"); + return value; +} + +static const CConfigValue& nstackCenterSingleMaster() { + static const CConfigValue value("plugin:nstack:layout:center_single_master"); + return value; +} + SP CHyprNstackAlgorithm::getNodeFromTarget(SP x) { for (auto& nd : m_lMasterNodesData) { if (nd->pTarget == x) @@ -49,11 +121,11 @@ int CHyprNstackAlgorithm::getMastersCount() { void CHyprNstackAlgorithm::applyWorkspaceLayoutOptions() { - const auto wsrule = g_pConfigManager->getWorkspaceRuleFor(m_parent->space()->workspace()); - const auto wslayoutopts = wsrule.layoutopts; + const auto wsrule = Config::workspaceRuleMgr()->getWorkspaceRuleFor(m_parent->space()->workspace()); + static const std::map EMPTY_LAYOUT_OPTS; + const auto& wslayoutopts = wsrule ? wsrule->m_layoutopts : EMPTY_LAYOUT_OPTS; - static auto* const orientation = (Hyprlang::STRING const*)HyprlandAPI::getConfigValue(PHANDLE, "plugin:nstack:layout:orientation")->getDataStaticPtr(); - std::string wsorientation = *orientation; + std::string wsorientation = *nstackOrientation(); if (wslayoutopts.contains("nstack-orientation")) wsorientation = wslayoutopts.at("nstack-orientation"); @@ -74,8 +146,7 @@ void CHyprNstackAlgorithm::applyWorkspaceLayoutOptions() { m_workspaceData.orientation = NSTACK_ORIENTATION_HCENTER; } - static auto* const NUMSTACKS = (Hyprlang::INT* const*)HyprlandAPI::getConfigValue(PHANDLE, "plugin:nstack:layout:stacks")->getDataStaticPtr(); - auto wsstacks = **NUMSTACKS; + auto wsstacks = *nstackStacks(); if (wslayoutopts.contains("nstack-stacks")) { try { @@ -87,8 +158,7 @@ void CHyprNstackAlgorithm::applyWorkspaceLayoutOptions() { m_workspaceData.m_iStackCount = wsstacks; } - static auto* const MFACT = (Hyprlang::FLOAT* const*)HyprlandAPI::getConfigValue(PHANDLE, "plugin:nstack:layout:mfact")->getDataStaticPtr(); - auto wsmfact = **MFACT; + auto wsmfact = *nstackMasterFactor(); if (wslayoutopts.contains("nstack-mfact")) { std::string mfactstr = wslayoutopts.at("nstack-mfact"); try { @@ -97,8 +167,7 @@ void CHyprNstackAlgorithm::applyWorkspaceLayoutOptions() { } m_workspaceData.master_factor = wsmfact; - static auto* const SMFACT = (Hyprlang::FLOAT* const*)HyprlandAPI::getConfigValue(PHANDLE, "plugin:nstack:layout:single_mfact")->getDataStaticPtr(); - auto wssmfact = **SMFACT; + auto wssmfact = *nstackSingleMasterFactor(); if (wslayoutopts.contains("nstack-single_mfact")) { std::string smfactstr = wslayoutopts.at("nstack-single_mfact"); @@ -108,8 +177,7 @@ void CHyprNstackAlgorithm::applyWorkspaceLayoutOptions() { } m_workspaceData.single_master_factor = wssmfact; - static auto* const SSFACT = (Hyprlang::FLOAT* const*)HyprlandAPI::getConfigValue(PHANDLE, "plugin:nstack:layout:special_scale_factor")->getDataStaticPtr(); - auto wsssfact = **SSFACT; + auto wsssfact = *nstackSpecialScaleFactor(); if (wslayoutopts.contains("nstack-special_scale_factor")) { std::string ssfactstr = wslayoutopts.at("nstack-special_scale_factor"); try { @@ -118,34 +186,34 @@ void CHyprNstackAlgorithm::applyWorkspaceLayoutOptions() { } m_workspaceData.special_scale_factor = wsssfact; - static auto* const NEWTOP = (Hyprlang::INT* const*)HyprlandAPI::getConfigValue(PHANDLE, "plugin:nstack:layout:new_on_top")->getDataStaticPtr(); - auto wsnewtop = **NEWTOP; + auto wsnewtop = *nstackNewOnTop(); if (wslayoutopts.contains("nstack-new_on_top")) - wsnewtop = configStringToInt(wslayoutopts.at("nstack-new_on_top")).value_or(0); + wsnewtop = nstackConfigStringToInt(wslayoutopts.at("nstack-new_on_top")).value_or(0); m_workspaceData.new_on_top = wsnewtop; - static auto* const NEWMASTER = (Hyprlang::INT* const*)HyprlandAPI::getConfigValue(PHANDLE, "plugin:nstack:layout:new_is_master")->getDataStaticPtr(); - auto wsnewmaster = **NEWMASTER; + auto wsnewnearfocused = *nstackNewNearFocused(); + if (wslayoutopts.contains("nstack-new_near_focused")) + wsnewnearfocused = nstackConfigStringToInt(wslayoutopts.at("nstack-new_near_focused")).value_or(0); + m_workspaceData.new_near_focused = wsnewnearfocused; + + auto wsnewmaster = *nstackNewIsMaster(); if (wslayoutopts.contains("nstack-new_is_master")) - wsnewmaster = configStringToInt(wslayoutopts.at("nstack-new_is_master")).value_or(0); + wsnewmaster = nstackConfigStringToInt(wslayoutopts.at("nstack-new_is_master")).value_or(0); m_workspaceData.new_is_master = wsnewmaster; - static auto* const NGWO = (Hyprlang::INT* const*)HyprlandAPI::getConfigValue(PHANDLE, "plugin:nstack:layout:no_gaps_when_only")->getDataStaticPtr(); - auto wsngwo = **NGWO; + auto wsngwo = *nstackNoGapsWhenOnly(); if (wslayoutopts.contains("nstack-no_gaps_when_only")) - wsngwo = configStringToInt(wslayoutopts.at("nstack-no_gaps_when_only")).value_or(0); + wsngwo = nstackConfigStringToInt(wslayoutopts.at("nstack-no_gaps_when_only")).value_or(0); m_workspaceData.no_gaps_when_only = wsngwo; - static auto* const INHERITFS = (Hyprlang::INT* const*)HyprlandAPI::getConfigValue(PHANDLE, "plugin:nstack:layout:inherit_fullscreen")->getDataStaticPtr(); - auto wsinheritfs = **INHERITFS; + auto wsinheritfs = *nstackInheritFullscreen(); if (wslayoutopts.contains("nstack-inherit_fullscreen")) - wsinheritfs = configStringToInt(wslayoutopts.at("nstack-inherit_fullscreen")).value_or(0); + wsinheritfs = nstackConfigStringToInt(wslayoutopts.at("nstack-inherit_fullscreen")).value_or(0); m_workspaceData.inherit_fullscreen = wsinheritfs; - static auto* const CENTERSM = (Hyprlang::INT* const*)HyprlandAPI::getConfigValue(PHANDLE, "plugin:nstack:layout:center_single_master")->getDataStaticPtr(); - auto wscentersm = **CENTERSM; + auto wscentersm = *nstackCenterSingleMaster(); if (wslayoutopts.contains("nstack-center_single_master")) - wscentersm = configStringToInt(wslayoutopts.at("nstack-center_single_master")).value_or(0); + wscentersm = nstackConfigStringToInt(wslayoutopts.at("nstack-center_single_master")).value_or(0); m_workspaceData.center_single_master = wscentersm; } @@ -169,7 +237,10 @@ void CHyprNstackAlgorithm::newTarget(SP target) { } void CHyprNstackAlgorithm::movedTarget(SP target, std::optional focalPoint) { - addTarget(target, true); + if (!focalPoint) + focalPoint = target->position().middle(); + + addTarget(target, false, focalPoint); } @@ -178,20 +249,37 @@ int CHyprNstackAlgorithm::getNodesNo() { } -void CHyprNstackAlgorithm::addTarget(SP target, bool firstMap) { - const auto PWORKSPACE = m_parent->space()->workspace(); +void CHyprNstackAlgorithm::addTarget(SP target, bool firstMap, std::optional insertionPoint) { + const auto PWORKSPACE = m_parent->space()->workspace(); - applyWorkspaceLayoutOptions(); + applyWorkspaceLayoutOptions(); const auto PMONITOR = PWORKSPACE->m_monitor; - const auto PNODE = m_workspaceData.new_on_top ? *m_lMasterNodesData.emplace(m_lMasterNodesData.begin(), makeShared()) : m_lMasterNodesData.emplace_back(makeShared()); - PNODE->pTarget = target; - - auto OPENINGON = isWindowTiled(Desktop::focusState()->window()) && Desktop::focusState()->window()->m_workspace == PWORKSPACE ? - getNodeFromWindow(Desktop::focusState()->window()) : - getMasterNode(); + auto OPENINGON = getMasterNode(); + if (insertionPoint) + OPENINGON = getClosestNode(*insertionPoint); + else if (isWindowTiled(Desktop::focusState()->window()) && Desktop::focusState()->window()->m_workspace == PWORKSPACE) + OPENINGON = getNodeFromWindow(Desktop::focusState()->window()); + + auto INSERTPOSITION = m_workspaceData.new_on_top ? m_lMasterNodesData.begin() : m_lMasterNodesData.end(); + if (insertionPoint && OPENINGON) { + const auto OPENINGPOSITION = std::ranges::find(m_lMasterNodesData, OPENINGON); + if (OPENINGPOSITION != m_lMasterNodesData.end()) { + const auto ORIENTATION = m_userWorkspaceData.orientation.value_or(m_workspaceData.orientation); + const auto BOX = OPENINGON->pTarget->position(); + const bool VERTICALSTACKS = ORIENTATION == NSTACK_ORIENTATION_TOP || ORIENTATION == NSTACK_ORIENTATION_BOTTOM || ORIENTATION == NSTACK_ORIENTATION_VCENTER; + const bool INSERTAFTER = VERTICALSTACKS ? insertionPoint->y > BOX.middle().y : insertionPoint->x > BOX.middle().x; + INSERTPOSITION = INSERTAFTER ? std::next(OPENINGPOSITION) : OPENINGPOSITION; + } + } else if (m_workspaceData.new_near_focused && OPENINGON) { + const auto FOCUSEDPOSITION = std::ranges::find(m_lMasterNodesData, OPENINGON); + if (FOCUSEDPOSITION != m_lMasterNodesData.end()) + INSERTPOSITION = std::next(FOCUSEDPOSITION); + } + const auto PNODE = *m_lMasterNodesData.emplace(INSERTPOSITION, makeShared()); + PNODE->pTarget = target; const auto WINDOWSONWORKSPACE = getNodesNo(); float lastSplitPercent = 0.5f; @@ -247,7 +335,12 @@ void CHyprNstackAlgorithm::addTarget(SP target, bool firstMap) { } +#if HYPRNSTACK_HAS_RECALCULATE_REASON +void CHyprNstackAlgorithm::recalculate(eRecalculateReason reason) { + (void)reason; +#else void CHyprNstackAlgorithm::recalculate() { +#endif calculateWorkspace(); } @@ -260,14 +353,10 @@ void CHyprNstackAlgorithm::calculateWorkspace() { Hyprutils::Utils::CScopeGuard x([this] { g_pHyprRenderer->damageMonitor(m_parent->space()->workspace()->m_monitor.lock()); - - for (const auto& n : m_lMasterNodesData) { - n->pTarget->warpPositionSize(); - } }); - auto NUMSTACKS = m_userWorkspaceData.m_iStackCount.value_or(m_workspaceData.m_iStackCount); + auto NUMSTACKS = std::max(2, m_userWorkspaceData.m_iStackCount.value_or(m_workspaceData.m_iStackCount)); const auto NODECOUNT = getNodesNo(); @@ -784,7 +873,8 @@ SP CHyprNstackAlgorithm::getClosestNode(const Vector2D& point) double distClosest = -1; for (auto& n : m_lMasterNodesData) { if (n->pTarget && Desktop::View::validMapped(n->pTarget->window())) { - auto distAnother = vecToRectDistanceSquared(point, n->position, n->position + n->size); + const auto BOX = n->pTarget->position(); + auto distAnother = vecToRectDistanceSquared(point, BOX.pos(), BOX.pos() + BOX.size()); if (!res || distAnother < distClosest) { res = n; distClosest = distAnother; @@ -793,6 +883,84 @@ SP CHyprNstackAlgorithm::getClosestNode(const Vector2D& point) } return res; } + +SP CHyprNstackAlgorithm::getDirectionalTarget(SP t, Math::eDirection dir, bool loop) { + if (!t || !validMapped(t->window())) + return nullptr; + + if (const auto PWINDOW2 = g_pCompositor->getWindowInDirection(t->window(), dir); PWINDOW2 && PWINDOW2 != t->window() && PWINDOW2->m_workspace == t->window()->m_workspace) { + if (const auto NODE = getNodeFromWindow(PWINDOW2); NODE) + return NODE->pTarget.lock(); + } + + if (!loop) + return nullptr; + + const auto CURRENT = getNodeFromTarget(t); + if (!CURRENT) + return nullptr; + + const Vector2D CURRENTCENTER = CURRENT->position + CURRENT->size / 2.F; + + SP directTarget = nullptr; + double directScore = std::numeric_limits::infinity(); + SP wrapTarget = nullptr; + double wrapScore = std::numeric_limits::infinity(); + + for (auto& n : m_lMasterNodesData) { + const auto TARGET = n->pTarget.lock(); + if (!TARGET || TARGET == t || !validMapped(TARGET->window()) || TARGET->window()->m_workspace != t->window()->m_workspace) + continue; + + const Vector2D CENTER = n->position + n->size / 2.F; + double primaryDelta = 0; + double crossDelta = 0; + double wrapPrimary = 0; + + switch (dir) { + case Math::DIRECTION_UP: + primaryDelta = CURRENTCENTER.y - CENTER.y; + crossDelta = CURRENTCENTER.x - CENTER.x; + wrapPrimary = -CENTER.y; + break; + case Math::DIRECTION_RIGHT: + primaryDelta = CENTER.x - CURRENTCENTER.x; + crossDelta = CURRENTCENTER.y - CENTER.y; + wrapPrimary = CENTER.x; + break; + case Math::DIRECTION_DOWN: + primaryDelta = CENTER.y - CURRENTCENTER.y; + crossDelta = CURRENTCENTER.x - CENTER.x; + wrapPrimary = CENTER.y; + break; + case Math::DIRECTION_LEFT: + primaryDelta = CURRENTCENTER.x - CENTER.x; + crossDelta = CURRENTCENTER.y - CENTER.y; + wrapPrimary = -CENTER.x; + break; + default: return nullptr; + } + + const double crossScore = crossDelta * crossDelta; + if (primaryDelta > 0) { + const double score = primaryDelta * primaryDelta + crossScore; + if (score < directScore) { + directTarget = n; + directScore = score; + } + } + + const double score = wrapPrimary * 1000000.0 + crossScore; + if (score < wrapScore) { + wrapTarget = n; + wrapScore = score; + } + } + + const auto TARGET = directTarget ? directTarget : wrapTarget; + return TARGET ? TARGET->pTarget.lock() : nullptr; +} + void CHyprNstackAlgorithm::moveTargetInDirection(SP t, Math::eDirection dir, bool silent) { static auto PMONITORFALLBACK = CConfigValue("binds:window_direction_monitor_fallback"); @@ -823,6 +991,7 @@ void CHyprNstackAlgorithm::moveTargetInDirection(SP t, Math::eDirection t->assignToSpace(targetWs->m_space, focalPointForDir(t, dir)); } else if (PWINDOW2) { // if same monitor, switch windows + PWINDOW2->setAnimationsToMove(); g_layoutManager->switchTargets(t, PWINDOW2->layoutTarget()); if (silent) Desktop::focusState()->fullWindowFocus(PWINDOW2, Desktop::FOCUS_REASON_KEYBIND); @@ -831,7 +1000,7 @@ void CHyprNstackAlgorithm::moveTargetInDirection(SP t, Math::eDirection } } -std::expected CHyprNstackAlgorithm::layoutMsg(const std::string_view& sv) { +Config::ErrorResult CHyprNstackAlgorithm::layoutMsg(const std::string_view& sv) { auto switchToWindow = [&](SP target) { if (!target || !validMapped(target->window())) @@ -845,11 +1014,22 @@ std::expected CHyprNstackAlgorithm::layoutMsg(const std::stri g_pInputManager->m_forcedFocus.reset(); }; - CVarList2 vars(std::string{sv}, 0, 's'); + auto swapWithWindow = [&](PHLWINDOW window, SP target) { + if (!window || !target || !validMapped(target->window())) + return; + + g_pCompositor->setWindowFullscreenInternal(window, FSMODE_NONE); + window->setAnimationsToMove(); + target->window()->setAnimationsToMove(); + g_layoutManager->switchTargets(window->layoutTarget(), target); + switchToWindow(window->layoutTarget()); + }; + + Hyprutils::String::CVarList2 vars(std::string{sv}, 0, 's'); if (vars.size() < 1 || vars[0].empty()) { Log::logger->log(Log::ERR, "layoutmsg called without params"); - return std::unexpected("layoutmsg without params"); + return Config::configError("layoutmsg without params"); } auto command = vars[0]; @@ -864,15 +1044,15 @@ std::expected CHyprNstackAlgorithm::layoutMsg(const std::stri if (command == "swapwithmaster") { if (!PWINDOW) - return std::unexpected("No focused window"); + return Config::configError("No focused window"); if (!isWindowTiled(PWINDOW)) - return std::unexpected("focused window isn't tiled"); + return Config::configError("focused window isn't tiled"); const auto PMASTER = getMasterNode(); if (!PMASTER) - return std::unexpected("no master node"); + return Config::configError("no master node"); const auto NEWCHILD = PMASTER->pTarget.lock(); @@ -904,12 +1084,12 @@ std::expected CHyprNstackAlgorithm::layoutMsg(const std::stri else if (command == "focusmaster") { if (!PWINDOW) - return std::unexpected("no focused window"); + return Config::configError("no focused window"); const auto PMASTER = getMasterNode(); if (!PMASTER) - return std::unexpected("no master"); + return Config::configError("no master"); if (PMASTER->pTarget.lock() != PWINDOW->layoutTarget()) { switchToWindow(PMASTER->pTarget.lock()); @@ -929,7 +1109,7 @@ std::expected CHyprNstackAlgorithm::layoutMsg(const std::stri } else if (command == "cyclenext") { if (!PWINDOW) - return std::unexpected("no window"); + return Config::configError("no window"); const bool NOLOOP = vars.size() >= 2 && vars[1] == "noloop"; const auto PNEXTWINDOW = getNextTarget(PWINDOW->layoutTarget(), true, !NOLOOP); @@ -937,14 +1117,14 @@ std::expected CHyprNstackAlgorithm::layoutMsg(const std::stri } else if (command == "cycleprev") { if (!PWINDOW) - return std::unexpected("no window"); + return Config::configError("no window"); const bool NOLOOP = vars.size() >= 2 && vars[1] == "noloop"; const auto PPREVWINDOW = getNextTarget(PWINDOW->layoutTarget(), false, !NOLOOP); switchToWindow(PPREVWINDOW); } else if (command == "swapnext") { if (!validMapped(PWINDOW)) - return std::unexpected("no window"); + return Config::configError("no window"); if (PWINDOW->layoutTarget()->floating()) { g_pKeybindManager->m_dispatchers["swapnext"](""); @@ -955,33 +1135,50 @@ std::expected CHyprNstackAlgorithm::layoutMsg(const std::stri const auto PWINDOWTOSWAPWITH = getNextTarget(PWINDOW->layoutTarget(), true, !NOLOOP); if (PWINDOWTOSWAPWITH) { - g_pCompositor->setWindowFullscreenInternal(PWINDOW, FSMODE_NONE); - g_layoutManager->switchTargets(PWINDOW->layoutTarget(), PWINDOWTOSWAPWITH); - switchToWindow(PWINDOW->layoutTarget()); + swapWithWindow(PWINDOW, PWINDOWTOSWAPWITH); } } else if (command == "swapprev") { if (!validMapped(PWINDOW)) - return std::unexpected("no window"); + return Config::configError("no window"); if (PWINDOW->layoutTarget()->floating()) { - g_pKeybindManager->m_dispatchers["swapnext"](""); + g_pKeybindManager->m_dispatchers["swapprev"](""); return {}; } const bool NOLOOP = vars.size() >= 2 && vars[1] == "noloop"; - const auto PWINDOWTOSWAPWITH = getNextTarget(PWINDOW->layoutTarget(), true, !NOLOOP); + const auto PWINDOWTOSWAPWITH = getNextTarget(PWINDOW->layoutTarget(), false, !NOLOOP); + + if (PWINDOWTOSWAPWITH) { + swapWithWindow(PWINDOW, PWINDOWTOSWAPWITH); + } + } else if (command == "swapdirection") { + if (!validMapped(PWINDOW)) + return Config::configError("no window"); + + if (vars.size() < 2 || vars[1].empty()) + return Config::configError("missing direction"); + + if (PWINDOW->layoutTarget()->floating()) { + g_pKeybindManager->m_dispatchers["swapwindow"](std::string{vars[1]}); + return {}; + } + const bool NOLOOP = vars.size() >= 3 && vars[2] == "noloop"; + const auto DIR = Math::fromChar(vars[1][0]); + if (DIR == Math::DIRECTION_DEFAULT) + return Config::configError("bad direction"); + + const auto PWINDOWTOSWAPWITH = getDirectionalTarget(PWINDOW->layoutTarget(), DIR, !NOLOOP); if (PWINDOWTOSWAPWITH) { - g_pCompositor->setWindowFullscreenInternal(PWINDOW, FSMODE_NONE); - g_layoutManager->switchTargets(PWINDOW->layoutTarget(), PWINDOWTOSWAPWITH); - switchToWindow(PWINDOW->layoutTarget()); + swapWithWindow(PWINDOW, PWINDOWTOSWAPWITH); } } else if (command == "addmaster") { if (!validMapped(PWINDOW)) - return std::unexpected("no window"); + return Config::configError("no window"); if (PWINDOW->layoutTarget()->floating()) - return std::unexpected("window is floating"); + return Config::configError("window is floating"); const auto PNODE = getNodeFromTarget(PWINDOW->layoutTarget()); @@ -1002,10 +1199,10 @@ std::expected CHyprNstackAlgorithm::layoutMsg(const std::stri } else if (command == "removemaster") { if (!validMapped(PWINDOW)) - return std::unexpected("no window"); + return Config::configError("no window"); if (PWINDOW->layoutTarget()->floating()) - return std::unexpected("window isn't tiled"); + return Config::configError("window isn't tiled"); const auto PNODE = getNodeFromTarget(PWINDOW->layoutTarget()); @@ -1013,7 +1210,7 @@ std::expected CHyprNstackAlgorithm::layoutMsg(const std::stri const auto MASTERS = getMastersCount(); if (WINDOWS < 2 || MASTERS < 2) - return std::unexpected("nothing to do"); + return Config::configError("nothing to do"); if (!PNODE || !PNODE->isMaster) { // first non-master node @@ -1032,7 +1229,7 @@ std::expected CHyprNstackAlgorithm::layoutMsg(const std::stri command == "orientationhcenter" || command == "orientationvcenter") { if (!PWINDOW) - return std::unexpected("no window"); + return Config::configError("no window"); g_pCompositor->setWindowFullscreenInternal(PWINDOW, FSMODE_NONE); @@ -1059,20 +1256,20 @@ std::expected CHyprNstackAlgorithm::layoutMsg(const std::stri runOrientationCycle(&vars, 1); } else if (command == "resetsplits") { if (!PWINDOW) - return std::unexpected("no window"); + return Config::configError("no window"); resetNodeSplits(); } else if (command == "mfact") { const bool exact = vars[1] == "exact"; float ratio = 0.F; try { ratio = std::stof(std::string{exact ? vars[2] : vars[1]}); - } catch (...) { return std::unexpected("bad ratio");} + } catch (...) { return Config::configError("bad ratio");} float newRatio = exact ? ratio : m_userWorkspaceData.master_factor.value_or(m_workspaceData.master_factor) + ratio; m_userWorkspaceData.master_factor = std::clamp(newRatio, 0.05f, 0.95f); recalculate(); } else if (command == "setstackcount") { if (!PWINDOW) - return std::unexpected("no window"); + return Config::configError("no window"); if (vars.size() >= 2) { int newStackCount = 2; switch (vars[1][0]) { @@ -1151,4 +1348,3 @@ void CHyprNstackAlgorithm::buildOrientationCycleVectorFromVars(std::vector #include -#include #include #include #include +#include +#include #include +#if defined(AQUAMARINE_VERSION_MAJOR) && (AQUAMARINE_VERSION_MAJOR > 0 || AQUAMARINE_VERSION_MINOR >= 11) +#define HYPRNSTACK_HAS_RECALCULATE_REASON 1 +#else +#define HYPRNSTACK_HAS_RECALCULATE_REASON 0 +#endif + namespace Layout { @@ -58,6 +65,7 @@ namespace Layout::Tiled { std::vector stackNodeCount; int m_iStackCount = 2; bool new_on_top = false; + bool new_near_focused = true; bool new_is_master = true; bool center_single_master = false; bool inherit_fullscreen = true; @@ -85,10 +93,14 @@ namespace Layout::Tiled { virtual void movedTarget(SP target, std::optional focalPoint = std::nullopt); virtual void removeTarget(SP target); virtual void resizeTarget(const Vector2D& Δ, SP target, eRectCorner corner = CORNER_NONE); +#if HYPRNSTACK_HAS_RECALCULATE_REASON + virtual void recalculate(eRecalculateReason reason = RECALCULATE_REASON_UNKNOWN); +#else virtual void recalculate(); +#endif virtual SP getNextCandidate(SP old); - virtual std::expected layoutMsg(const std::string_view& sv); + virtual Config::ErrorResult layoutMsg(const std::string_view& sv); virtual std::optional predictSizeForNewTarget(); virtual void swapTargets(SP a, SP b); virtual void moveTargetInDirection(SP t, Math::eDirection dir, bool silent); @@ -102,7 +114,7 @@ namespace Layout::Tiled { bool m_bForceWarps = false; - void addTarget(SP target, bool firstMap); + void addTarget(SP target, bool firstMap, std::optional insertionPoint = std::nullopt); void buildOrientationCycleVectorFromVars(std::vector& cycle, Hyprutils::String::CVarList2* vars); void buildOrientationCycleVectorFromEOperation(std::vector& cycle); void runOrientationCycle(Hyprutils::String::CVarList2* vars, int next); @@ -120,6 +132,7 @@ namespace Layout::Tiled { void applyWorkspaceLayoutOptions(); bool isWindowTiled(PHLWINDOW pWindow); SP getNextTarget(SP, bool, bool); + SP getDirectionalTarget(SP, Math::eDirection, bool); friend struct SNstackNodeData; friend struct SNstackWorkspaceData;