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
2 changes: 1 addition & 1 deletion include/modules/hyprland/window.hpp
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@ class Window : public waybar::AAppIconLabel, public EventHandler {

private:
struct Workspace {
int id = 0;
std::string address;
int windows = 0;
std::string last_window;
std::string last_window_title;
Expand Down
2 changes: 1 addition & 1 deletion include/modules/hyprland/windowcount.hpp
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@ class WindowCount : public waybar::AAppIconLabel, public EventHandler {

private:
struct Workspace {
int id;
std::string address;
int windows;
bool hasfullscreen;
static auto parse(const Json::Value& value) -> Workspace;
Expand Down
19 changes: 11 additions & 8 deletions include/modules/hyprland/workspace.hpp
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@
#include "bar.hpp"
#include "modules/hyprland/backend.hpp"
#include "modules/hyprland/windowcreationpayload.hpp"
#include "modules/hyprland/workspace_identity.hpp"
#include "util/enum.hpp"
#include "util/regex_collection.hpp"

Expand All @@ -34,11 +35,14 @@ class Workspace {
std::string& selectString(std::map<std::string, std::string>& string_map);
Gtk::Button& button() { return m_button; };

int id() const { return m_id; };
std::string name() const { return m_name; };
const WorkspaceIdentity& identity() const { return m_identity; };
const std::string& address() const { return m_identity.address; };
WorkspaceKind kind() const { return m_identity.kind; };
std::optional<int> number() const { return m_identity.number(); };
std::string name() const { return m_identity.name; };
std::string output() const { return m_output; };
bool isActive() const { return m_isActive; };
bool isSpecial() const { return m_isSpecial; };
bool isSpecial() const { return m_identity.kind == WorkspaceKind::Special; };
bool isPersistent() const { return m_isPersistentRule || m_isPersistentConfig; };
bool isPersistentConfig() const { return m_isPersistentConfig; };
bool isPersistentRule() const { return m_isPersistentRule; };
Expand All @@ -61,8 +65,9 @@ class Workspace {
void setUrgent(bool value = true) { m_isUrgent = value; };
void setVisible(bool value = true) { m_isVisible = value; };
void setWindows(uint value) { m_windows = value; };
void setId(int value) { m_id = value; };
void setName(std::string const& value) { m_name = value; };
void setAddress(std::string const& value);
void setIdentity(WorkspaceIdentity const& value) { m_identity = value; };
void setName(std::string const& value) { m_identity.name = value; };
void setOutput(std::string const& value) { m_output = value; };
bool containsWindow(WindowAddress const& addr) const {
return std::ranges::any_of(m_windowMap,
Expand All @@ -80,13 +85,11 @@ class Workspace {
private:
Workspaces& m_workspaceManager;

int m_id;
std::string m_name;
WorkspaceIdentity m_identity;
std::string m_prevNameClass;
std::string m_output;
uint m_windows;
bool m_isActive = false;
bool m_isSpecial = false;
bool m_isPersistentRule = false; // represents the persistent state in hyprland
bool m_isPersistentConfig = false; // represents the persistent state in the Waybar config
bool m_isUrgent = false;
Expand Down
123 changes: 123 additions & 0 deletions include/modules/hyprland/workspace_identity.hpp
Original file line number Diff line number Diff line change
@@ -0,0 +1,123 @@
#pragma once

#include <json/value.h>

#include <optional>
#include <string>
#include <utility>

namespace waybar::modules::hyprland {

// The declaration order is load-bearing: it is the display order the
// workspaces module groups by, which workspaceKindRank() reads off directly.
enum class WorkspaceKind { Numbered, Named, Special };

// The hyprwm/Hyprland#16140 IPC `type` name for a kind, which spells out all
// three kinds. Hyprland has since collapsed numbered and named into `normal`,
// but parseWorkspaceIdentity() still reads these, so they remain the unambiguous
// way to build a payload that has no `id`.
const char* workspaceTypeName(WorkspaceKind kind);

// Classifies a bare string for the paths that have no IPC `type` field to read:
// the `workspacev2`-style rename that moves a workspace between the numbered
// and named namespaces, and `persistent-workspaces` entries.
WorkspaceKind workspaceKindForAddress(const std::string& address);

@kolayne kolayne Sep 18, 2026 •

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Should workspaceKindForAddress and parseWorkspaceSelector become just constructors of the corresponding classes?

Maybe some other functions can be converted to methods, too.

Upd: enum classes cannot have methods.


// The name to display for a workspace whose IPC payload reported `rawName`.
//
// Hyprland namespaces a special workspace as `special:<name>`, and exactly one
// such prefix is removed -- so a special workspace the user named `special:123`
// is reported as `special:special:123` and displays as `special:123`.
//
// This is not idempotent, and is not meant to be: it maps a raw IPC name to a
// display name once. Nothing calls it on a name that has already been through
// it -- WorkspaceIdentity stores the display name, so every later reader takes
// it from there. The kind is still checked alongside the prefix because a
// Hyprland older than 0.41 reported the generic special workspace as a bare
// `special`, with no prefix to strip.
//
// No other kind carries a prefix. In particular `name:` is selector syntax that
// Hyprland never prepends to a reported name, so a named workspace actually
// called `name:foo` displays verbatim.
std::string workspaceDisplayName(const std::string& rawName, WorkspaceKind kind);

// A workspace named the way a user writes it -- a `persistent-workspaces` entry
// or the identifier Hyprland puts in an event payload -- split into the kind it
// selects and the workspace name it selects.
struct WorkspaceSelector {
WorkspaceKind kind = WorkspaceKind::Named;
std::string name;

WorkspaceSelector(WorkspaceKind kind, std::string name) : kind(kind), name(std::move(name)) {}

// Parses selector syntax:
// "7" -> {Numbered, "7"}
// "web" -> {Named, "web"}
// "name:web" -> {Named, "web"}
// "special" -> {Special, "special"}
// "special:spotify" -> {Special, "spotify"}
// Every string names some workspace, so this parse is total and belongs in a
// constructor. Contrast parseWorkspaceIdentity(), which can fail.
// TODO: support the rest of the selector grammar.
// https://wiki.hyprland.org/Configuring/Workspace-Rules/#workspace-selectors
explicit WorkspaceSelector(const std::string& selector);

// The raw IPC name Hyprland reports for the workspace this selects. Inverse
// of workspaceDisplayName(), used to build the placeholder payload for a
// persistent-workspaces entry so that the Workspace constructor only ever
// sees IPC-shaped names.
std::string rawName() const;
};

// A workspace's stable key, display name and kind, normalized across Hyprland
// IPC schema versions.
struct WorkspaceIdentity {
std::string address;
std::string name;
WorkspaceKind kind = WorkspaceKind::Named;

// Read off the address rather than stored alongside it, so the two cannot
// disagree. Empty unless this workspace is Numbered.
std::optional<int> number() const;

WorkspaceSelector asSelector() const { return {kind, name}; }

// Applies a `changeworkspaceid` event, which Hyprland only emits for a
// numbered workspace. Like Hyprland, a name set by `renameworkspace` is kept
// and a name still equal to the old number follows the new one.
void renumber(const std::string& newAddress);

bool matches(const WorkspaceSelector& selector) const {
// The kind is what separates a named workspace `foo` from a special one
// `special:foo`: both display `foo`.
return kind == selector.kind && name == selector.name;
}

// True when `identifier` -- the workspace field of an IPC event payload, or a
// `persistent-workspaces` entry -- refers to this workspace. The address
// alone is not enough: Hyprland announces a named workspace by the `name:foo`
// selector it was created with while reporting its address as `foo`.
bool matches(const std::string& identifier) const {
return address == identifier || matches(WorkspaceSelector{identifier});
}

bool operator==(const WorkspaceIdentity& other) const { return address == other.address; }
};

// Normalizes a workspace object from `workspaces`, `activeworkspace`, or a
// client's nested "workspace" value. Returns nullopt when the payload carries
// neither schema's identity fields -- a parse that can fail, so a factory
// rather than a constructor.
std::optional<WorkspaceIdentity> parseWorkspaceIdentity(const Json::Value& workspace);

// Display grouping for the workspaces module: numbered -> named -> special.
// Taken from WorkspaceKind's declaration order rather than a second, hand-kept
// table that could drift from it.
constexpr int workspaceKindRank(WorkspaceKind kind) { return static_cast<int>(kind); }

// The `sort-by: id` and `sort-by: default` orderings. Both are strict weak
// orderings even for a Numbered workspace whose address yielded no number.
bool workspaceLessById(const WorkspaceIdentity& a, const WorkspaceIdentity& b);
bool workspaceLessByDefault(const WorkspaceIdentity& a, const WorkspaceIdentity& b);

} // namespace waybar::modules::hyprland
11 changes: 7 additions & 4 deletions include/modules/hyprland/workspaces.hpp
Original file line number Diff line number Diff line change
Expand Up @@ -83,6 +83,10 @@ class Workspaces : public AModule, public EventHandler {

static Json::Value createMonitorWorkspaceData(std::string const& name,
std::string const& monitor);
// Locates a workspace by its stable address, falling back to the selector
// syntax for the identifiers that are not addresses.
std::vector<std::unique_ptr<Workspace>>::iterator findWorkspace(
std::string const& addressOrSelector);
void removeWorkspace(std::string const& workspaceString);
void setUrgentWorkspace(std::string const& windowaddress);

Expand All @@ -109,7 +113,6 @@ class Workspaces : public AModule, public EventHandler {
void onWorkspaceMoved(std::string const& payload);
void onWorkspaceRenamed(std::string const& payload);
void onWorkspaceIdChanged(std::string const& payload);
static std::optional<int> parseWorkspaceId(std::string const& workspaceIdStr);

// monitor events
void onMonitorFocused(std::string const& payload);
Expand Down Expand Up @@ -139,11 +142,11 @@ class Workspaces : public AModule, public EventHandler {
void doUpdate();
void removeWorkspacesToRemove();
void createWorkspacesToCreate();
static std::vector<int> getVisibleWorkspaces();
static std::vector<std::string> getVisibleWorkspaces();
void updateWorkspaceStates();
bool updateWindowsToCreate();

void extendOrphans(int workspaceId, Json::Value const& clientsJson);
void extendOrphans(const std::string& workspaceAddress, Json::Value const& clientsJson);
void registerOrphanWindow(WindowCreationPayload create_window_payload);

void initializeWorkspaces();
Expand Down Expand Up @@ -190,7 +193,7 @@ class Workspaces : public AModule, public EventHandler {

bool m_withIcon;
uint64_t m_monitorId;
int m_activeWorkspaceId;
std::string m_activeWorkspaceIdentifier;
std::string m_activeSpecialWorkspaceName;
std::vector<std::unique_ptr<Workspace>> m_workspaces;
std::vector<std::pair<Json::Value, Json::Value>> m_workspacesToCreate;
Expand Down
6 changes: 3 additions & 3 deletions man/waybar-hyprland-workspaces.5.scd
Original file line number Diff line number Diff line change
Expand Up @@ -192,9 +192,9 @@ This setting is ignored if *workspace-taskbar.enable* is set to true.
default: "default" ++
If set to number, workspaces will sort by number.
If set to name, workspaces will sort by name.
If set to id, workspaces will sort by id.
If set to id, workspaces will sort numerically; workspaces that have no number are grouped after them as numbered, then named, then special.
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.
If none of those, workspaces will sort with default behavior: grouped as numbered, then named, then special, and within a group by number where there is one and by name otherwise.

*tooltip*: ++
typeof: bool ++
Expand All @@ -212,7 +212,7 @@ This setting is ignored if *workspace-taskbar.enable* is set to true.

# FORMAT REPLACEMENTS

*{id}*: id of workspace assigned by compositor
*{id}*: Address of the workspace as reported by the compositor. For a numbered workspace this is its number; for named and special workspaces it is the address string (for example *special:magic*), where older Hyprland releases reported a negative id.

*{name}*: workspace name assigned by compositor

Expand Down
1 change: 1 addition & 0 deletions meson.build
Original file line number Diff line number Diff line change
Expand Up @@ -345,6 +345,7 @@ if true
'src/modules/hyprland/window.cpp',
'src/modules/hyprland/windowcount.cpp',
'src/modules/hyprland/workspace.cpp',
'src/modules/hyprland/workspace_identity.cpp',
'src/modules/hyprland/workspaces.cpp',
'src/modules/hyprland/windowcreationpayload.cpp',
)
Expand Down
44 changes: 32 additions & 12 deletions src/modules/hyprland/window.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@
#include <vector>

#include "modules/hyprland/backend.hpp"
#include "modules/hyprland/workspace_identity.hpp"
#include "util/rewrite_string.hpp"
#include "util/sanitize_str.hpp"

Expand Down Expand Up @@ -124,23 +125,40 @@ auto Window::getActiveWorkspace(const std::string& monitorName) -> Workspace {
if (monitor == std::end(monitors)) {
spdlog::warn("Monitor not found: {}", monitorName);
return Workspace{
.id = -1,
.address = "",
.windows = 0,
.last_window = "",
.last_window_title = "",
};
}
// An inactive special workspace still parses into an identity: it reports
// an empty name, and id 0 on the legacy schema.
auto special = parseWorkspaceIdentity((*monitor)["specialWorkspace"]);
if (special.has_value() && ((*monitor)["specialWorkspace"]["name"].asString().empty() ||
special->address.empty() || special->address == "0")) {
special.reset();
}
const auto active =
special.has_value() ? special : parseWorkspaceIdentity((*monitor)["activeWorkspace"]);
if (!active.has_value()) {
return Workspace{
.address = "",
.windows = 0,
.last_window = "",
.last_window_title = "",
};
}
const int special_id = (*monitor)["specialWorkspace"]["id"].asInt();
const int id = special_id != 0 ? special_id : (*monitor)["activeWorkspace"]["id"].asInt();

const auto workspaces = IPC::inst().getSocket1JsonReply("workspaces");
if (workspaces.isArray()) {
auto workspace = std::ranges::find_if(
workspaces, [&](const Json::Value& workspace) { return workspace["id"] == id; });
auto workspace = std::ranges::find_if(workspaces, [&](const Json::Value& workspace) {
const auto identity = parseWorkspaceIdentity(workspace);
return identity.has_value() && identity->address == active->address;
});
if (workspace == std::end(workspaces)) {
spdlog::warn("No workspace with id {}", id);
spdlog::warn("No workspace with address {}", active->address);
return Workspace{
.id = -1,
.address = "",
.windows = 0,
.last_window = "",
.last_window_title = "",
Expand All @@ -155,7 +173,7 @@ auto Window::getActiveWorkspace(const std::string& monitorName) -> Workspace {

auto Window::Workspace::parse(const Json::Value& value) -> Window::Workspace {
return Workspace{
.id = value["id"].asInt(),
.address = parseWorkspaceIdentity(value).value_or(WorkspaceIdentity{}).address,
.windows = value["windows"].asInt(),
.last_window = value["lastwindow"].asString(),
.last_window_title = value["lastwindowtitle"].asString(),
Expand Down Expand Up @@ -209,10 +227,12 @@ void Window::queryActiveWorkspace() {
windowData_ = WindowData::parse(*activeWindow);
updateAppIconName(windowData_.class_name, windowData_.initial_class_name);
std::vector<Json::Value> workspaceWindows;
std::ranges::copy_if(
clients, std::back_inserter(workspaceWindows), [&](const Json::Value& window) {
return window["workspace"]["id"] == workspace_.id && window["mapped"].asBool();
});
std::ranges::copy_if(clients, std::back_inserter(workspaceWindows),
[&](const Json::Value& window) {
const auto identity = parseWorkspaceIdentity(window["workspace"]);
return identity.has_value() && identity->address == workspace_.address &&
window["mapped"].asBool();
});
swallowing_ = std::ranges::any_of(workspaceWindows, [&](const Json::Value& window) {
return !window["swallowing"].isNull() && window["swallowing"].asString() != "0x0";
});
Expand Down
Loading