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;