Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
14 changes: 14 additions & 0 deletions include/modules/hyprland/workspace.hpp
Original file line number Diff line number Diff line change
Expand Up @@ -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<WindowAddress, WindowPosition>;

class Workspaces;
class Workspace {
public:
Expand Down Expand Up @@ -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);
Expand Down
21 changes: 21 additions & 0 deletions include/modules/hyprland/workspaces.hpp
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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);

Expand Down Expand Up @@ -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<WindowSortMethod> m_windowSortEnumParser;
WindowSortMethod m_windowSortBy = WindowSortMethod::INSERTION;
std::map<std::string, WindowSortMethod> 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;

Expand All @@ -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;
Expand Down
14 changes: 14 additions & 0 deletions man/waybar-hyprland-workspaces.5.scd
Original file line number Diff line number Diff line change
Expand Up @@ -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 ++
Expand Down
20 changes: 20 additions & 0 deletions src/modules/hyprland/workspace.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -4,9 +4,11 @@

#include <algorithm>
#include <cctype>
#include <climits>
#include <memory>
#include <set>
#include <string>
#include <tuple>
#include <utility>

#include "modules/hyprland/workspaces.hpp"
Expand Down Expand Up @@ -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<long> activeIdx;
for (size_t i = 0; i < m_windowMap.size(); ++i) {
Expand Down
82 changes: 80 additions & 2 deletions src/modules/hyprland/workspaces.cpp
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
#include "modules/hyprland/workspaces.hpp"

#include <glibmm/main.h>
#include <json/value.h>
#include <spdlog/spdlog.h>

Expand Down Expand Up @@ -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() {
Expand All @@ -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<std::mutex> lg(m_mutex);
Expand Down Expand Up @@ -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"]);
Expand All @@ -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);

Expand All @@ -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) {
Expand Down Expand Up @@ -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()) {
Expand Down Expand Up @@ -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;
Expand All @@ -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
Expand Down
1 change: 1 addition & 0 deletions src/util/enum.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,7 @@ EnumType EnumParser<EnumType>::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<modules::hyprland::Workspaces::SortMethod>;
template struct EnumParser<modules::hyprland::Workspaces::WindowSortMethod>;
template struct EnumParser<modules::hyprland::Workspaces::ActiveWindowPosition>;
template struct EnumParser<util::KillSignalAction>;

Expand Down