diff --git a/.gitignore b/.gitignore index 68bc0dc4..b486237e 100644 --- a/.gitignore +++ b/.gitignore @@ -48,3 +48,5 @@ packagecache # Nix result result-* + +.ccls-cache diff --git a/include/modules/hyprland/windowcreationpayload.hpp b/include/modules/hyprland/windowcreationpayload.hpp new file mode 100644 index 00000000..45229ed4 --- /dev/null +++ b/include/modules/hyprland/windowcreationpayload.hpp @@ -0,0 +1,61 @@ +#pragma once + +#include +#include +#include + +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include "AModule.hpp" +#include "bar.hpp" +#include "modules/hyprland/backend.hpp" +#include "util/enum.hpp" +#include "util/regex_collection.hpp" + +using WindowAddress = std::string; + +namespace waybar::modules::hyprland { + +class Workspaces; + +class WindowCreationPayload { + public: + WindowCreationPayload(std::string workspace_name, WindowAddress window_address, + std::string window_repr); + WindowCreationPayload(std::string workspace_name, WindowAddress window_address, + std::string window_class, std::string window_title); + WindowCreationPayload(Json::Value const& client_data); + + int incrementTimeSpentUncreated(); + bool isEmpty(Workspaces& workspace_manager); + bool reprIsReady() const { return std::holds_alternative(m_window); } + std::string repr(Workspaces& workspace_manager); + + std::string getWorkspaceName() const { return m_workspaceName; } + WindowAddress getAddress() const { return m_windowAddress; } + + void moveToWorksace(std::string& new_workspace_name); + + private: + void clearAddr(); + void clearWorkspaceName(); + + using Repr = std::string; + using ClassAndTitle = std::pair; + std::variant m_window; + + WindowAddress m_windowAddress; + std::string m_workspaceName; + + int m_timeSpentUncreated = 0; +}; + +} // namespace waybar::modules::hyprland diff --git a/include/modules/hyprland/workspace.hpp b/include/modules/hyprland/workspace.hpp new file mode 100644 index 00000000..f1fea4e8 --- /dev/null +++ b/include/modules/hyprland/workspace.hpp @@ -0,0 +1,88 @@ +#pragma once + +#include +#include +#include + +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include "AModule.hpp" +#include "bar.hpp" +#include "modules/hyprland/backend.hpp" +#include "modules/hyprland/windowcreationpayload.hpp" +#include "util/enum.hpp" +#include "util/regex_collection.hpp" + +using WindowAddress = std::string; + +namespace waybar::modules::hyprland { + +class Workspaces; +class Workspace { + public: + explicit Workspace(const Json::Value& workspace_data, Workspaces& workspace_manager, + const Json::Value& clients_data = Json::Value::nullRef); + std::string& selectIcon(std::map& icons_map); + Gtk::Button& button() { return m_button; }; + + int id() const { return m_id; }; + std::string name() const { return m_name; }; + std::string output() const { return m_output; }; + bool isActive() const { return m_isActive; }; + bool isSpecial() const { return m_isSpecial; }; + bool isPersistent() const { return m_isPersistentRule || m_isPersistentConfig; }; + bool isPersistentConfig() const { return m_isPersistentConfig; }; + bool isPersistentRule() const { return m_isPersistentRule; }; + bool isVisible() const { return m_isVisible; }; + bool isEmpty() const { return m_windows == 0; }; + bool isUrgent() const { return m_isUrgent; }; + + bool handleClicked(GdkEventButton* bt) const; + void setActive(bool value = true) { m_isActive = value; }; + void setPersistentRule(bool value = true) { m_isPersistentRule = value; }; + void setPersistentConfig(bool value = true) { m_isPersistentConfig = value; }; + void setUrgent(bool value = true) { m_isUrgent = value; }; + void setVisible(bool value = true) { m_isVisible = value; }; + void setWindows(uint value) { m_windows = value; }; + void setName(std::string const& value) { m_name = value; }; + void setOutput(std::string const& value) { m_output = value; }; + bool containsWindow(WindowAddress const& addr) const { return m_windowMap.contains(addr); } + void insertWindow(WindowCreationPayload create_window_paylod); + std::string removeWindow(WindowAddress const& addr); + void initializeWindowMap(const Json::Value& clients_data); + + bool onWindowOpened(WindowCreationPayload const& create_window_paylod); + std::optional closeWindow(WindowAddress const& addr); + + void update(const std::string& format, const std::string& icon); + + private: + Workspaces& m_workspaceManager; + + int m_id; + std::string m_name; + 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; + bool m_isVisible = false; + + std::map m_windowMap; + + Gtk::Button m_button; + Gtk::Box m_content; + Gtk::Label m_label; +}; + +} // namespace waybar::modules::hyprland diff --git a/include/modules/hyprland/workspaces.hpp b/include/modules/hyprland/workspaces.hpp index 82872348..e99bf40c 100644 --- a/include/modules/hyprland/workspaces.hpp +++ b/include/modules/hyprland/workspaces.hpp @@ -17,6 +17,8 @@ #include "AModule.hpp" #include "bar.hpp" #include "modules/hyprland/backend.hpp" +#include "modules/hyprland/windowcreationpayload.hpp" +#include "modules/hyprland/workspace.hpp" #include "util/enum.hpp" #include "util/regex_collection.hpp" @@ -26,97 +28,6 @@ namespace waybar::modules::hyprland { class Workspaces; -class WindowCreationPayload { - public: - WindowCreationPayload(std::string workspace_name, WindowAddress window_address, - std::string window_repr); - WindowCreationPayload(std::string workspace_name, WindowAddress window_address, - std::string window_class, std::string window_title); - WindowCreationPayload(Json::Value const& client_data); - - int incrementTimeSpentUncreated(); - bool isEmpty(Workspaces& workspace_manager); - bool reprIsReady() const { return std::holds_alternative(m_window); } - std::string repr(Workspaces& workspace_manager); - - std::string getWorkspaceName() const { return m_workspaceName; } - WindowAddress getAddress() const { return m_windowAddress; } - - void moveToWorksace(std::string& new_workspace_name); - - private: - void clearAddr(); - void clearWorkspaceName(); - - using Repr = std::string; - using ClassAndTitle = std::pair; - std::variant m_window; - - WindowAddress m_windowAddress; - std::string m_workspaceName; - - int m_timeSpentUncreated = 0; -}; - -class Workspace { - public: - explicit Workspace(const Json::Value& workspace_data, Workspaces& workspace_manager, - const Json::Value& clients_data = Json::Value::nullRef); - std::string& selectIcon(std::map& icons_map); - Gtk::Button& button() { return m_button; }; - - int id() const { return m_id; }; - std::string name() const { return m_name; }; - std::string output() const { return m_output; }; - bool isActive() const { return m_isActive; }; - bool isSpecial() const { return m_isSpecial; }; - bool isPersistent() const { return m_isPersistentRule || m_isPersistentConfig; }; - bool isPersistentConfig() const { return m_isPersistentConfig; }; - bool isPersistentRule() const { return m_isPersistentRule; }; - bool isVisible() const { return m_isVisible; }; - bool isEmpty() const { return m_windows == 0; }; - bool isUrgent() const { return m_isUrgent; }; - - bool handleClicked(GdkEventButton* bt) const; - void setActive(bool value = true) { m_isActive = value; }; - void setPersistentRule(bool value = true) { m_isPersistentRule = value; }; - void setPersistentConfig(bool value = true) { m_isPersistentConfig = value; }; - void setUrgent(bool value = true) { m_isUrgent = value; }; - void setVisible(bool value = true) { m_isVisible = value; }; - void setWindows(uint value) { m_windows = value; }; - void setName(std::string const& value) { m_name = value; }; - void setOutput(std::string const& value) { m_output = value; }; - bool containsWindow(WindowAddress const& addr) const { return m_windowMap.contains(addr); } - void insertWindow(WindowCreationPayload create_window_paylod); - std::string removeWindow(WindowAddress const& addr); - void initializeWindowMap(const Json::Value& clients_data); - - bool onWindowOpened(WindowCreationPayload const& create_window_paylod); - std::optional closeWindow(WindowAddress const& addr); - - void update(const std::string& format, const std::string& icon); - - private: - Workspaces& m_workspaceManager; - - int m_id; - std::string m_name; - 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; - bool m_isVisible = false; - - std::map m_windowMap; - - Gtk::Button m_button; - Gtk::Box m_content; - Gtk::Label m_label; -}; - class Workspaces : public AModule, public EventHandler { public: Workspaces(const std::string&, const waybar::Bar&, const Json::Value&); @@ -143,9 +54,21 @@ class Workspaces : public AModule, public EventHandler { void sortWorkspaces(); void createWorkspace(Json::Value const& workspaceData, Json::Value const& clientsData = Json::Value::nullRef); + + Json::Value createMonitorWorkspaceData(std::string const& name, std::string const& monitor); void removeWorkspace(std::string const& name); void setUrgentWorkspace(std::string const& windowaddress); + + // Config void parseConfig(const Json::Value& config); + auto populateIconsMap(const Json::Value& formatIcons) -> void; + auto populateBoolConfig(const Json::Value& config, const std::string& key, bool& member) -> void; + auto populateSortByConfig(const Json::Value& config) -> void; + auto populateIgnoreWorkspacesConfig(const Json::Value& config) -> void; + auto populatePersistentWorkspacesConfig(const Json::Value& config) -> void; + auto populateFormatWindowSeparatorConfig(const Json::Value& config) -> void; + auto populateWindowRewriteConfig(const Json::Value& config) -> void; + void registerIpc(); // workspace events @@ -171,7 +94,13 @@ class Workspaces : public AModule, public EventHandler { int windowRewritePriorityFunction(std::string const& window_rule); + // Update methods void doUpdate(); + void removeWorkspacesToRemove(); + void createWorkspacesToCreate(); + std::vector getVisibleWorkspaces(); + void updateWorkspaceStates(const std::vector& visibleWorkspaces); + bool updateWindowsToCreate(); void extendOrphans(int workspaceId, Json::Value const& clientsJson); void registerOrphanWindow(WindowCreationPayload create_window_payload); diff --git a/meson.build b/meson.build index 102bb878..1af8e2b2 100644 --- a/meson.build +++ b/meson.build @@ -305,7 +305,9 @@ if true 'src/modules/hyprland/language.cpp', 'src/modules/hyprland/submap.cpp', 'src/modules/hyprland/window.cpp', + 'src/modules/hyprland/workspace.cpp', 'src/modules/hyprland/workspaces.cpp', + 'src/modules/hyprland/windowcreationpayload.cpp', ) man_files += files( 'man/waybar-hyprland-language.5.scd', diff --git a/src/modules/hyprland/windowcreationpayload.cpp b/src/modules/hyprland/windowcreationpayload.cpp new file mode 100644 index 00000000..22febc59 --- /dev/null +++ b/src/modules/hyprland/windowcreationpayload.cpp @@ -0,0 +1,111 @@ +#include "modules/hyprland/windowcreationpayload.hpp" + +#include +#include + +#include +#include +#include +#include +#include + +#include "modules/hyprland/workspaces.hpp" +#include "util/regex_collection.hpp" + +namespace waybar::modules::hyprland { + +WindowCreationPayload::WindowCreationPayload(Json::Value const &client_data) + : m_window(std::make_pair(client_data["class"].asString(), client_data["title"].asString())), + m_windowAddress(client_data["address"].asString()), + m_workspaceName(client_data["workspace"]["name"].asString()) { + clearAddr(); + clearWorkspaceName(); +} + +WindowCreationPayload::WindowCreationPayload(std::string workspace_name, + WindowAddress window_address, std::string window_repr) + : m_window(std::move(window_repr)), + m_windowAddress(std::move(window_address)), + m_workspaceName(std::move(workspace_name)) { + clearAddr(); + clearWorkspaceName(); +} + +WindowCreationPayload::WindowCreationPayload(std::string workspace_name, + WindowAddress window_address, std::string window_class, + std::string window_title) + : m_window(std::make_pair(std::move(window_class), std::move(window_title))), + m_windowAddress(std::move(window_address)), + m_workspaceName(std::move(workspace_name)) { + clearAddr(); + clearWorkspaceName(); +} + +void WindowCreationPayload::clearAddr() { + // substr(2, ...) is necessary because Hyprland's JSON follows this format: + // 0x{ADDR} + // While Hyprland's IPC follows this format: + // {ADDR} + static const std::string ADDR_PREFIX = "0x"; + static const int ADDR_PREFIX_LEN = ADDR_PREFIX.length(); + + if (m_windowAddress.starts_with(ADDR_PREFIX)) { + m_windowAddress = + m_windowAddress.substr(ADDR_PREFIX_LEN, m_windowAddress.length() - ADDR_PREFIX_LEN); + } +} + +void WindowCreationPayload::clearWorkspaceName() { + // The workspace name may optionally feature "special:" at the beginning. + // If so, we need to remove it because the workspace is saved WITHOUT the + // special qualifier. The reasoning is that not all of Hyprland's IPC events + // use this qualifier, so it's better to be consistent about our uses. + + static const std::string SPECIAL_QUALIFIER_PREFIX = "special:"; + static const int SPECIAL_QUALIFIER_PREFIX_LEN = SPECIAL_QUALIFIER_PREFIX.length(); + + if (m_workspaceName.starts_with(SPECIAL_QUALIFIER_PREFIX)) { + m_workspaceName = m_workspaceName.substr( + SPECIAL_QUALIFIER_PREFIX_LEN, m_workspaceName.length() - SPECIAL_QUALIFIER_PREFIX_LEN); + } + + std::size_t spaceFound = m_workspaceName.find(' '); + if (spaceFound != std::string::npos) { + m_workspaceName.erase(m_workspaceName.begin() + spaceFound, m_workspaceName.end()); + } +} + +bool WindowCreationPayload::isEmpty(Workspaces &workspace_manager) { + if (std::holds_alternative(m_window)) { + return std::get(m_window).empty(); + } + if (std::holds_alternative(m_window)) { + auto [window_class, window_title] = std::get(m_window); + return (window_class.empty() && + (!workspace_manager.windowRewriteConfigUsesTitle() || window_title.empty())); + } + // Unreachable + spdlog::error("WorkspaceWindow::isEmpty: Unreachable"); + throw std::runtime_error("WorkspaceWindow::isEmpty: Unreachable"); +} + +int WindowCreationPayload::incrementTimeSpentUncreated() { return m_timeSpentUncreated++; } + +void WindowCreationPayload::moveToWorksace(std::string &new_workspace_name) { + m_workspaceName = new_workspace_name; +} + +std::string WindowCreationPayload::repr(Workspaces &workspace_manager) { + if (std::holds_alternative(m_window)) { + return std::get(m_window); + } + if (std::holds_alternative(m_window)) { + auto [window_class, window_title] = std::get(m_window); + return workspace_manager.getRewrite(window_class, window_title); + } + // Unreachable + spdlog::error("WorkspaceWindow::repr: Unreachable"); + throw std::runtime_error("WorkspaceWindow::repr: Unreachable"); +} + +} // namespace waybar::modules::hyprland diff --git a/src/modules/hyprland/workspace.cpp b/src/modules/hyprland/workspace.cpp new file mode 100644 index 00000000..694d3b0d --- /dev/null +++ b/src/modules/hyprland/workspace.cpp @@ -0,0 +1,214 @@ +#include +#include + +#include +#include +#include +#include +#include + +#include "modules/hyprland/workspaces.hpp" +#include "util/regex_collection.hpp" + +namespace waybar::modules::hyprland { + +Workspace::Workspace(const Json::Value &workspace_data, Workspaces &workspace_manager, + const Json::Value &clients_data) + : m_workspaceManager(workspace_manager), + m_id(workspace_data["id"].asInt()), + m_name(workspace_data["name"].asString()), + m_output(workspace_data["monitor"].asString()), // TODO:allow using monitor desc + m_windows(workspace_data["windows"].asInt()), + m_isActive(true), + m_isPersistentRule(workspace_data["persistent-rule"].asBool()), + m_isPersistentConfig(workspace_data["persistent-config"].asBool()) { + if (m_name.starts_with("name:")) { + m_name = m_name.substr(5); + } else if (m_name.starts_with("special")) { + m_name = m_id == -99 ? m_name : m_name.substr(8); + m_isSpecial = true; + } + + m_button.add_events(Gdk::BUTTON_PRESS_MASK); + m_button.signal_button_press_event().connect(sigc::mem_fun(*this, &Workspace::handleClicked), + false); + + m_button.set_relief(Gtk::RELIEF_NONE); + m_content.set_center_widget(m_label); + m_button.add(m_content); + + initializeWindowMap(clients_data); +} + +void addOrRemoveClass(const Glib::RefPtr &context, bool condition, + const std::string &class_name) { + if (condition) { + context->add_class(class_name); + } else { + context->remove_class(class_name); + } +} + +std::optional Workspace::closeWindow(WindowAddress const &addr) { + if (m_windowMap.contains(addr)) { + return removeWindow(addr); + } + return std::nullopt; +} + +bool Workspace::handleClicked(GdkEventButton *bt) const { + if (bt->type == GDK_BUTTON_PRESS) { + try { + if (id() > 0) { // normal + if (m_workspaceManager.moveToMonitor()) { + gIPC->getSocket1Reply("dispatch focusworkspaceoncurrentmonitor " + std::to_string(id())); + } else { + gIPC->getSocket1Reply("dispatch workspace " + std::to_string(id())); + } + } else if (!isSpecial()) { // named (this includes persistent) + if (m_workspaceManager.moveToMonitor()) { + gIPC->getSocket1Reply("dispatch focusworkspaceoncurrentmonitor name:" + name()); + } else { + gIPC->getSocket1Reply("dispatch workspace name:" + name()); + } + } else if (id() != -99) { // named special + gIPC->getSocket1Reply("dispatch togglespecialworkspace " + name()); + } else { // special + gIPC->getSocket1Reply("dispatch togglespecialworkspace"); + } + return true; + } catch (const std::exception &e) { + spdlog::error("Failed to dispatch workspace: {}", e.what()); + } + } + return false; +} + +void Workspace::initializeWindowMap(const Json::Value &clients_data) { + m_windowMap.clear(); + for (auto client : clients_data) { + if (client["workspace"]["id"].asInt() == id()) { + insertWindow({client}); + } + } +} + +void Workspace::insertWindow(WindowCreationPayload create_window_paylod) { + if (!create_window_paylod.isEmpty(m_workspaceManager)) { + m_windowMap[create_window_paylod.getAddress()] = create_window_paylod.repr(m_workspaceManager); + } +}; + +bool Workspace::onWindowOpened(WindowCreationPayload const &create_window_paylod) { + if (create_window_paylod.getWorkspaceName() == name()) { + insertWindow(create_window_paylod); + return true; + } + return false; +} + +std::string Workspace::removeWindow(WindowAddress const &addr) { + std::string windowRepr = m_windowMap[addr]; + m_windowMap.erase(addr); + return windowRepr; +} + +std::string &Workspace::selectIcon(std::map &icons_map) { + spdlog::trace("Selecting icon for workspace {}", name()); + if (isUrgent()) { + auto urgentIconIt = icons_map.find("urgent"); + if (urgentIconIt != icons_map.end()) { + return urgentIconIt->second; + } + } + + if (isActive()) { + auto activeIconIt = icons_map.find("active"); + if (activeIconIt != icons_map.end()) { + return activeIconIt->second; + } + } + + if (isSpecial()) { + auto specialIconIt = icons_map.find("special"); + if (specialIconIt != icons_map.end()) { + return specialIconIt->second; + } + } + + auto namedIconIt = icons_map.find(name()); + if (namedIconIt != icons_map.end()) { + return namedIconIt->second; + } + + if (isVisible()) { + auto visibleIconIt = icons_map.find("visible"); + if (visibleIconIt != icons_map.end()) { + return visibleIconIt->second; + } + } + + if (isEmpty()) { + auto emptyIconIt = icons_map.find("empty"); + if (emptyIconIt != icons_map.end()) { + return emptyIconIt->second; + } + } + + if (isPersistent()) { + auto persistentIconIt = icons_map.find("persistent"); + if (persistentIconIt != icons_map.end()) { + return persistentIconIt->second; + } + } + + auto defaultIconIt = icons_map.find("default"); + if (defaultIconIt != icons_map.end()) { + return defaultIconIt->second; + } + + return m_name; +} + +void Workspace::update(const std::string &format, const std::string &icon) { + // clang-format off + if (this->m_workspaceManager.activeOnly() && \ + !this->isActive() && \ + !this->isPersistent() && \ + !this->isVisible() && \ + !this->isSpecial()) { + // clang-format on + // if activeOnly is true, hide if not active, persistent, visible or special + m_button.hide(); + return; + } + m_button.show(); + + auto styleContext = m_button.get_style_context(); + addOrRemoveClass(styleContext, isActive(), "active"); + addOrRemoveClass(styleContext, isSpecial(), "special"); + addOrRemoveClass(styleContext, isEmpty(), "empty"); + addOrRemoveClass(styleContext, isPersistent(), "persistent"); + addOrRemoveClass(styleContext, isUrgent(), "urgent"); + addOrRemoveClass(styleContext, isVisible(), "visible"); + addOrRemoveClass(styleContext, m_workspaceManager.getBarOutput() == output(), "hosting-monitor"); + + std::string windows; + auto windowSeparator = m_workspaceManager.getWindowSeparator(); + + bool isNotFirst = false; + + for (auto &[_pid, window_repr] : m_windowMap) { + if (isNotFirst) { + windows.append(windowSeparator); + } + isNotFirst = true; + windows.append(window_repr); + } + + m_label.set_markup(fmt::format(fmt::runtime(format), fmt::arg("id", id()), + fmt::arg("name", name()), fmt::arg("icon", icon), + fmt::arg("windows", windows))); +} + +} // namespace waybar::modules::hyprland diff --git a/src/modules/hyprland/workspaces.cpp b/src/modules/hyprland/workspaces.cpp index 3c03c708..eca30134 100644 --- a/src/modules/hyprland/workspaces.cpp +++ b/src/modules/hyprland/workspaces.cpp @@ -13,26 +13,6 @@ namespace waybar::modules::hyprland { -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 - bool const hasTitle = window_rule.find("title") != std::string::npos; - bool const hasClass = window_rule.find("class") != std::string::npos; - - if (hasTitle && hasClass) { - m_anyWindowRewriteRuleUsesTitle = true; - return 3; - } - if (hasTitle) { - m_anyWindowRewriteRuleUsesTitle = true; - return 2; - } - if (hasClass) { - return 1; - } - return 0; -} - Workspaces::Workspaces(const std::string &id, const Bar &bar, const Json::Value &config) : AModule(config, "workspaces", id, false, false), m_bar(bar), m_box(bar.orientation, 0) { modulesReady = true; @@ -54,125 +34,87 @@ Workspaces::Workspaces(const std::string &id, const Bar &bar, const Json::Value registerIpc(); } -auto Workspaces::parseConfig(const Json::Value &config) -> void { - const Json::Value &configFormat = config["format"]; +Workspaces::~Workspaces() { + gIPC->unregisterForIPC(this); + // wait for possible event handler to finish + std::lock_guard lg(m_mutex); +} - m_format = configFormat.isString() ? configFormat.asString() : "{name}"; - m_withIcon = m_format.find("{icon}") != std::string::npos; +void Workspaces::init() { + m_activeWorkspaceName = (gIPC->getSocket1JsonReply("activeworkspace"))["name"].asString(); - if (m_withIcon && m_iconsMap.empty()) { - Json::Value formatIcons = config["format-icons"]; - for (std::string &name : formatIcons.getMemberNames()) { - m_iconsMap.emplace(name, formatIcons[name].asString()); + initializeWorkspaces(); + dp.emit(); +} + +Json::Value Workspaces::createMonitorWorkspaceData(std::string const &name, + std::string const &monitor) { + spdlog::trace("Creating persistent workspace: {} on monitor {}", name, monitor); + Json::Value workspaceData; + try { + // numbered persistent workspaces get the name as ID + workspaceData["id"] = name == "special" ? -99 : std::stoi(name); + } catch (const std::exception &e) { + // named persistent workspaces start with ID=0 + workspaceData["id"] = 0; + } + workspaceData["name"] = name; + workspaceData["monitor"] = monitor; + workspaceData["windows"] = 0; + return workspaceData; +} + +void Workspaces::createWorkspace(Json::Value const &workspace_data, + Json::Value const &clients_data) { + auto workspaceName = workspace_data["name"].asString(); + spdlog::debug("Creating workspace {}", workspaceName); + + // avoid recreating existing workspaces + auto workspace = std::find_if( + m_workspaces.begin(), m_workspaces.end(), + [workspaceName](std::unique_ptr const &w) { + return (workspaceName.starts_with("special:") && workspaceName.substr(8) == w->name()) || + workspaceName == w->name(); + }); + + if (workspace != m_workspaces.end()) { + // don't recreate workspace, but update persistency if necessary + const auto keys = workspace_data.getMemberNames(); + + const auto *k = "persistent-rule"; + if (std::find(keys.begin(), keys.end(), k) != keys.end()) { + spdlog::debug("Set dynamic persistency of workspace {} to: {}", workspaceName, + workspace_data[k].asBool() ? "true" : "false"); + (*workspace)->setPersistentRule(workspace_data[k].asBool()); } - m_iconsMap.emplace("", ""); - } - auto configAllOutputs = config_["all-outputs"]; - if (configAllOutputs.isBool()) { - m_allOutputs = configAllOutputs.asBool(); - } - - auto configShowSpecial = config_["show-special"]; - if (configShowSpecial.isBool()) { - m_showSpecial = configShowSpecial.asBool(); - } - - auto configActiveOnly = config_["active-only"]; - if (configActiveOnly.isBool()) { - m_activeOnly = configActiveOnly.asBool(); - } - - auto configMoveToMonitor = config_["move-to-monitor"]; - if (configMoveToMonitor.isBool()) { - m_moveToMonitor = configMoveToMonitor.asBool(); - } - - auto configSortBy = config_["sort-by"]; - if (configSortBy.isString()) { - auto sortByStr = configSortBy.asString(); - try { - m_sortBy = m_enumParser.parseStringToEnum(sortByStr, m_sortMap); - } catch (const std::invalid_argument &e) { - // Handle the case where the string is not a valid enum representation. - m_sortBy = SortMethod::DEFAULT; - g_warning("Invalid string representation for sort-by. Falling back to default sort method."); + k = "persistent-config"; + if (std::find(keys.begin(), keys.end(), k) != keys.end()) { + spdlog::debug("Set config persistency of workspace {} to: {}", workspaceName, + workspace_data[k].asBool() ? "true" : "false"); + (*workspace)->setPersistentConfig(workspace_data[k].asBool()); } - } - Json::Value ignoreWorkspaces = config["ignore-workspaces"]; - if (ignoreWorkspaces.isArray()) { - for (Json::Value &workspaceRegex : ignoreWorkspaces) { - if (workspaceRegex.isString()) { - std::string ruleString = workspaceRegex.asString(); - try { - const std::regex rule{ruleString, std::regex_constants::icase}; - m_ignoreWorkspaces.emplace_back(rule); - } catch (const std::regex_error &e) { - spdlog::error("Invalid rule {}: {}", ruleString, e.what()); - } - } else { - spdlog::error("Not a string: '{}'", workspaceRegex); - } - } - } - - if (config_["persistent_workspaces"].isObject()) { - spdlog::warn( - "persistent_workspaces is deprecated. Please change config to use persistent-workspaces."); - } - - if (config_["persistent-workspaces"].isObject() || config_["persistent_workspaces"].isObject()) { - m_persistentWorkspaceConfig = config_["persistent-workspaces"].isObject() - ? config_["persistent-workspaces"] - : config_["persistent_workspaces"]; - } - - const Json::Value &formatWindowSeparator = config["format-window-separator"]; - m_formatWindowSeparator = - formatWindowSeparator.isString() ? formatWindowSeparator.asString() : " "; - - const Json::Value &windowRewrite = config["window-rewrite"]; - if (!windowRewrite.isObject()) { - spdlog::debug("window-rewrite is not defined or is not an object, using default rules."); return; } - const Json::Value &windowRewriteDefaultConfig = config["window-rewrite-default"]; - std::string windowRewriteDefault = - windowRewriteDefaultConfig.isString() ? windowRewriteDefaultConfig.asString() : "?"; - - m_windowRewriteRules = util::RegexCollection( - windowRewrite, windowRewriteDefault, - [this](std::string &window_rule) { return windowRewritePriorityFunction(window_rule); }); + // create new workspace + m_workspaces.emplace_back(std::make_unique(workspace_data, *this, clients_data)); + Gtk::Button &newWorkspaceButton = m_workspaces.back()->button(); + m_box.pack_start(newWorkspaceButton, false, false); + sortWorkspaces(); + newWorkspaceButton.show_all(); } -void Workspaces::registerOrphanWindow(WindowCreationPayload create_window_payload) { - if (!create_window_payload.isEmpty(*this)) { - m_orphanWindowMap[create_window_payload.getAddress()] = create_window_payload.repr(*this); +void Workspaces::createWorkspacesToCreate() { + for (const auto &[workspaceData, clientsData] : m_workspacesToCreate) { + createWorkspace(workspaceData, clientsData); } -} - -auto Workspaces::registerIpc() -> void { - gIPC->registerForIPC("workspace", this); - gIPC->registerForIPC("activespecial", this); - gIPC->registerForIPC("createworkspace", this); - gIPC->registerForIPC("destroyworkspace", this); - gIPC->registerForIPC("focusedmon", this); - gIPC->registerForIPC("moveworkspace", this); - gIPC->registerForIPC("renameworkspace", this); - gIPC->registerForIPC("openwindow", this); - gIPC->registerForIPC("closewindow", this); - gIPC->registerForIPC("movewindow", this); - gIPC->registerForIPC("urgent", this); - gIPC->registerForIPC("configreloaded", this); - - if (windowRewriteConfigUsesTitle()) { - spdlog::info( - "Registering for Hyprland's 'windowtitle' events because a user-defined window " - "rewrite rule uses the 'title' field."); - gIPC->registerForIPC("windowtitle", this); + if (!m_workspacesToCreate.empty()) { + updateWindowCount(); + sortWorkspaces(); } + m_workspacesToCreate.clear(); } /** @@ -184,108 +126,90 @@ auto Workspaces::registerIpc() -> void { void Workspaces::doUpdate() { std::unique_lock lock(m_mutex); - // remove workspaces that wait to be removed - for (auto &elem : m_workspacesToRemove) { - removeWorkspace(elem); - } - m_workspacesToRemove.clear(); + removeWorkspacesToRemove(); + createWorkspacesToCreate(); - // add workspaces that wait to be created - for (auto &[workspaceData, clientsData] : m_workspacesToCreate) { - createWorkspace(workspaceData, clientsData); - } - if (!m_workspacesToCreate.empty()) { - updateWindowCount(); - sortWorkspaces(); - } - m_workspacesToCreate.clear(); + std::vector visibleWorkspaces = getVisibleWorkspaces(); - // get all active workspaces - spdlog::trace("Getting active workspaces"); - auto monitors = gIPC->getSocket1JsonReply("monitors"); - std::vector visibleWorkspaces; - for (Json::Value &monitor : monitors) { - auto ws = monitor["activeWorkspace"]; - if (ws.isObject() && (ws["name"].isString())) { - visibleWorkspaces.push_back(ws["name"].asString()); - } - auto sws = monitor["specialWorkspace"]; - auto name = sws["name"].asString(); - if (sws.isObject() && (sws["name"].isString()) && !name.empty()) { - visibleWorkspaces.push_back(!name.starts_with("special:") ? name : name.substr(8)); - } - } + updateWorkspaceStates(visibleWorkspaces); + updateWindowCount(); + sortWorkspaces(); - spdlog::trace("Updating workspace states"); - auto updated_workspaces = gIPC->getSocket1JsonReply("workspaces"); - for (auto &workspace : m_workspaces) { - // active - workspace->setActive(workspace->name() == m_activeWorkspaceName || - workspace->name() == m_activeSpecialWorkspaceName); - // disable urgency if workspace is active - if (workspace->name() == m_activeWorkspaceName && workspace->isUrgent()) { - workspace->setUrgent(false); - } - - // visible - workspace->setVisible(std::find(visibleWorkspaces.begin(), visibleWorkspaces.end(), - workspace->name()) != visibleWorkspaces.end()); - - // set workspace icon - std::string &workspaceIcon = m_iconsMap[""]; - if (m_withIcon) { - workspaceIcon = workspace->selectIcon(m_iconsMap); - } - - // update m_output - auto updated_workspace = - std::find_if(updated_workspaces.begin(), updated_workspaces.end(), [&workspace](auto &w) { - auto wNameRaw = w["name"].asString(); - auto wName = wNameRaw.starts_with("special:") ? wNameRaw.substr(8) : wNameRaw; - return wName == workspace->name(); - }); - - if (updated_workspace != updated_workspaces.end()) { - workspace->setOutput((*updated_workspace)["monitor"].asString()); - } - - workspace->update(m_format, workspaceIcon); - } - - spdlog::trace("Updating window count"); - bool anyWindowCreated = false; - std::vector notCreated; - - for (auto &windowPayload : m_windowsToCreate) { - bool created = false; - for (auto &workspace : m_workspaces) { - if (workspace->onWindowOpened(windowPayload)) { - created = true; - anyWindowCreated = true; - break; - } - } - if (!created) { - static auto const WINDOW_CREATION_TIMEOUT = 2; - if (windowPayload.incrementTimeSpentUncreated() < WINDOW_CREATION_TIMEOUT) { - notCreated.push_back(windowPayload); - } else { - registerOrphanWindow(windowPayload); - } - } - } + bool anyWindowCreated = updateWindowsToCreate(); if (anyWindowCreated) { dp.emit(); } - - m_windowsToCreate.clear(); - m_windowsToCreate = notCreated; } -auto Workspaces::update() -> void { - doUpdate(); - AModule::update(); +void Workspaces::extendOrphans(int workspaceId, Json::Value const &clientsJson) { + spdlog::trace("Extending orphans with workspace {}", workspaceId); + for (const auto &client : clientsJson) { + if (client["workspace"]["id"].asInt() == workspaceId) { + registerOrphanWindow({client}); + } + } +} + +std::string Workspaces::getRewrite(std::string window_class, std::string window_title) { + std::string windowReprKey; + if (windowRewriteConfigUsesTitle()) { + windowReprKey = fmt::format("class<{}> title<{}>", window_class, window_title); + } else { + windowReprKey = fmt::format("class<{}>", window_class); + } + auto const rewriteRule = m_windowRewriteRules.get(windowReprKey); + return fmt::format(fmt::runtime(rewriteRule), fmt::arg("class", window_class), + fmt::arg("title", window_title)); +} + +std::vector Workspaces::getVisibleWorkspaces() { + std::vector visibleWorkspaces; + auto monitors = gIPC->getSocket1JsonReply("monitors"); + for (const auto &monitor : monitors) { + auto ws = monitor["activeWorkspace"]; + if (ws.isObject() && ws["name"].isString()) { + visibleWorkspaces.push_back(ws["name"].asString()); + } + auto sws = monitor["specialWorkspace"]; + auto name = sws["name"].asString(); + if (sws.isObject() && sws["name"].isString() && !name.empty()) { + visibleWorkspaces.push_back(!name.starts_with("special:") ? name : name.substr(8)); + } + } + return visibleWorkspaces; +} + +void Workspaces::initializeWorkspaces() { + spdlog::debug("Initializing workspaces"); + + // if the workspace rules changed since last initialization, make sure we reset everything: + for (auto &workspace : m_workspaces) { + m_workspacesToRemove.push_back(workspace->name()); + } + + // get all current workspaces + auto const workspacesJson = gIPC->getSocket1JsonReply("workspaces"); + auto const clientsJson = gIPC->getSocket1JsonReply("clients"); + + for (Json::Value workspaceJson : workspacesJson) { + std::string workspaceName = workspaceJson["name"].asString(); + if ((allOutputs() || m_bar.output->name == workspaceJson["monitor"].asString()) && + (!workspaceName.starts_with("special") || showSpecial()) && + !isWorkspaceIgnored(workspaceName)) { + m_workspacesToCreate.emplace_back(workspaceJson, clientsJson); + } else { + extendOrphans(workspaceJson["id"].asInt(), clientsJson); + } + } + + spdlog::debug("Initializing persistent workspaces"); + if (m_persistentWorkspaceConfig.isObject()) { + // a persistent workspace config is defined, so use that instead of workspace rules + loadPersistentWorkspacesFromConfig(clientsJson); + } + // load Hyprland's workspace rules + loadPersistentWorkspacesFromWorkspaceRules(clientsJson); } bool isDoubleSpecial(std::string const &workspace_name) { @@ -307,6 +231,90 @@ bool Workspaces::isWorkspaceIgnored(std::string const &name) { return false; } +void Workspaces::loadPersistentWorkspacesFromConfig(Json::Value const &clientsJson) { + spdlog::info("Loading persistent workspaces from Waybar config"); + const std::vector keys = m_persistentWorkspaceConfig.getMemberNames(); + std::vector persistentWorkspacesToCreate; + + const std::string currentMonitor = m_bar.output->name; + const bool monitorInConfig = std::find(keys.begin(), keys.end(), currentMonitor) != keys.end(); + for (const std::string &key : keys) { + // only add if either: + // 1. key is the current monitor name + // 2. key is "*" and this monitor is not already defined in the config + bool canCreate = key == currentMonitor || (key == "*" && !monitorInConfig); + const Json::Value &value = m_persistentWorkspaceConfig[key]; + spdlog::trace("Parsing persistent workspace config: {} => {}", key, value.toStyledString()); + + if (value.isInt()) { + // value is a number => create that many workspaces for this monitor + if (canCreate) { + int amount = value.asInt(); + spdlog::debug("Creating {} persistent workspaces for monitor {}", amount, currentMonitor); + for (int i = 0; i < amount; i++) { + persistentWorkspacesToCreate.emplace_back(std::to_string(m_monitorId * amount + i + 1)); + } + } + } else if (value.isArray() && !value.empty()) { + // value is an array => create defined workspaces for this monitor + if (canCreate) { + for (const Json::Value &workspace : value) { + if (workspace.isInt()) { + spdlog::debug("Creating workspace {} on monitor {}", workspace, currentMonitor); + persistentWorkspacesToCreate.emplace_back(std::to_string(workspace.asInt())); + } + } + } else { + // key is the workspace and value is array of monitors to create on + for (const Json::Value &monitor : value) { + if (monitor.isString() && monitor.asString() == currentMonitor) { + persistentWorkspacesToCreate.emplace_back(currentMonitor); + break; + } + } + } + } else { + // this workspace should be displayed on all monitors + persistentWorkspacesToCreate.emplace_back(key); + } + } + + for (auto const &workspace : persistentWorkspacesToCreate) { + auto workspaceData = createMonitorWorkspaceData(workspace, m_bar.output->name); + workspaceData["persistent-config"] = true; + m_workspacesToCreate.emplace_back(workspaceData, clientsJson); + } +} + +void Workspaces::loadPersistentWorkspacesFromWorkspaceRules(const Json::Value &clientsJson) { + spdlog::info("Loading persistent workspaces from Hyprland workspace rules"); + + auto const workspaceRules = gIPC->getSocket1JsonReply("workspacerules"); + for (Json::Value const &rule : workspaceRules) { + if (!rule["workspaceString"].isString()) { + spdlog::warn("Workspace rules: invalid workspaceString, skipping: {}", rule); + continue; + } + if (!rule["persistent"].asBool()) { + continue; + } + auto const &workspace = rule["workspaceString"].asString(); + auto const &monitor = rule["monitor"].asString(); + // create this workspace persistently if: + // 1. the allOutputs config option is enabled + // 2. the rule's monitor is the current monitor + // 3. no monitor is specified in the rule => assume it needs to be persistent on every monitor + if (allOutputs() || m_bar.output->name == monitor || monitor.empty()) { + // => persistent workspace should be shown on this monitor + auto workspaceData = createMonitorWorkspaceData(workspace, m_bar.output->name); + workspaceData["persistent-rule"] = true; + m_workspacesToCreate.emplace_back(workspaceData, clientsJson); + } else { + m_workspacesToRemove.emplace_back(workspace); + } + } +} + void Workspaces::onEvent(const std::string &ev) { std::lock_guard lock(m_mutex); std::string eventName(begin(ev), begin(ev) + ev.find_first_of('>')); @@ -557,102 +565,139 @@ void Workspaces::onConfigReloaded() { init(); } -void Workspaces::updateWindowCount() { - const Json::Value workspacesJson = gIPC->getSocket1JsonReply("workspaces"); - for (auto &workspace : m_workspaces) { - auto workspaceJson = - std::find_if(workspacesJson.begin(), workspacesJson.end(), [&](Json::Value const &x) { - return x["name"].asString() == workspace->name() || - (workspace->isSpecial() && x["name"].asString() == "special:" + workspace->name()); - }); - uint32_t count = 0; - if (workspaceJson != workspacesJson.end()) { - try { - count = (*workspaceJson)["windows"].asUInt(); - } catch (const std::exception &e) { - spdlog::error("Failed to update window count: {}", e.what()); +auto Workspaces::parseConfig(const Json::Value &config) -> void { + const auto &configFormat = config["format"]; + m_format = configFormat.isString() ? configFormat.asString() : "{name}"; + m_withIcon = m_format.find("{icon}") != std::string::npos; + + if (m_withIcon && m_iconsMap.empty()) { + populateIconsMap(config["format-icons"]); + } + + populateBoolConfig(config, "all-outputs", m_allOutputs); + populateBoolConfig(config, "show-special", m_showSpecial); + populateBoolConfig(config, "active-only", m_activeOnly); + populateBoolConfig(config, "move-to-monitor", m_moveToMonitor); + + populateSortByConfig(config); + populateIgnoreWorkspacesConfig(config); + populatePersistentWorkspacesConfig(config); + populateFormatWindowSeparatorConfig(config); + populateWindowRewriteConfig(config); +} + +auto Workspaces::populateIconsMap(const Json::Value &formatIcons) -> void { + for (const auto &name : formatIcons.getMemberNames()) { + m_iconsMap.emplace(name, formatIcons[name].asString()); + } + m_iconsMap.emplace("", ""); +} + +auto Workspaces::populateBoolConfig(const Json::Value &config, const std::string &key, bool &member) + -> void { + auto configValue = config[key]; + if (configValue.isBool()) { + member = configValue.asBool(); + } +} + +auto Workspaces::populateSortByConfig(const Json::Value &config) -> void { + auto configSortBy = config["sort-by"]; + if (configSortBy.isString()) { + auto sortByStr = configSortBy.asString(); + try { + m_sortBy = m_enumParser.parseStringToEnum(sortByStr, m_sortMap); + } catch (const std::invalid_argument &e) { + m_sortBy = SortMethod::DEFAULT; + spdlog::warn( + "Invalid string representation for sort-by. Falling back to default sort method."); + } + } +} + +auto Workspaces::populateIgnoreWorkspacesConfig(const Json::Value &config) -> void { + auto ignoreWorkspaces = config["ignore-workspaces"]; + if (ignoreWorkspaces.isArray()) { + for (const auto &workspaceRegex : ignoreWorkspaces) { + if (workspaceRegex.isString()) { + std::string ruleString = workspaceRegex.asString(); + try { + const std::regex rule{ruleString, std::regex_constants::icase}; + m_ignoreWorkspaces.emplace_back(rule); + } catch (const std::regex_error &e) { + spdlog::error("Invalid rule {}: {}", ruleString, e.what()); + } + } else { + spdlog::error("Not a string: '{}'", workspaceRegex); } } - workspace->setWindows(count); } } -void Workspace::initializeWindowMap(const Json::Value &clients_data) { - m_windowMap.clear(); - for (auto client : clients_data) { - if (client["workspace"]["id"].asInt() == id()) { - insertWindow({client}); - } +auto Workspaces::populatePersistentWorkspacesConfig(const Json::Value &config) -> void { + if (config.isMember("persistent-workspaces") || config.isMember("persistent_workspaces")) { + spdlog::warn( + "persistent_workspaces is deprecated. Please change config to use persistent-workspaces."); + m_persistentWorkspaceConfig = + config.get("persistent-workspaces", config.get("persistent_workspaces", Json::Value())); } } -void Workspace::insertWindow(WindowCreationPayload create_window_paylod) { - if (!create_window_paylod.isEmpty(m_workspaceManager)) { - m_windowMap[create_window_paylod.getAddress()] = create_window_paylod.repr(m_workspaceManager); - } -}; - -std::string Workspace::removeWindow(WindowAddress const &addr) { - std::string windowRepr = m_windowMap[addr]; - m_windowMap.erase(addr); - return windowRepr; +auto Workspaces::populateFormatWindowSeparatorConfig(const Json::Value &config) -> void { + auto formatWindowSeparator = config["format-window-separator"]; + m_formatWindowSeparator = + formatWindowSeparator.isString() ? formatWindowSeparator.asString() : " "; } -bool Workspace::onWindowOpened(WindowCreationPayload const &create_window_paylod) { - if (create_window_paylod.getWorkspaceName() == name()) { - insertWindow(create_window_paylod); - return true; - } - return false; -} - -std::optional Workspace::closeWindow(WindowAddress const &addr) { - if (m_windowMap.contains(addr)) { - return removeWindow(addr); - } - return std::nullopt; -} - -void Workspaces::createWorkspace(Json::Value const &workspace_data, - Json::Value const &clients_data) { - auto workspaceName = workspace_data["name"].asString(); - spdlog::debug("Creating workspace {}", workspaceName); - - // avoid recreating existing workspaces - auto workspace = std::find_if( - m_workspaces.begin(), m_workspaces.end(), - [workspaceName](std::unique_ptr const &w) { - return (workspaceName.starts_with("special:") && workspaceName.substr(8) == w->name()) || - workspaceName == w->name(); - }); - - if (workspace != m_workspaces.end()) { - // don't recreate workspace, but update persistency if necessary - const auto keys = workspace_data.getMemberNames(); - - const auto *k = "persistent-rule"; - if (std::find(keys.begin(), keys.end(), k) != keys.end()) { - spdlog::debug("Set dynamic persistency of workspace {} to: {}", workspaceName, - workspace_data[k].asBool() ? "true" : "false"); - (*workspace)->setPersistentRule(workspace_data[k].asBool()); - } - - k = "persistent-config"; - if (std::find(keys.begin(), keys.end(), k) != keys.end()) { - spdlog::debug("Set config persistency of workspace {} to: {}", workspaceName, - workspace_data[k].asBool() ? "true" : "false"); - (*workspace)->setPersistentConfig(workspace_data[k].asBool()); - } - +auto Workspaces::populateWindowRewriteConfig(const Json::Value &config) -> void { + const auto &windowRewrite = config["window-rewrite"]; + if (!windowRewrite.isObject()) { + spdlog::debug("window-rewrite is not defined or is not an object, using default rules."); return; } - // create new workspace - m_workspaces.emplace_back(std::make_unique(workspace_data, *this, clients_data)); - Gtk::Button &newWorkspaceButton = m_workspaces.back()->button(); - m_box.pack_start(newWorkspaceButton, false, false); - sortWorkspaces(); - newWorkspaceButton.show_all(); + const auto &windowRewriteDefaultConfig = config["window-rewrite-default"]; + std::string windowRewriteDefault = + windowRewriteDefaultConfig.isString() ? windowRewriteDefaultConfig.asString() : "?"; + + m_windowRewriteRules = util::RegexCollection( + windowRewrite, windowRewriteDefault, + [this](std::string &window_rule) { return windowRewritePriorityFunction(window_rule); }); +} + +void Workspaces::registerOrphanWindow(WindowCreationPayload create_window_payload) { + if (!create_window_payload.isEmpty(*this)) { + m_orphanWindowMap[create_window_payload.getAddress()] = create_window_payload.repr(*this); + } +} + +auto Workspaces::registerIpc() -> void { + gIPC->registerForIPC("workspace", this); + gIPC->registerForIPC("activespecial", this); + gIPC->registerForIPC("createworkspace", this); + gIPC->registerForIPC("destroyworkspace", this); + gIPC->registerForIPC("focusedmon", this); + gIPC->registerForIPC("moveworkspace", this); + gIPC->registerForIPC("renameworkspace", this); + gIPC->registerForIPC("openwindow", this); + gIPC->registerForIPC("closewindow", this); + gIPC->registerForIPC("movewindow", this); + gIPC->registerForIPC("urgent", this); + gIPC->registerForIPC("configreloaded", this); + + if (windowRewriteConfigUsesTitle()) { + spdlog::info( + "Registering for Hyprland's 'windowtitle' events because a user-defined window " + "rewrite rule uses the 'title' field."); + gIPC->registerForIPC("windowtitle", this); + } +} + +void Workspaces::removeWorkspacesToRemove() { + for (const auto &workspaceName : m_workspacesToRemove) { + removeWorkspace(workspaceName); + } + m_workspacesToRemove.clear(); } void Workspaces::removeWorkspace(std::string const &name) { @@ -676,106 +721,6 @@ void Workspaces::removeWorkspace(std::string const &name) { m_workspaces.erase(workspace); } -Json::Value createMonitorWorkspaceData(std::string const &name, std::string const &monitor) { - spdlog::trace("Creating persistent workspace: {} on monitor {}", name, monitor); - Json::Value workspaceData; - try { - // numbered persistent workspaces get the name as ID - workspaceData["id"] = name == "special" ? -99 : std::stoi(name); - } catch (const std::exception &e) { - // named persistent workspaces start with ID=0 - workspaceData["id"] = 0; - } - workspaceData["name"] = name; - workspaceData["monitor"] = monitor; - workspaceData["windows"] = 0; - return workspaceData; -} - -void Workspaces::loadPersistentWorkspacesFromConfig(Json::Value const &clientsJson) { - spdlog::info("Loading persistent workspaces from Waybar config"); - const std::vector keys = m_persistentWorkspaceConfig.getMemberNames(); - std::vector persistentWorkspacesToCreate; - - const std::string currentMonitor = m_bar.output->name; - const bool monitorInConfig = std::find(keys.begin(), keys.end(), currentMonitor) != keys.end(); - for (const std::string &key : keys) { - // only add if either: - // 1. key is the current monitor name - // 2. key is "*" and this monitor is not already defined in the config - bool canCreate = key == currentMonitor || (key == "*" && !monitorInConfig); - const Json::Value &value = m_persistentWorkspaceConfig[key]; - spdlog::trace("Parsing persistent workspace config: {} => {}", key, value.toStyledString()); - - if (value.isInt()) { - // value is a number => create that many workspaces for this monitor - if (canCreate) { - int amount = value.asInt(); - spdlog::debug("Creating {} persistent workspaces for monitor {}", amount, currentMonitor); - for (int i = 0; i < amount; i++) { - persistentWorkspacesToCreate.emplace_back(std::to_string(m_monitorId * amount + i + 1)); - } - } - } else if (value.isArray() && !value.empty()) { - // value is an array => create defined workspaces for this monitor - if (canCreate) { - for (const Json::Value &workspace : value) { - if (workspace.isInt()) { - spdlog::debug("Creating workspace {} on monitor {}", workspace, currentMonitor); - persistentWorkspacesToCreate.emplace_back(std::to_string(workspace.asInt())); - } - } - } else { - // key is the workspace and value is array of monitors to create on - for (const Json::Value &monitor : value) { - if (monitor.isString() && monitor.asString() == currentMonitor) { - persistentWorkspacesToCreate.emplace_back(currentMonitor); - break; - } - } - } - } else { - // this workspace should be displayed on all monitors - persistentWorkspacesToCreate.emplace_back(key); - } - } - - for (auto const &workspace : persistentWorkspacesToCreate) { - auto workspaceData = createMonitorWorkspaceData(workspace, m_bar.output->name); - workspaceData["persistent-config"] = true; - m_workspacesToCreate.emplace_back(workspaceData, clientsJson); - } -} - -void Workspaces::loadPersistentWorkspacesFromWorkspaceRules(const Json::Value &clientsJson) { - spdlog::info("Loading persistent workspaces from Hyprland workspace rules"); - - auto const workspaceRules = gIPC->getSocket1JsonReply("workspacerules"); - for (Json::Value const &rule : workspaceRules) { - if (!rule["workspaceString"].isString()) { - spdlog::warn("Workspace rules: invalid workspaceString, skipping: {}", rule); - continue; - } - if (!rule["persistent"].asBool()) { - continue; - } - auto const &workspace = rule["workspaceString"].asString(); - auto const &monitor = rule["monitor"].asString(); - // create this workspace persistently if: - // 1. the allOutputs config option is enabled - // 2. the rule's monitor is the current monitor - // 3. no monitor is specified in the rule => assume it needs to be persistent on every monitor - if (allOutputs() || m_bar.output->name == monitor || monitor.empty()) { - // => persistent workspace should be shown on this monitor - auto workspaceData = createMonitorWorkspaceData(workspace, m_bar.output->name); - workspaceData["persistent-rule"] = true; - m_workspacesToCreate.emplace_back(workspaceData, clientsJson); - } else { - m_workspacesToRemove.emplace_back(workspace); - } - } -} - void Workspaces::setCurrentMonitorId() { // get monitor ID from name (used by persistent workspaces) m_monitorId = 0; @@ -791,138 +736,6 @@ void Workspaces::setCurrentMonitorId() { } } -void Workspaces::initializeWorkspaces() { - spdlog::debug("Initializing workspaces"); - - // if the workspace rules changed since last initialization, make sure we reset everything: - for (auto &workspace : m_workspaces) { - m_workspacesToRemove.push_back(workspace->name()); - } - - // get all current workspaces - auto const workspacesJson = gIPC->getSocket1JsonReply("workspaces"); - auto const clientsJson = gIPC->getSocket1JsonReply("clients"); - - for (Json::Value workspaceJson : workspacesJson) { - std::string workspaceName = workspaceJson["name"].asString(); - if ((allOutputs() || m_bar.output->name == workspaceJson["monitor"].asString()) && - (!workspaceName.starts_with("special") || showSpecial()) && - !isWorkspaceIgnored(workspaceName)) { - m_workspacesToCreate.emplace_back(workspaceJson, clientsJson); - } else { - extendOrphans(workspaceJson["id"].asInt(), clientsJson); - } - } - - spdlog::debug("Initializing persistent workspaces"); - if (m_persistentWorkspaceConfig.isObject()) { - // a persistent workspace config is defined, so use that instead of workspace rules - loadPersistentWorkspacesFromConfig(clientsJson); - } - // load Hyprland's workspace rules - loadPersistentWorkspacesFromWorkspaceRules(clientsJson); -} - -void Workspaces::extendOrphans(int workspaceId, Json::Value const &clientsJson) { - spdlog::trace("Extending orphans with workspace {}", workspaceId); - for (const auto &client : clientsJson) { - if (client["workspace"]["id"].asInt() == workspaceId) { - registerOrphanWindow({client}); - } - } -} - -void Workspaces::init() { - m_activeWorkspaceName = (gIPC->getSocket1JsonReply("activeworkspace"))["name"].asString(); - - initializeWorkspaces(); - dp.emit(); -} - -Workspaces::~Workspaces() { - gIPC->unregisterForIPC(this); - // wait for possible event handler to finish - std::lock_guard lg(m_mutex); -} - -Workspace::Workspace(const Json::Value &workspace_data, Workspaces &workspace_manager, - const Json::Value &clients_data) - : m_workspaceManager(workspace_manager), - m_id(workspace_data["id"].asInt()), - m_name(workspace_data["name"].asString()), - m_output(workspace_data["monitor"].asString()), // TODO:allow using monitor desc - m_windows(workspace_data["windows"].asInt()), - m_isActive(true), - m_isPersistentRule(workspace_data["persistent-rule"].asBool()), - m_isPersistentConfig(workspace_data["persistent-config"].asBool()) { - if (m_name.starts_with("name:")) { - m_name = m_name.substr(5); - } else if (m_name.starts_with("special")) { - m_name = m_id == -99 ? m_name : m_name.substr(8); - m_isSpecial = true; - } - - m_button.add_events(Gdk::BUTTON_PRESS_MASK); - m_button.signal_button_press_event().connect(sigc::mem_fun(*this, &Workspace::handleClicked), - false); - - m_button.set_relief(Gtk::RELIEF_NONE); - m_content.set_center_widget(m_label); - m_button.add(m_content); - - initializeWindowMap(clients_data); -} - -void addOrRemoveClass(const Glib::RefPtr &context, bool condition, - const std::string &class_name) { - if (condition) { - context->add_class(class_name); - } else { - context->remove_class(class_name); - } -} - -void Workspace::update(const std::string &format, const std::string &icon) { - // clang-format off - if (this->m_workspaceManager.activeOnly() && \ - !this->isActive() && \ - !this->isPersistent() && \ - !this->isVisible() && \ - !this->isSpecial()) { - // clang-format on - // if activeOnly is true, hide if not active, persistent, visible or special - m_button.hide(); - return; - } - m_button.show(); - - auto styleContext = m_button.get_style_context(); - addOrRemoveClass(styleContext, isActive(), "active"); - addOrRemoveClass(styleContext, isSpecial(), "special"); - addOrRemoveClass(styleContext, isEmpty(), "empty"); - addOrRemoveClass(styleContext, isPersistent(), "persistent"); - addOrRemoveClass(styleContext, isUrgent(), "urgent"); - addOrRemoveClass(styleContext, isVisible(), "visible"); - addOrRemoveClass(styleContext, m_workspaceManager.getBarOutput() == output(), "hosting-monitor"); - - std::string windows; - auto windowSeparator = m_workspaceManager.getWindowSeparator(); - - bool isNotFirst = false; - - for (auto &[_pid, window_repr] : m_windowMap) { - if (isNotFirst) { - windows.append(windowSeparator); - } - isNotFirst = true; - windows.append(window_repr); - } - - m_label.set_markup(fmt::format(fmt::runtime(format), fmt::arg("id", id()), - fmt::arg("name", name()), fmt::arg("icon", icon), - fmt::arg("windows", windows))); -} - void Workspaces::sortWorkspaces() { std::sort(m_workspaces.begin(), m_workspaces.end(), [&](std::unique_ptr &a, std::unique_ptr &b) { @@ -986,91 +799,6 @@ void Workspaces::sortWorkspaces() { } } -std::string &Workspace::selectIcon(std::map &icons_map) { - spdlog::trace("Selecting icon for workspace {}", name()); - if (isUrgent()) { - auto urgentIconIt = icons_map.find("urgent"); - if (urgentIconIt != icons_map.end()) { - return urgentIconIt->second; - } - } - - if (isActive()) { - auto activeIconIt = icons_map.find("active"); - if (activeIconIt != icons_map.end()) { - return activeIconIt->second; - } - } - - if (isSpecial()) { - auto specialIconIt = icons_map.find("special"); - if (specialIconIt != icons_map.end()) { - return specialIconIt->second; - } - } - - auto namedIconIt = icons_map.find(name()); - if (namedIconIt != icons_map.end()) { - return namedIconIt->second; - } - - if (isVisible()) { - auto visibleIconIt = icons_map.find("visible"); - if (visibleIconIt != icons_map.end()) { - return visibleIconIt->second; - } - } - - if (isEmpty()) { - auto emptyIconIt = icons_map.find("empty"); - if (emptyIconIt != icons_map.end()) { - return emptyIconIt->second; - } - } - - if (isPersistent()) { - auto persistentIconIt = icons_map.find("persistent"); - if (persistentIconIt != icons_map.end()) { - return persistentIconIt->second; - } - } - - auto defaultIconIt = icons_map.find("default"); - if (defaultIconIt != icons_map.end()) { - return defaultIconIt->second; - } - - return m_name; -} - -bool Workspace::handleClicked(GdkEventButton *bt) const { - if (bt->type == GDK_BUTTON_PRESS) { - try { - if (id() > 0) { // normal - if (m_workspaceManager.moveToMonitor()) { - gIPC->getSocket1Reply("dispatch focusworkspaceoncurrentmonitor " + std::to_string(id())); - } else { - gIPC->getSocket1Reply("dispatch workspace " + std::to_string(id())); - } - } else if (!isSpecial()) { // named (this includes persistent) - if (m_workspaceManager.moveToMonitor()) { - gIPC->getSocket1Reply("dispatch focusworkspaceoncurrentmonitor name:" + name()); - } else { - gIPC->getSocket1Reply("dispatch workspace name:" + name()); - } - } else if (id() != -99) { // named special - gIPC->getSocket1Reply("dispatch togglespecialworkspace " + name()); - } else { // special - gIPC->getSocket1Reply("dispatch togglespecialworkspace"); - } - return true; - } catch (const std::exception &e) { - spdlog::error("Failed to dispatch workspace: {}", e.what()); - } - } - return false; -} - void Workspaces::setUrgentWorkspace(std::string const &windowaddress) { const Json::Value clientsJson = gIPC->getSocket1JsonReply("clients"); int workspaceId = -1; @@ -1090,110 +818,102 @@ void Workspaces::setUrgentWorkspace(std::string const &windowaddress) { } } -std::string Workspaces::getRewrite(std::string window_class, std::string window_title) { - std::string windowReprKey; - if (windowRewriteConfigUsesTitle()) { - windowReprKey = fmt::format("class<{}> title<{}>", window_class, window_title); - } else { - windowReprKey = fmt::format("class<{}>", window_class); - } - auto const rewriteRule = m_windowRewriteRules.get(windowReprKey); - return fmt::format(fmt::runtime(rewriteRule), fmt::arg("class", window_class), - fmt::arg("title", window_title)); +auto Workspaces::update() -> void { + doUpdate(); + AModule::update(); } -WindowCreationPayload::WindowCreationPayload(std::string workspace_name, - WindowAddress window_address, std::string window_repr) - : m_window(std::move(window_repr)), - m_windowAddress(std::move(window_address)), - m_workspaceName(std::move(workspace_name)) { - clearAddr(); - clearWorkspaceName(); -} - -WindowCreationPayload::WindowCreationPayload(std::string workspace_name, - WindowAddress window_address, std::string window_class, - std::string window_title) - : m_window(std::make_pair(std::move(window_class), std::move(window_title))), - m_windowAddress(std::move(window_address)), - m_workspaceName(std::move(workspace_name)) { - clearAddr(); - clearWorkspaceName(); -} - -WindowCreationPayload::WindowCreationPayload(Json::Value const &client_data) - : m_window(std::make_pair(client_data["class"].asString(), client_data["title"].asString())), - m_windowAddress(client_data["address"].asString()), - m_workspaceName(client_data["workspace"]["name"].asString()) { - clearAddr(); - clearWorkspaceName(); -} - -std::string WindowCreationPayload::repr(Workspaces &workspace_manager) { - if (std::holds_alternative(m_window)) { - return std::get(m_window); - } - if (std::holds_alternative(m_window)) { - auto [window_class, window_title] = std::get(m_window); - return workspace_manager.getRewrite(window_class, window_title); - } - // Unreachable - spdlog::error("WorkspaceWindow::repr: Unreachable"); - throw std::runtime_error("WorkspaceWindow::repr: Unreachable"); -} - -bool WindowCreationPayload::isEmpty(Workspaces &workspace_manager) { - if (std::holds_alternative(m_window)) { - return std::get(m_window).empty(); - } - if (std::holds_alternative(m_window)) { - auto [window_class, window_title] = std::get(m_window); - return (window_class.empty() && - (!workspace_manager.windowRewriteConfigUsesTitle() || window_title.empty())); - } - // Unreachable - spdlog::error("WorkspaceWindow::isEmpty: Unreachable"); - throw std::runtime_error("WorkspaceWindow::isEmpty: Unreachable"); -} - -int WindowCreationPayload::incrementTimeSpentUncreated() { return m_timeSpentUncreated++; } - -void WindowCreationPayload::clearAddr() { - // substr(2, ...) is necessary because Hyprland's JSON follows this format: - // 0x{ADDR} - // While Hyprland's IPC follows this format: - // {ADDR} - static const std::string ADDR_PREFIX = "0x"; - static const int ADDR_PREFIX_LEN = ADDR_PREFIX.length(); - - if (m_windowAddress.starts_with(ADDR_PREFIX)) { - m_windowAddress = - m_windowAddress.substr(ADDR_PREFIX_LEN, m_windowAddress.length() - ADDR_PREFIX_LEN); +void Workspaces::updateWindowCount() { + const Json::Value workspacesJson = gIPC->getSocket1JsonReply("workspaces"); + for (auto &workspace : m_workspaces) { + auto workspaceJson = + std::find_if(workspacesJson.begin(), workspacesJson.end(), [&](Json::Value const &x) { + return x["name"].asString() == workspace->name() || + (workspace->isSpecial() && x["name"].asString() == "special:" + workspace->name()); + }); + uint32_t count = 0; + if (workspaceJson != workspacesJson.end()) { + try { + count = (*workspaceJson)["windows"].asUInt(); + } catch (const std::exception &e) { + spdlog::error("Failed to update window count: {}", e.what()); + } + } + workspace->setWindows(count); } } -void WindowCreationPayload::clearWorkspaceName() { - // The workspace name may optionally feature "special:" at the beginning. - // If so, we need to remove it because the workspace is saved WITHOUT the - // special qualifier. The reasoning is that not all of Hyprland's IPC events - // use this qualifier, so it's better to be consistent about our uses. - - static const std::string SPECIAL_QUALIFIER_PREFIX = "special:"; - static const int SPECIAL_QUALIFIER_PREFIX_LEN = SPECIAL_QUALIFIER_PREFIX.length(); - - if (m_workspaceName.starts_with(SPECIAL_QUALIFIER_PREFIX)) { - m_workspaceName = m_workspaceName.substr( - SPECIAL_QUALIFIER_PREFIX_LEN, m_workspaceName.length() - SPECIAL_QUALIFIER_PREFIX_LEN); +bool Workspaces::updateWindowsToCreate() { + bool anyWindowCreated = false; + std::vector notCreated; + for (auto &windowPayload : m_windowsToCreate) { + bool created = false; + for (auto &workspace : m_workspaces) { + if (workspace->onWindowOpened(windowPayload)) { + created = true; + anyWindowCreated = true; + break; + } + } + if (!created) { + static auto const WINDOW_CREATION_TIMEOUT = 2; + if (windowPayload.incrementTimeSpentUncreated() < WINDOW_CREATION_TIMEOUT) { + notCreated.push_back(windowPayload); + } else { + registerOrphanWindow(windowPayload); + } + } } + m_windowsToCreate.clear(); + m_windowsToCreate = notCreated; + return anyWindowCreated; +} - std::size_t spaceFound = m_workspaceName.find(' '); - if (spaceFound != std::string::npos) { - m_workspaceName.erase(m_workspaceName.begin() + spaceFound, m_workspaceName.end()); +void Workspaces::updateWorkspaceStates(const std::vector &visibleWorkspaces) { + auto updatedWorkspaces = gIPC->getSocket1JsonReply("workspaces"); + for (auto &workspace : m_workspaces) { + workspace->setActive(workspace->name() == m_activeWorkspaceName || + workspace->name() == m_activeSpecialWorkspaceName); + if (workspace->name() == m_activeWorkspaceName && workspace->isUrgent()) { + workspace->setUrgent(false); + } + workspace->setVisible(std::find(visibleWorkspaces.begin(), visibleWorkspaces.end(), + workspace->name()) != visibleWorkspaces.end()); + std::string &workspaceIcon = m_iconsMap[""]; + if (m_withIcon) { + workspaceIcon = workspace->selectIcon(m_iconsMap); + } + auto updatedWorkspace = std::find_if( + updatedWorkspaces.begin(), updatedWorkspaces.end(), [&workspace](const auto &w) { + auto wNameRaw = w["name"].asString(); + auto wName = wNameRaw.starts_with("special:") ? wNameRaw.substr(8) : wNameRaw; + return wName == workspace->name(); + }); + if (updatedWorkspace != updatedWorkspaces.end()) { + workspace->setOutput((*updatedWorkspace)["monitor"].asString()); + } + workspace->update(m_format, workspaceIcon); } } -void WindowCreationPayload::moveToWorksace(std::string &new_workspace_name) { - m_workspaceName = new_workspace_name; +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 + bool const hasTitle = window_rule.find("title") != std::string::npos; + bool const hasClass = window_rule.find("class") != std::string::npos; + + if (hasTitle && hasClass) { + m_anyWindowRewriteRuleUsesTitle = true; + return 3; + } + if (hasTitle) { + m_anyWindowRewriteRuleUsesTitle = true; + return 2; + } + if (hasClass) { + return 1; + } + return 0; } } // namespace waybar::modules::hyprland