feat(niri): add workspace-taskbar support
Adds a per-workspace app-icon taskbar (Workspace class) to the
niri/workspaces module. Rebased onto master: integrated with the
existing window-rewrite feature so the {windows} and {total} format
replacements continue to work alongside the new taskbar.
This commit is contained in:
@@ -0,0 +1,47 @@
|
|||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include <gtkmm/box.h>
|
||||||
|
#include <gtkmm/button.h>
|
||||||
|
#include <gtkmm/image.h>
|
||||||
|
#include <gtkmm/label.h>
|
||||||
|
#include <json/value.h>
|
||||||
|
|
||||||
|
#include <string>
|
||||||
|
|
||||||
|
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<Json::Value>& all_windows,
|
||||||
|
const std::string& windows_str, std::size_t total);
|
||||||
|
|
||||||
|
private:
|
||||||
|
void rebuildTaskbar(const std::vector<Json::Value>& my_windows);
|
||||||
|
|
||||||
|
Glib::RefPtr<Gdk::Pixbuf> 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
|
||||||
@@ -1,41 +1,50 @@
|
|||||||
#pragma once
|
#pragma once
|
||||||
|
|
||||||
#include <gtkmm/button.h>
|
#include <gtkmm/box.h>
|
||||||
#include <json/value.h>
|
#include <json/value.h>
|
||||||
|
|
||||||
|
#include <memory>
|
||||||
#include <regex>
|
#include <regex>
|
||||||
#include <vector>
|
#include <vector>
|
||||||
|
|
||||||
#include "AModule.hpp"
|
#include "AModule.hpp"
|
||||||
#include "bar.hpp"
|
#include "bar.hpp"
|
||||||
#include "modules/niri/backend.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 {
|
namespace waybar::modules::niri {
|
||||||
|
|
||||||
class Workspaces : public AModule, public EventHandler {
|
class Workspaces : public AModule, public EventHandler {
|
||||||
public:
|
public:
|
||||||
Workspaces(const std::string&, const Bar&, const Json::Value&);
|
Workspaces(const std::string& id, const Bar& bar, const Json::Value& config);
|
||||||
~Workspaces() override;
|
~Workspaces() override;
|
||||||
|
|
||||||
void update() 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:
|
private:
|
||||||
void onEvent(const Json::Value& ev) override;
|
void onEvent(const Json::Value& ev) override;
|
||||||
void doUpdate();
|
void doUpdate();
|
||||||
void sortWorkspaces(std::vector<Json::Value>& workspaces) const;
|
void createWorkspace(const Json::Value& workspace_data);
|
||||||
Gtk::Button& addButton(const Json::Value& ws);
|
void sortWorkspaces(std::vector<const Json::Value*>& workspaces) const;
|
||||||
std::string getIcon(const std::string& value, const Json::Value& ws);
|
|
||||||
bool isWorkspaceIgnored(const std::string& name);
|
bool isWorkspaceIgnored(const std::string& name);
|
||||||
bool handleScroll(GdkEventScroll* /*unused*/) override;
|
bool handleScroll(GdkEventScroll* /*unused*/) override;
|
||||||
// Added for window rewrite
|
// Added for window rewrite
|
||||||
void populateWindowRewriteConfig();
|
void populateWindowRewriteConfig();
|
||||||
void populateFormatWindowSeparatorConfig();
|
void populateFormatWindowSeparatorConfig();
|
||||||
std::string getRewrite(const std::string& app_id, const std::string& title);
|
std::string getRewrite(const std::string& app_id, const std::string& title);
|
||||||
|
std::string getWindowsRepresentation(const Json::Value& ws);
|
||||||
|
|
||||||
const Bar& bar_;
|
const Bar& bar_;
|
||||||
Gtk::Box box_;
|
Gtk::Box box_;
|
||||||
// Map from niri workspace id to button.
|
|
||||||
std::unordered_map<uint64_t, Gtk::Button> buttons_;
|
std::vector<std::unique_ptr<Workspace>> workspaces_;
|
||||||
|
|
||||||
// Vec of regex rules to ignore workspaces.
|
// Vec of regex rules to ignore workspaces.
|
||||||
std::vector<std::regex> ignoreWorkspaces_;
|
std::vector<std::regex> ignoreWorkspaces_;
|
||||||
|
|
||||||
|
|||||||
@@ -91,6 +91,20 @@ If none of the sorting options are enabled, workspaces keep their output/index o
|
|||||||
typeof: bool ++
|
typeof: bool ++
|
||||||
default: false ++
|
default: false ++
|
||||||
Enables this module to consume all left over space dynamically.
|
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*: ++
|
*ignore-workspaces*: ++
|
||||||
typeof: array ++
|
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
|
# Style
|
||||||
|
|
||||||
- *#workspaces button*
|
- *#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.
|
the bar that it is displayed on.
|
||||||
- *#workspaces button#niri-workspace-<name>*: Workspaces named this, or index
|
- *#workspaces button#niri-workspace-<name>*: Workspaces named this, or index
|
||||||
for unnamed workspaces.
|
for unnamed workspaces.
|
||||||
|
- *#workspaces button.niri-workspace*: The main container for the workspace.
|
||||||
|
- *#workspaces button .niri-taskbar-btn*: The icon buttons within the taskbar.
|
||||||
|
|||||||
@@ -74,6 +74,7 @@ wayland_client = dependency('wayland-client')
|
|||||||
wayland_cursor = dependency('wayland-cursor')
|
wayland_cursor = dependency('wayland-cursor')
|
||||||
wayland_protos = dependency('wayland-protocols')
|
wayland_protos = dependency('wayland-protocols')
|
||||||
gtkmm = dependency('gtkmm-3.0', version : ['>=3.22.0'])
|
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'))
|
dbusmenu_gtk = dependency('dbusmenu-gtk3-0.4', required: get_option('dbusmenu-gtk'))
|
||||||
giounix = dependency('gio-unix-2.0')
|
giounix = dependency('gio-unix-2.0')
|
||||||
jsoncpp = dependency('jsoncpp', version : ['>=1.9.2'], fallback : ['jsoncpp', 'jsoncpp_dep'])
|
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/language.cpp',
|
||||||
'src/modules/niri/window.cpp',
|
'src/modules/niri/window.cpp',
|
||||||
'src/modules/niri/workspaces.cpp',
|
'src/modules/niri/workspaces.cpp',
|
||||||
|
'src/modules/niri/workspace.cpp',
|
||||||
)
|
)
|
||||||
man_files += files(
|
man_files += files(
|
||||||
'man/waybar-niri-language.5.scd',
|
'man/waybar-niri-language.5.scd',
|
||||||
@@ -575,6 +577,7 @@ executable(
|
|||||||
jsoncpp,
|
jsoncpp,
|
||||||
wayland_cursor,
|
wayland_cursor,
|
||||||
gtkmm,
|
gtkmm,
|
||||||
|
giomm,
|
||||||
dbusmenu_gtk,
|
dbusmenu_gtk,
|
||||||
giounix,
|
giounix,
|
||||||
libinput,
|
libinput,
|
||||||
|
|||||||
@@ -0,0 +1,261 @@
|
|||||||
|
#include "modules/niri/workspace.hpp"
|
||||||
|
|
||||||
|
#include <gdkmm/pixbuf.h>
|
||||||
|
#include <gtkmm/icontheme.h>
|
||||||
|
#include <gtkmm/image.h>
|
||||||
|
#include <spdlog/spdlog.h>
|
||||||
|
#include <giomm/desktopappinfo.h>
|
||||||
|
#include <giomm/icon.h>
|
||||||
|
|
||||||
|
#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<Json::Value>& 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<Json::Value> 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<Json::Value>& 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<Gtk::Button>();
|
||||||
|
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<Gtk::Image>(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<Gtk::Label>(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<Gdk::Pixbuf> 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<Gdk::Pixbuf> {
|
||||||
|
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
|
||||||
+104
-198
@@ -1,8 +1,6 @@
|
|||||||
#include "modules/niri/workspaces.hpp"
|
#include "modules/niri/workspaces.hpp"
|
||||||
|
|
||||||
#include <fmt/ranges.h> // Needed for joining window representations
|
#include <fmt/ranges.h> // Needed for joining window representations
|
||||||
#include <gtkmm/button.h>
|
|
||||||
#include <gtkmm/label.h>
|
|
||||||
#include <spdlog/spdlog.h>
|
#include <spdlog/spdlog.h>
|
||||||
|
|
||||||
#include <algorithm>
|
#include <algorithm>
|
||||||
@@ -73,6 +71,12 @@ Workspaces::Workspaces(const std::string& id, const Bar& bar, const Json::Value&
|
|||||||
gIPC->registerForIPC("WorkspaceActiveWindowChanged", this);
|
gIPC->registerForIPC("WorkspaceActiveWindowChanged", this);
|
||||||
gIPC->registerForIPC("WorkspaceUrgencyChanged", 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()) {
|
if (config["enable-bar-scroll"].asBool()) {
|
||||||
auto& window = const_cast<Bar&>(bar_).window;
|
auto& window = const_cast<Bar&>(bar_).window;
|
||||||
window.add_events(Gdk::SCROLL_MASK | Gdk::SMOOTH_SCROLL_MASK);
|
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); }
|
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() {
|
void Workspaces::doUpdate() {
|
||||||
auto ipcLock = gIPC->lockData();
|
auto ipcLock = gIPC->lockData();
|
||||||
@@ -95,178 +99,78 @@ void Workspaces::doUpdate() {
|
|||||||
|
|
||||||
const auto alloutputs = config_["all-outputs"].asBool();
|
const auto alloutputs = config_["all-outputs"].asBool();
|
||||||
const auto display_cond = config_["display-condition"].asString();
|
const auto display_cond = config_["display-condition"].asString();
|
||||||
std::vector<Json::Value> my_workspaces;
|
const auto& all_workspaces = gIPC->workspaces();
|
||||||
const auto& workspaces = gIPC->workspaces();
|
const auto& all_windows = gIPC->windows();
|
||||||
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<std::string> 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 ---
|
|
||||||
|
|
||||||
|
std::vector<const Json::Value*> my_workspaces;
|
||||||
|
my_workspaces.reserve(all_workspaces.size());
|
||||||
|
for (const auto& ws : all_workspaces) {
|
||||||
std::string name;
|
std::string name;
|
||||||
if (ws["name"]) {
|
if (ws["name"]) {
|
||||||
name = ws["name"].asString();
|
name = ws["name"].asString();
|
||||||
} else {
|
} else {
|
||||||
name = std::to_string(ws["idx"].asUInt());
|
name = std::to_string(ws["idx"].asUInt());
|
||||||
}
|
}
|
||||||
button.set_name("niri-workspace-" + name);
|
if (isWorkspaceIgnored(name)) {
|
||||||
|
continue;
|
||||||
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 (config_["current-only"].asBool()) {
|
if (display_cond == "only-populated") {
|
||||||
const auto* property = alloutputs ? "is_focused" : "is_active";
|
if (ws["active_window_id"].isNull() && !ws["is_active"].asBool()) continue;
|
||||||
if (ws[property].asBool())
|
} else if (display_cond == "keep-named") {
|
||||||
button.show();
|
if (ws["name"].isNull() && ws["active_window_id"].isNull() && !ws["is_active"].asBool())
|
||||||
else
|
continue;
|
||||||
button.hide();
|
}
|
||||||
} else if (config_["hide-empty"].asBool()) {
|
|
||||||
if (ws["active_window_id"].isNull() && !ws["is_focused"].asBool())
|
if (alloutputs || ws["output"].asString() == bar_.output->name) {
|
||||||
button.hide();
|
my_workspaces.push_back(&ws);
|
||||||
else
|
|
||||||
button.show();
|
|
||||||
} else {
|
|
||||||
button.show();
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Refresh the button order.
|
sortWorkspaces(my_workspaces);
|
||||||
for (auto it = my_workspaces.cbegin(); it != my_workspaces.cend(); ++it) {
|
|
||||||
const auto& ws = *it;
|
|
||||||
|
|
||||||
const auto pos = static_cast<int>(std::distance(my_workspaces.cbegin(), it));
|
workspaces_.erase(std::remove_if(workspaces_.begin(), workspaces_.end(),
|
||||||
|
[&](const std::unique_ptr<Workspace>& 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()];
|
for (const auto* ws_ptr : my_workspaces) {
|
||||||
box_.reorder_child(button, pos);
|
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<Workspace>& w) { return w->id() == ws_id; });
|
||||||
|
|
||||||
|
if (it == workspaces_.end()) {
|
||||||
|
createWorkspace(ws);
|
||||||
|
it = workspaces_.end() - 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
std::vector<Json::Value> 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<int>(std::distance(my_workspaces.cbegin(), pos_it));
|
||||||
|
|
||||||
|
auto it =
|
||||||
|
std::find_if(workspaces_.begin(), workspaces_.end(),
|
||||||
|
[ws_id](const std::unique_ptr<Workspace>& 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();
|
AModule::update();
|
||||||
}
|
}
|
||||||
|
|
||||||
Gtk::Button& Workspaces::addButton(const Json::Value& ws) {
|
void Workspaces::createWorkspace(const Json::Value& workspace_data) {
|
||||||
std::string name;
|
auto ws = std::make_unique<Workspace>(workspace_data, *this);
|
||||||
if (ws["name"]) {
|
box_.pack_start(ws->button(), false, false, 0);
|
||||||
name = ws["name"].asString();
|
workspaces_.push_back(std::move(ws));
|
||||||
} 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;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// --- Start New Helper Functions ---
|
// --- Start New Helper Functions ---
|
||||||
@@ -383,18 +261,44 @@ std::string Workspaces::getRewrite(const std::string& app_id, const std::string&
|
|||||||
substitutions["title"] = title;
|
substitutions["title"] = title;
|
||||||
return util::rewriteString(m_windowRewriteDefault, substitutions);
|
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<std::string> 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 ---
|
// --- 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"];
|
const auto& icons = config_["format-icons"];
|
||||||
if (!icons) return value;
|
if (!icons) return value;
|
||||||
|
|
||||||
if (ws["is_urgent"].asBool() && icons["urgent"]) return icons["urgent"].asString();
|
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_active"].asBool() && icons["active"]) return icons["active"].asString();
|
||||||
|
|
||||||
if (ws["is_focused"].asBool() && icons["focused"]) return icons["focused"].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["active_window_id"].isNull() && icons["empty"]) return icons["empty"].asString();
|
||||||
|
|
||||||
if (ws["name"]) {
|
if (ws["name"]) {
|
||||||
@@ -458,7 +362,7 @@ bool Workspaces::handleScroll(GdkEventScroll* e) {
|
|||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
void Workspaces::sortWorkspaces(std::vector<Json::Value>& workspaces) const {
|
void Workspaces::sortWorkspaces(std::vector<const Json::Value*>& workspaces) const {
|
||||||
auto get_name = [](const Json::Value& ws) -> std::string {
|
auto get_name = [](const Json::Value& ws) -> std::string {
|
||||||
if (ws["name"]) return ws["name"].asString();
|
if (ws["name"]) return ws["name"].asString();
|
||||||
return std::to_string(ws["idx"].asUInt());
|
return std::to_string(ws["idx"].asUInt());
|
||||||
@@ -471,14 +375,16 @@ void Workspaces::sortWorkspaces(std::vector<Json::Value>& workspaces) const {
|
|||||||
|
|
||||||
const bool names_are_numeric =
|
const bool names_are_numeric =
|
||||||
std::all_of(workspaces.begin(), workspaces.end(),
|
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) {
|
auto compare_numeric_strings = [](const std::string& a, const std::string& b) {
|
||||||
if (a.size() != b.size()) return a.size() < b.size();
|
if (a.size() != b.size()) return a.size() < b.size();
|
||||||
return a < b;
|
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_) {
|
if (sort_by_id_) {
|
||||||
return a["id"].asUInt64() < b["id"].asUInt64();
|
return a["id"].asUInt64() < b["id"].asUInt64();
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user