diff --git a/include/modules/niri/workspace.hpp b/include/modules/niri/workspace.hpp new file mode 100644 index 00000000..e1bf7ba6 --- /dev/null +++ b/include/modules/niri/workspace.hpp @@ -0,0 +1,47 @@ +#pragma once + +#include +#include +#include +#include +#include + +#include + +namespace waybar::modules::niri { + +class Workspaces; + +class Workspace { + public: + Workspace(const Json::Value& workspace_data, Workspaces& manager); + ~Workspace() = default; + + Workspace(const Workspace&) = delete; + Workspace& operator=(const Workspace&) = delete; + + Gtk::Button& button() { return button_; } + uint64_t id() const { return id_; } + + void update(const Json::Value& workspace_data, const std::vector& all_windows, + const std::string& windows_str, std::size_t total); + + private: + void rebuildTaskbar(const std::vector& my_windows); + + Glib::RefPtr loadIcon(const std::string& app_id, int size); + + Workspaces& manager_; + uint64_t id_; + + // Layout: button_ + // └─ box_ (horizontal) + // ├─ label_ workspace label / icon + // └─ taskbar_box_ app icon buttons (shown only when taskbar enabled) + Gtk::Button button_; + Gtk::Box box_; + Gtk::Label label_; + Gtk::Box taskbar_box_; +}; + +} // namespace waybar::modules::niri \ No newline at end of file diff --git a/include/modules/niri/workspaces.hpp b/include/modules/niri/workspaces.hpp index 7b717e9f..97030223 100644 --- a/include/modules/niri/workspaces.hpp +++ b/include/modules/niri/workspaces.hpp @@ -1,41 +1,50 @@ #pragma once -#include +#include #include +#include #include #include #include "AModule.hpp" #include "bar.hpp" #include "modules/niri/backend.hpp" -#include "util/regex_collection.hpp" // Added for rewrite rules +#include "modules/niri/workspace.hpp" +#include "util/regex_collection.hpp" // Added for rewrite rules namespace waybar::modules::niri { class Workspaces : public AModule, public EventHandler { public: - Workspaces(const std::string&, const Bar&, const Json::Value&); + Workspaces(const std::string& id, const Bar& bar, const Json::Value& config); ~Workspaces() override; + void update() override; + const Json::Value& config() const { return config_; } + const Bar& bar() const { return bar_; } + + std::string getIcon(const std::string& value, const Json::Value& ws) const; + private: void onEvent(const Json::Value& ev) override; void doUpdate(); - void sortWorkspaces(std::vector& workspaces) const; - Gtk::Button& addButton(const Json::Value& ws); - std::string getIcon(const std::string& value, const Json::Value& ws); + void createWorkspace(const Json::Value& workspace_data); + void sortWorkspaces(std::vector& workspaces) const; bool isWorkspaceIgnored(const std::string& name); bool handleScroll(GdkEventScroll* /*unused*/) override; // Added for window rewrite void populateWindowRewriteConfig(); void populateFormatWindowSeparatorConfig(); std::string getRewrite(const std::string& app_id, const std::string& title); + std::string getWindowsRepresentation(const Json::Value& ws); const Bar& bar_; Gtk::Box box_; - // Map from niri workspace id to button. - std::unordered_map buttons_; + + std::vector> workspaces_; + // Vec of regex rules to ignore workspaces. std::vector ignoreWorkspaces_; diff --git a/man/waybar-niri-workspaces.5.scd b/man/waybar-niri-workspaces.5.scd index 13e2d27c..0627776c 100644 --- a/man/waybar-niri-workspaces.5.scd +++ b/man/waybar-niri-workspaces.5.scd @@ -91,6 +91,20 @@ If none of the sorting options are enabled, workspaces keep their output/index o typeof: bool ++ default: false ++ Enables this module to consume all left over space dynamically. + +*workspace-taskbar*: ++ + typeof: object ++ + Contains settings for the workspace taskbar, which displays app icons within each workspace. + + *enable*: ++ + typeof: bool ++ + default: false ++ + Enables the workspace taskbar mode. + + *icon-size*: ++ + typeof: int ++ + default: 18 ++ + Size of the icons in the workspace taskbar. *ignore-workspaces*: ++ typeof: array ++ @@ -166,6 +180,16 @@ Additional to workspace name matching, the following *format-icons* can be set. } ``` +``` +"niri/workspaces": { + "format": "{icon}", + "workspace-taskbar": { + "enable": true, + "icon-size": 18 + } +} +``` + # Style - *#workspaces button* @@ -177,3 +201,5 @@ Additional to workspace name matching, the following *format-icons* can be set. the bar that it is displayed on. - *#workspaces button#niri-workspace-*: Workspaces named this, or index for unnamed workspaces. +- *#workspaces button.niri-workspace*: The main container for the workspace. +- *#workspaces button .niri-taskbar-btn*: The icon buttons within the taskbar. diff --git a/meson.build b/meson.build index 5563d610..a7b42ece 100644 --- a/meson.build +++ b/meson.build @@ -74,6 +74,7 @@ wayland_client = dependency('wayland-client') wayland_cursor = dependency('wayland-cursor') wayland_protos = dependency('wayland-protocols') gtkmm = dependency('gtkmm-3.0', version : ['>=3.22.0']) +giomm = dependency('giomm-2.4', version : ['>=2.4.0']) dbusmenu_gtk = dependency('dbusmenu-gtk3-0.4', required: get_option('dbusmenu-gtk')) giounix = dependency('gio-unix-2.0') jsoncpp = dependency('jsoncpp', version : ['>=1.9.2'], fallback : ['jsoncpp', 'jsoncpp_dep']) @@ -350,6 +351,7 @@ if get_option('niri') 'src/modules/niri/language.cpp', 'src/modules/niri/window.cpp', 'src/modules/niri/workspaces.cpp', + 'src/modules/niri/workspace.cpp', ) man_files += files( 'man/waybar-niri-language.5.scd', @@ -575,6 +577,7 @@ executable( jsoncpp, wayland_cursor, gtkmm, + giomm, dbusmenu_gtk, giounix, libinput, diff --git a/src/modules/niri/workspace.cpp b/src/modules/niri/workspace.cpp new file mode 100644 index 00000000..73445b50 --- /dev/null +++ b/src/modules/niri/workspace.cpp @@ -0,0 +1,261 @@ +#include "modules/niri/workspace.hpp" + +#include +#include +#include +#include +#include +#include + +#include "modules/niri/backend.hpp" +#include "modules/niri/workspaces.hpp" + +namespace waybar::modules::niri { + +Workspace::Workspace(const Json::Value& workspace_data, Workspaces& manager) + : manager_(manager), + id_(workspace_data["id"].asUInt64()), + box_(Gtk::ORIENTATION_HORIZONTAL, 0), + taskbar_box_(Gtk::ORIENTATION_HORIZONTAL, 0) { + button_.add(box_); + box_.pack_start(label_, false, false, 0); + box_.pack_start(taskbar_box_, false, false, 0); + + button_.set_relief(Gtk::RELIEF_NONE); + button_.get_style_context()->add_class("niri-workspace"); + + if (!manager_.config()["disable-click"].asBool()) { + const auto ws_id = id_; + button_.signal_pressed().connect([ws_id] { + try { + Json::Value request(Json::objectValue); + auto& action = (request["Action"] = Json::Value(Json::objectValue)); + auto& focusWorkspace = (action["FocusWorkspace"] = Json::Value(Json::objectValue)); + auto& reference = (focusWorkspace["reference"] = Json::Value(Json::objectValue)); + reference["Id"] = ws_id; + IPC::send(request); + } catch (const std::exception& e) { + spdlog::error("Niri: error focusing workspace: {}", e.what()); + } + }); + } + + button_.show_all(); +} + +void Workspace::update(const Json::Value& data, const std::vector& all_windows, + const std::string& windows_str, std::size_t total) { + // ── CSS classes ────────────────────────────────────────────────────────── + auto style = button_.get_style_context(); + + auto setClass = [&](const char* cls, bool on) { + if (on) + style->add_class(cls); + else + style->remove_class(cls); + }; + + setClass("focused", data["is_focused"].asBool()); + setClass("active", data["is_active"].asBool()); + setClass("urgent", data["is_urgent"].asBool()); + setClass("empty", data["active_window_id"].isNull()); + setClass("current_output", + data["output"] && data["output"].asString() == manager_.bar().output->name); + + // ── Workspace label ─────────────────────────────────────────────────────── + std::string name; + if (data["name"]) { + name = data["name"].asString(); + } else { + name = std::to_string(data["idx"].asUInt()); + } + + button_.set_name("niri-workspace-" + name); + + const auto& cfg = manager_.config(); + + if (cfg["format"].isString()) { + auto format = cfg["format"].asString(); + name = fmt::format(fmt::runtime(format), fmt::arg("icon", manager_.getIcon(name, data)), + fmt::arg("value", name), fmt::arg("name", data["name"].asString()), + fmt::arg("index", data["idx"].asUInt()), + fmt::arg("output", data["output"].asString()), fmt::arg("total", total), + fmt::arg("windows", windows_str)); + } + + if (!cfg["disable-markup"].asBool()) { + label_.set_markup(name); + } else { + label_.set_text(name); + } + + // ── Visibility ─────────────────────────────────────────────────────────── + const bool alloutputs = cfg["all-outputs"].asBool(); + if (cfg["current-only"].asBool()) { + const auto* prop = alloutputs ? "is_focused" : "is_active"; + data[prop].asBool() ? button_.show() : button_.hide(); + } else if (cfg["hide-empty"].asBool()) { + (data["active_window_id"].isNull() && !data["is_focused"].asBool()) ? button_.hide() + : button_.show(); + } else { + button_.show(); + } + + // ── Taskbar ─────────────────────────────────────────────────────────────── + const auto& taskbar_cfg = cfg["workspace-taskbar"]; + if (taskbar_cfg.isObject() && taskbar_cfg["enable"].asBool()) { + std::vector my_windows; + for (const auto& win : all_windows) { + if (win["workspace_id"].asUInt64() == id_) { + my_windows.push_back(win); + } + } + + std::sort(my_windows.begin(), my_windows.end(), [](const Json::Value& a, const Json::Value& b) { + const auto& la = a["layout"]; + const auto& lb = b["layout"]; + const bool ha = la.isObject() && la["pos_in_scrolling_layout"].isArray(); + const bool hb = lb.isObject() && lb["pos_in_scrolling_layout"].isArray(); + if (!ha && !hb) return false; + if (!ha) return false; + if (!hb) return true; + const int col_a = la["pos_in_scrolling_layout"][0].asInt(); + const int col_b = lb["pos_in_scrolling_layout"][0].asInt(); + if (col_a != col_b) return col_a < col_b; + return la["pos_in_scrolling_layout"][1].asInt() < lb["pos_in_scrolling_layout"][1].asInt(); + }); + + rebuildTaskbar(my_windows); + taskbar_box_.show(); + label_.hide(); + } else { + for (auto* child : taskbar_box_.get_children()) { + taskbar_box_.remove(*child); + } + taskbar_box_.hide(); + } +} + +// ── Taskbar rebuild ────────────────────────────────────────────────────────── + +void Workspace::rebuildTaskbar(const std::vector& my_windows) { + for (auto* child : taskbar_box_.get_children()) { + taskbar_box_.remove(*child); + } + + const auto& taskbar_cfg = manager_.config()["workspace-taskbar"]; + const int icon_size = taskbar_cfg["icon-size"].isInt() ? taskbar_cfg["icon-size"].asInt() : 16; + + for (const auto& win : my_windows) { + const auto win_id = win["id"].asUInt64(); + const std::string app_id = win["app_id"].isString() ? win["app_id"].asString() : ""; + const std::string title = win["title"].isString() ? win["title"].asString() : app_id; + const bool is_focused = win["is_focused"].asBool(); + + auto* btn = Gtk::make_managed(); + btn->set_relief(Gtk::RELIEF_NONE); + btn->get_style_context()->add_class("niri-taskbar-btn"); + if (is_focused) btn->get_style_context()->add_class("focused"); + btn->set_tooltip_text(title); + + auto pixbuf = loadIcon(app_id, icon_size); + if (pixbuf) { + auto* img = Gtk::make_managed(pixbuf); + btn->add(*img); + } else { + std::string fallback = app_id.empty() ? title : app_id; + if (!fallback.empty()) { + fallback = fallback.substr(0, 3); + } else { + fallback = "?"; + } + auto* lbl = Gtk::make_managed(fallback); + btn->add(*lbl); + } + + // Left click → focus window. + btn->signal_clicked().connect([win_id] { + try { + Json::Value request(Json::objectValue); + auto& action = (request["Action"] = Json::Value(Json::objectValue)); + auto& focusWindow = (action["FocusWindow"] = Json::Value(Json::objectValue)); + focusWindow["id"] = win_id; + IPC::send(request); + } catch (const std::exception& e) { + spdlog::error("Niri: error focusing window {}: {}", win_id, e.what()); + } + }); + + // Middle click → close window. + btn->signal_button_release_event().connect([win_id](GdkEventButton* event) -> bool { + if (event->button == GDK_BUTTON_MIDDLE) { + try { + Json::Value request(Json::objectValue); + auto& action = (request["Action"] = Json::Value(Json::objectValue)); + auto& closeWindow = (action["CloseWindow"] = Json::Value(Json::objectValue)); + closeWindow["id"] = win_id; + IPC::send(request); + } catch (const std::exception& e) { + spdlog::error("Niri: error closing window {}: {}", win_id, e.what()); + } + return true; + } + return false; + }); + + taskbar_box_.pack_start(*btn, false, false, 0); + btn->show_all(); + } +} + +// ── Icon loading ───────────────────────────────────────────────────────────── + +Glib::RefPtr Workspace::loadIcon(const std::string& app_id, int size) { + if (app_id.empty()) return {}; + auto app_info = Gio::DesktopAppInfo::create(app_id + ".desktop"); + + if (app_info) { + auto icon = app_info->get_icon(); + if (icon) { + auto theme = Gtk::IconTheme::get_default(); + auto icon_info = theme->lookup_icon(icon, size, Gtk::ICON_LOOKUP_FORCE_SIZE); + + if (icon_info) { + try { + + return icon_info.load_icon(); + } catch (...) { + + } + } + } + } + + auto theme = Gtk::IconTheme::get_default(); + + auto tryLoad = [&](const std::string& name) -> Glib::RefPtr { + if (!theme->has_icon(name)) return {}; + try { + return theme->load_icon(name, size, Gtk::ICON_LOOKUP_FORCE_SIZE); + } catch (...) { + return {}; + } + }; + + if (auto pb = tryLoad(app_id)) return pb; + + std::string lower = app_id; + std::transform(lower.begin(), lower.end(), lower.begin(), ::tolower); + if (auto pb = tryLoad(lower)) return pb; + + auto dot = app_id.rfind('.'); + if (dot != std::string::npos) { + std::string last = app_id.substr(dot + 1); + std::transform(last.begin(), last.end(), last.begin(), ::tolower); + if (auto pb = tryLoad(last)) return pb; + } + + return {}; +} + +} // namespace waybar::modules::niri \ No newline at end of file diff --git a/src/modules/niri/workspaces.cpp b/src/modules/niri/workspaces.cpp index 21e12c0e..0bdcf85b 100644 --- a/src/modules/niri/workspaces.cpp +++ b/src/modules/niri/workspaces.cpp @@ -1,8 +1,6 @@ #include "modules/niri/workspaces.hpp" #include // Needed for joining window representations -#include -#include #include #include @@ -73,6 +71,12 @@ Workspaces::Workspaces(const std::string& id, const Bar& bar, const Json::Value& gIPC->registerForIPC("WorkspaceActiveWindowChanged", this); gIPC->registerForIPC("WorkspaceUrgencyChanged", this); + gIPC->registerForIPC("WindowsChanged", this); + gIPC->registerForIPC("WindowOpenedOrChanged", this); + gIPC->registerForIPC("WindowLayoutsChanged", this); + gIPC->registerForIPC("WindowFocusChanged", this); + gIPC->registerForIPC("WindowClosed", this); + if (config["enable-bar-scroll"].asBool()) { auto& window = const_cast(bar_).window; window.add_events(Gdk::SCROLL_MASK | Gdk::SMOOTH_SCROLL_MASK); @@ -84,7 +88,7 @@ Workspaces::Workspaces(const std::string& id, const Bar& bar, const Json::Value& Workspaces::~Workspaces() { gIPC->unregisterForIPC(this); } -void Workspaces::onEvent(const Json::Value& ev) { dp.emit(); } +void Workspaces::onEvent(const Json::Value& /*ev*/) { dp.emit(); } void Workspaces::doUpdate() { auto ipcLock = gIPC->lockData(); @@ -95,178 +99,78 @@ void Workspaces::doUpdate() { const auto alloutputs = config_["all-outputs"].asBool(); const auto display_cond = config_["display-condition"].asString(); - std::vector my_workspaces; - const auto& workspaces = gIPC->workspaces(); - std::copy_if( - workspaces.cbegin(), workspaces.cend(), std::back_inserter(my_workspaces), - [&](const auto& ws) { - std::string name; - if (ws["name"]) { - name = ws["name"].asString(); - } else { - name = std::to_string(ws["idx"].asUInt()); - } - if (isWorkspaceIgnored(name)) { - return false; - } - - if (display_cond == "only-populated") { - if (ws["active_window_id"].isNull() && !ws["is_active"].asBool()) return false; - } else if (display_cond == "keep-named") { - if (ws["name"].isNull() && ws["active_window_id"].isNull() && !ws["is_active"].asBool()) - return false; - } - - if (alloutputs) return true; - return ws["output"].asString() == bar_.output->name; - }); - - sortWorkspaces(my_workspaces); - - // Remove buttons for removed workspaces. - for (auto it = buttons_.begin(); it != buttons_.end();) { - auto ws = std::find_if(my_workspaces.begin(), my_workspaces.end(), - [it](const auto& ws) { return ws["id"].asUInt64() == it->first; }); - if (ws == my_workspaces.end()) { - it = buttons_.erase(it); - } else { - ++it; - } - } - - // Add buttons for new workspaces, update existing ones. - for (const auto& ws : my_workspaces) { - // Debug: print workspace JSON - spdlog::debug("[niri/workspaces] workspace id={} json={} ", ws["id"].asUInt64(), - ws.toStyledString()); - - auto bit = buttons_.find(ws["id"].asUInt64()); - auto& button = bit == buttons_.end() ? addButton(ws) : bit->second; - auto style_context = button.get_style_context(); - - if (ws["is_focused"].asBool()) - style_context->add_class("focused"); - else - style_context->remove_class("focused"); - - if (ws["is_active"].asBool()) - style_context->add_class("active"); - else - style_context->remove_class("active"); - - if (ws["is_urgent"].asBool()) - style_context->add_class("urgent"); - else - style_context->remove_class("urgent"); - - if (ws["output"]) { - if (ws["output"].asString() == bar_.output->name) - style_context->add_class("current_output"); - else - style_context->remove_class("current_output"); - } else { - style_context->remove_class("current_output"); - } - - if (ws["active_window_id"].isNull()) - style_context->add_class("empty"); - else - style_context->remove_class("empty"); - - // --- Start Window Rewrite Logic --- - std::vector window_reps; - if (ws.isMember("windows") && ws["windows"].isArray()) { - spdlog::debug("[niri/workspaces] workspace id={} has {} windows", ws["id"].asUInt64(), - ws["windows"].size()); - for (const auto& win : ws["windows"]) { - spdlog::debug("[niri/workspaces] window json: {}", win.toStyledString()); - std::string app_id = - win.isMember("app_id") && win["app_id"].isString() ? win["app_id"].asString() : ""; - std::string title = - win.isMember("title") && win["title"].isString() ? win["title"].asString() : ""; - if (!app_id.empty() || !title.empty()) { // Only add if we have some identifier - auto rep = getRewrite(app_id, title); - spdlog::debug("[niri/workspaces] rewrite: app_id='{}' title='{}' => '{}'", app_id, title, - rep); - window_reps.push_back(rep); - } - } - } else { - spdlog::debug( - "[niri/workspaces] workspace id={} has no 'windows' array, collecting from global " - "windows", - ws["id"].asUInt64()); - // Fallback: collect from global windows list by matching workspace_id - for (const auto& win : gIPC->windows()) { - if (!win.isMember("workspace_id")) continue; - if (win["workspace_id"].asUInt64() != ws["id"].asUInt64()) continue; - spdlog::debug("[niri/workspaces] global window json: {}", win.toStyledString()); - std::string app_id = - win.isMember("app_id") && win["app_id"].isString() ? win["app_id"].asString() : ""; - std::string title = - win.isMember("title") && win["title"].isString() ? win["title"].asString() : ""; - if (!app_id.empty() || !title.empty()) { - auto rep = getRewrite(app_id, title); - spdlog::debug("[niri/workspaces] rewrite (global): app_id='{}' title='{}' => '{}'", - app_id, title, rep); - window_reps.push_back(rep); - } - } - } - // Join representations with the separator - auto windows_str = fmt::format("{}", fmt::join(window_reps, m_formatWindowSeparator)); - spdlog::debug("[niri/workspaces] workspace id={} windows_str='{}'", ws["id"].asUInt64(), - windows_str); - // --- End Window Rewrite Logic --- + const auto& all_workspaces = gIPC->workspaces(); + const auto& all_windows = gIPC->windows(); + std::vector my_workspaces; + my_workspaces.reserve(all_workspaces.size()); + for (const auto& ws : all_workspaces) { std::string name; if (ws["name"]) { name = ws["name"].asString(); } else { name = std::to_string(ws["idx"].asUInt()); } - button.set_name("niri-workspace-" + name); - - if (config_["format"].isString()) { - auto format = config_["format"].asString(); - name = fmt::format( - fmt::runtime(format), fmt::arg("icon", getIcon(name, ws)), fmt::arg("value", name), - fmt::arg("name", ws["name"].asString()), fmt::arg("index", ws["idx"].asUInt()), - fmt::arg("output", ws["output"].asString()), fmt::arg("total", my_workspaces.size()), - fmt::arg("windows", windows_str)); // Added windows arg - } - if (!config_["disable-markup"].asBool()) { - auto* child = gtk_bin_get_child(GTK_BIN(button.gobj())); - if (child != nullptr && GTK_IS_LABEL(child)) - gtk_label_set_markup(GTK_LABEL(child), name.c_str()); - } else { - button.set_label(name); + if (isWorkspaceIgnored(name)) { + continue; } - if (config_["current-only"].asBool()) { - const auto* property = alloutputs ? "is_focused" : "is_active"; - if (ws[property].asBool()) - button.show(); - else - button.hide(); - } else if (config_["hide-empty"].asBool()) { - if (ws["active_window_id"].isNull() && !ws["is_focused"].asBool()) - button.hide(); - else - button.show(); - } else { - button.show(); + if (display_cond == "only-populated") { + if (ws["active_window_id"].isNull() && !ws["is_active"].asBool()) continue; + } else if (display_cond == "keep-named") { + if (ws["name"].isNull() && ws["active_window_id"].isNull() && !ws["is_active"].asBool()) + continue; + } + + if (alloutputs || ws["output"].asString() == bar_.output->name) { + my_workspaces.push_back(&ws); } } - // Refresh the button order. - for (auto it = my_workspaces.cbegin(); it != my_workspaces.cend(); ++it) { - const auto& ws = *it; + sortWorkspaces(my_workspaces); - const auto pos = static_cast(std::distance(my_workspaces.cbegin(), it)); + workspaces_.erase(std::remove_if(workspaces_.begin(), workspaces_.end(), + [&](const std::unique_ptr& w) { + bool gone = std::none_of( + my_workspaces.begin(), my_workspaces.end(), + [&](const Json::Value* ws) { + return ws->operator[]("id").asUInt64() == w->id(); + }); + if (gone) box_.remove(w->button()); + return gone; + }), + workspaces_.end()); - auto& button = buttons_[ws["id"].asUInt64()]; - box_.reorder_child(button, pos); + for (const auto* ws_ptr : my_workspaces) { + const auto& ws = *ws_ptr; + const auto ws_id = ws.isMember("id") ? ws["id"].asUInt64() : 0; + + auto it = + std::find_if(workspaces_.begin(), workspaces_.end(), + [ws_id](const std::unique_ptr& w) { return w->id() == ws_id; }); + + if (it == workspaces_.end()) { + createWorkspace(ws); + it = workspaces_.end() - 1; + } + + std::vector windows_vec(all_windows.begin(), all_windows.end()); + const auto windows_str = getWindowsRepresentation(ws); + (*it)->update(ws, windows_vec, windows_str, my_workspaces.size()); + } + + for (auto pos_it = my_workspaces.cbegin(); pos_it != my_workspaces.cend(); ++pos_it) { + const auto& ws = **pos_it; + const auto ws_id = ws.isMember("id") ? ws["id"].asUInt64() : 0; + + const auto pos = static_cast(std::distance(my_workspaces.cbegin(), pos_it)); + + auto it = + std::find_if(workspaces_.begin(), workspaces_.end(), + [ws_id](const std::unique_ptr& w) { return w->id() == ws_id; }); + if (it != workspaces_.end()) { + box_.reorder_child((*it)->button(), pos); + } } } @@ -275,36 +179,10 @@ void Workspaces::update() { AModule::update(); } -Gtk::Button& Workspaces::addButton(const Json::Value& ws) { - std::string name; - if (ws["name"]) { - name = ws["name"].asString(); - } else { - name = std::to_string(ws["idx"].asUInt()); - } - - auto pair = buttons_.emplace(ws["id"].asUInt64(), name); - auto&& button = pair.first->second; - box_.pack_start(button, false, false, 0); - button.set_relief(Gtk::RELIEF_NONE); - if (!config_["disable-click"].asBool()) { - const auto id = ws["id"].asUInt64(); - button.signal_pressed().connect([=] { - try { - // {"Action":{"FocusWorkspace":{"reference":{"Id":1}}}} - Json::Value request(Json::objectValue); - auto& action = (request["Action"] = Json::Value(Json::objectValue)); - auto& focusWorkspace = (action["FocusWorkspace"] = Json::Value(Json::objectValue)); - auto& reference = (focusWorkspace["reference"] = Json::Value(Json::objectValue)); - reference["Id"] = id; - - IPC::send(request); - } catch (const std::exception& e) { - spdlog::error("Error switching workspace: {}", e.what()); - } - }); - } - return button; +void Workspaces::createWorkspace(const Json::Value& workspace_data) { + auto ws = std::make_unique(workspace_data, *this); + box_.pack_start(ws->button(), false, false, 0); + workspaces_.push_back(std::move(ws)); } // --- Start New Helper Functions --- @@ -383,18 +261,44 @@ std::string Workspaces::getRewrite(const std::string& app_id, const std::string& substitutions["title"] = title; return util::rewriteString(m_windowRewriteDefault, substitutions); } + +// Build the "{windows}" replacement string for a workspace using window-rewrite rules. +std::string Workspaces::getWindowsRepresentation(const Json::Value& ws) { + std::vector window_reps; + auto collect = [&](const Json::Value& win) { + std::string app_id = + win.isMember("app_id") && win["app_id"].isString() ? win["app_id"].asString() : ""; + std::string title = + win.isMember("title") && win["title"].isString() ? win["title"].asString() : ""; + if (!app_id.empty() || !title.empty()) { + window_reps.push_back(getRewrite(app_id, title)); + } + }; + + if (ws.isMember("windows") && ws["windows"].isArray()) { + for (const auto& win : ws["windows"]) { + collect(win); + } + } else { + // Fallback: collect from global windows list by matching workspace_id. + for (const auto& win : gIPC->windows()) { + if (!win.isMember("workspace_id")) continue; + if (win["workspace_id"].asUInt64() != ws["id"].asUInt64()) continue; + collect(win); + } + } + + return fmt::format("{}", fmt::join(window_reps, m_formatWindowSeparator)); +} // --- End New Helper Functions --- -std::string Workspaces::getIcon(const std::string& value, const Json::Value& ws) { +std::string Workspaces::getIcon(const std::string& value, const Json::Value& ws) const { const auto& icons = config_["format-icons"]; if (!icons) return value; if (ws["is_urgent"].asBool() && icons["urgent"]) return icons["urgent"].asString(); - if (ws["is_active"].asBool() && icons["active"]) return icons["active"].asString(); - if (ws["is_focused"].asBool() && icons["focused"]) return icons["focused"].asString(); - if (ws["active_window_id"].isNull() && icons["empty"]) return icons["empty"].asString(); if (ws["name"]) { @@ -458,7 +362,7 @@ bool Workspaces::handleScroll(GdkEventScroll* e) { return true; } -void Workspaces::sortWorkspaces(std::vector& workspaces) const { +void Workspaces::sortWorkspaces(std::vector& workspaces) const { auto get_name = [](const Json::Value& ws) -> std::string { if (ws["name"]) return ws["name"].asString(); return std::to_string(ws["idx"].asUInt()); @@ -471,14 +375,16 @@ void Workspaces::sortWorkspaces(std::vector& workspaces) const { const bool names_are_numeric = std::all_of(workspaces.begin(), workspaces.end(), - [&](const auto& ws) { return is_numeric(get_name(ws)); }); + [&](const auto* ws) { return is_numeric(get_name(*ws)); }); auto compare_numeric_strings = [](const std::string& a, const std::string& b) { if (a.size() != b.size()) return a.size() < b.size(); return a < b; }; - std::sort(workspaces.begin(), workspaces.end(), [&](const auto& a, const auto& b) { + std::sort(workspaces.begin(), workspaces.end(), [&](const auto* ap, const auto* bp) { + const auto& a = *ap; + const auto& b = *bp; if (sort_by_id_) { return a["id"].asUInt64() < b["id"].asUInt64(); }