From 94607cd53f2ddac88f6b26261393275e7dd590ef Mon Sep 17 00:00:00 2001 From: Ivan Malison Date: Mon, 27 Apr 2026 12:21:57 -0700 Subject: [PATCH 01/11] Add flake for Hyprland Lua builds --- flake.lock | 502 +++++++++++++++++++++++++++++++++++++++++++++++ flake.nix | 81 ++++++++ nstackLayout.cpp | 9 +- 3 files changed, 588 insertions(+), 4 deletions(-) create mode 100644 flake.lock create mode 100644 flake.nix diff --git a/flake.lock b/flake.lock new file mode 100644 index 0000000..d4045fd --- /dev/null +++ b/flake.lock @@ -0,0 +1,502 @@ +{ + "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": 1777207870, + "narHash": "sha256-Jld5Lp1HdjRVYJgAnW3vwOPcRUgbfP+IzhcpUjFR2Ns=", + "ref": "refs/pull/13817/head", + "rev": "c35a8a565bf14fcf71b34663e7bd7683ca3facfd", + "revCount": 7174, + "submodules": true, + "type": "git", + "url": "https://github.com/hyprwm/Hyprland" + }, + "original": { + "ref": "refs/pull/13817/head", + "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": 1776428866, + "narHash": "sha256-XfRlBolGtjvalTHJp3XvvpYLBjkMhaZLLU0WqZ91Fcg=", + "owner": "hyprwm", + "repo": "hyprutils", + "rev": "eedd60805cd96d4442586f2ba5fe51d549b12674", + "type": "github" + }, + "original": { + "owner": "hyprwm", + "repo": "hyprutils", + "type": "github" + } + }, + "hyprwayland-scanner": { + "inputs": { + "nixpkgs": [ + "hyprland", + "nixpkgs" + ], + "systems": [ + "hyprland", + "systems" + ] + }, + "locked": { + "lastModified": 1776430932, + "narHash": "sha256-Yv3RPiUvl7CAsJgwIVsqcj7akn1gLyJP1F/mocof5hA=", + "owner": "hyprwm", + "repo": "hyprwayland-scanner", + "rev": "4c2fcc06dc9722c97dbb54ba649c69b18ce83d2e", + "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": 1776608502, + "narHash": "sha256-UH8YoQxx4hFOm6qjMdjRQNRvSejFIR/wBZ8fW1p9sME=", + "owner": "hyprwm", + "repo": "xdg-desktop-portal-hyprland", + "rev": "4a293523d36dfa367e67ec304cc718ea66a8fec2", + "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..a8018ca --- /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&ref=refs/pull/13817/head"; + 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/nstackLayout.cpp b/nstackLayout.cpp index 066da8b..0ec2a63 100644 --- a/nstackLayout.cpp +++ b/nstackLayout.cpp @@ -1,4 +1,5 @@ #include "nstackLayout.hpp" +#include #include #include #include @@ -49,8 +50,9 @@ 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; @@ -845,7 +847,7 @@ std::expected CHyprNstackAlgorithm::layoutMsg(const std::stri g_pInputManager->m_forcedFocus.reset(); }; - CVarList2 vars(std::string{sv}, 0, 's'); + 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"); @@ -1151,4 +1153,3 @@ void CHyprNstackAlgorithm::buildOrientationCycleVectorFromVars(std::vector Date: Wed, 29 Apr 2026 03:38:41 -0700 Subject: [PATCH 02/11] Fix nstack config reads under Lua config --- nstackLayout.cpp | 80 ++++++++++++++++++++++++++++++++++++------------ 1 file changed, 60 insertions(+), 20 deletions(-) diff --git a/nstackLayout.cpp b/nstackLayout.cpp index 0ec2a63..7710bb9 100644 --- a/nstackLayout.cpp +++ b/nstackLayout.cpp @@ -26,6 +26,56 @@ using namespace Layout; using namespace Layout::Tiled; +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& 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) @@ -54,8 +104,7 @@ void CHyprNstackAlgorithm::applyWorkspaceLayoutOptions() { 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"); @@ -76,8 +125,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 { @@ -89,8 +137,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 { @@ -99,8 +146,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"); @@ -110,8 +156,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 { @@ -120,32 +165,27 @@ 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); 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 wsnewmaster = *nstackNewIsMaster(); if (wslayoutopts.contains("nstack-new_is_master")) wsnewmaster = configStringToInt(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); 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); 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); m_workspaceData.center_single_master = wscentersm; From d1b4307c0804ad22a17882bd5c5c2f5a7df2b3ea Mon Sep 17 00:00:00 2001 From: Ivan Malison Date: Wed, 29 Apr 2026 07:12:40 -0700 Subject: [PATCH 03/11] Use Hyprland Lua config values --- main.cpp | 24 +++++++++++++----------- nstackLayout.cpp | 2 +- 2 files changed, 14 insertions(+), 12 deletions(-) diff --git a/main.cpp b/main.cpp index f523aa2..11bf43d 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,20 @@ 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_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 7710bb9..bf43737 100644 --- a/nstackLayout.cpp +++ b/nstackLayout.cpp @@ -309,7 +309,7 @@ void CHyprNstackAlgorithm::calculateWorkspace() { }); - 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(); From a59a9a1502525e844b0df541e633193fa9dca24a Mon Sep 17 00:00:00 2001 From: Ivan Malison Date: Wed, 29 Apr 2026 07:48:06 -0700 Subject: [PATCH 04/11] Preserve animations during nstack relayout --- nstackLayout.cpp | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/nstackLayout.cpp b/nstackLayout.cpp index bf43737..6cb8182 100644 --- a/nstackLayout.cpp +++ b/nstackLayout.cpp @@ -302,10 +302,6 @@ 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(); - } }); @@ -865,6 +861,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); From d43c8a506b32dc5057fdce0569c38f401c01eb60 Mon Sep 17 00:00:00 2001 From: Ivan Malison Date: Wed, 29 Apr 2026 08:04:34 -0700 Subject: [PATCH 05/11] Add wrapping directional swap layout message --- README.md | 3 +- nstackLayout.cpp | 123 ++++++++++++++++++++++++++++++++++++++++++++--- nstackLayout.hpp | 1 + 3 files changed, 118 insertions(+), 9 deletions(-) diff --git a/README.md b/README.md index d8f5e9d..d4ee9f4 100644 --- a/README.md +++ b/README.md @@ -38,9 +38,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/nstackLayout.cpp b/nstackLayout.cpp index 6cb8182..a363bb7 100644 --- a/nstackLayout.cpp +++ b/nstackLayout.cpp @@ -1,4 +1,5 @@ #include "nstackLayout.hpp" +#include #include #include #include @@ -831,6 +832,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"); @@ -884,6 +963,17 @@ std::expected CHyprNstackAlgorithm::layoutMsg(const std::stri g_pInputManager->m_forcedFocus.reset(); }; + 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()) { @@ -994,26 +1084,43 @@ 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"); 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 std::unexpected("no window"); + + if (vars.size() < 2 || vars[1].empty()) + return std::unexpected("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 std::unexpected("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)) diff --git a/nstackLayout.hpp b/nstackLayout.hpp index 0840bd6..6734990 100644 --- a/nstackLayout.hpp +++ b/nstackLayout.hpp @@ -120,6 +120,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; From e2b07d6a7b472a0dd5bef00a1c0c186ccd2f46c6 Mon Sep 17 00:00:00 2001 From: Ivan Malison Date: Wed, 29 Apr 2026 14:39:44 -0700 Subject: [PATCH 06/11] Follow Hyprland main API --- flake.lock | 29 ++++++++++++++--------------- flake.nix | 2 +- nstackLayout.cpp | 46 +++++++++++++++++++++++----------------------- nstackLayout.hpp | 4 ++-- 4 files changed, 40 insertions(+), 41 deletions(-) diff --git a/flake.lock b/flake.lock index d4045fd..80c4bc6 100644 --- a/flake.lock +++ b/flake.lock @@ -148,17 +148,16 @@ "xdph": "xdph" }, "locked": { - "lastModified": 1777207870, - "narHash": "sha256-Jld5Lp1HdjRVYJgAnW3vwOPcRUgbfP+IzhcpUjFR2Ns=", - "ref": "refs/pull/13817/head", - "rev": "c35a8a565bf14fcf71b34663e7bd7683ca3facfd", - "revCount": 7174, + "lastModified": 1777497195, + "narHash": "sha256-I9z3somAUJ5jGwv4ZBjADco8CmdglEUqYs0AgJ5LSDs=", + "ref": "refs/heads/main", + "rev": "56d7a43102b53f79a2311662783d5dd94cb2f1a5", + "revCount": 7215, "submodules": true, "type": "git", "url": "https://github.com/hyprwm/Hyprland" }, "original": { - "ref": "refs/pull/13817/head", "submodules": true, "type": "git", "url": "https://github.com/hyprwm/Hyprland" @@ -328,11 +327,11 @@ ] }, "locked": { - "lastModified": 1776428866, - "narHash": "sha256-XfRlBolGtjvalTHJp3XvvpYLBjkMhaZLLU0WqZ91Fcg=", + "lastModified": 1777492286, + "narHash": "sha256-PwuoEJQcjSKJNP5T55qhfDwIP0tw5zxEhfu8GDfKfeg=", "owner": "hyprwm", "repo": "hyprutils", - "rev": "eedd60805cd96d4442586f2ba5fe51d549b12674", + "rev": "ec5c0c709706bad5b82f667fd8758eae442577ce", "type": "github" }, "original": { @@ -353,11 +352,11 @@ ] }, "locked": { - "lastModified": 1776430932, - "narHash": "sha256-Yv3RPiUvl7CAsJgwIVsqcj7akn1gLyJP1F/mocof5hA=", + "lastModified": 1777148232, + "narHash": "sha256-Uv0WZLhu89SafuSOmYDA7akrPt4wBRmsa1ucasO5aXg=", "owner": "hyprwm", "repo": "hyprwayland-scanner", - "rev": "4c2fcc06dc9722c97dbb54ba649c69b18ce83d2e", + "rev": "fec9cf1abcc1011e46f0a0986f46bf93c6bf8b92", "type": "github" }, "original": { @@ -483,11 +482,11 @@ ] }, "locked": { - "lastModified": 1776608502, - "narHash": "sha256-UH8YoQxx4hFOm6qjMdjRQNRvSejFIR/wBZ8fW1p9sME=", + "lastModified": 1777035886, + "narHash": "sha256-m1TNuBoSXUBSKhD9UVMkU90M0wFTPTfvIOOltO8IM8A=", "owner": "hyprwm", "repo": "xdg-desktop-portal-hyprland", - "rev": "4a293523d36dfa367e67ec304cc718ea66a8fec2", + "rev": "ecfcdcc781f48821d83e1e2a0e30d7beca0eeb5e", "type": "github" }, "original": { diff --git a/flake.nix b/flake.nix index a8018ca..5cbbb66 100644 --- a/flake.nix +++ b/flake.nix @@ -4,7 +4,7 @@ inputs = { nixpkgs.url = "github:NixOS/nixpkgs/nixos-unstable"; hyprland = { - url = "git+https://github.com/hyprwm/Hyprland?submodules=1&ref=refs/pull/13817/head"; + url = "git+https://github.com/hyprwm/Hyprland?submodules=1"; inputs.nixpkgs.follows = "nixpkgs"; }; }; diff --git a/nstackLayout.cpp b/nstackLayout.cpp index a363bb7..d5ebcc3 100644 --- a/nstackLayout.cpp +++ b/nstackLayout.cpp @@ -949,7 +949,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())) @@ -978,7 +978,7 @@ std::expected CHyprNstackAlgorithm::layoutMsg(const std::stri 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]; @@ -993,15 +993,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(); @@ -1033,12 +1033,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()); @@ -1058,7 +1058,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); @@ -1066,14 +1066,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"](""); @@ -1088,7 +1088,7 @@ std::expected CHyprNstackAlgorithm::layoutMsg(const std::stri } } 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["swapprev"](""); @@ -1103,10 +1103,10 @@ std::expected CHyprNstackAlgorithm::layoutMsg(const std::stri } } else if (command == "swapdirection") { if (!validMapped(PWINDOW)) - return std::unexpected("no window"); + return Config::configError("no window"); if (vars.size() < 2 || vars[1].empty()) - return std::unexpected("missing direction"); + return Config::configError("missing direction"); if (PWINDOW->layoutTarget()->floating()) { g_pKeybindManager->m_dispatchers["swapwindow"](std::string{vars[1]}); @@ -1116,7 +1116,7 @@ std::expected CHyprNstackAlgorithm::layoutMsg(const std::stri const bool NOLOOP = vars.size() >= 3 && vars[2] == "noloop"; const auto DIR = Math::fromChar(vars[1][0]); if (DIR == Math::DIRECTION_DEFAULT) - return std::unexpected("bad direction"); + return Config::configError("bad direction"); const auto PWINDOWTOSWAPWITH = getDirectionalTarget(PWINDOW->layoutTarget(), DIR, !NOLOOP); if (PWINDOWTOSWAPWITH) { @@ -1124,10 +1124,10 @@ std::expected CHyprNstackAlgorithm::layoutMsg(const std::stri } } 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()); @@ -1148,10 +1148,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()); @@ -1159,7 +1159,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 @@ -1178,7 +1178,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); @@ -1205,20 +1205,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]) { diff --git a/nstackLayout.hpp b/nstackLayout.hpp index 6734990..24f9394 100644 --- a/nstackLayout.hpp +++ b/nstackLayout.hpp @@ -3,11 +3,11 @@ #include "globals.hpp" #include #include -#include #include #include #include +#include #include @@ -88,7 +88,7 @@ namespace Layout::Tiled { virtual void recalculate(); 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); From 7d723e850b2a60e08f10d49e11c4e9f9697317a8 Mon Sep 17 00:00:00 2001 From: Ivan Malison Date: Wed, 6 May 2026 12:14:53 -0700 Subject: [PATCH 07/11] Add focus-local new window placement --- README.md | 1 + main.cpp | 1 + nstackLayout.cpp | 24 +++++++++++++++++++++--- nstackLayout.hpp | 1 + 4 files changed, 24 insertions(+), 3 deletions(-) diff --git a/README.md b/README.md index d4ee9f4..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 diff --git a/main.cpp b/main.cpp index 11bf43d..bb4bf1f 100644 --- a/main.cpp +++ b/main.cpp @@ -21,6 +21,7 @@ APICALL EXPORT PLUGIN_DESCRIPTION_INFO PLUGIN_INIT(HANDLE handle) { 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)); diff --git a/nstackLayout.cpp b/nstackLayout.cpp index d5ebcc3..c0c8342 100644 --- a/nstackLayout.cpp +++ b/nstackLayout.cpp @@ -57,6 +57,11 @@ static const CConfigValue& nstackNewOnTop() { 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; @@ -171,6 +176,11 @@ void CHyprNstackAlgorithm::applyWorkspaceLayoutOptions() { wsnewtop = configStringToInt(wslayoutopts.at("nstack-new_on_top")).value_or(0); m_workspaceData.new_on_top = wsnewtop; + auto wsnewnearfocused = *nstackNewNearFocused(); + if (wslayoutopts.contains("nstack-new_near_focused")) + wsnewnearfocused = configStringToInt(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); @@ -228,13 +238,21 @@ void CHyprNstackAlgorithm::addTarget(SP target, bool firstMap) { 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 INSERTPOSITION = m_workspaceData.new_on_top ? m_lMasterNodesData.begin() : m_lMasterNodesData.end(); + 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; diff --git a/nstackLayout.hpp b/nstackLayout.hpp index 24f9394..f2e83e1 100644 --- a/nstackLayout.hpp +++ b/nstackLayout.hpp @@ -58,6 +58,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; From d0cb53faa1982e2a86fde143bfa8d926ea3e11ff Mon Sep 17 00:00:00 2001 From: Ivan Malison Date: Thu, 7 May 2026 18:00:58 -0700 Subject: [PATCH 08/11] Place retiled windows near prior position --- nstackLayout.cpp | 44 +++++++++++++++++++++++++++++++------------- nstackLayout.hpp | 2 +- 2 files changed, 32 insertions(+), 14 deletions(-) diff --git a/nstackLayout.cpp b/nstackLayout.cpp index d5ebcc3..c69b4d5 100644 --- a/nstackLayout.cpp +++ b/nstackLayout.cpp @@ -212,7 +212,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); } @@ -221,23 +224,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 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 = getMasterNode(); + if (insertionPoint) + OPENINGON = getClosestNode(*insertionPoint); + else if (isWindowTiled(Desktop::focusState()->window()) && Desktop::focusState()->window()->m_workspace == PWORKSPACE) + OPENINGON = getNodeFromWindow(Desktop::focusState()->window()); - auto OPENINGON = isWindowTiled(Desktop::focusState()->window()) && Desktop::focusState()->window()->m_workspace == PWORKSPACE ? - getNodeFromWindow(Desktop::focusState()->window()) : - getMasterNode(); + 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; + } + } + const auto PNODE = *m_lMasterNodesData.emplace(INSERTPOSITION, makeShared()); + PNODE->pTarget = target; - const auto WINDOWSONWORKSPACE = getNodesNo(); - float lastSplitPercent = 0.5f; + const auto WINDOWSONWORKSPACE = getNodesNo(); + float lastSplitPercent = 0.5f; bool lastMasterAdjusted = false; /* @@ -823,7 +840,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; diff --git a/nstackLayout.hpp b/nstackLayout.hpp index 24f9394..9f0ee18 100644 --- a/nstackLayout.hpp +++ b/nstackLayout.hpp @@ -102,7 +102,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); From 1ac1b85443304f730aa9de58e6e4abdb90b4ca3d Mon Sep 17 00:00:00 2001 From: Ivan Malison Date: Sun, 10 May 2026 01:11:42 -0700 Subject: [PATCH 09/11] Fix build with current Hyprland API --- nstackLayout.cpp | 17 ++++++++++++++++- nstackLayout.hpp | 2 +- 2 files changed, 17 insertions(+), 2 deletions(-) diff --git a/nstackLayout.cpp b/nstackLayout.cpp index fe13d09..82a06dc 100644 --- a/nstackLayout.cpp +++ b/nstackLayout.cpp @@ -20,6 +20,7 @@ #include #include #include +#include @@ -27,6 +28,20 @@ using namespace Layout; using namespace Layout::Tiled; +static std::optional configStringToInt(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; @@ -320,7 +335,7 @@ void CHyprNstackAlgorithm::addTarget(SP target, bool firstMap, std::opt } -void CHyprNstackAlgorithm::recalculate() { +void CHyprNstackAlgorithm::recalculate(eRecalculateReason reason) { calculateWorkspace(); } diff --git a/nstackLayout.hpp b/nstackLayout.hpp index 6f3f196..024fc07 100644 --- a/nstackLayout.hpp +++ b/nstackLayout.hpp @@ -86,7 +86,7 @@ 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); - virtual void recalculate(); + virtual void recalculate(eRecalculateReason reason = RECALCULATE_REASON_UNKNOWN); virtual SP getNextCandidate(SP old); virtual Config::ErrorResult layoutMsg(const std::string_view& sv); From a24434c39aef08663f38402cbf0d0950ec78235b Mon Sep 17 00:00:00 2001 From: Ivan Malison Date: Sun, 10 May 2026 12:59:27 -0700 Subject: [PATCH 10/11] Fix build with Hyprland recalculate API --- nstackLayout.cpp | 16 ++++++++-------- nstackLayout.hpp | 2 +- 2 files changed, 9 insertions(+), 9 deletions(-) diff --git a/nstackLayout.cpp b/nstackLayout.cpp index 82a06dc..99e7d85 100644 --- a/nstackLayout.cpp +++ b/nstackLayout.cpp @@ -28,7 +28,7 @@ using namespace Layout; using namespace Layout::Tiled; -static std::optional configStringToInt(const std::string& value) { +static std::optional nstackConfigStringToInt(const std::string& value) { if (value == "true" || value == "on" || value == "yes") return 1; if (value == "false" || value == "off" || value == "no") @@ -188,32 +188,32 @@ void CHyprNstackAlgorithm::applyWorkspaceLayoutOptions() { 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; auto wsnewnearfocused = *nstackNewNearFocused(); if (wslayoutopts.contains("nstack-new_near_focused")) - wsnewnearfocused = configStringToInt(wslayoutopts.at("nstack-new_near_focused")).value_or(0); + 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; 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; 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; 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; } @@ -335,7 +335,7 @@ void CHyprNstackAlgorithm::addTarget(SP target, bool firstMap, std::opt } -void CHyprNstackAlgorithm::recalculate(eRecalculateReason reason) { +void CHyprNstackAlgorithm::recalculate() { calculateWorkspace(); } diff --git a/nstackLayout.hpp b/nstackLayout.hpp index 024fc07..6f3f196 100644 --- a/nstackLayout.hpp +++ b/nstackLayout.hpp @@ -86,7 +86,7 @@ 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); - virtual void recalculate(eRecalculateReason reason = RECALCULATE_REASON_UNKNOWN); + virtual void recalculate(); virtual SP getNextCandidate(SP old); virtual Config::ErrorResult layoutMsg(const std::string_view& sv); From 1abcbbe76bda28915ab6f76572bef2f6b3364b01 Mon Sep 17 00:00:00 2001 From: Ivan Malison Date: Sat, 16 May 2026 22:34:54 -0700 Subject: [PATCH 11/11] Support Hyprland recalculate reason API --- nstackLayout.cpp | 5 +++++ nstackLayout.hpp | 11 +++++++++++ 2 files changed, 16 insertions(+) diff --git a/nstackLayout.cpp b/nstackLayout.cpp index 99e7d85..0c77765 100644 --- a/nstackLayout.cpp +++ b/nstackLayout.cpp @@ -335,7 +335,12 @@ void CHyprNstackAlgorithm::addTarget(SP target, bool firstMap, std::opt } +#if HYPRNSTACK_HAS_RECALCULATE_REASON +void CHyprNstackAlgorithm::recalculate(eRecalculateReason reason) { + (void)reason; +#else void CHyprNstackAlgorithm::recalculate() { +#endif calculateWorkspace(); } diff --git a/nstackLayout.hpp b/nstackLayout.hpp index 6f3f196..24f3807 100644 --- a/nstackLayout.hpp +++ b/nstackLayout.hpp @@ -8,8 +8,15 @@ #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 { @@ -86,7 +93,11 @@ 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 Config::ErrorResult layoutMsg(const std::string_view& sv);