Merge pull request #5078 from DreamMaoMao/mango

feat: add mango modules: workspace,language,keymode,window,layout
This commit is contained in:
Alexis Rouillard
2026-07-03 22:44:04 +02:00
committed by GitHub
21 changed files with 1597 additions and 0 deletions
+24
View File
@@ -42,6 +42,13 @@
#include "modules/niri/window.hpp"
#include "modules/niri/workspaces.hpp"
#endif
#ifdef HAVE_MANGO
#include "modules/mango/language.hpp"
#include "modules/mango/keymode.hpp"
#include "modules/mango/window.hpp"
#include "modules/mango/workspaces.hpp"
#include "modules/mango/layout.hpp"
#endif
#ifdef HAVE_WAYFIRE
#include "modules/wayfire/window.hpp"
#include "modules/wayfire/workspaces.hpp"
@@ -231,6 +238,23 @@ waybar::AModule* waybar::Factory::makeModule(const std::string& name,
return new waybar::modules::niri::Workspaces(id, bar_, config_[name]);
}
#endif
#ifdef HAVE_MANGO
if (ref == "mango/window") {
return new waybar::modules::mango::Window(id, bar_, config_[name]);
}
if (ref == "mango/workspaces") {
return new waybar::modules::mango::Workspaces(id, bar_, config_[name]);
}
if (ref == "mango/language") {
return new waybar::modules::mango::Language(id, bar_, config_[name]);
}
if (ref == "mango/keymode") {
return new waybar::modules::mango::Keymode(id, bar_, config_[name]);
}
if (ref == "mango/layout") {
return new waybar::modules::mango::Layout(id, bar_, config_[name]);
}
#endif
#ifdef HAVE_WAYFIRE
if (ref == "wayfire/window") {
return new waybar::modules::wayfire::Window(id, bar_, config_[name]);
+317
View File
@@ -0,0 +1,317 @@
#include "modules/mango/backend.hpp"
#include <fcntl.h>
#include <poll.h>
#include <spdlog/spdlog.h>
#include <sys/poll.h>
#include <sys/socket.h>
#include <sys/un.h>
#include <unistd.h>
#include <sstream>
#include <thread>
#include <vector>
#include "util/scoped_fd.hpp"
namespace waybar::modules::mango {
int IPC::connectToSocket() {
const char* socket_path = getenv("MANGO_INSTANCE_SIGNATURE");
if (!socket_path) {
throw std::runtime_error("Mango IPC: MANGO_INSTANCE_SIGNATURE not set");
}
struct sockaddr_un addr;
util::ScopedFd fd(socket(AF_UNIX, SOCK_STREAM, 0));
if (fd == -1) throw std::runtime_error("socket() failed");
addr.sun_family = AF_UNIX;
strncpy(addr.sun_path, socket_path, sizeof(addr.sun_path) - 1);
addr.sun_path[sizeof(addr.sun_path) - 1] = 0;
if (connect(fd, (struct sockaddr*)&addr, sizeof(addr)) == -1) {
throw std::runtime_error("connect() failed");
}
return fd.release();
}
Json::Value IPC::sendCommand(const std::string& cmd) {
util::ScopedFd fd(IPC::connectToSocket());
std::string full_cmd = cmd + "\n";
ssize_t total_written = 0;
while (total_written < (ssize_t)full_cmd.size()) {
ssize_t res = write(fd, full_cmd.c_str() + total_written, full_cmd.size() - total_written);
if (res < 0) {
if (errno == EINTR) continue;
throw std::runtime_error("Failed to write command");
}
total_written += res;
}
char buf[4096];
std::string response;
while (true) {
ssize_t n = read(fd, buf, sizeof(buf) - 1);
if (n <= 0) {
if (n == 0) break;
if (errno == EINTR || errno == EAGAIN || errno == EWOULDBLOCK) continue;
throw std::runtime_error("Read error");
}
buf[n] = '\0';
response += buf;
if (response.find('\n') != std::string::npos) break;
}
Json::Value root;
std::istringstream iss(response);
Json::CharReaderBuilder builder;
std::string errors;
if (!Json::parseFromStream(builder, iss, &root, &errors)) {
throw std::runtime_error("JSON parse error: " + errors);
}
return root;
}
Json::Value IPC::send(const Json::Value& request) {
if (!request.isMember("command")) {
throw std::runtime_error("Mango IPC: request must have 'command' field");
}
return sendCommand(request["command"].asString());
}
void IPC::sendAsync(const Json::Value& request) {
if (!request.isMember("command")) {
spdlog::error("Mango IPC: request must have 'command' field");
return;
}
std::string cmd = request["command"].asString();
std::thread([cmd]() {
try {
IPC::sendCommand(cmd);
} catch (const std::exception& e) {
spdlog::error("IPC async send failed: {}", e.what());
}
}).detach();
}
IPC::IPC() : sockfd_(-1), active_client_(Json::nullValue) { startIPC(); }
IPC::~IPC() {
if (sockfd_ != -1) close(sockfd_);
if (ipc_thread_.joinable()) ipc_thread_.join();
}
void IPC::startIPC() {
sockfd_ = IPC::connectToSocket();
ipc_thread_ = std::thread([this]() {
spdlog::info("Mango IPC thread started");
struct pollfd pfd;
pfd.fd = sockfd_;
pfd.events = POLLIN;
const std::vector<std::string> subs = {"watch all-monitors"};
for (const auto& cmd : subs) {
if (write(sockfd_, cmd.c_str(), cmd.size()) != (ssize_t)cmd.size() ||
write(sockfd_, "\n", 1) != 1) {
spdlog::error("Failed to subscribe to {}", cmd);
return;
}
}
char buf[4096];
std::string buffer;
while (true) {
int ret = poll(&pfd, 1, 1000);
if (ret == 0) continue;
if (ret < 0) {
if (errno == EINTR) continue;
spdlog::error("IPC poll error: {}", strerror(errno));
break;
}
if (pfd.revents & (POLLERR | POLLHUP | POLLNVAL)) {
spdlog::info("Mango IPC socket closed or invalid");
break;
}
if (pfd.revents & POLLIN) {
ssize_t n = read(sockfd_, buf, sizeof(buf));
if (n == 0) {
spdlog::info("Mango IPC connection closed");
break;
}
if (n < 0) {
if (errno == EINTR) continue;
spdlog::error("IPC read error: {}", strerror(errno));
break;
}
buffer.append(buf, n);
size_t pos;
while ((pos = buffer.find('\n')) != std::string::npos) {
std::string line = buffer.substr(0, pos);
buffer.erase(0, pos + 1);
if (line.empty()) continue;
try {
parseIPC(line);
} catch (const std::exception& e) {
spdlog::warn("Failed to parse IPC line: {} - {}", line, e.what());
}
}
}
}
});
}
void IPC::parseIPC(const std::string& line) {
Json::Value root;
Json::CharReaderBuilder builder;
std::string errors;
std::istringstream iss(line);
if (!Json::parseFromStream(builder, iss, &root, &errors)) {
throw std::runtime_error("JSON parse error: " + errors);
}
if (root.isMember("monitors") && root["monitors"].isArray()) {
for (const auto& mon : root["monitors"]) {
handleMonitorUpdate(mon);
}
Json::Value active_monitor;
for (const auto& mon : root["monitors"]) {
if (mon["active"].asBool()) {
active_monitor = mon;
break;
}
}
if (!active_monitor.isNull()) {
const auto& active_client = active_monitor["active_client"];
updateFocusingClient(active_client);
if (active_monitor.isMember("keyboardlayout")) {
updateKeyboardLayout(active_monitor["keyboardlayout"].asString());
}
if (active_monitor.isMember("keymode")) {
std::lock_guard<std::mutex> lock(data_mutex_);
keymode_ = active_monitor["keymode"].asString();
}
}
std::vector<EventHandler*> handlers_to_notify;
{
std::lock_guard<std::mutex> lock(callback_mutex_);
for (auto& [ev, handler] : callbacks_) {
if (ev == "monitor") {
handlers_to_notify.push_back(handler);
}
}
}
for (auto* handler : handlers_to_notify) {
handler->onEvent(root);
}
return;
}
spdlog::debug("Unhandled IPC message: {}", line);
}
std::unordered_map<std::string, Json::Value> IPC::getMonitors() const {
std::lock_guard<std::mutex> lock(data_mutex_);
return monitors_;
}
IPC& IPC::getInstance() {
static IPC instance;
return instance;
}
Json::Value IPC::getMonitor(const std::string& name) {
std::lock_guard<std::mutex> lock(data_mutex_);
auto it = monitors_.find(name);
if (it != monitors_.end()) {
return it->second;
}
return Json::nullValue;
}
std::string IPC::getKeyboardLayout() const {
std::lock_guard<std::mutex> lock(data_mutex_);
return keyboard_layout_;
}
std::string IPC::getKeymode() const {
std::lock_guard<std::mutex> lock(data_mutex_);
return keymode_;
}
Json::Value IPC::getActiveClientForMonitor(const std::string& name) const {
std::lock_guard<std::mutex> lock(data_mutex_);
auto it = monitors_.find(name);
if (it != monitors_.end() && it->second.isMember("active_client")) {
return it->second["active_client"];
}
return Json::nullValue;
}
std::string IPC::getLayoutSymbolForMonitor(const std::string& name) const {
std::lock_guard<std::mutex> lock(data_mutex_);
auto it = monitors_.find(name);
if (it != monitors_.end() && it->second.isMember("layout_symbol")) {
return it->second["layout_symbol"].asString();
}
return {};
}
void IPC::handleMonitorUpdate(const Json::Value& mon) {
std::lock_guard<std::mutex> lock(data_mutex_);
monitors_[mon["name"].asString()] = mon;
}
void IPC::updateFocusingClient(const Json::Value& client) {
{
std::lock_guard<std::mutex> lock(data_mutex_);
active_client_ = client;
if (client.isNull() || !client.isObject() || client["id"].isNull()) {
focusing_client_id_ = 0;
} else {
focusing_client_id_ = client["id"].asUInt64();
clients_[focusing_client_id_] = client;
}
}
}
void IPC::updateKeyboardLayout(const std::string& layout) {
{
std::lock_guard<std::mutex> lock(data_mutex_);
keyboard_layout_ = layout;
}
}
void IPC::registerForIPC(const std::string& ev, EventHandler* handler) {
if (!handler) return;
std::lock_guard<std::mutex> lock(callback_mutex_);
callbacks_.emplace_back(ev, handler);
}
void IPC::unregisterForIPC(EventHandler* handler) {
if (!handler) return;
std::lock_guard<std::mutex> lock(callback_mutex_);
for (auto it = callbacks_.begin(); it != callbacks_.end();) {
if (it->second == handler)
it = callbacks_.erase(it);
else
++it;
}
}
} // namespace waybar::modules::mango
+58
View File
@@ -0,0 +1,58 @@
#include "modules/mango/keymode.hpp"
#include <spdlog/spdlog.h>
namespace waybar::modules::mango {
Keymode::Keymode(const std::string& id, const Bar& bar, const Json::Value& config)
: ALabel(config, "keymode", id, "{}", 0, false), bar_(bar) {
IPC::getInstance().registerForIPC("monitor", this);
dp.emit();
}
Keymode::~Keymode() { IPC::getInstance().unregisterForIPC(this); }
void Keymode::onEvent(const Json::Value& ev) { dp.emit(); }
void Keymode::doUpdate() {
std::lock_guard<std::mutex> lock(mutex_);
std::string current = IPC::getInstance().getKeymode();
// if keymode is empty, hide the label
if (current.empty()) {
label_.hide();
last_keymode_.clear();
return;
}
// if keymode is the same as last time, skip style changes
if (current != last_keymode_) {
if (!last_keymode_.empty()) label_.get_style_context()->remove_class(last_keymode_);
label_.get_style_context()->add_class(current);
last_keymode_ = current;
}
// support config's format-keymode custom format (such as format-default, format-resize, etc.)
std::string text;
std::string format_key = "format-" + current;
if (config_.isMember(format_key)) {
text = fmt::format(fmt::runtime(config_[format_key].asString()), fmt::arg("mode", current));
} else {
text = fmt::format(fmt::runtime(format_), fmt::arg("mode", current));
}
if (!text.empty()) {
label_.show();
label_.set_markup(text);
} else {
label_.hide();
}
}
void Keymode::update() {
doUpdate();
ALabel::update();
}
} // namespace waybar::modules::mango
+148
View File
@@ -0,0 +1,148 @@
#include "modules/mango/language.hpp"
#include <spdlog/spdlog.h>
#include <xkbcommon/xkbcommon.h>
#include <xkbcommon/xkbregistry.h>
#include "util/string.hpp"
namespace waybar::modules::mango {
Language::Language(const std::string& id, const Bar& bar, const Json::Value& config)
: ALabel(config, "language", id, "{}", 0, false), bar_(bar), rxkb_ctx_(nullptr) {
rxkb_ctx_ = rxkb_context_new(RXKB_CONTEXT_LOAD_EXOTIC_RULES);
if (rxkb_ctx_) {
rxkb_context_parse_default_ruleset(rxkb_ctx_);
}
IPC::getInstance().registerForIPC("monitor", this);
updateFromIPC();
dp.emit();
}
Language::~Language() {
IPC::getInstance().unregisterForIPC(this);
if (rxkb_ctx_) rxkb_context_unref(rxkb_ctx_);
}
void Language::updateFromIPC() {
std::lock_guard<std::mutex> lock(mutex_);
std::string layout = IPC::getInstance().getKeyboardLayout();
layouts_.clear();
if (!layout.empty()) {
Layout l = getLayout(layout);
layouts_.push_back(l);
current_idx_ = 0;
} else {
current_idx_ = 0;
}
}
void Language::doUpdate() {
std::lock_guard<std::mutex> lock(mutex_);
if (layouts_.empty() || current_idx_ >= layouts_.size()) {
label_.hide();
return;
}
const auto& layout = layouts_[current_idx_];
if (!last_short_name_.empty()) label_.get_style_context()->remove_class(last_short_name_);
if (!layout.short_name.empty()) {
label_.get_style_context()->add_class(layout.short_name);
last_short_name_ = layout.short_name;
}
std::string layoutName;
std::string variant_key = "format-" + layout.short_description + "-" + layout.variant;
if (!layout.variant.empty() && config_.isMember(variant_key)) {
layoutName =
fmt::format(fmt::runtime(config_[variant_key].asString()),
fmt::arg("long", layout.full_name), fmt::arg("short", layout.short_name),
fmt::arg("shortDescription", layout.short_description),
fmt::arg("variant", layout.variant));
} else if (config_.isMember("format-" + layout.short_description)) {
std::string key = "format-" + layout.short_description;
layoutName =
fmt::format(fmt::runtime(config_[key].asString()), fmt::arg("long", layout.full_name),
fmt::arg("short", layout.short_name),
fmt::arg("shortDescription", layout.short_description),
fmt::arg("variant", layout.variant));
} else {
layoutName = fmt::format(fmt::runtime(format_), fmt::arg("long", layout.full_name),
fmt::arg("short", layout.short_name),
fmt::arg("shortDescription", layout.short_description),
fmt::arg("variant", layout.variant));
}
if (!layoutName.empty()) {
label_.show();
label_.set_markup(layoutName);
} else {
label_.hide();
}
}
void Language::update() {
updateFromIPC();
doUpdate();
ALabel::update();
}
void Language::onEvent(const Json::Value& ev) {
updateFromIPC();
dp.emit();
}
Language::Layout Language::getLayout(const std::string& fullName) {
if (rxkb_ctx_) {
rxkb_layout* layout = rxkb_layout_first(rxkb_ctx_);
while (layout != nullptr) {
std::string desc = rxkb_layout_get_description(layout);
if (desc == fullName) {
std::string short_name = rxkb_layout_get_name(layout);
const char* variant_ptr = rxkb_layout_get_variant(layout);
std::string variant = variant_ptr ? variant_ptr : "";
const char* brief_ptr = rxkb_layout_get_brief(layout);
std::string short_description = brief_ptr ? brief_ptr : "";
if (short_description.empty()) {
short_description = short_name;
}
short_description = short_name;
Layout info{desc, short_name, variant, short_description};
return info;
}
layout = rxkb_layout_next(layout);
}
}
spdlog::warn("mango language: rxkb failed to find layout '{}', using string parsing fallback",
fullName);
Layout l;
l.full_name = fullName;
l.variant = "";
size_t paren_start = fullName.find('(');
size_t paren_end = fullName.find(')');
if (paren_start != std::string::npos && paren_end != std::string::npos &&
paren_end > paren_start) {
l.short_name = fullName.substr(paren_start + 1, paren_end - paren_start - 1);
} else if (fullName.length() >= 2) {
l.short_name = fullName.substr(0, 2);
} else {
l.short_name = fullName;
}
std::transform(l.short_name.begin(), l.short_name.end(), l.short_name.begin(),
[](unsigned char c) { return std::tolower(c); });
l.short_description = l.short_name;
return l;
}
} // namespace waybar::modules::mango
+56
View File
@@ -0,0 +1,56 @@
#include "modules/mango/layout.hpp"
#include <spdlog/spdlog.h>
namespace waybar::modules::mango {
Layout::Layout(const std::string& id, const Bar& bar, const Json::Value& config)
: ALabel(config, "layout", id, "{}", 0, false), bar_(bar) {
IPC::getInstance().registerForIPC("monitor", this);
dp.emit();
}
Layout::~Layout() { IPC::getInstance().unregisterForIPC(this); }
void Layout::onEvent(const Json::Value& ev) { dp.emit(); }
void Layout::doUpdate() {
std::lock_guard<std::mutex> lock(mutex_);
std::string symbol = IPC::getInstance().getLayoutSymbolForMonitor(bar_.output->name);
if (symbol.empty()) {
label_.hide();
last_symbol_.clear();
return;
}
if (symbol != last_symbol_) {
if (!last_symbol_.empty()) label_.get_style_context()->remove_class(last_symbol_);
label_.get_style_context()->add_class(symbol);
last_symbol_ = symbol;
}
std::string text;
std::string format_key = "format-" + symbol;
if (config_.isMember(format_key)) {
text = fmt::format(fmt::runtime(config_[format_key].asString()), fmt::arg("symbol", symbol));
} else {
text = fmt::format(fmt::runtime(format_), fmt::arg("symbol", symbol));
}
if (!text.empty()) {
label_.show();
label_.set_markup(text);
} else {
label_.hide();
}
}
void Layout::update() {
doUpdate();
ALabel::update();
}
} // namespace waybar::modules::mango
+93
View File
@@ -0,0 +1,93 @@
#include "modules/mango/window.hpp"
#include <spdlog/spdlog.h>
#include "util/rewrite_string.hpp"
#include "util/sanitize_str.hpp"
namespace waybar::modules::mango {
Window::Window(const std::string& id, const Bar& bar, const Json::Value& config)
: AAppIconLabel(config, "window", id, "{title}", 0, true), bar_(bar) {
IPC::getInstance().registerForIPC("monitor", this);
dp.emit();
}
Window::~Window() { IPC::getInstance().unregisterForIPC(this); }
void Window::onEvent(const Json::Value& ev) { dp.emit(); }
void Window::doUpdate() {
std::lock_guard<std::mutex> lock(mutex_);
const Json::Value& client = IPC::getInstance().getActiveClientForMonitor(bar_.output->name);
// judge whether to hide: active_client is null or title field is null
if (client.isNull() || !client.isObject() || client["title"].isNull()) {
event_box_.hide();
label_.hide();
updateAppIconName("", "");
setClass("empty", true);
if (!oldAppId_.empty()) setClass(oldAppId_, false);
oldAppId_.clear();
return;
}
// if we have a valid client, show the label and update content
event_box_.show();
label_.show();
setClass("empty", false);
std::string title = client["title"].asString();
std::string appid = client["appid"].asString();
std::string sanitized_title = waybar::util::sanitize_string(title);
std::string sanitized_appid = waybar::util::sanitize_string(appid);
label_.set_markup(waybar::util::rewriteString(
fmt::format(fmt::runtime(format_), fmt::arg("title", sanitized_title),
fmt::arg("app_id", sanitized_appid)),
config_["rewrite"]));
updateAppIconName(appid, "");
if (tooltipEnabled()) label_.set_tooltip_markup(title);
// Solo judgment
bool solo = false;
if (client.isMember("tags") && client["tags"].isArray() && client["tags"].size() == 1) {
int tag_idx = client["tags"][0].asInt();
const auto& monitors = IPC::getInstance().getMonitors();
auto mon_it = monitors.find(client["monitor"].asString());
if (mon_it != monitors.end()) {
const auto& tags = mon_it->second["tags"];
for (const auto& tag : tags) {
if (tag["index"].asInt() == tag_idx) {
solo = (tag["client_count"].asInt() == 1);
break;
}
}
}
}
setClass("solo", solo);
if (!appid.empty()) setClass(appid, solo);
if (oldAppId_ != appid) {
if (!oldAppId_.empty()) setClass(oldAppId_, false);
oldAppId_ = appid;
}
}
void Window::update() {
doUpdate();
AAppIconLabel::update();
}
void Window::setClass(const std::string& className, bool enable) {
auto style_context = event_box_.get_style_context();
if (enable) {
if (!style_context->has_class(className)) style_context->add_class(className);
} else {
style_context->remove_class(className);
}
}
} // namespace waybar::modules::mango
+252
View File
@@ -0,0 +1,252 @@
#include "modules/mango/workspaces.hpp"
#include <spdlog/spdlog.h>
#include <algorithm>
namespace waybar::modules::mango {
Workspaces::Workspaces(const std::string& id, const Bar& bar, const Json::Value& config)
: AModule(config, "workspaces", id, false, false), bar_(bar), box_(bar.orientation, 0) {
box_.set_name("workspaces");
if (!id.empty()) box_.get_style_context()->add_class(id);
box_.get_style_context()->add_class(MODULE_CLASS);
event_box_.add(box_);
if (config_["on-click"].isString()) on_click_left_ = config_["on-click"].asString();
if (config_["on-click-middle"].isString())
on_click_middle_ = config_["on-click-middle"].asString();
if (config_["on-click-right"].isString()) on_click_right_ = config_["on-click-right"].asString();
overview_button_ = new Gtk::Button("OVERVIEW");
overview_button_->set_relief(Gtk::RELIEF_NONE);
box_.pack_start(*overview_button_, false, false, 0);
if (!on_click_left_.empty() || !on_click_middle_.empty() || !on_click_right_.empty()) {
overview_button_->add_events(Gdk::BUTTON_PRESS_MASK);
overview_button_->signal_button_press_event().connect(
[this](GdkEventButton* event) -> bool { return handleButtonClick(event, 0, true); }, false);
}
IPC::getInstance().registerForIPC("monitor", this);
dp.emit();
}
Workspaces::~Workspaces() {
IPC::getInstance().unregisterForIPC(this);
if (overview_button_) {
box_.remove(*overview_button_);
delete overview_button_;
overview_button_ = nullptr;
}
for (auto& [idx, btn] : buttons_) {
box_.remove(btn);
}
buttons_.clear();
}
void Workspaces::onEvent(const Json::Value& ev) { dp.emit(); }
void Workspaces::doUpdate() {
Json::Value monitor = IPC::getInstance().getMonitor(bar_.output->name);
if (monitor.isNull()) return;
const auto& tags = monitor["tags"];
bool overview_mode = false;
if (monitor.isMember("active_tags") && monitor["active_tags"].isArray()) {
const auto& active_tags = monitor["active_tags"];
if (active_tags.size() == 1 && active_tags[0].asInt() == 0) {
overview_mode = true;
}
}
for (auto& [idx, btn] : buttons_) {
btn.hide();
}
if (overview_mode) {
overview_button_->show();
auto style = overview_button_->get_style_context();
style->add_class("overview");
if (monitor["active"].asBool())
style->add_class("current_output");
else
style->remove_class("current_output");
std::string label =
config_["overview-label"].isString() ? config_["overview-label"].asString() : "OVERVIEW";
if (!config_["disable-markup"].asBool()) {
if (auto gtk_label = dynamic_cast<Gtk::Label*>(overview_button_->get_child())) {
gtk_label->set_markup(label);
}
} else {
overview_button_->set_label(label);
}
} else {
overview_button_->hide();
for (auto btn_it = buttons_.begin(); btn_it != buttons_.end();) {
uint64_t id = btn_it->first;
bool found = std::any_of(tags.begin(), tags.end(), [id](const Json::Value& tag) {
return tag["index"].asUInt64() == id;
});
if (!found) {
box_.remove(btn_it->second);
btn_it = buttons_.erase(btn_it);
} else {
++btn_it;
}
}
for (const auto& tag : tags) {
uint64_t idx = tag["index"].asUInt64();
auto btn_it = buttons_.find(idx);
Gtk::Button& button = (btn_it == buttons_.end()) ? addButton(idx) : btn_it->second;
updateButtonState(button, tag, monitor);
}
std::vector<uint64_t> indices;
for (const auto& tag : tags) indices.push_back(tag["index"].asUInt64());
std::sort(indices.begin(), indices.end());
int pos = 0;
for (uint64_t idx : indices) {
box_.reorder_child(buttons_[idx], pos + 1);
pos++;
}
}
}
void Workspaces::update() {
doUpdate();
AModule::update();
}
Gtk::Button& Workspaces::addButton(uint64_t idx) {
auto [it, _] = buttons_.emplace(idx, std::to_string(idx));
auto& button = it->second;
box_.pack_start(button, false, false, 0);
button.set_relief(Gtk::RELIEF_NONE);
if (!on_click_left_.empty() || !on_click_middle_.empty() || !on_click_right_.empty()) {
button.add_events(Gdk::BUTTON_PRESS_MASK);
button.signal_button_press_event().connect(
[this, idx](GdkEventButton* event) -> bool { return handleButtonClick(event, idx, false); },
false);
}
button.show_all();
return button;
}
void Workspaces::updateButtonState(Gtk::Button& button, const Json::Value& tag,
const Json::Value& monitor) {
auto style = button.get_style_context();
bool active = tag["is_active"].asBool();
bool urgent = tag["is_urgent"].asBool();
bool empty = (tag["client_count"].asInt() == 0);
if (active)
style->add_class("active");
else
style->remove_class("active");
if (urgent)
style->add_class("urgent");
else
style->remove_class("urgent");
if (empty)
style->add_class("empty");
else
style->remove_class("empty");
if (monitor["active"].asBool())
style->add_class("current_output");
else
style->remove_class("current_output");
uint64_t idx = tag["index"].asUInt64();
std::string name = std::to_string(idx);
if (config_["format"].isString()) {
name = fmt::format(fmt::runtime(config_["format"].asString()),
fmt::arg("icon", getIcon(name, tag)), fmt::arg("value", name),
fmt::arg("index", idx), fmt::arg("output", monitor["name"].asString()));
}
if (!config_["disable-markup"].asBool()) {
if (auto gtk_label = dynamic_cast<Gtk::Label*>(button.get_child())) {
gtk_label->set_markup(name);
}
} else {
button.set_label(name);
}
if (config_["current-only"].asBool()) {
if (active)
button.show();
else
button.hide();
} else if (config_["hide-empty"].asBool() && empty && !active) {
button.hide();
} else {
button.show();
}
}
std::string Workspaces::getIcon(const std::string& value, const Json::Value& tag) {
const auto& icons = config_["format-icons"];
if (!icons) return value;
if (tag["is_urgent"].asBool() && icons["urgent"]) return icons["urgent"].asString();
if (tag["is_active"].asBool() && icons["active"]) return icons["active"].asString();
if (tag["client_count"].asInt() == 0 && icons["empty"]) return icons["empty"].asString();
std::string idx = std::to_string(tag["index"].asUInt());
if (icons[idx]) return icons[idx].asString();
if (icons["default"]) return icons["default"].asString();
return value;
}
bool Workspaces::handleButtonClick(GdkEventButton* event, uint64_t idx, bool isOverview) {
std::string action;
if (event->button == 1)
action = on_click_left_;
else if (event->button == 2)
action = on_click_middle_;
else if (event->button == 3)
action = on_click_right_;
if (action.empty()) return true;
try {
std::string cmd;
if (isOverview) {
if (action == "activate")
cmd = "dispatch overview";
else if (action == "toggle")
cmd = "dispatch toggleoverview";
} else {
if (action == "activate")
cmd = "dispatch view," + std::to_string(idx);
else if (action == "toggle")
cmd = "dispatch toggleview," + std::to_string(idx);
}
if (!cmd.empty()) {
Json::Value req;
req["command"] = cmd;
IPC::sendAsync(req);
}
} catch (const std::exception& e) {
spdlog::error("Error sending IPC command: {}", e.what());
}
return true;
}
} // namespace waybar::modules::mango