From 3ed4a12107179efcc6b716ea1cdc6ebb203d82df Mon Sep 17 00:00:00 2001 From: Matthew Keaton Date: Wed, 29 Jul 2026 18:24:41 +0200 Subject: [PATCH] [hyprland/workspaces] add window-sort-by to order windows by position The windows rendered by {windows} and workspace-taskbar come out of Workspace::m_windowMap in hyprctl clients reply order, i.e. creation order, which under a tiling layout is unrelated to where the windows actually are on screen. Add window-sort-by, with values "insertion" (default, unchanged), "position" ((x, y), floating windows inline) and "position-floating-last" ((isFloating, x, y)). Hyprland emits no IPC event when windows are rearranged within a workspace, so the positions are polled on a timer (window-sort-interval, default 500ms). The poll compares the new address->position map against the previous one and only redraws when it differs, so an idle desktop produces no updates. The timer is only armed when the module actually renders windows. Sorting is applied in updateWorkspaceStates(), after which active-window-position is re-applied so the two options compose instead of the sort silently undoing the hoist. Closes #4782 --- include/modules/hyprland/workspace.hpp | 14 +++++ include/modules/hyprland/workspaces.hpp | 21 +++++++ man/waybar-hyprland-workspaces.5.scd | 14 +++++ src/modules/hyprland/workspace.cpp | 20 ++++++ src/modules/hyprland/workspaces.cpp | 82 ++++++++++++++++++++++++- src/util/enum.cpp | 1 + 6 files changed, 150 insertions(+), 2 deletions(-) diff --git a/include/modules/hyprland/workspace.hpp b/include/modules/hyprland/workspace.hpp index 519c347bb..28ae93ff4 100644 --- a/include/modules/hyprland/workspace.hpp +++ b/include/modules/hyprland/workspace.hpp @@ -25,6 +25,19 @@ using WindowAddress = std::string; namespace waybar::modules::hyprland { +// Where a window sits in the layout, as reported by the "clients" IPC reply. +struct WindowPosition { + bool floating = false; + int x = 0; + int y = 0; + + bool operator==(const WindowPosition& other) const = default; +}; + +// Keyed by address with the "0x" prefix stripped, to match WindowRepr::address +// (see WindowCreationPayload::clearAddr). +using WindowPositions = std::map; + class Workspaces; class Workspace { public: @@ -69,6 +82,7 @@ class Workspace { }; void insertWindow(WindowCreationPayload create_window_payload); void initializeWindowMap(const Json::Value& clients_data); + void sortWindowsByPosition(WindowPositions const& positions, bool floating_last); void setActiveWindow(WindowAddress const& addr); bool onWindowOpened(WindowCreationPayload const& create_window_payload); diff --git a/include/modules/hyprland/workspaces.hpp b/include/modules/hyprland/workspaces.hpp index 1237ef24c..a0d91d6b7 100644 --- a/include/modules/hyprland/workspaces.hpp +++ b/include/modules/hyprland/workspaces.hpp @@ -92,6 +92,7 @@ class Workspaces : public AModule, public EventHandler { static auto populateBoolConfig(const Json::Value& config, const std::string& key, bool& member) -> void; auto populateSortByConfig(const Json::Value& config) -> void; + auto populateWindowSortByConfig(const Json::Value& config) -> void; auto populateIgnoreWorkspacesConfig(const Json::Value& config) -> void; auto populateFormatWindowSeparatorConfig(const Json::Value& config) -> void; auto populateWindowRewriteConfig(const Json::Value& config) -> void; @@ -142,6 +143,12 @@ class Workspaces : public AModule, public EventHandler { void updateWorkspaceStates(); bool updateWindowsToCreate(); + // Window position sorting. Hyprland emits no event when windows are rearranged within a + // workspace (layoutmsg swapcol, movewindow, drag-moves), so this has to be polled. + WindowPositions queryWindowPositions() const; + bool checkWindowPositions(); + bool sortsWindowsByPosition() const { return m_windowSortBy != WindowSortMethod::INSERTION; } + void extendOrphans(int workspaceId, Json::Value const& clientsJson); void registerOrphanWindow(WindowCreationPayload create_window_payload); @@ -175,6 +182,19 @@ class Workspaces : public AModule, public EventHandler { {"SPECIAL-CENTERED", SortMethod::SPECIAL_CENTERED}, {"DEFAULT", SortMethod::DEFAULT}}; + // How the windows *within* a workspace are ordered. INSERTION is Hyprland's creation order, + // the historical behaviour; the POSITION variants follow the on-screen layout. + enum class WindowSortMethod { INSERTION, POSITION, POSITION_FLOATING_LAST }; + util::EnumParser m_windowSortEnumParser; + WindowSortMethod m_windowSortBy = WindowSortMethod::INSERTION; + std::map m_windowSortMap = { + {"INSERTION", WindowSortMethod::INSERTION}, + {"POSITION", WindowSortMethod::POSITION}, + {"POSITION-FLOATING-LAST", WindowSortMethod::POSITION_FLOATING_LAST}}; + int m_windowSortInterval = 500; + WindowPositions m_lastWindowPositions; + sigc::connection m_windowPositionPoll; + std::string m_formatBefore; std::string m_formatAfter; @@ -188,6 +208,7 @@ class Workspaces : public AModule, public EventHandler { std::string m_windowRewriteGroupFormat = "{icon}×{count}"; bool m_withIcon; + bool m_withWindows = false; uint64_t m_monitorId; int m_activeWorkspaceId; std::string m_activeSpecialWorkspaceName; diff --git a/man/waybar-hyprland-workspaces.5.scd b/man/waybar-hyprland-workspaces.5.scd index 3010fb455..50832422f 100644 --- a/man/waybar-hyprland-workspaces.5.scd +++ b/man/waybar-hyprland-workspaces.5.scd @@ -196,6 +196,20 @@ This setting is ignored if *workspace-taskbar.enable* is set to true. If set to special-centered, workspaces will sort by default with special workspaces in the center. If none of those, workspaces will sort with default behavior. +*window-sort-by*: ++ + typeof: string ++ + default: "insertion" ++ + Controls the order of the windows *within* a workspace. Applies both to the *{windows}* replacement and to *workspace-taskbar*. + If set to insertion, windows appear in the order Hyprland created them. This is the historical behavior. + If set to position, windows appear in on-screen layout order: left to right, then top to bottom. Floating windows are ordered inline with tiled ones. + If set to position-floating-last, the same as position, except floating windows are placed after all tiled ones. + When combined with *workspace-taskbar*'s *active-window-position*, windows are sorted by position first and the active window is then moved to the requested end, so both options apply. + +*window-sort-interval*: ++ + typeof: integer ++ + default: 500 ++ + How often, in milliseconds, to check for changes in window positions. Only used when *window-sort-by* orders by position. Hyprland emits no event when windows are rearranged within a workspace (for example *layoutmsg swapcol*, *movewindow*, or a drag-move), so this is polled. A redraw only happens when a position actually changed. Values below 50 are clamped. + *tooltip*: ++ typeof: bool ++ default: true ++ diff --git a/src/modules/hyprland/workspace.cpp b/src/modules/hyprland/workspace.cpp index 79ff5c3dd..87fb43460 100644 --- a/src/modules/hyprland/workspace.cpp +++ b/src/modules/hyprland/workspace.cpp @@ -4,9 +4,11 @@ #include #include +#include #include #include #include +#include #include #include "modules/hyprland/workspaces.hpp" @@ -239,6 +241,24 @@ void Workspace::initializeWindowMap(const Json::Value& clients_data) { } } +// Order the windows the way they are laid out on screen: left to right, then top to bottom. +// Hyprland reports windows in creation order, which under a tiling layout says nothing about +// where they actually are. +void Workspace::sortWindowsByPosition(WindowPositions const& positions, bool floating_last) { + // A window with no reported position (opened between the query and this call) sorts last and, + // thanks to stable_sort, keeps its insertion order relative to other such windows. + static constexpr WindowPosition UNKNOWN{.floating = true, .x = INT_MAX, .y = INT_MAX}; + + auto key = [&positions, floating_last](const WindowRepr& window) { + auto it = positions.find(window.address); + const WindowPosition& position = it == positions.end() ? UNKNOWN : it->second; + return std::tuple{floating_last && position.floating, position.x, position.y}; + }; + + std::ranges::stable_sort( + m_windowMap, [&key](const auto& lhs, const auto& rhs) { return key(lhs) < key(rhs); }); +} + void Workspace::setActiveWindow(WindowAddress const& addr) { std::optional activeIdx; for (size_t i = 0; i < m_windowMap.size(); ++i) { diff --git a/src/modules/hyprland/workspaces.cpp b/src/modules/hyprland/workspaces.cpp index 202b892bb..e9e896bad 100644 --- a/src/modules/hyprland/workspaces.cpp +++ b/src/modules/hyprland/workspaces.cpp @@ -1,5 +1,6 @@ #include "modules/hyprland/workspaces.hpp" +#include #include #include @@ -31,6 +32,12 @@ Workspaces::Workspaces(const std::string& id, const Bar& bar, const Json::Value& setCurrentMonitorId(); init(); registerIpc(); + + // Only worth polling if this module actually renders windows. + if (sortsWindowsByPosition() && (m_withWindows || m_enableTaskbar)) { + m_windowPositionPoll = Glib::signal_timeout().connect( + sigc::mem_fun(*this, &Workspaces::checkWindowPositions), m_windowSortInterval); + } } Workspaces::~Workspaces() { @@ -42,6 +49,9 @@ Workspaces::~Workspaces() { if (m_debounceTimer.connected()) { m_debounceTimer.disconnect(); } + if (m_windowPositionPoll.connected()) { + m_windowPositionPoll.disconnect(); + } m_ipc.unregisterForIPC(this); // wait for possible event handler to finish std::lock_guard lg(m_mutex); @@ -610,7 +620,7 @@ auto Workspaces::parseConfig(const Json::Value& config) -> void { const auto& configFormat = config["format"]; m_formatBefore = configFormat.isString() ? configFormat.asString() : "{name}"; m_withIcon = m_formatBefore.find("{icon}") != std::string::npos; - auto withWindows = m_formatBefore.find("{windows}") != std::string::npos; + m_withWindows = m_formatBefore.find("{windows}") != std::string::npos; if (m_withIcon && m_iconsMap.empty()) { populateIconsMap(config["format-icons"]); @@ -637,6 +647,7 @@ auto Workspaces::parseConfig(const Json::Value& config) -> void { m_persistentWorkspaceConfig = config.get("persistent-workspaces", Json::Value()); populateSortByConfig(config); + populateWindowSortByConfig(config); populateIgnoreWorkspacesConfig(config); populateFormatWindowSeparatorConfig(config); @@ -652,7 +663,7 @@ auto Workspaces::parseConfig(const Json::Value& config) -> void { populateWindowRewriteConfig(config); populateMaxWindowsConfig(config); - if (withWindows) { + if (m_withWindows) { populateWorkspaceTaskbarConfig(config); } if (m_enableTaskbar) { @@ -691,6 +702,24 @@ auto Workspaces::populateSortByConfig(const Json::Value& config) -> void { } } +auto Workspaces::populateWindowSortByConfig(const Json::Value& config) -> void { + const auto& configWindowSortBy = config["window-sort-by"]; + if (configWindowSortBy.isString()) { + auto windowSortByStr = configWindowSortBy.asString(); + try { + m_windowSortBy = m_windowSortEnumParser.parseStringToEnum(windowSortByStr, m_windowSortMap); + } catch (const std::invalid_argument& e) { + m_windowSortBy = WindowSortMethod::INSERTION; + spdlog::warn("Invalid string representation for window-sort-by. Falling back to insertion."); + } + } + + const auto& configInterval = config["window-sort-interval"]; + if (configInterval.isInt()) { + m_windowSortInterval = std::max(configInterval.asInt(), 50); + } +} + auto Workspaces::populateIgnoreWorkspacesConfig(const Json::Value& config) -> void { auto ignoreWorkspaces = config["ignore-workspaces"]; if (ignoreWorkspaces.isArray()) { @@ -1101,6 +1130,10 @@ void Workspaces::updateWorkspaceStates() { std::string currentWorkspaceName = currentWorkspace.isMember("name") ? currentWorkspace["name"].asString() : ""; + if (sortsWindowsByPosition()) { + m_lastWindowPositions = queryWindowPositions(); + } + for (auto& workspace : m_workspaces) { bool isActiveByName = !currentWorkspaceName.empty() && workspace->name() == currentWorkspaceName; @@ -1127,10 +1160,55 @@ void Workspaces::updateWorkspaceStates() { if (updatedWorkspace != updatedWorkspaces.end()) { workspace->setOutput((*updatedWorkspace)["monitor"].asString()); } + if (sortsWindowsByPosition()) { + workspace->sortWindowsByPosition(m_lastWindowPositions, + m_windowSortBy == WindowSortMethod::POSITION_FLOATING_LAST); + // Sorting rewrites the order active-window-position established, so re-apply it on top. + // Skipped until an active window is known, so this never clears the active flag. + if (m_activeWindowPosition != ActiveWindowPosition::NONE && + !m_currentActiveWindowAddress.empty()) { + workspace->setActiveWindow(m_currentActiveWindowAddress); + } + } workspace->update(workspaceIcon, workspaceTooltip); } } +WindowPositions Workspaces::queryWindowPositions() const { + WindowPositions positions; + for (const auto& client : m_ipc.getSocket1JsonReply("clients")) { + const auto& at = client["at"]; + if (!at.isArray() || at.size() < 2) { + continue; + } + auto address = client["address"].asString(); + if (address.starts_with("0x")) { + address = address.substr(2); + } + positions.emplace(std::move(address), WindowPosition{.floating = client["floating"].asBool(), + .x = at[0].asInt(), + .y = at[1].asInt()}); + } + return positions; +} + +// Hyprland emits no event when windows are rearranged within a workspace, so poll for it. +// Redraw only when something actually moved, which keeps an idle desktop free of updates. +bool Workspaces::checkWindowPositions() { + try { + auto positions = queryWindowPositions(); + if (positions != m_lastWindowPositions) { + m_lastWindowPositions = std::move(positions); + dp.emit(); + } + } catch (const std::exception& e) { + // Hyprland may be gone while this bar is still shutting down; keep the timer alive rather + // than letting the exception escape into the GLib main loop. + spdlog::debug("Failed to poll window positions: {}", e.what()); + } + return true; +} + int Workspaces::windowRewritePriorityFunction(std::string const& window_rule) { // Rules that match against title are prioritized // Rules that don't specify if they're matching against either title or class are deprioritized diff --git a/src/util/enum.cpp b/src/util/enum.cpp index 6b5d55624..32a213db7 100644 --- a/src/util/enum.cpp +++ b/src/util/enum.cpp @@ -41,6 +41,7 @@ EnumType EnumParser::parseStringToEnum(const std::string& str, // Explicit instantiations for specific EnumType types you intend to use // Add explicit instantiations for all relevant EnumType types template struct EnumParser; +template struct EnumParser; template struct EnumParser; template struct EnumParser;