Merge remote-tracking branch 'origin/master' into pr-3017

# Conflicts:
#	include/modules/hyprland/workspaces.hpp
#	man/waybar-hyprland-workspaces.5.scd
#	src/modules/hyprland/workspaces.cpp
This commit is contained in:
Alex
2026-07-04 00:55:38 +02:00
357 changed files with 22757 additions and 7219 deletions
+34 -19
View File
@@ -33,21 +33,21 @@ std::string toLowerCase(const std::string& input) {
std::optional<std::string> getFileBySuffix(const std::string& dir, const std::string& suffix,
bool check_lower_case) {
if (!std::filesystem::exists(dir)) {
return {};
}
for (const auto& entry : std::filesystem::recursive_directory_iterator(dir)) {
if (entry.is_regular_file()) {
std::string filename = entry.path().filename().string();
if (filename.size() < suffix.size()) {
continue;
}
if ((filename.compare(filename.size() - suffix.size(), suffix.size(), suffix) == 0) ||
(check_lower_case && filename.compare(filename.size() - suffix.size(), suffix.size(),
toLowerCase(suffix)) == 0)) {
return entry.path().string();
try {
for (const auto& entry : std::filesystem::recursive_directory_iterator(dir)) {
if (entry.is_regular_file()) {
std::string filename = entry.path().filename().string();
if (filename.size() < suffix.size()) {
continue;
}
if ((filename.compare(filename.size() - suffix.size(), suffix.size(), suffix) == 0) ||
(check_lower_case && filename.compare(filename.size() - suffix.size(), suffix.size(),
toLowerCase(suffix)) == 0)) {
return entry.path().string();
}
}
}
} catch (const std::filesystem::filesystem_error&) {
}
return {};
@@ -63,7 +63,8 @@ std::optional<std::string> getDesktopFilePath(const std::string& app_identifier,
return {};
}
const auto data_dirs = Glib::get_system_data_dirs();
auto data_dirs = Glib::get_system_data_dirs();
data_dirs.insert(data_dirs.begin(), Glib::get_user_data_dir());
for (const auto& data_dir : data_dirs) {
const auto data_app_dir = data_dir + "/applications/";
auto desktop_file_suffix = app_identifier + ".desktop";
@@ -96,14 +97,14 @@ std::optional<Glib::ustring> getIconName(const std::string& app_identifier,
return app_identifier;
}
const auto app_identifier_desktop = app_identifier + "-desktop";
auto app_identifier_desktop = app_identifier + "-desktop";
if (DefaultGtkIconThemeWrapper::has_icon(app_identifier_desktop)) {
return app_identifier_desktop;
}
const auto first_space = app_identifier.find_first_of(' ');
auto first_space = app_identifier.find_first_of(' ');
if (first_space != std::string::npos) {
const auto first_word = toLowerCase(app_identifier.substr(0, first_space));
auto first_word = toLowerCase(app_identifier.substr(0, first_space));
if (DefaultGtkIconThemeWrapper::has_icon(first_word)) {
return first_word;
}
@@ -111,7 +112,7 @@ std::optional<Glib::ustring> getIconName(const std::string& app_identifier,
const auto first_dash = app_identifier.find_first_of('-');
if (first_dash != std::string::npos) {
const auto first_word = toLowerCase(app_identifier.substr(0, first_dash));
auto first_word = toLowerCase(app_identifier.substr(0, first_dash));
if (DefaultGtkIconThemeWrapper::has_icon(first_word)) {
return first_word;
}
@@ -150,10 +151,24 @@ void AAppIconLabel::updateAppIconName(const std::string& app_identifier,
}
void AAppIconLabel::updateAppIcon() {
if (update_app_icon_) {
if (update_app_icon_ || (!iconEnabled() && image_.get_visible())) {
update_app_icon_ = false;
if (app_icon_name_.empty()) {
image_.set_visible(false);
} else if (app_icon_name_.front() == '/') {
try {
int scaled_icon_size = app_icon_size_ * image_.get_scale_factor();
auto pixbuf =
Gdk::Pixbuf::create_from_file(app_icon_name_, scaled_icon_size, scaled_icon_size);
auto surface = Gdk::Cairo::create_surface_from_pixbuf(pixbuf, image_.get_scale_factor(),
image_.get_window());
image_.set(surface);
image_.set_visible(true);
} catch (const Glib::Exception& e) {
spdlog::warn("Failed to load app icon {}: {}", app_icon_name_, std::string(e.what()));
image_.set_visible(false);
}
} else {
image_.set_from_icon_name(app_icon_name_, Gtk::ICON_SIZE_INVALID);
image_.set_visible(true);
+298
View File
@@ -0,0 +1,298 @@
#include "AGraph.hpp"
#include <cairomm/context.h>
#include <fmt/format.h>
#include <cmath>
#include <fstream>
#include <iostream>
#include <util/command.hpp>
#include "config.hpp"
namespace waybar {
AGraph::AGraph(const Json::Value& config, const std::string& name, const std::string& id,
uint16_t interval, bool enable_click, bool enable_scroll)
: AModule(config, name, id,
config["format-alt"].isString() || config["menu"].isString() || enable_click,
enable_scroll),
interval_(config_["interval"] == "once"
? std::chrono::seconds::max()
: std::chrono::seconds(
config_["interval"].isUInt() ? config_["interval"].asUInt() : interval)) {
graph_.signal_draw().connect(sigc::mem_fun(*this, &AGraph::onDraw));
graph_.set_name(name);
if (!id.empty()) {
graph_.get_style_context()->add_class(id);
}
graph_.get_style_context()->add_class(MODULE_CLASS);
if (config_["width"].isUInt()) {
graph_.set_size_request(config_["width"].asUInt(), -1);
} else {
graph_.set_size_request(100, -1);
}
event_box_.add(graph_);
if (config_["datapoints"].isUInt() && config_["datapoints"].asUInt() > 0) {
datapoints_ = config_["datapoints"].asUInt();
}
if (config_["graph_type"].isString()) {
std::string type = config_["graph_type"].asString();
if (type == "line") {
graph_type_ = GraphType::LINE;
} else if (type == "bar") {
graph_type_ = GraphType::BAR;
} else if (type == "gauge") {
graph_type_ = GraphType::GAUGE;
}
}
// If a GTKMenu is requested in the config
if (config_["menu"].isString()) {
// Create the GTKMenu widget
try {
// Check that the file exists
std::string menuFile = config_["menu-file"].asString();
// there might be "~" or "$HOME" in original path, try to expand it.
auto result = Config::tryExpandPath(menuFile, "");
if (result.empty()) {
throw std::runtime_error("Failed to expand file: " + menuFile);
}
menuFile = result.front();
// Read the menu descriptor file
std::ifstream file(menuFile);
if (!file.is_open()) {
throw std::runtime_error("Failed to open file: " + menuFile);
}
std::stringstream fileContent;
fileContent << file.rdbuf();
GtkBuilder* builder = gtk_builder_new();
// Make the GtkBuilder and check for errors in his parsing
if (gtk_builder_add_from_string(builder, fileContent.str().c_str(), -1, nullptr) == 0U) {
throw std::runtime_error("Error found in the file " + menuFile);
}
menu_ = gtk_builder_get_object(builder, "menu");
if (menu_ == nullptr) {
throw std::runtime_error("Failed to get 'menu' object from GtkBuilder");
}
submenus_ = std::map<std::string, GtkMenuItem*>();
menuActionsMap_ = std::map<std::string, std::string>();
// Linking actions to the GTKMenu based on
for (Json::Value::const_iterator it = config_["menu-actions"].begin();
it != config_["menu-actions"].end(); ++it) {
std::string key = it.key().asString();
submenus_[key] = GTK_MENU_ITEM(gtk_builder_get_object(builder, key.c_str()));
menuActionsMap_[key] = it->asString();
g_signal_connect(submenus_[key], "activate", G_CALLBACK(handleGtkMenuEvent),
(gpointer)menuActionsMap_[key].c_str());
}
} catch (std::runtime_error& e) {
spdlog::warn("Error while creating the menu : {}. Menu popup not activated.", e.what());
}
}
}
auto AGraph::update() -> void {
graph_.queue_draw();
AModule::update();
}
void AGraph::handleGtkMenuEvent(GtkMenuItem* /*menuitem*/, gpointer data) {
waybar::util::command::res res = waybar::util::command::exec((char*)data, "GtkMenu");
}
void AGraph::addValue(const int n) {
if (datapoints_ > 0 && values_.size() >= datapoints_) {
values_.pop_front();
}
values_.push_back(n);
}
bool AGraph::onDraw(const Cairo::RefPtr<Cairo::Context>& cr) {
const int width = graph_.get_allocated_width();
const int height = graph_.get_allocated_height() - 1;
if (values_.empty() || width <= 0 || height <= 0) {
return false;
}
auto style_context = graph_.get_style_context();
Gdk::RGBA fg_color = style_context->get_color(Gtk::STATE_FLAG_NORMAL);
Gdk::RGBA bg_color = fg_color;
bg_color.set_alpha(0.3);
cr->set_line_width(1.0);
const double step_width = static_cast<double>(width) / datapoints_;
const int values_count = values_.size();
const int empty_space = datapoints_ - values_count;
std::vector<std::pair<double, double>> points;
points.reserve(values_count);
for (int i = empty_space; i < datapoints_; ++i) {
double x = i * step_width;
int value_index = i - empty_space;
int value = values_[value_index];
double y = height - (static_cast<double>(value) / 100.0 * height);
points.emplace_back(x, y);
}
if (!points.empty()) {
switch (graph_type_) {
case GraphType::LINE:
drawFilledArea(cr, points, height, bg_color);
drawLine(cr, points, fg_color);
break;
case GraphType::BAR:
drawBars(cr, width, height, values_.empty() ? 0 : values_.back(), fg_color);
break;
case GraphType::GAUGE:
drawGauge(cr, width, height, values_.empty() ? 0 : values_.back(), fg_color);
break;
}
}
return false;
}
void AGraph::drawFilledArea(const Cairo::RefPtr<Cairo::Context>& cr,
const std::vector<std::pair<double, double>>& points, double height,
const Gdk::RGBA& bg_color) {
if (points.empty()) return;
double first_x = points.front().first;
double last_x = points.back().first;
drawPath(cr, points);
cr->line_to(last_x, height);
cr->line_to(first_x, height);
cr->close_path();
cr->set_source_rgba(bg_color.get_red(), bg_color.get_green(), bg_color.get_blue(),
bg_color.get_alpha());
cr->fill();
}
void AGraph::drawLine(const Cairo::RefPtr<Cairo::Context>& cr,
const std::vector<std::pair<double, double>>& points,
const Gdk::RGBA& fg_color) {
if (points.empty()) return;
cr->begin_new_path();
drawPath(cr, points);
cr->set_source_rgba(fg_color.get_red(), fg_color.get_green(), fg_color.get_blue(),
fg_color.get_alpha());
cr->stroke();
}
void AGraph::drawPath(const Cairo::RefPtr<Cairo::Context>& cr,
const std::vector<std::pair<double, double>>& points) {
if (points.empty()) return;
bool first_point = true;
for (const auto& point : points) {
if (first_point) {
cr->move_to(point.first, point.second);
first_point = false;
} else {
cr->line_to(point.first, point.second);
}
}
}
void AGraph::drawBars(const Cairo::RefPtr<Cairo::Context>& cr, double width, double height,
int current_value, const Gdk::RGBA& fg_color) {
current_value = std::min(100, std::max(0, current_value));
double green_height = height * (std::min(current_value, 40) / 100.0);
cr->set_source_rgba(fg_color.get_red(), fg_color.get_green(), fg_color.get_blue(), 0.5);
cr->rectangle(0, height - green_height, width, green_height);
cr->fill();
if (current_value > 40) {
double yellow_height = height * (std::min(current_value, 75) - 40) / 100.0;
cr->set_source_rgba(fg_color.get_red(), fg_color.get_green(), fg_color.get_blue(), 0.7);
cr->rectangle(0, height - green_height - yellow_height, width, yellow_height);
cr->fill();
}
if (current_value > 75) {
double orange_height = height * (std::min(current_value, 85) - 75) / 100.0;
cr->set_source_rgba(fg_color.get_red(), fg_color.get_green(), fg_color.get_blue(), 0.85);
double yellow_height = height * (std::min(current_value, 75) - 40) / 100.0;
cr->rectangle(0, height - green_height - yellow_height - orange_height, width, orange_height);
cr->fill();
}
if (current_value > 85) {
double red_height = height * (current_value - 85) / 100.0;
cr->set_source_rgba(fg_color.get_red(), fg_color.get_green(), fg_color.get_blue(), 1.0);
double yellow_height = height * (std::min(current_value, 75) - 40) / 100.0;
double orange_height = height * (std::min(current_value, 85) - 75) / 100.0;
cr->rectangle(0, height - green_height - yellow_height - orange_height - red_height, width,
red_height);
cr->fill();
}
double value_height = height * (current_value / 100.0);
cr->set_source_rgba(0.2, 0.2, 0.2, 0.8);
cr->rectangle(0, height - value_height, width, 2);
cr->fill();
}
void AGraph::drawGauge(const Cairo::RefPtr<Cairo::Context>& cr, double width, double height,
int current_value, const Gdk::RGBA& fg_color) {
double center_x = width / 2.0;
double center_y = height;
double radius = height / 2.0;
cr->set_line_width(10.0);
double angle1 = M_PI;
double angle2 = angle1 + 0.3 * angle1;
// Green section (0-33%)
cr->set_source_rgba(fg_color.get_red(), fg_color.get_green(), fg_color.get_blue(), 0.5);
cr->arc(center_x, center_y, radius, angle1, angle2);
cr->stroke();
// Yellow section (33-66%)
angle1 = angle2;
angle2 = angle1 + 0.3 * angle1;
cr->set_source_rgba(fg_color.get_red(), fg_color.get_green(), fg_color.get_blue(), 0.75);
cr->arc(center_x, center_y, radius, angle1, angle2);
cr->stroke();
// Red section (66-100%)
angle1 = angle2;
angle2 = 0.0;
cr->set_source_rgba(fg_color.get_red(), fg_color.get_green(), fg_color.get_blue(), 1.0);
cr->arc(center_x, center_y, radius, angle1, angle2);
cr->stroke();
// Draw needle
double percentage = std::min(100, std::max(0, current_value)) / 100.0;
double needle_angle = M_PI * percentage;
double needle_length = radius;
double needle_x = center_x - needle_length * cos(needle_angle);
double needle_y = center_y - needle_length * sin(needle_angle);
cr->set_source_rgba(1.0, 1.0, 1.0, 1.0);
cr->set_line_width(2.0);
cr->begin_new_path();
cr->move_to(center_x, center_y);
cr->line_to(needle_x, needle_y);
cr->stroke();
}
} // namespace waybar
+79 -6
View File
@@ -1,13 +1,20 @@
#include "AIconLabel.hpp"
#include <gdkmm/pixbuf.h>
#include <spdlog/spdlog.h>
#include <regex>
#include <string>
namespace waybar {
AIconLabel::AIconLabel(const Json::Value &config, const std::string &name, const std::string &id,
const std::string &format, uint16_t interval, bool ellipsize,
AIconLabel::AIconLabel(const Json::Value& config, const std::string& name, const std::string& id,
const std::string& format, uint16_t interval, bool ellipsize,
bool enable_click, bool enable_scroll)
: ALabel(config, name, id, format, interval, ellipsize, enable_click, enable_scroll) {
if (config["icon-size"].isUInt()) {
app_icon_size_ = config["icon-size"].asUInt();
}
image_.set_pixel_size(app_icon_size_);
event_box_.remove();
label_.unset_name();
label_.get_style_context()->remove_class(MODULE_CLASS);
@@ -17,25 +24,91 @@ AIconLabel::AIconLabel(const Json::Value &config, const std::string &name, const
box_.get_style_context()->add_class(id);
}
box_.set_orientation(Gtk::Orientation::ORIENTATION_HORIZONTAL);
int rot = 0;
if (config_["rotate"].isUInt()) {
rot = config["rotate"].asUInt() % 360;
if ((rot % 90) != 00) rot = 0;
rot /= 90;
}
if ((rot % 2) == 0)
box_.set_orientation(Gtk::Orientation::ORIENTATION_HORIZONTAL);
else
box_.set_orientation(Gtk::Orientation::ORIENTATION_VERTICAL);
box_.set_name(name);
int spacing = config_["icon-spacing"].isInt() ? config_["icon-spacing"].asInt() : 8;
box_.set_spacing(spacing);
box_.add(image_);
box_.add(label_);
bool swap_icon_label = false;
if (config_["swap-icon-label"].isNull()) {
} else if (config_["swap-icon-label"].isBool()) {
swap_icon_label = config_["swap-icon-label"].asBool();
} else {
spdlog::warn("'swap-icon-label' must be a bool, found '{}'. Using default value (false).",
config_["swap-icon-label"].asString());
}
if ((rot == 0 || rot == 3) ^ swap_icon_label) {
box_.add(image_);
box_.add(label_);
} else {
box_.add(label_);
box_.add(image_);
}
event_box_.add(box_);
}
std::tuple<std::string, std::string> AIconLabel::extractIcon(const std::string& input) {
std::string icon_result = "";
std::string label_result = input;
try {
static const std::regex icon_search(R"((?=\\0icon\\1f).+?(?=\\n))");
std::smatch icon_match;
if (std::regex_search(input, icon_match, icon_search)) {
icon_result = icon_match[0].str().substr(9);
static const std::regex clean_label_pattern(R"(\\0icon\\1f.+?\\n)");
label_result = std::regex_replace(input, clean_label_pattern, "");
}
} catch (const std::exception& e) {
spdlog::warn("Error while parsing icon from label. {}", e.what());
}
return std::make_tuple(icon_result, label_result);
}
auto AIconLabel::update() -> void {
label_contains_icon = false;
auto [iconLabel, cleanLabel] = extractIcon(label_.get_label().c_str());
label_contains_icon = iconLabel.length() > 0;
if (label_contains_icon) {
label_.set_markup(cleanLabel);
if (iconLabel.front() == '/') {
int scaled_icon_size = app_icon_size_ * image_.get_scale_factor();
auto pixbuf = Gdk::Pixbuf::create_from_file(iconLabel, scaled_icon_size, scaled_icon_size);
auto surface = Gdk::Cairo::create_surface_from_pixbuf(pixbuf, image_.get_scale_factor(),
image_.get_window());
image_.set(surface);
image_.set_visible(true);
} else {
image_.set_from_icon_name(iconLabel, Gtk::ICON_SIZE_INVALID);
image_.set_visible(true);
}
}
image_.set_visible(image_.get_visible() && iconEnabled());
ALabel::update();
}
bool AIconLabel::iconEnabled() const {
return config_["icon"].isBool() ? config_["icon"].asBool() : false;
return label_contains_icon || (config_["icon"].isBool() ? config_["icon"].asBool() : false);
}
} // namespace waybar
+155 -9
View File
@@ -2,19 +2,32 @@
#include <fmt/format.h>
#include <fstream>
#include <iostream>
#include <util/command.hpp>
#include "config.hpp"
namespace waybar {
ALabel::ALabel(const Json::Value& config, const std::string& name, const std::string& id,
const std::string& format, uint16_t interval, bool ellipsize, bool enable_click,
bool enable_scroll)
: AModule(config, name, id, config["format-alt"].isString() || enable_click, enable_scroll),
: AModule(config, name, id,
config["format-alt"].isString() || config["menu"].isString() || enable_click,
enable_scroll),
format_(config_["format"].isString() ? config_["format"].asString() : format),
// Leave the default option outside of the std::max(1L, ...), because the zero value
// (default) is used in modules/custom.cpp to make the difference between
// two types of custom scripts. Fixes #4521.
interval_(config_["interval"] == "once"
? std::chrono::seconds::max()
: std::chrono::seconds(
config_["interval"].isUInt() ? config_["interval"].asUInt() : interval)),
? std::chrono::milliseconds::max()
: std::chrono::milliseconds(
(config_["interval"].isNumeric()
? std::max(1L, // Minimum 1ms due to millisecond precision
static_cast<long>(config_["interval"].asDouble() * 1000))
: 1000 * (long)interval))),
default_format_(format_) {
label_.set_name(name);
if (!id.empty()) {
@@ -39,6 +52,8 @@ ALabel::ALabel(const Json::Value& config, const std::string& name, const std::st
if (config_["rotate"].isUInt()) {
rotate = config["rotate"].asUInt();
if (not(rotate == 0 || rotate == 90 || rotate == 180 || rotate == 270))
spdlog::warn("'rotate' is only supported in 90 degree increments {} is not valid.", rotate);
label_.set_angle(rotate);
}
@@ -51,6 +66,65 @@ ALabel::ALabel(const Json::Value& config, const std::string& name, const std::st
}
}
// If a GTKMenu is requested in the config
if (config_["menu"].isString()) {
// Create the GTKMenu widget
try {
// Check that the file exists
std::string menuFile = config_["menu-file"].asString();
// there might be "~" or "$HOME" in original path, try to expand it.
auto result = Config::tryExpandPath(menuFile, "");
if (result.empty()) {
throw std::runtime_error("Failed to expand file: " + menuFile);
}
menuFile = result.front();
// Read the menu descriptor file
std::ifstream file(menuFile);
if (!file.is_open()) {
throw std::runtime_error("Failed to open file: " + menuFile);
}
std::stringstream fileContent;
fileContent << file.rdbuf();
GtkBuilder* builder = gtk_builder_new();
// Make the GtkBuilder and check for errors in his parsing
if (gtk_builder_add_from_string(builder, fileContent.str().c_str(), -1, nullptr) == 0U) {
g_object_unref(builder);
throw std::runtime_error("Error found in the file " + menuFile);
}
menu_ = gtk_builder_get_object(builder, "menu");
if (menu_ == nullptr) {
g_object_unref(builder);
throw std::runtime_error("Failed to get 'menu' object from GtkBuilder");
}
// Keep the menu alive after dropping the transient GtkBuilder.
g_object_ref(menu_);
submenus_ = std::map<std::string, GtkMenuItem*>();
menuActionsMap_ = std::map<std::string, std::string>();
// Linking actions to the GTKMenu based on
for (Json::Value::const_iterator it = config_["menu-actions"].begin();
it != config_["menu-actions"].end(); ++it) {
std::string key = it.key().asString();
auto* item = gtk_builder_get_object(builder, key.c_str());
if (item == nullptr) {
spdlog::warn("Menu item '{}' not found in builder file", key);
continue;
}
submenus_[key] = GTK_MENU_ITEM(item);
menuActionsMap_[key] = it->asString();
g_signal_connect(submenus_[key], "activate", G_CALLBACK(handleGtkMenuEvent),
(gpointer)g_strdup(menuActionsMap_[key].c_str()));
}
g_object_unref(builder);
} catch (std::runtime_error& e) {
spdlog::warn("Error while creating the menu : {}. Menu popup not activated.", e.what());
}
}
if (config_["justify"].isString()) {
auto justify_str = config_["justify"].asString();
if (justify_str == "left") {
@@ -65,6 +139,26 @@ ALabel::ALabel(const Json::Value& config, const std::string& name, const std::st
auto ALabel::update() -> void { AModule::update(); }
bool ALabel::setLabelMarkup(const Glib::ustring& markup) {
if (last_label_markup_ == markup) {
return false;
}
label_.set_markup(markup);
last_label_markup_ = markup;
return true;
}
bool ALabel::setTooltipMarkup(const Glib::ustring& markup) {
if (last_tooltip_markup_ == markup) {
return false;
}
label_.set_tooltip_markup(markup);
last_tooltip_markup_ = markup;
return true;
}
std::string ALabel::getIcon(uint16_t percentage, const std::string& alt, uint16_t max) {
auto format_icons = config_["format-icons"];
if (format_icons.isObject()) {
@@ -76,8 +170,28 @@ std::string ALabel::getIcon(uint16_t percentage, const std::string& alt, uint16_
}
if (format_icons.isArray()) {
auto size = format_icons.size();
if (size) {
auto idx = std::clamp(percentage / ((max == 0 ? 100 : max) / size), 0U, size - 1);
if (size != 0U && format_icons[0].isObject()) {
std::string last_icon;
for (const auto& threshold : format_icons) {
if (!threshold.isObject() || !threshold["icon"].isString() || !threshold["max"].isUInt()) {
static bool warned = false;
if (!warned) {
spdlog::warn("format-icons: skipping invalid threshold object, expected {\"icon\": \"...\", \"max\": N}");
warned = true;
}
continue;
}
last_icon = threshold["icon"].asString();
if (percentage <= threshold["max"].asUInt()) {
return last_icon;
}
}
if (!last_icon.empty()) {
return last_icon;
}
} else if (size != 0U) {
auto divisor = std::max(1U, (max == 0 ? 100U : static_cast<unsigned>(max)) / size);
auto idx = std::clamp(percentage / divisor, 0U, size - 1);
format_icons = format_icons[idx];
}
}
@@ -102,8 +216,28 @@ std::string ALabel::getIcon(uint16_t percentage, const std::vector<std::string>&
}
if (format_icons.isArray()) {
auto size = format_icons.size();
if (size) {
auto idx = std::clamp(percentage / ((max == 0 ? 100 : max) / size), 0U, size - 1);
if (size != 0U && format_icons[0].isObject()) {
std::string last_icon;
for (const auto& threshold : format_icons) {
if (!threshold.isObject() || !threshold["icon"].isString() || !threshold["max"].isUInt()) {
static bool warned = false;
if (!warned) {
spdlog::warn("format-icons: skipping invalid threshold object, expected {\"icon\": \"...\", \"max\": N}");
warned = true;
}
continue;
}
last_icon = threshold["icon"].asString();
if (percentage <= threshold["max"].asUInt()) {
return last_icon;
}
}
if (!last_icon.empty()) {
return last_icon;
}
} else if (size != 0U) {
auto divisor = std::max(1U, (max == 0 ? 100U : static_cast<unsigned>(max)) / size);
auto idx = std::clamp(percentage / divisor, 0U, size - 1);
format_icons = format_icons[idx];
}
}
@@ -113,6 +247,10 @@ std::string ALabel::getIcon(uint16_t percentage, const std::vector<std::string>&
return "";
}
void ALabel::copyToClipboard(const std::string& literal) {
Gtk::Clipboard::get()->set_text(literal);
}
bool waybar::ALabel::handleToggle(GdkEventButton* const& e) {
if (config_["format-alt-click"].isUInt() && e->button == config_["format-alt-click"].asUInt()) {
alt_ = !alt_;
@@ -122,9 +260,17 @@ bool waybar::ALabel::handleToggle(GdkEventButton* const& e) {
format_ = default_format_;
}
}
if (config_["on-click-copy"].isBool() && config_["on-click-copy"].asBool()) {
copyToClipboard(label_.get_text());
}
return AModule::handleToggle(e);
}
void ALabel::handleGtkMenuEvent(GtkMenuItem* /*menuitem*/, gpointer data) {
waybar::util::command::forkExec((char*)data, "GtkMenu");
}
std::string ALabel::getState(uint8_t value, bool lesser) {
if (!config_["states"].isObject()) {
return "";
@@ -139,7 +285,7 @@ std::string ALabel::getState(uint8_t value, bool lesser) {
}
}
// Sort states
std::sort(states.begin(), states.end(), [&lesser](auto& a, auto& b) {
std::ranges::sort(states.begin(), states.end(), [&lesser](auto& a, auto& b) {
return lesser ? a.second < b.second : a.second > b.second;
});
std::string valid_state;
+114 -17
View File
@@ -1,44 +1,60 @@
#include "AModule.hpp"
#include <fmt/format.h>
#include <spdlog/spdlog.h>
#include <util/command.hpp>
#include "gdk/gdk.h"
#include "gdkmm/cursor.h"
namespace waybar {
AModule::AModule(const Json::Value& config, const std::string& name, const std::string& id,
bool enable_click, bool enable_scroll)
: name_(std::move(name)),
config_(std::move(config)),
: name_(name),
config_(config),
isTooltip{config_["tooltip"].isBool() ? config_["tooltip"].asBool() : true},
isExpand{config_["expand"].isBool() ? config_["expand"].asBool() : false},
distance_scrolled_y_(0.0),
distance_scrolled_x_(0.0) {
distance_scrolled_x_(0.0),
cursor_timeout_conn_() {
// Configure module action Map
const Json::Value actions{config_["actions"]};
disable_on_sleep_ =
config_["disable-on-sleep"].isBool() ? config_["disable-on-sleep"].asBool() : false;
for (Json::Value::const_iterator it = actions.begin(); it != actions.end(); ++it) {
if (it.key().isString() && it->isString())
if (eventActionMap_.count(it.key().asString()) == 0) {
if (!eventActionMap_.contains(it.key().asString())) {
eventActionMap_.insert({it.key().asString(), it->asString()});
enable_click = true;
enable_scroll = true;
} else
spdlog::warn("Dublicate action is ignored: {0}", it.key().asString());
spdlog::warn("Duplicate action is ignored: {0}", it.key().asString());
else
spdlog::warn("Wrong actions section configuration. See config by index: {}", it.index());
}
event_box_.signal_enter_notify_event().connect(sigc::mem_fun(*this, &AModule::handleMouseEnter));
event_box_.signal_leave_notify_event().connect(sigc::mem_fun(*this, &AModule::handleMouseLeave));
// configure events' user commands
// hasUserEvent is true if any element from eventMap_ is satisfying the condition in the lambda
bool hasUserEvent =
// hasUserEvents is true if any element from eventMap_ is satisfying the condition in the lambda
bool hasUserEvents =
std::find_if(eventMap_.cbegin(), eventMap_.cend(), [&config](const auto& eventEntry) {
// True if there is any non-release type event
return eventEntry.first.second != GdkEventType::GDK_BUTTON_RELEASE &&
config[eventEntry.second].isString();
(config[eventEntry.second].isString() || config[eventEntry.second].isBool());
}) != eventMap_.cend();
if (enable_click || hasUserEvent) {
if (enable_click || hasUserEvents) {
hasUserEvents_ = true;
event_box_.add_events(Gdk::BUTTON_PRESS_MASK);
event_box_.signal_button_press_event().connect(sigc::mem_fun(*this, &AModule::handleToggle));
} else {
hasUserEvents_ = false;
}
bool hasReleaseEvent =
@@ -57,32 +73,97 @@ AModule::AModule(const Json::Value& config, const std::string& name, const std::
event_box_.add_events(Gdk::SCROLL_MASK | Gdk::SMOOTH_SCROLL_MASK);
event_box_.signal_scroll_event().connect(sigc::mem_fun(*this, &AModule::handleScroll));
}
// Respect user configuration of cursor
if (config_.isMember("cursor")) {
if (config_["cursor"].isBool()) {
if (config_["cursor"].asBool()) {
setCursor("pointer");
} else {
setCursor("default");
}
} else if (config_["cursor"].isString()) {
setCursor(config_["cursor"].asString());
} else {
spdlog::warn("unknown cursor option configured on module {}", name_);
}
}
}
AModule::~AModule() {
for (const auto& pid : pid_) {
if (cursor_timeout_conn_.connected()) {
cursor_timeout_conn_.disconnect();
}
for (const auto& pid : pid_children_) {
if (pid != -1) {
killpg(pid, SIGTERM);
}
}
if (menu_ != nullptr) {
g_object_unref(menu_);
menu_ = nullptr;
}
}
auto AModule::update() -> void {
// Run user-provided update handler if configured
if (config_["on-update"].isString()) {
pid_.push_back(util::command::forkExec(config_["on-update"].asString()));
pid_children_.push_back(util::command::forkExec(config_["on-update"].asString()));
}
}
// Get mapping between event name and module action name
// Then call overrided doAction in order to call appropriate module action
// Then call overridden doAction in order to call appropriate module action
auto AModule::doAction(const std::string& name) -> void {
if (!name.empty()) {
const std::map<std::string, std::string>::const_iterator& recA{eventActionMap_.find(name)};
// Call overrided action if derrived class has implemented it
// Call overridden action if derived class has implemented it
if (recA != eventActionMap_.cend() && name != recA->second) this->doAction(recA->second);
}
}
void AModule::setCursor(std::string const& c) {
auto gdk_window = event_box_.get_window();
if (gdk_window) {
auto cursor = Gdk::Cursor::create(gdk_window->get_display(), c);
gdk_window->set_cursor(cursor);
} else {
// window may not be accessible yet, in this case,
// schedule another call for setting the cursor in 1 sec
cursor_timeout_conn_ = Glib::signal_timeout().connect_seconds(
[this, c]() {
setCursor(c);
return false;
},
1);
}
}
bool AModule::handleMouseEnter(GdkEventCrossing* const& e) {
if (auto* module = event_box_.get_child(); module != nullptr) {
module->set_state_flags(Gtk::StateFlags::STATE_FLAG_PRELIGHT);
}
// Default behavior indicating event availability
if (hasUserEvents_ && !config_.isMember("cursor")) {
setCursor("pointer");
}
return false;
}
bool AModule::handleMouseLeave(GdkEventCrossing* const& e) {
if (auto* module = event_box_.get_child(); module != nullptr) {
module->unset_state_flags(Gtk::StateFlags::STATE_FLAG_PRELIGHT);
}
// Default behavior indicating event availability
if (hasUserEvents_ && !config_.isMember("cursor")) {
setCursor("default");
}
return false;
}
bool AModule::handleToggle(GdkEventButton* const& e) { return handleUserEvent(e); }
bool AModule::handleRelease(GdkEventButton* const& e) { return handleUserEvent(e); }
@@ -91,12 +172,27 @@ bool AModule::handleUserEvent(GdkEventButton* const& e) {
std::string format{};
const std::map<std::pair<uint, GdkEventType>, std::string>::const_iterator& rec{
eventMap_.find(std::pair(e->button, e->type))};
if (rec != eventMap_.cend()) {
// First call module actions
this->AModule::doAction(rec->second);
format = rec->second;
}
// Check that a menu has been configured
if (rec != eventMap_.cend() && config_["menu"].isString()) {
// Check if the event is the one specified for the "menu" option
if (rec->second == config_["menu"].asString() && menu_ != nullptr) {
// Popup the menu
gtk_widget_show_all(GTK_WIDGET(menu_));
gtk_menu_popup_at_pointer(GTK_MENU(menu_), reinterpret_cast<GdkEvent*>(e));
// Manually reset prelight to make sure the module doesn't stay in a hover state
if (auto* module = event_box_.get_child(); module != nullptr) {
module->unset_state_flags(Gtk::StateFlags::STATE_FLAG_PRELIGHT);
}
}
}
// Second call user scripts
if (!format.empty()) {
if (config_[format].isString())
@@ -105,7 +201,7 @@ bool AModule::handleUserEvent(GdkEventButton* const& e) {
format.clear();
}
if (!format.empty()) {
pid_.push_back(util::command::forkExec(format));
pid_children_.push_back(util::command::forkExec(format));
}
dp.emit();
return true;
@@ -118,7 +214,7 @@ AModule::SCROLL_DIR AModule::getScrollDir(GdkEventScroll* e) {
// ignore reverse-scrolling if event comes from a mouse wheel
GdkDevice* device = gdk_event_get_source_device((GdkEvent*)e);
if (device != NULL && gdk_device_get_source(device) == GDK_SOURCE_MOUSE) {
if (device != nullptr && gdk_device_get_source(device) == GDK_SOURCE_MOUSE) {
reverse = reverse_mouse;
}
@@ -190,13 +286,14 @@ bool AModule::handleScroll(GdkEventScroll* e) {
this->AModule::doAction(eventName);
// Second call user scripts
if (config_[eventName].isString())
pid_.push_back(util::command::forkExec(config_[eventName].asString()));
pid_children_.push_back(util::command::forkExec(config_[eventName].asString()));
dp.emit();
return true;
}
bool AModule::tooltipEnabled() { return isTooltip; }
bool AModule::tooltipEnabled() const { return isTooltip; }
bool AModule::expandEnabled() const { return isExpand; }
AModule::operator Gtk::Widget&() { return event_box_; }
+184 -29
View File
@@ -3,11 +3,15 @@
#include <gtk-layer-shell.h>
#include <spdlog/spdlog.h>
#include <ostream>
#include <type_traits>
#include "client.hpp"
#include "factory.hpp"
#include "group.hpp"
#include "util/enum.hpp"
#include "util/hosts_check.hpp"
#include "util/kill_signal.hpp"
#ifdef HAVE_SWAY
#include "modules/sway/bar.hpp"
@@ -37,7 +41,7 @@ const Bar::bar_mode_map Bar::PRESET_MODES = { //
.visible = true}},
{"hide",
{//
.layer = bar_layer::TOP,
.layer = bar_layer::OVERLAY,
.exclusive = false,
.passthrough = false,
.visible = true}},
@@ -49,7 +53,7 @@ const Bar::bar_mode_map Bar::PRESET_MODES = { //
.visible = false}},
{"overlay",
{//
.layer = bar_layer::TOP,
.layer = bar_layer::OVERLAY,
.exclusive = false,
.passthrough = true,
.visible = true}}};
@@ -72,16 +76,16 @@ void from_json(const Json::Value& j, bar_layer& l) {
/* Deserializer for struct bar_mode */
void from_json(const Json::Value& j, bar_mode& m) {
if (j.isObject()) {
if (auto v = j["layer"]; v.isString()) {
if (const auto& v = j["layer"]; v.isString()) {
from_json(v, m.layer);
}
if (auto v = j["exclusive"]; v.isBool()) {
if (const auto& v = j["exclusive"]; v.isBool()) {
m.exclusive = v.asBool();
}
if (auto v = j["passthrough"]; v.isBool()) {
if (const auto& v = j["passthrough"]; v.isBool()) {
m.passthrough = v.asBool();
}
if (auto v = j["visible"]; v.isBool()) {
if (const auto& v = j["visible"]; v.isBool()) {
m.visible = v.asBool();
}
}
@@ -118,7 +122,7 @@ Glib::ustring to_string(Gtk::PositionType pos) {
* Assumes that all the values in the object are deserializable to the same type.
*/
template <typename Key, typename Value,
typename = std::enable_if_t<std::is_convertible<std::string, Key>::value>>
typename = std::enable_if_t<std::is_convertible_v<std::string, Key>>>
void from_json(const Json::Value& j, std::map<Key, Value>& m) {
if (j.isObject()) {
for (auto it = j.begin(); it != j.end(); ++it) {
@@ -132,6 +136,7 @@ void from_json(const Json::Value& j, std::map<Key, Value>& m) {
waybar::Bar::Bar(struct waybar_output* w_output, const Json::Value& w_config)
: output(w_output),
config(w_config),
surface(nullptr),
window{Gtk::WindowType::WINDOW_TOPLEVEL},
x_global(0),
y_global(0),
@@ -169,6 +174,10 @@ waybar::Bar::Bar(struct waybar_output* w_output, const Json::Value& w_config)
right_.set_spacing(spacing);
}
if (config.isMember("height") && !config["height"].isUInt()) {
spdlog::warn("Invalid type for 'height', expected unsigned integer");
}
height_ = config["height"].isUInt() ? config["height"].asUInt() : 0;
width_ = config["width"].isUInt() ? config["width"].asUInt() : 0;
@@ -226,7 +235,8 @@ waybar::Bar::Bar(struct waybar_output* w_output, const Json::Value& w_config)
gtk_layer_init_for_window(gtk_window);
gtk_layer_set_keyboard_mode(gtk_window, GTK_LAYER_SHELL_KEYBOARD_MODE_NONE);
gtk_layer_set_monitor(gtk_window, output->monitor->gobj());
gtk_layer_set_namespace(gtk_window, "waybar");
gtk_layer_set_namespace(gtk_window,
config["name"].isString() ? config["name"].asCString() : "waybar");
gtk_layer_set_margin(gtk_window, GTK_LAYER_SHELL_EDGE_LEFT, margins_.left);
gtk_layer_set_margin(gtk_window, GTK_LAYER_SHELL_EDGE_RIGHT, margins_.right);
@@ -259,6 +269,16 @@ waybar::Bar::Bar(struct waybar_output* w_output, const Json::Value& w_config)
window.signal_map_event().connect_notify(sigc::mem_fun(*this, &Bar::onMap));
window.signal_unmap().connect([this]() {
spdlog::debug("Output {} unmapped (DPMS off), suspending modules", output->name);
toggleSuspend(true);
});
window.signal_map().connect([this]() {
spdlog::debug("Output {} mapped (DPMS on), resuming modules", output->name);
toggleSuspend(false);
});
#if HAVE_SWAY
if (auto ipc = config["ipc"]; ipc.isBool() && ipc.asBool()) {
bar_id = Client::inst()->bar_id;
@@ -276,9 +296,48 @@ waybar::Bar::Bar(struct waybar_output* w_output, const Json::Value& w_config)
}
#endif
waybar::util::EnumParser<util::KillSignalAction> m_signalActionEnumParser;
const auto& configSigusr1 = config["on-sigusr1"];
if (configSigusr1.isString()) {
auto strSigusr1 = configSigusr1.asString();
try {
onSigusr1 =
m_signalActionEnumParser.parseStringToEnum(strSigusr1, util::userKillSignalActions);
} catch (const std::invalid_argument& e) {
onSigusr1 = util::SIGNALACTION_DEFAULT_SIGUSR1;
spdlog::warn(
"Invalid string representation for on-sigusr1. Falling back to default mode (toggle).");
}
}
const auto& configSigusr2 = config["on-sigusr2"];
if (configSigusr2.isString()) {
auto strSigusr2 = configSigusr2.asString();
try {
onSigusr2 =
m_signalActionEnumParser.parseStringToEnum(strSigusr2, util::userKillSignalActions);
} catch (const std::invalid_argument& e) {
onSigusr2 = util::SIGNALACTION_DEFAULT_SIGUSR2;
spdlog::warn(
"Invalid string representation for on-sigusr2. Falling back to default mode (reload).");
}
}
setupWidgets();
window.show_all();
/*
* If gtk-layer-shell's synchronous wait for the initial configure timed out, show_all() can
* return with a configured but not-yet-presented surface. Kick GTK/layer-shell once control has
* returned to the main loop, when any late initial configure has been dispatched and widgets have
* had a chance to allocate/draw.
*/
Glib::signal_idle().connect(sigc::track_obj([this] {
window.queue_resize();
window.queue_draw();
forceLayerCommit();
return false;
}, *this));
if (spdlog::should_log(spdlog::level::debug)) {
// Unfortunately, this function isn't in the C++ bindings, so we have to call the C version.
char* gtk_tree = gtk_style_context_to_string(
@@ -339,6 +398,20 @@ void waybar::Bar::setMode(const struct bar_mode& mode) {
window.get_style_context()->add_class("hidden");
window.set_opacity(0);
}
/*
* All the changes above require `wl_surface_commit`.
* gtk-layer-shell schedules a commit on the next frame event in GTK, but this could fail in
* certain scenarios, such as fully occluded bar.
*/
forceLayerCommit();
}
void waybar::Bar::forceLayerCommit() {
auto* gtk_window = window.gobj();
if (gtk_window != nullptr && gtk_widget_get_realized(GTK_WIDGET(gtk_window))) {
gtk_layer_try_force_commit(gtk_window);
}
wl_display_flush(Client::inst()->wl_display);
}
void waybar::Bar::setPassThrough(bool passthrough) {
@@ -393,11 +466,22 @@ void waybar::Bar::setPosition(Gtk::PositionType position) {
}
}
void waybar::Bar::onMap(GdkEventAny*) {
void waybar::Bar::onMap(GdkEventAny* /*unused*/) {
/*
* Obtain a pointer to the custom layer surface for modules that require it (idle_inhibitor).
*/
auto gdk_window = window.get_window()->gobj();
auto gdk_window_ref = window.get_window();
if (!gdk_window_ref) {
spdlog::warn("Failed to get GDK window during onMap, deferring surface initialization");
return;
}
auto* gdk_window = gdk_window_ref->gobj();
if (!gdk_window) {
spdlog::warn("GDK window object is null during onMap, deferring surface initialization");
return;
}
surface = gdk_wayland_window_get_wl_surface(gdk_window);
configureGlobalOffset(gdk_window_get_width(gdk_window), gdk_window_get_height(gdk_window));
@@ -414,6 +498,8 @@ void waybar::Bar::setVisible(bool value) {
}
void waybar::Bar::toggle() { setVisible(!visible); }
void waybar::Bar::show() { setVisible(true); }
void waybar::Bar::hide() { setVisible(false); }
// Converting string to button code rn as to avoid doing it later
void waybar::Bar::setupAltFormatKeyForModule(const std::string& module_name) {
@@ -449,7 +535,17 @@ void waybar::Bar::setupAltFormatKeyForModuleList(const char* module_list_name) {
Json::Value& modules = config[module_list_name];
for (const Json::Value& module_name : modules) {
if (module_name.isString()) {
setupAltFormatKeyForModule(module_name.asString());
auto ref = module_name.asString();
if (ref.compare(0, 6, "group/") == 0 && ref.size() > 6) {
Json::Value& group_modules = config[ref]["modules"];
for (const Json::Value& module_name : group_modules) {
if (module_name.isString()) {
setupAltFormatKeyForModule(module_name.asString());
}
}
} else {
setupAltFormatKeyForModule(ref);
}
}
}
}
@@ -461,13 +557,21 @@ void waybar::Bar::handleSignal(int signal) {
}
}
waybar::util::KillSignalAction waybar::Bar::getOnSigusr1Action() { return this->onSigusr1; }
waybar::util::KillSignalAction waybar::Bar::getOnSigusr2Action() { return this->onSigusr2; }
void waybar::Bar::getModules(const Factory& factory, const std::string& pos,
waybar::Group* group = nullptr) {
auto module_list = group ? config[pos]["modules"] : config[pos];
auto module_list = group != nullptr ? config[pos]["modules"] : config[pos];
if (module_list.isArray()) {
for (const auto& name : module_list) {
try {
auto ref = name.asString();
if (config[ref].isMember("hosts") && !waybar::util::valid_host(config[ref])) {
continue;
}
AModule* module;
if (ref.compare(0, 6, "group/") == 0 && ref.size() > 6) {
@@ -475,19 +579,25 @@ void waybar::Bar::getModules(const Factory& factory, const std::string& pos,
auto id_name = ref.substr(6, hash_pos - 6);
auto class_name = hash_pos != std::string::npos ? ref.substr(hash_pos + 1) : "";
auto vertical = (group ? group->getBox().get_orientation() : box_.get_orientation()) ==
Gtk::ORIENTATION_VERTICAL;
auto vertical = (group != nullptr ? group->getBox().get_orientation()
: box_.get_orientation()) == Gtk::ORIENTATION_VERTICAL;
auto group_module = new waybar::Group(id_name, class_name, config[ref], vertical);
getModules(factory, ref, group_module);
module = group_module;
const Json::Value& group_config = config[ref];
if (group_config["modules"].isNull()) {
spdlog::warn("Group definition '{}' has not been found, group will be hidden", ref);
}
auto group_module =
std::make_unique<waybar::Group>(id_name, class_name, group_config, vertical);
getModules(factory, ref, group_module.get());
module = group_module.release();
} else {
module = factory.makeModule(ref, pos);
}
std::shared_ptr<AModule> module_sp(module);
modules_all_.emplace_back(module_sp);
if (group) {
if (group != nullptr) {
group->addWidget(*module);
} else {
if (pos == "modules-left") {
@@ -516,13 +626,21 @@ void waybar::Bar::getModules(const Factory& factory, const std::string& pos,
auto waybar::Bar::setupWidgets() -> void {
window.add(box_);
box_.pack_start(left_, false, false);
if (config["fixed-center"].isBool() ? config["fixed-center"].asBool() : true) {
box_.set_center_widget(center_);
} else {
box_.pack_start(center_, true, false);
bool expand_left = config["expand-left"].isBool() ? config["expand-left"].asBool() : false;
bool expand_center = config["expand-center"].isBool() ? config["expand-center"].asBool() : false;
bool expand_right = config["expand-right"].isBool() ? config["expand-right"].asBool() : false;
bool no_center = config["no-center"].isBool() ? config["no-center"].asBool() : false;
box_.pack_start(left_, expand_left, expand_left);
if (!no_center) {
if (config["fixed-center"].isBool() ? config["fixed-center"].asBool() : true) {
box_.set_center_widget(center_);
} else {
box_.pack_start(center_, true, expand_center);
}
}
box_.pack_end(right_, false, false);
box_.pack_end(right_, expand_right, expand_right);
// Convert to button code for every module that is used.
setupAltFormatKeyForModuleList("modules-left");
@@ -531,17 +649,24 @@ auto waybar::Bar::setupWidgets() -> void {
Factory factory(*this, config);
getModules(factory, "modules-left");
getModules(factory, "modules-center");
if (!no_center) {
getModules(factory, "modules-center");
}
getModules(factory, "modules-right");
for (auto const& module : modules_left_) {
left_.pack_start(*module, false, false);
left_.pack_start(*module, module->expandEnabled(), module->expandEnabled());
}
for (auto const& module : modules_center_) {
center_.pack_start(*module, false, false);
if (!no_center) {
for (auto const& module : modules_center_) {
center_.pack_start(*module, module->expandEnabled(), module->expandEnabled());
}
}
std::reverse(modules_right_.begin(), modules_right_.end());
for (auto const& module : modules_right_) {
right_.pack_end(*module, false, false);
right_.pack_end(*module, module->expandEnabled(), module->expandEnabled());
}
}
@@ -568,6 +693,17 @@ void waybar::Bar::onConfigure(GdkEventConfigure* ev) {
configureGlobalOffset(ev->width, ev->height);
spdlog::info(BAR_SIZE_MSG, ev->width, ev->height, output->name);
/*
* gtk-layer-shell waits for the compositor's initial configure while realizing the window. On a
* busy compositor (common during session startup) that wait can time out even though the initial
* configure arrives shortly afterwards. In that case GTK may not schedule the frame commit that
* presents the first layer-surface buffer, leaving an otherwise configured bar invisible. Force a
* commit after every configure so late initial configures, and later compositor-driven resizes,
* always result in a submitted surface state.
*/
window.queue_draw();
forceLayerCommit();
}
void waybar::Bar::configureGlobalOffset(int width, int height) {
@@ -612,3 +748,22 @@ void waybar::Bar::configureGlobalOffset(int width, int height) {
void waybar::Bar::onOutputGeometryChanged() {
configureGlobalOffset(window.get_width(), window.get_height());
}
void waybar::Bar::toggleSuspend(bool suspend) {
auto process_modules = [suspend](Gtk::Box& module_box) {
for (auto* widget : module_box.get_children()) {
auto* module = dynamic_cast<waybar::AModule*>(widget);
if (module && module->shouldSuspend()) {
if (suspend) {
module->suspend();
} else {
module->resume();
}
}
}
};
process_modules(left_);
process_modules(center_);
process_modules(right_);
}
+139 -66
View File
@@ -3,67 +3,89 @@
#include <gtk-layer-shell.h>
#include <spdlog/spdlog.h>
#include <algorithm>
#include <iostream>
#include <utility>
#include "gtkmm/icontheme.h"
#include "ext-idle-notify-v1-client-protocol.h"
#include "idle-inhibit-unstable-v1-client-protocol.h"
#include "util/clara.hpp"
#include "util/format.hpp"
#include "util/hex_checker.hpp"
waybar::Client *waybar::Client::inst() {
static auto c = new Client();
waybar::Client* waybar::Client::inst() {
static auto* c = new Client();
return c;
}
void waybar::Client::handleGlobal(void *data, struct wl_registry *registry, uint32_t name,
const char *interface, uint32_t version) {
auto client = static_cast<Client *>(data);
void waybar::Client::handleGlobal(void* data, struct wl_registry* registry, uint32_t name,
const char* interface, uint32_t version) {
auto* client = static_cast<Client*>(data);
if (strcmp(interface, zxdg_output_manager_v1_interface.name) == 0 &&
version >= ZXDG_OUTPUT_V1_NAME_SINCE_VERSION) {
client->xdg_output_manager = static_cast<struct zxdg_output_manager_v1 *>(wl_registry_bind(
if (client->xdg_output_manager != nullptr) {
zxdg_output_manager_v1_destroy(client->xdg_output_manager);
client->xdg_output_manager = nullptr;
}
client->xdg_output_manager = static_cast<struct zxdg_output_manager_v1*>(wl_registry_bind(
registry, name, &zxdg_output_manager_v1_interface, ZXDG_OUTPUT_V1_NAME_SINCE_VERSION));
} else if (strcmp(interface, zwp_idle_inhibit_manager_v1_interface.name) == 0) {
client->idle_inhibit_manager = static_cast<struct zwp_idle_inhibit_manager_v1 *>(
if (client->idle_inhibit_manager != nullptr) {
zwp_idle_inhibit_manager_v1_destroy(client->idle_inhibit_manager);
client->idle_inhibit_manager = nullptr;
}
client->idle_inhibit_manager = static_cast<struct zwp_idle_inhibit_manager_v1*>(
wl_registry_bind(registry, name, &zwp_idle_inhibit_manager_v1_interface, 1));
} else if (strcmp(interface, ext_idle_notifier_v1_interface.name) == 0) {
// Bind version 2 if available (for get_input_idle_notification), otherwise version 1
auto bind_version = std::min(version, 2u);
client->idle_notifier = static_cast<struct ext_idle_notifier_v1 *>(
wl_registry_bind(registry, name, &ext_idle_notifier_v1_interface, bind_version));
spdlog::debug("Bound ext-idle-notifier-v1 at version {}", bind_version);
}
}
void waybar::Client::handleGlobalRemove(void *data, struct wl_registry * /*registry*/,
void waybar::Client::handleGlobalRemove(void* data, struct wl_registry* /*registry*/,
uint32_t name) {
// Nothing here
}
void waybar::Client::handleOutput(struct waybar_output &output) {
void waybar::Client::handleOutput(struct waybar_output& output) {
static const struct zxdg_output_v1_listener xdgOutputListener = {
.logical_position = [](void *, struct zxdg_output_v1 *, int32_t, int32_t) {},
.logical_size = [](void *, struct zxdg_output_v1 *, int32_t, int32_t) {},
.logical_position = [](void*, struct zxdg_output_v1*, int32_t, int32_t) {},
.logical_size = [](void*, struct zxdg_output_v1*, int32_t, int32_t) {},
.done = &handleOutputDone,
.name = &handleOutputName,
.description = &handleOutputDescription,
};
// owned by output->monitor; no need to destroy
auto wl_output = gdk_wayland_monitor_get_wl_output(output.monitor->gobj());
auto* wl_output = gdk_wayland_monitor_get_wl_output(output.monitor->gobj());
output.xdg_output.reset(zxdg_output_manager_v1_get_xdg_output(xdg_output_manager, wl_output));
zxdg_output_v1_add_listener(output.xdg_output.get(), &xdgOutputListener, &output);
}
struct waybar::waybar_output &waybar::Client::getOutput(void *addr) {
struct waybar::waybar_output& waybar::Client::getOutput(void* addr) {
auto it = std::find_if(outputs_.begin(), outputs_.end(),
[&addr](const auto &output) { return &output == addr; });
[&addr](const auto& output) { return &output == addr; });
if (it == outputs_.end()) {
throw std::runtime_error("Unable to find valid output");
}
return *it;
}
std::vector<Json::Value> waybar::Client::getOutputConfigs(struct waybar_output &output) {
std::vector<Json::Value> waybar::Client::getOutputConfigs(struct waybar_output& output) {
return config.getOutputConfigs(output.name, output.identifier);
}
void waybar::Client::handleOutputDone(void *data, struct zxdg_output_v1 * /*xdg_output*/) {
auto client = waybar::Client::inst();
void waybar::Client::handleOutputDone(void* data, struct zxdg_output_v1* /*xdg_output*/) {
auto* client = waybar::Client::inst();
try {
auto &output = client->getOutput(data);
auto& output = client->getOutput(data);
/**
* Multiple .done events may arrive in batch. In this case libwayland would queue
* xdg_output.destroy and dispatch all pending events, triggering this callback several times
@@ -77,52 +99,75 @@ void waybar::Client::handleOutputDone(void *data, struct zxdg_output_v1 * /*xdg_
output.xdg_output.reset();
spdlog::debug("Output detection done: {} ({})", output.name, output.identifier);
auto configs = client->getOutputConfigs(output);
if (!configs.empty()) {
for (const auto &config : configs) {
client->bars.emplace_back(std::make_unique<Bar>(&output, config));
}
client->pending_outputs_.push_back(&output);
if (!client->bars_scheduled_) {
client->bars_scheduled_ = true;
Glib::signal_idle().connect_once([client]() {
client->createBarsBatch();
}, Glib::PRIORITY_HIGH_IDLE);
}
}
} catch (const std::exception &e) {
std::cerr << e.what() << std::endl;
} catch (const std::exception& e) {
spdlog::warn("caught exception in zxdg_output_v1_listener::done: {}", e.what());
}
}
void waybar::Client::handleOutputName(void *data, struct zxdg_output_v1 * /*xdg_output*/,
const char *name) {
auto client = waybar::Client::inst();
void waybar::Client::createBarsBatch() {
pending_outputs_.remove_if([this](auto* output) { return std::none_of(outputs_.begin(), outputs_.end(), [&output](const auto& o) { return &o == output; }); });
for (auto* output : pending_outputs_) {
try {
auto configs = getOutputConfigs(*output);
if (!configs.empty()) {
for (const auto& config : configs) {
bars.emplace_back(std::make_unique<Bar>(output, config));
}
}
} catch (const std::exception& e) {
spdlog::warn("Error creating bar: {}", e.what());
}
}
pending_outputs_.clear();
bars_scheduled_ = false;
}
void waybar::Client::handleOutputName(void* data, struct zxdg_output_v1* /*xdg_output*/,
const char* name) {
auto* client = waybar::Client::inst();
try {
auto &output = client->getOutput(data);
auto& output = client->getOutput(data);
output.name = name;
} catch (const std::exception &e) {
std::cerr << e.what() << std::endl;
} catch (const std::exception& e) {
spdlog::warn("caught exception in zxdg_output_v1_listener::name: {}", e.what());
}
}
void waybar::Client::handleOutputDescription(void *data, struct zxdg_output_v1 * /*xdg_output*/,
const char *description) {
auto client = waybar::Client::inst();
void waybar::Client::handleOutputDescription(void* data, struct zxdg_output_v1* /*xdg_output*/,
const char* description) {
auto* client = waybar::Client::inst();
try {
auto &output = client->getOutput(data);
const char *open_paren = strrchr(description, '(');
auto& output = client->getOutput(data);
// Description format: "identifier (name)"
size_t identifier_length = open_paren - description;
output.identifier = std::string(description, identifier_length - 1);
} catch (const std::exception &e) {
std::cerr << e.what() << std::endl;
auto s = std::string(description);
auto pos = s.find(" (");
output.identifier = pos != std::string::npos ? s.substr(0, pos) : s;
} catch (const std::exception& e) {
spdlog::warn("caught exception in zxdg_output_v1_listener::description: {}", e.what());
}
}
void waybar::Client::handleMonitorAdded(Glib::RefPtr<Gdk::Monitor> monitor) {
auto &output = outputs_.emplace_back();
output.monitor = monitor;
auto& output = outputs_.emplace_back();
output.monitor = std::move(monitor);
handleOutput(output);
}
void waybar::Client::handleMonitorRemoved(Glib::RefPtr<Gdk::Monitor> monitor) {
spdlog::debug("Output removed: {} {}", monitor->get_manufacturer(), monitor->get_model());
spdlog::debug("Output removed: {} {}", monitor->get_manufacturer().c_str(),
monitor->get_model().c_str());
/* This event can be triggered from wl_display_roundtrip called by GTK or our code.
* Defer destruction of bars for the output to the next iteration of the event loop to avoid
* deleting objects referenced by currently executed code.
@@ -144,47 +189,65 @@ void waybar::Client::handleDeferredMonitorRemoval(Glib::RefPtr<Gdk::Monitor> mon
++it;
}
}
outputs_.remove_if([&monitor](const auto &output) { return output.monitor == monitor; });
outputs_.remove_if([&monitor](const auto& output) { return output.monitor == monitor; });
}
const std::string waybar::Client::getStyle(const std::string &style,
const std::string waybar::Client::getStyle(const std::string& style,
std::optional<Appearance> appearance = std::nullopt) {
auto gtk_settings = Gtk::Settings::get_default();
std::optional<std::string> css_file;
if (style.empty()) {
std::vector<std::string> search_files;
switch (appearance.value_or(portal->getAppearance())) {
case waybar::Appearance::LIGHT:
search_files.push_back("style-light.css");
search_files.emplace_back("style-light.css");
gtk_settings->property_gtk_application_prefer_dark_theme() = false;
break;
case waybar::Appearance::DARK:
search_files.push_back("style-dark.css");
search_files.emplace_back("style-dark.css");
gtk_settings->property_gtk_application_prefer_dark_theme() = true;
break;
case waybar::Appearance::UNKNOWN:
break;
}
search_files.push_back("style.css");
search_files.emplace_back("style.css");
css_file = Config::findConfigPath(search_files);
} else {
css_file = style;
}
if (!css_file) {
throw std::runtime_error("Missing required resource files");
}
spdlog::info("Using CSS file {}", css_file.value());
return css_file.value();
};
auto waybar::Client::setupCss(const std::string &css_file) -> void {
css_provider_ = Gtk::CssProvider::create();
style_context_ = Gtk::StyleContext::create();
// Load our css file, wherever that may be hiding
if (!css_provider_->load_from_path(css_file)) {
throw std::runtime_error("Can't open style file");
auto waybar::Client::setupCss(const std::string& css_file) -> void {
auto screen = Gdk::Screen::get_default();
if (!screen) {
throw std::runtime_error("No default screen");
}
// there's always only one screen
style_context_->add_provider_for_screen(Gdk::Screen::get_default(), css_provider_,
GTK_STYLE_PROVIDER_PRIORITY_USER);
if (css_provider_) {
Gtk::StyleContext::remove_provider_for_screen(screen, css_provider_);
css_provider_.reset();
}
css_provider_ = Gtk::CssProvider::create();
auto [modified_css, was_transformed] = transform_8bit_to_hex(css_file);
if (was_transformed) {
css_provider_->load_from_data(modified_css);
} else {
if (!css_provider_->load_from_path(css_file)) {
css_provider_.reset();
throw std::runtime_error("Can't open style file");
}
}
Gtk::StyleContext::add_provider_for_screen(screen, css_provider_,
GTK_STYLE_PROVIDER_PRIORITY_USER);
}
void waybar::Client::bindInterfaces() {
@@ -196,24 +259,33 @@ void waybar::Client::bindInterfaces() {
wl_registry_add_listener(registry, &registry_listener, this);
wl_display_roundtrip(wl_display);
if (!gtk_layer_is_supported()) {
if (gtk_layer_is_supported() == 0) {
throw std::runtime_error("The Wayland compositor does not support wlr-layer-shell protocol");
}
if (xdg_output_manager == nullptr) {
throw std::runtime_error("Failed to acquire required resources.");
}
// Disconnect previous signal handlers to prevent duplicate handlers on reload
monitor_added_connection_.disconnect();
monitor_removed_connection_.disconnect();
// Clear stale outputs from previous run
outputs_.clear();
// add existing outputs and subscribe to updates
for (auto i = 0; i < gdk_display->get_n_monitors(); ++i) {
auto monitor = gdk_display->get_monitor(i);
handleMonitorAdded(monitor);
}
gdk_display->signal_monitor_added().connect(sigc::mem_fun(*this, &Client::handleMonitorAdded));
gdk_display->signal_monitor_removed().connect(
monitor_added_connection_ = gdk_display->signal_monitor_added().connect(
sigc::mem_fun(*this, &Client::handleMonitorAdded));
monitor_removed_connection_ = gdk_display->signal_monitor_removed().connect(
sigc::mem_fun(*this, &Client::handleMonitorRemoved));
}
int waybar::Client::main(int argc, char *argv[]) {
int waybar::Client::main(int argc, char* argv[]) {
bool show_help = false;
bool show_version = false;
std::string config_opt;
@@ -233,11 +305,11 @@ int waybar::Client::main(int argc, char *argv[]) {
return 1;
}
if (show_help) {
std::cout << cli << std::endl;
std::cout << cli << '\n';
return 0;
}
if (show_version) {
std::cout << "Waybar v" << VERSION << std::endl;
std::cout << "Waybar v" << VERSION << '\n';
return 0;
}
if (!log_level.empty()) {
@@ -264,9 +336,10 @@ int waybar::Client::main(int argc, char *argv[]) {
}
m_cssFile = getStyle(style_opt);
setupCss(m_cssFile);
m_cssReloadHelper = std::make_unique<CssReloadHelper>(m_cssFile, [&]() { setupCss(m_cssFile); });
m_cssReloadHelper = std::make_unique<CssReloadHelper>(m_cssFile, [&](const std::string& css_file) { setupCss(css_file); });
portal->signal_appearance_changed().connect([&](waybar::Appearance appearance) {
auto css_file = getStyle(style_opt, appearance);
m_cssReloadHelper->changeCssFile(css_file);
setupCss(css_file);
});
@@ -274,7 +347,7 @@ int waybar::Client::main(int argc, char *argv[]) {
if (m_config.isObject() && m_config["reload_style_on_change"].asBool()) {
m_cssReloadHelper->monitorChanges();
} else if (m_config.isArray()) {
for (const auto &conf : m_config) {
for (const auto& conf : m_config) {
if (conf["reload_style_on_change"].asBool()) {
m_cssReloadHelper->monitorChanges();
break;
+94 -37
View File
@@ -2,7 +2,11 @@
#include <spdlog/spdlog.h>
#include <unistd.h>
#ifndef __OpenBSD__
#include <wordexp.h>
#else
#include <glob.h>
#endif
#include <filesystem>
#include <fstream>
@@ -19,12 +23,13 @@ const std::vector<std::string> Config::CONFIG_DIRS = {
"/etc/xdg/waybar/", SYSCONFDIR "/xdg/waybar/", "./resources/",
};
const char *Config::CONFIG_PATH_ENV = "WAYBAR_CONFIG_DIR";
const char* Config::CONFIG_PATH_ENV = "WAYBAR_CONFIG_DIR";
std::optional<std::string> tryExpandPath(const std::string base, const std::string filename) {
std::vector<std::string> Config::tryExpandPath(const std::string& base,
const std::string& filename) {
fs::path path;
if (filename != "") {
if (!filename.empty()) {
path = fs::path(base) / fs::path(filename);
} else {
path = fs::path(base);
@@ -32,40 +37,55 @@ std::optional<std::string> tryExpandPath(const std::string base, const std::stri
spdlog::debug("Try expanding: {}", path.string());
std::vector<std::string> results;
#ifndef __OpenBSD__
wordexp_t p;
if (wordexp(path.c_str(), &p, 0) == 0) {
if (access(*p.we_wordv, F_OK) == 0) {
std::string result = *p.we_wordv;
wordfree(&p);
spdlog::debug("Found config file: {}", path.string());
return result;
for (size_t i = 0; i < p.we_wordc; i++) {
if (access(p.we_wordv[i], F_OK) == 0) {
results.emplace_back(p.we_wordv[i]);
spdlog::debug("Found config file: {}", p.we_wordv[i]);
}
}
wordfree(&p);
}
return std::nullopt;
#else
glob_t p;
if (glob(path.c_str(), 0, NULL, &p) == 0) {
for (size_t i = 0; i < p.gl_pathc; i++) {
if (access(p.gl_pathv[i], F_OK) == 0) {
results.emplace_back(p.gl_pathv[i]);
spdlog::debug("Found config file: {}", p.gl_pathv[i]);
}
}
globfree(&p);
}
#endif
return results;
}
std::optional<std::string> Config::findConfigPath(const std::vector<std::string> &names,
const std::vector<std::string> &dirs) {
if (const char *dir = std::getenv(Config::CONFIG_PATH_ENV)) {
for (const auto &name : names) {
if (auto res = tryExpandPath(dir, name); res) {
return res;
std::optional<std::string> Config::findConfigPath(const std::vector<std::string>& names,
const std::vector<std::string>& dirs) {
if (const char* dir = std::getenv(Config::CONFIG_PATH_ENV)) {
for (const auto& name : names) {
if (auto res = tryExpandPath(dir, name); !res.empty()) {
return res.front();
}
}
}
for (const auto &dir : dirs) {
for (const auto &name : names) {
if (auto res = tryExpandPath(dir, name); res) {
return res;
for (const auto& dir : dirs) {
for (const auto& name : names) {
if (auto res = tryExpandPath(dir, name); !res.empty()) {
return res.front();
}
}
}
return std::nullopt;
}
void Config::setupConfig(Json::Value &dst, const std::string &config_file, int depth) {
void Config::setupConfig(Json::Value& dst, const std::string& config_file, int depth) {
if (depth > 100) {
throw std::runtime_error("Aborting due to likely recursive include in config files");
}
@@ -77,7 +97,7 @@ void Config::setupConfig(Json::Value &dst, const std::string &config_file, int d
util::JsonParser parser;
Json::Value tmp_config = parser.parse(str);
if (tmp_config.isArray()) {
for (auto &config_part : tmp_config) {
for (auto& config_part : tmp_config) {
resolveConfigIncludes(config_part, depth);
}
} else {
@@ -86,25 +106,59 @@ void Config::setupConfig(Json::Value &dst, const std::string &config_file, int d
mergeConfig(dst, tmp_config);
}
void Config::resolveConfigIncludes(Json::Value &config, int depth) {
std::vector<std::string> Config::findIncludePath(const std::string& name,
const std::vector<std::string>& dirs) {
auto match1 = tryExpandPath(name, "");
if (!match1.empty()) {
return match1;
}
if (const char* dir = std::getenv(Config::CONFIG_PATH_ENV)) {
if (auto res = tryExpandPath(dir, name); !res.empty()) {
return res;
}
}
for (const auto& dir : dirs) {
if (auto res = tryExpandPath(dir, name); !res.empty()) {
return res;
}
}
return {};
}
void Config::resolveConfigIncludes(Json::Value& config, int depth) {
Json::Value includes = config["include"];
if (includes.isArray()) {
for (const auto &include : includes) {
for (const auto& include : includes) {
spdlog::info("Including resource file: {}", include.asString());
setupConfig(config, tryExpandPath(include.asString(), "").value_or(""), ++depth);
auto matches = findIncludePath(include.asString());
if (!matches.empty()) {
for (const auto& match : matches) {
setupConfig(config, match, depth + 1);
}
} else {
spdlog::warn("Unable to find resource file: {}", include.asString());
}
}
} else if (includes.isString()) {
spdlog::info("Including resource file: {}", includes.asString());
setupConfig(config, tryExpandPath(includes.asString(), "").value_or(""), ++depth);
auto matches = findIncludePath(includes.asString());
if (!matches.empty()) {
for (const auto& match : matches) {
setupConfig(config, match, depth + 1);
}
} else {
spdlog::warn("Unable to find resource file: {}", includes.asString());
}
}
}
void Config::mergeConfig(Json::Value &a_config_, Json::Value &b_config_) {
void Config::mergeConfig(Json::Value& a_config_, Json::Value& b_config_) {
if (!a_config_) {
// For the first config
a_config_ = b_config_;
} else if (a_config_.isObject() && b_config_.isObject()) {
for (const auto &key : b_config_.getMemberNames()) {
for (const auto& key : b_config_.getMemberNames()) {
// [] creates key with default value. Use `get` to avoid that.
if (a_config_.get(key, Json::Value::nullSingleton()).isObject() &&
b_config_[key].isObject()) {
@@ -120,18 +174,18 @@ void Config::mergeConfig(Json::Value &a_config_, Json::Value &b_config_) {
spdlog::error("Cannot merge config, conflicting or invalid JSON types");
}
}
bool isValidOutput(const Json::Value &config, const std::string &name,
const std::string &identifier) {
bool isValidOutput(const Json::Value& config, const std::string& name,
const std::string& identifier) {
if (config["output"].isArray()) {
for (auto const &output_conf : config["output"]) {
for (auto const& output_conf : config["output"]) {
if (output_conf.isString()) {
auto config_output = output_conf.asString();
if (config_output.substr(0, 1) == "!") {
if (config_output.substr(1) == name || config_output.substr(1) == identifier) {
return false;
} else {
continue;
}
continue;
}
if (config_output == name || config_output == identifier) {
return true;
@@ -142,7 +196,9 @@ bool isValidOutput(const Json::Value &config, const std::string &name,
}
}
return false;
} else if (config["output"].isString()) {
}
if (config["output"].isString()) {
auto config_output = config["output"].asString();
if (!config_output.empty()) {
if (config_output.substr(0, 1) == "!") {
@@ -155,21 +211,22 @@ bool isValidOutput(const Json::Value &config, const std::string &name,
return true;
}
void Config::load(const std::string &config) {
void Config::load(const std::string& config) {
auto file = config.empty() ? findConfigPath({"config", "config.jsonc"}) : config;
if (!file) {
throw std::runtime_error("Missing required resource files");
}
config_file_ = file.value();
spdlog::info("Using configuration file {}", config_file_);
config_ = Json::Value();
setupConfig(config_, config_file_, 0);
}
std::vector<Json::Value> Config::getOutputConfigs(const std::string &name,
const std::string &identifier) {
std::vector<Json::Value> Config::getOutputConfigs(const std::string& name,
const std::string& identifier) {
std::vector<Json::Value> configs;
if (config_.isArray()) {
for (auto const &config : config_) {
for (auto const& config : config_) {
if (config.isObject() && isValidOutput(config, name, identifier)) {
configs.push_back(config);
}
+86 -14
View File
@@ -17,8 +17,8 @@
#ifdef HAVE_WLR_TASKBAR
#include "modules/wlr/taskbar.hpp"
#endif
#ifdef HAVE_WLR_WORKSPACES
#include "modules/wlr/workspace_manager.hpp"
#ifdef HAVE_EXT_WORKSPACES
#include "modules/ext/workspace_manager.hpp"
#endif
#ifdef HAVE_RIVER
#include "modules/river/layout.hpp"
@@ -28,19 +28,38 @@
#endif
#ifdef HAVE_DWL
#include "modules/dwl/tags.hpp"
#include "modules/dwl/window.hpp"
#endif
#ifdef HAVE_HYPRLAND
#include "modules/hyprland/language.hpp"
#include "modules/hyprland/submap.hpp"
#include "modules/hyprland/window.hpp"
#include "modules/hyprland/windowcount.hpp"
#include "modules/hyprland/workspaces.hpp"
#endif
#ifdef HAVE_NIRI
#include "modules/niri/language.hpp"
#include "modules/niri/window.hpp"
#include "modules/niri/workspaces.hpp"
#endif
#ifdef HAVE_MANGO
#include "modules/mango/keymode.hpp"
#include "modules/mango/language.hpp"
#include "modules/mango/layout.hpp"
#include "modules/mango/window.hpp"
#include "modules/mango/workspaces.hpp"
#endif
#ifdef HAVE_WAYFIRE
#include "modules/wayfire/window.hpp"
#include "modules/wayfire/workspaces.hpp"
#endif
#if defined(__FreeBSD__) || defined(__linux__)
#include "modules/battery.hpp"
#endif
#if defined(HAVE_CPU_LINUX) || defined(HAVE_CPU_BSD)
#include "modules/cpu.hpp"
#include "modules/cpu_frequency.hpp"
#include "modules/cpu_graph.hpp"
#include "modules/cpu_usage.hpp"
#include "modules/load.hpp"
#endif
@@ -69,7 +88,7 @@
#include "modules/gamemode.hpp"
#endif
#ifdef HAVE_UPOWER
#include "modules/upower/upower.hpp"
#include "modules/upower.hpp"
#endif
#ifdef HAVE_PIPEWIRE
#include "modules/privacy/privacy.hpp"
@@ -97,14 +116,16 @@
#ifdef HAVE_LIBWIREPLUMBER
#include "modules/wireplumber.hpp"
#endif
#ifdef HAVE_LIBCAVA
#include "modules/cava.hpp"
#endif
#ifdef HAVE_SYSTEMD_MONITOR
#include "modules/systemd_failed_units.hpp"
#endif
#ifdef HAVE_LIBGPS
#include "modules/gps.hpp"
#endif
#include "modules/cava/cava_frontend.hpp"
#include "modules/cffi.hpp"
#include "modules/custom.hpp"
#include "modules/custom_graph.hpp"
#include "modules/image.hpp"
#include "modules/temperature.hpp"
#include "modules/user.hpp"
@@ -129,12 +150,12 @@ waybar::AModule* waybar::Factory::makeModule(const std::string& name,
#endif
#ifdef HAVE_UPOWER
if (ref == "upower") {
return new waybar::modules::upower::UPower(id, config_[name]);
return new waybar::modules::UPower(id, config_[name]);
}
#endif
#ifdef HAVE_PIPEWIRE
if (ref == "privacy") {
return new waybar::modules::privacy::Privacy(id, config_[name], pos);
return new waybar::modules::privacy::Privacy(id, config_[name], bar_.orientation, pos);
}
#endif
#ifdef HAVE_MPRIS
@@ -164,9 +185,9 @@ waybar::AModule* waybar::Factory::makeModule(const std::string& name,
return new waybar::modules::wlr::Taskbar(id, bar_, config_[name]);
}
#endif
#ifdef HAVE_WLR_WORKSPACES
if (ref == "wlr/workspaces") {
return new waybar::modules::wlr::WorkspaceManager(id, bar_, config_[name]);
#ifdef HAVE_EXT_WORKSPACES
if (ref == "ext/workspaces") {
return new waybar::modules::ext::WorkspaceManager(id, bar_, config_[name]);
}
#endif
#ifdef HAVE_RIVER
@@ -187,11 +208,17 @@ waybar::AModule* waybar::Factory::makeModule(const std::string& name,
if (ref == "dwl/tags") {
return new waybar::modules::dwl::Tags(id, bar_, config_[name]);
}
if (ref == "dwl/window") {
return new waybar::modules::dwl::Window(id, bar_, config_[name]);
}
#endif
#ifdef HAVE_HYPRLAND
if (ref == "hyprland/window") {
return new waybar::modules::hyprland::Window(id, bar_, config_[name]);
}
if (ref == "hyprland/windowcount") {
return new waybar::modules::hyprland::WindowCount(id, bar_, config_[name]);
}
if (ref == "hyprland/language") {
return new waybar::modules::hyprland::Language(id, bar_, config_[name]);
}
@@ -201,6 +228,42 @@ waybar::AModule* waybar::Factory::makeModule(const std::string& name,
if (ref == "hyprland/workspaces") {
return new waybar::modules::hyprland::Workspaces(id, bar_, config_[name]);
}
#endif
#ifdef HAVE_NIRI
if (ref == "niri/language") {
return new waybar::modules::niri::Language(id, bar_, config_[name]);
}
if (ref == "niri/window") {
return new waybar::modules::niri::Window(id, bar_, config_[name]);
}
if (ref == "niri/workspaces") {
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]);
}
if (ref == "wayfire/workspaces") {
return new waybar::modules::wayfire::Workspaces(id, bar_, config_[name]);
}
#endif
if (ref == "idle_inhibitor") {
return new waybar::modules::IdleInhibitor(id, bar_, config_[name]);
@@ -214,6 +277,9 @@ waybar::AModule* waybar::Factory::makeModule(const std::string& name,
if (ref == "cpu") {
return new waybar::modules::Cpu(id, config_[name]);
}
if (ref == "cpu_graph") {
return new waybar::modules::CpuGraph(id, config_[name]);
}
#if defined(HAVE_CPU_LINUX)
if (ref == "cpu_frequency") {
return new waybar::modules::CpuFrequency(id, config_[name]);
@@ -302,15 +368,18 @@ waybar::AModule* waybar::Factory::makeModule(const std::string& name,
return new waybar::modules::Wireplumber(id, config_[name]);
}
#endif
#ifdef HAVE_LIBCAVA
if (ref == "cava") {
return new waybar::modules::Cava(id, config_[name]);
return waybar::modules::cava::getModule(id, config_[name]);
}
#endif
#ifdef HAVE_SYSTEMD_MONITOR
if (ref == "systemd-failed-units") {
return new waybar::modules::SystemdFailedUnits(id, config_[name]);
}
#endif
#ifdef HAVE_LIBGPS
if (ref == "gps") {
return new waybar::modules::Gps(id, config_[name]);
}
#endif
if (ref == "temperature") {
return new waybar::modules::Temperature(id, config_[name]);
@@ -318,6 +387,9 @@ waybar::AModule* waybar::Factory::makeModule(const std::string& name,
if (ref.compare(0, 7, "custom/") == 0 && ref.size() > 7) {
return new waybar::modules::Custom(ref.substr(7), id, config_[name], bar_.output->name);
}
if (ref.compare(0, 13, "custom-graph/") == 0 && ref.size() > 13) {
return new waybar::modules::CustomGraph(ref.substr(13), id, config_[name], bar_.output->name);
}
if (ref.compare(0, 5, "cffi/") == 0 && ref.size() > 5) {
return new waybar::modules::CFFI(ref.substr(5), id, config_[name]);
}
+125 -32
View File
@@ -4,12 +4,12 @@
#include <util/command.hpp>
#include "gdkmm/device.h"
#include "gtkmm/enums.h"
#include "gtkmm/widget.h"
namespace waybar {
const Gtk::RevealerTransitionType getPreferredTransitionType(bool is_vertical) {
Gtk::RevealerTransitionType getPreferredTransitionType(bool is_vertical) {
/* The transition direction of a drawer is not actually determined by the transition type,
* but rather by the order of 'box' and 'revealer_box':
* 'REVEALER_TRANSITION_TYPE_SLIDE_LEFT' and 'REVEALER_TRANSITION_TYPE_SLIDE_RIGHT'
@@ -19,17 +19,18 @@ const Gtk::RevealerTransitionType getPreferredTransitionType(bool is_vertical) {
if (is_vertical) {
return Gtk::RevealerTransitionType::REVEALER_TRANSITION_TYPE_SLIDE_UP;
} else {
return Gtk::RevealerTransitionType::REVEALER_TRANSITION_TYPE_SLIDE_LEFT;
}
return Gtk::RevealerTransitionType::REVEALER_TRANSITION_TYPE_SLIDE_LEFT;
}
Group::Group(const std::string& name, const std::string& id, const Json::Value& config,
bool vertical)
: AModule(config, name, id, true, true),
: AModule(config, name, id, true, false),
box{vertical ? Gtk::ORIENTATION_VERTICAL : Gtk::ORIENTATION_HORIZONTAL, 0},
revealer_box{vertical ? Gtk::ORIENTATION_VERTICAL : Gtk::ORIENTATION_HORIZONTAL, 0} {
box.set_name(name_);
box.get_style_context()->add_class("empty");
if (!id.empty()) {
box.get_style_context()->add_class(id);
}
@@ -62,12 +63,30 @@ Group::Group(const std::string& name, const std::string& id, const Json::Value&
const bool left_to_right = (drawer_config["transition-left-to-right"].isBool()
? drawer_config["transition-left-to-right"].asBool()
: true);
const bool reveal_by_default =
(drawer_config["reveal-by-default"].isBool() ? drawer_config["reveal-by-default"].asBool()
: false);
click_to_reveal = drawer_config["click-to-reveal"].asBool();
reveal_delay = drawer_config["reveal-delay"].asInt();
const bool start_expanded =
(drawer_config["start-expanded"].isBool() ? drawer_config["start-expanded"].asBool()
: false);
empty_if_drawer_empty = (drawer_config["empty-if-drawer-empty"].isBool()
? drawer_config["empty-if-drawer-empty"].asBool()
: false);
auto transition_type = getPreferredTransitionType(vertical);
revealer.set_transition_type(transition_type);
revealer.set_transition_duration(transition_duration);
revealer.set_reveal_child(false);
if ((click_to_reveal && reveal_by_default) || start_expanded) {
box.set_state_flags(Gtk::StateFlags::STATE_FLAG_PRELIGHT);
revealer.set_reveal_child(true);
} else {
revealer.set_reveal_child(false);
}
revealer.get_style_context()->add_class("drawer");
@@ -78,34 +97,110 @@ Group::Group(const std::string& name, const std::string& id, const Json::Value&
} else {
box.pack_start(revealer);
}
addHoverHandlerTo(revealer);
}
event_box_.add(box);
}
bool Group::handleMouseHover(GdkEventCrossing* const& e) {
switch (e->type) {
case GDK_ENTER_NOTIFY:
revealer.set_reveal_child(true);
break;
case GDK_LEAVE_NOTIFY:
revealer.set_reveal_child(false);
break;
default:
break;
}
void Group::show_group() {
box.set_state_flags(Gtk::StateFlags::STATE_FLAG_PRELIGHT);
revealer.set_reveal_child(true);
box.get_style_context()->add_class("expanded");
}
void Group::hide_group() {
box.unset_state_flags(Gtk::StateFlags::STATE_FLAG_PRELIGHT);
revealer.set_reveal_child(false);
box.get_style_context()->remove_class("expanded");
}
bool Group::handleMouseEnter(GdkEventCrossing* const& e) {
if (!click_to_reveal) {
if (reveal_delay > 0) {
if (reveal_timeout_.connected()) {
reveal_timeout_.disconnect();
}
reveal_timeout_ = Glib::signal_timeout().connect(
[this]() {
show_group();
return false;
},
reveal_delay);
} else {
show_group();
}
}
return false;
}
bool Group::handleMouseLeave(GdkEventCrossing* const& e) {
if (!click_to_reveal && e->detail != GDK_NOTIFY_INFERIOR) {
if (reveal_delay > 0 && reveal_timeout_.connected()) {
reveal_timeout_.disconnect();
}
hide_group();
}
return false;
}
bool Group::handleToggle(GdkEventButton* const& e) {
if (!click_to_reveal || e->button != 1) {
return false;
}
if ((box.get_state_flags() & Gtk::StateFlags::STATE_FLAG_PRELIGHT) != 0U) {
hide_group();
} else {
show_group();
}
return true;
}
void Group::addHoverHandlerTo(Gtk::Widget& widget) {
widget.add_events(Gdk::EventMask::ENTER_NOTIFY_MASK | Gdk::EventMask::LEAVE_NOTIFY_MASK);
widget.signal_enter_notify_event().connect(sigc::mem_fun(*this, &Group::handleMouseHover));
widget.signal_leave_notify_event().connect(sigc::mem_fun(*this, &Group::handleMouseHover));
auto Group::update() -> void {
bool has_visible_child = false;
bool has_visible_drawer_child = false;
if (is_drawer) {
for (auto* rev_child : revealer_box.get_children()) {
if (rev_child->get_visible()) {
has_visible_drawer_child = true;
break;
}
}
}
if (is_drawer && empty_if_drawer_empty) {
has_visible_child = has_visible_drawer_child;
} else {
for (auto* child : box.get_children()) {
if (child == &revealer) {
if (has_visible_drawer_child) {
has_visible_child = true;
break;
}
} else if (child->get_visible()) {
has_visible_child = true;
break;
}
}
}
auto style = box.get_style_context();
if (has_visible_child) {
if (style->has_class("empty")) {
style->remove_class("empty");
}
} else {
if (!style->has_class("empty")) {
style->add_class("empty");
}
}
}
auto Group::update() -> void {
// noop
bool Group::handleScroll(GdkEventScroll* e) {
// no scroll.
return true;
}
Gtk::Box& Group::getBox() { return is_drawer ? (is_first_widget ? box : revealer_box) : box; }
@@ -113,17 +208,15 @@ Gtk::Box& Group::getBox() { return is_drawer ? (is_first_widget ? box : revealer
void Group::addWidget(Gtk::Widget& widget) {
getBox().pack_start(widget, false, false);
if (is_drawer) {
// Necessary because of GTK's hitbox detection
addHoverHandlerTo(widget);
if (!is_first_widget) {
widget.get_style_context()->add_class(add_class_to_drawer_children);
}
if (is_drawer && !is_first_widget) {
widget.get_style_context()->add_class(add_class_to_drawer_children);
}
is_first_widget = false;
widget.property_visible().signal_changed().connect(sigc::mem_fun(*this, &Group::update));
update();
}
Group::operator Gtk::Widget&() { return box; }
Group::operator Gtk::Widget&() { return event_box_; }
} // namespace waybar
+203 -70
View File
@@ -1,3 +1,4 @@
#include <fcntl.h>
#include <spdlog/spdlog.h>
#include <sys/types.h>
#include <sys/wait.h>
@@ -6,99 +7,227 @@
#include <list>
#include <mutex>
#ifdef HAVE_LIBSYSTEMD
#include <spdlog/sinks/systemd_sink.h>
#include <sys/stat.h>
#include <cassert>
#include <charconv>
#include <cstddef>
#include <cstdlib>
#include <system_error>
#endif
#include "bar.hpp"
#include "client.hpp"
#include "util/SafeSignal.hpp"
std::mutex reap_mtx;
std::list<pid_t> reap;
volatile bool reload;
void* signalThread(void* args) {
int err, signum;
sigset_t mask;
sigemptyset(&mask);
sigaddset(&mask, SIGCHLD);
static int signal_pipe_write_fd;
// Write a single signal to `signal_pipe_write_fd`.
// This function is set as a signal handler, so it must be async-signal-safe.
static void writeSignalToPipe(int signum) {
ssize_t amt = write(signal_pipe_write_fd, &signum, sizeof(int));
// There's not much we can safely do inside of a signal handler.
// Let's just ignore any errors.
(void)amt;
}
// This initializes `signal_pipe_write_fd`, and sets up signal handlers.
//
// This function will run forever, emitting every `SIGUSR1`, `SIGUSR2`,
// `SIGINT`, `SIGCHLD`, and `SIGRTMIN + 1`...`SIGRTMAX` signal received
// to `signal_handler`.
static void catchSignals(waybar::SafeSignal<int>& signal_handler) {
int fd[2];
if (pipe(fd) != 0) {
spdlog::error("Failed to create signal pipe: {}", strerror(errno));
return;
}
int signal_pipe_read_fd = fd[0];
signal_pipe_write_fd = fd[1];
// This pipe should be able to buffer ~thousands of signals. If it fills up,
// we'll drop signals instead of blocking.
// We can't allow the write end to block because we'll be writing to it in a
// signal handler, which could interrupt the loop that's reading from it and
// deadlock.
fcntl(signal_pipe_write_fd, F_SETFL, O_NONBLOCK);
std::signal(SIGUSR1, writeSignalToPipe);
std::signal(SIGUSR2, writeSignalToPipe);
std::signal(SIGINT, writeSignalToPipe);
std::signal(SIGCHLD, writeSignalToPipe);
#ifdef SIGRTMIN
for (int sig = SIGRTMIN + 1; sig <= SIGRTMAX; ++sig) {
std::signal(sig, writeSignalToPipe);
}
#endif
while (true) {
err = sigwait(&mask, &signum);
if (err != 0) {
spdlog::error("sigwait failed: {}", strerror(errno));
int signum;
ssize_t amt = read(signal_pipe_read_fd, &signum, sizeof(int));
if (amt < 0) {
spdlog::error("read from signal pipe failed with error {}, closing thread", strerror(errno));
break;
}
if (amt != sizeof(int)) {
continue;
}
switch (signum) {
case SIGCHLD:
spdlog::debug("Received SIGCHLD in signalThread");
if (!reap.empty()) {
reap_mtx.lock();
for (auto it = reap.begin(); it != reap.end(); ++it) {
if (waitpid(*it, nullptr, WNOHANG) == *it) {
spdlog::debug("Reaped child with PID: {}", *it);
it = reap.erase(it);
}
}
reap_mtx.unlock();
}
signal_handler.emit(signum);
}
}
waybar::util::KillSignalAction getActionForBar(waybar::Bar* bar, int signal) {
switch (signal) {
case SIGUSR1:
return bar->getOnSigusr1Action();
case SIGUSR2:
return bar->getOnSigusr2Action();
default:
return waybar::util::KillSignalAction::NOOP;
}
}
void handleUserSignal(int signal, bool& reload) {
int i = 0;
for (auto& bar : waybar::Client::inst()->bars) {
switch (getActionForBar(bar.get(), signal)) {
case waybar::util::KillSignalAction::HIDE:
spdlog::debug("Visibility 'hide' for bar ", i);
bar->hide();
break;
default:
spdlog::debug("Received signal with number {}, but not handling", signum);
case waybar::util::KillSignalAction::SHOW:
spdlog::debug("Visibility 'show' for bar ", i);
bar->show();
break;
case waybar::util::KillSignalAction::TOGGLE:
spdlog::debug("Visibility 'toggle' for bar ", i);
bar->toggle();
break;
case waybar::util::KillSignalAction::RELOAD:
spdlog::info("Reloading...");
reload = true;
waybar::Client::inst()->reset();
return;
case waybar::util::KillSignalAction::NOOP:
break;
}
i++;
}
}
void startSignalThread(void) {
int err;
sigset_t mask;
sigemptyset(&mask);
sigaddset(&mask, SIGCHLD);
// Block SIGCHLD so it can be handled by the signal thread
// Any threads created by this one (the main thread) should not
// modify their signal mask to unblock SIGCHLD
err = pthread_sigmask(SIG_BLOCK, &mask, nullptr);
if (err != 0) {
spdlog::error("pthread_sigmask failed in startSignalThread: {}", strerror(err));
exit(1);
// Must be called on the main thread.
//
// If this signal should restart or close the bar, this function will write
// `true` or `false`, respectively, into `reload`.
static void handleSignalMainThread(int signum, bool& reload) {
#ifdef SIGRTMIN
if (signum >= SIGRTMIN + 1 && signum <= SIGRTMAX) {
for (auto& bar : waybar::Client::inst()->bars) {
bar->handleSignal(signum);
}
return;
}
#endif
pthread_t thread_id;
err = pthread_create(&thread_id, nullptr, signalThread, nullptr);
if (err != 0) {
spdlog::error("pthread_create failed in startSignalThread: {}", strerror(err));
exit(1);
}
}
int main(int argc, char* argv[]) {
try {
auto client = waybar::Client::inst();
std::signal(SIGUSR1, [](int /*signal*/) {
for (auto& bar : waybar::Client::inst()->bars) {
bar->toggle();
}
});
std::signal(SIGUSR2, [](int /*signal*/) {
spdlog::info("Reloading...");
reload = true;
waybar::Client::inst()->reset();
});
std::signal(SIGINT, [](int /*signal*/) {
switch (signum) {
case SIGUSR1:
handleUserSignal(SIGUSR1, reload);
break;
case SIGUSR2:
handleUserSignal(SIGUSR2, reload);
break;
case SIGINT:
spdlog::info("Quitting.");
reload = false;
waybar::Client::inst()->reset();
});
for (int sig = SIGRTMIN + 1; sig <= SIGRTMAX; ++sig) {
std::signal(sig, [](int sig) {
for (auto& bar : waybar::Client::inst()->bars) {
bar->handleSignal(sig);
break;
case SIGCHLD:
spdlog::debug("Received SIGCHLD in signalThread");
{
std::lock_guard<std::mutex> lock(reap_mtx);
for (auto it = reap.begin(); it != reap.end();) {
if (waitpid(*it, nullptr, WNOHANG) == *it) {
spdlog::debug("Reaped child with PID: {}", *it);
it = reap.erase(it);
} else {
++it;
}
}
});
}
break;
default:
spdlog::debug("Received signal with number {}, but not handling", signum);
break;
}
}
static void logToJournalIfRunAsService() {
#ifdef HAVE_LIBSYSTEMD
/* Implementation of automatic protocol upgrading (from stderr to journal)
** as described in https://systemd.io/JOURNAL_NATIVE_PROTOCOL */
char const* journal_stream = std::getenv("JOURNAL_STREAM");
if (journal_stream != nullptr) {
dev_t device;
ino_t inode;
size_t len = std::strlen(journal_stream);
auto result = std::from_chars(journal_stream, journal_stream + len, device);
if (result.ec == std::errc{})
result = std::from_chars(result.ptr + 1, journal_stream + len, inode);
if (result.ec != std::errc{}) {
spdlog::warn("malformed JOURNAL_STREAM (\"{}\"): {}, logging to console", journal_stream,
std::make_error_condition(result.ec).message());
}
startSignalThread();
struct stat f_stderr;
if (fstat(STDERR_FILENO, &f_stderr) != 0) {
spdlog::warn("unable to check stderr device and inode numbers: {}", strerror(errno));
} else if (device == f_stderr.st_dev && inode == f_stderr.st_ino) {
auto journald = spdlog::systemd_logger_st("native_journal", "waybar", false);
/* systemd_logger_st is thread-safe with enable_formatter = false
** thanks to underlying sd_journal_send being thread-safe
** https://github.com/gabime/spdlog/issues/2320#issuecomment-1079766037
*/
spdlog::set_default_logger(journald);
} else {
spdlog::info("JOURNAL_STREAM does not point to stderr, logging to console");
}
} else {
spdlog::info("no JOURNAL_STREAM, logging to console");
}
#endif
}
int main(int argc, char* argv[]) {
logToJournalIfRunAsService();
try {
auto* client = waybar::Client::inst();
bool reload;
waybar::SafeSignal<int> posix_signal_received;
posix_signal_received.connect([&](int signum) { handleSignalMainThread(signum, reload); });
std::thread signal_thread([&]() { catchSignals(posix_signal_received); });
// Every `std::thread` must be joined or detached.
// This thread should run forever, so detach it.
signal_thread.detach();
auto ret = 0;
do {
@@ -106,6 +235,10 @@ int main(int argc, char* argv[]) {
ret = client->main(argc, argv);
} while (reload);
std::signal(SIGUSR1, SIG_IGN);
std::signal(SIGUSR2, SIG_IGN);
std::signal(SIGINT, SIG_IGN);
delete client;
return ret;
} catch (const std::exception& e) {
+29 -17
View File
@@ -13,7 +13,7 @@
#include "util/backend_common.hpp"
#include "util/backlight_backend.hpp"
waybar::modules::Backlight::Backlight(const std::string &id, const Json::Value &config)
waybar::modules::Backlight::Backlight(const std::string& id, const Json::Value& config)
: ALabel(config, "backlight", id, "{percent}%", 2),
preferred_device_(config["device"].isString() ? config["device"].asString() : ""),
backend(interval_, [this] { dp.emit(); }) {
@@ -36,25 +36,29 @@ auto waybar::modules::Backlight::update() -> void {
if (best->get_powered()) {
event_box_.show();
const uint8_t percent =
best->get_max() == 0 ? 100 : round(best->get_actual() * 100.0f / best->get_max());
std::string desc = fmt::format(fmt::runtime(format_), fmt::arg("percent", percent),
fmt::arg("icon", getIcon(percent)));
label_.set_markup(desc);
getState(percent);
if (tooltipEnabled()) {
std::string tooltip_format;
if (config_["tooltip-format"].isString()) {
tooltip_format = config_["tooltip-format"].asString();
}
if (!tooltip_format.empty()) {
label_.set_tooltip_text(fmt::format(fmt::runtime(tooltip_format),
fmt::arg("percent", percent),
fmt::arg("icon", getIcon(percent))));
} else {
label_.set_tooltip_text(desc);
const uint8_t percent_exp =
best->get_max() == 0
? 100
: roundf(powf((float)best->get_actual() / best->get_max(), 1.0f / 2.718f) * 100);
// Get the state and apply state-specific format if available
auto state = getState(percent);
std::string current_format = format_;
if (!state.empty()) {
std::string state_format_name = "format-" + state;
if (config_[state_format_name].isString()) {
current_format = config_[state_format_name].asString();
}
}
updateLabelAndTooltip(current_format, current_format, fmt::arg("percent", percent),
fmt::arg("percent_exp", percent_exp),
fmt::arg("icon", getIcon(percent)),
fmt::arg("icon_exp", getIcon(percent_exp)));
} else {
event_box_.hide();
}
@@ -69,7 +73,7 @@ auto waybar::modules::Backlight::update() -> void {
ALabel::update();
}
bool waybar::modules::Backlight::handleScroll(GdkEventScroll *e) {
bool waybar::modules::Backlight::handleScroll(GdkEventScroll* e) {
// Check if the user has set a custom command for scrolling
if (config_["on-scroll-up"].isString() || config_["on-scroll-down"].isString()) {
return AModule::handleScroll(e);
@@ -112,6 +116,14 @@ bool waybar::modules::Backlight::handleScroll(GdkEventScroll *e) {
step = config_["scroll-step"].asDouble();
}
double min_brightness = 0;
if (config_["min-brightness"].isDouble()) {
min_brightness = config_["min-brightness"].asDouble();
}
if (backend.get_scaled_brightness(preferred_device_) <= min_brightness &&
ct == util::ChangeType::Decrease) {
return true;
}
backend.set_brightness(preferred_device_, ct, step);
return true;
+244 -113
View File
@@ -1,31 +1,49 @@
#include "modules/battery.hpp"
#include <algorithm>
#include <cctype>
#include <cmath>
#include <cstdio>
#include <string>
#include "util/command.hpp"
#if defined(__FreeBSD__)
#include <sys/sysctl.h>
#endif
#include <libudev.h>
#include <poll.h>
#include <spdlog/spdlog.h>
#include <sys/signalfd.h>
#include <iostream>
waybar::modules::Battery::Battery(const std::string& id, const Bar& bar, const Json::Value& config)
: ALabel(config, "battery", id, "{capacity}%", 60), bar_(bar) {
: ALabel(config, "battery", id, "{capacity}%", 60), last_event_(""), bar_(bar) {
#if defined(__linux__)
battery_watch_fd_ = inotify_init1(IN_CLOEXEC);
if (battery_watch_fd_ == -1) {
throw std::runtime_error("Unable to listen batteries.");
}
global_watch_fd_ = inotify_init1(IN_CLOEXEC);
if (global_watch_fd_ == -1) {
throw std::runtime_error("Unable to listen batteries.");
udev_ = std::unique_ptr<udev, util::UdevDeleter>(udev_new());
if (udev_ == nullptr) {
throw std::runtime_error("udev_new failed");
}
// Watch the directory for any added or removed batteries
global_watch = inotify_add_watch(global_watch_fd_, data_dir_.c_str(), IN_CREATE | IN_DELETE);
if (global_watch < 0) {
throw std::runtime_error("Could not watch for battery plug/unplug");
mon_ = std::unique_ptr<udev_monitor, util::UdevMonitorDeleter>(
udev_monitor_new_from_netlink(udev_.get(), "kernel"));
if (mon_ == nullptr) {
throw std::runtime_error("udev monitor new failed");
}
if (udev_monitor_filter_add_match_subsystem_devtype(mon_.get(), "power_supply", nullptr) < 0) {
throw std::runtime_error("udev failed to add monitor filter");
}
udev_monitor_enable_receiving(mon_.get());
if (config_["smooth-power"].isBool()) {
smoothPowerEnable_ = config_["smooth-power"].asBool();
if (smoothPowerEnable_ && config_["smooth-power-time-constant"].isNumeric())
time_constant_s_ = std::max(1.0, config_["smooth-power-time-constant"].asDouble());
}
if (config_["weighted-average"].isBool()) weightedAverage_ = config_["weighted-average"].asBool();
#endif
spdlog::debug("battery: worker interval is {}", interval_.count());
worker();
}
@@ -33,11 +51,6 @@ waybar::modules::Battery::~Battery() {
#if defined(__linux__)
std::lock_guard<std::mutex> guard(battery_list_mutex_);
if (global_watch >= 0) {
inotify_rm_watch(global_watch_fd_, global_watch);
}
close(global_watch_fd_);
for (auto it = batteries_.cbegin(), next_it = it; it != batteries_.cend(); it = next_it) {
++next_it;
auto watch_id = (*it).second;
@@ -74,12 +87,18 @@ void waybar::modules::Battery::worker() {
dp.emit();
};
thread_battery_update_ = [this] {
struct inotify_event event = {0};
int nbytes = read(global_watch_fd_, &event, sizeof(event));
if (nbytes != sizeof(event) || event.mask & IN_IGNORED) {
poll_fds_[0].revents = 0;
poll_fds_[0].events = POLLIN;
poll_fds_[0].fd = udev_monitor_get_fd(mon_.get());
int ret = poll(poll_fds_.data(), poll_fds_.size(), -1);
if (ret < 0) {
thread_.stop();
return;
}
if ((poll_fds_[0].revents & POLLIN) != 0) {
signalfd_siginfo signal_info;
read(poll_fds_[0].fd, &signal_info, sizeof(signal_info));
}
refreshBatteries();
dp.emit();
};
@@ -106,17 +125,16 @@ void waybar::modules::Battery::refreshBatteries() {
if (((bat_defined && dir_name == config_["bat"].asString()) || !bat_defined) &&
(fs::exists(node.path() / "capacity") || fs::exists(node.path() / "charge_now")) &&
fs::exists(node.path() / "uevent") &&
(fs::exists(node.path() / "status") || bat_compatibility) &&
fs::exists(node.path() / "type")) {
(fs::exists(node.path() / "status") || bat_compatibility)) {
std::string type;
std::ifstream(node.path() / "type") >> type;
if (!type.compare("Battery")) {
if (std::ifstream{node.path() / "type"} >> type && !type.compare("Battery")) {
// Ignore non-system power supplies unless explicitly requested
if (!bat_defined && fs::exists(node.path() / "scope")) {
if (!bat_defined) {
std::string scope;
std::ifstream(node.path() / "scope") >> scope;
if (g_ascii_strcasecmp(scope.data(), "device") == 0) {
// for hotplug-in device, access it is always unstable because you may remove the
// device anytime so just allow failure happen and do nothing
if (std::ifstream{node.path() / "scope"} >> scope &&
g_ascii_strcasecmp(scope.data(), "device") == 0) {
continue;
}
}
@@ -128,7 +146,9 @@ void waybar::modules::Battery::refreshBatteries() {
auto event_path = (node.path() / "uevent");
auto wd = inotify_add_watch(battery_watch_fd_, event_path.c_str(), IN_ACCESS);
if (wd < 0) {
throw std::runtime_error("Could not watch events for " + node.path().string());
spdlog::warn("Could not watch events for {} (device may have been removed)",
node.path().string());
continue;
}
batteries_[node.path()] = wd;
}
@@ -141,7 +161,7 @@ void waybar::modules::Battery::refreshBatteries() {
}
}
} catch (fs::filesystem_error& e) {
throw std::runtime_error(e.what());
spdlog::warn("Battery directory tracking failed: {}", e.what());
}
if (warnFirstTime_ && batteries_.empty()) {
if (config_["bat"].isString()) {
@@ -181,7 +201,8 @@ static bool status_gt(const std::string& a, const std::string& b) {
return false;
}
const std::tuple<uint8_t, float, std::string, float> waybar::modules::Battery::getInfos() {
std::tuple<uint8_t, float, std::string, float, uint16_t, float>
waybar::modules::Battery::getInfos() {
std::lock_guard<std::mutex> guard(battery_list_mutex_);
try {
@@ -234,7 +255,7 @@ const std::tuple<uint8_t, float, std::string, float> waybar::modules::Battery::g
}
// spdlog::info("{} {} {} {}", capacity,time,status,rate);
return {capacity, time / 60.0, status, rate};
return {capacity, time / 60.0, status, rate, 0, 0.0F};
#elif defined(__linux__)
uint32_t total_power = 0; // μW
@@ -252,105 +273,161 @@ const std::tuple<uint8_t, float, std::string, float> waybar::modules::Battery::g
uint32_t time_to_full_now = 0;
bool time_to_full_now_exists = false;
uint32_t largestDesignCapacity = 0;
uint16_t mainBatCycleCount = 0;
float mainBatHealthPercent = 0.0F;
std::string status = "Unknown";
for (auto const& item : batteries_) {
auto bat = item.first;
std::string _status;
/* Check for adapter status if battery is not available */
if (!std::ifstream(bat / "status")) {
std::getline(std::ifstream(adapter_ / "status"), _status);
} else {
std::getline(std::ifstream(bat / "status"), _status);
{
std::ifstream f{bat / "status"};
if (!std::getline(f, _status)) {
std::getline(std::ifstream(adapter_ / "status"), _status);
}
}
// Some battery will report current and charge in μA/μAh.
// Scale these by the voltage to get μW/μWh.
uint32_t capacity = 0;
bool capacity_exists = false;
if (fs::exists(bat / "capacity")) {
capacity_exists = true;
std::ifstream(bat / "capacity") >> capacity;
}
uint32_t current_now = 0;
int32_t _current_now_int = 0;
bool current_now_exists = false;
if (fs::exists(bat / "current_now")) {
current_now_exists = true;
std::ifstream(bat / "current_now") >> current_now;
} else if (fs::exists(bat / "current_avg")) {
current_now_exists = true;
std::ifstream(bat / "current_avg") >> current_now;
if (std::ifstream current_now_f{bat / "current_now"}) {
if (current_now_f >> _current_now_int) {
current_now_exists = true;
}
} else if (std::ifstream current_avg_f{bat / "current_avg"}) {
if (current_avg_f >> _current_now_int) {
current_now_exists = true;
}
}
// Documentation ABI allows a negative value when discharging, positive
// value when charging.
current_now = std::abs(_current_now_int);
if (std::ifstream f{bat / "time_to_empty_now"}) {
if (f >> time_to_empty_now) {
time_to_empty_now_exists = true;
}
}
if (fs::exists(bat / "time_to_empty_now")) {
time_to_empty_now_exists = true;
std::ifstream(bat / "time_to_empty_now") >> time_to_empty_now;
}
if (fs::exists(bat / "time_to_full_now")) {
time_to_full_now_exists = true;
std::ifstream(bat / "time_to_full_now") >> time_to_full_now;
if (std::ifstream f{bat / "time_to_full_now"}) {
if (f >> time_to_full_now) {
time_to_full_now_exists = true;
}
}
uint32_t voltage_now = 0;
bool voltage_now_exists = false;
if (fs::exists(bat / "voltage_now")) {
voltage_now_exists = true;
std::ifstream(bat / "voltage_now") >> voltage_now;
} else if (fs::exists(bat / "voltage_avg")) {
voltage_now_exists = true;
std::ifstream(bat / "voltage_avg") >> voltage_now;
if (std::ifstream voltage_now_f{bat / "voltage_now"}) {
if (voltage_now_f >> voltage_now) {
voltage_now_exists = true;
}
} else if (std::ifstream voltage_avg_f{bat / "voltage_avg"}) {
if (voltage_avg_f >> voltage_now) {
voltage_now_exists = true;
}
}
uint32_t charge_full = 0;
bool charge_full_exists = false;
if (fs::exists(bat / "charge_full")) {
charge_full_exists = true;
std::ifstream(bat / "charge_full") >> charge_full;
if (std::ifstream f{bat / "charge_full"}) {
if (f >> charge_full) {
charge_full_exists = true;
}
}
uint32_t charge_full_design = 0;
bool charge_full_design_exists = false;
if (fs::exists(bat / "charge_full_design")) {
charge_full_design_exists = true;
std::ifstream(bat / "charge_full_design") >> charge_full_design;
if (std::ifstream f{bat / "charge_full_design"}) {
if (f >> charge_full_design) {
charge_full_design_exists = true;
}
}
uint32_t charge_now = 0;
bool charge_now_exists = false;
if (fs::exists(bat / "charge_now")) {
charge_now_exists = true;
std::ifstream(bat / "charge_now") >> charge_now;
if (std::ifstream f{bat / "charge_now"}) {
if (f >> charge_now) {
charge_now_exists = true;
}
}
uint32_t power_now = 0;
int32_t _power_now_int = 0;
bool power_now_exists = false;
if (fs::exists(bat / "power_now")) {
power_now_exists = true;
std::ifstream(bat / "power_now") >> power_now;
if (std::ifstream f{bat / "power_now"}) {
if (f >> _power_now_int) {
power_now_exists = true;
}
}
// Some drivers (example: Qualcomm) exposes use a negative value when
// discharging, positive value when charging.
power_now = std::abs(_power_now_int);
uint32_t energy_now = 0;
bool energy_now_exists = false;
if (fs::exists(bat / "energy_now")) {
energy_now_exists = true;
std::ifstream(bat / "energy_now") >> energy_now;
if (std::ifstream f{bat / "energy_now"}) {
if (f >> energy_now) {
energy_now_exists = true;
}
}
uint32_t energy_full = 0;
bool energy_full_exists = false;
if (fs::exists(bat / "energy_full")) {
energy_full_exists = true;
std::ifstream(bat / "energy_full") >> energy_full;
if (std::ifstream f{bat / "energy_full"}) {
if (f >> energy_full) {
energy_full_exists = true;
}
}
uint32_t energy_full_design = 0;
bool energy_full_design_exists = false;
if (fs::exists(bat / "energy_full_design")) {
energy_full_design_exists = true;
std::ifstream(bat / "energy_full_design") >> energy_full_design;
if (std::ifstream f{bat / "energy_full_design"}) {
if (f >> energy_full_design) {
energy_full_design_exists = true;
}
}
uint16_t cycleCount = 0;
if (std::ifstream f{bat / "cycle_count"}) {
f >> cycleCount;
}
if (charge_full_design >= largestDesignCapacity) {
largestDesignCapacity = charge_full_design;
if (cycleCount > mainBatCycleCount) {
mainBatCycleCount = cycleCount;
}
if (charge_full_exists && charge_full_design_exists) {
float batHealthPercent = ((float)charge_full / charge_full_design) * 100;
if (mainBatHealthPercent == 0.0F || batHealthPercent < mainBatHealthPercent) {
mainBatHealthPercent = batHealthPercent;
}
} else if (energy_full_exists && energy_full_design_exists) {
float batHealthPercent = ((float)energy_full / energy_full_design) * 100;
if (mainBatHealthPercent == 0.0F || batHealthPercent < mainBatHealthPercent) {
mainBatHealthPercent = batHealthPercent;
}
}
}
uint32_t capacity = 0;
bool capacity_exists = false;
if (charge_now_exists && charge_full_exists && charge_full != 0) {
capacity_exists = true;
capacity = 100 * (uint64_t)charge_now / (uint64_t)charge_full;
} else if (energy_now_exists && energy_full_exists && energy_full != 0) {
capacity_exists = true;
capacity = 100 * (uint64_t)energy_now / (uint64_t)energy_full;
} else if (std::ifstream f{bat / "capacity"}) {
if (f >> capacity) {
capacity_exists = true;
}
}
if (!voltage_now_exists) {
@@ -393,13 +470,7 @@ const std::tuple<uint8_t, float, std::string, float> waybar::modules::Battery::g
}
if (!capacity_exists) {
if (charge_now_exists && charge_full_exists && charge_full != 0) {
capacity_exists = true;
capacity = 100 * (uint64_t)charge_now / (uint64_t)charge_full;
} else if (energy_now_exists && energy_full_exists && energy_full != 0) {
capacity_exists = true;
capacity = 100 * (uint64_t)energy_now / (uint64_t)energy_full;
} else if (charge_now_exists && energy_full_exists && voltage_now_exists) {
if (charge_now_exists && energy_full_exists && voltage_now_exists) {
if (!charge_full_exists && voltage_now != 0) {
charge_full_exists = true;
charge_full = 1000000 * (uint64_t)energy_full / (uint64_t)voltage_now;
@@ -514,11 +585,31 @@ const std::tuple<uint8_t, float, std::string, float> waybar::modules::Battery::g
if (online && current_status != "Discharging") status = "Plugged";
}
if (total_energy_exists && total_power_exists && total_power != 0) {
if (!smoothPowerEnable_) {
smooth_power_ = total_power;
} else {
if (status != old_status_raw_) {
smooth_power_ = total_power;
last_t_ = std::chrono::steady_clock::now();
} else {
std::chrono::steady_clock::time_point now = std::chrono::steady_clock::now();
double dt_s =
std::chrono::duration_cast<std::chrono::duration<double> >(now - last_t_).count();
smooth_power_ = smooth_power_ + ((1 - std::exp(-dt_s / time_constant_s_)) *
(total_power - smooth_power_));
last_t_ = now;
}
old_status_raw_ = status;
}
}
float time_remaining{0.0f};
if (status == "Discharging" && time_to_empty_now_exists) {
if (time_to_empty_now != 0) time_remaining = (float)time_to_empty_now / 3600.0f;
} else if (status == "Discharging" && total_power_exists && total_energy_exists) {
if (total_power != 0) time_remaining = (float)total_energy / total_power;
if (smooth_power_ != 0) time_remaining = (float)total_energy / smooth_power_;
} else if (status == "Charging" && time_to_full_now_exists) {
if (time_to_full_now_exists && (time_to_full_now != 0))
time_remaining = -(float)time_to_full_now / 3600.0f;
@@ -536,17 +627,17 @@ const std::tuple<uint8_t, float, std::string, float> waybar::modules::Battery::g
float calculated_capacity{0.0f};
if (total_capacity_exists) {
if (total_capacity > 0.0f)
if (total_capacity > 0.0f) {
calculated_capacity = (float)total_capacity / batteries_.size();
else if (total_energy_full_exists && total_energy_exists) {
if (total_energy_full > 0.0f)
} else if (total_energy_full_exists && total_energy_exists) {
if (total_energy_full > 0.0f) {
calculated_capacity = ((float)total_energy * 100.0f / (float)total_energy_full);
}
}
}
// Handle weighted-average
if ((config_["weighted-average"].isBool() ? config_["weighted-average"].asBool() : false) &&
total_energy_exists && total_energy_full_exists) {
if (weightedAverage_ && total_energy_exists && total_energy_full_exists) {
if (total_energy_full > 0.0f)
calculated_capacity = ((float)total_energy * 100.0f / (float)total_energy_full);
}
@@ -573,11 +664,12 @@ const std::tuple<uint8_t, float, std::string, float> waybar::modules::Battery::g
// still charging but not yet done
if (cap == 100 && status == "Charging") status = "Full";
return {cap, time_remaining, status, total_power / 1e6};
return {
cap, time_remaining, status, total_power / 1e6, mainBatCycleCount, mainBatHealthPercent};
#endif
} catch (const std::exception& e) {
spdlog::error("Battery: {}", e.what());
return {0, 0, "Unknown", 0};
return {0, 0, "Unknown", 0, 0, 0.0f};
}
}
@@ -633,24 +725,36 @@ auto waybar::modules::Battery::update() -> void {
return;
}
#endif
auto [capacity, time_remaining, status, power] = getInfos();
auto [capacity, time_remaining, status, power, cycles, health] = getInfos();
if (status == "Unknown") {
status = getAdapterStatus(capacity);
}
auto status_pretty = status;
// Transform to lowercase and replace space with dash
std::transform(status.begin(), status.end(), status.begin(),
[](char ch) { return ch == ' ' ? '-' : std::tolower(ch); });
auto format = format_;
auto state = getState(capacity, true);
bool adapter_online = false;
if (!adapter_.empty()) {
std::ifstream(adapter_ / "online") >> adapter_online;
}
if (config_["full-at-plugged"].asBool() && status == "Full" && adapter_online) {
status = "Plugged";
}
auto status_pretty = status;
// Transform to lowercase and replace space with dash
std::ranges::transform(status.begin(), status.end(), status.begin(),
[](char ch) { return ch == ' ' ? '-' : std::tolower(ch); });
auto format = format_;
processEvents(state, status, capacity);
setBarClass(state);
auto time_remaining_formatted = formatTimeRemaining(time_remaining);
if (tooltipEnabled()) {
std::string tooltip_text_default;
std::string tooltip_format = "{timeTo}";
if (time_remaining != 0) {
std::string time_to = std::string("Time to ") + ((time_remaining > 0) ? "empty" : "full");
tooltip_text_default = time_to + ": " + time_remaining_formatted;
if (time_remaining > 0) {
tooltip_text_default = std::string("Empty in ") + time_remaining_formatted;
} else {
tooltip_text_default = std::string("Full in ") + time_remaining_formatted;
}
} else {
tooltip_text_default = status_pretty;
}
@@ -663,10 +767,11 @@ auto waybar::modules::Battery::update() -> void {
} else if (config_["tooltip-format"].isString()) {
tooltip_format = config_["tooltip-format"].asString();
}
label_.set_tooltip_text(fmt::format(fmt::runtime(tooltip_format),
fmt::arg("timeTo", tooltip_text_default),
fmt::arg("power", power), fmt::arg("capacity", capacity),
fmt::arg("time", time_remaining_formatted)));
setTooltipMarkup(
fmt::format(fmt::runtime(tooltip_format), fmt::arg("timeTo", tooltip_text_default),
fmt::arg("power", power), fmt::arg("capacity", capacity),
fmt::arg("time", time_remaining_formatted), fmt::arg("cycles", cycles),
fmt::arg("health", fmt::format("{:.3}", health))));
}
if (!old_status_.empty()) {
label_.get_style_context()->remove_class(old_status_);
@@ -685,9 +790,10 @@ auto waybar::modules::Battery::update() -> void {
} else {
event_box_.show();
auto icons = std::vector<std::string>{status + "-" + state, status, state};
label_.set_markup(fmt::format(
setLabelMarkup(fmt::format(
fmt::runtime(format), fmt::arg("capacity", capacity), fmt::arg("power", power),
fmt::arg("icon", getIcon(capacity, icons)), fmt::arg("time", time_remaining_formatted)));
fmt::arg("icon", getIcon(capacity, icons)), fmt::arg("time", time_remaining_formatted),
fmt::arg("cycles", cycles), fmt::arg("health", fmt::format("{:.3}", health))));
}
// Call parent update
ALabel::update();
@@ -727,3 +833,28 @@ void waybar::modules::Battery::setBarClass(std::string& state) {
bar_.window.get_style_context()->add_class(new_class);
}
}
void waybar::modules::Battery::processEvents(std::string& state, std::string& status,
uint8_t capacity) {
// There are no events specified, skip
auto events = config_["events"];
if (!events.isObject() || events.empty()) {
return;
}
auto exec = [](Json::Value const& event) {
if (!event.isString()) return;
if (auto command = event.asString(); !command.empty()) {
util::command::exec(command, "");
}
};
std::string status_name = status == "discharging" ? "on-discharging" : "on-charging";
std::string event_name = status_name + '-' + (state.empty() ? std::to_string(capacity) : state);
if (last_event_ != event_name) {
spdlog::debug("battery: triggering event {}", event_name);
exec(events[event_name]);
if (!last_event_.empty() && last_event_[3] != event_name[3]) {
exec(events[status_name]);
}
last_event_ = event_name;
}
}
+223 -18
View File
@@ -83,6 +83,68 @@ auto getUcharProperty(GDBusProxy* proxy, const char* property_name) -> unsigned
return 0;
}
auto isChildPath(const std::string& child, const std::string& parent) -> bool {
return child.starts_with(parent);
}
auto readBatteryCharacteristicValue(GDBusProxy* proxy_char) -> std::optional<unsigned char> {
GVariantBuilder builder;
g_variant_builder_init(&builder, G_VARIANT_TYPE("a{sv}"));
GError* error = nullptr;
GVariant* gvar =
g_dbus_proxy_call_sync(proxy_char, "ReadValue", g_variant_new("(a{sv})", &builder),
G_DBUS_CALL_FLAGS_NONE, -1, nullptr, &error);
if (error != nullptr) {
g_error_free(error);
return std::nullopt;
}
if (gvar == nullptr) {
return std::nullopt;
}
GVariant* value_array = g_variant_get_child_value(gvar, 0);
gsize n_elements;
const auto* data = static_cast<const guchar*>(
g_variant_get_fixed_array(value_array, &n_elements, sizeof(guchar)));
std::optional<unsigned char> result;
if (data != nullptr && n_elements > 0) {
result = data[0];
}
g_variant_unref(value_array);
g_variant_unref(gvar);
return result;
}
auto hasUserDescriptionDescriptor(GList* objects, const std::string& char_path,
const std::string& user_description_uuid) -> bool {
for (GList* n = objects; n != nullptr; n = n->next) {
GDBusObject* desc_object = G_DBUS_OBJECT(n->data);
std::string desc_path = g_dbus_object_get_object_path(desc_object);
if (!isChildPath(desc_path, char_path)) {
continue;
}
GDBusProxy* proxy_desc =
G_DBUS_PROXY(g_dbus_object_get_interface(desc_object, "org.bluez.GattDescriptor1"));
if (proxy_desc == nullptr) {
continue;
}
auto desc_uuid = getOptionalStringProperty(proxy_desc, "UUID");
g_object_unref(proxy_desc);
if (desc_uuid.has_value() &&
desc_uuid.value().find(user_description_uuid) != std::string::npos) {
return true;
}
}
return false;
}
} // namespace
waybar::modules::Bluetooth::Bluetooth(const std::string& id, const Json::Value& config)
@@ -98,30 +160,33 @@ waybar::modules::Bluetooth::Bluetooth(const std::string& id, const Json::Value&
std::back_inserter(device_preference_), [](auto x) { return x.asString(); });
}
// NOTE: assumption made that the controller that is selected stays unchanged
// for duration of the module
if (cur_controller_ = findCurController(); !cur_controller_) {
if (config_["controller-alias"].isString()) {
spdlog::error("findCurController() failed: no bluetooth controller found with alias '{}'",
config_["controller-alias"].asString());
spdlog::warn("no bluetooth controller found with alias '{}'",
config_["controller-alias"].asString());
} else {
spdlog::error("findCurController() failed: no bluetooth controller found");
spdlog::warn("no bluetooth controller found");
}
update();
} else {
// These calls only make sense if a controller could be found
// This call only make sense if a controller could be found
findConnectedDevices(cur_controller_->path, connected_devices_);
}
if (manager_) {
g_signal_connect(manager_.get(), "object-added", G_CALLBACK(onObjectAdded), this);
g_signal_connect(manager_.get(), "object-removed", G_CALLBACK(onObjectRemoved), this);
g_signal_connect(manager_.get(), "interface-proxy-properties-changed",
G_CALLBACK(onInterfaceProxyPropertiesChanged), this);
g_signal_connect(manager_.get(), "interface-added", G_CALLBACK(onInterfaceAddedOrRemoved),
this);
g_signal_connect(manager_.get(), "interface-removed", G_CALLBACK(onInterfaceAddedOrRemoved),
this);
}
#ifdef WANT_RFKILL
rfkill_.on_update.connect(sigc::hide(sigc::mem_fun(*this, &Bluetooth::update)));
rfkill_.on_update.connect(sigc::hide(sigc::mem_fun(*this, &Bluetooth::update)));
#endif
}
dp.emit();
}
@@ -219,7 +284,7 @@ auto waybar::modules::Bluetooth::update() -> void {
event_box_.hide();
} else {
event_box_.show();
label_.set_markup(fmt::format(
setLabelMarkup(fmt::format(
fmt::runtime(format_), fmt::arg("status", state_),
fmt::arg("num_connections", connected_devices_.size()),
fmt::arg("controller_address", cur_controller_ ? cur_controller_->address : "null"),
@@ -229,8 +294,9 @@ auto waybar::modules::Bluetooth::update() -> void {
fmt::arg("device_address", cur_focussed_device_.address),
fmt::arg("device_address_type", cur_focussed_device_.address_type),
fmt::arg("device_alias", cur_focussed_device_.alias), fmt::arg("icon", icon_label),
fmt::arg("device_battery_percentage",
cur_focussed_device_.battery_percentage.value_or(0))));
fmt::arg("device_battery_percentage", cur_focussed_device_.battery_percentage.value_or(0)),
fmt::arg("device_battery_percentage_peripheral",
cur_focussed_device_.battery_percentage_peripheral.value_or(0))));
}
if (tooltipEnabled()) {
@@ -255,7 +321,9 @@ auto waybar::modules::Bluetooth::update() -> void {
fmt::runtime(enumerate_format), fmt::arg("device_address", dev.address),
fmt::arg("device_address_type", dev.address_type),
fmt::arg("device_alias", dev.alias), fmt::arg("icon", enumerate_icon),
fmt::arg("device_battery_percentage", dev.battery_percentage.value_or(0)));
fmt::arg("device_battery_percentage", dev.battery_percentage.value_or(0)),
fmt::arg("device_battery_percentage_peripheral",
dev.battery_percentage_peripheral.value_or(0)));
}
}
device_enumerate_ = ss.str();
@@ -264,7 +332,7 @@ auto waybar::modules::Bluetooth::update() -> void {
device_enumerate_.erase(0, 1);
}
}
label_.set_tooltip_text(fmt::format(
setTooltipMarkup(fmt::format(
fmt::runtime(tooltip_format), fmt::arg("status", state_),
fmt::arg("num_connections", connected_devices_.size()),
fmt::arg("controller_address", cur_controller_ ? cur_controller_->address : "null"),
@@ -275,6 +343,8 @@ auto waybar::modules::Bluetooth::update() -> void {
fmt::arg("device_address_type", cur_focussed_device_.address_type),
fmt::arg("device_alias", cur_focussed_device_.alias), fmt::arg("icon", icon_tooltip),
fmt::arg("device_battery_percentage", cur_focussed_device_.battery_percentage.value_or(0)),
fmt::arg("device_battery_percentage_peripheral",
cur_focussed_device_.battery_percentage_peripheral.value_or(0)),
fmt::arg("device_enumerate", device_enumerate_)));
}
@@ -282,6 +352,46 @@ auto waybar::modules::Bluetooth::update() -> void {
ALabel::update();
}
auto waybar::modules::Bluetooth::onObjectAdded(GDBusObjectManager* manager, GDBusObject* object,
gpointer user_data) -> void {
ControllerInfo info;
Bluetooth* bt = static_cast<Bluetooth*>(user_data);
if (!bt->cur_controller_.has_value() && bt->getControllerProperties(object, info) &&
(!bt->config_["controller-alias"].isString() ||
bt->config_["controller-alias"].asString() == info.alias)) {
bt->cur_controller_ = std::move(info);
bt->dp.emit();
}
}
auto waybar::modules::Bluetooth::onObjectRemoved(GDBusObjectManager* manager, GDBusObject* object,
gpointer user_data) -> void {
Bluetooth* bt = static_cast<Bluetooth*>(user_data);
GDBusProxy* proxy_controller;
if (!bt->cur_controller_.has_value()) {
return;
}
proxy_controller = G_DBUS_PROXY(g_dbus_object_get_interface(object, "org.bluez.Adapter1"));
if (proxy_controller != NULL) {
std::string object_path = g_dbus_object_get_object_path(object);
if (object_path == bt->cur_controller_->path) {
bt->cur_controller_ = bt->findCurController();
if (bt->cur_controller_.has_value()) {
bt->connected_devices_.clear();
bt->findConnectedDevices(bt->cur_controller_->path, bt->connected_devices_);
}
bt->dp.emit();
}
g_object_unref(proxy_controller);
}
}
// NOTE: only for when the org.bluez.Battery1 interface is added/removed after/before a device is
// connected/disconnected
auto waybar::modules::Bluetooth::onInterfaceAddedOrRemoved(GDBusObjectManager* manager,
@@ -292,11 +402,13 @@ auto waybar::modules::Bluetooth::onInterfaceAddedOrRemoved(GDBusObjectManager* m
std::string object_path = g_dbus_proxy_get_object_path(G_DBUS_PROXY(interface));
if (interface_name == "org.bluez.Battery1") {
Bluetooth* bt = static_cast<Bluetooth*>(user_data);
auto device = std::find_if(bt->connected_devices_.begin(), bt->connected_devices_.end(),
[object_path](auto d) { return d.path == object_path; });
if (device != bt->connected_devices_.end()) {
device->battery_percentage = bt->getDeviceBatteryPercentage(object);
bt->dp.emit();
if (bt->cur_controller_.has_value()) {
auto device = std::find_if(bt->connected_devices_.begin(), bt->connected_devices_.end(),
[object_path](auto d) { return d.path == object_path; });
if (device != bt->connected_devices_.end()) {
device->battery_percentage = bt->getDeviceBatteryPercentage(object);
bt->dp.emit();
}
}
}
}
@@ -309,6 +421,11 @@ auto waybar::modules::Bluetooth::onInterfaceProxyPropertiesChanged(
std::string object_path = g_dbus_object_get_object_path(G_DBUS_OBJECT(object_proxy));
Bluetooth* bt = static_cast<Bluetooth*>(user_data);
if (!bt->cur_controller_.has_value()) {
return;
}
if (interface_name == "org.bluez.Adapter1") {
if (object_path == bt->cur_controller_->path) {
bt->getControllerProperties(G_DBUS_OBJECT(object_proxy), *bt->cur_controller_);
@@ -348,6 +465,85 @@ auto waybar::modules::Bluetooth::getDeviceBatteryPercentage(GDBusObject* object)
return std::nullopt;
}
auto waybar::modules::Bluetooth::getDeviceGattBatteryLevels(
GDBusObject* device_object, std::optional<unsigned char>& central_battery,
std::optional<unsigned char>& peripheral_battery) -> void {
const std::string BATTERY_SERVICE_UUID = "0000180f-0000-1000-8000-00805f9b34fb";
const std::string BATTERY_LEVEL_UUID = "00002a19-0000-1000-8000-00805f9b34fb";
const std::string USER_DESCRIPTION_UUID = "00002901-0000-1000-8000-00805f9b34fb";
GList* objects = g_dbus_object_manager_get_objects(manager_.get());
std::string device_path = g_dbus_object_get_object_path(device_object);
for (GList* l = objects; l != nullptr; l = l->next) {
GDBusObject* service_object = G_DBUS_OBJECT(l->data);
std::string service_path = g_dbus_object_get_object_path(service_object);
if (!isChildPath(service_path, device_path)) {
continue;
}
GDBusProxy* proxy_service =
G_DBUS_PROXY(g_dbus_object_get_interface(service_object, "org.bluez.GattService1"));
if (proxy_service == nullptr) {
continue;
}
auto service_uuid = getOptionalStringProperty(proxy_service, "UUID");
g_object_unref(proxy_service);
if (!service_uuid.has_value() ||
service_uuid.value().find(BATTERY_SERVICE_UUID) == std::string::npos) {
continue;
}
processBatteryServiceCharacteristics(objects, service_path, BATTERY_LEVEL_UUID,
USER_DESCRIPTION_UUID, central_battery,
peripheral_battery);
}
g_list_free_full(objects, g_object_unref);
}
auto waybar::modules::Bluetooth::processBatteryServiceCharacteristics(
GList* objects, const std::string& service_path, const std::string& battery_level_uuid,
const std::string& user_description_uuid, std::optional<unsigned char>& central_battery,
std::optional<unsigned char>& peripheral_battery) -> void {
for (GList* m = objects; m != nullptr; m = m->next) {
GDBusObject* char_object = G_DBUS_OBJECT(m->data);
std::string char_path = g_dbus_object_get_object_path(char_object);
if (!isChildPath(char_path, service_path)) {
continue;
}
GDBusProxy* proxy_char =
G_DBUS_PROXY(g_dbus_object_get_interface(char_object, "org.bluez.GattCharacteristic1"));
if (proxy_char == nullptr) {
continue;
}
auto char_uuid = getOptionalStringProperty(proxy_char, "UUID");
if (!char_uuid.has_value() || char_uuid.value().find(battery_level_uuid) == std::string::npos) {
g_object_unref(proxy_char);
continue;
}
auto battery_value = readBatteryCharacteristicValue(proxy_char);
g_object_unref(proxy_char);
if (!battery_value.has_value()) {
continue;
}
if (hasUserDescriptionDescriptor(objects, char_path, user_description_uuid)) {
peripheral_battery = battery_value.value();
} else {
central_battery = battery_value.value();
}
}
}
auto waybar::modules::Bluetooth::getDeviceProperties(GDBusObject* object, DeviceInfo& device_info)
-> bool {
GDBusProxy* proxy_device = G_DBUS_PROXY(g_dbus_object_get_interface(object, "org.bluez.Device1"));
@@ -368,6 +564,8 @@ auto waybar::modules::Bluetooth::getDeviceProperties(GDBusObject* object, Device
g_object_unref(proxy_device);
device_info.battery_percentage = getDeviceBatteryPercentage(object);
getDeviceGattBatteryLevels(object, device_info.battery_percentage,
device_info.battery_percentage_peripheral);
return true;
}
@@ -399,6 +597,10 @@ auto waybar::modules::Bluetooth::getControllerProperties(GDBusObject* object,
auto waybar::modules::Bluetooth::findCurController() -> std::optional<ControllerInfo> {
std::optional<ControllerInfo> controller_info;
if (!manager_) {
return controller_info;
}
GList* objects = g_dbus_object_manager_get_objects(manager_.get());
for (GList* l = objects; l != NULL; l = l->next) {
GDBusObject* object = G_DBUS_OBJECT(l->data);
@@ -418,6 +620,9 @@ auto waybar::modules::Bluetooth::findCurController() -> std::optional<Controller
auto waybar::modules::Bluetooth::findConnectedDevices(const std::string& cur_controller_path,
std::vector<DeviceInfo>& connected_devices)
-> void {
if (!manager_) {
return;
}
GList* objects = g_dbus_object_manager_get_objects(manager_.get());
for (GList* l = objects; l != NULL; l = l->next) {
GDBusObject* object = G_DBUS_OBJECT(l->data);
-207
View File
@@ -1,207 +0,0 @@
#include "modules/cava.hpp"
#include <spdlog/spdlog.h>
waybar::modules::Cava::Cava(const std::string& id, const Json::Value& config)
: ALabel(config, "cava", id, "{}", 60, false, false, false) {
// Load waybar module config
char cfgPath[PATH_MAX];
cfgPath[0] = '\0';
if (config_["cava_config"].isString()) {
std::string strPath{config_["cava_config"].asString()};
const std::string fnd{"XDG_CONFIG_HOME"};
const std::string::size_type npos{strPath.find("$" + fnd)};
if (npos != std::string::npos) strPath.replace(npos, fnd.length() + 1, getenv(fnd.c_str()));
strcpy(cfgPath, strPath.data());
}
// Load cava config
error_.length = 0;
if (!load_config(cfgPath, &prm_, false, &error_)) {
spdlog::error("Error loading config. {0}", error_.message);
exit(EXIT_FAILURE);
}
// Override cava parameters by the user config
prm_.inAtty = 0;
prm_.output = cava::output_method::OUTPUT_RAW;
strcpy(prm_.data_format, "ascii");
strcpy(prm_.raw_target, "/dev/stdout");
prm_.ascii_range = config_["format-icons"].size() - 1;
prm_.bar_width = 2;
prm_.bar_spacing = 0;
prm_.bar_height = 32;
prm_.bar_width = 1;
prm_.orientation = cava::ORIENT_TOP;
prm_.xaxis = cava::xaxis_scale::NONE;
prm_.mono_opt = cava::AVERAGE;
prm_.autobars = 0;
prm_.gravity = 0;
prm_.integral = 1;
if (config_["framerate"].isInt()) prm_.framerate = config_["framerate"].asInt();
if (config_["autosens"].isInt()) prm_.autosens = config_["autosens"].asInt();
if (config_["sensitivity"].isInt()) prm_.sens = config_["sensitivity"].asInt();
if (config_["bars"].isInt()) prm_.fixedbars = config_["bars"].asInt();
if (config_["lower_cutoff_freq"].isNumeric())
prm_.lower_cut_off = config_["lower_cutoff_freq"].asLargestInt();
if (config_["higher_cutoff_freq"].isNumeric())
prm_.upper_cut_off = config_["higher_cutoff_freq"].asLargestInt();
if (config_["sleep_timer"].isInt()) prm_.sleep_timer = config_["sleep_timer"].asInt();
if (config_["method"].isString())
prm_.input = cava::input_method_by_name(config_["method"].asString().c_str());
if (config_["source"].isString()) prm_.audio_source = config_["source"].asString().data();
if (config_["sample_rate"].isNumeric()) prm_.samplerate = config_["sample_rate"].asLargestInt();
if (config_["sample_bits"].isInt()) prm_.samplebits = config_["sample_bits"].asInt();
if (config_["stereo"].isBool()) prm_.stereo = config_["stereo"].asBool();
if (config_["reverse"].isBool()) prm_.reverse = config_["reverse"].asBool();
if (config_["bar_delimiter"].isInt()) prm_.bar_delim = config_["bar_delimiter"].asInt();
if (config_["monstercat"].isBool()) prm_.monstercat = config_["monstercat"].asBool();
if (config_["waves"].isBool()) prm_.waves = config_["waves"].asBool();
if (config_["noise_reduction"].isDouble())
prm_.noise_reduction = config_["noise_reduction"].asDouble();
if (config_["input_delay"].isInt())
fetch_input_delay_ = std::chrono::seconds(config_["input_delay"].asInt());
if (config_["hide_on_silence"].isBool()) hide_on_silence_ = config_["hide_on_silence"].asBool();
// Make cava parameters configuration
plan_ = new cava::cava_plan{};
audio_raw_.height = prm_.ascii_range;
audio_data_.format = -1;
audio_data_.source = new char[1 + strlen(prm_.audio_source)];
audio_data_.source[0] = '\0';
strcpy(audio_data_.source, prm_.audio_source);
audio_data_.rate = 0;
audio_data_.samples_counter = 0;
audio_data_.channels = 2;
audio_data_.IEEE_FLOAT = 0;
audio_data_.input_buffer_size = BUFFER_SIZE * audio_data_.channels;
audio_data_.cava_buffer_size = audio_data_.input_buffer_size * 8;
audio_data_.cava_in = new double[audio_data_.cava_buffer_size]{0.0};
audio_data_.terminate = 0;
audio_data_.suspendFlag = false;
input_source_ = get_input(&audio_data_, &prm_);
if (!input_source_) {
spdlog::error("cava API didn't provide input audio source method");
exit(EXIT_FAILURE);
}
// Calculate delay for Update() thread
frame_time_milsec_ = std::chrono::milliseconds((int)(1e3 / prm_.framerate));
// Init cava plan, audio_raw structure
audio_raw_init(&audio_data_, &audio_raw_, &prm_, plan_);
if (!plan_) spdlog::error("cava plan is not provided");
audio_raw_.previous_frame[0] = -1; // For first Update() call need to rePaint text message
// Read audio source trough cava API. Cava orginizes this process via infinity loop
thread_fetch_input_ = [this] {
thread_fetch_input_.sleep_for(fetch_input_delay_);
input_source_(&audio_data_);
};
thread_ = [this] {
dp.emit();
thread_.sleep_for(frame_time_milsec_);
};
}
waybar::modules::Cava::~Cava() {
thread_fetch_input_.stop();
thread_.stop();
delete plan_;
plan_ = nullptr;
}
void upThreadDelay(std::chrono::milliseconds& delay, std::chrono::seconds& delta) {
if (delta == std::chrono::seconds{0}) {
delta += std::chrono::seconds{1};
delay += delta;
}
}
void downThreadDelay(std::chrono::milliseconds& delay, std::chrono::seconds& delta) {
if (delta > std::chrono::seconds{0}) {
delay -= delta;
delta -= std::chrono::seconds{1};
}
}
auto waybar::modules::Cava::update() -> void {
if (audio_data_.suspendFlag) return;
silence_ = true;
for (int i{0}; i < audio_data_.input_buffer_size; ++i) {
if (audio_data_.cava_in[i]) {
silence_ = false;
sleep_counter_ = 0;
break;
}
}
if (silence_ && prm_.sleep_timer) {
if (sleep_counter_ <=
(int)(std::chrono::milliseconds(prm_.sleep_timer * 1s) / frame_time_milsec_)) {
++sleep_counter_;
silence_ = false;
}
}
if (!silence_) {
downThreadDelay(frame_time_milsec_, suspend_silence_delay_);
// Process: execute cava
pthread_mutex_lock(&audio_data_.lock);
cava::cava_execute(audio_data_.cava_in, audio_data_.samples_counter, audio_raw_.cava_out,
plan_);
if (audio_data_.samples_counter > 0) audio_data_.samples_counter = 0;
pthread_mutex_unlock(&audio_data_.lock);
// Do transformation under raw data
audio_raw_fetch(&audio_raw_, &prm_, &rePaint_, plan_);
if (rePaint_ == 1) {
text_.clear();
for (int i{0}; i < audio_raw_.number_of_bars; ++i) {
audio_raw_.previous_frame[i] = audio_raw_.bars[i];
text_.append(
getIcon((audio_raw_.bars[i] > prm_.ascii_range) ? prm_.ascii_range : audio_raw_.bars[i],
"", prm_.ascii_range + 1));
if (prm_.bar_delim != 0) text_.push_back(prm_.bar_delim);
}
label_.set_markup(text_);
label_.show();
ALabel::update();
}
} else {
upThreadDelay(frame_time_milsec_, suspend_silence_delay_);
if (hide_on_silence_) label_.hide();
}
}
auto waybar::modules::Cava::doAction(const std::string& name) -> void {
if ((actionMap_[name])) {
(this->*actionMap_[name])();
} else
spdlog::error("Cava. Unsupported action \"{0}\"", name);
}
// Cava actions
void waybar::modules::Cava::pause_resume() {
pthread_mutex_lock(&audio_data_.lock);
if (audio_data_.suspendFlag) {
audio_data_.suspendFlag = false;
pthread_cond_broadcast(&audio_data_.resumeCond);
downThreadDelay(frame_time_milsec_, suspend_silence_delay_);
} else {
audio_data_.suspendFlag = true;
upThreadDelay(frame_time_milsec_, suspend_silence_delay_);
}
pthread_mutex_unlock(&audio_data_.lock);
}
+271
View File
@@ -0,0 +1,271 @@
#include "modules/cava/cavaGLSL.hpp"
#include <spdlog/spdlog.h>
#include <fstream>
waybar::modules::cava::CavaGLSL::CavaGLSL(const std::string& id, const Json::Value& config)
: AModule(config, "cavaGLSL", id, false, false),
backend_{waybar::modules::cava::CavaBackend::inst(config)} {
set_name(name_);
if (config_["hide_on_silence"].isBool()) hide_on_silence_ = config_["hide_on_silence"].asBool();
if (!id.empty()) {
get_style_context()->add_class(id);
}
get_style_context()->add_class(MODULE_CLASS);
set_use_es(true);
// set_auto_render(true);
signal_realize().connect(sigc::mem_fun(*this, &CavaGLSL::onRealize));
signal_render().connect(sigc::mem_fun(*this, &CavaGLSL::onRender), false);
// Get parameters_config struct from the backend
prm_ = *backend_->getPrm();
// Set widget length
int length{0};
if (config_["min-length"].isUInt())
length = config_["min-length"].asUInt();
else if (config_["max-length"].isUInt())
length = config_["max-length"].asUInt();
else
length = prm_.sdl_width;
set_size_request(length, prm_.sdl_height);
// Subscribe for changes
backend_->signal_audio_raw_update().connect(sigc::mem_fun(*this, &CavaGLSL::onUpdate));
// Subscribe for silence
backend_->signal_silence().connect(sigc::mem_fun(*this, &CavaGLSL::onSilence));
event_box_.add(*this);
}
auto waybar::modules::cava::CavaGLSL::onUpdate(const ::cava::audio_raw& input) -> void {
Glib::signal_idle().connect_once([this, input]() {
m_data_ = std::make_shared<::cava::audio_raw>(input);
if (silence_) {
get_style_context()->remove_class("silent");
if (!get_style_context()->has_class("updated")) get_style_context()->add_class("updated");
show();
silence_ = false;
}
queue_render();
});
}
auto waybar::modules::cava::CavaGLSL::onSilence() -> void {
Glib::signal_idle().connect_once([this]() {
if (!silence_) {
if (get_style_context()->has_class("updated")) get_style_context()->remove_class("updated");
if (hide_on_silence_) hide();
silence_ = true;
get_style_context()->add_class("silent");
// Set clear color to black
glClearColor(0.0f, 0.0f, 0.0f, 1.0f);
queue_render();
}
});
}
bool waybar::modules::cava::CavaGLSL::onRender(const Glib::RefPtr<Gdk::GLContext>& context) {
if (!m_data_) return true;
glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT);
glActiveTexture(GL_TEXTURE0);
glBindTexture(GL_TEXTURE_2D, texture_);
glUniform1i(glGetUniformLocation(shaderProgram_, "inputTexture"), 0);
glUniform1fv(uniform_bars_, m_data_->number_of_bars, m_data_->bars_raw);
glUniform1fv(uniform_previous_bars_, m_data_->number_of_bars, m_data_->previous_bars_raw);
glUniform1i(uniform_bars_count_, m_data_->number_of_bars);
++frame_counter;
glUniform1f(uniform_time_, (frame_counter / backend_->getFrameTimeMilsec().count()) / 1e3);
// glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT);
glDrawElements(GL_TRIANGLE_FAN, 4, GL_UNSIGNED_INT, nullptr);
glBindFramebuffer(GL_FRAMEBUFFER, fbo_);
glDrawElements(GL_TRIANGLE_FAN, 4, GL_UNSIGNED_INT, nullptr);
glBindFramebuffer(GL_FRAMEBUFFER, 0);
return true;
}
void waybar::modules::cava::CavaGLSL::onRealize() {
make_current();
initShaders();
initGLSL();
initSurface();
}
struct colors {
uint16_t R;
uint16_t G;
uint16_t B;
};
static void parse_color(char* color_string, struct colors* color) {
if (color_string[0] == '#') {
sscanf(++color_string, "%02hx%02hx%02hx", &color->R, &color->G, &color->B);
}
}
void waybar::modules::cava::CavaGLSL::initGLSL() {
GLint gVertexPos2DLocation{glGetAttribLocation(shaderProgram_, "vertexPosition_modelspace")};
if (gVertexPos2DLocation == -1) {
spdlog::error("{0}. Could not find vertex position shader variable", name_);
}
glClearColor(0.f, 0.f, 0.f, 1.f);
GLfloat vertexData[]{-1.0f, -1.0f, 1.0f, -1.0f, 1.0f, 1.0f, -1.0f, 1.0f};
GLint indexData[]{0, 1, 2, 3};
GLuint gVBO{0};
glGenBuffers(1, &gVBO);
glBindBuffer(GL_ARRAY_BUFFER, gVBO);
glBufferData(GL_ARRAY_BUFFER, 2 * 4 * sizeof(GLfloat), vertexData, GL_STATIC_DRAW);
GLuint gIBO{0};
glGenBuffers(1, &gIBO);
glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, gIBO);
glBufferData(GL_ELEMENT_ARRAY_BUFFER, 4 * sizeof(GLuint), indexData, GL_STATIC_DRAW);
GLuint gVAO{0};
glGenVertexArrays(1, &gVAO);
glBindVertexArray(gVAO);
glEnableVertexAttribArray(gVertexPos2DLocation);
glBindBuffer(GL_ARRAY_BUFFER, gVBO);
glVertexAttribPointer(gVertexPos2DLocation, 2, GL_FLOAT, GL_FALSE, 2 * sizeof(GLfloat), nullptr);
glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, gIBO);
glGenFramebuffers(1, &fbo_);
glBindFramebuffer(GL_FRAMEBUFFER, fbo_);
// Create a texture to attach the framebuffer
glGenTextures(1, &texture_);
glBindTexture(GL_TEXTURE_2D, texture_);
glTexImage2D(GL_TEXTURE_2D, 0, GL_RGBA, prm_.sdl_width, prm_.sdl_height, 0, GL_RGBA,
GL_UNSIGNED_BYTE, nullptr);
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR);
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR);
glFramebufferTexture2D(GL_FRAMEBUFFER, GL_COLOR_ATTACHMENT0, GL_TEXTURE_2D, texture_, 0);
// Check is framebuffer is complete
if (glCheckFramebufferStatus(GL_FRAMEBUFFER) != GL_FRAMEBUFFER_COMPLETE) {
spdlog::error("{0}. Framebuffer not complete", name_);
}
// Unbind the framebuffer
glBindFramebuffer(GL_FRAMEBUFFER, 0);
uniform_bars_ = glGetUniformLocation(shaderProgram_, "bars");
uniform_previous_bars_ = glGetUniformLocation(shaderProgram_, "previous_bars");
uniform_bars_count_ = glGetUniformLocation(shaderProgram_, "bars_count");
uniform_time_ = glGetUniformLocation(shaderProgram_, "shader_time");
GLuint err{glGetError()};
if (err != 0) {
spdlog::error("{0}. Error on initGLSL: {1}", name_, err);
}
}
void waybar::modules::cava::CavaGLSL::initSurface() {
colors color = {0};
GLint uniform_bg_col{glGetUniformLocation(shaderProgram_, "bg_color")};
parse_color(prm_.bcolor, &color);
glUniform3f(uniform_bg_col, (float)color.R / 255.0, (float)color.G / 255.0,
(float)color.B / 255.0);
GLint uniform_fg_col{glGetUniformLocation(shaderProgram_, "fg_color")};
parse_color(prm_.color, &color);
glUniform3f(uniform_fg_col, (float)color.R / 255.0, (float)color.G / 255.0,
(float)color.B / 255.0);
GLint uniform_res{glGetUniformLocation(shaderProgram_, "u_resolution")};
glUniform3f(uniform_res, (float)prm_.sdl_width, (float)prm_.sdl_height, 0.0f);
GLint uniform_bar_width{glGetUniformLocation(shaderProgram_, "bar_width")};
glUniform1i(uniform_bar_width, prm_.bar_width);
GLint uniform_bar_spacing{glGetUniformLocation(shaderProgram_, "bar_spacing")};
glUniform1i(uniform_bar_spacing, prm_.bar_spacing);
GLint uniform_gradient_count{glGetUniformLocation(shaderProgram_, "gradient_count")};
glUniform1i(uniform_gradient_count, prm_.gradient_count);
GLint uniform_gradient_colors{glGetUniformLocation(shaderProgram_, "gradient_colors")};
GLfloat gradient_colors[8][3];
for (int i{0}; i < prm_.gradient_count; ++i) {
parse_color(prm_.gradient_colors[i], &color);
gradient_colors[i][0] = (float)color.R / 255.0;
gradient_colors[i][1] = (float)color.G / 255.0;
gradient_colors[i][2] = (float)color.B / 255.0;
}
glUniform3fv(uniform_gradient_colors, 8, (const GLfloat*)gradient_colors);
glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT);
glDrawElements(GL_TRIANGLE_FAN, 4, GL_UNSIGNED_INT, nullptr);
}
void waybar::modules::cava::CavaGLSL::initShaders() {
shaderProgram_ = glCreateProgram();
GLuint vertexShader{loadShader(prm_.vertex_shader, GL_VERTEX_SHADER)};
GLuint fragmentShader{loadShader(prm_.fragment_shader, GL_FRAGMENT_SHADER)};
glAttachShader(shaderProgram_, vertexShader);
glAttachShader(shaderProgram_, fragmentShader);
glLinkProgram(shaderProgram_);
glDeleteShader(vertexShader);
glDeleteShader(fragmentShader);
// Check for linking errors
GLint success, len;
glGetProgramiv(shaderProgram_, GL_LINK_STATUS, &success);
if (!success) {
glGetProgramiv(shaderProgram_, GL_INFO_LOG_LENGTH, &len);
GLchar* infoLog{(char*)'\0'};
glGetProgramInfoLog(shaderProgram_, len, &len, infoLog);
spdlog::error("{0}. Shader linking error: {1}", name_, infoLog);
}
glReleaseShaderCompiler();
glUseProgram(shaderProgram_);
}
GLuint waybar::modules::cava::CavaGLSL::loadShader(const std::string& fileName, GLenum type) {
spdlog::debug("{0}. loadShader: {1}", name_, fileName);
// Read shader source code from the file
std::ifstream shaderFile{fileName};
if (!shaderFile.is_open()) {
spdlog::error("{0}. Could not open shader file: {1}", name_, fileName);
}
std::ostringstream buffer;
buffer << shaderFile.rdbuf(); // read file content into stringstream
std::string str{buffer.str()};
const char* shaderSource = str.c_str();
shaderFile.close();
GLuint shaderID{glCreateShader(type)};
if (shaderID == 0) spdlog::error("{0}. Error creating shader type: {0}", type);
glShaderSource(shaderID, 1, &shaderSource, nullptr);
glCompileShader(shaderID);
// Check for compilation errors
GLint success, len;
glGetShaderiv(shaderID, GL_COMPILE_STATUS, &success);
if (!success) {
glGetShaderiv(shaderID, GL_INFO_LOG_LENGTH, &len);
GLchar* infoLog{(char*)'\0'};
glGetShaderInfoLog(shaderID, len, nullptr, infoLog);
spdlog::error("{0}. Shader compilation error in {1}: {2}", name_, fileName, infoLog);
}
return shaderID;
}
+60
View File
@@ -0,0 +1,60 @@
#include "modules/cava/cavaRaw.hpp"
#include <spdlog/spdlog.h>
waybar::modules::cava::Cava::Cava(const std::string& id, const Json::Value& config)
: ALabel(config, "cava", id, "{}", 60, false, false, false),
backend_{waybar::modules::cava::CavaBackend::inst(config)} {
if (config_["hide_on_silence"].isBool()) hide_on_silence_ = config_["hide_on_silence"].asBool();
if (config_["format_silent"].isString()) format_silent_ = config_["format_silent"].asString();
ascii_range_ = backend_->getAsciiRange();
backend_->signal_update().connect(sigc::mem_fun(*this, &Cava::onUpdate));
backend_->signal_silence().connect(sigc::mem_fun(*this, &Cava::onSilence));
backend_->Update();
}
auto waybar::modules::cava::Cava::doAction(const std::string& name) -> void {
if ((actionMap_[name])) {
(this->*actionMap_[name])();
} else
spdlog::error("Cava. Unsupported action \"{0}\"", name);
}
// Cava actions
void waybar::modules::cava::Cava::pause_resume() { backend_->doPauseResume(); }
auto waybar::modules::cava::Cava::onUpdate(const std::string& input) -> void {
Glib::signal_idle().connect_once([this, input]() {
if (silence_) {
silence_ = false;
label_.get_style_context()->remove_class("silent");
if (!label_.get_style_context()->has_class("updated"))
label_.get_style_context()->add_class("updated");
}
label_text_.clear();
for (auto& ch : input)
label_text_.append(getIcon((ch > ascii_range_) ? ascii_range_ : ch, "", ascii_range_ + 1));
label_.set_markup(label_text_);
label_.show();
ALabel::update();
});
}
auto waybar::modules::cava::Cava::onSilence() -> void {
Glib::signal_idle().connect_once([this]() {
if (!silence_) {
if (label_.get_style_context()->has_class("updated"))
label_.get_style_context()->remove_class("updated");
if (hide_on_silence_) {
// Clear the label markup before hiding to prevent GTK from rendering a NULL Pango layout
label_.set_markup("");
label_.hide();
} else if (config_["format_silent"].isString())
label_.set_markup(format_silent_);
silence_ = true;
label_.get_style_context()->add_class("silent");
}
});
}
+273
View File
@@ -0,0 +1,273 @@
#include "modules/cava/cava_backend.hpp"
#include <spdlog/spdlog.h>
std::shared_ptr<waybar::modules::cava::CavaBackend> waybar::modules::cava::CavaBackend::inst(
const Json::Value& config) {
static auto* backend = new CavaBackend(config);
static std::shared_ptr<CavaBackend> backend_ptr{backend};
return backend_ptr;
}
waybar::modules::cava::CavaBackend::CavaBackend(const Json::Value& config) : config_(config) {
// Load waybar module config
loadConfig();
// Read audio source trough cava API. Cava orginizes this process via infinity loop
read_thread_ = [this] {
try {
input_source_(&audio_data_);
} catch (const std::runtime_error& e) {
spdlog::warn("Cava backend. Read source error: {0}", e.what());
}
read_thread_.sleep_for(fetch_input_delay_);
loadConfig();
};
// Write outcoming data. Emit signals
out_thread_ = [this] {
doUpdate(false);
out_thread_.sleep_for(frame_time_milsec_);
};
}
waybar::modules::cava::CavaBackend::~CavaBackend() {
out_thread_.stop();
read_thread_.stop();
freeBackend();
}
static bool upThreadDelay(std::chrono::milliseconds& delay, std::chrono::seconds& delta) {
if (delta == std::chrono::seconds{0}) {
delta += std::chrono::seconds{1};
delay += delta;
return true;
}
return false;
}
static bool downThreadDelay(std::chrono::milliseconds& delay, std::chrono::seconds& delta) {
if (delta > std::chrono::seconds{0}) {
delay -= delta;
delta -= std::chrono::seconds{1};
return true;
}
return false;
}
bool waybar::modules::cava::CavaBackend::isSilence() {
for (int i{0}; i < audio_data_.input_buffer_size; ++i) {
if (audio_data_.cava_in[i]) {
return false;
}
}
return true;
}
int waybar::modules::cava::CavaBackend::getAsciiRange() { return prm_.ascii_range; }
// Process: execute cava
void waybar::modules::cava::CavaBackend::invoke() {
pthread_mutex_lock(&audio_data_.lock);
::cava::cava_execute(audio_data_.cava_in, audio_data_.samples_counter, audio_raw_.cava_out,
plan_);
if (audio_data_.samples_counter > 0) audio_data_.samples_counter = 0;
pthread_mutex_unlock(&audio_data_.lock);
}
// Do transformation under raw data
void waybar::modules::cava::CavaBackend::execute() {
invoke();
audio_raw_fetch(&audio_raw_, &prm_, &re_paint_, plan_);
if (re_paint_ == 1) {
output_.clear();
for (int i{0}; i < audio_raw_.number_of_bars; ++i) {
audio_raw_.previous_frame[i] = audio_raw_.bars[i];
output_.push_back(audio_raw_.bars[i]);
if (prm_.bar_delim != 0) output_.push_back(prm_.bar_delim);
}
}
}
void waybar::modules::cava::CavaBackend::doPauseResume() {
pthread_mutex_lock(&audio_data_.lock);
if (audio_data_.suspendFlag) {
audio_data_.suspendFlag = false;
pthread_cond_broadcast(&audio_data_.resumeCond);
downThreadDelay(frame_time_milsec_, suspend_silence_delay_);
} else {
audio_data_.suspendFlag = true;
upThreadDelay(frame_time_milsec_, suspend_silence_delay_);
}
pthread_mutex_unlock(&audio_data_.lock);
Update();
}
waybar::modules::cava::CavaBackend::type_signal_update
waybar::modules::cava::CavaBackend::signal_update() {
return m_signal_update_;
}
waybar::modules::cava::CavaBackend::type_signal_audio_raw_update
waybar::modules::cava::CavaBackend::signal_audio_raw_update() {
return m_signal_audio_raw_;
}
waybar::modules::cava::CavaBackend::type_signal_silence
waybar::modules::cava::CavaBackend::signal_silence() {
return m_signal_silence_;
}
void waybar::modules::cava::CavaBackend::Update() { doUpdate(true); }
void waybar::modules::cava::CavaBackend::doUpdate(bool force) {
if (audio_data_.suspendFlag && !force) return;
silence_ = isSilence();
if (!silence_) sleep_counter_ = 0;
if (silence_ && prm_.sleep_timer != 0) {
if (sleep_counter_ <=
(int)(std::chrono::milliseconds(prm_.sleep_timer * 1s) / frame_time_milsec_)) {
++sleep_counter_;
silence_ = false;
}
}
if (!silence_ || prm_.sleep_timer == 0) {
if (downThreadDelay(frame_time_milsec_, suspend_silence_delay_)) Update();
execute();
if (re_paint_ == 1 || force || prm_.continuous_rendering) {
m_signal_update_.emit(output_);
m_signal_audio_raw_.emit(audio_raw_);
}
} else {
if (upThreadDelay(frame_time_milsec_, suspend_silence_delay_)) Update();
if (silence_ != silence_prev_ || force) m_signal_silence_.emit();
}
silence_prev_ = silence_;
}
void waybar::modules::cava::CavaBackend::freeBackend() {
if (plan_ != NULL) {
cava_destroy(plan_);
plan_ = NULL;
}
audio_raw_clean(&audio_raw_);
pthread_mutex_lock(&audio_data_.lock);
audio_data_.terminate = 1;
pthread_mutex_unlock(&audio_data_.lock);
free_config(&prm_);
free(audio_data_.source);
free(audio_data_.cava_in);
}
void waybar::modules::cava::CavaBackend::loadConfig() {
freeBackend();
// Load waybar module config
char cfgPath[PATH_MAX];
cfgPath[0] = '\0';
if (config_["cava_config"].isString()) strcpy(cfgPath, config_["cava_config"].asString().data());
// Load cava config
error_.length = 0;
if (!load_config(cfgPath, &prm_, &error_)) {
spdlog::error("cava backend. Error loading config. {0}", error_.message);
exit(EXIT_FAILURE);
}
// Override cava parameters by the user config
prm_.inAtty = 0;
auto const output{prm_.output};
// prm_.output = ::cava::output_method::OUTPUT_RAW;
if (prm_.data_format) free(prm_.data_format);
// Default to ascii for format-icons output; allow user override
prm_.data_format = strdup(
config_["data_format"].isString() ? config_["data_format"].asString().c_str() : "ascii");
if (config_["raw_target"].isString()) {
if (prm_.raw_target) free(prm_.raw_target);
prm_.raw_target = strdup(config_["raw_target"].asString().c_str());
}
prm_.ascii_range = config_["format-icons"].size() - 1;
if (config_["bar_spacing"].isInt()) prm_.bar_spacing = config_["bar_spacing"].asInt();
if (config_["bar_width"].isInt()) prm_.bar_width = config_["bar_width"].asInt();
if (config_["bar_height"].isInt()) prm_.bar_height = config_["bar_height"].asInt();
prm_.orientation = ::cava::ORIENT_TOP;
prm_.xaxis = ::cava::xaxis_scale::NONE;
prm_.mono_opt = ::cava::AVERAGE;
prm_.autobars = 0;
if (config_["gravity"].isInt()) prm_.gravity = config_["gravity"].asInt();
if (config_["integral"].isInt()) prm_.integral = config_["integral"].asInt();
if (config_["framerate"].isInt()) prm_.framerate = config_["framerate"].asInt();
// Calculate delay for Update() thread
frame_time_milsec_ = std::chrono::milliseconds((int)(1e3 / prm_.framerate));
if (config_["autosens"].isInt()) prm_.autosens = config_["autosens"].asInt();
if (config_["sensitivity"].isInt()) prm_.sens = config_["sensitivity"].asInt();
if (config_["bars"].isInt()) prm_.fixedbars = config_["bars"].asInt();
if (config_["lower_cutoff_freq"].isNumeric())
prm_.lower_cut_off = config_["lower_cutoff_freq"].asLargestInt();
if (config_["higher_cutoff_freq"].isNumeric())
prm_.upper_cut_off = config_["higher_cutoff_freq"].asLargestInt();
if (config_["sleep_timer"].isInt()) prm_.sleep_timer = config_["sleep_timer"].asInt();
if (config_["method"].isString())
prm_.input = ::cava::input_method_by_name(config_["method"].asString().c_str());
if (config_["source"].isString()) {
if (prm_.audio_source) free(prm_.audio_source);
prm_.audio_source = strdup(config_["source"].asString().c_str());
}
if (config_["sample_rate"].isNumeric()) prm_.samplerate = config_["sample_rate"].asLargestInt();
if (config_["sample_bits"].isInt()) prm_.samplebits = config_["sample_bits"].asInt();
if (config_["stereo"].isBool()) prm_.stereo = config_["stereo"].asBool();
if (config_["reverse"].isBool()) prm_.reverse = config_["reverse"].asBool();
if (config_["bar_delimiter"].isInt()) prm_.bar_delim = config_["bar_delimiter"].asInt();
if (config_["monstercat"].isBool()) prm_.monstercat = config_["monstercat"].asBool();
if (config_["waves"].isBool()) prm_.waves = config_["waves"].asBool();
if (config_["noise_reduction"].isDouble())
prm_.noise_reduction = config_["noise_reduction"].asDouble();
if (config_["input_delay"].isInt())
fetch_input_delay_ = std::chrono::seconds(config_["input_delay"].asInt());
if (config_["gradient"].isInt()) prm_.gradient = config_["gradient"].asInt();
if (prm_.gradient == 0)
prm_.gradient_count = 0;
else if (config_["gradient_count"].isInt())
prm_.gradient_count = config_["gradient_count"].asInt();
if (config_["sdl_width"].isInt()) prm_.sdl_width = config_["sdl_width"].asInt();
if (config_["sdl_height"].isInt()) prm_.sdl_height = config_["sdl_height"].asInt();
audio_raw_.height = prm_.ascii_range;
audio_data_.format = -1;
audio_data_.rate = 0;
audio_data_.samples_counter = 0;
audio_data_.channels = 2;
audio_data_.IEEE_FLOAT = 0;
audio_data_.input_buffer_size = BUFFER_SIZE * audio_data_.channels;
audio_data_.cava_buffer_size = audio_data_.input_buffer_size * 8;
audio_data_.terminate = 0;
audio_data_.suspendFlag = false;
input_source_ = get_input(&audio_data_, &prm_);
if (!input_source_) {
spdlog::error("cava backend API didn't provide input audio source method");
exit(EXIT_FAILURE);
}
prm_.output = ::cava::output_method::OUTPUT_RAW;
// Make cava parameters configuration
// Init cava plan, audio_raw structure
audio_raw_init(&audio_data_, &audio_raw_, &prm_, &plan_);
if (!plan_) spdlog::error("cava backend plan is not provided");
audio_raw_.previous_frame[0] = -1; // For first Update() call need to rePaint text message
prm_.output = output;
}
const struct ::cava::config_params* waybar::modules::cava::CavaBackend::getPrm() { return &prm_; }
std::chrono::milliseconds waybar::modules::cava::CavaBackend::getFrameTimeMilsec() {
return frame_time_milsec_;
};
+10 -5
View File
@@ -28,14 +28,14 @@ CFFI::CFFI(const std::string& name, const std::string& id, const Json::Value& co
}
// Fetch functions
if (*wbcffi_version == 1) {
if (*wbcffi_version == 1 || *wbcffi_version == 2) {
// Mandatory functions
hooks_.init = reinterpret_cast<InitFn*>(dlsym(handle, "wbcffi_init"));
if (!hooks_.init) {
throw std::runtime_error{std::string{"Missing wbcffi_init function: "} + dlerror()};
}
hooks_.deinit = reinterpret_cast<DenitFn*>(dlsym(handle, "wbcffi_deinit"));
if (!hooks_.init) {
if (!hooks_.deinit) {
throw std::runtime_error{std::string{"Missing wbcffi_deinit function: "} + dlerror()};
}
// Optional functions
@@ -58,15 +58,20 @@ CFFI::CFFI(const std::string& name, const std::string& id, const Json::Value& co
const auto& keys = config.getMemberNames();
for (size_t i = 0; i < keys.size(); i++) {
const auto& value = config[keys[i]];
if (value.isConvertibleTo(Json::ValueType::stringValue)) {
config_entries_stringstor.push_back(config[keys[i]].asString());
if (*wbcffi_version == 1) {
if (value.isConvertibleTo(Json::ValueType::stringValue)) {
config_entries_stringstor.push_back(value.asString());
} else {
config_entries_stringstor.push_back(value.toStyledString());
}
} else {
config_entries_stringstor.push_back(config[keys[i]].toStyledString());
config_entries_stringstor.push_back(value.toStyledString());
}
}
// Prepare config_entries array
std::vector<ffi::wbcffi_config_entry> config_entries;
config_entries.reserve(keys.size());
for (size_t i = 0; i < keys.size(); i++) {
config_entries.push_back({keys[i].c_str(), config_entries_stringstor[i].c_str()});
}
+202 -59
View File
@@ -1,5 +1,7 @@
#include "modules/clock.hpp"
#include <glib.h>
#include <gtkmm/tooltip.h>
#include <spdlog/spdlog.h>
#include <chrono>
@@ -7,24 +9,33 @@
#include <regex>
#include <sstream>
#include "util/command.hpp"
#include "util/ustring_clen.hpp"
#ifdef HAVE_LANGINFO_1STDAY
#include <langinfo.h>
#include <locale.h>
#include <clocale>
#endif
using namespace date;
namespace fmt_lib = waybar::util::date::format;
waybar::modules::Clock::Clock(const std::string& id, const Json::Value& config)
: ALabel(config, "clock", id, "{:%H:%M}", 60, false, false, true),
locale_{std::locale(config_["locale"].isString() ? config_["locale"].asString() : "")},
tlpFmt_{(config_["tooltip-format"].isString()) ? config_["tooltip-format"].asString() : ""},
cldInTooltip_{tlpFmt_.find("{" + kCldPlaceholder + "}") != std::string::npos},
tzInTooltip_{tlpFmt_.find("{" + kTZPlaceholder + "}") != std::string::npos},
m_locale_{std::locale(config_["locale"].isString() ? config_["locale"].asString() : "")},
m_tlpFmt_{(config_["tooltip-format"].isString()) ? config_["tooltip-format"].asString() : ""},
m_tooltip_{new Gtk::Label()},
cldInTooltip_{m_tlpFmt_.find("{" + kCldPlaceholder + "}") != std::string::npos},
cldYearShift_{January / 1 / 1900},
cldMonShift_{year(1900) / January},
tzInTooltip_{m_tlpFmt_.find("{" + kTZPlaceholder + "}") != std::string::npos},
tzCurrIdx_{0},
ordInTooltip_{tlpFmt_.find("{" + kOrdPlaceholder + "}") != std::string::npos} {
tlpText_ = tlpFmt_;
tzTooltipFormat_{config_["timezone-tooltip-format"].isString()
? config_["timezone-tooltip-format"].asString()
: ""},
ordInTooltip_{m_tlpFmt_.find("{" + kOrdPlaceholder + "}") != std::string::npos} {
m_tlpText_ = m_tlpFmt_;
if (config_["timezones"].isArray() && !config_["timezones"].empty()) {
for (const auto& zone_name : config_["timezones"]) {
@@ -56,8 +67,8 @@ waybar::modules::Clock::Clock(const std::string& id, const Json::Value& config)
if (cldInTooltip_) {
if (config_[kCldPlaceholder]["mode"].isString()) {
const std::string cfgMode{config_[kCldPlaceholder]["mode"].asString()};
const std::map<std::string_view, const CldMode&> monthModes{{"month", CldMode::MONTH},
{"year", CldMode::YEAR}};
const std::map<std::string, CldMode> monthModes{{"month", CldMode::MONTH},
{"year", CldMode::YEAR}};
if (monthModes.find(cfgMode) != monthModes.end())
cldMode_ = monthModes.at(cfgMode);
else
@@ -66,6 +77,11 @@ waybar::modules::Clock::Clock(const std::string& id, const Json::Value& config)
"using instead",
cfgMode);
}
if (config_[kCldPlaceholder]["iso8601"].isBool()) {
iso8601Calendar_ = config_[kCldPlaceholder]["iso8601"].asBool();
}
if (config_[kCldPlaceholder]["weeks-pos"].isString()) {
if (config_[kCldPlaceholder]["weeks-pos"].asString() == "left") cldWPos_ = WS::LEFT;
if (config_[kCldPlaceholder]["weeks-pos"].asString() == "right") cldWPos_ = WS::RIGHT;
@@ -85,23 +101,25 @@ waybar::modules::Clock::Clock(const std::string& id, const Json::Value& config)
fmtMap_.insert({2, "{}"});
if (config_[kCldPlaceholder]["format"]["today"].isString()) {
fmtMap_.insert({3, config_[kCldPlaceholder]["format"]["today"].asString()});
cldBaseDay_ =
year_month_day{
floor<days>(zoned_time{current_zone(), system_clock::now()}.get_local_time())}
.day();
auto local_time = zoned_time{local_zone(), system_clock::now()}.get_local_time();
cldBaseDay_ = year_month_day{floor<days>(local_time)}.day();
} else
fmtMap_.insert({3, "{}"});
if (config_[kCldPlaceholder]["format"]["weeks"].isString() && cldWPos_ != WS::HIDDEN) {
const auto defaultFmt =
iso8601Calendar_ ? "{:%V}" : ((first_day_of_week() == Monday) ? "{:%W}" : "{:%U}");
fmtMap_.insert({4, std::regex_replace(config_[kCldPlaceholder]["format"]["weeks"].asString(),
std::regex("\\{\\}"),
(first_day_of_week() == Monday) ? "{:%W}" : "{:%U}")});
std::regex("\\{\\}"), defaultFmt)});
Glib::ustring tmp{std::regex_replace(fmtMap_[4], std::regex("</?[^>]+>|\\{.*\\}"), "")};
cldWnLen_ += tmp.size();
} else {
if (cldWPos_ != WS::HIDDEN)
fmtMap_.insert({4, (first_day_of_week() == Monday) ? "{:%W}" : "{:%U}"});
else
if (cldWPos_ != WS::HIDDEN) {
const auto defaultFmt =
iso8601Calendar_ ? "{:%V}" : ((first_day_of_week() == Monday) ? "{:%W}" : "{:%U}");
fmtMap_.insert({4, defaultFmt});
} else {
cldWnLen_ = 0;
}
}
if (config_[kCldPlaceholder]["mode-mon-col"].isInt()) {
cldMonCols_ = config_[kCldPlaceholder]["mode-mon-col"].asInt();
@@ -115,6 +133,7 @@ waybar::modules::Clock::Clock(const std::string& id, const Json::Value& config)
} else
cldMonCols_ = 1;
if (config_[kCldPlaceholder]["on-scroll"].isInt()) {
cldShift_ = config_[kCldPlaceholder]["on-scroll"].asInt();
event_box_.add_events(Gdk::LEAVE_NOTIFY_MASK);
event_box_.signal_leave_notify_event().connect([this](GdkEventCrossing*) {
cldCurrShift_ = months{0};
@@ -123,17 +142,28 @@ waybar::modules::Clock::Clock(const std::string& id, const Json::Value& config)
}
}
if (tooltipEnabled()) {
label_.set_has_tooltip(true);
label_.signal_query_tooltip().connect(sigc::mem_fun(*this, &Clock::query_tlp_cb));
}
thread_ = [this] {
dp.emit();
thread_.sleep_for(interval_ - system_clock::now().time_since_epoch() % interval_);
};
}
bool waybar::modules::Clock::query_tlp_cb(int, int, bool,
const Glib::RefPtr<Gtk::Tooltip>& tooltip) {
tooltip->set_custom(*m_tooltip_.get());
return true;
}
auto waybar::modules::Clock::update() -> void {
const auto* tz = tzList_[tzCurrIdx_] != nullptr ? tzList_[tzCurrIdx_] : current_zone();
const auto* tz = tzList_[tzCurrIdx_] != nullptr ? tzList_[tzCurrIdx_] : local_zone();
const zoned_time now{tz, floor<seconds>(system_clock::now())};
label_.set_markup(fmt_lib::vformat(locale_, format_, fmt_lib::make_format_args(now)));
setLabelMarkup(fmt_lib::vformat(m_locale_, format_, fmt_lib::make_format_args(now)));
if (tooltipEnabled()) {
const year_month_day today{floor<days>(now.get_local_time())};
@@ -146,16 +176,59 @@ auto waybar::modules::Clock::update() -> void {
if (ordInTooltip_) ordText_ = get_ordinal_date(shiftedDay);
if (tzInTooltip_ || cldInTooltip_ || ordInTooltip_) {
// std::vformat doesn't support named arguments.
tlpText_ = std::regex_replace(tlpFmt_, std::regex("\\{" + kTZPlaceholder + "\\}"), tzText_);
tlpText_ =
std::regex_replace(tlpText_, std::regex("\\{" + kCldPlaceholder + "\\}"), cldText_);
tlpText_ =
std::regex_replace(tlpText_, std::regex("\\{" + kOrdPlaceholder + "\\}"), ordText_);
m_tlpText_ =
std::regex_replace(m_tlpFmt_, std::regex("\\{" + kTZPlaceholder + "\\}"), tzText_);
m_tlpText_ = std::regex_replace(
m_tlpText_, std::regex("\\{" + kCldPlaceholder + "\\}"),
fmt_lib::vformat(m_locale_, cldText_, fmt_lib::make_format_args(shiftedNow)));
m_tlpText_ =
std::regex_replace(m_tlpText_, std::regex("\\{" + kOrdPlaceholder + "\\}"), ordText_);
} else {
m_tlpText_ = m_tlpFmt_;
}
tlpText_ = fmt_lib::vformat(locale_, tlpText_, fmt_lib::make_format_args(shiftedNow));
m_tlpText_ = fmt_lib::vformat(m_locale_, m_tlpText_, fmt_lib::make_format_args(now));
label_.set_tooltip_markup(tlpText_);
// Pango doesn't support CSS classes but to continue using it while staying
// backwards compatible this approach uses post-posting to replace fake
// classes with attributes Pango does understand.
//
// The benefit of this approach is anyone using the original styling choices
// can continue doing that and folks can optionally opt into using classes.
//
// It's also forwards compatible to where if this implemention ever changes
// to support proper classes anyone using them will continue to work.
auto context = label_.get_style_context();
static const std::vector<std::pair<std::string, std::string>> calendar_class_map = {
{"calendar-today", "class='today'"},
{"calendar-days", "class='days'"},
{"calendar-weeks", "class='weeks'"},
{"calendar-weekdays", "class='weekdays'"},
{"calendar-months", "class='months'"}};
for (const auto& [css_class, search_str] : calendar_class_map) {
try {
context->add_class(css_class);
const Gdk::RGBA color = context->get_color();
context->remove_class(css_class);
const std::string replace_str = fmt::format(
"color='#{:02x}{:02x}{:02x}'", static_cast<int>(color.get_red() * 255),
static_cast<int>(color.get_green() * 255), static_cast<int>(color.get_blue() * 255));
m_tlpText_ = std::regex_replace(m_tlpText_, std::regex(search_str), replace_str);
} catch (const Glib::Error& e) {
spdlog::warn("Clock: Failed to fetch CSS color for {}: {}", css_class, e.what().raw());
continue;
} catch (...) {
// Catch-all for any other weirdness.
continue;
}
}
m_tooltip_->set_markup(m_tlpText_);
label_.trigger_tooltip_query();
}
ALabel::update();
@@ -165,11 +238,26 @@ auto waybar::modules::Clock::getTZtext(sys_seconds now) -> std::string {
if (tzList_.size() == 1) return "";
std::stringstream os;
bool first = true;
for (size_t tz_idx{0}; tz_idx < tzList_.size(); ++tz_idx) {
if (static_cast<int>(tz_idx) == tzCurrIdx_) continue;
const auto* tz = tzList_[tz_idx] != nullptr ? tzList_[tz_idx] : current_zone();
// Skip local timezone (nullptr) - never show it in tooltip
if (tzList_[tz_idx] == nullptr) continue;
// Skip current timezone unless timezone-tooltip-format is specified
if (static_cast<int>(tz_idx) == tzCurrIdx_ && tzTooltipFormat_.empty()) continue;
const auto* tz = tzList_[tz_idx];
auto zt{zoned_time{tz, now}};
os << fmt_lib::vformat(locale_, format_, fmt_lib::make_format_args(zt)) << '\n';
// Add newline before each entry except the first
if (!first) {
os << '\n';
}
first = false;
// Use timezone-tooltip-format if specified, otherwise use format_
const std::string& fmt = tzTooltipFormat_.empty() ? format_ : tzTooltipFormat_;
os << fmt_lib::vformat(m_locale_, fmt, fmt_lib::make_format_args(zt));
}
return os.str();
@@ -181,19 +269,21 @@ const unsigned cldRowsInMonth(const year_month& ym, const weekday& firstdow) {
auto cldGetWeekForLine(const year_month& ym, const weekday& firstdow, const unsigned line)
-> const year_month_weekday {
unsigned index{line - 2};
if (weekday{ym / 1} == firstdow) ++index;
return ym / firstdow[index];
const unsigned idx = line - 2;
const auto indexed_first_day_of_week =
weekday{ym / 1} == firstdow ? firstdow[idx + 1] : firstdow[idx];
return ym / indexed_first_day_of_week;
}
auto getCalendarLine(const year_month_day& currDate, const year_month ym, const unsigned line,
const weekday& firstdow, const std::locale* const locale_) -> std::string {
const weekday& firstdow, const std::locale* const m_locale_) -> std::string {
std::ostringstream os;
switch (line) {
// Print month and year title
case 0: {
os << date::format(*locale_, "{:L%B %Y}", ym);
os << date::format(*m_locale_, "{:L%B %Y}", ym);
break;
}
// Print weekday names title
@@ -203,7 +293,7 @@ auto getCalendarLine(const year_month_day& currDate, const year_month ym, const
Glib::ustring::size_type wdLen{0};
int clen{0};
do {
wdStr = date::format(*locale_, "{:L%a}", wd);
wdStr = date::format(*m_locale_, "{:L%a}", wd);
clen = ustring_clen(wdStr);
wdLen = wdStr.length();
while (clen > 2) {
@@ -226,7 +316,7 @@ auto getCalendarLine(const year_month_day& currDate, const year_month ym, const
os << std::string((wd - firstdow).count() * 3, ' ');
if (currDate != ym / d)
os << date::format(*locale_, "{:L%e}", d);
os << date::format(*m_locale_, "{:L%e}", d);
else
os << "{today}";
@@ -234,7 +324,7 @@ auto getCalendarLine(const year_month_day& currDate, const year_month ym, const
++d;
if (currDate != ym / d)
os << date::format(*locale_, " {:L%e}", d);
os << date::format(*m_locale_, " {:L%e}", d);
else
os << " {today}";
}
@@ -242,20 +332,20 @@ auto getCalendarLine(const year_month_day& currDate, const year_month ym, const
}
// Print non-first week
default: {
auto ymdTmp{cldGetWeekForLine(ym, firstdow, line)};
const auto ymdTmp{cldGetWeekForLine(ym, firstdow, line)};
if (ymdTmp.ok()) {
auto d{year_month_day{ymdTmp}.day()};
const auto dlast{(ym / last).day()};
auto wd{firstdow};
if (currDate != ym / d)
os << date::format(*locale_, "{:L%e}", d);
os << date::format(*m_locale_, "{:L%e}", d);
else
os << "{today}";
while (++wd != firstdow && ++d <= dlast) {
if (currDate != ym / d)
os << date::format(*locale_, " {:L%e}", d);
os << date::format(*m_locale_, " {:L%e}", d);
else
os << " {today}";
}
@@ -325,23 +415,42 @@ auto waybar::modules::Clock::get_calendar(const year_month_day& today, const yea
if (line > 1) {
if (line < ml[(unsigned)ymTmp.month() - 1u]) {
os << fmt_lib::vformat(
locale_, fmtMap_[4],
m_locale_, fmtMap_[4],
fmt_lib::make_format_args(
(line == 2)
? static_cast<const date::zoned_seconds&&>(
? static_cast<const zoned_seconds&&>(
zoned_seconds{tz, local_days{ymTmp / 1}})
: static_cast<const date::zoned_seconds&&>(zoned_seconds{
: static_cast<const zoned_seconds&&>(zoned_seconds{
tz, local_days{cldGetWeekForLine(ymTmp, firstdow, line)}})))
<< ' ';
} else
} else {
os << pads;
}
}
}
os << Glib::ustring::format((cldWPos_ != WS::LEFT || line == 0) ? std::left : std::right,
std::setfill(L' '),
std::setw(cldMonColLen_ + ((line < 2) ? cldWnLen_ : 0)),
getCalendarLine(today, ymTmp, line, firstdow, &locale_));
// Count wide characters to avoid extra padding
size_t wideCharCount = 0;
std::string calendarLine = getCalendarLine(today, ymTmp, line, firstdow, &m_locale_);
if (line < 2) {
for (gchar *data = calendarLine.data(), *end = data + calendarLine.size();
data != nullptr;) {
gunichar c = g_utf8_get_char_validated(data, end - data);
if (g_unichar_iswide(c)) {
wideCharCount++;
}
data = g_utf8_find_next_char(data, end);
}
}
// Note: the stream's default fill character is already a space (L' ' on
// libstdc++'s wide FormatStream, ' ' on libc++'s narrow one), so no
// std::setfill is needed. Passing std::setfill(' ')/std::setfill(L' ')
// here is not portable because the fill char type must match the
// FormatStream's char type, which differs between standard libraries.
os << Glib::ustring::format(
(cldWPos_ != WS::LEFT || line == 0) ? std::left : std::right,
std::setw(cldMonColLen_ + ((line < 2) ? cldWnLen_ - wideCharCount : 0)),
calendarLine);
// Week numbers on the right
if (cldWPos_ == WS::RIGHT && line > 0) {
@@ -349,11 +458,11 @@ auto waybar::modules::Clock::get_calendar(const year_month_day& today, const yea
if (line < ml[(unsigned)ymTmp.month() - 1u])
os << ' '
<< fmt_lib::vformat(
locale_, fmtMap_[4],
m_locale_, fmtMap_[4],
fmt_lib::make_format_args(
(line == 2) ? static_cast<const date::zoned_seconds&&>(
(line == 2) ? static_cast<const zoned_seconds&&>(
zoned_seconds{tz, local_days{ymTmp / 1}})
: static_cast<const date::zoned_seconds&&>(
: static_cast<const zoned_seconds&&>(
zoned_seconds{tz, local_days{cldGetWeekForLine(
ymTmp, firstdow, line)}})));
else
@@ -365,7 +474,7 @@ auto waybar::modules::Clock::get_calendar(const year_month_day& today, const yea
// Apply user's formats
if (line < 2)
tmp << fmt_lib::vformat(
locale_, fmtMap_[line],
m_locale_, fmtMap_[line],
fmt_lib::make_format_args(static_cast<const std::string_view&&>(os.str())));
else
tmp << os.str();
@@ -377,10 +486,10 @@ auto waybar::modules::Clock::get_calendar(const year_month_day& today, const yea
}
os << std::regex_replace(
fmt_lib::vformat(locale_, fmtMap_[2],
fmt_lib::vformat(m_locale_, fmtMap_[2],
fmt_lib::make_format_args(static_cast<const std::string_view&&>(tmp.str()))),
std::regex("\\{today\\}"),
fmt_lib::vformat(locale_, fmtMap_[3],
fmt_lib::vformat(m_locale_, fmtMap_[3],
fmt_lib::make_format_args(
static_cast<const std::string_view&&>(date::format("{:L%e}", d)))));
@@ -392,10 +501,24 @@ auto waybar::modules::Clock::get_calendar(const year_month_day& today, const yea
return os.str();
}
auto waybar::modules::Clock::local_zone() -> const time_zone* {
const char* tz_name = getenv("TZ");
if (tz_name) {
try {
return locate_zone(tz_name);
} catch (const std::runtime_error& e) {
spdlog::warn("Timezone: {0}. {1}", tz_name, e.what());
}
}
return current_zone();
}
// Actions handler
auto waybar::modules::Clock::doAction(const std::string& name) -> void {
if (actionMap_[name]) {
(this->*actionMap_[name])();
} else if (auto key = name.substr(0, name.find(" ")); actionWithArgsMap_[key]) {
(this->*actionWithArgsMap_[key])(name);
} else
spdlog::error("Clock. Unsupported action \"{0}\"", name);
}
@@ -405,11 +528,12 @@ void waybar::modules::Clock::cldModeSwitch() {
cldMode_ = (cldMode_ == CldMode::YEAR) ? CldMode::MONTH : CldMode::YEAR;
}
void waybar::modules::Clock::cldShift_up() {
cldCurrShift_ += (months)((cldMode_ == CldMode::YEAR) ? 12 : 1);
cldCurrShift_ += (months)((cldMode_ == CldMode::YEAR) ? 12 : 1) * cldShift_;
}
void waybar::modules::Clock::cldShift_down() {
cldCurrShift_ -= (months)((cldMode_ == CldMode::YEAR) ? 12 : 1);
cldCurrShift_ -= (months)((cldMode_ == CldMode::YEAR) ? 12 : 1) * cldShift_;
}
void waybar::modules::Clock::cldShift_reset() { cldCurrShift_ = (months)0; }
void waybar::modules::Clock::tz_up() {
const auto tzSize{tzList_.size()};
if (tzSize == 1) return;
@@ -421,6 +545,10 @@ void waybar::modules::Clock::tz_down() {
if (tzSize == 1) return;
tzCurrIdx_ = (tzCurrIdx_ == 0) ? tzSize - 1 : tzCurrIdx_ - 1;
}
void waybar::modules::Clock::action_exec(const std::string& action) {
auto cmd = action.substr(strlen("exec "));
pid_children_.push_back(util::command::forkExec(cmd));
}
#ifdef HAVE_LANGINFO_1STDAY
template <auto fn>
@@ -432,9 +560,24 @@ using deleting_unique_ptr = std::unique_ptr<T, deleter_from_fn<fn>>;
// Computations done similarly to Linux cal utility.
auto waybar::modules::Clock::first_day_of_week() -> weekday {
const auto firstdow = config_[kCldPlaceholder]["first-day-of-week"];
if (firstdow.isInt()) {
const int firstDay = firstdow.asInt();
if (!(firstDay >= 0 && firstDay <= 6)) {
spdlog::warn(
"Clock calender configuration first-day-of-week = {0} must be in range [0, 6]. Default "
"value is used instead",
firstDay);
} else {
return weekday{static_cast<unsigned>(firstDay)};
}
}
if (iso8601Calendar_) {
return Monday;
}
#ifdef HAVE_LANGINFO_1STDAY
deleting_unique_ptr<std::remove_pointer<locale_t>::type, freelocale> posix_locale{
newlocale(LC_ALL, locale_.name().c_str(), nullptr)};
newlocale(LC_ALL, m_locale_.name().c_str(), nullptr)};
if (posix_locale) {
const auto i{(int)((std::intptr_t)nl_langinfo_l(_NL_TIME_WEEK_1STDAY, posix_locale.get()))};
const weekday wd{year_month_day{year(i / 10000) / month(i / 100 % 100) / day(i % 100)}};
@@ -468,4 +611,4 @@ auto waybar::modules::Clock::get_ordinal_date(const year_month_day& today) -> st
res << "th";
}
return res.str();
}
}
+15 -8
View File
@@ -26,9 +26,7 @@ auto waybar::modules::Cpu::update() -> void {
auto [load1, load5, load15] = Load::getLoad();
auto [cpu_usage, tooltip] = CpuUsage::getCpuUsage(prev_times_);
auto [max_frequency, min_frequency, avg_frequency] = CpuFrequency::getCpuFrequency();
if (tooltipEnabled()) {
label_.set_tooltip_text(tooltip);
}
auto format = format_;
auto total_usage = cpu_usage.empty() ? 0 : cpu_usage[0];
auto state = getState(total_usage);
@@ -43,19 +41,28 @@ auto waybar::modules::Cpu::update() -> void {
auto icons = std::vector<std::string>{state};
fmt::dynamic_format_arg_store<fmt::format_context> store;
store.push_back(fmt::arg("load", load1));
store.push_back(fmt::arg("load1", load1));
store.push_back(fmt::arg("load5", load5));
store.push_back(fmt::arg("load15", load15));
store.push_back(fmt::arg("usage", total_usage));
store.push_back(fmt::arg("icon", getIcon(total_usage, icons)));
store.push_back(fmt::arg("max_frequency", max_frequency));
store.push_back(fmt::arg("min_frequency", min_frequency));
store.push_back(fmt::arg("avg_frequency", avg_frequency));
std::vector<std::string> arg_names;
arg_names.reserve(cpu_usage.size() * 2);
std::string all_icons;
for (size_t i = 1; i < cpu_usage.size(); ++i) {
auto core_i = i - 1;
auto core_format = fmt::format("usage{}", core_i);
store.push_back(fmt::arg(core_format.c_str(), cpu_usage[i]));
auto icon_format = fmt::format("icon{}", core_i);
store.push_back(fmt::arg(icon_format.c_str(), getIcon(cpu_usage[i], icons)));
arg_names.push_back(fmt::format("usage{}", core_i));
store.push_back(fmt::arg(arg_names.back().c_str(), cpu_usage[i]));
auto core_icon = getIcon(cpu_usage[i], icons);
all_icons += core_icon;
arg_names.push_back(fmt::format("icon{}", core_i));
store.push_back(fmt::arg(arg_names.back().c_str(), core_icon));
}
label_.set_markup(fmt::vformat(format, store));
store.push_back(fmt::arg("icons", all_icons));
updateLabelAndTooltipForState(state, format, tooltip, store);
}
// Call parent update
+9 -2
View File
@@ -5,11 +5,12 @@
std::vector<float> waybar::modules::CpuFrequency::parseCpuFrequencies() {
std::vector<float> frequencies;
char buffer[256];
size_t len;
int32_t freq;
uint32_t i = 0;
#ifndef __OpenBSD__
char buffer[256];
uint32_t i = 0;
while (true) {
len = 4;
snprintf(buffer, 256, "dev.cpu.%u.freq", i);
@@ -17,6 +18,12 @@ std::vector<float> waybar::modules::CpuFrequency::parseCpuFrequencies() {
frequencies.push_back(freq);
++i;
}
#else
int getMhz[] = {CTL_HW, HW_CPUSPEED};
len = sizeof(freq);
sysctl(getMhz, 2, &freq, &len, NULL, 0);
frequencies.push_back((float)freq);
#endif
if (frequencies.empty()) {
spdlog::warn("cpu/bsd: parseCpuFrequencies failed, not found in sysctl");
+7 -12
View File
@@ -20,12 +20,7 @@ waybar::modules::CpuFrequency::CpuFrequency(const std::string& id, const Json::V
auto waybar::modules::CpuFrequency::update() -> void {
// TODO: as creating dynamic fmt::arg arrays is buggy we have to calc both
auto [max_frequency, min_frequency, avg_frequency] = CpuFrequency::getCpuFrequency();
if (tooltipEnabled()) {
auto tooltip =
fmt::format("Minimum frequency: {}\nAverage frequency: {}\nMaximum frequency: {}\n",
min_frequency, avg_frequency, max_frequency);
label_.set_tooltip_text(tooltip);
}
auto format = format_;
auto state = getState(avg_frequency);
if (!state.empty() && config_["format-" + state].isString()) {
@@ -37,12 +32,12 @@ auto waybar::modules::CpuFrequency::update() -> void {
} else {
event_box_.show();
auto icons = std::vector<std::string>{state};
fmt::dynamic_format_arg_store<fmt::format_context> store;
store.push_back(fmt::arg("icon", getIcon(avg_frequency, icons)));
store.push_back(fmt::arg("max_frequency", max_frequency));
store.push_back(fmt::arg("min_frequency", min_frequency));
store.push_back(fmt::arg("avg_frequency", avg_frequency));
label_.set_markup(fmt::vformat(format, store));
updateLabelAndTooltip(
format,
"Minimum frequency: {min_frequency}\nAverage frequency: {avg_frequency}\nMaximum "
"frequency: {max_frequency}\n",
fmt::arg("icon", getIcon(avg_frequency, icons)), fmt::arg("max_frequency", max_frequency),
fmt::arg("min_frequency", min_frequency), fmt::arg("avg_frequency", avg_frequency));
}
// Call parent update
+9 -11
View File
@@ -23,23 +23,21 @@ std::vector<float> waybar::modules::CpuFrequency::parseCpuFrequencies() {
if (frequencies.size() <= 0) {
std::string cpufreq_dir = "/sys/devices/system/cpu/cpufreq";
if (std::filesystem::exists(cpufreq_dir)) {
try {
std::vector<std::string> frequency_files = {"/cpuinfo_min_freq", "/cpuinfo_max_freq"};
for (auto& p : std::filesystem::directory_iterator(cpufreq_dir)) {
for (auto freq_file : frequency_files) {
for (const auto& freq_file : frequency_files) {
std::string freq_file_path = p.path().string() + freq_file;
if (std::filesystem::exists(freq_file_path)) {
std::string freq_value;
std::ifstream freq(freq_file_path);
if (freq.is_open()) {
getline(freq, freq_value);
float frequency = std::strtol(freq_value.c_str(), nullptr, 10);
frequencies.push_back(frequency / 1000);
freq.close();
}
std::string freq_value;
std::ifstream freq(freq_file_path);
if (freq.is_open()) {
getline(freq, freq_value);
float frequency = std::strtol(freq_value.c_str(), nullptr, 10);
frequencies.push_back(frequency / 1000);
}
}
}
} catch (const std::filesystem::filesystem_error&) {
}
}
+47
View File
@@ -0,0 +1,47 @@
#include "modules/cpu_graph.hpp"
#include "modules/cpu_frequency.hpp"
#include "modules/cpu_usage.hpp"
#include "modules/load.hpp"
// In the 80000 version of fmt library authors decided to optimize imports
// and moved declarations required for fmt::dynamic_format_arg_store in new
// header fmt/args.h
#if (FMT_VERSION >= 80000)
#include <fmt/args.h>
#else
#include <fmt/core.h>
#endif
waybar::modules::CpuGraph::CpuGraph(const std::string& id, const Json::Value& config)
: AGraph(config, "cpu_graph", id, 5) {
thread_ = [this] {
dp.emit();
thread_.sleep_for(interval_);
};
}
auto waybar::modules::CpuGraph::update() -> void {
// TODO: as creating dynamic fmt::arg arrays is buggy we have to calc both
auto [cpu_usage, tooltip] = CpuUsage::getCpuUsage(prev_times_);
if (tooltipEnabled()) {
graph_.set_tooltip_text(tooltip);
}
auto total_usage = cpu_usage.empty() ? 0 : cpu_usage[0];
addValue(total_usage);
graph_.get_style_context()->remove_class(MODERATE_CLASS);
graph_.get_style_context()->remove_class(HIGH_CLASS);
graph_.get_style_context()->remove_class(INTENSIVE_CLASS);
if (total_usage > 90) {
graph_.get_style_context()->add_class(INTENSIVE_CLASS);
} else if (total_usage > 70) {
graph_.get_style_context()->add_class(HIGH_CLASS);
} else if (total_usage > 30) {
graph_.get_style_context()->add_class(MODERATE_CLASS);
}
// Call parent update
AGraph::update();
}
+1 -1
View File
@@ -33,7 +33,7 @@ std::vector<std::tuple<size_t, size_t>> waybar::modules::CpuUsage::parseCpuinfo(
size_t sum_sz = sizeof(sum_cp_time);
int ncpu = sysconf(_SC_NPROCESSORS_CONF);
size_t sz = CPUSTATES * (ncpu + 1) * sizeof(pcp_time_t);
pcp_time_t *cp_time = static_cast<pcp_time_t *>(malloc(sz)), *pcp_time = cp_time;
pcp_time_t *cp_time = static_cast<pcp_time_t*>(malloc(sz)), *pcp_time = cp_time;
waybar::util::ScopeGuard cp_time_deleter([cp_time]() {
if (cp_time) {
free(cp_time);
+42 -9
View File
@@ -20,9 +20,7 @@ waybar::modules::CpuUsage::CpuUsage(const std::string& id, const Json::Value& co
auto waybar::modules::CpuUsage::update() -> void {
// TODO: as creating dynamic fmt::arg arrays is buggy we have to calc both
auto [cpu_usage, tooltip] = CpuUsage::getCpuUsage(prev_times_);
if (tooltipEnabled()) {
label_.set_tooltip_text(tooltip);
}
auto format = format_;
auto total_usage = cpu_usage.empty() ? 0 : cpu_usage[0];
auto state = getState(total_usage);
@@ -38,14 +36,20 @@ auto waybar::modules::CpuUsage::update() -> void {
fmt::dynamic_format_arg_store<fmt::format_context> store;
store.push_back(fmt::arg("usage", total_usage));
store.push_back(fmt::arg("icon", getIcon(total_usage, icons)));
std::vector<std::string> arg_names;
arg_names.reserve(cpu_usage.size() * 2);
std::string all_icons;
for (size_t i = 1; i < cpu_usage.size(); ++i) {
auto core_i = i - 1;
auto core_format = fmt::format("usage{}", core_i);
store.push_back(fmt::arg(core_format.c_str(), cpu_usage[i]));
auto icon_format = fmt::format("icon{}", core_i);
store.push_back(fmt::arg(icon_format.c_str(), getIcon(cpu_usage[i], icons)));
arg_names.push_back(fmt::format("usage{}", core_i));
store.push_back(fmt::arg(arg_names.back().c_str(), cpu_usage[i]));
auto core_icon = getIcon(cpu_usage[i], icons);
all_icons += core_icon;
arg_names.push_back(fmt::format("icon{}", core_i));
store.push_back(fmt::arg(arg_names.back().c_str(), core_icon));
}
label_.set_markup(fmt::vformat(format, store));
store.push_back(fmt::arg("icons", all_icons));
updateLabelAndTooltip(format, tooltip, store);
}
// Call parent update
@@ -61,12 +65,41 @@ std::tuple<std::vector<uint16_t>, std::string> waybar::modules::CpuUsage::getCpu
std::vector<std::tuple<size_t, size_t>> curr_times = CpuUsage::parseCpuinfo();
std::string tooltip;
std::vector<uint16_t> usage;
if (curr_times.size() != prev_times.size()) {
// The number of CPUs has changed, eg. due to CPU hotplug
// We don't know which CPU came up or went down
// so only give total usage (if we can)
if (!curr_times.empty() && !prev_times.empty()) {
auto [curr_idle, curr_total] = curr_times[0];
auto [prev_idle, prev_total] = prev_times[0];
const float delta_idle = curr_idle - prev_idle;
const float delta_total = curr_total - prev_total;
uint16_t tmp =
(delta_total > 0) ? static_cast<uint16_t>(100 * (1 - delta_idle / delta_total)) : 0;
tooltip = fmt::format("Total: {}%\nCores: (pending)", tmp);
usage.push_back(tmp);
} else {
tooltip = "(pending)";
usage.push_back(0);
}
prev_times = curr_times;
return {usage, tooltip};
}
for (size_t i = 0; i < curr_times.size(); ++i) {
auto [curr_idle, curr_total] = curr_times[i];
auto [prev_idle, prev_total] = prev_times[i];
if (i > 0 && (curr_total == 0 || prev_total == 0)) {
// This CPU is offline
tooltip = tooltip + fmt::format("\nCore{}: offline", i - 1);
usage.push_back(0);
continue;
}
const float delta_idle = curr_idle - prev_idle;
const float delta_total = curr_total - prev_total;
uint16_t tmp = 100 * (1 - delta_idle / delta_total);
uint16_t tmp =
(delta_total > 0) ? static_cast<uint16_t>(100 * (1 - delta_idle / delta_total)) : 0;
if (i == 0) {
tooltip = fmt::format("Total: {}%", tmp);
} else {
+36 -2
View File
@@ -3,6 +3,23 @@
#include "modules/cpu_usage.hpp"
std::vector<std::tuple<size_t, size_t>> waybar::modules::CpuUsage::parseCpuinfo() {
// Get the "existing CPU count" from /sys/devices/system/cpu/present
// Probably this is what the user wants the offline CPUs accounted from
// For further details see:
// https://www.kernel.org/doc/html/latest/core-api/cpu_hotplug.html
const std::string sys_cpu_present_path = "/sys/devices/system/cpu/present";
size_t cpu_present_last = 0;
std::ifstream cpu_present_file(sys_cpu_present_path);
std::string cpu_present_text;
if (cpu_present_file.is_open()) {
getline(cpu_present_file, cpu_present_text);
// This is a comma-separated list of ranges, eg. 0,2-4,7
size_t last_separator = cpu_present_text.find_last_of("-,");
if (last_separator < cpu_present_text.size()) {
std::stringstream(cpu_present_text.substr(last_separator + 1)) >> cpu_present_last;
}
}
const std::string data_dir_ = "/proc/stat";
std::ifstream info(data_dir_);
if (!info.is_open()) {
@@ -10,14 +27,23 @@ std::vector<std::tuple<size_t, size_t>> waybar::modules::CpuUsage::parseCpuinfo(
}
std::vector<std::tuple<size_t, size_t>> cpuinfo;
std::string line;
size_t current_cpu_number = -1; // First line is total, second line is cpu 0
while (getline(info, line)) {
if (line.substr(0, 3).compare("cpu") != 0) {
break;
}
size_t line_cpu_number;
if (current_cpu_number >= 0) {
std::stringstream(line.substr(3)) >> line_cpu_number;
while (line_cpu_number > current_cpu_number) {
// Fill in 0 for offline CPUs missing inside the lines of /proc/stat
cpuinfo.emplace_back(0, 0);
current_cpu_number++;
}
}
std::stringstream sline(line.substr(5));
std::vector<size_t> times;
for (size_t time = 0; sline >> time; times.push_back(time))
;
for (size_t time = 0; sline >> time; times.push_back(time));
size_t idle_time = 0;
size_t total_time = 0;
@@ -27,6 +53,14 @@ std::vector<std::tuple<size_t, size_t>> waybar::modules::CpuUsage::parseCpuinfo(
total_time = std::accumulate(times.begin(), times.end(), 0);
}
cpuinfo.emplace_back(idle_time, total_time);
current_cpu_number++;
}
while (cpu_present_last >= current_cpu_number) {
// Fill in 0 for offline CPUs missing after the lines of /proc/stat
cpuinfo.emplace_back(0, 0);
current_cpu_number++;
}
return cpuinfo;
}
+105 -42
View File
@@ -2,17 +2,23 @@
#include <spdlog/spdlog.h>
#include <utility>
#include "util/scope_guard.hpp"
waybar::modules::Custom::Custom(const std::string& name, const std::string& id,
const Json::Value& config, const std::string& output_name)
: ALabel(config, "custom-" + name, id, "{}"),
: AIconLabel(config, "custom-" + name, id, "{}"),
name_(name),
output_name_(output_name),
id_(id),
tooltip_format_enabled_{config_["tooltip-format"].isString()},
percentage_(0),
fp_(nullptr),
pid_(-1) {
if (config.isNull()) {
spdlog::warn("There is no configuration for 'custom/{}', element will be hidden", name);
}
dp.emit();
if (!config_["signal"].empty() && config_["interval"].empty() &&
config_["restart-interval"].empty()) {
@@ -22,6 +28,15 @@ waybar::modules::Custom::Custom(const std::string& name, const std::string& id,
} else if (config_["exec"].isString()) {
continuousWorker();
}
if (config_["image-path"].isString()) {
image_path_ = config_["image-path"].asString();
}
if (config_["image-name"].isString()) {
image_name_ = config_["image-name"].asString();
}
if (config["icon-size"].isUInt()) {
app_icon_size_ = config["icon-size"].asUInt();
}
}
waybar::modules::Custom::~Custom() {
@@ -33,7 +48,19 @@ waybar::modules::Custom::~Custom() {
}
void waybar::modules::Custom::delayWorker() {
if (!config_["exec"].isString() && !config_["exec-if"].isString()) {
dp.emit();
return;
}
thread_ = [this] {
for (int i : this->pid_children_) {
int status;
waitpid(i, &status, 0);
}
this->pid_children_.clear();
bool can_update = true;
if (config_["exec-if"].isString()) {
output_ = util::command::execNoRead(config_["exec-if"].asString());
@@ -61,7 +88,7 @@ void waybar::modules::Custom::continuousWorker() {
}
thread_ = [this, cmd] {
char* buff = nullptr;
waybar::util::ScopeGuard buff_deleter([buff]() {
waybar::util::ScopeGuard buff_deleter([&buff]() {
if (buff) {
free(buff);
}
@@ -78,9 +105,11 @@ void waybar::modules::Custom::continuousWorker() {
dp.emit();
spdlog::error("{} stopped unexpectedly, is it endless?", name_);
}
if (config_["restart-interval"].isUInt()) {
if (config_["restart-interval"].isNumeric()) {
pid_ = -1;
thread_.sleep_for(std::chrono::seconds(config_["restart-interval"].asUInt()));
thread_.sleep_for(std::chrono::milliseconds(
std::max(1L, // Minimum 1ms due to millisecond precision
static_cast<long>(config_["restart-interval"].asDouble() * 1000))));
fp_ = util::command::open(cmd, pid_, output_name_);
if (!fp_) {
throw std::runtime_error("Unable to open " + cmd);
@@ -123,9 +152,11 @@ void waybar::modules::Custom::waitingWorker() {
}
void waybar::modules::Custom::refresh(int sig) {
if (sig == SIGRTMIN + config_["signal"].asInt()) {
#ifdef SIGRTMIN
if (config_["signal"].isInt() && sig == SIGRTMIN + config_["signal"].asInt()) {
thread_.wake_up();
}
#endif
}
void waybar::modules::Custom::handleEvent() {
@@ -158,47 +189,68 @@ auto waybar::modules::Custom::update() -> void {
parseOutputRaw();
}
auto str = fmt::format(fmt::runtime(format_), text_, fmt::arg("alt", alt_),
fmt::arg("icon", getIcon(percentage_, alt_)),
fmt::arg("percentage", percentage_));
if (str.empty()) {
event_box_.hide();
} else {
label_.set_markup(str);
if (tooltipEnabled()) {
if (text_ == tooltip_) {
if (label_.get_tooltip_markup() != str) {
label_.set_tooltip_markup(str);
}
} else if (config_["tooltip-format"].isString()) {
auto tooltip = config_["tooltip-format"].asString();
tooltip = fmt::format(fmt::runtime(tooltip), text_, fmt::arg("alt", alt_),
fmt::arg("icon", getIcon(percentage_, alt_)),
fmt::arg("percentage", percentage_));
label_.set_tooltip_markup(tooltip);
} else {
if (label_.get_tooltip_markup() != tooltip_) {
label_.set_tooltip_markup(tooltip_);
try {
auto str = fmt::format(fmt::runtime(format_), fmt::arg("text", text_), fmt::arg("alt", alt_),
fmt::arg("icon", getIcon(percentage_, alt_)),
fmt::arg("percentage", percentage_));
if ((config_["hide-empty-text"].asBool() && text_.empty()) ||
(str.empty() && image_path_.empty() && image_name_.empty())) {
event_box_.hide();
} else {
setLabelMarkup(str);
if (tooltipEnabled()) {
std::string tooltip_markup;
if (tooltip_format_enabled_) {
auto tooltip = config_["tooltip-format"].asString();
tooltip_markup = fmt::format(fmt::runtime(tooltip), fmt::arg("text", text_),
fmt::arg("tooltip", tooltip_), fmt::arg("alt", alt_),
fmt::arg("icon", getIcon(percentage_, alt_)),
fmt::arg("percentage", percentage_));
} else if (text_ == tooltip_) {
tooltip_markup = str;
} else {
tooltip_markup = tooltip_;
}
setTooltipMarkup(tooltip_markup);
}
auto style = label_.get_style_context();
auto classes = style->list_classes();
for (auto const& c : classes) {
if (c == id_) continue;
style->remove_class(c);
}
for (auto const& c : class_) {
style->add_class(c);
}
style->add_class("flat");
style->add_class("text-button");
style->add_class(MODULE_CLASS);
auto image_style = image_.get_style_context();
image_style->add_class("image-button");
event_box_.show();
if (!image_path_.empty()) {
auto pixbuf = Gdk::Pixbuf::create_from_file(image_path_, app_icon_size_, app_icon_size_);
image_.set(pixbuf);
} else if (!image_name_.empty()) {
image_.set_from_icon_name(image_name_, Gtk::ICON_SIZE_INVALID);
image_.set_pixel_size(app_icon_size_);
}
image_.set_visible(!image_name_.empty() || !image_path_.empty());
label_.set_visible(!str.empty());
}
auto style = label_.get_style_context();
auto classes = style->list_classes();
for (auto const& c : classes) {
if (c == id_) continue;
style->remove_class(c);
}
for (auto const& c : class_) {
style->add_class(c);
}
style->add_class("flat");
style->add_class("text-button");
style->add_class(MODULE_CLASS);
event_box_.show();
} catch (const fmt::format_error& e) {
if (std::strcmp(e.what(), "cannot switch from manual to automatic argument indexing") != 0)
throw;
throw fmt::format_error(
"mixing manual and automatic argument indexing is no longer supported; "
"try replacing \"{}\" with \"{text}\" in your format specifier");
}
}
// Call parent update
ALabel::update();
AIconLabel::update();
}
void waybar::modules::Custom::parseOutputRaw() {
@@ -214,13 +266,19 @@ void waybar::modules::Custom::parseOutputRaw() {
if (i == 0) {
if (config_["escape"].isBool() && config_["escape"].asBool()) {
text_ = Glib::Markup::escape_text(validated_line);
tooltip_ = Glib::Markup::escape_text(validated_line);
} else {
text_ = validated_line;
tooltip_ = validated_line;
}
tooltip_ = validated_line;
class_.clear();
} else if (i == 1) {
tooltip_ = validated_line;
if (config_["escape"].isBool() && config_["escape"].asBool()) {
tooltip_ = Glib::Markup::escape_text(validated_line);
} else {
tooltip_ = validated_line;
}
} else if (i == 2) {
class_.push_back(validated_line);
} else {
@@ -246,7 +304,11 @@ void waybar::modules::Custom::parseOutputJson() {
} else {
alt_ = parsed["alt"].asString();
}
tooltip_ = parsed["tooltip"].asString();
if (config_["escape"].isBool() && config_["escape"].asBool()) {
tooltip_ = Glib::Markup::escape_text(parsed["tooltip"].asString());
} else {
tooltip_ = parsed["tooltip"].asString();
}
if (parsed["class"].isString()) {
class_.push_back(parsed["class"].asString());
} else if (parsed["class"].isArray()) {
@@ -254,6 +316,7 @@ void waybar::modules::Custom::parseOutputJson() {
class_.push_back(c.asString());
}
}
if (!parsed["percentage"].asString().empty() && parsed["percentage"].isNumeric()) {
percentage_ = (int)lround(parsed["percentage"].asFloat());
} else {
+281
View File
@@ -0,0 +1,281 @@
#include "modules/custom_graph.hpp"
#include <spdlog/spdlog.h>
#include "util/scope_guard.hpp"
waybar::modules::CustomGraph::CustomGraph(const std::string& name, const std::string& id,
const Json::Value& config, const std::string& output_name)
: AGraph(config, "custom-graph-" + name, id),
name_(name),
output_name_(output_name),
id_(id),
tooltip_format_enabled_{config_["tooltip-format"].isString()},
percentage_(0),
fp_(nullptr),
pid_(-1) {
if (config.isNull()) {
spdlog::warn("There is no configuration for 'custom-graph/{}', element will be hidden", name);
}
dp.emit();
if (!config_["signal"].empty() && config_["interval"].empty() &&
config_["restart-interval"].empty()) {
waitingWorker();
} else if (interval_.count() > 0) {
delayWorker();
} else if (config_["exec"].isString()) {
continuousWorker();
}
}
waybar::modules::CustomGraph::~CustomGraph() {
if (pid_ != -1) {
killpg(pid_, SIGTERM);
waitpid(pid_, NULL, 0);
pid_ = -1;
}
}
void waybar::modules::CustomGraph::delayWorker() {
thread_ = [this] {
for (int i : this->pid_children_) {
int status;
waitpid(i, &status, 0);
}
this->pid_children_.clear();
bool can_update = true;
if (config_["exec-if"].isString()) {
output_ = util::command::execNoRead(config_["exec-if"].asString());
if (output_.exit_code != 0) {
can_update = false;
dp.emit();
}
}
if (can_update) {
if (config_["exec"].isString()) {
output_ = util::command::exec(config_["exec"].asString(), output_name_);
}
dp.emit();
}
thread_.sleep_for(interval_);
};
}
void waybar::modules::CustomGraph::continuousWorker() {
auto cmd = config_["exec"].asString();
pid_ = -1;
fp_ = util::command::open(cmd, pid_, output_name_);
if (!fp_) {
throw std::runtime_error("Unable to open " + cmd);
}
thread_ = [this, cmd] {
char* buff = nullptr;
waybar::util::ScopeGuard buff_deleter([&buff]() {
if (buff) {
free(buff);
}
});
size_t len = 0;
if (getline(&buff, &len, fp_) == -1) {
int exit_code = 1;
if (fp_) {
exit_code = WEXITSTATUS(util::command::close(fp_, pid_));
fp_ = nullptr;
}
if (exit_code != 0) {
output_ = {exit_code, ""};
dp.emit();
spdlog::error("{} stopped unexpectedly, is it endless?", name_);
}
if (config_["restart-interval"].isUInt()) {
pid_ = -1;
thread_.sleep_for(std::chrono::seconds(config_["restart-interval"].asUInt()));
fp_ = util::command::open(cmd, pid_, output_name_);
if (!fp_) {
throw std::runtime_error("Unable to open " + cmd);
}
} else {
thread_.stop();
return;
}
} else {
std::string output = buff;
// Remove last newline
if (!output.empty() && output[output.length() - 1] == '\n') {
output.erase(output.length() - 1);
}
output_ = {0, output};
dp.emit();
}
};
}
void waybar::modules::CustomGraph::waitingWorker() {
thread_ = [this] {
bool can_update = true;
if (config_["exec-if"].isString()) {
output_ = util::command::execNoRead(config_["exec-if"].asString());
if (output_.exit_code != 0) {
can_update = false;
dp.emit();
}
}
if (can_update) {
if (config_["exec"].isString()) {
output_ = util::command::exec(config_["exec"].asString(), output_name_);
}
dp.emit();
}
thread_.sleep();
};
}
void waybar::modules::CustomGraph::refresh(int sig) {
if (sig == SIGRTMIN + config_["signal"].asInt()) {
thread_.wake_up();
}
}
void waybar::modules::CustomGraph::handleEvent() {
if (!config_["exec-on-event"].isBool() || config_["exec-on-event"].asBool()) {
thread_.wake_up();
}
}
bool waybar::modules::CustomGraph::handleScroll(GdkEventScroll* e) {
auto ret = AGraph::handleScroll(e);
handleEvent();
return ret;
}
bool waybar::modules::CustomGraph::handleToggle(GdkEventButton* const& e) {
auto ret = AGraph::handleToggle(e);
handleEvent();
return ret;
}
auto waybar::modules::CustomGraph::update() -> void {
// Hide label if output is empty
if ((config_["exec"].isString() || config_["exec-if"].isString()) &&
(output_.out.empty() || output_.exit_code != 0)) {
event_box_.hide();
} else {
if (config_["return-type"].asString() == "json") {
parseOutputJson();
} else {
parseOutputRaw();
}
try {
addValue(percentage_);
if (tooltipEnabled()) {
if (tooltip_format_enabled_) {
auto tooltip = config_["tooltip-format"].asString();
tooltip = fmt::format(fmt::runtime(tooltip), fmt::arg("text", text_),
fmt::arg("alt", alt_), fmt::arg("percentage", percentage_));
graph_.set_tooltip_markup(tooltip);
} else {
if (graph_.get_tooltip_markup() != tooltip_) {
graph_.set_tooltip_markup(tooltip_);
}
}
}
auto style = graph_.get_style_context();
auto classes = style->list_classes();
for (auto const& c : classes) {
if (c == id_) continue;
style->remove_class(c);
}
for (auto const& c : class_) {
style->add_class(c);
}
style->add_class("flat");
style->add_class(MODULE_CLASS);
event_box_.show();
} catch (const fmt::format_error& e) {
if (std::strcmp(e.what(), "cannot switch from manual to automatic argument indexing") != 0)
throw;
throw fmt::format_error(
"mixing manual and automatic argument indexing is no longer supported; "
"try replacing \"{}\" with \"{text}\" in your format specifier");
}
}
// Call parent update
AGraph::update();
}
void waybar::modules::CustomGraph::parseOutputRaw() {
std::istringstream output(output_.out);
std::string line;
int i = 0;
while (getline(output, line)) {
Glib::ustring validated_line = line;
if (!validated_line.validate()) {
validated_line = validated_line.make_valid();
}
if (i == 0) {
if (config_["escape"].isBool() && config_["escape"].asBool()) {
text_ = Glib::Markup::escape_text(validated_line);
tooltip_ = Glib::Markup::escape_text(validated_line);
} else {
text_ = validated_line;
tooltip_ = validated_line;
}
class_.clear();
} else if (i == 1) {
if (config_["escape"].isBool() && config_["escape"].asBool()) {
tooltip_ = Glib::Markup::escape_text(validated_line);
} else {
tooltip_ = validated_line;
}
} else if (i == 2) {
class_.push_back(validated_line);
} else {
break;
}
i++;
}
}
void waybar::modules::CustomGraph::parseOutputJson() {
std::istringstream output(output_.out);
std::string line;
class_.clear();
while (getline(output, line)) {
auto parsed = parser_.parse(line);
if (config_["escape"].isBool() && config_["escape"].asBool()) {
text_ = Glib::Markup::escape_text(parsed["text"].asString());
} else {
text_ = parsed["text"].asString();
}
if (config_["escape"].isBool() && config_["escape"].asBool()) {
alt_ = Glib::Markup::escape_text(parsed["alt"].asString());
} else {
alt_ = parsed["alt"].asString();
}
if (config_["escape"].isBool() && config_["escape"].asBool()) {
tooltip_ = Glib::Markup::escape_text(parsed["tooltip"].asString());
} else {
tooltip_ = parsed["tooltip"].asString();
}
if (parsed["class"].isString()) {
class_.push_back(parsed["class"].asString());
} else if (parsed["class"].isArray()) {
for (auto const& c : parsed["class"]) {
class_.push_back(c.asString());
}
}
if (!parsed["percentage"].asString().empty() && parsed["percentage"].isNumeric()) {
percentage_ = (int)lround(parsed["percentage"].asFloat());
} else {
percentage_ = 0;
}
break;
}
}
+113 -64
View File
@@ -1,15 +1,32 @@
#include "modules/disk.hpp"
#include <spdlog/spdlog.h>
using namespace waybar::util;
waybar::modules::Disk::Disk(const std::string& id, const Json::Value& config)
: ALabel(config, "disk", id, "{}%", 30), path_("/") {
: ALabel(config, "disk", id, "{}%", 30), header_(""), paths_(), separator_(" ") {
thread_ = [this] {
dp.emit();
thread_.sleep_for(interval_);
};
if (config["path"].isString()) {
path_ = config["path"].asString();
if (config["header"].isString()) {
header_ = config["header"].asString();
}
if (config["path"].isString() && !config["paths"].isArray()) {
spdlog::warn("Disk: path is deprecated use paths instead!");
paths_.push_back(config["path"].asString());
}
if (config["paths"].isArray()) {
for (const auto& path : config["paths"]) {
paths_.push_back(path.asString());
}
}
if (!config["path"].isString() && !config["paths"].isArray()) {
paths_.emplace_back("/");
}
if (config["separator"].isString()) {
separator_ = config["separator"].asString();
}
if (config["unit"].isString()) {
unit_ = config["unit"].asString();
@@ -17,82 +34,114 @@ waybar::modules::Disk::Disk(const std::string& id, const Json::Value& config)
}
auto waybar::modules::Disk::update() -> void {
struct statvfs /* {
unsigned long f_bsize; // filesystem block size
unsigned long f_frsize; // fragment size
fsblkcnt_t f_blocks; // size of fs in f_frsize units
fsblkcnt_t f_bfree; // # free blocks
fsblkcnt_t f_bavail; // # free blocks for unprivileged users
fsfilcnt_t f_files; // # inodes
fsfilcnt_t f_ffree; // # free inodes
fsfilcnt_t f_favail; // # free inodes for unprivileged users
unsigned long f_fsid; // filesystem ID
unsigned long f_flag; // mount flags
unsigned long f_namemax; // maximum filename length
}; */
stats;
int err = statvfs(path_.c_str(), &stats);
std::string tooltip_label;
std::string label = header_;
/* Conky options
fs_bar - Bar that shows how much space is used
fs_free - Free space on a file system
fs_free_perc - Free percentage of space
fs_size - File system size
fs_used - File system used space
*/
bool had_valid_disk = false;
if (err != 0) {
event_box_.hide();
return;
}
for (size_t i = 0; i < paths_.size(); ++i) {
const auto& path = paths_[i];
float specific_free, specific_used, specific_total, divisor;
struct statvfs /* {
unsigned long f_bsize; // filesystem block size
unsigned long f_frsize; // fragment size
fsblkcnt_t f_blocks; // size of fs in f_frsize units
fsblkcnt_t f_bfree; // # free blocks
fsblkcnt_t f_bavail; // # free blocks for unprivileged users
fsfilcnt_t f_files; // # inodes
fsfilcnt_t f_ffree; // # free inodes
fsfilcnt_t f_favail; // # free inodes for unprivileged users
unsigned long f_fsid; // filesystem ID
unsigned long f_flag; // mount flags
unsigned long f_namemax; // maximum filename length
}; */
stats;
divisor = calc_specific_divisor(unit_);
specific_free = (stats.f_bavail * stats.f_frsize) / divisor;
specific_used = ((stats.f_blocks - stats.f_bfree) * stats.f_frsize) / divisor;
specific_total = (stats.f_blocks * stats.f_frsize) / divisor;
int err = statvfs(path.c_str(), &stats);
auto free = pow_format(stats.f_bavail * stats.f_frsize, "B", true);
auto used = pow_format((stats.f_blocks - stats.f_bfree) * stats.f_frsize, "B", true);
auto total = pow_format(stats.f_blocks * stats.f_frsize, "B", true);
auto percentage_used = (stats.f_blocks - stats.f_bfree) * 100 / stats.f_blocks;
/* Conky options
fs_bar - Bar that shows how much space is used
fs_free - Free space on a file system
fs_free_perc - Free percentage of space
fs_size - File system size
fs_used - File system used space
*/
auto format = format_;
auto state = getState(percentage_used);
if (!state.empty() && config_["format-" + state].isString()) {
format = config_["format-" + state].asString();
}
if (err != 0 || stats.f_blocks == 0) {
spdlog::warn("Disk: statvfs failed for path '{}' (errno={})", path, errno);
continue;
}
if (format.empty()) {
event_box_.hide();
} else {
event_box_.show();
label_.set_markup(fmt::format(
fmt::runtime(format), stats.f_bavail * 100 / stats.f_blocks, fmt::arg("free", free),
fmt::arg("percentage_free", stats.f_bavail * 100 / stats.f_blocks), fmt::arg("used", used),
fmt::arg("percentage_used", percentage_used), fmt::arg("total", total),
fmt::arg("path", path_), fmt::arg("specific_free", specific_free),
fmt::arg("specific_used", specific_used), fmt::arg("specific_total", specific_total)));
}
float specific_free, specific_used, specific_total, divisor;
divisor = calc_specific_divisor(unit_);
specific_free = (stats.f_bavail * stats.f_frsize) / divisor;
specific_used = ((stats.f_blocks - stats.f_bfree) * stats.f_frsize) / divisor;
specific_total = (stats.f_blocks * stats.f_frsize) / divisor;
auto free = pow_format(stats.f_bavail * stats.f_frsize, "B", true);
auto used = pow_format((stats.f_blocks - stats.f_bfree) * stats.f_frsize, "B", true);
auto total = pow_format(stats.f_blocks * stats.f_frsize, "B", true);
auto percentage_used = (stats.f_blocks - stats.f_bfree) * 100 / stats.f_blocks;
std::string disk_format = format_;
auto state = getState(percentage_used);
if (!state.empty() && config_["format-" + state].isString()) {
disk_format = config_["format-" + state].asString();
}
if (!disk_format.empty()) {
if (had_valid_disk) {
label += separator_;
}
label += fmt::format(
fmt::runtime(disk_format), stats.f_bavail * 100 / stats.f_blocks, fmt::arg("free", free),
fmt::arg("percentage_free", stats.f_bavail * 100 / stats.f_blocks),
fmt::arg("used", used), fmt::arg("percentage_used", percentage_used),
fmt::arg("total", total), fmt::arg("path", path),
fmt::arg("specific_free", specific_free), fmt::arg("specific_used", specific_used),
fmt::arg("specific_total", specific_total));
}
if (tooltipEnabled()) {
std::string tooltip_format = "{used} used out of {total} on {path} ({percentage_used}%)";
if (config_["tooltip-format"].isString()) {
tooltip_format = config_["tooltip-format"].asString();
}
label_.set_tooltip_text(fmt::format(
fmt::runtime(tooltip_format), stats.f_bavail * 100 / stats.f_blocks, fmt::arg("free", free),
fmt::arg("percentage_free", stats.f_bavail * 100 / stats.f_blocks), fmt::arg("used", used),
fmt::arg("percentage_used", percentage_used), fmt::arg("total", total),
fmt::arg("path", path_), fmt::arg("specific_free", specific_free),
fmt::arg("specific_used", specific_used), fmt::arg("specific_total", specific_total)));
if (!tooltip_format.empty()) {
if (had_valid_disk) {
tooltip_label += "\n";
}
tooltip_label += fmt::format(
fmt::runtime(tooltip_format), stats.f_bavail * 100 / stats.f_blocks,
fmt::arg("free", free),
fmt::arg("percentage_free", stats.f_bavail * 100 / stats.f_blocks),
fmt::arg("used", used), fmt::arg("percentage_used", percentage_used),
fmt::arg("total", total), fmt::arg("path", path),
fmt::arg("specific_free", specific_free), fmt::arg("specific_used", specific_used),
fmt::arg("specific_total", specific_total));
}
had_valid_disk = true;
}
if (had_valid_disk) {
event_box_.show();
} else {
event_box_.hide();
}
setLabelMarkup(label);
if (tooltipEnabled() && !tooltip_label.empty()) {
setTooltipMarkup(tooltip_label);
}
// Call parent update
ALabel::update();
}
float waybar::modules::Disk::calc_specific_divisor(std::string divisor) {
float waybar::modules::Disk::calc_specific_divisor(const std::string& divisor) {
if (divisor == "kB") {
return 1000.0;
} else if (divisor == "kiB") {
@@ -109,7 +158,7 @@ float waybar::modules::Disk::calc_specific_divisor(std::string divisor) {
return 1000.0 * 1000.0 * 1000.0 * 1000.0;
} else if (divisor == "TiB") {
return 1024.0 * 1024.0 * 1024.0 * 1024.0;
} else { // default to Bytes if it is anything that we don't recongnise
} else { // default to Bytes if it is anything that we don't recognise
return 1.0;
}
}
}
+72 -27
View File
@@ -21,40 +21,40 @@ wl_array tags, layouts;
static uint num_tags = 0;
void toggle_visibility(void *data, zdwl_ipc_output_v2 *zdwl_output_v2) {
static void toggle_visibility(void* data, zdwl_ipc_output_v2* zdwl_output_v2) {
// Intentionally empty
}
void active(void *data, zdwl_ipc_output_v2 *zdwl_output_v2, uint32_t active) {
// Intentionally empty
static void active(void* data, zdwl_ipc_output_v2* zdwl_output_v2, uint32_t active) {
static_cast<Tags*>(data)->handle_active_output(zdwl_output_v2, active);
}
static void set_tag(void *data, zdwl_ipc_output_v2 *zdwl_output_v2, uint32_t tag, uint32_t state,
static void set_tag(void* data, zdwl_ipc_output_v2* zdwl_output_v2, uint32_t tag, uint32_t state,
uint32_t clients, uint32_t focused) {
static_cast<Tags *>(data)->handle_view_tags(tag, state, clients, focused);
static_cast<Tags*>(data)->handle_view_tags(tag, state, clients, focused);
num_tags = (state & ZDWL_IPC_OUTPUT_V2_TAG_STATE_ACTIVE) ? num_tags | (1 << tag)
: num_tags & ~(1 << tag);
}
void set_layout_symbol(void *data, zdwl_ipc_output_v2 *zdwl_output_v2, const char *layout) {
static void set_layout_symbol(void* data, zdwl_ipc_output_v2* zdwl_output_v2, const char* layout) {
// Intentionally empty
}
void title(void *data, zdwl_ipc_output_v2 *zdwl_output_v2, const char *title) {
static void title(void* data, zdwl_ipc_output_v2* zdwl_output_v2, const char* title) {
// Intentionally empty
}
void dwl_frame(void *data, zdwl_ipc_output_v2 *zdwl_output_v2) {
static void dwl_frame(void* data, zdwl_ipc_output_v2* zdwl_output_v2) {
// Intentionally empty
}
static void set_layout(void *data, zdwl_ipc_output_v2 *zdwl_output_v2, uint32_t layout) {
static void set_layout(void* data, zdwl_ipc_output_v2* zdwl_output_v2, uint32_t layout) {
// Intentionally empty
}
static void appid(void *data, zdwl_ipc_output_v2 *zdwl_output_v2, const char *appid){
// Intentionally empty
static void appid(void* data, zdwl_ipc_output_v2* zdwl_output_v2, const char* appid) {
// Intentionally empty
};
static const zdwl_ipc_output_v2_listener output_status_listener_impl{
@@ -68,35 +68,55 @@ static const zdwl_ipc_output_v2_listener output_status_listener_impl{
.frame = dwl_frame,
};
static void handle_global(void *data, struct wl_registry *registry, uint32_t name,
const char *interface, uint32_t version) {
static void handle_global(void* data, struct wl_registry* registry, uint32_t name,
const char* interface, uint32_t version) {
if (std::strcmp(interface, zdwl_ipc_manager_v2_interface.name) == 0) {
static_cast<Tags *>(data)->status_manager_ = static_cast<struct zdwl_ipc_manager_v2 *>(
(zdwl_ipc_manager_v2 *)wl_registry_bind(registry, name, &zdwl_ipc_manager_v2_interface, 1));
auto* self = static_cast<Tags*>(data);
if (self->status_manager_) {
zdwl_ipc_manager_v2_destroy(self->status_manager_);
self->status_manager_ = nullptr;
}
self->status_manager_ = static_cast<struct zdwl_ipc_manager_v2*>(
wl_registry_bind(registry, name, &zdwl_ipc_manager_v2_interface, 1));
}
if (std::strcmp(interface, wl_seat_interface.name) == 0) {
auto* self = static_cast<Tags*>(data);
if (self->seat_) {
wl_seat_destroy(self->seat_);
self->seat_ = nullptr;
}
version = std::min<uint32_t>(version, 1);
static_cast<Tags *>(data)->seat_ = static_cast<struct wl_seat *>(
wl_registry_bind(registry, name, &wl_seat_interface, version));
self->seat_ =
static_cast<struct wl_seat*>(wl_registry_bind(registry, name, &wl_seat_interface, version));
}
}
static void handle_global_remove(void *data, struct wl_registry *registry, uint32_t name) {
static void handle_global_remove(void* data, struct wl_registry* registry, uint32_t name) {
/* Ignore event */
}
static const wl_registry_listener registry_listener_impl = {.global = handle_global,
.global_remove = handle_global_remove};
Tags::Tags(const std::string &id, const waybar::Bar &bar, const Json::Value &config)
Tags::Tags(const std::string& id, const waybar::Bar& bar, const Json::Value& config)
: waybar::AModule(config, "tags", id, false, false),
status_manager_{nullptr},
seat_{nullptr},
bar_(bar),
box_{bar.orientation, 0},
hide_vacant_(false),
output_status_{nullptr} {
struct wl_display *display = Client::inst()->wl_display;
struct wl_registry *registry = wl_display_get_registry(display);
if (config_["hide-vacant"].asBool()) {
hide_vacant_ = config_["hide-vacant"].asBool();
}
struct wl_display* display = Client::inst()->wl_display;
struct wl_registry* registry = wl_display_get_registry(display);
wl_registry_add_listener(registry, &registry_listener_impl, this);
wl_display_roundtrip(display);
@@ -132,8 +152,8 @@ Tags::Tags(const std::string &id, const waybar::Bar &bar, const Json::Value &con
}
uint32_t i = 1;
for (const auto &tag_label : tag_labels) {
Gtk::Button &button = buttons_.emplace_back(tag_label);
for (const auto& tag_label : tag_labels) {
Gtk::Button& button = buttons_.emplace_back(tag_label);
button.set_relief(Gtk::RELIEF_NONE);
box_.pack_start(button, false, false, 0);
if (!config_["disable-click"].asBool()) {
@@ -155,7 +175,7 @@ Tags::Tags(const std::string &id, const waybar::Bar &bar, const Json::Value &con
}
Tags::~Tags() {
if(output_status_) {
if (output_status_) {
zdwl_ipc_output_v2_destroy(output_status_);
}
if (status_manager_) {
@@ -169,7 +189,7 @@ void Tags::handle_primary_clicked(uint32_t tag) {
zdwl_ipc_output_v2_set_tags(output_status_, tag, 1);
}
bool Tags::handle_button_press(GdkEventButton *event_button, uint32_t tag) {
bool Tags::handle_button_press(GdkEventButton* event_button, uint32_t tag) {
if (event_button->type == GDK_BUTTON_PRESS && event_button->button == 3) {
if (!output_status_) return true;
zdwl_ipc_output_v2_set_tags(output_status_, num_tags ^ tag, 0);
@@ -178,14 +198,21 @@ bool Tags::handle_button_press(GdkEventButton *event_button, uint32_t tag) {
}
void Tags::handle_view_tags(uint32_t tag, uint32_t state, uint32_t clients, uint32_t focused) {
if (tag >= buttons_.size()) return;
// First clear all occupied state
auto &button = buttons_[tag];
auto& button = buttons_[tag];
if (clients) {
button.get_style_context()->add_class("occupied");
} else {
button.get_style_context()->remove_class("occupied");
}
if (clients & TAG_INACTIVE) {
button.get_style_context()->remove_class("empty");
} else {
button.get_style_context()->add_class("empty");
}
if (state & TAG_ACTIVE) {
button.get_style_context()->add_class("focused");
} else {
@@ -197,6 +224,24 @@ void Tags::handle_view_tags(uint32_t tag, uint32_t state, uint32_t clients, uint
} else {
button.get_style_context()->remove_class("urgent");
}
if (hide_vacant_ && !clients && !(state & TAG_ACTIVE)) {
button.set_visible(false);
} else {
button.set_visible(true);
}
}
void Tags::handle_active_output(zdwl_ipc_output_v2* zdwl_output_v2, uint32_t active) {
if (output_status_ == zdwl_output_v2) {
for (size_t i = 0; i < buttons_.size(); ++i) {
if (active == 0) {
buttons_[i].get_style_context()->remove_class("output");
} else {
buttons_[i].get_style_context()->add_class("output");
}
}
}
}
} /* namespace waybar::modules::dwl */
+148
View File
@@ -0,0 +1,148 @@
#include "modules/dwl/window.hpp"
#include <gdkmm/pixbuf.h>
#include <glibmm/fileutils.h>
#include <glibmm/keyfile.h>
#include <glibmm/miscutils.h>
#include <gtkmm/enums.h>
#include <spdlog/spdlog.h>
#include "client.hpp"
#include "dwl-ipc-unstable-v2-client-protocol.h"
#include "glibmm/markup.h"
#include "util/rewrite_string.hpp"
namespace waybar::modules::dwl {
static void toggle_visibility(void* data, zdwl_ipc_output_v2* zdwl_output_v2) {
// Intentionally empty
}
static void active(void* data, zdwl_ipc_output_v2* zdwl_output_v2, uint32_t active) {
static_cast<Window*>(data)->handle_active(active);
}
static void set_tag(void* data, zdwl_ipc_output_v2* zdwl_output_v2, uint32_t tag, uint32_t state,
uint32_t clients, uint32_t focused) {
// Intentionally empty
}
static void set_layout_symbol(void* data, zdwl_ipc_output_v2* zdwl_output_v2, const char* layout) {
static_cast<Window*>(data)->handle_layout_symbol(layout);
}
static void title(void* data, zdwl_ipc_output_v2* zdwl_output_v2, const char* title) {
static_cast<Window*>(data)->handle_title(title);
}
static void dwl_frame(void* data, zdwl_ipc_output_v2* zdwl_output_v2) {
static_cast<Window*>(data)->handle_frame();
}
static void set_layout(void* data, zdwl_ipc_output_v2* zdwl_output_v2, uint32_t layout) {
static_cast<Window*>(data)->handle_layout(layout);
}
static void appid(void* data, zdwl_ipc_output_v2* zdwl_output_v2, const char* appid) {
static_cast<Window*>(data)->handle_appid(appid);
};
static const zdwl_ipc_output_v2_listener output_status_listener_impl{
.toggle_visibility = toggle_visibility,
.active = active,
.tag = set_tag,
.layout = set_layout,
.title = title,
.appid = appid,
.layout_symbol = set_layout_symbol,
.frame = dwl_frame,
};
static void handle_global(void* data, struct wl_registry* registry, uint32_t name,
const char* interface, uint32_t version) {
if (std::strcmp(interface, zdwl_ipc_manager_v2_interface.name) == 0) {
static_cast<Window*>(data)->status_manager_ = static_cast<struct zdwl_ipc_manager_v2*>(
(zdwl_ipc_manager_v2*)wl_registry_bind(registry, name, &zdwl_ipc_manager_v2_interface, 1));
}
}
static void handle_global_remove(void* data, struct wl_registry* registry, uint32_t name) {
/* Ignore event */
}
static const wl_registry_listener registry_listener_impl = {.global = handle_global,
.global_remove = handle_global_remove};
Window::Window(const std::string& id, const Bar& bar, const Json::Value& config)
: AAppIconLabel(config, "window", id, "{}", 0, true),
bar_(bar),
active_(false),
hide_inactive_(false),
hide_empty_(false) {
if (config_["hide-inactive"].isBool()) {
hide_inactive_ = config["hide-inactive"].asBool();
}
if (config_["hide-empty"].isBool()) {
hide_empty_ = config["hide-empty"].asBool();
}
struct wl_display* display = Client::inst()->wl_display;
struct wl_registry* registry = wl_display_get_registry(display);
wl_registry_add_listener(registry, &registry_listener_impl, this);
wl_display_roundtrip(display);
if (status_manager_ == nullptr) {
spdlog::error("dwl_status_manager_v2 not advertised");
return;
}
struct wl_output* output = gdk_wayland_monitor_get_wl_output(bar_.output->monitor->gobj());
output_status_ = zdwl_ipc_manager_v2_get_output(status_manager_, output);
zdwl_ipc_output_v2_add_listener(output_status_, &output_status_listener_impl, this);
zdwl_ipc_manager_v2_destroy(status_manager_);
}
Window::~Window() {
if (output_status_ != nullptr) {
zdwl_ipc_output_v2_destroy(output_status_);
}
}
void Window::handle_title(const char* title) { title_ = Glib::Markup::escape_text(title); }
void Window::handle_appid(const char* appid) { appid_ = Glib::Markup::escape_text(appid); }
void Window::handle_active(const uint32_t active) { active_ = active != 0; }
void Window::handle_layout_symbol(const char* layout_symbol) {
layout_symbol_ = Glib::Markup::escape_text(layout_symbol);
}
void Window::handle_layout(const uint32_t layout) { layout_ = layout; }
void Window::handle_frame() {
label_.set_markup(waybar::util::rewriteString(
fmt::format(fmt::runtime(format_), fmt::arg("title", title_),
fmt::arg("layout", layout_symbol_), fmt::arg("app_id", appid_)),
config_["rewrite"]));
updateAppIconName(appid_, "");
updateAppIcon();
if (tooltipEnabled()) {
label_.set_tooltip_markup(title_);
}
if (hide_empty_ && title_.empty()) {
box_.set_visible(false);
} else {
if (active_) {
box_.get_style_context()->add_class("active");
box_.set_visible(true);
} else {
box_.get_style_context()->remove_class("active");
if (hide_inactive_) {
box_.set_visible(false);
}
}
}
}
} // namespace waybar::modules::dwl
+506
View File
@@ -0,0 +1,506 @@
#include "modules/ext/workspace_manager.hpp"
#include <gdk/gdkwayland.h>
#include <gtkmm.h>
#include <spdlog/spdlog.h>
#include <algorithm>
#include <iostream>
#include <vector>
#include "client.hpp"
#include "gtkmm/widget.h"
#include "modules/ext/workspace_manager_binding.hpp"
namespace waybar::modules::ext {
// WorkspaceManager
uint32_t WorkspaceManager::group_global_id = 0;
uint32_t WorkspaceManager::workspace_global_id = 0;
std::map<std::string, std::string> Workspace::icon_map_;
WorkspaceManager::WorkspaceManager(const std::string& id, const waybar::Bar& bar,
const Json::Value& config)
: waybar::AModule(config, "workspaces", id, false, false), bar_(bar), box_(bar.orientation, 0) {
add_registry_listener(this);
// parse configuration
const auto config_sort_by_number = config_["sort-by-number"];
if (config_sort_by_number.isBool()) {
spdlog::warn("[ext/workspaces]: Prefer sort-by-id instead of sort-by-number");
sort_by_id_ = config_sort_by_number.asBool();
}
const auto config_sort_by_id = config_["sort-by-id"];
if (config_sort_by_id.isBool()) {
sort_by_id_ = config_sort_by_id.asBool();
}
const auto config_sort_by_name = config_["sort-by-name"];
if (config_sort_by_name.isBool()) {
sort_by_name_ = config_sort_by_name.asBool();
}
const auto config_sort_by_coordinates = config_["sort-by-coordinates"];
if (config_sort_by_coordinates.isBool()) {
sort_by_coordinates_ = config_sort_by_coordinates.asBool();
}
const auto config_all_outputs = config_["all-outputs"];
if (config_all_outputs.isBool()) {
all_outputs_ = config_all_outputs.asBool();
}
// setup UI
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_);
spdlog::debug("[ext/workspaces]: Workspace manager created");
}
WorkspaceManager::~WorkspaceManager() {
workspaces_.clear();
groups_.clear();
if (ext_manager_ != nullptr) {
auto* display = Client::inst()->wl_display;
// Send `stop` request and wait for one roundtrip.
ext_workspace_manager_v1_stop(ext_manager_);
wl_display_roundtrip(display);
}
if (ext_manager_ != nullptr) {
spdlog::warn("[ext/workspaces]: Destroying workspace manager before .finished event");
ext_workspace_manager_v1_destroy(ext_manager_);
}
spdlog::debug("[ext/workspaces]: Workspace manager destroyed");
}
void WorkspaceManager::register_manager(wl_registry* registry, uint32_t name, uint32_t version) {
if (ext_manager_ != nullptr) {
spdlog::warn("[ext/workspaces]: Register workspace manager again although already registered!");
return;
}
if (version != 1) {
spdlog::warn("[ext/workspaces]: Using different workspace manager protocol version: {}",
version);
}
ext_manager_ = workspace_manager_bind(registry, name, version, this);
}
void WorkspaceManager::remove_workspace_group(uint32_t id) {
const auto it =
std::find_if(groups_.begin(), groups_.end(), [id](const auto& g) { return g->id() == id; });
if (it == groups_.end()) {
spdlog::warn("[ext/workspaces]: Can't find workspace group with id {}", id);
return;
}
groups_.erase(it);
}
void WorkspaceManager::remove_workspace(uint32_t id) {
const auto it = std::find_if(workspaces_.begin(), workspaces_.end(),
[id](const auto& w) { return w->id() == id; });
if (it == workspaces_.end()) {
spdlog::warn("[ext/workspaces]: Can't find workspace with id {}", id);
return;
}
workspaces_.erase(it);
}
void WorkspaceManager::handle_workspace_group(ext_workspace_group_handle_v1* handle) {
const auto new_id = ++group_global_id;
groups_.push_back(std::make_unique<WorkspaceGroup>(*this, handle, new_id));
spdlog::debug("[ext/workspaces]: Workspace group {} created", new_id);
}
void WorkspaceManager::handle_workspace(ext_workspace_handle_v1* handle) {
const auto new_id = ++workspace_global_id;
const auto new_name = std::to_string(++workspace_name);
workspaces_.push_back(std::make_unique<Workspace>(config_, *this, handle, new_id, new_name));
set_needs_sorting();
spdlog::debug("[ext/workspaces]: Workspace {} created", new_id);
}
void WorkspaceManager::handle_done() { dp.emit(); }
void WorkspaceManager::handle_finished() {
spdlog::debug("[ext/workspaces]: Finishing workspace manager");
ext_workspace_manager_v1_destroy(ext_manager_);
ext_manager_ = nullptr;
}
void WorkspaceManager::commit() const {
if (ext_manager_) ext_workspace_manager_v1_commit(ext_manager_);
}
void WorkspaceManager::update() {
spdlog::debug("[ext/workspaces]: Updating state");
if (needs_sorting_) {
clear_buttons();
sort_workspaces();
needs_sorting_ = false;
}
update_buttons();
AModule::update();
}
bool WorkspaceManager::has_button(const Gtk::Button* button) {
const auto buttons = box_.get_children();
return std::find(buttons.begin(), buttons.end(), button) != buttons.end();
}
void WorkspaceManager::sort_workspaces() {
// determine if workspace ID's and names can be sort numerically or literally
auto is_numeric = [](const std::string& s) {
return !s.empty() && std::all_of(s.begin(), s.end(), ::isdigit);
};
auto sort_by_workspace_id_numerically =
std::all_of(workspaces_.begin(), workspaces_.end(),
[&](const auto& w) { return is_numeric(w->workspace_id()); });
auto sort_by_name_numerically = std::all_of(workspaces_.begin(), workspaces_.end(),
[&](const auto& w) { return is_numeric(w->name()); });
// sort based on configuration setting with sort-by-id as fallback
std::sort(workspaces_.begin(), workspaces_.end(), [&](const auto& w1, const auto& w2) {
if (sort_by_id_ || (!sort_by_name_ && !sort_by_coordinates_)) {
if (w1->workspace_id() == w2->workspace_id()) {
return w1->id() < w2->id();
}
if (sort_by_workspace_id_numerically) {
// the idea is that phonetic compare can be applied just to numbers
// with same number of digits
return w1->workspace_id().size() < w2->workspace_id().size() ||
(w1->workspace_id().size() == w2->workspace_id().size() &&
w1->workspace_id() < w2->workspace_id());
}
return w1->workspace_id() < w2->workspace_id();
}
if (sort_by_name_) {
if (w1->name() == w2->name()) {
return w1->id() < w2->id();
}
if (sort_by_name_numerically) {
// see above about numeric sorting
return w1->name().size() < w2->name().size() ||
(w1->name().size() == w2->name().size() && w1->name() < w2->name());
}
return w1->name() < w2->name();
}
if (sort_by_coordinates_) {
if (w1->coordinates() == w2->coordinates()) {
return w1->id() < w2->id();
}
return w1->coordinates() < w2->coordinates();
}
return w1->id() < w2->id();
});
}
void WorkspaceManager::clear_buttons() {
for (const auto& workspace : workspaces_) {
if (has_button(&workspace->button())) {
box_.remove(workspace->button());
}
}
}
void WorkspaceManager::update_buttons() {
const auto* output = gdk_wayland_monitor_get_wl_output(bar_.output->monitor->gobj());
// go through all workspace
for (const auto& workspace : workspaces_) {
const bool workspace_on_any_group_for_output =
std::any_of(groups_.begin(), groups_.end(), [&](const auto& group) {
const bool group_on_output = group->has_output(output) || all_outputs_;
const bool workspace_on_group = group->has_workspace(workspace->handle());
return group_on_output && workspace_on_group;
});
const bool bar_contains_button = has_button(&workspace->button());
// add or remove buttons if needed, update button state
if (workspace_on_any_group_for_output) {
if (!bar_contains_button) {
// add button to bar
box_.pack_start(workspace->button(), false, false);
workspace->button().show_all();
}
workspace->update();
} else {
if (bar_contains_button) {
// remove button from bar
box_.remove(workspace->button());
}
}
}
}
// WorkspaceGroup
WorkspaceGroup::WorkspaceGroup(WorkspaceManager& manager, ext_workspace_group_handle_v1* handle,
uint32_t id)
: workspaces_manager_(manager), ext_handle_(handle), id_(id) {
add_workspace_group_listener(ext_handle_, this);
}
WorkspaceGroup::~WorkspaceGroup() {
if (ext_handle_ != nullptr) {
ext_workspace_group_handle_v1_destroy(ext_handle_);
}
spdlog::debug("[ext/workspaces]: Workspace group {} destroyed", id_);
}
bool WorkspaceGroup::has_output(const wl_output* output) {
return std::find(outputs_.begin(), outputs_.end(), output) != outputs_.end();
}
bool WorkspaceGroup::has_workspace(const ext_workspace_handle_v1* workspace) {
return std::find(workspaces_.begin(), workspaces_.end(), workspace) != workspaces_.end();
}
void WorkspaceGroup::handle_capabilities(uint32_t capabilities) {
spdlog::debug("[ext/workspaces]: Capabilities for workspace group {}:", id_);
if ((capabilities & EXT_WORKSPACE_GROUP_HANDLE_V1_GROUP_CAPABILITIES_CREATE_WORKSPACE) ==
capabilities) {
spdlog::debug("[ext/workspaces]: - create-workspace");
}
}
void WorkspaceGroup::handle_output_enter(wl_output* output) { outputs_.push_back(output); }
void WorkspaceGroup::handle_output_leave(wl_output* output) {
const auto it = std::find(outputs_.begin(), outputs_.end(), output);
if (it != outputs_.end()) {
outputs_.erase(it);
}
}
void WorkspaceGroup::handle_workspace_enter(ext_workspace_handle_v1* handle) {
workspaces_.push_back(handle);
}
void WorkspaceGroup::handle_workspace_leave(ext_workspace_handle_v1* handle) {
const auto it = std::find(workspaces_.begin(), workspaces_.end(), handle);
if (it != workspaces_.end()) {
workspaces_.erase(it);
}
}
void WorkspaceGroup::handle_removed() {
spdlog::debug("[ext/workspaces]: Removing workspace group {}", id_);
workspaces_manager_.remove_workspace_group(id_);
}
// Workspace
Workspace::Workspace(const Json::Value& config, WorkspaceManager& manager,
ext_workspace_handle_v1* handle, uint32_t id, const std::string& name)
: workspace_manager_(manager), ext_handle_(handle), id_(id), workspace_id_(name), name_(name) {
add_workspace_listener(ext_handle_, this);
// parse configuration
const auto& config_active_only = config["active-only"];
if (config_active_only.isBool()) {
active_only_ = config_active_only.asBool();
}
const auto& config_ignore_hidden = config["ignore-hidden"];
if (config_ignore_hidden.isBool()) {
ignore_hidden_ = config_ignore_hidden.asBool();
}
const auto& config_format = config["format"];
format_ = config_format.isString() ? config_format.asString() : "{name}";
with_icon_ = format_.find("{icon}") != std::string::npos;
if (with_icon_ && icon_map_.empty()) {
const auto& format_icons = config["format-icons"];
for (auto& n : format_icons.getMemberNames()) {
icon_map_.emplace(n, format_icons[n].asString());
}
}
const bool config_on_click = config["on-click"].isString();
if (config_on_click) {
on_click_action_ = config["on-click"].asString();
}
const bool config_on_click_middle = config["on-click-middle"].isString();
if (config_on_click_middle) {
on_click_middle_action_ = config["on-click-middle"].asString();
}
const bool config_on_click_right = config["on-click-right"].isString();
if (config_on_click_right) {
on_click_right_action_ = config["on-click-right"].asString();
}
// setup UI
if (config_on_click || config_on_click_middle || config_on_click_right) {
button_.add_events(Gdk::BUTTON_PRESS_MASK);
button_.signal_button_press_event().connect(sigc::mem_fun(*this, &Workspace::handle_clicked),
false);
}
button_.set_relief(Gtk::RELIEF_NONE);
content_.set_center_widget(label_);
button_.add(content_);
}
Workspace::~Workspace() {
if (ext_handle_ != nullptr) {
ext_workspace_handle_v1_destroy(ext_handle_);
}
spdlog::debug("[ext/workspaces]: Workspace {} destroyed", id_);
}
void Workspace::update() {
const auto style_context = button_.get_style_context();
// update style and visibility
if (!has_state(EXT_WORKSPACE_HANDLE_V1_STATE_ACTIVE)) {
style_context->remove_class("active");
}
if (!has_state(EXT_WORKSPACE_HANDLE_V1_STATE_URGENT)) {
style_context->remove_class("urgent");
}
if (!has_state(EXT_WORKSPACE_HANDLE_V1_STATE_HIDDEN)) {
style_context->remove_class("hidden");
}
if (has_state(EXT_WORKSPACE_HANDLE_V1_STATE_ACTIVE)) {
button_.set_visible(true);
style_context->add_class("active");
}
if (has_state(EXT_WORKSPACE_HANDLE_V1_STATE_URGENT)) {
button_.set_visible(true);
style_context->add_class("urgent");
}
if (has_state(EXT_WORKSPACE_HANDLE_V1_STATE_HIDDEN)) {
button_.set_visible(!active_only_ && !ignore_hidden_);
style_context->add_class("hidden");
}
if (state_ == 0) {
button_.set_visible(!active_only_);
}
// update label
label_.set_markup(fmt::format(fmt::runtime(format_), fmt::arg("name", name_),
fmt::arg("id", workspace_id_),
fmt::arg("icon", with_icon_ ? icon() : "")));
}
void Workspace::handle_id(const std::string& id) {
spdlog::debug("[ext/workspaces]: ID for workspace {}: {}", id_, id);
workspace_id_ = id;
workspace_manager_.set_needs_sorting();
}
void Workspace::handle_name(const std::string& name) {
spdlog::debug("[ext/workspaces]: Name for workspace {}: {}", id_, name);
name_ = name;
workspace_manager_.set_needs_sorting();
}
void Workspace::handle_coordinates(const std::vector<uint32_t>& coordinates) {
coordinates_ = coordinates;
workspace_manager_.set_needs_sorting();
}
void Workspace::handle_state(uint32_t state) { state_ = state; }
void Workspace::handle_capabilities(uint32_t capabilities) {
spdlog::debug("[ext/workspaces]: Capabilities for workspace {}:", id_);
if ((capabilities & EXT_WORKSPACE_HANDLE_V1_WORKSPACE_CAPABILITIES_ACTIVATE) == capabilities) {
spdlog::debug("[ext/workspaces]: - activate");
}
if ((capabilities & EXT_WORKSPACE_HANDLE_V1_WORKSPACE_CAPABILITIES_DEACTIVATE) == capabilities) {
spdlog::debug("[ext/workspaces]: - deactivate");
}
if ((capabilities & EXT_WORKSPACE_HANDLE_V1_WORKSPACE_CAPABILITIES_REMOVE) == capabilities) {
spdlog::debug("[ext/workspaces]: - remove");
}
if ((capabilities & EXT_WORKSPACE_HANDLE_V1_WORKSPACE_CAPABILITIES_ASSIGN) == capabilities) {
spdlog::debug("[ext/workspaces]: - assign");
}
}
void Workspace::handle_removed() {
spdlog::debug("[ext/workspaces]: Removing workspace {}", id_);
workspace_manager_.remove_workspace(id_);
}
bool Workspace::handle_clicked(const GdkEventButton* button) const {
std::string action;
if (button->button == GDK_BUTTON_PRIMARY) {
action = on_click_action_;
} else if (button->button == GDK_BUTTON_MIDDLE) {
action = on_click_middle_action_;
} else if (button->button == GDK_BUTTON_SECONDARY) {
action = on_click_right_action_;
}
if (action.empty()) {
return true;
}
if (action == "activate") {
ext_workspace_handle_v1_activate(ext_handle_);
} else if (action == "deactivate") {
ext_workspace_handle_v1_deactivate(ext_handle_);
} else if (action == "close") {
ext_workspace_handle_v1_remove(ext_handle_);
} else {
spdlog::warn("[ext/workspaces]: Unknown action {}", action);
}
workspace_manager_.commit();
return true;
}
std::string Workspace::icon() {
if (has_state(EXT_WORKSPACE_HANDLE_V1_STATE_ACTIVE)) {
const auto active_icon_it = icon_map_.find("active");
if (active_icon_it != icon_map_.end()) {
return active_icon_it->second;
}
}
const auto named_icon_it = icon_map_.find(name_);
if (named_icon_it != icon_map_.end()) {
return named_icon_it->second;
}
const auto default_icon_it = icon_map_.find("default");
if (default_icon_it != icon_map_.end()) {
return default_icon_it->second;
}
return name_;
}
} // namespace waybar::modules::ext
@@ -0,0 +1,159 @@
#include "modules/ext/workspace_manager_binding.hpp"
#include <spdlog/spdlog.h>
#include <cstdint>
#include "client.hpp"
#include "modules/ext/workspace_manager.hpp"
namespace waybar::modules::ext {
static void handle_global(void* data, wl_registry* registry, uint32_t name, const char* interface,
uint32_t version) {
if (std::strcmp(interface, ext_workspace_manager_v1_interface.name) == 0) {
static_cast<WorkspaceManager*>(data)->register_manager(registry, name, version);
}
}
static void handle_global_remove(void* data, wl_registry* registry, uint32_t name) {
/* Nothing to do here */
}
static const wl_registry_listener registry_listener_impl = {.global = handle_global,
.global_remove = handle_global_remove};
void add_registry_listener(void* data) {
wl_display* display = Client::inst()->wl_display;
wl_registry* registry = wl_display_get_registry(display);
wl_registry_add_listener(registry, &registry_listener_impl, data);
wl_display_roundtrip(display);
}
static void workspace_manager_handle_workspace_group(
void* data, ext_workspace_manager_v1* _, ext_workspace_group_handle_v1* workspace_group) {
static_cast<WorkspaceManager*>(data)->handle_workspace_group(workspace_group);
}
static void workspace_manager_handle_workspace(void* data, ext_workspace_manager_v1* _,
ext_workspace_handle_v1* workspace) {
static_cast<WorkspaceManager*>(data)->handle_workspace(workspace);
}
static void workspace_manager_handle_done(void* data, ext_workspace_manager_v1* _) {
static_cast<WorkspaceManager*>(data)->handle_done();
}
static void workspace_manager_handle_finished(void* data, ext_workspace_manager_v1* _) {
static_cast<WorkspaceManager*>(data)->handle_finished();
}
static const ext_workspace_manager_v1_listener workspace_manager_impl = {
.workspace_group = workspace_manager_handle_workspace_group,
.workspace = workspace_manager_handle_workspace,
.done = workspace_manager_handle_done,
.finished = workspace_manager_handle_finished,
};
ext_workspace_manager_v1* workspace_manager_bind(wl_registry* registry, uint32_t name,
uint32_t version, void* data) {
auto* workspace_manager = static_cast<ext_workspace_manager_v1*>(
wl_registry_bind(registry, name, &ext_workspace_manager_v1_interface, version));
if (workspace_manager)
ext_workspace_manager_v1_add_listener(workspace_manager, &workspace_manager_impl, data);
else
spdlog::error("Failed to register manager");
return workspace_manager;
}
static void workspace_group_handle_capabilities(void* data, ext_workspace_group_handle_v1* _,
uint32_t capabilities) {
static_cast<WorkspaceGroup*>(data)->handle_capabilities(capabilities);
}
static void workspace_group_handle_output_enter(void* data, ext_workspace_group_handle_v1* _,
wl_output* output) {
static_cast<WorkspaceGroup*>(data)->handle_output_enter(output);
}
static void workspace_group_handle_output_leave(void* data, ext_workspace_group_handle_v1* _,
wl_output* output) {
static_cast<WorkspaceGroup*>(data)->handle_output_leave(output);
}
static void workspace_group_handle_workspace_enter(void* data, ext_workspace_group_handle_v1* _,
ext_workspace_handle_v1* workspace) {
static_cast<WorkspaceGroup*>(data)->handle_workspace_enter(workspace);
}
static void workspace_group_handle_workspace_leave(void* data, ext_workspace_group_handle_v1* _,
ext_workspace_handle_v1* workspace) {
static_cast<WorkspaceGroup*>(data)->handle_workspace_leave(workspace);
}
static void workspace_group_handle_removed(void* data, ext_workspace_group_handle_v1* _) {
static_cast<WorkspaceGroup*>(data)->handle_removed();
}
static const ext_workspace_group_handle_v1_listener workspace_group_impl = {
.capabilities = workspace_group_handle_capabilities,
.output_enter = workspace_group_handle_output_enter,
.output_leave = workspace_group_handle_output_leave,
.workspace_enter = workspace_group_handle_workspace_enter,
.workspace_leave = workspace_group_handle_workspace_leave,
.removed = workspace_group_handle_removed};
void add_workspace_group_listener(ext_workspace_group_handle_v1* workspace_group_handle,
void* data) {
ext_workspace_group_handle_v1_add_listener(workspace_group_handle, &workspace_group_impl, data);
}
void workspace_handle_name(void* data, struct ext_workspace_handle_v1* _, const char* name) {
static_cast<Workspace*>(data)->handle_name(name);
}
void workspace_handle_id(void* data, struct ext_workspace_handle_v1* _, const char* id) {
static_cast<Workspace*>(data)->handle_id(id);
}
void workspace_handle_coordinates(void* data, struct ext_workspace_handle_v1* _,
struct wl_array* coordinates) {
std::vector<uint32_t> coords_vec;
auto coords = static_cast<uint32_t*>(coordinates->data);
for (size_t i = 0; i < coordinates->size / sizeof(uint32_t); ++i) {
coords_vec.push_back(coords[i]);
}
static_cast<Workspace*>(data)->handle_coordinates(coords_vec);
}
void workspace_handle_state(void* data, struct ext_workspace_handle_v1* workspace_handle,
uint32_t state) {
static_cast<Workspace*>(data)->handle_state(state);
}
static void workspace_handle_capabilities(void* data,
struct ext_workspace_handle_v1* workspace_handle,
uint32_t capabilities) {
static_cast<Workspace*>(data)->handle_capabilities(capabilities);
}
void workspace_handle_removed(void* data, struct ext_workspace_handle_v1* workspace_handle) {
static_cast<Workspace*>(data)->handle_removed();
}
static const ext_workspace_handle_v1_listener workspace_impl = {
.id = workspace_handle_id,
.name = workspace_handle_name,
.coordinates = workspace_handle_coordinates,
.state = workspace_handle_state,
.capabilities = workspace_handle_capabilities,
.removed = workspace_handle_removed};
void add_workspace_listener(ext_workspace_handle_v1* workspace_handle, void* data) {
ext_workspace_handle_v1_add_listener(workspace_handle, &workspace_impl, data);
}
} // namespace waybar::modules::ext
+9 -9
View File
@@ -53,7 +53,6 @@ Gamemode::Gamemode(const std::string& id, const Json::Value& config)
if (config_["icon-spacing"].isUInt()) {
iconSpacing = config_["icon-spacing"].asUInt();
}
box_.set_spacing(iconSpacing);
// Whether to use icon or not
if (config_["use-icon"].isBool()) {
@@ -64,7 +63,6 @@ Gamemode::Gamemode(const std::string& id, const Json::Value& config)
if (config_["icon-size"].isUInt()) {
iconSize = config_["icon-size"].asUInt();
}
icon_.set_pixel_size(iconSize);
// Format
if (config_["format"].isString()) {
@@ -130,9 +128,9 @@ void Gamemode::getData() {
Glib::VariantContainerBase data = gamemode_proxy->call_sync("Get", parameters);
if (data && data.is_of_type(Glib::VariantType("(v)"))) {
Glib::VariantBase variant;
g_variant_get(data.gobj_copy(), "(v)", &variant);
g_variant_get(const_cast<GVariant*>(data.gobj()), "(v)", &variant);
if (variant && variant.is_of_type(Glib::VARIANT_TYPE_INT32)) {
g_variant_get(variant.gobj_copy(), "i", &gameCount);
g_variant_get(const_cast<GVariant*>(variant.gobj()), "i", &gameCount);
return;
}
}
@@ -160,7 +158,7 @@ void Gamemode::prepareForSleep_cb(const Glib::RefPtr<Gio::DBus::Connection>& con
const Glib::VariantContainerBase& parameters) {
if (parameters.is_of_type(Glib::VariantType("(b)"))) {
gboolean sleeping;
g_variant_get(parameters.gobj_copy(), "(b)", &sleeping);
g_variant_get(const_cast<GVariant*>(parameters.gobj()), "(b)", &sleeping);
if (!sleeping) {
getData();
dp.emit();
@@ -212,10 +210,7 @@ auto Gamemode::update() -> void {
lastStatus = status;
// Tooltip
if (tooltip) {
std::string text = fmt::format(fmt::runtime(tooltip_format), fmt::arg("count", gameCount));
box_.set_tooltip_text(text);
}
updateTooltip(box_, tooltip_format, fmt::arg("count", gameCount));
// Label format
std::string str = fmt::format(fmt::runtime(showAltText ? format_alt : format),
@@ -228,6 +223,11 @@ auto Gamemode::update() -> void {
iconName = DEFAULT_ICON_NAME;
}
icon_.set_from_icon_name(iconName, Gtk::ICON_SIZE_INVALID);
box_.set_spacing(iconSpacing);
icon_.set_pixel_size(iconSize);
} else {
box_.set_spacing(0);
icon_.set_pixel_size(0);
}
// Call parent update
+186
View File
@@ -0,0 +1,186 @@
#include "modules/gps.hpp"
#include <gps.h>
#include <spdlog/spdlog.h>
#include <cmath>
#include <cstdio>
// In the 80000 version of fmt library authors decided to optimize imports
// and moved declarations required for fmt::dynamic_format_arg_store in new
// header fmt/args.h
#if (FMT_VERSION >= 80000)
#include <fmt/args.h>
#else
#include <fmt/core.h>
#endif
namespace {
using namespace waybar::util;
constexpr const char* DEFAULT_FORMAT = "{mode}";
} // namespace
waybar::modules::Gps::Gps(const std::string& id, const Json::Value& config)
: ALabel(config, "gps", id, "{}", 5)
#ifdef WANT_RFKILL
,
rfkill_{RFKILL_TYPE_GPS}
#endif
{
thread_ = [this] {
dp.emit();
thread_.sleep_for(interval_);
};
if (0 != gps_open("localhost", "2947", &gps_data_)) {
throw std::runtime_error("Can't open gpsd socket");
}
if (config_["hide-disconnected"].isBool()) {
hideDisconnected = config_["hide-disconnected"].asBool();
}
if (config_["hide-no-fix"].isBool()) {
hideNoFix = config_["hide-no-fix"].asBool();
}
gps_thread_ = [this] {
dp.emit();
gps_stream(&gps_data_, WATCH_ENABLE, NULL);
int last_gps_mode = 0;
while (gps_waiting(&gps_data_, 5000000)) {
if (gps_read(&gps_data_, NULL, 0) == -1) {
throw std::runtime_error("Can't read data from gpsd.");
}
if (MODE_SET != (MODE_SET & gps_data_.set)) {
// did not even get mode, nothing to see here
continue;
}
if (gps_data_.fix.mode != last_gps_mode) {
// significant update
dp.emit();
}
last_gps_mode = gps_data_.fix.mode;
}
};
#ifdef WANT_RFKILL
rfkill_.on_update.connect(sigc::hide(sigc::mem_fun(*this, &Gps::update)));
#endif
}
const std::string waybar::modules::Gps::getFixModeName() const {
switch (gps_data_.fix.mode) {
case MODE_NO_FIX:
return "fix-none";
case MODE_2D:
return "fix-2d";
case MODE_3D:
return "fix-3d";
default:
#ifdef WANT_RFKILL
if (rfkill_.getState()) return "disabled";
#endif
return "disconnected";
}
}
const std::string waybar::modules::Gps::getFixModeString() const {
switch (gps_data_.fix.mode) {
case MODE_NO_FIX:
return "No fix";
case MODE_2D:
return "2D Fix";
case MODE_3D:
return "3D Fix";
default:
return "Disconnected";
}
}
const std::string waybar::modules::Gps::getFixStatusString() const {
switch (gps_data_.fix.status) {
case STATUS_GPS:
return "GPS";
case STATUS_DGPS:
return "DGPS";
case STATUS_RTK_FIX:
return "RTK Fixed";
case STATUS_RTK_FLT:
return "RTK Float";
case STATUS_DR:
return "Dead Reckoning";
case STATUS_GNSSDR:
return "GNSS + Dead Reckoning";
case STATUS_TIME:
return "Time Only";
case STATUS_PPS_FIX:
return "PPS Fix";
default:
#ifdef WANT_RFKILL
if (rfkill_.getState()) return "Disabled";
#endif
return "Unknown";
}
}
auto waybar::modules::Gps::update() -> void {
sleep(0); // Wait for gps status change
if ((gps_data_.fix.mode == MODE_NOT_SEEN && hideDisconnected) ||
(gps_data_.fix.mode == MODE_NO_FIX && hideNoFix)) {
event_box_.set_visible(false);
return;
}
// Show the module
if (!event_box_.get_visible()) event_box_.set_visible(true);
std::string tooltip_state;
if (!alt_) {
auto state = getFixModeName();
if (!state_.empty() && label_.get_style_context()->has_class(state_)) {
label_.get_style_context()->remove_class(state_);
}
if (config_["format-" + state].isString()) {
default_format_ = config_["format-" + state].asString();
} else if (config_["format"].isString()) {
default_format_ = config_["format"].asString();
} else {
default_format_ = DEFAULT_FORMAT;
}
if (!label_.get_style_context()->has_class(state)) {
label_.get_style_context()->add_class(state);
}
format_ = default_format_;
state_ = state;
tooltip_state = state;
}
auto format = format_;
updateLabelAndTooltipForState(
tooltip_state, format, format, fmt::arg("mode", getFixModeString()),
fmt::arg("status", getFixStatusString()), fmt::arg("latitude", gps_data_.fix.latitude),
fmt::arg("latitude_error", gps_data_.fix.epy), fmt::arg("longitude", gps_data_.fix.longitude),
fmt::arg("longitude_error", gps_data_.fix.epx),
fmt::arg("altitude_hae", gps_data_.fix.altHAE),
fmt::arg("altitude_msl", gps_data_.fix.altMSL), fmt::arg("altitude_error", gps_data_.fix.epv),
fmt::arg("speed", gps_data_.fix.speed), fmt::arg("speed_error", gps_data_.fix.eps),
fmt::arg("climb", gps_data_.fix.climb), fmt::arg("climb_error", gps_data_.fix.epc),
fmt::arg("satellites_used", gps_data_.satellites_used),
fmt::arg("satellites_visible", gps_data_.satellites_visible));
// Call parent update
ALabel::update();
}
waybar::modules::Gps::~Gps() {
gps_stream(&gps_data_, WATCH_DISABLE, NULL);
gps_close(&gps_data_);
}
+251 -85
View File
@@ -9,71 +9,169 @@
#include <sys/un.h>
#include <unistd.h>
#include <array>
#include <cerrno>
#include <cstring>
#include <filesystem>
#include <optional>
#include <string>
#include <thread>
#include "util/scoped_fd.hpp"
namespace waybar::modules::hyprland {
void IPC::startIPC() {
std::filesystem::path IPC::socketFolder_;
std::optional<bool> IPC::s_luaProtocolDetected_;
std::filesystem::path IPC::getSocketFolder(const char* instanceSig) {
static std::mutex folderMutex;
std::unique_lock lock(folderMutex);
if (socketFolder_.empty()) {
const char* xdgRuntimeDirEnv = std::getenv("XDG_RUNTIME_DIR");
std::filesystem::path xdgRuntimeDir;
// Only set path if env variable is set
if (xdgRuntimeDirEnv != nullptr) {
xdgRuntimeDir = std::filesystem::path(xdgRuntimeDirEnv);
}
if (!xdgRuntimeDir.empty() && std::filesystem::exists(xdgRuntimeDir / "hypr")) {
socketFolder_ = xdgRuntimeDir / "hypr";
} else {
spdlog::warn("$XDG_RUNTIME_DIR/hypr does not exist, falling back to /tmp/hypr");
socketFolder_ = std::filesystem::path("/tmp") / "hypr";
}
}
return socketFolder_ / instanceSig;
}
IPC::IPC() {
// will start IPC and relay events to parseIPC
socketOwnerPid_ = getpid();
ipcThread_ = std::thread([this]() { socketListener(); });
}
std::thread([&]() {
// check for hyprland
const char* his = getenv("HYPRLAND_INSTANCE_SIGNATURE");
IPC::~IPC() {
// Do no stop Hyprland IPC if a child process (with successful fork() but
// failed exec()) exits.
if (getpid() != socketOwnerPid_) return;
if (his == nullptr) {
spdlog::warn("Hyprland is not running, Hyprland IPC will not be available.");
return;
running_.store(false, std::memory_order_relaxed);
spdlog::info("Hyprland IPC stopping...");
{
std::lock_guard<std::mutex> lock(socketMutex_);
if (socketfd_ != -1) {
spdlog::trace("Shutting down socket");
if (shutdown(socketfd_, SHUT_RDWR) == -1 && errno != ENOTCONN) {
spdlog::error("Hyprland IPC: Couldn't shutdown socket");
}
}
}
if (ipcThread_.joinable()) {
ipcThread_.join();
}
}
IPC& IPC::inst() {
static IPC ipc;
return ipc;
}
void IPC::socketListener() {
// check for hyprland
const char* his = getenv("HYPRLAND_INSTANCE_SIGNATURE");
if (his == nullptr) {
spdlog::warn("Hyprland is not running, Hyprland IPC will not be available.");
return;
}
spdlog::info("Hyprland IPC starting");
struct sockaddr_un addr = {};
const int socketfd = socket(AF_UNIX, SOCK_STREAM, 0);
if (socketfd == -1) {
spdlog::error("Hyprland IPC: socketfd failed");
return;
}
addr.sun_family = AF_UNIX;
auto socketPath = IPC::getSocketFolder(his) / ".socket2.sock";
if (socketPath.native().size() >= sizeof(addr.sun_path)) {
spdlog::error("Hyprland IPC: Socket path is too long: {}", socketPath.string());
close(socketfd);
return;
}
strncpy(addr.sun_path, socketPath.c_str(), sizeof(addr.sun_path) - 1);
int l = sizeof(struct sockaddr_un);
if (connect(socketfd, (struct sockaddr*)&addr, l) == -1) {
spdlog::error("Hyprland IPC: Unable to connect? {}", std::strerror(errno));
close(socketfd);
return;
}
{
std::lock_guard<std::mutex> lock(socketMutex_);
socketfd_ = socketfd;
}
std::string pending;
while (running_.load(std::memory_order_relaxed)) {
std::array<char, 1024> buffer; // Hyprland socket2 events are max 1024 bytes
const ssize_t bytes_read = read(socketfd, buffer.data(), buffer.size());
if (bytes_read == 0) {
if (running_.load(std::memory_order_relaxed)) {
spdlog::warn("Hyprland IPC: Socket closed by peer");
}
break;
}
if (!modulesReady) return;
spdlog::info("Hyprland IPC starting");
struct sockaddr_un addr;
int socketfd = socket(AF_UNIX, SOCK_STREAM, 0);
if (socketfd == -1) {
spdlog::error("Hyprland IPC: socketfd failed");
return;
}
addr.sun_family = AF_UNIX;
// socket path, specified by EventManager of Hyprland
std::string socketPath = "/tmp/hypr/" + std::string(his) + "/.socket2.sock";
strncpy(addr.sun_path, socketPath.c_str(), sizeof(addr.sun_path) - 1);
addr.sun_path[sizeof(addr.sun_path) - 1] = 0;
int l = sizeof(struct sockaddr_un);
if (connect(socketfd, (struct sockaddr*)&addr, l) == -1) {
spdlog::error("Hyprland IPC: Unable to connect?");
return;
}
auto* file = fdopen(socketfd, "r");
while (true) {
std::array<char, 1024> buffer; // Hyprland socket2 events are max 1024 bytes
auto* receivedCharPtr = fgets(buffer.data(), buffer.size(), file);
if (receivedCharPtr == nullptr) {
std::this_thread::sleep_for(std::chrono::milliseconds(1));
if (bytes_read < 0) {
if (errno == EINTR) {
continue;
}
std::string messageReceived(buffer.data());
messageReceived = messageReceived.substr(0, messageReceived.find_first_of('\n'));
spdlog::debug("hyprland IPC received {}", messageReceived);
parseIPC(messageReceived);
std::this_thread::sleep_for(std::chrono::milliseconds(1));
if (!running_.load(std::memory_order_relaxed)) {
break;
}
spdlog::error("Hyprland IPC: read failed: {}", std::strerror(errno));
break;
}
}).detach();
pending.append(buffer.data(), static_cast<std::size_t>(bytes_read));
for (auto newline_pos = pending.find('\n'); newline_pos != std::string::npos;
newline_pos = pending.find('\n')) {
std::string messageReceived = pending.substr(0, newline_pos);
pending.erase(0, newline_pos + 1);
if (messageReceived.empty()) {
continue;
}
spdlog::debug("hyprland IPC received {}", messageReceived);
try {
parseIPC(messageReceived);
} catch (std::exception& e) {
spdlog::warn("Failed to parse IPC message: {}, reason: {}", messageReceived, e.what());
} catch (...) {
throw;
}
}
}
{
std::lock_guard<std::mutex> lock(socketMutex_);
if (socketfd_ != -1) {
if (close(socketfd_) == -1) {
spdlog::error("Hyprland IPC: Couldn't close socket");
}
socketfd_ = -1;
}
}
spdlog::debug("Hyprland IPC stopped");
}
void IPC::parseIPC(const std::string& ev) {
@@ -116,79 +214,147 @@ void IPC::unregisterForIPC(EventHandler* ev_handler) {
std::string IPC::getSocket1Reply(const std::string& rq) {
// basically hyprctl
struct addrinfo aiHints;
struct addrinfo* aiRes = nullptr;
const auto serverSocket = socket(AF_UNIX, SOCK_STREAM, 0);
util::ScopedFd serverSocket(socket(AF_UNIX, SOCK_STREAM, 0));
if (serverSocket < 0) {
spdlog::error("Hyprland IPC: Couldn't open a socket (1)");
return "";
}
memset(&aiHints, 0, sizeof(struct addrinfo));
aiHints.ai_family = AF_UNSPEC;
aiHints.ai_socktype = SOCK_STREAM;
if (getaddrinfo("localhost", nullptr, &aiHints, &aiRes) != 0) {
spdlog::error("Hyprland IPC: Couldn't get host (2)");
return "";
throw std::runtime_error("Hyprland IPC: Couldn't open a socket (1)");
}
// get the instance signature
auto* instanceSig = getenv("HYPRLAND_INSTANCE_SIGNATURE");
if (instanceSig == nullptr) {
spdlog::error("Hyprland IPC: HYPRLAND_INSTANCE_SIGNATURE was not set! (Is Hyprland running?)");
return "";
throw std::runtime_error(
"Hyprland IPC: HYPRLAND_INSTANCE_SIGNATURE was not set! (Is Hyprland running?)");
}
std::string instanceSigStr = std::string(instanceSig);
sockaddr_un serverAddress = {0};
serverAddress.sun_family = AF_UNIX;
std::string socketPath = "/tmp/hypr/" + instanceSigStr + "/.socket.sock";
std::string socketPath = IPC::getSocketFolder(instanceSig) / ".socket.sock";
// Use snprintf to copy the socketPath string into serverAddress.sun_path
if (snprintf(serverAddress.sun_path, sizeof(serverAddress.sun_path), "%s", socketPath.c_str()) <
0) {
spdlog::error("Hyprland IPC: Couldn't copy socket path (6)");
return "";
const auto socketPathLength =
snprintf(serverAddress.sun_path, sizeof(serverAddress.sun_path), "%s", socketPath.c_str());
if (socketPathLength < 0 ||
socketPathLength >= static_cast<int>(sizeof(serverAddress.sun_path))) {
throw std::runtime_error("Hyprland IPC: Couldn't copy socket path (6)");
}
if (connect(serverSocket, reinterpret_cast<sockaddr*>(&serverAddress), sizeof(serverAddress)) <
0) {
spdlog::error("Hyprland IPC: Couldn't connect to " + socketPath + ". (3)");
return "";
throw std::runtime_error("Hyprland IPC: Couldn't connect to " + socketPath + ". (3)");
}
auto sizeWritten = write(serverSocket, rq.c_str(), rq.length());
std::size_t totalWritten = 0;
while (totalWritten < rq.length()) {
const auto sizeWritten =
write(serverSocket, rq.c_str() + totalWritten, rq.length() - totalWritten);
if (sizeWritten < 0) {
spdlog::error("Hyprland IPC: Couldn't write (4)");
return "";
if (sizeWritten < 0) {
if (errno == EINTR) {
continue;
}
spdlog::error("Hyprland IPC: Couldn't write (4)");
return "";
}
if (sizeWritten == 0) {
spdlog::error("Hyprland IPC: Socket write made no progress");
return "";
}
totalWritten += static_cast<std::size_t>(sizeWritten);
}
std::array<char, 8192> buffer = {0};
std::string response;
ssize_t sizeWritten = 0;
do {
sizeWritten = read(serverSocket, buffer.data(), 8192);
if (sizeWritten < 0) {
spdlog::error("Hyprland IPC: Couldn't read (5)");
close(serverSocket);
return "";
}
response.append(buffer.data(), sizeWritten);
} while (sizeWritten > 0);
close(serverSocket);
return response;
}
Json::Value IPC::getSocket1JsonReply(const std::string& rq) {
return parser_.parse(getSocket1Reply("j/" + rq));
std::string reply = getSocket1Reply("j/" + rq);
if (reply.empty()) {
return {};
}
return parser_.parse(reply);
}
bool IPC::isLuaProtocol() {
if (s_luaProtocolDetected_.has_value()) {
return *s_luaProtocolDetected_;
}
// Probe: send a harmless old-style dispatch and check the error.
// In Lua-based Hyprland (>= 0.54) the error contains "hl.dispatch".
// In older versions it returns "ok" or a different error.
auto reply = getSocket1Reply("dispatch workspace __waybar_probe__");
bool luaProto = reply.find("hl.dispatch") != std::string::npos;
if (luaProto) {
spdlog::info("Hyprland IPC: detected Lua-based dispatch protocol (Hyprland >= 0.54)");
} else {
spdlog::info("Hyprland IPC: detected legacy dispatch protocol");
}
s_luaProtocolDetected_ = luaProto;
return luaProto;
}
std::string IPC::buildLuaDispatch(const std::string& dispatcher, const std::string& arg) {
// Map old-style dispatchers to the new Lua hl.dsp API.
//
// Old format: dispatch workspace 1
// New format: /dispatch hl.dsp.focus({ workspace = "1" })
//
// Old format: dispatch focusworkspaceoncurrentmonitor 2
// New format: /dispatch hl.dsp.focus({ workspace = "2", on_current_monitor = true })
//
// Old format: dispatch togglespecialworkspace name
// New format: /dispatch hl.dsp.workspace.toggle_special("name")
if (dispatcher == "workspace") {
return "/dispatch hl.dsp.focus({ workspace = \"" + arg + "\" })";
}
if (dispatcher == "focusworkspaceoncurrentmonitor") {
return "/dispatch hl.dsp.focus({ workspace = \"" + arg + "\", on_current_monitor = true })";
}
if (dispatcher == "togglespecialworkspace") {
if (arg.empty()) {
return "/dispatch hl.dsp.workspace.toggle_special()";
}
return "/dispatch hl.dsp.workspace.toggle_special(\"" + arg + "\")";
}
// Fallback for any other dispatcher: try the old format wrapped in dispatch().
// This may not work for all dispatchers, but it's a reasonable default.
spdlog::warn("Hyprland IPC: unknown dispatcher '{}' in Lua mode, attempting generic format",
dispatcher);
return "/dispatch hl.dsp." + dispatcher + "(\"" + arg + "\")";
}
std::string IPC::dispatch(const std::string& dispatcher, const std::string& arg) {
if (isLuaProtocol()) {
return getSocket1Reply(buildLuaDispatch(dispatcher, arg));
}
// Legacy format: "dispatch <dispatcher> <arg>"
std::string cmd = "dispatch " + dispatcher;
if (!arg.empty()) {
cmd += " " + arg;
}
return getSocket1Reply(cmd);
}
} // namespace waybar::modules::hyprland
+106 -15
View File
@@ -10,13 +10,7 @@
namespace waybar::modules::hyprland {
Language::Language(const std::string& id, const Bar& bar, const Json::Value& config)
: ALabel(config, "language", id, "{}", 0, true), bar_(bar) {
modulesReady = true;
if (!gIPC) {
gIPC = std::make_unique<IPC>();
}
: ALabel(config, "language", id, "{}", 0, true), bar_(bar), m_ipc(IPC::inst()) {
// get the active layout when open
initLanguage();
@@ -24,11 +18,11 @@ Language::Language(const std::string& id, const Bar& bar, const Json::Value& con
update();
// register for hyprland ipc
gIPC->registerForIPC("activelayout", this);
m_ipc.registerForIPC("activelayout", this);
}
Language::~Language() {
gIPC->unregisterForIPC(this);
m_ipc.unregisterForIPC(this);
// wait for possible event handler to finish
std::lock_guard<std::mutex> lg(mutex_);
}
@@ -36,6 +30,11 @@ Language::~Language() {
auto Language::update() -> void {
std::lock_guard<std::mutex> lg(mutex_);
spdlog::debug("hyprland language update with full name {}", layout_.full_name);
spdlog::debug("hyprland language update with short name {}", layout_.short_name);
spdlog::debug("hyprland language update with short description {}", layout_.short_description);
spdlog::debug("hyprland language update with variant {}", layout_.variant);
std::string layoutName = std::string{};
if (config_.isMember("format-" + layout_.short_description + "-" + layout_.variant)) {
const auto propName = "format-" + layout_.short_description + "-" + layout_.variant;
@@ -49,28 +48,109 @@ auto Language::update() -> void {
fmt::arg("shortDescription", layout_.short_description),
fmt::arg("variant", layout_.variant)));
}
spdlog::debug("hyprland language formatted layout name {}", layoutName);
std::string tooltipContent = std::string{};
bool tooltip_enabled = tooltipEnabled();
if (tooltip_enabled) {
if (config_.isMember("tooltip-format")) {
auto tooltip_format = config_["tooltip-format"].asString();
if (config_.isMember("tooltip-format-" + layout_.short_description + "-" + layout_.variant)) {
const auto propName = "tooltip-format-" + layout_.short_description + "-" + layout_.variant;
tooltipContent = fmt::format(fmt::runtime(tooltip_format), config_[propName].asString());
} else if (config_.isMember("tooltip-format-" + layout_.short_description)) {
const auto propName = "tooltip-format-" + layout_.short_description;
tooltipContent = fmt::format(fmt::runtime(tooltip_format), config_[propName].asString());
} else {
tooltipContent =
trim(fmt::format(fmt::runtime(tooltip_format), 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 no tooltip format is provided, use the same text as the module
tooltipContent = layoutName;
}
spdlog::debug("hyprland language formatted tooltip content {}", tooltipContent);
}
if (!format_.empty()) {
label_.show();
label_.set_markup(layoutName);
setLabelMarkup(layoutName);
if (tooltip_enabled) {
setTooltipMarkup(tooltipContent);
}
} else {
label_.hide();
}
// Tooltip support
if (tooltipEnabled()) {
std::string tooltipFormat;
if (config_["tooltip-format"].isString()) {
tooltipFormat = config_["tooltip-format"].asString();
} else {
tooltipFormat = "{long}";
}
auto tooltipText =
trim(fmt::format(fmt::runtime(tooltipFormat), fmt::arg("long", layout_.full_name),
fmt::arg("short", layout_.short_name),
fmt::arg("shortDescription", layout_.short_description),
fmt::arg("variant", layout_.variant)));
label_.set_tooltip_text(tooltipText);
} else {
label_.set_tooltip_text("");
}
ALabel::update();
}
void Language::onEvent(const std::string& ev) {
std::lock_guard<std::mutex> lg(mutex_);
std::string kbName(begin(ev) + ev.find_last_of('>') + 1, begin(ev) + ev.find_first_of(','));
auto layoutName = ev.substr(ev.find_first_of(',') + 1);
const auto payloadStart = ev.find(">>");
if (payloadStart == std::string::npos) {
spdlog::warn("hyprland language received malformed event: {}", ev);
return;
}
const auto payload = ev.substr(payloadStart + 2);
const auto kbSeparator = payload.find(',');
if (kbSeparator == std::string::npos) {
spdlog::warn("hyprland language received malformed event payload: {}", ev);
return;
}
// Last comma before variants parenthesis, eg:
// activelayout>>micro-star-int'l-co.,-ltd.-msi-gk50-elite-gaming-keyboard,English (US, intl.,
// with dead keys)
std::string beforeParenthesis;
auto parenthesisPos = payload.find_last_of('(');
if (parenthesisPos == std::string::npos) {
beforeParenthesis = payload;
} else {
beforeParenthesis = payload.substr(0, parenthesisPos);
}
const auto layoutSeparator = beforeParenthesis.find_last_of(',');
if (layoutSeparator == std::string::npos) {
spdlog::warn("hyprland language received malformed layout payload: {}", ev);
return;
}
auto layoutName = payload.substr(layoutSeparator + 1);
if (config_.isMember("keyboard-name") && kbName != config_["keyboard-name"].asString())
return; // ignore
if (config_.isMember("keyboard-name")) {
const auto keyboardName = config_["keyboard-name"].asString();
// The keyboard name itself can contain commas, so match it as a full prefix
// (followed by the ',' separator) rather than comparing against the substring
// before the first comma, which would truncate such names and drop the event.
if (payload.size() <= keyboardName.size() || payload[keyboardName.size()] != ',' ||
payload.compare(0, keyboardName.size(), keyboardName) != 0)
return; // ignore
}
layoutName = waybar::util::sanitize_string(layoutName);
removeXkbLayoutCssClass();
layout_ = getLayout(layoutName);
addXkbLayoutCssClass();
spdlog::debug("hyprland language onevent with {}", layoutName);
@@ -78,7 +158,7 @@ void Language::onEvent(const std::string& ev) {
}
void Language::initLanguage() {
const auto inputDevices = gIPC->getSocket1Reply("devices");
const auto inputDevices = m_ipc.getSocket1Reply("devices");
const auto kbName = config_["keyboard-name"].asString();
@@ -92,6 +172,7 @@ void Language::initLanguage() {
searcher = waybar::util::sanitize_string(searcher);
layout_ = getLayout(searcher);
addXkbLayoutCssClass();
spdlog::debug("hyprland language initLanguage found {}", layout_.full_name);
@@ -101,6 +182,16 @@ void Language::initLanguage() {
}
}
auto Language::removeXkbLayoutCssClass() -> void {
label_.get_style_context()->remove_class(layout_.short_name);
spdlog::debug("hyprland language try to remove currently short_name css class {}",
layout_.short_name);
}
auto Language::addXkbLayoutCssClass() -> void {
label_.get_style_context()->add_class(layout_.short_name);
spdlog::debug("hyprland language add new short_name css class {}", layout_.short_name);
}
auto Language::getLayout(const std::string& fullName) -> Layout {
auto* const context = rxkb_context_new(RXKB_CONTEXT_LOAD_EXOTIC_RULES);
rxkb_context_parse_default_ruleset(context);
+63 -18
View File
@@ -2,41 +2,78 @@
#include <spdlog/spdlog.h>
#include "util/sanitize_str.hpp"
namespace waybar::modules::hyprland {
Submap::Submap(const std::string& id, const Bar& bar, const Json::Value& config)
: ALabel(config, "submap", id, "{}", 0, true), bar_(bar) {
modulesReady = true;
if (!gIPC) {
gIPC = std::make_unique<IPC>();
}
: ALabel(config, "submap", id, "{}", 0, true), bar_(bar), m_ipc(IPC::inst()) {
parseConfig(config);
label_.hide();
ALabel::update();
// Displays widget immediately if always_on_ assuming default submap
// Needs an actual way to retrieve current submap on startup
if (always_on_) {
submap_ = default_submap_;
label_.get_style_context()->add_class(submap_);
}
// register for hyprland ipc
gIPC->registerForIPC("submap", this);
m_ipc.registerForIPC("submap", this);
dp.emit();
if (config["icons"].isObject()) {
const Json::Value& icons = config["icons"];
for (const std::string& key : icons.getMemberNames()) {
const Json::Value& value = icons[key];
if (value.isString()) {
icons_[key] = value.asString();
}
}
}
}
Submap::~Submap() {
gIPC->unregisterForIPC(this);
m_ipc.unregisterForIPC(this);
// wait for possible event handler to finish
std::lock_guard<std::mutex> lg(mutex_);
}
auto Submap::parseConfig(const Json::Value& config) -> void {
auto const& alwaysOn = config["always-on"];
if (alwaysOn.isBool()) {
always_on_ = alwaysOn.asBool();
}
auto const& defaultSubmap = config["default-submap"];
if (defaultSubmap.isString()) {
default_submap_ = defaultSubmap.asString();
}
}
auto Submap::update() -> void {
std::lock_guard<std::mutex> lg(mutex_);
// Handle style class changes
if (!prev_submap_.empty()) {
label_.get_style_context()->remove_class(prev_submap_);
}
if (!submap_.empty()) {
label_.get_style_context()->add_class(submap_);
}
prev_submap_ = submap_;
if (submap_.empty()) {
event_box_.hide();
} else {
label_.set_markup(fmt::format(fmt::runtime(format_), submap_));
label_.set_markup(
fmt::format(fmt::runtime(format_), fmt::arg("submap", submap_), fmt::arg("icon", icon_)));
if (tooltipEnabled()) {
label_.set_tooltip_text(submap_);
label_.set_tooltip_markup(submap_);
}
event_box_.show();
}
@@ -51,16 +88,24 @@ void Submap::onEvent(const std::string& ev) {
return;
}
auto submapName = ev.substr(ev.find_last_of('>') + 1);
submapName = waybar::util::sanitize_string(submapName);
if (!submap_.empty()) {
label_.get_style_context()->remove_class(submap_);
const auto separator = ev.find(">>");
if (separator == std::string::npos) {
spdlog::warn("hyprland submap received malformed event: {}", ev);
return;
}
auto submapName = ev.substr(separator + 2);
submap_ = submapName;
label_.get_style_context()->add_class(submap_);
if (!icons_[submap_].empty()) {
icon_ = icons_[submap_];
} else {
icon_ = "";
}
if (submap_.empty() && always_on_) {
submap_ = default_submap_;
}
spdlog::debug("hyprland submap onevent with {}", submap_);
+144 -114
View File
@@ -1,11 +1,13 @@
#include "modules/hyprland/window.hpp"
#include <fmt/format.h>
#include <glibmm/fileutils.h>
#include <glibmm/keyfile.h>
#include <glibmm/miscutils.h>
#include <spdlog/spdlog.h>
#include <algorithm>
#include <shared_mutex>
#include <vector>
#include "modules/hyprland/backend.hpp"
@@ -14,54 +16,77 @@
namespace waybar::modules::hyprland {
std::shared_mutex windowIpcSmtx;
Window::Window(const std::string& id, const Bar& bar, const Json::Value& config)
: AAppIconLabel(config, "window", id, "{title}", 0, true), bar_(bar) {
modulesReady = true;
: AAppIconLabel(config, "window", id, "{title}", 0, true), bar_(bar), m_ipc(IPC::inst()) {
separateOutputs_ = config["separate-outputs"].asBool();
if (!gIPC) {
gIPC = std::make_unique<IPC>();
}
queryActiveWorkspace();
update();
dp.emit();
// register for hyprland ipc
gIPC->registerForIPC("activewindow", this);
gIPC->registerForIPC("closewindow", this);
gIPC->registerForIPC("movewindow", this);
gIPC->registerForIPC("changefloatingmode", this);
gIPC->registerForIPC("fullscreen", this);
std::unique_lock<std::shared_mutex> windowIpcUniqueLock(windowIpcSmtx);
m_ipc.registerForIPC("activewindow", this);
m_ipc.registerForIPC("closewindow", this);
m_ipc.registerForIPC("movewindow", this);
m_ipc.registerForIPC("changefloatingmode", this);
m_ipc.registerForIPC("fullscreen", this);
windowIpcUniqueLock.unlock();
dp.emit();
}
Window::~Window() {
gIPC->unregisterForIPC(this);
// wait for possible event handler to finish
std::lock_guard<std::mutex> lg(mutex_);
std::unique_lock<std::shared_mutex> windowIpcUniqueLock(windowIpcSmtx);
m_ipc.unregisterForIPC(this);
}
auto Window::update() -> void {
// fix ampersands
std::lock_guard<std::mutex> lg(mutex_);
std::shared_lock<std::shared_mutex> windowIpcShareLock(windowIpcSmtx);
queryActiveWorkspace();
std::string windowName = waybar::util::sanitize_string(workspace_.last_window_title);
std::string windowAddress = workspace_.last_window;
windowData_.title = windowName;
std::string label_text;
if (!format_.empty()) {
label_.show();
label_.set_markup(waybar::util::rewriteString(
fmt::format(fmt::runtime(format_), fmt::arg("title", windowName),
// If the focused window name is empty and fallback is configured, use fallback text
std::string displayTitle = windowName;
if (displayTitle.empty() && config_["fallback"].isString()) {
displayTitle = config_["fallback"].asString();
}
label_text = waybar::util::rewriteString(
fmt::format(fmt::runtime(format_), fmt::arg("title", displayTitle),
fmt::arg("initialTitle", windowData_.initial_title),
fmt::arg("class", windowData_.class_name),
fmt::arg("initialClass", windowData_.initial_class_name)),
config_["rewrite"]));
config_["rewrite"]);
setLabelMarkup(label_text);
} else {
label_.hide();
}
if (tooltipEnabled()) {
std::string tooltip_format;
if (config_["tooltip-format"].isString()) {
tooltip_format = config_["tooltip-format"].asString();
}
if (!tooltip_format.empty()) {
setTooltipMarkup(fmt::format(fmt::runtime(tooltip_format), fmt::arg("title", windowName),
fmt::arg("initialTitle", windowData_.initial_title),
fmt::arg("class", windowData_.class_name),
fmt::arg("initialClass", windowData_.initial_class_name)));
} else if (!label_text.empty()) {
setTooltipMarkup(label_text);
}
}
if (focused_) {
image_.show();
} else {
@@ -90,125 +115,130 @@ auto Window::update() -> void {
AAppIconLabel::update();
}
auto Window::getActiveWorkspace() -> Workspace {
const auto workspace = gIPC->getSocket1JsonReply("activeworkspace");
assert(workspace.isObject());
return Workspace::parse(workspace);
}
auto Window::getActiveWorkspace() -> Workspace { return getActiveWorkspace(""); }
auto Window::getActiveWorkspace(const std::string& monitorName) -> Workspace {
const auto monitors = gIPC->getSocket1JsonReply("monitors");
assert(monitors.isArray());
auto monitor = std::find_if(monitors.begin(), monitors.end(),
[&](Json::Value monitor) { return monitor["name"] == monitorName; });
if (monitor == std::end(monitors)) {
spdlog::warn("Monitor not found: {}", monitorName);
return Workspace{-1, 0, "", ""};
}
const int id = (*monitor)["activeWorkspace"]["id"].asInt();
const auto monitors = IPC::inst().getSocket1JsonReply("monitors");
if (monitors.isArray()) {
auto monitor = std::ranges::find_if(monitors, [&](const Json::Value& monitor) {
return monitorName.empty() ? monitor["focused"].asBool() : monitor["name"] == monitorName;
});
if (monitor == std::end(monitors)) {
spdlog::warn("Monitor not found: {}", monitorName);
return Workspace{
.id = -1,
.windows = 0,
.last_window = "",
.last_window_title = "",
};
}
const int special_id = (*monitor)["specialWorkspace"]["id"].asInt();
const int id = special_id != 0 ? special_id : (*monitor)["activeWorkspace"]["id"].asInt();
const auto workspaces = gIPC->getSocket1JsonReply("workspaces");
assert(workspaces.isArray());
auto workspace = std::find_if(workspaces.begin(), workspaces.end(),
[&](Json::Value workspace) { return workspace["id"] == id; });
if (workspace == std::end(workspaces)) {
spdlog::warn("No workspace with id {}", id);
return Workspace{-1, 0, "", ""};
}
return Workspace::parse(*workspace);
const auto workspaces = IPC::inst().getSocket1JsonReply("workspaces");
if (workspaces.isArray()) {
auto workspace = std::ranges::find_if(
workspaces, [&](const Json::Value& workspace) { return workspace["id"] == id; });
if (workspace == std::end(workspaces)) {
spdlog::warn("No workspace with id {}", id);
return Workspace{
.id = -1,
.windows = 0,
.last_window = "",
.last_window_title = "",
};
}
return Workspace::parse(*workspace);
};
};
return {};
}
auto Window::Workspace::parse(const Json::Value& value) -> Window::Workspace {
return Workspace{
value["id"].asInt(),
value["windows"].asInt(),
value["lastwindow"].asString(),
value["lastwindowtitle"].asString(),
.id = value["id"].asInt(),
.windows = value["windows"].asInt(),
.last_window = value["lastwindow"].asString(),
.last_window_title = value["lastwindowtitle"].asString(),
};
}
auto Window::WindowData::parse(const Json::Value& value) -> Window::WindowData {
return WindowData{value["floating"].asBool(), value["monitor"].asInt(),
value["class"].asString(), value["initialClass"].asString(),
value["title"].asString(), value["initialTitle"].asString(),
value["fullscreen"].asBool(), !value["grouped"].empty()};
return WindowData{.floating = value["floating"].asBool(),
.monitor = value["monitor"].asInt(),
.class_name = value["class"].asString(),
.initial_class_name = value["initialClass"].asString(),
.title = value["title"].asString(),
.initial_title = value["initialTitle"].asString(),
.fullscreen = value["fullscreen"].asBool(),
.grouped = !value["grouped"].empty()};
}
void Window::queryActiveWorkspace() {
std::lock_guard<std::mutex> lg(mutex_);
if (separateOutputs_) {
workspace_ = getActiveWorkspace(this->bar_.output->name);
} else {
workspace_ = getActiveWorkspace();
}
focused_ = false;
windowData_ = WindowData{};
allFloating_ = false;
swallowing_ = false;
fullscreen_ = false;
solo_ = false;
soloClass_.clear();
if (workspace_.windows <= 0) {
return;
}
const auto clients = m_ipc.getSocket1JsonReply("clients");
if (!clients.isArray()) {
return;
}
auto activeWindow = std::ranges::find_if(clients, [&](const Json::Value& window) {
return window["address"] == workspace_.last_window;
});
if (activeWindow == std::end(clients)) {
return;
}
focused_ = true;
if (workspace_.windows > 0) {
const auto clients = gIPC->getSocket1JsonReply("clients");
assert(clients.isArray());
auto activeWindow = std::find_if(clients.begin(), clients.end(), [&](Json::Value window) {
return window["address"] == workspace_.last_window;
});
windowData_ = WindowData::parse(*activeWindow);
updateAppIconName(windowData_.class_name, windowData_.initial_class_name);
std::vector<Json::Value> workspaceWindows;
std::ranges::copy_if(
clients, std::back_inserter(workspaceWindows), [&](const Json::Value& window) {
return window["workspace"]["id"] == workspace_.id && window["mapped"].asBool();
});
swallowing_ = std::ranges::any_of(workspaceWindows, [&](const Json::Value& window) {
return !window["swallowing"].isNull() && window["swallowing"].asString() != "0x0";
});
std::vector<Json::Value> visibleWindows;
std::ranges::copy_if(workspaceWindows, std::back_inserter(visibleWindows),
[&](const Json::Value& window) { return !window["hidden"].asBool(); });
solo_ =
1 == std::count_if(visibleWindows.begin(), visibleWindows.end(),
[&](const Json::Value& window) { return !window["floating"].asBool(); });
allFloating_ = std::ranges::all_of(
visibleWindows, [&](const Json::Value& window) { return window["floating"].asBool(); });
fullscreen_ = windowData_.fullscreen;
if (activeWindow == std::end(clients)) {
focused_ = false;
return;
}
// Fullscreen windows look like they are solo
if (fullscreen_) {
solo_ = true;
}
windowData_ = WindowData::parse(*activeWindow);
updateAppIconName(windowData_.class_name, windowData_.initial_class_name);
std::vector<Json::Value> workspaceWindows;
std::copy_if(clients.begin(), clients.end(), std::back_inserter(workspaceWindows),
[&](Json::Value window) {
return window["workspace"]["id"] == workspace_.id && window["mapped"].asBool();
});
swallowing_ =
std::any_of(workspaceWindows.begin(), workspaceWindows.end(), [&](Json::Value window) {
return !window["swallowing"].isNull() && window["swallowing"].asString() != "0x0";
});
std::vector<Json::Value> visibleWindows;
std::copy_if(workspaceWindows.begin(), workspaceWindows.end(),
std::back_inserter(visibleWindows),
[&](Json::Value window) { return !window["hidden"].asBool(); });
solo_ = 1 == std::count_if(visibleWindows.begin(), visibleWindows.end(),
[&](Json::Value window) { return !window["floating"].asBool(); });
allFloating_ = std::all_of(visibleWindows.begin(), visibleWindows.end(),
[&](Json::Value window) { return window["floating"].asBool(); });
fullscreen_ = windowData_.fullscreen;
// Fullscreen windows look like they are solo
if (fullscreen_) {
solo_ = true;
}
// Grouped windows have a tab bar and therefore don't look fullscreen or solo
if (windowData_.grouped) {
fullscreen_ = false;
solo_ = false;
}
if (solo_) {
soloClass_ = windowData_.class_name;
} else {
soloClass_ = "";
}
} else {
focused_ = false;
windowData_ = WindowData{};
allFloating_ = false;
swallowing_ = false;
fullscreen_ = false;
solo_ = false;
soloClass_ = "";
if (solo_) {
soloClass_ = windowData_.class_name;
}
}
void Window::onEvent(const std::string& ev) {
queryActiveWorkspace();
dp.emit();
}
void Window::onEvent(const std::string& ev) { dp.emit(); }
void Window::setClass(const std::string& classname, bool enable) {
if (enable) {
+140
View File
@@ -0,0 +1,140 @@
#include "modules/hyprland/windowcount.hpp"
#include <glibmm/fileutils.h>
#include <glibmm/keyfile.h>
#include <glibmm/miscutils.h>
#include <spdlog/spdlog.h>
#include <algorithm>
#include <vector>
#include "modules/hyprland/backend.hpp"
namespace waybar::modules::hyprland {
WindowCount::WindowCount(const std::string& id, const Bar& bar, const Json::Value& config)
: AAppIconLabel(config, "windowcount", id, "{count}", 0, true), bar_(bar), m_ipc(IPC::inst()) {
separateOutputs_ =
config.isMember("separate-outputs") ? config["separate-outputs"].asBool() : true;
queryActiveWorkspace();
update();
dp.emit();
// register for hyprland ipc
m_ipc.registerForIPC("fullscreen", this);
m_ipc.registerForIPC("workspace", this);
m_ipc.registerForIPC("focusedmon", this);
m_ipc.registerForIPC("openwindow", this);
m_ipc.registerForIPC("closewindow", this);
m_ipc.registerForIPC("movewindow", this);
}
WindowCount::~WindowCount() {
m_ipc.unregisterForIPC(this);
// wait for possible event handler to finish
std::lock_guard<std::mutex> lg(mutex_);
}
auto WindowCount::update() -> void {
std::lock_guard<std::mutex> lg(mutex_);
queryActiveWorkspace();
std::string format = config_["format"].asString();
std::string formatEmpty = config_["format-empty"].asString();
std::string formatWindowed = config_["format-windowed"].asString();
std::string formatFullscreen = config_["format-fullscreen"].asString();
setClass("empty", workspace_.windows == 0);
setClass("fullscreen", workspace_.hasfullscreen);
if (workspace_.windows == 0 && !formatEmpty.empty()) {
label_.set_markup(fmt::format(fmt::runtime(formatEmpty), workspace_.windows));
} else if (!workspace_.hasfullscreen && !formatWindowed.empty()) {
label_.set_markup(fmt::format(fmt::runtime(formatWindowed), workspace_.windows));
} else if (workspace_.hasfullscreen && !formatFullscreen.empty()) {
label_.set_markup(fmt::format(fmt::runtime(formatFullscreen), workspace_.windows));
} else if (!format.empty()) {
label_.set_markup(fmt::format(fmt::runtime(format), workspace_.windows));
} else {
label_.set_markup(fmt::format("{}", workspace_.windows));
}
label_.show();
AAppIconLabel::update();
}
auto WindowCount::getActiveWorkspace() -> Workspace {
const auto workspace = m_ipc.getSocket1JsonReply("activeworkspace");
if (workspace.isObject()) {
return Workspace::parse(workspace);
}
return {};
}
auto WindowCount::getActiveWorkspace(const std::string& monitorName) -> Workspace {
const auto monitors = m_ipc.getSocket1JsonReply("monitors");
if (monitors.isArray()) {
auto monitor = std::ranges::find_if(
monitors, [&](const Json::Value& monitor) { return monitor["name"] == monitorName; });
if (monitor == std::end(monitors)) {
spdlog::warn("Monitor not found: {}", monitorName);
return Workspace{
.id = -1,
.windows = 0,
.hasfullscreen = false,
};
}
const int id = (*monitor)["activeWorkspace"]["id"].asInt();
const auto workspaces = m_ipc.getSocket1JsonReply("workspaces");
if (workspaces.isArray()) {
auto workspace = std::ranges::find_if(
workspaces, [&](const Json::Value& workspace) { return workspace["id"] == id; });
if (workspace == std::end(workspaces)) {
spdlog::warn("No workspace with id {}", id);
return Workspace{
.id = -1,
.windows = 0,
.hasfullscreen = false,
};
}
return Workspace::parse(*workspace);
};
};
return {};
}
auto WindowCount::Workspace::parse(const Json::Value& value) -> WindowCount::Workspace {
return Workspace{
.id = value["id"].asInt(),
.windows = value["windows"].asInt(),
.hasfullscreen = value["hasfullscreen"].asBool(),
};
}
void WindowCount::queryActiveWorkspace() {
if (separateOutputs_) {
workspace_ = getActiveWorkspace(this->bar_.output->name);
} else {
workspace_ = getActiveWorkspace();
}
}
void WindowCount::onEvent(const std::string& ev) { dp.emit(); }
void WindowCount::setClass(const std::string& classname, bool enable) {
if (enable) {
if (!bar_.window.get_style_context()->has_class(classname)) {
bar_.window.get_style_context()->add_class(classname);
}
} else {
bar_.window.get_style_context()->remove_class(classname);
}
}
} // namespace waybar::modules::hyprland
@@ -0,0 +1,111 @@
#include "modules/hyprland/windowcreationpayload.hpp"
#include <json/value.h>
#include <spdlog/spdlog.h>
#include <string>
#include <utility>
#include <variant>
#include "modules/hyprland/workspaces.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(const std::string& workspace_name,
WindowAddress window_address, WindowRepr window_repr)
: m_window(std::move(window_repr)),
m_windowAddress(std::move(window_address)),
m_workspaceName(std::move(workspace_name)) {
clearAddr();
clearWorkspaceName();
}
WindowCreationPayload::WindowCreationPayload(const std::string& workspace_name,
WindowAddress window_address,
const std::string& window_class,
const std::string& window_title, bool is_active)
: 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)),
m_isActive(is_active) {
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<Repr>(m_window)) {
return std::get<Repr>(m_window).empty();
}
if (std::holds_alternative<ClassAndTitle>(m_window)) {
auto [window_class, window_title] = std::get<ClassAndTitle>(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::moveToWorkspace(std::string& new_workspace_name) {
m_workspaceName = new_workspace_name;
}
WindowRepr WindowCreationPayload::repr(Workspaces& workspace_manager) {
if (std::holds_alternative<Repr>(m_window)) {
return std::get<Repr>(m_window);
}
if (std::holds_alternative<ClassAndTitle>(m_window)) {
auto const& [window_class, window_title] = std::get<ClassAndTitle>(m_window);
return {m_windowAddress, window_class, window_title,
workspace_manager.getRewrite(window_class, window_title), m_isActive};
}
// Unreachable
spdlog::error("WorkspaceWindow::repr: Unreachable");
throw std::runtime_error("WorkspaceWindow::repr: Unreachable");
}
} // namespace waybar::modules::hyprland
+632
View File
@@ -0,0 +1,632 @@
#include <glibmm/main.h>
#include <json/value.h>
#include <spdlog/spdlog.h>
#include <algorithm>
#include <cctype>
#include <memory>
#include <string>
#include <utility>
#include "modules/hyprland/workspaces.hpp"
#include "util/command.hpp"
#include "util/icon_loader.hpp"
namespace {
constexpr std::string_view kCssClassPrefix = "ws-";
// Convert a workspace name to a valid CSS class name.
// Lowercases, replaces non-alphanumeric runs with single hyphens,
// and prefixes digit-leading names (CSS classes can't start with a digit).
std::string sanitizeCssClass(const std::string& name) {
std::string result;
result.reserve(name.size() + kCssClassPrefix.size());
for (auto c : name) {
auto uc = static_cast<unsigned char>(c);
if (std::isalnum(uc)) {
result += static_cast<char>(std::tolower(uc));
} else if (!result.empty() && result.back() != '-') {
result += '-';
}
}
if (!result.empty() && result.back() == '-') {
result.pop_back();
}
if (!result.empty() && std::isdigit(static_cast<unsigned char>(result.front()))) {
result.insert(0, kCssClassPrefix);
}
return result;
}
} // namespace
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()),
m_ipc(IPC::inst()) {
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.add_events(Gdk::ENTER_NOTIFY_MASK | Gdk::LEAVE_NOTIFY_MASK);
m_button.signal_enter_notify_event().connect(sigc::mem_fun(*this, &Workspace::handleEnter));
m_button.signal_leave_notify_event().connect(sigc::mem_fun(*this, &Workspace::handleLeave));
m_button.signal_button_press_event().connect(sigc::mem_fun(*this, &Workspace::handleClicked),
false);
m_button.set_relief(Gtk::RELIEF_NONE);
if (m_workspaceManager.enableTaskbar()) {
m_content.set_orientation(m_workspaceManager.taskbarOrientation());
m_content.pack_start(m_labelBefore, false, false);
} else {
m_content.set_center_widget(m_labelBefore);
}
m_button.add(m_content);
initializeWindowMap(clients_data);
}
Workspace::~Workspace() {
// Disconnect the hover-check timeout so it can't fire on this destroyed
// instance (Workspaces are removed at runtime while a check may be armed).
stopHoverCheck();
}
void addOrRemoveClass(const Glib::RefPtr<Gtk::StyleContext>& context, bool condition,
const std::string& class_name) {
if (condition) {
context->add_class(class_name);
} else {
context->remove_class(class_name);
}
}
std::optional<WindowRepr> Workspace::closeWindow(WindowAddress const& addr) {
auto it = std::ranges::find_if(m_windowMap,
[&addr](const auto& window) { return window.address == addr; });
// If the vector contains the address, remove it and return the window representation
if (it != m_windowMap.end()) {
WindowRepr windowRepr = *it;
m_windowMap.erase(it);
return windowRepr;
}
return std::nullopt;
}
bool Workspace::pointerInsideButton() {
auto display = Gdk::Display::get_default();
if (!display) {
return false;
}
auto seat = display->get_default_seat();
if (!seat) {
return false;
}
auto pointer = seat->get_pointer();
if (!pointer) {
return false;
}
Glib::RefPtr<Gdk::Screen> screen;
int pointerRootX = 0;
int pointerRootY = 0;
pointer->get_position(screen, pointerRootX, pointerRootY);
Gtk::Widget* toplevel = m_button.get_toplevel();
if (toplevel == nullptr || !toplevel->get_window()) {
return false;
}
int buttonX = 0;
int buttonY = 0;
if (!m_button.translate_coordinates(*toplevel, 0, 0, buttonX, buttonY)) {
return false;
}
int windowRootX = 0;
int windowRootY = 0;
toplevel->get_window()->get_root_origin(windowRootX, windowRootY);
const auto allocation = m_button.get_allocation();
const int buttonRootX = windowRootX + buttonX;
const int buttonRootY = windowRootY + buttonY;
const int buttonWidth = allocation.get_width();
const int buttonHeight = allocation.get_height();
return pointerRootX >= buttonRootX && pointerRootY >= buttonRootY &&
pointerRootX < buttonRootX + buttonWidth && pointerRootY < buttonRootY + buttonHeight;
}
bool Workspace::syncHoverClass() {
auto styleContext = m_button.get_style_context();
if (pointerInsideButton()) {
styleContext->add_class("workspace-hover");
return true;
}
styleContext->remove_class("workspace-hover");
stopHoverCheck();
return false;
}
void Workspace::startHoverCheck() {
if (m_hoverCheckConnection.connected()) {
return;
}
m_hoverCheckConnection =
Glib::signal_timeout().connect(sigc::mem_fun(*this, &Workspace::syncHoverClass), 50);
}
void Workspace::stopHoverCheck() {
if (m_hoverCheckConnection.connected()) {
m_hoverCheckConnection.disconnect();
}
}
bool Workspace::handleEnter(GdkEventCrossing* /*event*/) {
m_button.get_style_context()->add_class("workspace-hover");
startHoverCheck();
return false;
}
bool Workspace::handleLeave(GdkEventCrossing* /*event*/) {
/*
* Do not remove immediately.
* Workspace taskbar children can fire misleading leave events while the
* pointer is still visually inside the workspace button.
*
* The polling check will remove the class once the pointer really leaves.
*/
startHoverCheck();
return false;
}
bool Workspace::handleClicked(GdkEventButton* bt) const {
if (bt->type == GDK_BUTTON_PRESS) {
try {
if (id() > 0) { // normal
if (m_workspaceManager.moveToMonitor()) {
IPC::dispatch("focusworkspaceoncurrentmonitor", std::to_string(id()));
} else {
IPC::dispatch("workspace", std::to_string(id()));
}
} else if (!isSpecial()) { // named (this includes persistent)
if (m_workspaceManager.moveToMonitor()) {
IPC::dispatch("focusworkspaceoncurrentmonitor", "name:" + name());
} else {
IPC::dispatch("workspace", "name:" + name());
}
} else if (id() != -99) { // named special
IPC::dispatch("togglespecialworkspace", name());
} else { // special
IPC::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 (const auto& client : clients_data) {
if (client["workspace"]["id"].asInt() == id()) {
insertWindow({client});
}
}
}
void Workspace::setActiveWindow(WindowAddress const& addr) {
std::optional<long> activeIdx;
for (size_t i = 0; i < m_windowMap.size(); ++i) {
auto& window = m_windowMap[i];
bool isActive = (window.address == addr);
window.setActive(isActive);
if (isActive) {
activeIdx = i;
}
}
auto activeWindowPos = m_workspaceManager.activeWindowPosition();
const bool has_active_window =
activeIdx.has_value() && activeWindowPos != Workspaces::ActiveWindowPosition::NONE;
if (has_active_window) {
auto window = std::move(m_windowMap[*activeIdx]);
m_windowMap.erase(m_windowMap.begin() + *activeIdx);
if (activeWindowPos == Workspaces::ActiveWindowPosition::FIRST) {
m_windowMap.insert(m_windowMap.begin(), std::move(window));
} else if (activeWindowPos == Workspaces::ActiveWindowPosition::LAST) {
m_windowMap.emplace_back(std::move(window));
}
}
}
void Workspace::insertWindow(WindowCreationPayload create_window_payload) {
if (!create_window_payload.isEmpty(m_workspaceManager)) {
auto repr = create_window_payload.repr(m_workspaceManager);
const bool should_display = !repr.empty() || m_workspaceManager.enableTaskbar();
if (should_display) {
auto addr = create_window_payload.getAddress();
auto it = std::ranges::find_if(
m_windowMap, [&addr](const auto& window) { return window.address == addr; });
// If the vector contains the address, update the window representation, otherwise insert it
if (it != m_windowMap.end()) {
*it = repr;
} else {
m_windowMap.emplace_back(repr);
}
}
}
}
bool Workspace::onWindowOpened(WindowCreationPayload const& create_window_payload) {
if (create_window_payload.getWorkspaceName() == name()) {
insertWindow(create_window_payload);
return true;
}
return false;
}
std::string& Workspace::selectString(std::map<std::string, std::string>& icons_map) {
spdlog::trace("Selecting string for workspace {}", name());
if (isUrgent()) {
auto urgentNamedIconIt = icons_map.find("urgent:" + name());
if (urgentNamedIconIt != icons_map.end()) {
return urgentNamedIconIt->second;
}
auto urgentIconIt = icons_map.find("urgent");
if (urgentIconIt != icons_map.end()) {
return urgentIconIt->second;
}
}
if (isActive() && isSpecial()) {
auto activeIconIt = icons_map.find("active:" + name());
if (activeIconIt != icons_map.end()) {
return activeIconIt->second;
}
auto namedIconIt = icons_map.find(name());
if (namedIconIt != icons_map.end()) {
return namedIconIt->second;
}
}
if (isActive()) {
auto activeNamedIconIt = icons_map.find("active:" + name());
if (activeNamedIconIt != icons_map.end()) {
return activeNamedIconIt->second;
}
auto activeIconIt = icons_map.find("active");
if (activeIconIt != icons_map.end()) {
return activeIconIt->second;
}
}
if (isSpecial()) {
auto specialNamedIconIt = icons_map.find("special:" + name());
if (specialNamedIconIt != icons_map.end()) {
return specialNamedIconIt->second;
}
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& workspace_icon, const std::string& workspace_tooltip) {
if (this->m_workspaceManager.persistentOnly() && !this->isPersistent()) {
m_button.hide();
return;
}
// clang-format off
if (this->m_workspaceManager.hideActive() && \
this->isActive() && \
!this->isPersistent() && \
!this->isSpecial()) {
// clang-format on
m_button.hide();
return;
}
// 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;
}
if (this->m_workspaceManager.specialVisibleOnly() && this->isSpecial() && !this->isVisible()) {
m_button.hide();
return;
}
m_button.show();
auto styleContext = m_button.get_style_context();
addOrRemoveClass(styleContext, isActive(), "active");
addOrRemoveClass(styleContext, isSpecial(), "special");
addOrRemoveClass(styleContext, isSpecial(), name());
addOrRemoveClass(styleContext, isEmpty(), "empty");
addOrRemoveClass(styleContext, isPersistent(), "persistent");
addOrRemoveClass(styleContext, isUrgent(), "urgent");
addOrRemoveClass(styleContext, isVisible(), "visible");
addOrRemoveClass(styleContext, m_workspaceManager.getBarOutput() == output(), "hosting-monitor");
// Add workspace name as CSS class for per-workspace styling
if (!m_prevNameClass.empty()) {
styleContext->remove_class(m_prevNameClass);
m_prevNameClass.clear();
}
auto nameClass = sanitizeCssClass(name());
if (!nameClass.empty()) {
styleContext->add_class(nameClass);
m_prevNameClass = nameClass;
}
std::string windows;
// Optimization: The {windows} substitution string is only possible if the taskbar is disabled, no
// need to compute this if enableTaskbar() is true
if (!m_workspaceManager.enableTaskbar()) {
auto windowSeparator = m_workspaceManager.getWindowSeparator();
auto groupThreshold = m_workspaceManager.windowRewriteGroupThreshold();
auto end_it = m_workspaceManager.maxWindows() == 0
? m_windowMap.end()
: m_windowMap.begin() + m_workspaceManager.maxWindows();
if (groupThreshold > 0) {
// Build ordered counts of each unique icon (including singular ones when threshold set to 1)
std::vector<std::pair<std::string, int>> iconCounts;
for (auto it = m_windowMap.begin(); it != end_it; ++it) {
const auto& window_repr = *it;
auto found = std::ranges::find_if(
iconCounts, [&](const auto& p) { return p.first == window_repr.repr_rewrite; });
if (found != iconCounts.end()) {
found->second++;
} else {
iconCounts.emplace_back(window_repr.repr_rewrite, 1);
}
}
// Format the group string
auto groupFormat = m_workspaceManager.getWindowRewriteGroupFormat();
bool isNotFirst = false;
for (const auto& [icon, count] : iconCounts) {
if (count >= groupThreshold) {
if (isNotFirst) windows.append(windowSeparator);
isNotFirst = true;
try {
windows.append(fmt::format(fmt::runtime(groupFormat), fmt::arg("icon", icon),
fmt::arg("count", count)));
} catch (const fmt::format_error& e) {
spdlog::warn("Formatting window-rewrite-group-format error: {}", e.what());
windows.append(icon);
}
} else {
for (int i = 0; i < count; ++i) {
if (isNotFirst) windows.append(windowSeparator);
isNotFirst = true;
windows.append(icon);
}
}
}
} else {
// Not grouping icons
bool isNotFirst = false;
for (auto it = m_windowMap.begin(); it != end_it; ++it) {
if (isNotFirst) windows.append(windowSeparator);
isNotFirst = true;
windows.append(it->repr_rewrite);
}
}
}
if (!workspace_tooltip.empty()) {
m_button.set_tooltip_text(
fmt::format(fmt::runtime(workspace_tooltip), fmt::arg("id", id()), fmt::arg("name", name()),
fmt::arg("icon", workspace_icon), fmt::arg("windows", windows)));
}
auto formatBefore = m_workspaceManager.formatBefore();
m_labelBefore.set_markup(fmt::format(fmt::runtime(formatBefore), fmt::arg("id", id()),
fmt::arg("name", name()), fmt::arg("icon", workspace_icon),
fmt::arg("windows", windows)));
m_labelBefore.get_style_context()->add_class("workspace-label");
if (m_workspaceManager.enableTaskbar()) {
updateTaskbar(workspace_icon);
}
}
bool Workspace::isEmpty() const {
auto ignore_list = m_workspaceManager.getIgnoredWindows();
const bool no_ignore_rules = ignore_list.empty();
if (no_ignore_rules) {
return m_windows == 0;
}
// If there are windows but they are all ignored, consider the workspace empty
return std::all_of(
m_windowMap.begin(), m_windowMap.end(),
[this, &ignore_list](const auto& window_repr) { return shouldSkipWindow(window_repr); });
}
void Workspace::updateTaskbar(const std::string& workspace_icon) {
for (auto child : m_content.get_children()) {
if (child != &m_labelBefore) {
m_content.remove(*child);
}
}
bool isFirst = true;
auto processWindow = [&](const WindowRepr& window_repr) {
if (shouldSkipWindow(window_repr)) {
return; // skip
}
if (isFirst) {
isFirst = false;
} else if (m_workspaceManager.getWindowSeparator() != "") {
auto windowSeparator = Gtk::make_managed<Gtk::Label>(m_workspaceManager.getWindowSeparator());
m_content.pack_start(*windowSeparator, false, false);
windowSeparator->show();
}
auto window_box = Gtk::make_managed<Gtk::Box>(Gtk::ORIENTATION_HORIZONTAL);
window_box->set_tooltip_markup(window_repr.window_title);
auto button = Gtk::manage(new Gtk::Button());
button->set_relief(Gtk::RELIEF_NONE);
button->add(*window_box);
button->get_style_context()->add_class("taskbar-window");
if (window_repr.isActive) {
button->get_style_context()->add_class("active");
}
if (m_workspaceManager.onClickWindow() != "") {
button->signal_button_press_event().connect(
sigc::bind(sigc::mem_fun(*this, &Workspace::handleClick), window_repr.address), false);
}
auto text_before = fmt::format(fmt::runtime(m_workspaceManager.taskbarFormatBefore()),
fmt::arg("title", window_repr.window_title));
if (!text_before.empty()) {
auto window_label_before = Gtk::make_managed<Gtk::Label>(text_before);
window_box->pack_start(*window_label_before, true, true);
}
if (m_workspaceManager.taskbarWithIcon()) {
auto app_info_ = IconLoader::get_app_info_from_app_id_list(window_repr.window_class);
int icon_size = m_workspaceManager.taskbarIconSize();
auto window_icon = Gtk::make_managed<Gtk::Image>();
m_workspaceManager.iconLoader().image_load_icon(*window_icon, app_info_, icon_size);
window_box->pack_start(*window_icon, false, false);
}
auto text_after = fmt::format(fmt::runtime(m_workspaceManager.taskbarFormatAfter()),
fmt::arg("title", window_repr.window_title));
if (!text_after.empty()) {
auto window_label_after = Gtk::make_managed<Gtk::Label>(text_after);
window_box->pack_start(*window_label_after, true, true);
}
m_content.pack_start(*button, true, false);
button->show_all();
};
if (m_workspaceManager.taskbarReverseDirection()) {
auto rend_it = m_workspaceManager.maxWindows() == 0
? m_windowMap.rend()
: m_windowMap.rbegin() + m_workspaceManager.maxWindows();
for (auto it = m_windowMap.rbegin(); it != rend_it; ++it) {
processWindow(*it);
}
} else {
auto end_it = m_workspaceManager.maxWindows() == 0
? m_windowMap.end()
: m_windowMap.begin() + m_workspaceManager.maxWindows();
for (auto it = m_windowMap.begin(); it != end_it; ++it) {
processWindow(*it);
}
}
auto formatAfter = m_workspaceManager.formatAfter();
const bool has_format_after = !formatAfter.empty();
if (has_format_after) {
m_labelAfter.set_markup(fmt::format(fmt::runtime(formatAfter), fmt::arg("id", id()),
fmt::arg("name", name()),
fmt::arg("icon", workspace_icon)));
m_content.pack_end(m_labelAfter, false, false);
m_labelAfter.show();
}
}
bool Workspace::handleClick(const GdkEventButton* event_button, WindowAddress const& addr) const {
if (event_button->type == GDK_BUTTON_PRESS) {
std::string command = std::regex_replace(m_workspaceManager.onClickWindow(),
std::regex("\\{address\\}"), "0x" + addr);
command = std::regex_replace(command, std::regex("\\{button\\}"),
std::to_string(event_button->button));
auto res = util::command::execNoRead(command);
if (res.exit_code != 0) {
spdlog::error("Failed to execute {}: {}", command, res.out);
}
}
return true;
}
bool Workspace::shouldSkipWindow(const WindowRepr& window_repr) const {
auto ignore_list = m_workspaceManager.getIgnoredWindows();
auto it = std::ranges::find_if(ignore_list, [&window_repr](const auto& ignoreItem) {
return std::regex_match(window_repr.window_class, ignoreItem) ||
std::regex_match(window_repr.window_title, ignoreItem);
});
return it != ignore_list.end();
}
} // namespace waybar::modules::hyprland
File diff suppressed because it is too large Load Diff
+150 -24
View File
@@ -1,5 +1,6 @@
#include "modules/idle_inhibitor.hpp"
#include "ext-idle-notify-v1-client-protocol.h"
#include "idle-inhibit-unstable-v1-client-protocol.h"
#include "util/command.hpp"
@@ -11,11 +12,24 @@ waybar::modules::IdleInhibitor::IdleInhibitor(const std::string& id, const Bar&
: ALabel(config, "idle_inhibitor", id, "{status}", 0, false, true),
bar_(bar),
idle_inhibitor_(nullptr),
pid_(-1) {
idle_notification_(nullptr),
idle_timeout_ms_(0),
pid_(-1),
wait_for_activity_(false) {
if (waybar::Client::inst()->idle_inhibit_manager == nullptr) {
throw std::runtime_error("idle-inhibit not available");
}
// Read the wait-for-activity config option
if (config_["wait-for-activity"].isBool()) {
wait_for_activity_ = config_["wait-for-activity"].asBool();
// Check if ext-idle-notify protocol is available when wait-for-activity is enabled
if (wait_for_activity_ && waybar::Client::inst()->idle_notifier == nullptr) {
throw std::runtime_error("wait-for-activity requires ext-idle-notify-v1 protocol support");
}
}
if (waybar::modules::IdleInhibitor::modules.empty() && config_["start-activated"].isBool() &&
config_["start-activated"].asBool() != status) {
toggleStatus();
@@ -32,6 +46,8 @@ waybar::modules::IdleInhibitor::IdleInhibitor(const std::string& id, const Bar&
}
waybar::modules::IdleInhibitor::~IdleInhibitor() {
teardownIdleNotification();
if (idle_inhibitor_ != nullptr) {
zwp_idle_inhibitor_v1_destroy(idle_inhibitor_);
idle_inhibitor_ = nullptr;
@@ -63,20 +79,24 @@ auto waybar::modules::IdleInhibitor::update() -> void {
}
std::string status_text = status ? "activated" : "deactivated";
label_.set_markup(fmt::format(fmt::runtime(format_), fmt::arg("status", status_text),
fmt::arg("icon", getIcon(0, status_text))));
updateLabelAndTooltipForState(status_text, format_, "{status}", fmt::arg("status", status_text),
fmt::arg("icon", getIcon(0, status_text)));
label_.get_style_context()->add_class(status_text);
if (tooltipEnabled()) {
auto config = config_[status ? "tooltip-format-activated" : "tooltip-format-deactivated"];
auto tooltip_format = config.isString() ? config.asString() : "{status}";
label_.set_tooltip_markup(fmt::format(fmt::runtime(tooltip_format),
fmt::arg("status", status_text),
fmt::arg("icon", getIcon(0, status_text))));
}
// Call parent update
ALabel::update();
}
auto waybar::modules::IdleInhibitor::refresh(int sig) -> void {
if (config_["signal"].isInt() && sig == SIGRTMIN + config_["signal"].asInt()) {
toggleStatus();
// Make all other idle inhibitor modules update
for (auto const& module : waybar::modules::IdleInhibitor::modules) {
module->update();
}
}
}
void waybar::modules::IdleInhibitor::toggleStatus() {
status = !status;
@@ -88,21 +108,34 @@ void waybar::modules::IdleInhibitor::toggleStatus() {
if (status && config_["timeout"].isNumeric()) {
auto timeoutMins = config_["timeout"].asDouble();
int timeoutSecs = timeoutMins * 60;
idle_timeout_ms_ = timeoutSecs * 1000;
timeout_ = Glib::signal_timeout().connect_seconds(
[]() {
/* intentionally not tied to a module instance lifetime
* as the output with `this` can be disconnected
*/
spdlog::info("deactivating idle_inhibitor by timeout");
status = false;
for (auto const& module : waybar::modules::IdleInhibitor::modules) {
module->update();
}
/* disconnect */
return false;
},
timeoutSecs);
// If wait-for-activity is enabled, set up idle notification
if (wait_for_activity_) {
spdlog::debug("idle_inhibitor: wait-for-activity enabled, timeout: {} ms", idle_timeout_ms_);
// Tear down any existing notification first to ensure fresh setup
teardownIdleNotification();
setupIdleNotification();
} else {
// Original behavior: simple timeout
timeout_ = Glib::signal_timeout().connect_seconds(
[]() {
/* intentionally not tied to a module instance lifetime
* as the output with `this` can be disconnected
*/
spdlog::info("deactivating idle_inhibitor by timeout");
status = false;
for (auto const& module : waybar::modules::IdleInhibitor::modules) {
module->update();
}
/* disconnect */
return false;
},
timeoutSecs);
}
} else {
// When deactivated, tear down idle notification
teardownIdleNotification();
}
}
@@ -121,3 +154,96 @@ bool waybar::modules::IdleInhibitor::handleToggle(GdkEventButton* const& e) {
ALabel::handleToggle(e);
return true;
}
void waybar::modules::IdleInhibitor::handleIdled(void* data,
ext_idle_notification_v1* /*notification*/) {
spdlog::info("deactivating idle_inhibitor due to user inactivity");
status = false;
// Clean up the notification since we're deactivating
auto* self = static_cast<IdleInhibitor*>(data);
if (self != nullptr) {
self->teardownIdleNotification();
}
for (auto const& module : waybar::modules::IdleInhibitor::modules) {
module->update();
}
}
void waybar::modules::IdleInhibitor::handleResumed(void* data,
ext_idle_notification_v1* /*notification*/) {
// User became active again - notification will continue monitoring
spdlog::debug("user activity detected, idle_inhibitor still active");
}
void waybar::modules::IdleInhibitor::setupIdleNotification() {
spdlog::debug("idle_inhibitor: setting up idle notification");
// Clean up any existing notification first
if (idle_notification_ != nullptr) {
spdlog::debug("idle_inhibitor: cleaning up existing notification before setup");
teardownIdleNotification();
}
auto* client = waybar::Client::inst();
if (client->idle_notifier == nullptr) {
spdlog::error("ext-idle-notify protocol not available");
return;
}
// Get the wayland seat from the display
auto* gdk_seat = gdk_display_get_default_seat(client->gdk_display->gobj());
if (gdk_seat == nullptr) {
spdlog::error("failed to get default seat");
return;
}
auto* wl_seat = gdk_wayland_seat_get_wl_seat(gdk_seat);
// Check protocol version to determine which function to use
uint32_t version =
wl_proxy_get_version(reinterpret_cast<struct wl_proxy*>(client->idle_notifier));
spdlog::debug("idle_inhibitor: creating notification with timeout {} ms (protocol version {})",
idle_timeout_ms_, version);
if (version >= 2) {
// Version 2+: Use get_input_idle_notification which ignores idle inhibitors
// This allows us to detect actual user inactivity even while the inhibitor is active
spdlog::debug("idle_inhibitor: using get_input_idle_notification (ignores inhibitors)");
idle_notification_ = ext_idle_notifier_v1_get_input_idle_notification(
client->idle_notifier, idle_timeout_ms_, wl_seat);
} else {
// Version 1: Fall back to get_idle_notification
// WARNING: This respects idle inhibitors, so it won't fire while inhibitor is active
spdlog::warn(
"idle_inhibitor: ext-idle-notifier-v1 version {} doesn't support "
"get_input_idle_notification, "
"wait-for-activity may not work correctly",
version);
idle_notification_ = ext_idle_notifier_v1_get_idle_notification(client->idle_notifier,
idle_timeout_ms_, wl_seat);
}
if (idle_notification_ == nullptr) {
spdlog::error("idle_inhibitor: failed to create idle notification");
return;
}
static const struct ext_idle_notification_v1_listener idle_notification_listener = {
.idled = &IdleInhibitor::handleIdled,
.resumed = &IdleInhibitor::handleResumed,
};
ext_idle_notification_v1_add_listener(idle_notification_, &idle_notification_listener, this);
wl_display_roundtrip(client->wl_display);
spdlog::debug("idle_inhibitor: idle notification setup complete");
}
void waybar::modules::IdleInhibitor::teardownIdleNotification() {
if (idle_notification_ != nullptr) {
spdlog::debug("idle_inhibitor: tearing down idle notification");
ext_idle_notification_v1_destroy(idle_notification_);
idle_notification_ = nullptr;
}
}
+43 -19
View File
@@ -1,5 +1,7 @@
#include "modules/image.hpp"
#include <config.hpp>
waybar::modules::Image::Image(const std::string& id, const Json::Value& config)
: AModule(config, "image", id), box_(Gtk::ORIENTATION_HORIZONTAL, 0) {
box_.pack_start(image_);
@@ -14,14 +16,32 @@ waybar::modules::Image::Image(const std::string& id, const Json::Value& config)
size_ = config["size"].asInt();
interval_ = config_["interval"].asInt();
const auto once = std::chrono::milliseconds::max();
if (!config_.isMember("interval") || config_["interval"].isNull() ||
config_["interval"] == "once") {
interval_ = once;
} else if (config_["interval"].isNumeric()) {
const auto interval_seconds = config_["interval"].asDouble();
if (interval_seconds <= 0) {
interval_ = once;
} else {
interval_ =
std::chrono::milliseconds(std::max(1L, // Minimum 1ms due to millisecond precision
static_cast<long>(interval_seconds * 1000)));
}
} else {
interval_ = once;
}
if (size_ == 0) {
size_ = 16;
}
if (interval_ == 0) {
interval_ = INT_MAX;
if (config_["path"].isString()) {
auto result = Config::tryExpandPath(config_["path"].asString(), "");
path_ = result.empty() ? "" : result.front();
} else {
path_.clear();
}
delayWorker();
@@ -30,40 +50,44 @@ waybar::modules::Image::Image(const std::string& id, const Json::Value& config)
void waybar::modules::Image::delayWorker() {
thread_ = [this] {
dp.emit();
auto interval = std::chrono::seconds(interval_);
thread_.sleep_for(interval);
thread_.sleep_for(interval_);
};
}
void waybar::modules::Image::refresh(int sig) {
if (sig == SIGRTMIN + config_["signal"].asInt()) {
#ifdef SIGRTMIN
if (config_["signal"].isInt() && sig == SIGRTMIN + config_["signal"].asInt()) {
thread_.wake_up();
}
#endif
}
auto waybar::modules::Image::update() -> void {
Glib::RefPtr<Gdk::Pixbuf> pixbuf;
if (config_["path"].isString()) {
path_ = config_["path"].asString();
} else if (config_["exec"].isString()) {
if (config_["exec"].isString()) {
output_ = util::command::exec(config_["exec"].asString(), "");
parseOutputRaw();
} else {
path_ = "";
// expand path if "~" or "$HOME" is present in original path
auto result = Config::tryExpandPath(path_, "");
path_ = result.empty() ? "" : result.front();
}
if (Glib::file_test(path_, Glib::FILE_TEST_EXISTS))
pixbuf = Gdk::Pixbuf::create_from_file(path_, size_, size_);
else
pixbuf = {};
if (pixbuf) {
if (Glib::file_test(path_, Glib::FILE_TEST_EXISTS)) {
Glib::RefPtr<Gdk::Pixbuf> pixbuf;
int scaled_icon_size = size_ * image_.get_scale_factor();
pixbuf = Gdk::Pixbuf::create_from_file(path_, scaled_icon_size, scaled_icon_size);
auto surface = Gdk::Cairo::create_surface_from_pixbuf(pixbuf, image_.get_scale_factor(),
image_.get_window());
image_.set(surface);
image_.show();
if (tooltipEnabled() && !tooltip_.empty()) {
if (box_.get_tooltip_markup() != tooltip_) {
box_.set_tooltip_markup(tooltip_);
}
}
image_.set(pixbuf);
image_.show();
box_.get_style_context()->remove_class("empty");
} else {
image_.clear();
+3 -2
View File
@@ -85,7 +85,8 @@ auto getInhibitors(const Json::Value& config) -> std::string {
if (config["what"].isArray()) {
inhibitors = checkInhibitor(config["what"][0].asString());
for (decltype(config["what"].size()) i = 1; i < config["what"].size(); ++i) {
inhibitors += ":" + checkInhibitor(config["what"][i].asString());
inhibitors.append(":");
inhibitors.append(checkInhibitor(config["what"][i].asString()));
}
return inhibitors;
}
@@ -123,7 +124,7 @@ auto Inhibitor::update() -> void {
label_.get_style_context()->add_class(status_text);
if (tooltipEnabled()) {
label_.set_tooltip_text(status_text);
label_.set_tooltip_markup(status_text);
}
return ALabel::update();
+15 -21
View File
@@ -2,7 +2,7 @@
namespace waybar::modules {
JACK::JACK(const std::string &id, const Json::Value &config)
JACK::JACK(const std::string& id, const Json::Value& config)
: ALabel(config, "jack", id, "{load}%", 1) {
running_ = false;
client_ = NULL;
@@ -53,7 +53,7 @@ std::string JACK::JACKState() {
auto JACK::update() -> void {
std::string format;
std::string state = JACKState();
float latency = 1000 * (float)bufsize_ / (float)samplerate_;
float latency = samplerate_ > 0 ? 1000.0f * (float)bufsize_ / (float)samplerate_ : 0.0f;
if (label_.get_style_context()->has_class("xrun")) {
label_.get_style_context()->remove_class("xrun");
@@ -72,35 +72,29 @@ auto JACK::update() -> void {
} else
format = "{load}%";
label_.set_markup(fmt::format(fmt::runtime(format), fmt::arg("load", std::round(load_)),
fmt::arg("bufsize", bufsize_), fmt::arg("samplerate", samplerate_),
fmt::arg("latency", fmt::format("{:.2f}", latency)),
fmt::arg("xruns", xruns_)));
if (tooltipEnabled()) {
std::string tooltip_format = "{bufsize}/{samplerate} {latency}ms";
if (config_["tooltip-format"].isString()) tooltip_format = config_["tooltip-format"].asString();
label_.set_tooltip_text(fmt::format(
fmt::runtime(tooltip_format), fmt::arg("load", std::round(load_)),
fmt::arg("bufsize", bufsize_), fmt::arg("samplerate", samplerate_),
fmt::arg("latency", fmt::format("{:.2f}", latency)), fmt::arg("xruns", xruns_)));
}
updateLabelAndTooltip(
format, "{bufsize}/{samplerate} {latency}ms", fmt::arg("load", std::round(load_)),
fmt::arg("bufsize", bufsize_), fmt::arg("samplerate", samplerate_),
fmt::arg("latency", fmt::format("{:.2f}", latency)), fmt::arg("xruns", xruns_));
// Call parent update
ALabel::update();
}
int JACK::bufSize(jack_nframes_t size) {
std::lock_guard<std::mutex> lock(mutex_);
bufsize_ = size;
return 0;
}
int JACK::sampleRate(jack_nframes_t rate) {
std::lock_guard<std::mutex> lock(mutex_);
samplerate_ = rate;
return 0;
}
int JACK::xrun() {
std::lock_guard<std::mutex> lock(mutex_);
xruns_ += 1;
state_ = "xrun";
return 0;
@@ -113,14 +107,14 @@ void JACK::shutdown() {
} // namespace waybar::modules
int bufSizeCallback(jack_nframes_t size, void *obj) {
return static_cast<waybar::modules::JACK *>(obj)->bufSize(size);
int bufSizeCallback(jack_nframes_t size, void* obj) {
return static_cast<waybar::modules::JACK*>(obj)->bufSize(size);
}
int sampleRateCallback(jack_nframes_t rate, void *obj) {
return static_cast<waybar::modules::JACK *>(obj)->sampleRate(rate);
int sampleRateCallback(jack_nframes_t rate, void* obj) {
return static_cast<waybar::modules::JACK*>(obj)->sampleRate(rate);
}
int xrunCallback(void *obj) { return static_cast<waybar::modules::JACK *>(obj)->xrun(); }
int xrunCallback(void* obj) { return static_cast<waybar::modules::JACK*>(obj)->xrun(); }
void shutdownCallback(void *obj) { return static_cast<waybar::modules::JACK *>(obj)->shutdown(); }
void shutdownCallback(void* obj) { return static_cast<waybar::modules::JACK*>(obj)->shutdown(); }
+85 -15
View File
@@ -78,6 +78,53 @@ auto supportsLockStates(const libevdev* dev) -> bool {
libevdev_has_event_code(dev, EV_LED, LED_SCROLLL);
}
auto isCommonFormatIcons(const Json::Value& config) -> bool {
return config["format-icons"].isObject() && (config["format-icons"]["locked"].isString() ||
config["format-icons"]["unlocked"].isString());
}
auto keyStateToIcons(const Json::Value& config)
-> std::unordered_map<std::string, std::vector<std::string>> {
std::unordered_map<std::string, std::vector<std::string>> key_icon_states;
std::vector<std::string> default_icons = {"unlocked", "locked"};
if (isCommonFormatIcons(config)) {
std::vector<std::string> icons = {
config["format-icons"]["unlocked"].isString()
? config["format-icons"]["unlocked"].asString()
: "unlocked",
config["format-icons"]["locked"].isString() ? config["format-icons"]["locked"].asString()
: "locked",
};
key_icon_states["Lock"] = icons;
return key_icon_states;
}
bool found_any = false;
for (const auto& key : std::vector<std::string>{"numlock", "capslock", "scrolllock"}) {
std::string map_key = key.substr(0, key.length() - 4);
map_key[0] = std::toupper(map_key[0]);
if (config["format-icons"].isObject() && config["format-icons"][key].isObject()) {
std::string unlocked = config["format-icons"][key]["unlocked"].isString()
? config["format-icons"][key]["unlocked"].asString()
: "unlocked";
std::string locked = config["format-icons"][key]["locked"].isString()
? config["format-icons"][key]["locked"].asString()
: "locked";
key_icon_states[map_key] = {unlocked, locked};
found_any = true;
}
}
if (!found_any) {
key_icon_states["Num"] = default_icons;
key_icon_states["Caps"] = default_icons;
key_icon_states["Scroll"] = default_icons;
}
return key_icon_states;
}
waybar::modules::KeyboardState::KeyboardState(const std::string& id, const Bar& bar,
const Json::Value& config)
: AModule(config, "keyboard-state", id, false, !config["disable-scroll"].asBool()),
@@ -98,12 +145,7 @@ waybar::modules::KeyboardState::KeyboardState(const std::string& id, const Bar&
: "{name} {icon}"),
interval_(
std::chrono::seconds(config_["interval"].isUInt() ? config_["interval"].asUInt() : 1)),
icon_locked_(config_["format-icons"]["locked"].isString()
? config_["format-icons"]["locked"].asString()
: "locked"),
icon_unlocked_(config_["format-icons"]["unlocked"].isString()
? config_["format-icons"]["unlocked"].asString()
: "unlocked"),
key_icon_states_(keyStateToIcons(config_)),
devices_path_("/dev/input/"),
libinput_(nullptr),
libinput_devices_({}) {
@@ -232,9 +274,12 @@ waybar::modules::KeyboardState::KeyboardState(const std::string& id, const Bar&
}
tryAddDevice(dev_path);
} else if (event->mask & IN_DELETE) {
std::lock_guard<std::mutex> lock(devices_mutex_);
auto it = libinput_devices_.find(dev_path);
if (it != libinput_devices_.end()) {
spdlog::info("Keyboard {} has been removed.", dev_path);
libinput_path_remove_device(it->second);
libinput_device_unref(it->second);
libinput_devices_.erase(it);
}
}
@@ -245,6 +290,7 @@ waybar::modules::KeyboardState::KeyboardState(const std::string& id, const Bar&
}
waybar::modules::KeyboardState::~KeyboardState() {
std::lock_guard<std::mutex> lock(devices_mutex_);
for (const auto& [_, dev_ptr] : libinput_devices_) {
libinput_path_remove_device(dev_ptr);
}
@@ -256,11 +302,17 @@ auto waybar::modules::KeyboardState::update() -> void {
try {
std::string dev_path;
if (config_["device-path"].isString() &&
libinput_devices_.find(config_["device-path"].asString()) != libinput_devices_.end()) {
dev_path = config_["device-path"].asString();
} else {
dev_path = libinput_devices_.begin()->first;
{
std::lock_guard<std::mutex> lock(devices_mutex_);
if (libinput_devices_.empty()) {
return;
}
if (config_["device-path"].isString() &&
libinput_devices_.find(config_["device-path"].asString()) != libinput_devices_.end()) {
dev_path = config_["device-path"].asString();
} else {
dev_path = libinput_devices_.begin()->first;
}
}
int fd = openFile(dev_path, O_NONBLOCK | O_CLOEXEC | O_RDONLY);
auto dev = openDevice(fd);
@@ -280,7 +332,7 @@ auto waybar::modules::KeyboardState::update() -> void {
bool state;
Gtk::Label& label;
const std::string& format;
const char* name;
const std::string name;
} label_states[] = {
{(bool)numl, numlock_label_, numlock_format_, "Num"},
{(bool)capsl, capslock_label_, capslock_format_, "Caps"},
@@ -288,8 +340,21 @@ auto waybar::modules::KeyboardState::update() -> void {
};
for (auto& label_state : label_states) {
std::string text;
std::string map_key = isCommonFormatIcons(config_) ? "Lock" : label_state.name;
if (key_icon_states_.find(map_key) == key_icon_states_.end()) {
spdlog::warn("keyboard-state: Missing icon configuration for '{}'", map_key);
continue;
}
auto& icons = key_icon_states_[map_key];
if (icons.size() < 2) {
spdlog::warn("keyboard-state: Invalid icon vector size for '{}'", map_key);
continue;
}
text = fmt::format(fmt::runtime(label_state.format),
fmt::arg("icon", label_state.state ? icon_locked_ : icon_unlocked_),
fmt::arg("icon", label_state.state ? icons[1] : icons[0]),
fmt::arg("name", label_state.name));
label_state.label.set_markup(text);
if (label_state.state) {
@@ -308,10 +373,15 @@ auto waybar::modules ::KeyboardState::tryAddDevice(const std::string& dev_path)
auto dev = openDevice(fd);
if (supportsLockStates(dev)) {
spdlog::info("Found device {} at '{}'", libevdev_get_name(dev), dev_path);
std::lock_guard<std::mutex> lock(devices_mutex_);
if (libinput_devices_.find(dev_path) == libinput_devices_.end()) {
auto device = libinput_path_add_device(libinput_, dev_path.c_str());
libinput_device_ref(device);
libinput_devices_[dev_path] = device;
if (device) {
libinput_device_ref(device);
libinput_devices_[dev_path] = device;
} else {
spdlog::warn("keyboard-state: Failed to add device to libinput: {}", dev_path);
}
}
}
libevdev_free(dev);
+1 -1
View File
@@ -22,7 +22,7 @@ auto waybar::modules::Load::update() -> void {
auto [load1, load5, load15] = Load::getLoad();
if (tooltipEnabled()) {
auto tooltip = fmt::format("Load 1: {}\nLoad 5: {}\nLoad 15: {}", load1, load5, load15);
label_.set_tooltip_text(tooltip);
label_.set_tooltip_markup(tooltip);
}
auto format = format_;
auto state = getState(load1);
+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
+4 -4
View File
@@ -21,13 +21,13 @@ static uint64_t get_total_memory() {
u_long physmem;
#endif
int mib[] = {
CTL_HW,
CTL_HW,
#if defined(HW_MEMSIZE)
HW_MEMSIZE,
HW_MEMSIZE,
#elif defined(HW_PHYSMEM64)
HW_PHYSMEM64,
HW_PHYSMEM64,
#else
HW_PHYSMEM,
HW_PHYSMEM,
#endif
};
u_int miblen = sizeof(mib) / sizeof(mib[0]);
+39 -34
View File
@@ -1,11 +1,30 @@
#include "modules/memory.hpp"
namespace {
const std::unordered_map<std::string, float> kUnits = {{"kB", 1.000},
{"kiB", 1.024},
{"MB", 1.000 * 1000.0},
{"MiB", 1.024 * 1024.0},
{"GB", 1.000 * 1000.0 * 1000.0},
{"GiB", 1.024 * 1024.0 * 1024.0},
{"TB", 1.000 * 1000.0 * 1000.0 * 1000.0},
{"TiB", 1.024 * 1024.0 * 1024.0 * 1024.0}};
}
waybar::modules::Memory::Memory(const std::string& id, const Json::Value& config)
: ALabel(config, "memory", id, "{}%", 30) {
thread_ = [this] {
dp.emit();
thread_.sleep_for(interval_);
};
if (config["unit"].isString()) {
unit_ = config["unit"].asString();
if (!kUnits.contains(unit_)) {
unit_ = "GiB";
}
} else {
unit_ = "GiB";
}
}
auto waybar::modules::Memory::update() -> void {
@@ -13,15 +32,15 @@ auto waybar::modules::Memory::update() -> void {
unsigned long memtotal = meminfo_["MemTotal"];
unsigned long swaptotal = 0;
if (meminfo_.count("SwapTotal")) {
if (meminfo_.contains("SwapTotal")) {
swaptotal = meminfo_["SwapTotal"];
}
unsigned long memfree;
unsigned long swapfree = 0;
if (meminfo_.count("SwapFree")) {
if (meminfo_.contains("SwapFree")) {
swapfree = meminfo_["SwapFree"];
}
if (meminfo_.count("MemAvailable")) {
if (meminfo_.contains("MemAvailable")) {
// New kernels (3.4+) have an accurate available memory field.
memfree = meminfo_["MemAvailable"] + meminfo_["zfs_size"];
} else {
@@ -31,18 +50,19 @@ auto waybar::modules::Memory::update() -> void {
}
if (memtotal > 0 && memfree >= 0) {
float total_ram_gigabytes =
0.01 * round(memtotal / 10485.76); // 100*10485.76 = 2^20 = 1024^2 = GiB/KiB
float total_swap_gigabytes = 0.01 * round(swaptotal / 10485.76);
int used_ram_percentage = 100 * (memtotal - memfree) / memtotal;
int used_swap_percentage = 0;
if (swaptotal && swapfree) {
if ((bool)swaptotal) {
used_swap_percentage = 100 * (swaptotal - swapfree) / swaptotal;
}
float used_ram_gigabytes = 0.01 * round((memtotal - memfree) / 10485.76);
float used_swap_gigabytes = 0.01 * round((swaptotal - swapfree) / 10485.76);
float available_ram_gigabytes = 0.01 * round(memfree / 10485.76);
float available_swap_gigabytes = 0.01 * round(swapfree / 10485.76);
float divisor = kUnits.at(unit_);
float total_ram = memtotal / divisor;
float total_swap = swaptotal / divisor;
float used_ram = (memtotal - memfree) / divisor;
float used_swap = (swaptotal - swapfree) / divisor;
float available_ram = memfree / divisor;
float available_swap = swapfree / divisor;
auto format = format_;
auto state = getState(used_ram_percentage);
@@ -55,29 +75,14 @@ auto waybar::modules::Memory::update() -> void {
} else {
event_box_.show();
auto icons = std::vector<std::string>{state};
label_.set_markup(fmt::format(
fmt::runtime(format), used_ram_percentage,
fmt::arg("icon", getIcon(used_ram_percentage, icons)),
fmt::arg("total", total_ram_gigabytes), fmt::arg("swapTotal", total_swap_gigabytes),
fmt::arg("percentage", used_ram_percentage),
fmt::arg("swapPercentage", used_swap_percentage), fmt::arg("used", used_ram_gigabytes),
fmt::arg("swapUsed", used_swap_gigabytes), fmt::arg("avail", available_ram_gigabytes),
fmt::arg("swapAvail", available_swap_gigabytes)));
}
if (tooltipEnabled()) {
if (config_["tooltip-format"].isString()) {
auto tooltip_format = config_["tooltip-format"].asString();
label_.set_tooltip_text(fmt::format(
fmt::runtime(tooltip_format), used_ram_percentage,
fmt::arg("total", total_ram_gigabytes), fmt::arg("swapTotal", total_swap_gigabytes),
fmt::arg("percentage", used_ram_percentage),
fmt::arg("swapPercentage", used_swap_percentage), fmt::arg("used", used_ram_gigabytes),
fmt::arg("swapUsed", used_swap_gigabytes), fmt::arg("avail", available_ram_gigabytes),
fmt::arg("swapAvail", available_swap_gigabytes)));
} else {
label_.set_tooltip_text(fmt::format("{:.{}f}GiB used", used_ram_gigabytes, 1));
}
updateLabelAndTooltip(
format, fmt::format("{:.{}f}{} used", used_ram, 1, unit_), used_ram_percentage,
fmt::arg("icon", getIcon(used_ram_percentage, icons)), fmt::arg("total", total_ram),
fmt::arg("swapTotal", total_swap), fmt::arg("percentage", used_ram_percentage),
fmt::arg("swapState", swaptotal == 0 ? "Off" : "On"),
fmt::arg("swapPercentage", used_swap_percentage), fmt::arg("used", used_ram),
fmt::arg("swapUsed", used_swap), fmt::arg("avail", available_ram),
fmt::arg("swapAvail", available_swap));
}
} else {
event_box_.hide();
+69 -49
View File
@@ -6,6 +6,7 @@
#include <system_error>
#include <util/sanitize_str.hpp>
#include <util/utf8_string.hpp>
using namespace waybar::util;
#include "modules/mpd/state.hpp"
@@ -22,9 +23,12 @@ waybar::modules::MPD::MPD(const std::string& id, const Json::Value& config)
port_(config_["port"].isUInt() ? config["port"].asUInt() : 0),
password_(config_["password"].empty() ? "" : config_["password"].asString()),
timeout_(config_["timeout"].isUInt() ? config_["timeout"].asUInt() * 1'000 : 30'000),
playing_interval_(config_["playing-interval"].isUInt() ? config_["playing-interval"].asUInt()
: 1'000),
connection_(nullptr, &mpd_connection_free),
status_(nullptr, &mpd_status_free),
song_(nullptr, &mpd_song_free) {
song_(nullptr, &mpd_song_free),
ellipsis_(config_["ellipsis"].isString() ? config_["ellipsis"].asString() : "\u2026") {
if (!config_["port"].isNull() && !config_["port"].isUInt()) {
spdlog::warn("{}: `port` configuration should be an unsigned int", module_name_);
}
@@ -33,6 +37,10 @@ waybar::modules::MPD::MPD(const std::string& id, const Json::Value& config)
spdlog::warn("{}: `timeout` configuration should be an unsigned int", module_name_);
}
if (!config_["playing-interval"].isNull() && !config_["playing-interval"].isUInt()) {
spdlog::warn("{}: `playing-interval` configuration should be an unsigned int", module_name_);
}
if (!config["server"].isNull()) {
if (!config_["server"].isString()) {
spdlog::warn("{}:`server` configuration should be a string", module_name_);
@@ -51,21 +59,6 @@ auto waybar::modules::MPD::update() -> void {
ALabel::update();
}
void waybar::modules::MPD::queryMPD() {
if (connection_ != nullptr) {
spdlog::trace("{}: fetching state information", module_name_);
try {
fetchState();
spdlog::trace("{}: fetch complete", module_name_);
} catch (std::exception const& e) {
spdlog::error("{}: {}", module_name_, e.what());
state_ = MPD_STATE_UNKNOWN;
}
dp.emit();
}
}
std::string waybar::modules::MPD::getTag(mpd_tag_type type, unsigned idx) const {
std::string result =
config_["unknown-tag"].isString() ? config_["unknown-tag"].asString() : "N/A";
@@ -98,7 +91,7 @@ void waybar::modules::MPD::setLabel() {
? config_["format-disconnected"].asString()
: "disconnected";
if (format.empty()) {
label_.set_markup(format);
setLabelMarkup(format);
label_.show();
} else {
label_.hide();
@@ -110,22 +103,22 @@ void waybar::modules::MPD::setLabel() {
? config_["tooltip-format-disconnected"].asString()
: "MPD (disconnected)";
// Nothing to format
label_.set_tooltip_text(tooltip_format);
setTooltipMarkup(tooltip_format);
}
return;
}
label_.get_style_context()->remove_class("disconnected");
auto format = format_;
Glib::ustring artist, album_artist, album, title;
std::string date, filename;
std::string date, filename, uri;
int song_pos = 0, queue_length = 0, volume = 0;
std::chrono::seconds elapsedTime, totalTime;
std::string stateIcon = "";
bool no_song = song_.get() == nullptr;
if (stopped() || no_song) {
if (no_song) spdlog::warn("Bug in mpd: no current song but state is not stopped.");
bool is_stopped = stopped();
if (is_stopped || no_song) {
if (no_song && !is_stopped) spdlog::warn("mpd: no current song while state is not stopped");
format =
config_["format-stopped"].isString() ? config_["format-stopped"].asString() : "stopped";
label_.get_style_context()->add_class("stopped");
@@ -137,20 +130,18 @@ void waybar::modules::MPD::setLabel() {
label_.get_style_context()->add_class("playing");
label_.get_style_context()->remove_class("paused");
} else if (paused()) {
format = config_["format-paused"].isString() ? config_["format-paused"].asString()
: config_["format"].asString();
if (config_["format-paused"].isString()) {
format = config_["format-paused"].asString();
}
label_.get_style_context()->add_class("paused");
label_.get_style_context()->remove_class("playing");
}
stateIcon = getStateIcon();
artist = sanitize_string(getTag(MPD_TAG_ARTIST));
album_artist = sanitize_string(getTag(MPD_TAG_ALBUM_ARTIST));
album = sanitize_string(getTag(MPD_TAG_ALBUM));
title = sanitize_string(getTag(MPD_TAG_TITLE));
date = sanitize_string(getTag(MPD_TAG_DATE));
filename = sanitize_string(getFilename());
uri = mpd_song_get_uri(song_.get());
song_pos = mpd_status_get_song_pos(status_.get()) + 1;
volume = mpd_status_get_volume(status_.get());
if (volume < 0) {
@@ -169,27 +160,22 @@ void waybar::modules::MPD::setLabel() {
std::string repeatIcon = getOptionIcon("repeat", repeatActivated);
bool singleActivated = mpd_status_get_single(status_.get());
std::string singleIcon = getOptionIcon("single", singleActivated);
if (config_["artist-len"].isInt()) artist = artist.substr(0, config_["artist-len"].asInt());
if (config_["album-artist-len"].isInt())
album_artist = album_artist.substr(0, config_["album-artist-len"].asInt());
if (config_["album-len"].isInt()) album = album.substr(0, config_["album-len"].asInt());
if (config_["title-len"].isInt()) title = title.substr(0, config_["title-len"].asInt());
try {
auto text = fmt::format(
fmt::runtime(format), fmt::arg("artist", artist.raw()),
fmt::arg("albumArtist", album_artist.raw()), fmt::arg("album", album.raw()),
fmt::arg("title", title.raw()), fmt::arg("date", date), fmt::arg("volume", volume),
fmt::runtime(format), fmt::arg("artist", getArtistStr(true)),
fmt::arg("albumArtist", getAlbumArtistStr(true)), fmt::arg("album", getAlbumStr(true)),
fmt::arg("title", getTitleStr(true)), fmt::arg("date", date), fmt::arg("volume", volume),
fmt::arg("elapsedTime", elapsedTime), fmt::arg("totalTime", totalTime),
fmt::arg("songPosition", song_pos), fmt::arg("queueLength", queue_length),
fmt::arg("stateIcon", stateIcon), fmt::arg("consumeIcon", consumeIcon),
fmt::arg("randomIcon", randomIcon), fmt::arg("repeatIcon", repeatIcon),
fmt::arg("singleIcon", singleIcon), fmt::arg("filename", filename));
fmt::arg("singleIcon", singleIcon), fmt::arg("filename", filename), fmt::arg("uri", uri));
if (text.empty()) {
label_.hide();
} else {
label_.show();
label_.set_markup(text);
setLabelMarkup(text);
}
} catch (fmt::format_error const& e) {
spdlog::warn("mpd: format error: {}", e.what());
@@ -200,16 +186,16 @@ void waybar::modules::MPD::setLabel() {
tooltip_format = config_["tooltip-format"].isString() ? config_["tooltip-format"].asString()
: "MPD (connected)";
try {
auto tooltip_text =
fmt::format(fmt::runtime(tooltip_format), fmt::arg("artist", artist.raw()),
fmt::arg("albumArtist", album_artist.raw()), fmt::arg("album", album.raw()),
fmt::arg("title", title.raw()), fmt::arg("date", date),
fmt::arg("volume", volume), fmt::arg("elapsedTime", elapsedTime),
fmt::arg("totalTime", totalTime), fmt::arg("songPosition", song_pos),
fmt::arg("queueLength", queue_length), fmt::arg("stateIcon", stateIcon),
fmt::arg("consumeIcon", consumeIcon), fmt::arg("randomIcon", randomIcon),
fmt::arg("repeatIcon", repeatIcon), fmt::arg("singleIcon", singleIcon));
label_.set_tooltip_text(tooltip_text);
auto tooltip_text = fmt::format(
fmt::runtime(tooltip_format), fmt::arg("artist", getArtistStr(false)),
fmt::arg("albumArtist", getAlbumArtistStr(false)), fmt::arg("album", getAlbumStr(false)),
fmt::arg("title", getTitleStr(false)), fmt::arg("date", date), fmt::arg("volume", volume),
fmt::arg("elapsedTime", elapsedTime), fmt::arg("totalTime", totalTime),
fmt::arg("songPosition", song_pos), fmt::arg("queueLength", queue_length),
fmt::arg("stateIcon", stateIcon), fmt::arg("consumeIcon", consumeIcon),
fmt::arg("randomIcon", randomIcon), fmt::arg("repeatIcon", repeatIcon),
fmt::arg("singleIcon", singleIcon), fmt::arg("filename", filename), fmt::arg("uri", uri));
setTooltipMarkup(tooltip_text);
} catch (fmt::format_error const& e) {
spdlog::warn("mpd: format error (tooltip): {}", e.what());
}
@@ -238,7 +224,8 @@ std::string waybar::modules::MPD::getStateIcon() const {
}
}
std::string waybar::modules::MPD::getOptionIcon(std::string optionName, bool activated) const {
std::string waybar::modules::MPD::getOptionIcon(const std::string& optionName,
bool activated) const {
if (!config_[optionName + "-icons"].isObject()) {
return "";
}
@@ -255,6 +242,38 @@ std::string waybar::modules::MPD::getOptionIcon(std::string optionName, bool act
}
}
std::string waybar::modules::MPD::getArtistStr(bool truncated) const {
std::string artist = getTag(MPD_TAG_ARTIST);
if (truncated && config_["artist-len"].isInt()) {
waybar::util::utf8_truncate(artist, ellipsis_, config_["artist-len"].asInt());
}
return sanitize_string(artist);
}
std::string waybar::modules::MPD::getAlbumArtistStr(bool truncated) const {
std::string album_artist = getTag(MPD_TAG_ALBUM_ARTIST);
if (truncated && config_["album-artist-len"].isInt()) {
waybar::util::utf8_truncate(album_artist, ellipsis_, config_["album-artist-len"].asInt());
}
return sanitize_string(album_artist);
}
std::string waybar::modules::MPD::getAlbumStr(bool truncated) const {
std::string album = getTag(MPD_TAG_ALBUM);
if (truncated && config_["album-len"].isInt()) {
waybar::util::utf8_truncate(album, ellipsis_, config_["album-len"].asInt());
}
return sanitize_string(album);
}
std::string waybar::modules::MPD::getTitleStr(bool truncated) const {
std::string title = getTag(MPD_TAG_TITLE);
if (truncated && config_["title-len"].isInt()) {
waybar::util::utf8_truncate(title, ellipsis_, config_["title-len"].asInt());
}
return sanitize_string(title);
}
static bool isServerUnavailable(const std::error_code& ec) {
if (ec.category() == std::system_category()) {
switch (ec.value()) {
@@ -322,6 +341,7 @@ void waybar::modules::MPD::checkErrors(mpd_connection* conn) {
case MPD_ERROR_SYSTEM:
if (auto ec = mpd_connection_get_system_error(conn); ec != 0) {
mpd_connection_clear_error(conn);
connection_.reset();
throw std::system_error(ec, std::system_category());
}
G_GNUC_FALLTHROUGH;
+108 -35
View File
@@ -19,39 +19,37 @@ auto format_as(enum mpd_idle val) {
namespace waybar::modules::detail {
#define IDLE_RUN_NOIDLE_AND_CMD(...) \
if (idle_connection_.connected()) { \
idle_connection_.disconnect(); \
auto conn = ctx_->connection().get(); \
if (!mpd_run_noidle(conn)) { \
if (mpd_connection_get_error(conn) != MPD_ERROR_SUCCESS) { \
spdlog::error("mpd: Idle: failed to unregister for IDLE events"); \
ctx_->checkErrors(conn); \
} \
} \
__VA_ARGS__; \
#define RUN_NOIDLE_AND_CMD(STATE, ...) \
if (idle_connection_.connected()) { \
idle_connection_.disconnect(); \
auto conn = ctx_->connection().get(); \
if (!mpd_run_noidle(conn)) { \
if (mpd_connection_get_error(conn) != MPD_ERROR_SUCCESS) { \
spdlog::error("mpd: STATE: failed to unregister for IDLE events"); \
ctx_->checkErrors(conn); \
} \
} \
__VA_ARGS__; \
}
void Idle::play() {
IDLE_RUN_NOIDLE_AND_CMD(mpd_run_play(conn));
RUN_NOIDLE_AND_CMD(Idle, mpd_run_play(conn));
ctx_->setState(std::make_unique<Playing>(ctx_));
}
void Idle::pause() {
IDLE_RUN_NOIDLE_AND_CMD(mpd_run_pause(conn, true));
RUN_NOIDLE_AND_CMD(Idle, mpd_run_pause(conn, true));
ctx_->setState(std::make_unique<Paused>(ctx_));
}
void Idle::stop() {
IDLE_RUN_NOIDLE_AND_CMD(mpd_run_stop(conn));
RUN_NOIDLE_AND_CMD(Idle, mpd_run_stop(conn));
ctx_->setState(std::make_unique<Stopped>(ctx_));
}
#undef IDLE_RUN_NOIDLE_AND_CMD
void Idle::update() noexcept {
// This is intentionally blank.
}
@@ -97,19 +95,16 @@ bool Idle::on_io(Glib::IOCondition const&) {
}
ctx_->fetchState();
ctx_->emit();
mpd_state state = ctx_->state();
if (state == MPD_STATE_STOP) {
ctx_->emit();
ctx_->setState(std::make_unique<Stopped>(ctx_));
} else if (state == MPD_STATE_PLAY) {
ctx_->emit();
ctx_->setState(std::make_unique<Playing>(ctx_));
} else if (state == MPD_STATE_PAUSE) {
ctx_->emit();
ctx_->setState(std::make_unique<Paused>(ctx_));
} else {
ctx_->emit();
// self transition
ctx_->setState(std::make_unique<Idle>(ctx_));
}
@@ -118,15 +113,46 @@ bool Idle::on_io(Glib::IOCondition const&) {
}
void Playing::entry() noexcept {
sigc::slot<bool> timer_slot = sigc::mem_fun(*this, &Playing::on_timer);
timer_connection_ = Glib::signal_timeout().connect_seconds(timer_slot, 1);
spdlog::debug("mpd: Playing: enabled 1 second periodic timer.");
timer();
idle();
spdlog::debug("mpd: Playing: enabled {}ms periodic timer.", ctx_->playing_interval());
}
void Playing::exit() noexcept {
if (idle_connection_.connected()) {
idle_connection_.disconnect();
spdlog::debug("mpd: Playing: unwatching FD");
}
if (timer_connection_.connected()) {
timer_connection_.disconnect();
spdlog::debug("mpd: Playing: disabled 1 second periodic timer.");
spdlog::debug("mpd: Playing: disabled {}ms periodic timer.", ctx_->playing_interval());
}
}
void Playing::timer() noexcept {
if (timer_connection_.connected()) {
timer_connection_.disconnect();
}
sigc::slot<bool> timer_slot = sigc::mem_fun(*this, &Playing::on_timer);
timer_connection_ = Glib::signal_timeout().connect(timer_slot, ctx_->playing_interval());
}
void Playing::idle() noexcept {
auto conn = ctx_->connection().get();
assert(conn != nullptr);
if (!mpd_send_idle_mask(
conn, static_cast<mpd_idle>(MPD_IDLE_PLAYER | MPD_IDLE_OPTIONS | MPD_IDLE_QUEUE))) {
ctx_->checkErrors(conn);
spdlog::error("mpd: Playing: failed to register for IDLE events");
} else if (!idle_connection_.connected()) {
spdlog::trace("mpd: Playing: watching FD");
sigc::slot<bool, Glib::IOCondition const&> idle_slot = sigc::mem_fun(*this, &Playing::on_io);
idle_connection_ =
Glib::signal_io().connect(idle_slot, mpd_connection_get_fd(conn),
Glib::IO_IN | Glib::IO_PRI | Glib::IO_ERR | Glib::IO_HUP);
}
}
@@ -141,9 +167,12 @@ bool Playing::on_timer() {
return false;
}
RUN_NOIDLE_AND_CMD(Playing);
ctx_->fetchState();
if (!ctx_->is_playing()) {
ctx_->emit();
if (ctx_->is_paused()) {
ctx_->setState(std::make_unique<Paused>(ctx_));
} else {
@@ -152,8 +181,51 @@ bool Playing::on_timer() {
return false;
}
ctx_->queryMPD();
ctx_->emit();
idle();
} catch (std::exception const& e) {
spdlog::warn("mpd: Playing: error: {}", e.what());
ctx_->setState(std::make_unique<Disconnected>(ctx_));
return false;
}
return true;
}
bool Playing::on_io(Glib::IOCondition const&) {
auto conn = ctx_->connection().get();
// callback should do this:
enum mpd_idle events = mpd_recv_idle(conn, /* ignore_timeout?= */ false);
spdlog::debug("mpd: Playing: recv_idle events -> {}", events);
mpd_response_finish(conn);
try {
ctx_->checkErrors(conn);
ctx_->fetchState();
if (!ctx_->is_playing()) {
ctx_->emit();
if (ctx_->is_paused()) {
ctx_->setState(std::make_unique<Paused>(ctx_));
} else {
ctx_->setState(std::make_unique<Stopped>(ctx_));
}
return false;
}
ctx_->emit();
if (!mpd_send_idle_mask(
conn, static_cast<mpd_idle>(MPD_IDLE_PLAYER | MPD_IDLE_OPTIONS | MPD_IDLE_QUEUE))) {
ctx_->checkErrors(conn);
spdlog::error("mpd: Playing: failed to register for IDLE events");
}
// Defer the next timer
timer();
} catch (std::exception const& e) {
spdlog::warn("mpd: Playing: error: {}", e.what());
ctx_->setState(std::make_unique<Disconnected>(ctx_));
@@ -164,21 +236,23 @@ bool Playing::on_timer() {
}
void Playing::stop() {
if (timer_connection_.connected()) {
timer_connection_.disconnect();
RUN_NOIDLE_AND_CMD(
Playing, if (timer_connection_.connected()) {
timer_connection_.disconnect();
mpd_run_stop(ctx_->connection().get());
}
mpd_run_stop(ctx_->connection().get());
});
ctx_->setState(std::make_unique<Stopped>(ctx_));
}
void Playing::pause() {
if (timer_connection_.connected()) {
timer_connection_.disconnect();
RUN_NOIDLE_AND_CMD(
Playing, if (timer_connection_.connected()) {
timer_connection_.disconnect();
mpd_run_pause(ctx_->connection().get(), true);
}
mpd_run_pause(ctx_->connection().get(), true);
});
ctx_->setState(std::make_unique<Paused>(ctx_));
}
@@ -212,7 +286,6 @@ bool Paused::on_timer() {
}
ctx_->fetchState();
ctx_->emit();
if (ctx_->is_paused()) {
@@ -283,7 +356,6 @@ bool Stopped::on_timer() {
}
ctx_->fetchState();
ctx_->emit();
if (ctx_->is_stopped()) {
@@ -387,4 +459,5 @@ bool Disconnected::on_timer() {
void Disconnected::update() noexcept { ctx_->do_update(); }
#undef RUN_NOIDLE_AND_CMD
} // namespace waybar::modules::detail
+172 -140
View File
@@ -7,6 +7,7 @@
#include <string>
#include "util/scope_guard.hpp"
#include "util/utf8_string.hpp"
extern "C" {
#include <playerctl/playerctl.h>
@@ -14,7 +15,6 @@ extern "C" {
#include <glib.h>
#include <spdlog/spdlog.h>
namespace waybar::modules::mpris {
const std::string DEFAULT_FORMAT = "{player} ({status}): {dynamic}";
@@ -31,6 +31,7 @@ Mpris::Mpris(const std::string& id, const Json::Value& config)
dynamic_separator_(" - "),
truncate_hours_(true),
tooltip_len_limits_(false),
prefer_album_artist_(false),
// this character is used in Gnome so it's fine to use it here
ellipsis_("\u2026"),
player_("playerctld"),
@@ -68,6 +69,9 @@ Mpris::Mpris(const std::string& id, const Json::Value& config)
if (config_["enable-tooltip-len-limits"].isBool()) {
tooltip_len_limits_ = config["enable-tooltip-len-limits"].asBool();
}
if (config_["prefer-album-artist"].isBool()) {
prefer_album_artist_ = config["prefer-album-artist"].asBool();
}
}
if (config["artist-len"].isUInt()) {
@@ -96,9 +100,9 @@ Mpris::Mpris(const std::string& id, const Json::Value& config)
}
if (config_["dynamic-order"].isArray()) {
dynamic_order_.clear();
for (auto it = config_["dynamic-order"].begin(); it != config_["dynamic-order"].end(); ++it) {
if (it->isString()) {
dynamic_order_.push_back(it->asString());
for (const auto& item : config_["dynamic-order"]) {
if (item.isString()) {
dynamic_order_.push_back(item.asString());
}
}
}
@@ -110,16 +114,16 @@ Mpris::Mpris(const std::string& id, const Json::Value& config)
player_ = config_["player"].asString();
}
if (config_["ignored-players"].isArray()) {
for (auto it = config_["ignored-players"].begin(); it != config_["ignored-players"].end();
++it) {
if (it->isString()) {
ignored_players_.push_back(it->asString());
ignored_players_.reserve(config_["ignored-players"].size());
for (const auto& item : config_["ignored-players"]) {
if (item.isString()) {
ignored_players_.push_back(item.asString());
}
}
}
GError* error = nullptr;
waybar::util::ScopeGuard error_deleter([error]() {
waybar::util::ScopeGuard error_deleter([&error]() {
if (error) {
g_error_free(error);
}
@@ -146,8 +150,8 @@ Mpris::Mpris(const std::string& id, const Json::Value& config)
throw std::runtime_error(fmt::format("unable to list players: {}", error->message));
}
for (auto p = players; p != NULL; p = p->next) {
auto pn = static_cast<PlayerctlPlayerName*>(p->data);
for (auto* p = players; p != nullptr; p = p->next) {
auto* pn = static_cast<PlayerctlPlayerName*>(p->data);
if (strcmp(pn->name, player_.c_str()) == 0) {
player = playerctl_player_new_from_name(pn, &error);
break;
@@ -163,8 +167,7 @@ Mpris::Mpris(const std::string& id, const Json::Value& config)
if (player) {
g_object_connect(player, "signal::play", G_CALLBACK(onPlayerPlay), this, "signal::pause",
G_CALLBACK(onPlayerPause), this, "signal::stop", G_CALLBACK(onPlayerStop),
this, "signal::stop", G_CALLBACK(onPlayerStop), this, "signal::metadata",
G_CALLBACK(onPlayerMetadata), this, NULL);
this, "signal::metadata", G_CALLBACK(onPlayerMetadata), this, NULL);
}
// allow setting an interval count that triggers periodic refreshes
@@ -180,17 +183,23 @@ Mpris::Mpris(const std::string& id, const Json::Value& config)
}
Mpris::~Mpris() {
if (manager != NULL) g_object_unref(manager);
if (player != NULL) g_object_unref(player);
if (manager != nullptr) {
g_signal_handlers_disconnect_by_data(manager, this);
}
if (player != nullptr) {
g_signal_handlers_disconnect_by_data(player, this);
}
if (last_active_player_ != nullptr && last_active_player_ != player) {
g_object_unref(last_active_player_);
}
g_clear_object(&manager);
g_clear_object(&player);
}
auto Mpris::getIconFromJson(const Json::Value& icons, const std::string& key) -> std::string {
if (icons.isObject()) {
if (icons[key].isString()) {
return icons[key].asString();
} else if (icons["default"].isString()) {
return icons["default"].asString();
}
if (icons[key].isString()) return icons[key].asString();
if (icons["default"].isString()) return icons["default"].asString();
}
return "";
}
@@ -198,69 +207,28 @@ auto Mpris::getIconFromJson(const Json::Value& icons, const std::string& key) ->
// Wide characters count as two, zero-width characters count as zero
// Modifies str in-place (unless width = std::string::npos)
// Returns the total width of the string pre-truncating
size_t utf8_truncate(std::string& str, size_t width = std::string::npos) {
if (str.length() == 0) return 0;
const gchar* trunc_end = nullptr;
size_t total_width = 0;
for (gchar *data = str.data(), *end = data + str.size(); data;) {
gunichar c = g_utf8_get_char_validated(data, end - data);
if (c == -1U || c == -2U) {
// invalid unicode, treat string as ascii
if (width != std::string::npos && str.length() > width) str.resize(width);
return str.length();
} else if (g_unichar_iswide(c)) {
total_width += 2;
} else if (!g_unichar_iszerowidth(c) && c != 0xAD) { // neither zero-width nor soft hyphen
total_width += 1;
}
data = g_utf8_find_next_char(data, end);
if (width != std::string::npos && total_width <= width && !g_unichar_isspace(c))
trunc_end = data;
}
if (trunc_end) str.resize(trunc_end - str.data());
return total_width;
}
size_t utf8_width(const std::string& str) { return utf8_truncate(const_cast<std::string&>(str)); }
void truncate(std::string& s, const std::string& ellipsis, size_t max_len) {
if (max_len == 0) {
s.resize(0);
return;
}
size_t len = utf8_truncate(s, max_len);
if (len > max_len) {
size_t ellipsis_len = utf8_width(ellipsis);
if (max_len >= ellipsis_len) {
if (ellipsis_len) utf8_truncate(s, max_len - ellipsis_len);
s += ellipsis;
} else {
s.resize(0);
}
}
}
auto Mpris::getArtistStr(const PlayerInfo& info, bool truncated) -> std::string {
auto artist = info.artist.value_or(std::string());
if (truncated && artist_len_ >= 0) truncate(artist, ellipsis_, artist_len_);
if (prefer_album_artist_) {
auto album_artist = info.album_artist.value_or(std::string());
if (!album_artist.empty()) {
artist = album_artist;
}
}
if (truncated && artist_len_ >= 0) waybar::util::utf8_truncate(artist, ellipsis_, artist_len_);
return artist;
}
auto Mpris::getAlbumStr(const PlayerInfo& info, bool truncated) -> std::string {
auto album = info.album.value_or(std::string());
if (truncated && album_len_ >= 0) truncate(album, ellipsis_, album_len_);
if (truncated && album_len_ >= 0) waybar::util::utf8_truncate(album, ellipsis_, album_len_);
return album;
}
auto Mpris::getTitleStr(const PlayerInfo& info, bool truncated) -> std::string {
auto title = info.title.value_or(std::string());
if (truncated && title_len_ >= 0) truncate(title, ellipsis_, title_len_);
if (truncated && title_len_ >= 0) waybar::util::utf8_truncate(title, ellipsis_, title_len_);
return title;
}
@@ -269,7 +237,7 @@ auto Mpris::getLengthStr(const PlayerInfo& info, bool truncated) -> std::string
auto length = info.length.value();
return (truncated && length.substr(0, 3) == "00:") ? length.substr(3) : length;
}
return std::string();
return {};
}
auto Mpris::getPositionStr(const PlayerInfo& info, bool truncated) -> std::string {
@@ -277,7 +245,7 @@ auto Mpris::getPositionStr(const PlayerInfo& info, bool truncated) -> std::strin
auto position = info.position.value();
return (truncated && position.substr(0, 3) == "00:") ? position.substr(3) : position;
}
return std::string();
return {};
}
auto Mpris::getDynamicStr(const PlayerInfo& info, bool truncated, bool html) -> std::string {
@@ -288,9 +256,9 @@ auto Mpris::getDynamicStr(const PlayerInfo& info, bool truncated, bool html) ->
// keep position format same as length format
auto position = getPositionStr(info, truncated && truncate_hours_ && length.length() < 6);
size_t artistLen = utf8_width(artist);
size_t albumLen = utf8_width(album);
size_t titleLen = utf8_width(title);
size_t artistLen = waybar::util::utf8_width(artist);
size_t albumLen = waybar::util::utf8_width(album);
size_t titleLen = waybar::util::utf8_width(title);
size_t lengthLen = length.length();
size_t posLen = position.length();
@@ -309,7 +277,7 @@ auto Mpris::getDynamicStr(const PlayerInfo& info, bool truncated, bool html) ->
// Since the first element doesn't present a separator and we don't know a priori which one
// it will be, we add a "virtual separatorLen" to the dynamicLen, since we are adding the
// separatorLen to all the other lengths.
size_t separatorLen = utf8_width(dynamic_separator_);
size_t separatorLen = waybar::util::utf8_width(dynamic_separator_);
size_t dynamicLen = dynamic_len_ + separatorLen;
if (showArtist) artistLen += separatorLen;
if (showAlbum) albumLen += separatorLen;
@@ -319,33 +287,33 @@ auto Mpris::getDynamicStr(const PlayerInfo& info, bool truncated, bool html) ->
size_t totalLen = 0;
for (auto it = dynamic_prio_.begin(); it != dynamic_prio_.end(); ++it) {
if (*it == "artist") {
for (const auto& item : dynamic_prio_) {
if (item == "artist") {
if (totalLen + artistLen > dynamicLen) {
showArtist = false;
} else if (showArtist) {
totalLen += artistLen;
}
} else if (*it == "album") {
} else if (item == "album") {
if (totalLen + albumLen > dynamicLen) {
showAlbum = false;
} else if (showAlbum) {
totalLen += albumLen;
}
} else if (*it == "title") {
} else if (item == "title") {
if (totalLen + titleLen > dynamicLen) {
showTitle = false;
} else if (showTitle) {
totalLen += titleLen;
}
} else if (*it == "length") {
} else if (item == "length") {
if (totalLen + lengthLen > dynamicLen) {
showLength = false;
} else if (showLength) {
totalLen += lengthLen;
posLen = std::max((size_t)2, posLen) - 2;
}
} else if (*it == "position") {
} else if (item == "position") {
if (totalLen + posLen > dynamicLen) {
showPos = false;
} else if (showPos) {
@@ -406,7 +374,7 @@ auto Mpris::getDynamicStr(const PlayerInfo& info, bool truncated, bool html) ->
auto Mpris::onPlayerNameAppeared(PlayerctlPlayerManager* manager, PlayerctlPlayerName* player_name,
gpointer data) -> void {
Mpris* mpris = static_cast<Mpris*>(data);
auto* mpris = static_cast<Mpris*>(data);
if (!mpris) return;
spdlog::debug("mpris: name-appeared callback: {}", player_name->name);
@@ -415,30 +383,36 @@ auto Mpris::onPlayerNameAppeared(PlayerctlPlayerManager* manager, PlayerctlPlaye
return;
}
mpris->player = playerctl_player_new_from_name(player_name, NULL);
if (mpris->player != nullptr) {
g_signal_handlers_disconnect_by_data(mpris->player, mpris);
g_clear_object(&mpris->player);
}
mpris->player = playerctl_player_new_from_name(player_name, nullptr);
g_object_connect(mpris->player, "signal::play", G_CALLBACK(onPlayerPlay), mpris, "signal::pause",
G_CALLBACK(onPlayerPause), mpris, "signal::stop", G_CALLBACK(onPlayerStop),
mpris, "signal::stop", G_CALLBACK(onPlayerStop), mpris, "signal::metadata",
G_CALLBACK(onPlayerMetadata), mpris, NULL);
mpris, "signal::metadata", G_CALLBACK(onPlayerMetadata), mpris, NULL);
mpris->dp.emit();
}
auto Mpris::onPlayerNameVanished(PlayerctlPlayerManager* manager, PlayerctlPlayerName* player_name,
gpointer data) -> void {
Mpris* mpris = static_cast<Mpris*>(data);
auto* mpris = static_cast<Mpris*>(data);
if (!mpris) return;
spdlog::debug("mpris: player-vanished callback: {}", player_name->name);
spdlog::debug("mpris: name-vanished callback: {}", player_name->name);
if (std::string(player_name->name) == mpris->player_) {
if (mpris->player_ == "playerctld") {
mpris->dp.emit();
} else if (mpris->player_ == player_name->name) {
mpris->player = nullptr;
mpris->event_box_.set_visible(false);
mpris->dp.emit();
}
}
auto Mpris::onPlayerPlay(PlayerctlPlayer* player, gpointer data) -> void {
Mpris* mpris = static_cast<Mpris*>(data);
auto* mpris = static_cast<Mpris*>(data);
if (!mpris) return;
spdlog::debug("mpris: player-play callback");
@@ -447,7 +421,7 @@ auto Mpris::onPlayerPlay(PlayerctlPlayer* player, gpointer data) -> void {
}
auto Mpris::onPlayerPause(PlayerctlPlayer* player, gpointer data) -> void {
Mpris* mpris = static_cast<Mpris*>(data);
auto* mpris = static_cast<Mpris*>(data);
if (!mpris) return;
spdlog::debug("mpris: player-pause callback");
@@ -456,19 +430,16 @@ auto Mpris::onPlayerPause(PlayerctlPlayer* player, gpointer data) -> void {
}
auto Mpris::onPlayerStop(PlayerctlPlayer* player, gpointer data) -> void {
Mpris* mpris = static_cast<Mpris*>(data);
auto* mpris = static_cast<Mpris*>(data);
if (!mpris) return;
spdlog::debug("mpris: player-stop callback");
// hide widget
mpris->event_box_.set_visible(false);
// update widget
// update widget (update() handles visibility)
mpris->dp.emit();
}
auto Mpris::onPlayerMetadata(PlayerctlPlayer* player, GVariant* metadata, gpointer data) -> void {
Mpris* mpris = static_cast<Mpris*>(data);
auto* mpris = static_cast<Mpris*>(data);
if (!mpris) return;
spdlog::debug("mpris: player-metadata callback");
@@ -482,7 +453,7 @@ auto Mpris::getPlayerInfo() -> std::optional<PlayerInfo> {
}
GError* error = nullptr;
waybar::util::ScopeGuard error_deleter([error]() {
waybar::util::ScopeGuard error_deleter([&error]() {
if (error) {
g_error_free(error);
}
@@ -490,7 +461,12 @@ auto Mpris::getPlayerInfo() -> std::optional<PlayerInfo> {
char* player_status = nullptr;
auto player_playback_status = PLAYERCTL_PLAYBACK_STATUS_STOPPED;
g_object_get(player, "status", &player_status, "playback-status", &player_playback_status, NULL);
// Clean up previous fallback player
if (last_active_player_ && last_active_player_ != player) {
g_object_unref(last_active_player_);
last_active_player_ = nullptr;
}
std::string player_name = player_;
if (player_name == "playerctld") {
@@ -500,16 +476,52 @@ auto Mpris::getPlayerInfo() -> std::optional<PlayerInfo> {
}
// > get the list of players [..] in order of activity
// https://github.com/altdesktop/playerctl/blob/b19a71cb9dba635df68d271bd2b3f6a99336a223/playerctl/playerctl-common.c#L248-L249
players = g_list_first(players);
if (players) player_name = static_cast<PlayerctlPlayerName*>(players->data)->name;
}
if (std::any_of(ignored_players_.begin(), ignored_players_.end(),
[&](const std::string& pn) { return player_name == pn; })) {
PlayerctlPlayer* first_valid_player = nullptr;
std::string first_valid_name;
for (auto* p = g_list_first(players); p != nullptr; p = p->next) {
auto* pn = static_cast<PlayerctlPlayerName*>(p->data);
std::string name = pn->name;
if (std::any_of(ignored_players_.begin(), ignored_players_.end(),
[&](const std::string& ignored) { return name == ignored; })) {
spdlog::warn("mpris[{}]: ignoring player update", name);
continue;
}
auto* tmp = playerctl_player_new_from_name(pn, &error);
if (error || !tmp) continue;
if (!first_valid_player) {
first_valid_player = tmp;
first_valid_name = name;
}
PlayerctlPlaybackStatus status;
g_object_get(tmp, "playback-status", &status, NULL);
if (status == PLAYERCTL_PLAYBACK_STATUS_PLAYING) {
if (tmp != first_valid_player) g_object_unref(first_valid_player);
last_active_player_ = tmp;
player_name = name;
break;
}
if (tmp != first_valid_player) g_object_unref(tmp);
}
if (!last_active_player_) {
if (!first_valid_player) return std::nullopt;
last_active_player_ = first_valid_player;
player_name = first_valid_name;
}
} else if (std::any_of(ignored_players_.begin(), ignored_players_.end(),
[&](const std::string& pn) { return player_name == pn; })) {
spdlog::warn("mpris[{}]: ignoring player update", player_name);
return std::nullopt;
} else {
last_active_player_ = player;
}
g_object_get(last_active_player_, "status", &player_status, "playback-status",
&player_playback_status, NULL);
if (!player_status) {
spdlog::error("mpris: failed to get player status");
return std::nullopt;
}
// make status lowercase
player_status[0] = std::tolower(player_status[0]);
@@ -519,34 +531,43 @@ auto Mpris::getPlayerInfo() -> std::optional<PlayerInfo> {
.status_string = player_status,
.artist = std::nullopt,
.album = std::nullopt,
.album_artist = std::nullopt,
.title = std::nullopt,
.length = std::nullopt,
};
if (auto artist_ = playerctl_player_get_artist(player, &error)) {
if (auto* artist_ = playerctl_player_get_artist(last_active_player_, &error)) {
spdlog::debug("mpris[{}]: artist = {}", info.name, artist_);
info.artist = artist_;
g_free(artist_);
}
if (error) goto errorexit;
if (auto album_ = playerctl_player_get_album(player, &error)) {
if (auto* album_artist_ =
playerctl_player_print_metadata_prop(player, "xesam:albumArtist", &error)) {
spdlog::debug("mpris[{}]: albumArtist = {}", info.name, album_artist_);
info.album_artist = album_artist_;
g_free(album_artist_);
}
if (auto* album_ = playerctl_player_get_album(last_active_player_, &error)) {
spdlog::debug("mpris[{}]: album = {}", info.name, album_);
info.album = album_;
g_free(album_);
}
if (error) goto errorexit;
if (auto title_ = playerctl_player_get_title(player, &error)) {
if (auto* title_ = playerctl_player_get_title(last_active_player_, &error)) {
spdlog::debug("mpris[{}]: title = {}", info.name, title_);
info.title = title_;
g_free(title_);
}
if (error) goto errorexit;
if (auto length_ = playerctl_player_print_metadata_prop(player, "mpris:length", &error)) {
if (auto* length_ =
playerctl_player_print_metadata_prop(last_active_player_, "mpris:length", &error)) {
spdlog::debug("mpris[{}]: mpris:length = {}", info.name, length_);
std::chrono::microseconds len = std::chrono::microseconds(std::strtol(length_, nullptr, 10));
auto len = std::chrono::microseconds(std::strtol(length_, nullptr, 10));
auto len_h = std::chrono::duration_cast<std::chrono::hours>(len);
auto len_m = std::chrono::duration_cast<std::chrono::minutes>(len - len_h);
auto len_s = std::chrono::duration_cast<std::chrono::seconds>(len - len_h - len_m);
@@ -556,14 +577,14 @@ auto Mpris::getPlayerInfo() -> std::optional<PlayerInfo> {
if (error) goto errorexit;
{
auto position_ = playerctl_player_get_position(player, &error);
auto position_ = playerctl_player_get_position(last_active_player_, &error);
if (error) {
// it's fine to have an error here because not all players report a position
g_error_free(error);
error = nullptr;
} else {
spdlog::debug("mpris[{}]: position = {}", info.name, position_);
std::chrono::microseconds len = std::chrono::microseconds(position_);
auto len = std::chrono::microseconds(position_);
auto len_h = std::chrono::duration_cast<std::chrono::hours>(len);
auto len_m = std::chrono::duration_cast<std::chrono::minutes>(len - len_h);
auto len_s = std::chrono::duration_cast<std::chrono::seconds>(len - len_h - len_m);
@@ -587,38 +608,46 @@ errorexit:
}
bool Mpris::handleToggle(GdkEventButton* const& e) {
if (!e || e->type != GdkEventType::GDK_BUTTON_PRESS) {
return false;
}
auto info = getPlayerInfo();
if (!info) return false;
struct ButtonAction {
guint button;
const char* config_key;
std::function<void()> builtin_action;
};
GError* error = nullptr;
waybar::util::ScopeGuard error_deleter([error]() {
waybar::util::ScopeGuard error_deleter([&error]() {
if (error) {
g_error_free(error);
}
});
auto info = getPlayerInfo();
if (!info) return false;
// Command pattern: encapsulate each button's action
auto* target = last_active_player_ ? last_active_player_ : player;
const ButtonAction actions[] = {
{1, "on-click", [&]() { playerctl_player_play_pause(target, &error); }},
{2, "on-click-middle", [&]() { playerctl_player_previous(target, &error); }},
{3, "on-click-right", [&]() { playerctl_player_next(target, &error); }},
{8, "on-click-backward", [&]() { playerctl_player_previous(target, &error); }},
{9, "on-click-forward", [&]() { playerctl_player_next(target, &error); }},
};
if (e->type == GdkEventType::GDK_BUTTON_PRESS) {
switch (e->button) {
case 1: // left-click
if (config_["on-click"].isString()) {
return ALabel::handleToggle(e);
}
playerctl_player_play_pause(player, &error);
break;
case 2: // middle-click
if (config_["on-click-middle"].isString()) {
return ALabel::handleToggle(e);
}
playerctl_player_previous(player, &error);
break;
case 3: // right-click
if (config_["on-click-right"].isString()) {
return ALabel::handleToggle(e);
}
playerctl_player_next(player, &error);
break;
for (const auto& action : actions) {
if (e->button == action.button) {
if (config_[action.config_key].isString()) {
return ALabel::handleToggle(e);
}
action.builtin_action();
break;
}
}
if (error) {
spdlog::error("mpris[{}]: error running builtin on-click action: {}", (*info).name,
error->message);
@@ -706,7 +735,7 @@ auto Mpris::update() -> void {
if (label_format.empty()) {
label_.hide();
} else {
label_.set_markup(label_format);
setLabelMarkup(label_format);
label_.show();
}
} catch (fmt::format_error const& e) {
@@ -718,15 +747,18 @@ auto Mpris::update() -> void {
auto tooltip_text = fmt::format(
fmt::runtime(tooltipstr), fmt::arg("player", info.name),
fmt::arg("status", info.status_string),
fmt::arg("artist", getArtistStr(info, tooltip_len_limits_)),
fmt::arg("title", getTitleStr(info, tooltip_len_limits_)),
fmt::arg("album", getAlbumStr(info, tooltip_len_limits_)),
fmt::arg("artist",
std::string(Glib::Markup::escape_text(getArtistStr(info, tooltip_len_limits_)))),
fmt::arg("title",
std::string(Glib::Markup::escape_text(getTitleStr(info, tooltip_len_limits_)))),
fmt::arg("album",
std::string(Glib::Markup::escape_text(getAlbumStr(info, tooltip_len_limits_)))),
fmt::arg("length", tooltipLength), fmt::arg("position", tooltipPosition),
fmt::arg("dynamic", getDynamicStr(info, tooltip_len_limits_, false)),
fmt::arg("player_icon", getIconFromJson(config_["player-icons"], info.name)),
fmt::arg("status_icon", getIconFromJson(config_["status-icons"], info.status_string)));
label_.set_tooltip_text(tooltip_text);
setTooltipMarkup(tooltip_text);
} catch (fmt::format_error const& e) {
spdlog::warn("mpris: format error (tooltip): {}", e.what());
}
+327 -219
View File
@@ -1,13 +1,18 @@
#include "modules/network.hpp"
#include <linux/if.h>
#include <linux/if_link.h>
#include <netlink/netlink.h>
#include <spdlog/spdlog.h>
#include <sys/eventfd.h>
#include <cassert>
#include <cstring>
#include <fstream>
#include <optional>
#include <sstream>
#include <string>
#include <vector>
#include "util/format.hpp"
#ifdef WANT_RFKILL
@@ -16,10 +21,10 @@
namespace {
using namespace waybar::util;
constexpr const char *DEFAULT_FORMAT = "{ifname}";
constexpr const char* DEFAULT_FORMAT = "{ifname}";
} // namespace
constexpr const char *NETDEV_FILE =
constexpr const char* NETDEV_FILE =
"/proc/net/dev"; // std::ifstream does not take std::string_view as param
std::optional<std::pair<unsigned long long, unsigned long long>>
waybar::modules::Network::readBandwidthUsage() {
@@ -40,7 +45,8 @@ waybar::modules::Network::readBandwidthUsage() {
std::istringstream iss(line);
std::string ifacename;
iss >> ifacename; // ifacename contains "eth0:"
iss >> ifacename; // ifacename contains "eth0:"
if (ifacename.empty()) continue;
ifacename.pop_back(); // remove trailing ':'
if (ifacename != ifname_) {
continue;
@@ -77,31 +83,20 @@ waybar::modules::Network::readBandwidthUsage() {
return {{receivedBytes, transmittedBytes}};
}
waybar::modules::Network::Network(const std::string &id, const Json::Value &config)
: ALabel(config, "network", id, DEFAULT_FORMAT, 60),
ifid_(-1),
family_(config["family"] == "ipv6" ? AF_INET6 : AF_INET),
efd_(-1),
ev_fd_(-1),
want_route_dump_(false),
want_link_dump_(false),
want_addr_dump_(false),
dump_in_progress_(false),
is_p2p_(false),
cidr_(0),
signal_strength_dbm_(0),
signal_strength_(0),
#ifdef WANT_RFKILL
rfkill_{RFKILL_TYPE_WLAN},
#endif
frequency_(0.0) {
waybar::modules::Network::Network(const std::string& id, const Json::Value& config)
: ALabel(config, "network", id, DEFAULT_FORMAT, 60) {
// Start with some "text" in the module's label_. update() will then
// update it. Since the text should be different, update() will be able
// to show or hide the event_box_. This is to work around the case where
// the module start with no text, but the event_box_ is shown.
label_.set_markup("<s></s>");
if (config_["family"] == "ipv6") {
addr_pref_ = IPV6;
} else if (config["family"] == "ipv4_6") {
addr_pref_ = IPV4_6;
}
auto bandwidth = readBandwidthUsage();
if (bandwidth.has_value()) {
bandwidth_down_total_ = (*bandwidth).first;
@@ -110,6 +105,7 @@ waybar::modules::Network::Network(const std::string &id, const Json::Value &conf
bandwidth_down_total_ = 0;
bandwidth_up_total_ = 0;
}
bandwidth_last_sample_time_ = std::chrono::steady_clock::now();
if (!config_["interface"].isString()) {
// "interface" isn't configured, then try to guess the external
@@ -141,12 +137,7 @@ waybar::modules::Network::~Network() {
close(efd_);
}
if (ev_sock_ != nullptr) {
nl_socket_drop_membership(ev_sock_, RTNLGRP_LINK);
if (family_ == AF_INET) {
nl_socket_drop_membership(ev_sock_, RTNLGRP_IPV4_IFADDR);
} else {
nl_socket_drop_membership(ev_sock_, RTNLGRP_IPV6_IFADDR);
}
nl_socket_drop_memberships(ev_sock_, RTNLGRP_LINK, RTNLGRP_IPV4_IFADDR, RTNLGRP_IPV6_IFADDR);
nl_close(ev_sock_);
nl_socket_free(ev_sock_);
}
@@ -161,7 +152,7 @@ void waybar::modules::Network::createEventSocket() {
nl_socket_disable_seq_check(ev_sock_);
nl_socket_modify_cb(ev_sock_, NL_CB_VALID, NL_CB_CUSTOM, handleEvents, this);
nl_socket_modify_cb(ev_sock_, NL_CB_FINISH, NL_CB_CUSTOM, handleEventsDone, this);
auto groups = RTMGRP_LINK | (family_ == AF_INET ? RTMGRP_IPV4_IFADDR : RTMGRP_IPV6_IFADDR);
auto groups = RTMGRP_LINK | RTMGRP_IPV4_IFADDR | RTMGRP_IPV6_IFADDR;
nl_join_groups(ev_sock_, groups); // Deprecated
if (nl_connect(ev_sock_, NETLINK_ROUTE) != 0) {
throw std::runtime_error("Can't connect network socket");
@@ -169,18 +160,9 @@ void waybar::modules::Network::createEventSocket() {
if (nl_socket_set_nonblocking(ev_sock_)) {
throw std::runtime_error("Can't set non-blocking on network socket");
}
nl_socket_add_membership(ev_sock_, RTNLGRP_LINK);
if (family_ == AF_INET) {
nl_socket_add_membership(ev_sock_, RTNLGRP_IPV4_IFADDR);
} else {
nl_socket_add_membership(ev_sock_, RTNLGRP_IPV6_IFADDR);
}
nl_socket_add_memberships(ev_sock_, RTNLGRP_LINK, RTNLGRP_IPV4_IFADDR, RTNLGRP_IPV6_IFADDR, 0);
if (!config_["interface"].isString()) {
if (family_ == AF_INET) {
nl_socket_add_membership(ev_sock_, RTNLGRP_IPV4_ROUTE);
} else {
nl_socket_add_membership(ev_sock_, RTNLGRP_IPV6_ROUTE);
}
nl_socket_add_memberships(ev_sock_, RTNLGRP_IPV4_ROUTE, RTNLGRP_IPV6_ROUTE, 0);
}
efd_ = epoll_create1(EPOLL_CLOEXEC);
@@ -230,13 +212,13 @@ void waybar::modules::Network::worker() {
std::lock_guard<std::mutex> lock(mutex_);
if (ifid_ > 0) {
getInfo();
dp.emit();
}
dp.emit();
}
thread_timer_.sleep_for(interval_);
};
#ifdef WANT_RFKILL
rfkill_.on_update.connect([this](auto &) {
rfkill_.on_update.connect([this](auto&) {
/* If we are here, it's likely that the network thread already holds the mutex and will be
* holding it for a next few seconds.
* Let's delegate the update to the timer thread instead of blocking the main thread.
@@ -278,14 +260,17 @@ void waybar::modules::Network::worker() {
}
const std::string waybar::modules::Network::getNetworkState() const {
if (ifid_ == -1) {
if (ifid_ == -1 || !carrier_) {
#ifdef WANT_RFKILL
if (rfkill_.getState()) return "disabled";
bool display_rfkill = true;
if (config_["rfkill"].isBool()) {
display_rfkill = config_["rfkill"].asBool();
}
if (rfkill_.getState() && display_rfkill) return "disabled";
#endif
return "disconnected";
}
if (!carrier_) return "disconnected";
if (ipaddr_.empty()) return "linked";
if (ipaddr_.empty() && ipaddr6_.empty()) return "linked";
if (essid_.empty()) return "ethernet";
return "wifi";
}
@@ -293,34 +278,63 @@ const std::string waybar::modules::Network::getNetworkState() const {
auto waybar::modules::Network::update() -> void {
std::lock_guard<std::mutex> lock(mutex_);
std::string tooltip_format;
auto now = std::chrono::steady_clock::now();
auto elapsed_seconds = std::chrono::duration<double>(now - bandwidth_last_sample_time_).count();
auto bandwidth = readBandwidthUsage();
auto bandwidth_down = 0ull;
auto bandwidth_up = 0ull;
if (bandwidth.has_value()) {
auto down_octets = (*bandwidth).first;
auto up_octets = (*bandwidth).second;
bandwidth_down = down_octets - bandwidth_down_total_;
bandwidth_down_total_ = down_octets;
// Only recalculate bandwidth when enough time has elapsed since the last
// sample. Event-driven dp.emit() calls (link/addr/route changes) can
// trigger update() between timer intervals, which would consume the byte
// delta prematurely and show near-zero bandwidth.
auto min_elapsed = std::chrono::duration<double>(interval_).count() * 0.5;
if (elapsed_seconds >= min_elapsed) {
if (elapsed_seconds <= 0.0) {
elapsed_seconds = std::chrono::duration<double>(interval_).count();
}
bandwidth_last_sample_time_ = now;
bandwidth_up = up_octets - bandwidth_up_total_;
bandwidth_up_total_ = up_octets;
auto bandwidth = readBandwidthUsage();
if (bandwidth.has_value()) {
auto down_octets = (*bandwidth).first;
auto up_octets = (*bandwidth).second;
bandwidth_down = down_octets - bandwidth_down_total_;
bandwidth_down_total_ = down_octets;
bandwidth_up = up_octets - bandwidth_up_total_;
bandwidth_up_total_ = up_octets;
bandwidth_down_prev_ = bandwidth_down;
bandwidth_up_prev_ = bandwidth_up;
}
} else {
bandwidth_down = bandwidth_down_prev_;
bandwidth_up = bandwidth_up_prev_;
elapsed_seconds = std::chrono::duration<double>(interval_).count();
}
auto threshold_state = getState(signal_strength_);
if (!alt_) {
auto state = getNetworkState();
if (!state_.empty() && label_.get_style_context()->has_class(state_)) {
label_.get_style_context()->remove_class(state_);
}
if (config_["format-" + state].isString()) {
if (!threshold_state.empty() && config_["format-" + state + "-" + threshold_state].isString()) {
default_format_ = config_["format-" + state + "-" + threshold_state].asString();
} else if (config_["format-" + state].isString()) {
default_format_ = config_["format-" + state].asString();
} else if (config_["format"].isString()) {
default_format_ = config_["format"].asString();
} else {
default_format_ = DEFAULT_FORMAT;
}
if (config_["tooltip-format-" + state].isString()) {
if (!threshold_state.empty() &&
config_["tooltip-format-" + state + "-" + threshold_state].isString()) {
tooltip_format = config_["tooltip-format-" + state + "-" + threshold_state].asString();
} else if (config_["tooltip-format-" + state].isString()) {
tooltip_format = config_["tooltip-format-" + state].asString();
}
if (!label_.get_style_context()->has_class(state)) {
@@ -329,29 +343,58 @@ auto waybar::modules::Network::update() -> void {
format_ = default_format_;
state_ = state;
}
getState(signal_strength_);
auto text = fmt::format(
fmt::runtime(format_), fmt::arg("essid", essid_), fmt::arg("signaldBm", signal_strength_dbm_),
fmt::arg("signalStrength", signal_strength_),
fmt::arg("signalStrengthApp", signal_strength_app_), fmt::arg("ifname", ifname_),
fmt::arg("netmask", netmask_), fmt::arg("ipaddr", ipaddr_), fmt::arg("gwaddr", gwaddr_),
fmt::arg("cidr", cidr_), fmt::arg("frequency", fmt::format("{:.1f}", frequency_)),
fmt::arg("icon", getIcon(signal_strength_, state_)),
fmt::arg("bandwidthDownBits", pow_format(bandwidth_down * 8ull / interval_.count(), "b/s")),
fmt::arg("bandwidthUpBits", pow_format(bandwidth_up * 8ull / interval_.count(), "b/s")),
std::string final_ipaddr_;
if (addr_pref_ == ip_addr_pref::IPV4) {
final_ipaddr_ = ipaddr_;
} else if (addr_pref_ == ip_addr_pref::IPV6) {
final_ipaddr_ = ipaddr6_;
} else if (addr_pref_ == ip_addr_pref::IPV4_6) {
final_ipaddr_.reserve(ipaddr_.length() + ipaddr6_.length() + 1);
final_ipaddr_ = ipaddr_;
final_ipaddr_ += '\n';
final_ipaddr_ += ipaddr6_;
}
fmt::dynamic_format_arg_store<fmt::format_context> store;
store.push_back(fmt::arg("essid", essid_));
store.push_back(fmt::arg("bssid", bssid_));
store.push_back(fmt::arg("signaldBm", signal_strength_dbm_));
store.push_back(fmt::arg("signalStrength", signal_strength_));
store.push_back(fmt::arg("signalStrengthApp", signal_strength_app_));
store.push_back(fmt::arg("ifname", ifname_));
store.push_back(fmt::arg("netmask", netmask_));
store.push_back(fmt::arg("netmask6", netmask6_));
store.push_back(fmt::arg("ipaddr", final_ipaddr_));
store.push_back(fmt::arg("gwaddr", gwaddr_));
store.push_back(fmt::arg("cidr", cidr_));
store.push_back(fmt::arg("cidr6", cidr6_));
store.push_back(fmt::arg("frequency", fmt::format("{:.1f}", frequency_)));
store.push_back(fmt::arg("icon", getIcon(signal_strength_, state_)));
store.push_back(
fmt::arg("bandwidthDownBits", pow_format(bandwidth_down * 8ull / elapsed_seconds, "b/s")));
store.push_back(
fmt::arg("bandwidthUpBits", pow_format(bandwidth_up * 8ull / elapsed_seconds, "b/s")));
store.push_back(
fmt::arg("bandwidthTotalBits",
pow_format((bandwidth_up + bandwidth_down) * 8ull / interval_.count(), "b/s")),
fmt::arg("bandwidthDownOctets", pow_format(bandwidth_down / interval_.count(), "o/s")),
fmt::arg("bandwidthUpOctets", pow_format(bandwidth_up / interval_.count(), "o/s")),
fmt::arg("bandwidthTotalOctets",
pow_format((bandwidth_up + bandwidth_down) / interval_.count(), "o/s")),
fmt::arg("bandwidthDownBytes", pow_format(bandwidth_down / interval_.count(), "B/s")),
fmt::arg("bandwidthUpBytes", pow_format(bandwidth_up / interval_.count(), "B/s")),
fmt::arg("bandwidthTotalBytes",
pow_format((bandwidth_up + bandwidth_down) / interval_.count(), "B/s")));
if (text.compare(label_.get_label()) != 0) {
label_.set_markup(text);
pow_format((bandwidth_up + bandwidth_down) * 8ull / elapsed_seconds, "b/s")));
store.push_back(
fmt::arg("bandwidthDownOctets", pow_format(bandwidth_down / elapsed_seconds, "o/s")));
store.push_back(fmt::arg("bandwidthUpOctets", pow_format(bandwidth_up / elapsed_seconds, "o/s")));
store.push_back(fmt::arg("bandwidthTotalOctets",
pow_format((bandwidth_up + bandwidth_down) / elapsed_seconds, "o/s")));
store.push_back(
fmt::arg("bandwidthDownBytes", pow_format(bandwidth_down / elapsed_seconds, "B/s")));
store.push_back(fmt::arg("bandwidthUpBytes", pow_format(bandwidth_up / elapsed_seconds, "B/s")));
store.push_back(fmt::arg("bandwidthDownBytesCompact",
pow_format(bandwidth_down / elapsed_seconds, "B", false, 2)));
store.push_back(fmt::arg("bandwidthUpBytesCompact",
pow_format(bandwidth_up / elapsed_seconds, "B", false, 2)));
store.push_back(fmt::arg("bandwidthTotalBytes",
pow_format((bandwidth_up + bandwidth_down) / elapsed_seconds, "B/s")));
auto text = fmt::vformat(format_, store);
if (setLabelMarkup(text)) {
if (text.empty()) {
event_box_.hide();
} else {
@@ -363,31 +406,9 @@ auto waybar::modules::Network::update() -> void {
tooltip_format = config_["tooltip-format"].asString();
}
if (!tooltip_format.empty()) {
auto tooltip_text = fmt::format(
fmt::runtime(tooltip_format), fmt::arg("essid", essid_),
fmt::arg("signaldBm", signal_strength_dbm_), fmt::arg("signalStrength", signal_strength_),
fmt::arg("signalStrengthApp", signal_strength_app_), fmt::arg("ifname", ifname_),
fmt::arg("netmask", netmask_), fmt::arg("ipaddr", ipaddr_), fmt::arg("gwaddr", gwaddr_),
fmt::arg("cidr", cidr_), fmt::arg("frequency", fmt::format("{:.1f}", frequency_)),
fmt::arg("icon", getIcon(signal_strength_, state_)),
fmt::arg("bandwidthDownBits",
pow_format(bandwidth_down * 8ull / interval_.count(), "b/s")),
fmt::arg("bandwidthUpBits", pow_format(bandwidth_up * 8ull / interval_.count(), "b/s")),
fmt::arg("bandwidthTotalBits",
pow_format((bandwidth_up + bandwidth_down) * 8ull / interval_.count(), "b/s")),
fmt::arg("bandwidthDownOctets", pow_format(bandwidth_down / interval_.count(), "o/s")),
fmt::arg("bandwidthUpOctets", pow_format(bandwidth_up / interval_.count(), "o/s")),
fmt::arg("bandwidthTotalOctets",
pow_format((bandwidth_up + bandwidth_down) / interval_.count(), "o/s")),
fmt::arg("bandwidthDownBytes", pow_format(bandwidth_down / interval_.count(), "B/s")),
fmt::arg("bandwidthUpBytes", pow_format(bandwidth_up / interval_.count(), "B/s")),
fmt::arg("bandwidthTotalBytes",
pow_format((bandwidth_up + bandwidth_down) / interval_.count(), "B/s")));
if (label_.get_tooltip_text() != tooltip_text) {
label_.set_tooltip_markup(tooltip_text);
}
} else if (label_.get_tooltip_text() != text) {
label_.set_tooltip_markup(text);
setTooltipMarkup(fmt::vformat(tooltip_format, store));
} else {
setTooltipMarkup(text);
}
}
@@ -395,11 +416,65 @@ auto waybar::modules::Network::update() -> void {
ALabel::update();
}
bool waybar::modules::Network::checkInterface(std::string name) {
if (config_["interface"].isString()) {
return config_["interface"].asString() == name ||
wildcardMatch(config_["interface"].asString(), name);
// https://gist.github.com/rressi/92af77630faf055934c723ce93ae2495
static bool wildcardMatch(const std::string& pattern, const std::string& text) {
auto P = int(pattern.size());
auto T = int(text.size());
auto p = 0, fallback_p = -1;
auto t = 0, fallback_t = -1;
while (t < T) {
// Wildcard match:
if (p < P && pattern[p] == '*') {
fallback_p = p++; // starting point after failures
fallback_t = t; // starting point after failures
}
// Simple match:
else if (p < P && (pattern[p] == '?' || pattern[p] == text[t])) {
p++;
t++;
}
// Failure, fall back just after last matched '*':
else if (fallback_p >= 0) {
p = fallback_p + 1; // position just after last matched '*"
t = ++fallback_t; // re-try to match text from here
}
// There were no '*' before, so we fail here:
else {
return false;
}
}
// Consume all '*' at the end of pattern:
while (p < P && pattern[p] == '*') p++;
return p == P;
}
bool waybar::modules::Network::matchInterface(const std::string& ifname,
const std::vector<std::string>& altnames,
std::string& matched) const {
if (!config_["interface"].isString()) {
return false;
}
auto config_ifname = config_["interface"].asString();
if (config_ifname == ifname || wildcardMatch(config_ifname, ifname)) {
matched = ifname;
return true;
}
for (const auto& altname : altnames) {
if (config_ifname == altname || wildcardMatch(config_ifname, altname)) {
matched = altname;
return true;
}
}
return false;
}
@@ -407,19 +482,24 @@ void waybar::modules::Network::clearIface() {
ifid_ = -1;
ifname_.clear();
essid_.clear();
bssid_.clear();
ipaddr_.clear();
ipaddr6_.clear();
gwaddr_.clear();
netmask_.clear();
netmask6_.clear();
carrier_ = false;
is_p2p_ = false;
cidr_ = 0;
cidr6_ = 0;
signal_strength_dbm_ = 0;
signal_strength_ = 0;
signal_strength_app_.clear();
frequency_ = 0.0;
}
int waybar::modules::Network::handleEvents(struct nl_msg *msg, void *data) {
auto net = static_cast<waybar::modules::Network *>(data);
int waybar::modules::Network::handleEvents(struct nl_msg* msg, void* data) {
auto net = static_cast<waybar::modules::Network*>(data);
std::lock_guard<std::mutex> lock(net->mutex_);
auto nh = nlmsg_hdr(msg);
bool is_del_event = false;
@@ -428,13 +508,17 @@ int waybar::modules::Network::handleEvents(struct nl_msg *msg, void *data) {
case RTM_DELLINK:
is_del_event = true;
case RTM_NEWLINK: {
struct ifinfomsg *ifi = static_cast<struct ifinfomsg *>(NLMSG_DATA(nh));
ssize_t attrlen = IFLA_PAYLOAD(nh);
struct rtattr *ifla = IFLA_RTA(ifi);
const char *ifname = NULL;
size_t ifname_len = 0;
struct ifinfomsg* ifi = static_cast<struct ifinfomsg*>(NLMSG_DATA(nh));
struct nlattr* attrs[IFLA_MAX + 1];
std::string ifname;
std::vector<std::string> altnames;
std::optional<bool> carrier;
if (nlmsg_parse(nh, sizeof(*ifi), attrs, IFLA_MAX, nullptr) < 0) {
spdlog::error("network: failed to parse netlink attributes");
return NL_SKIP;
}
if (net->ifid_ != -1 && ifi->ifi_index != net->ifid_) {
return NL_OK;
}
@@ -452,26 +536,33 @@ int waybar::modules::Network::handleEvents(struct nl_msg *msg, void *data) {
return NL_OK;
}
for (; RTA_OK(ifla, attrlen); ifla = RTA_NEXT(ifla, attrlen)) {
switch (ifla->rta_type) {
case IFLA_IFNAME:
ifname = static_cast<const char *>(RTA_DATA(ifla));
ifname_len = RTA_PAYLOAD(ifla) - 1; // minus \0
if (ifi->ifi_flags & IFF_POINTOPOINT && net->checkInterface(ifname))
net->is_p2p_ = true;
break;
case IFLA_CARRIER: {
carrier = *(char *)RTA_DATA(ifla) == 1;
break;
if (attrs[IFLA_IFNAME] != nullptr) {
const char* ifname_ptr = nla_get_string(attrs[IFLA_IFNAME]);
size_t ifname_len = nla_len(attrs[IFLA_IFNAME]) - 1; // minus \0
ifname = std::string(ifname_ptr, ifname_len);
}
if (attrs[IFLA_CARRIER] != nullptr) {
carrier = nla_get_u8(attrs[IFLA_CARRIER]) == 1;
}
if (attrs[IFLA_PROP_LIST] != nullptr) {
struct nlattr* prop;
int rem;
nla_for_each_nested(prop, attrs[IFLA_PROP_LIST], rem) {
if (nla_type(prop) == IFLA_ALT_IFNAME) {
const char* altname_ptr = nla_get_string(prop);
size_t altname_len = nla_len(prop) - 1; // minus \0
altnames.emplace_back(altname_ptr, altname_len);
}
}
}
if (!is_del_event && ifi->ifi_index == net->ifid_) {
// Update interface information
if (net->ifname_.empty() && ifname != NULL) {
std::string new_ifname(ifname, ifname_len);
net->ifname_ = new_ifname;
if (net->ifname_.empty() && !ifname.empty()) {
net->ifname_ = ifname;
}
if (carrier.has_value()) {
if (net->carrier_ != *carrier) {
@@ -481,6 +572,7 @@ int waybar::modules::Network::handleEvents(struct nl_msg *msg, void *data) {
} else {
// clear state related to WiFi connection
net->essid_.clear();
net->bssid_.clear();
net->signal_strength_dbm_ = 0;
net->signal_strength_ = 0;
net->signal_strength_app_.clear();
@@ -491,13 +583,25 @@ int waybar::modules::Network::handleEvents(struct nl_msg *msg, void *data) {
}
} else if (!is_del_event && net->ifid_ == -1) {
// Checking if it's an interface we care about.
std::string new_ifname(ifname, ifname_len);
if (net->checkInterface(new_ifname)) {
spdlog::debug("network: selecting new interface {}/{}", new_ifname, ifi->ifi_index);
std::string matched;
if (net->matchInterface(ifname, altnames, matched)) {
if (ifname == matched) {
spdlog::debug("network: selecting new interface {}/{}", ifname, ifi->ifi_index);
} else {
spdlog::debug("network: selecting new interface {}/{} (matched altname {})", ifname,
ifi->ifi_index, matched);
}
net->ifname_ = new_ifname;
net->ifname_ = ifname;
net->ifid_ = ifi->ifi_index;
if (ifi->ifi_flags & IFF_POINTOPOINT) net->is_p2p_ = true;
if ((ifi->ifi_flags & IFF_POINTOPOINT) != 0) {
net->is_p2p_ = true;
}
if ((ifi->ifi_flags & IFF_UP) == 0) {
// With some network drivers (e.g. mt7921e), the interface may
// report having a carrier even though interface is down.
carrier = false;
}
if (carrier.has_value()) {
net->carrier_ = carrier.value();
}
@@ -521,24 +625,19 @@ int waybar::modules::Network::handleEvents(struct nl_msg *msg, void *data) {
case RTM_DELADDR:
is_del_event = true;
case RTM_NEWADDR: {
struct ifaddrmsg *ifa = static_cast<struct ifaddrmsg *>(NLMSG_DATA(nh));
struct ifaddrmsg* ifa = static_cast<struct ifaddrmsg*>(NLMSG_DATA(nh));
ssize_t attrlen = IFA_PAYLOAD(nh);
struct rtattr *ifa_rta = IFA_RTA(ifa);
struct rtattr* ifa_rta = IFA_RTA(ifa);
if ((int)ifa->ifa_index != net->ifid_) {
return NL_OK;
}
if (ifa->ifa_family != net->family_) {
return NL_OK;
}
// We ignore address mark as scope for the link or host,
// which should leave scope global addresses.
if (ifa->ifa_scope >= RT_SCOPE_LINK) {
return NL_OK;
}
for (; RTA_OK(ifa_rta, attrlen); ifa_rta = RTA_NEXT(ifa_rta, attrlen)) {
switch (ifa_rta->rta_type) {
case IFA_ADDRESS:
@@ -546,8 +645,33 @@ int waybar::modules::Network::handleEvents(struct nl_msg *msg, void *data) {
case IFA_LOCAL:
char ipaddr[INET6_ADDRSTRLEN];
if (!is_del_event) {
net->ipaddr_ = inet_ntop(ifa->ifa_family, RTA_DATA(ifa_rta), ipaddr, sizeof(ipaddr));
net->cidr_ = ifa->ifa_prefixlen;
bool addr_changed = false;
std::string changed_ipaddr;
int changed_cidr = 0;
if ((net->addr_pref_ == ip_addr_pref::IPV4 ||
net->addr_pref_ == ip_addr_pref::IPV4_6) &&
net->cidr_ == 0 && ifa->ifa_family == AF_INET) {
if (inet_ntop(ifa->ifa_family, RTA_DATA(ifa_rta), ipaddr, sizeof(ipaddr)) !=
nullptr) {
net->ipaddr_ = ipaddr;
net->cidr_ = ifa->ifa_prefixlen;
addr_changed = true;
changed_ipaddr = net->ipaddr_;
changed_cidr = net->cidr_;
}
} else if ((net->addr_pref_ == ip_addr_pref::IPV6 ||
net->addr_pref_ == ip_addr_pref::IPV4_6) &&
net->cidr6_ == 0 && ifa->ifa_family == AF_INET6) {
if (inet_ntop(ifa->ifa_family, RTA_DATA(ifa_rta), ipaddr, sizeof(ipaddr)) !=
nullptr) {
net->ipaddr6_ = ipaddr;
net->cidr6_ = ifa->ifa_prefixlen;
addr_changed = true;
changed_ipaddr = net->ipaddr6_;
changed_cidr = net->cidr6_;
}
}
switch (ifa->ifa_family) {
case AF_INET: {
struct in_addr netmask;
@@ -555,21 +679,30 @@ int waybar::modules::Network::handleEvents(struct nl_msg *msg, void *data) {
net->netmask_ = inet_ntop(ifa->ifa_family, &netmask, ipaddr, sizeof(ipaddr));
}
case AF_INET6: {
struct in6_addr netmask;
struct in6_addr netmask6;
for (int i = 0; i < 16; i++) {
int v = (i + 1) * 8 - ifa->ifa_prefixlen;
if (v < 0) v = 0;
if (v > 8) v = 8;
netmask.s6_addr[i] = ~0 << v;
netmask6.s6_addr[i] = ~0 << v;
}
net->netmask_ = inet_ntop(ifa->ifa_family, &netmask, ipaddr, sizeof(ipaddr));
net->netmask6_ = inet_ntop(ifa->ifa_family, &netmask6, ipaddr, sizeof(ipaddr));
}
}
spdlog::debug("network: {}, new addr {}/{}", net->ifname_, net->ipaddr_, net->cidr_);
if (addr_changed) {
spdlog::debug("network: {}, new addr {}/{}", net->ifname_, changed_ipaddr,
changed_cidr);
}
} else {
net->ipaddr_.clear();
net->cidr_ = 0;
net->netmask_.clear();
if (ifa->ifa_family == AF_INET) {
net->ipaddr_.clear();
net->cidr_ = 0;
net->netmask_.clear();
} else if (ifa->ifa_family == AF_INET6) {
net->ipaddr6_.clear();
net->cidr6_ = 0;
net->netmask6_.clear();
}
spdlog::debug("network: {} addr deleted {}/{}", net->ifname_,
inet_ntop(ifa->ifa_family, RTA_DATA(ifa_rta), ipaddr, sizeof(ipaddr)),
ifa->ifa_prefixlen);
@@ -581,16 +714,17 @@ int waybar::modules::Network::handleEvents(struct nl_msg *msg, void *data) {
break;
}
char temp_gw_addr[INET6_ADDRSTRLEN];
case RTM_DELROUTE:
is_del_event = true;
case RTM_NEWROUTE: {
// Based on https://gist.github.com/Yawning/c70d804d4b8ae78cc698
// to find the interface used to reach the outside world
struct rtmsg *rtm = static_cast<struct rtmsg *>(NLMSG_DATA(nh));
struct rtmsg* rtm = static_cast<struct rtmsg*>(NLMSG_DATA(nh));
int family = rtm->rtm_family;
ssize_t attrlen = RTM_PAYLOAD(nh);
struct rtattr *attr = RTM_RTA(rtm);
struct rtattr* attr = RTM_RTA(rtm);
char gateway_addr[INET6_ADDRSTRLEN];
bool has_gateway = false;
bool has_destination = false;
int temp_idx = -1;
@@ -616,31 +750,35 @@ int waybar::modules::Network::handleEvents(struct nl_msg *msg, void *data) {
* If someone ever needs to figure out the gateway address as well,
* it's here as the attribute payload.
*/
inet_ntop(net->family_, RTA_DATA(attr), temp_gw_addr, sizeof(temp_gw_addr));
inet_ntop(family, RTA_DATA(attr), gateway_addr, sizeof(gateway_addr));
has_gateway = true;
break;
case RTA_DST: {
/* The destination address.
* Should be either missing, or maybe all 0s. Accept both.
*/
const uint32_t nr_zeroes = (net->family_ == AF_INET) ? 4 : 16;
unsigned char c = 0;
size_t dstlen = RTA_PAYLOAD(attr);
if (dstlen != nr_zeroes) {
break;
auto* dest = (const unsigned char*)RTA_DATA(attr);
size_t dest_size = RTA_PAYLOAD(attr);
for (size_t i = 0; i < dest_size; ++i) {
if (dest[i] != 0) {
has_destination = true;
break;
}
}
for (uint32_t i = 0; i < dstlen; i += 1) {
c |= *((unsigned char *)RTA_DATA(attr) + i);
if (rtm->rtm_dst_len != 0) {
// We have found a destination like 0.0.0.0/24, this is not a
// default gateway route.
has_destination = true;
}
has_destination = (c == 0);
break;
}
case RTA_OIF:
/* The output interface index. */
temp_idx = *static_cast<int *>(RTA_DATA(attr));
temp_idx = *static_cast<int*>(RTA_DATA(attr));
break;
case RTA_PRIORITY:
priority = *(uint32_t *)RTA_DATA(attr);
priority = *(uint32_t*)RTA_DATA(attr);
break;
default:
break;
@@ -661,8 +799,8 @@ int waybar::modules::Network::handleEvents(struct nl_msg *msg, void *data) {
net->clearIface();
net->ifid_ = temp_idx;
net->route_priority = priority;
net->gwaddr_ = temp_gw_addr;
spdlog::debug("network: new default route via {} on if{} metric {}", temp_gw_addr,
net->gwaddr_ = gateway_addr;
spdlog::debug("network: new default route via {} on if{} metric {}", gateway_addr,
temp_idx, priority);
/* Ask ifname associated with temp_idx as well as carrier status */
@@ -715,7 +853,6 @@ void waybar::modules::Network::askForStateDump(void) {
};
if (want_route_dump_) {
rt_hdr.rtgen_family = family_;
nl_send_simple(ev_sock_, RTM_GETROUTE, NLM_F_DUMP, &rt_hdr, sizeof(rt_hdr));
want_route_dump_ = false;
dump_in_progress_ = true;
@@ -726,25 +863,24 @@ void waybar::modules::Network::askForStateDump(void) {
dump_in_progress_ = true;
} else if (want_addr_dump_) {
rt_hdr.rtgen_family = family_;
nl_send_simple(ev_sock_, RTM_GETADDR, NLM_F_DUMP, &rt_hdr, sizeof(rt_hdr));
want_addr_dump_ = false;
dump_in_progress_ = true;
}
}
int waybar::modules::Network::handleEventsDone(struct nl_msg *msg, void *data) {
auto net = static_cast<waybar::modules::Network *>(data);
int waybar::modules::Network::handleEventsDone(struct nl_msg* msg, void* data) {
auto net = static_cast<waybar::modules::Network*>(data);
net->dump_in_progress_ = false;
net->askForStateDump();
return NL_OK;
}
int waybar::modules::Network::handleScan(struct nl_msg *msg, void *data) {
auto net = static_cast<waybar::modules::Network *>(data);
auto gnlh = static_cast<genlmsghdr *>(nlmsg_data(nlmsg_hdr(msg)));
struct nlattr *tb[NL80211_ATTR_MAX + 1];
struct nlattr *bss[NL80211_BSS_MAX + 1];
int waybar::modules::Network::handleScan(struct nl_msg* msg, void* data) {
auto net = static_cast<waybar::modules::Network*>(data);
auto gnlh = static_cast<genlmsghdr*>(nlmsg_data(nlmsg_hdr(msg)));
struct nlattr* tb[NL80211_ATTR_MAX + 1];
struct nlattr* bss[NL80211_BSS_MAX + 1];
struct nla_policy bss_policy[NL80211_BSS_MAX + 1]{};
bss_policy[NL80211_BSS_TSF].type = NLA_U64;
bss_policy[NL80211_BSS_FREQUENCY].type = NLA_U32;
@@ -772,12 +908,13 @@ int waybar::modules::Network::handleScan(struct nl_msg *msg, void *data) {
net->parseEssid(bss);
net->parseSignal(bss);
net->parseFreq(bss);
net->parseBssid(bss);
return NL_OK;
}
void waybar::modules::Network::parseEssid(struct nlattr **bss) {
void waybar::modules::Network::parseEssid(struct nlattr** bss) {
if (bss[NL80211_BSS_INFORMATION_ELEMENTS] != nullptr) {
auto ies = static_cast<char *>(nla_data(bss[NL80211_BSS_INFORMATION_ELEMENTS]));
auto ies = static_cast<char*>(nla_data(bss[NL80211_BSS_INFORMATION_ELEMENTS]));
auto ies_len = nla_len(bss[NL80211_BSS_INFORMATION_ELEMENTS]);
const auto hdr_len = 2;
while (ies_len > hdr_len && ies[0] != 0) {
@@ -794,7 +931,7 @@ void waybar::modules::Network::parseEssid(struct nlattr **bss) {
}
}
void waybar::modules::Network::parseSignal(struct nlattr **bss) {
void waybar::modules::Network::parseSignal(struct nlattr** bss) {
if (bss[NL80211_BSS_SIGNAL_MBM] != nullptr) {
// signalstrength in dBm from mBm
signal_strength_dbm_ = nla_get_s32(bss[NL80211_BSS_SIGNAL_MBM]) / 100;
@@ -830,14 +967,25 @@ void waybar::modules::Network::parseSignal(struct nlattr **bss) {
}
}
void waybar::modules::Network::parseFreq(struct nlattr **bss) {
void waybar::modules::Network::parseFreq(struct nlattr** bss) {
if (bss[NL80211_BSS_FREQUENCY] != nullptr) {
// in GHz
frequency_ = (double)nla_get_u32(bss[NL80211_BSS_FREQUENCY]) / 1000;
}
}
bool waybar::modules::Network::associatedOrJoined(struct nlattr **bss) {
void waybar::modules::Network::parseBssid(struct nlattr** bss) {
if (bss[NL80211_BSS_BSSID] != nullptr) {
auto bssid = static_cast<uint8_t*>(nla_data(bss[NL80211_BSS_BSSID]));
auto bssid_len = nla_len(bss[NL80211_BSS_BSSID]);
if (bssid_len == 6) {
bssid_ = fmt::format("{:x}:{:x}:{:x}:{:x}:{:x}:{:x}", bssid[0], bssid[1], bssid[2], bssid[3],
bssid[4], bssid[5]);
}
}
}
bool waybar::modules::Network::associatedOrJoined(struct nlattr** bss) {
if (bss[NL80211_BSS_STATUS] == nullptr) {
return false;
}
@@ -853,7 +1001,7 @@ bool waybar::modules::Network::associatedOrJoined(struct nlattr **bss) {
}
auto waybar::modules::Network::getInfo() -> void {
struct nl_msg *nl_msg = nlmsg_alloc();
struct nl_msg* nl_msg = nlmsg_alloc();
if (nl_msg == nullptr) {
return;
}
@@ -865,43 +1013,3 @@ auto waybar::modules::Network::getInfo() -> void {
}
nl_send_sync(sock_, nl_msg);
}
// https://gist.github.com/rressi/92af77630faf055934c723ce93ae2495
bool waybar::modules::Network::wildcardMatch(const std::string &pattern,
const std::string &text) const {
auto P = int(pattern.size());
auto T = int(text.size());
auto p = 0, fallback_p = -1;
auto t = 0, fallback_t = -1;
while (t < T) {
// Wildcard match:
if (p < P && pattern[p] == '*') {
fallback_p = p++; // starting point after failures
fallback_t = t; // starting point after failures
}
// Simple match:
else if (p < P && (pattern[p] == '?' || pattern[p] == text[t])) {
p++;
t++;
}
// Failure, fall back just after last matched '*':
else if (fallback_p >= 0) {
p = fallback_p + 1; // position just after last matched '*"
t = ++fallback_t; // re-try to match text from here
}
// There were no '*' before, so we fail here:
else {
return false;
}
}
// Consume all '*' at the end of pattern:
while (p < P && pattern[p] == '*') p++;
return p == P;
}
+277
View File
@@ -0,0 +1,277 @@
#include "modules/niri/backend.hpp"
#include <netdb.h>
#include <netinet/in.h>
#include <spdlog/spdlog.h>
#include <sys/socket.h>
#include <sys/stat.h>
#include <sys/types.h>
#include <sys/un.h>
#include <unistd.h>
#include <iostream>
#include <string>
#include <thread>
#include "giomm/datainputstream.h"
#include "giomm/dataoutputstream.h"
#include "giomm/unixinputstream.h"
#include "giomm/unixoutputstream.h"
#include "util/scoped_fd.hpp"
namespace waybar::modules::niri {
IPC::IPC() { startIPC(); }
int IPC::connectToSocket() {
const char* socket_path = getenv("NIRI_SOCKET");
if (socket_path == nullptr) {
throw std::runtime_error("Niri IPC: NIRI_SOCKET was not set! (Is Niri running?)");
}
struct sockaddr_un addr;
util::ScopedFd socketfd(socket(AF_UNIX, SOCK_STREAM, 0));
if (socketfd == -1) {
throw std::runtime_error("socketfd 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;
int l = sizeof(struct sockaddr_un);
if (connect(socketfd, (struct sockaddr*)&addr, l) == -1) {
throw std::runtime_error("unable to connect");
}
return socketfd.release();
}
void IPC::startIPC() {
// will start IPC and relay events to parseIPC
int socketfd = connectToSocket();
std::thread([this, socketfd]() {
spdlog::info("Niri IPC starting");
auto unix_istream = Gio::UnixInputStream::create(socketfd, true);
auto unix_ostream = Gio::UnixOutputStream::create(socketfd, false);
auto istream = Gio::DataInputStream::create(unix_istream);
auto ostream = Gio::DataOutputStream::create(unix_ostream);
if (!ostream->put_string("\"EventStream\"\n") || !ostream->flush()) {
spdlog::error("Niri IPC: failed to start event stream");
return;
}
std::string line;
if (!istream->read_line(line) || line != R"({"Ok":"Handled"})") {
spdlog::error("Niri IPC: failed to start event stream");
return;
}
while (istream->read_line(line)) {
spdlog::debug("Niri IPC: received {}", line);
try {
parseIPC(line);
} catch (std::exception& e) {
spdlog::warn("Failed to parse IPC message: {}, reason: {}", line, e.what());
} catch (...) {
throw;
}
std::this_thread::sleep_for(std::chrono::milliseconds(1));
}
}).detach();
}
void IPC::parseIPC(const std::string& line) {
const auto ev = parser_.parse(line);
const auto members = ev.getMemberNames();
if (members.size() != 1) throw std::runtime_error("Event must have a single member");
{
auto lock = lockData();
if (const auto& payload = ev["WorkspacesChanged"]) {
workspaces_.clear();
const auto& values = payload["workspaces"];
std::copy(values.begin(), values.end(), std::back_inserter(workspaces_));
std::sort(workspaces_.begin(), workspaces_.end(), [](const auto& a, const auto& b) {
const auto& aOutput = a["output"].asString();
const auto& bOutput = b["output"].asString();
const auto aIdx = a["idx"].asUInt();
const auto bIdx = b["idx"].asUInt();
if (aOutput == bOutput) return aIdx < bIdx;
return aOutput < bOutput;
});
} else if (const auto& payload = ev["WorkspaceActivated"]) {
const auto id = payload["id"].asUInt64();
const auto focused = payload["focused"].asBool();
auto it = std::find_if(workspaces_.begin(), workspaces_.end(),
[id](const auto& ws) { return ws["id"].asUInt64() == id; });
if (it != workspaces_.end()) {
const auto& ws = *it;
const auto& output = ws["output"].asString();
for (auto& ws : workspaces_) {
const auto got_activated = (ws["id"].asUInt64() == id);
if (ws["output"] == output) ws["is_active"] = got_activated;
if (focused) ws["is_focused"] = got_activated;
}
} else {
spdlog::error("Activated unknown workspace");
}
} else if (const auto& payload = ev["WorkspaceActiveWindowChanged"]) {
const auto workspaceId = payload["workspace_id"].asUInt64();
auto it = std::find_if(workspaces_.begin(), workspaces_.end(), [workspaceId](const auto& ws) {
return ws["id"].asUInt64() == workspaceId;
});
if (it != workspaces_.end()) {
auto& ws = *it;
ws["active_window_id"] = payload["active_window_id"];
} else {
spdlog::error("Active window changed on unknown workspace");
}
} else if (const auto& payload = ev["WorkspaceUrgencyChanged"]) {
const auto id = payload["id"].asUInt64();
const auto urgent = payload["urgent"].asBool();
auto it = std::find_if(workspaces_.begin(), workspaces_.end(),
[id](const auto& ws) { return ws["id"].asUInt64() == id; });
if (it != workspaces_.end()) {
auto& ws = *it;
ws["is_urgent"] = urgent;
} else {
spdlog::error("Urgency changed for unknown workspace");
}
} else if (const auto& payload = ev["KeyboardLayoutsChanged"]) {
const auto& layouts = payload["keyboard_layouts"];
const auto& names = layouts["names"];
keyboardLayoutCurrent_ = layouts["current_idx"].asUInt();
keyboardLayoutNames_.clear();
for (const auto& fullName : names) keyboardLayoutNames_.push_back(fullName.asString());
} else if (const auto& payload = ev["KeyboardLayoutSwitched"]) {
keyboardLayoutCurrent_ = payload["idx"].asUInt();
} else if (const auto& payload = ev["WindowsChanged"]) {
windows_.clear();
const auto& values = payload["windows"];
std::copy(values.begin(), values.end(), std::back_inserter(windows_));
} else if (const auto& payload = ev["WindowOpenedOrChanged"]) {
const auto& window = payload["window"];
const auto id = window["id"].asUInt64();
auto it = std::find_if(windows_.begin(), windows_.end(),
[id](const auto& win) { return win["id"].asUInt64() == id; });
if (it == windows_.end()) {
windows_.push_back(window);
if (window["is_focused"].asBool()) {
for (auto& win : windows_) {
win["is_focused"] = win["id"].asUInt64() == id;
}
}
} else {
*it = window;
}
} else if (const auto& payload = ev["WindowClosed"]) {
const auto id = payload["id"].asUInt64();
auto it = std::find_if(windows_.begin(), windows_.end(),
[id](const auto& win) { return win["id"].asUInt64() == id; });
if (it != windows_.end()) {
windows_.erase(it);
} else {
spdlog::error("Unknown window closed");
}
} else if (const auto& payload = ev["WindowFocusChanged"]) {
const auto focused = !payload["id"].isNull();
const auto id = payload["id"].asUInt64();
for (auto& win : windows_) {
win["is_focused"] = focused && win["id"].asUInt64() == id;
}
} else if (const auto &payload = ev["WindowLayoutsChanged"]) {
const auto &values = payload["changes"];
for (const auto &changed : values) {
const auto id = changed[0].asUInt64();
const auto &change = changed[1];
for (auto &win : windows_) {
if (win["id"].asUInt64() == id) {
win["layout"] = change;
break;
}
}
}
}
}
std::unique_lock lock(callbackMutex_);
for (auto& [eventname, handler] : callbacks_) {
if (eventname == members[0]) {
handler->onEvent(ev);
}
}
}
void IPC::registerForIPC(const std::string& ev, EventHandler* ev_handler) {
if (ev_handler == nullptr) {
return;
}
std::unique_lock lock(callbackMutex_);
callbacks_.emplace_back(ev, ev_handler);
}
void IPC::unregisterForIPC(EventHandler* ev_handler) {
if (ev_handler == nullptr) {
return;
}
std::unique_lock lock(callbackMutex_);
for (auto it = callbacks_.begin(); it != callbacks_.end();) {
auto& [eventname, handler] = *it;
if (handler == ev_handler) {
it = callbacks_.erase(it);
} else {
++it;
}
}
}
Json::Value IPC::send(const Json::Value& request) {
util::ScopedFd socketfd(connectToSocket());
auto unix_istream = Gio::UnixInputStream::create(socketfd, true);
auto unix_ostream = Gio::UnixOutputStream::create(socketfd, false);
auto istream = Gio::DataInputStream::create(unix_istream);
auto ostream = Gio::DataOutputStream::create(unix_ostream);
// Niri needs the request on a single line.
Json::StreamWriterBuilder builder;
builder["indentation"] = "";
std::unique_ptr<Json::StreamWriter> writer(builder.newStreamWriter());
std::ostringstream oss;
writer->write(request, &oss);
oss << '\n';
if (!ostream->put_string(oss.str()) || !ostream->flush())
throw std::runtime_error("error writing to niri socket");
std::string line;
if (!istream->read_line(line)) throw std::runtime_error("error reading from niri socket");
std::istringstream iss(std::move(line));
Json::Value response;
iss >> response;
return response;
}
} // namespace waybar::modules::niri
+147
View File
@@ -0,0 +1,147 @@
#include "modules/niri/language.hpp"
#include <spdlog/spdlog.h>
#include <xkbcommon/xkbcommon.h>
#include <xkbcommon/xkbregistry.h>
#include "util/string.hpp"
namespace waybar::modules::niri {
Language::Language(const std::string& id, const Bar& bar, const Json::Value& config)
: ALabel(config, "language", id, "{}", 0, false), bar_(bar) {
label_.hide();
if (!gIPC) gIPC = std::make_unique<IPC>();
gIPC->registerForIPC("KeyboardLayoutsChanged", this);
gIPC->registerForIPC("KeyboardLayoutSwitched", this);
updateFromIPC();
dp.emit();
}
Language::~Language() {
gIPC->unregisterForIPC(this);
// wait for possible event handler to finish
std::lock_guard<std::mutex> lock(mutex_);
}
void Language::updateFromIPC() {
std::lock_guard<std::mutex> lock(mutex_);
auto ipcLock = gIPC->lockData();
layouts_.clear();
layouts_.reserve(gIPC->keyboardLayoutNames().size());
for (const auto& fullName : gIPC->keyboardLayoutNames()) layouts_.push_back(getLayout(fullName));
current_idx_ = gIPC->keyboardLayoutCurrent();
}
/**
* Language::doUpdate - update workspaces in UI thread.
*
* Note: some member fields are modified by both UI thread and event listener thread, use mutex_ to
* protect these member fields, and lock should released before calling ALabel::update().
*/
void Language::doUpdate() {
std::lock_guard<std::mutex> lock(mutex_);
if (layouts_.size() <= current_idx_) {
spdlog::error("niri language layout index out of bounds");
label_.hide();
return;
}
const auto& layout = layouts_[current_idx_];
spdlog::debug("niri language update with full name {}", layout.full_name);
spdlog::debug("niri language update with short name {}", layout.short_name);
spdlog::debug("niri language update with short description {}", layout.short_description);
spdlog::debug("niri language update with variant {}", layout.variant);
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;
} else {
last_short_name_.clear();
}
std::string layoutName = std::string{};
if (config_.isMember("format-" + layout.short_description + "-" + layout.variant)) {
const auto propName = "format-" + layout.short_description + "-" + layout.variant;
layoutName = fmt::format(fmt::runtime(format_), config_[propName].asString());
} else if (config_.isMember("format-" + layout.short_description)) {
const auto propName = "format-" + layout.short_description;
layoutName = fmt::format(fmt::runtime(format_), config_[propName].asString());
} else {
layoutName = trim(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)));
}
spdlog::debug("niri language formatted layout name {}", layoutName);
if (!layoutName.empty()) {
label_.show();
label_.set_markup(layoutName);
} else {
label_.hide();
}
}
void Language::update() {
doUpdate();
ALabel::update();
}
void Language::onEvent(const Json::Value& ev) {
if (ev["KeyboardLayoutsChanged"]) {
updateFromIPC();
} else if (ev["KeyboardLayoutSwitched"]) {
std::lock_guard<std::mutex> lock(mutex_);
auto ipcLock = gIPC->lockData();
current_idx_ = gIPC->keyboardLayoutCurrent();
}
dp.emit();
}
Language::Layout Language::getLayout(const std::string& fullName) {
auto* const context = rxkb_context_new(RXKB_CONTEXT_LOAD_EXOTIC_RULES);
rxkb_context_parse_default_ruleset(context);
rxkb_layout* layout = rxkb_layout_first(context);
while (layout != nullptr) {
std::string nameOfLayout = rxkb_layout_get_description(layout);
if (nameOfLayout != fullName) {
layout = rxkb_layout_next(layout);
continue;
}
auto name = std::string(rxkb_layout_get_name(layout));
const auto* variantPtr = rxkb_layout_get_variant(layout);
std::string variant = variantPtr == nullptr ? "" : std::string(variantPtr);
const auto* descriptionPtr = rxkb_layout_get_brief(layout);
std::string description = descriptionPtr == nullptr ? "" : std::string(descriptionPtr);
Layout info = Layout{nameOfLayout, name, variant, description};
rxkb_context_unref(context);
return info;
}
rxkb_context_unref(context);
spdlog::debug("niri language didn't find matching layout for {}", fullName);
return Layout{"", "", "", ""};
}
} // namespace waybar::modules::niri
+125
View File
@@ -0,0 +1,125 @@
#include "modules/niri/window.hpp"
#include <gtkmm/button.h>
#include <gtkmm/label.h>
#include <spdlog/spdlog.h>
#include "util/rewrite_string.hpp"
#include "util/sanitize_str.hpp"
namespace waybar::modules::niri {
Window::Window(const std::string& id, const Bar& bar, const Json::Value& config)
: AAppIconLabel(config, "window", id, "{title}", 0, true), bar_(bar) {
if (!gIPC) gIPC = std::make_unique<IPC>();
gIPC->registerForIPC("WindowsChanged", this);
gIPC->registerForIPC("WindowOpenedOrChanged", this);
gIPC->registerForIPC("WindowClosed", this);
gIPC->registerForIPC("WindowFocusChanged", this);
gIPC->registerForIPC("WindowLayoutsChanged", this);
dp.emit();
}
Window::~Window() { gIPC->unregisterForIPC(this); }
void Window::onEvent(const Json::Value& ev) { dp.emit(); }
void Window::doUpdate() {
auto ipcLock = gIPC->lockData();
const auto& windows = gIPC->windows();
const auto& workspaces = gIPC->workspaces();
const auto separateOutputs = config_["separate-outputs"].asBool();
const auto ws_it = std::find_if(workspaces.cbegin(), workspaces.cend(), [&](const auto& ws) {
if (separateOutputs) {
return ws["is_active"].asBool() && ws["output"].asString() == bar_.output->name;
}
return ws["is_focused"].asBool();
});
std::vector<Json::Value>::const_iterator it;
if (ws_it == workspaces.cend() || (*ws_it)["active_window_id"].isNull()) {
it = windows.cend();
} else {
const auto id = (*ws_it)["active_window_id"].asUInt64();
it = std::find_if(windows.cbegin(), windows.cend(),
[id](const auto& win) { return win["id"].asUInt64() == id; });
}
setClass("empty", ws_it == workspaces.cend() || (*ws_it)["active_window_id"].isNull());
if (it != windows.cend()) {
const auto& window = *it;
auto max_col = -1;
for (const auto &win : windows) {
if (win["workspace_id"].asUInt64() != window["workspace_id"].asUInt64()) {
continue;
}
const auto col = win["layout"]["pos_in_scrolling_layout"][0].asInt64();
if (col > max_col) max_col = col;
}
const auto title = window["title"].asString();
const auto appId = window["app_id"].asString();
const auto col = window["layout"]["pos_in_scrolling_layout"][0].asInt64();
const auto sanitizedTitle = waybar::util::sanitize_string(title);
const auto sanitizedAppId = waybar::util::sanitize_string(appId);
label_.show();
label_.set_markup(waybar::util::rewriteString(
fmt::format(fmt::runtime(format_), fmt::arg("title", sanitizedTitle),
fmt::arg("app_id", sanitizedAppId), fmt::arg("col", col),
fmt::arg("max_col", max_col)),
config_["rewrite"]));
updateAppIconName(appId, "");
if (tooltipEnabled()) label_.set_tooltip_markup(title);
const auto id = window["id"].asUInt64();
const auto workspaceId = window["workspace_id"].asUInt64();
const auto isSolo = std::none_of(windows.cbegin(), windows.cend(), [&](const auto& win) {
return win["id"].asUInt64() != id && win["workspace_id"].asUInt64() == workspaceId;
});
setClass("solo", isSolo);
if (!appId.empty()) setClass(appId, isSolo);
if (oldAppId_ != appId) {
if (!oldAppId_.empty()) setClass(oldAppId_, false);
oldAppId_ = appId;
}
} else {
label_.show();
label_.set_markup(waybar::util::rewriteString(
fmt::format(fmt::runtime(format_), fmt::arg("title", ""),
fmt::arg("app_id", "")),
config_["rewrite"]));
updateAppIconName("", "");
setClass("solo", false);
if (!oldAppId_.empty()) setClass(oldAppId_, false);
oldAppId_.clear();
}
}
void Window::update() {
doUpdate();
AAppIconLabel::update();
}
void Window::setClass(const std::string& className, bool enable) {
auto styleContext = bar_.window.get_style_context();
if (enable) {
if (!styleContext->has_class(className)) {
styleContext->add_class(className);
}
} else {
styleContext->remove_class(className);
}
}
} // namespace waybar::modules::niri
+328
View File
@@ -0,0 +1,328 @@
#include "modules/niri/workspaces.hpp"
#include <gtkmm/button.h>
#include <gtkmm/label.h>
#include <spdlog/spdlog.h>
#include <algorithm>
#include <cctype>
namespace waybar::modules::niri {
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) {
const auto config_sort_by_number = config_["sort-by-number"];
if (config_sort_by_number.isBool()) {
spdlog::warn("[niri/workspaces]: Prefer sort-by-id instead of sort-by-number");
sort_by_id_ = config_sort_by_number.asBool();
}
const auto config_sort_by_id = config_["sort-by-id"];
if (config_sort_by_id.isBool()) {
sort_by_id_ = config_sort_by_id.asBool();
}
const auto config_sort_by_name = config_["sort-by-name"];
if (config_sort_by_name.isBool()) {
sort_by_name_ = config_sort_by_name.asBool();
}
const auto config_sort_by_coordinates = config_["sort-by-coordinates"];
if (config_sort_by_coordinates.isBool()) {
sort_by_coordinates_ = config_sort_by_coordinates.asBool();
}
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 (!gIPC) gIPC = std::make_unique<IPC>();
gIPC->registerForIPC("WorkspacesChanged", this);
gIPC->registerForIPC("WorkspaceActivated", this);
gIPC->registerForIPC("WorkspaceActiveWindowChanged", this);
gIPC->registerForIPC("WorkspaceUrgencyChanged", this);
if (config["enable-bar-scroll"].asBool()) {
auto& window = const_cast<Bar&>(bar_).window;
window.add_events(Gdk::SCROLL_MASK | Gdk::SMOOTH_SCROLL_MASK);
window.signal_scroll_event().connect(sigc::mem_fun(*this, &Workspaces::handleScroll));
}
dp.emit();
}
Workspaces::~Workspaces() { gIPC->unregisterForIPC(this); }
void Workspaces::onEvent(const Json::Value& ev) { dp.emit(); }
void Workspaces::doUpdate() {
auto ipcLock = gIPC->lockData();
const auto alloutputs = config_["all-outputs"].asBool();
std::vector<Json::Value> my_workspaces;
const auto& workspaces = gIPC->workspaces();
std::copy_if(workspaces.cbegin(), workspaces.cend(), std::back_inserter(my_workspaces),
[&](const auto& ws) {
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) {
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");
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()));
}
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()) {
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();
}
}
// Refresh the button order.
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));
auto& button = buttons_[ws["id"].asUInt64()];
box_.reorder_child(button, pos);
}
}
void Workspaces::update() {
doUpdate();
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;
}
std::string Workspaces::getIcon(const std::string& value, const Json::Value& ws) {
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"]) {
const auto& name = ws["name"].asString();
if (icons[name]) return icons[name].asString();
}
const auto idx = ws["idx"].asString();
if (icons[idx]) return icons[idx].asString();
if (icons["default"]) return icons["default"].asString();
return value;
}
bool Workspaces::handleScroll(GdkEventScroll* e) {
if (gdk_event_get_pointer_emulated((GdkEvent*)e) != 0) {
/**
* Ignore emulated scroll events on window
*/
return false;
}
auto dir = AModule::getScrollDir(e);
if (dir == SCROLL_DIR::NONE) {
return true;
}
try {
Json::Value request(Json::objectValue);
auto& action = (request["Action"] = Json::Value(Json::objectValue));
std::string action_name;
if (dir == SCROLL_DIR::DOWN || dir == SCROLL_DIR::RIGHT) {
action_name = "FocusWorkspaceDown";
} else if (dir == SCROLL_DIR::UP || dir == SCROLL_DIR::LEFT) {
action_name = "FocusWorkspaceUp";
} else {
return true;
}
action[action_name] = Json::Value(Json::objectValue);
IPC::send(request);
} catch (const std::exception& e) {
spdlog::error("Workspaces: {}", e.what());
return false;
}
return true;
}
void Workspaces::sortWorkspaces(std::vector<Json::Value>& 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());
};
auto is_numeric = [](const std::string& value) {
return !value.empty() &&
std::all_of(value.begin(), value.end(), [](unsigned char c) { return std::isdigit(c); });
};
const bool names_are_numeric =
std::all_of(workspaces.begin(), workspaces.end(),
[&](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) {
if (sort_by_id_) {
return a["id"].asUInt64() < b["id"].asUInt64();
}
if (sort_by_name_) {
const auto a_name = get_name(a);
const auto b_name = get_name(b);
if (a_name == b_name) return a["id"].asUInt64() < b["id"].asUInt64();
if (names_are_numeric) return compare_numeric_strings(a_name, b_name);
return a_name < b_name;
}
if (sort_by_coordinates_) {
const auto& a_output = a["output"].asString();
const auto& b_output = b["output"].asString();
if (a_output == b_output) {
const auto a_idx = a["idx"].asUInt();
const auto b_idx = b["idx"].asUInt();
if (a_idx == b_idx) return a["id"].asUInt64() < b["id"].asUInt64();
return a_idx < b_idx;
}
return a_output < b_output;
}
// Default to sorting by workspace index on each output.
const auto& a_output = a["output"].asString();
const auto& b_output = b["output"].asString();
const auto a_idx = a["idx"].asUInt();
const auto b_idx = b["idx"].asUInt();
if (a_output == b_output) return a_idx < b_idx;
return a_output < b_output;
});
}
} // namespace waybar::modules::niri
+54 -14
View File
@@ -12,7 +12,8 @@ PowerProfilesDaemon::PowerProfilesDaemon(const std::string& id, const Json::Valu
if (config_["tooltip-format"].isString()) {
tooltipFormat_ = config_["tooltip-format"].asString();
} else {
tooltipFormat_ = "Power profile: {profile}\nDriver: {driver}";
tooltipFormat_ =
"Power profile: {profile}\nCPU driver: {cpu_driver}\nPlatform driver: {platform_driver}";
}
// Fasten your seatbelt, we're up for quite a ride. The rest of the
// init is performed asynchronously. There's 2 callbacks involved.
@@ -29,14 +30,14 @@ PowerProfilesDaemon::PowerProfilesDaemon(const std::string& id, const Json::Valu
// method on the proxy to see whether or not something's responding
// on the other side.
// NOTE: the DBus adresses are under migration. They should be
// NOTE: the DBus addresses are under migration. They should be
// changed to org.freedesktop.UPower.PowerProfiles at some point.
//
// See
// https://gitlab.freedesktop.org/upower/power-profiles-daemon/-/releases/0.20
//
// The old name is still announced for now. Let's rather use the old
// adresses for compatibility sake.
// addresses for compatibility sake.
//
// Revisit this in 2026, systems should be updated by then.
@@ -95,15 +96,51 @@ void PowerProfilesDaemon::populateInitState() {
powerProfilesProxy_->get_cached_property(profilesVariant, "Profiles");
for (auto& variantDict : profilesVariant.get()) {
Glib::ustring name;
// Legacy single `Driver` property, still exposed by older
// power-profiles-daemon versions.
Glib::ustring driver;
Glib::ustring cpuDriver;
Glib::ustring platformDriver;
if (auto p = variantDict.find("Profile"); p != variantDict.end()) {
name = p->second.get();
}
if (auto d = variantDict.find("Driver"); d != variantDict.end()) {
driver = d->second.get();
}
if (auto cd = variantDict.find("CpuDriver"); cd != variantDict.end()) {
cpuDriver = cd->second.get();
}
if (auto pd = variantDict.find("PlatformDriver"); pd != variantDict.end()) {
platformDriver = pd->second.get();
}
// Recent power-profiles-daemon versions split the single `Driver`
// property into `CpuDriver` and `PlatformDriver`. When talking to an
// older daemon that only exposes `Driver`, fall back to it so the new
// {cpu_driver}/{platform_driver} placeholders still resolve.
if (cpuDriver.empty()) {
cpuDriver = driver;
}
if (platformDriver.empty()) {
platformDriver = driver;
}
// Conversely, keep the legacy {driver} placeholder working against a
// recent daemon that no longer exposes `Driver` by deriving it from
// the CPU driver.
if (driver.empty()) {
driver = cpuDriver;
}
if (driver.empty()) {
driver = "Unavailable";
cpuDriver = "Unavailable";
platformDriver = "Unavailable";
spdlog::warn("Cannot find power profiles daemon driver.");
}
if (!name.empty()) {
availableProfiles_.emplace_back(std::move(name), std::move(driver));
availableProfiles_.emplace_back(std::move(name), std::move(driver), std::move(cpuDriver),
std::move(platformDriver));
} else {
spdlog::error(
"Power profiles daemon: power-profiles-daemon sent us an empty power profile name. "
@@ -150,15 +187,14 @@ void PowerProfilesDaemon::switchToProfile(std::string const& str) {
auto PowerProfilesDaemon::update() -> void {
if (connected_ && activeProfile_ != availableProfiles_.end()) {
auto profile = (*activeProfile_);
// Set label
fmt::dynamic_format_arg_store<fmt::format_context> store;
store.push_back(fmt::arg("profile", profile.name));
store.push_back(fmt::arg("driver", profile.driver));
store.push_back(fmt::arg("icon", getIcon(0, profile.name)));
label_.set_markup(fmt::vformat(format_, store));
if (tooltipEnabled()) {
label_.set_tooltip_text(fmt::vformat(tooltipFormat_, store));
}
// Set label and tooltip
updateLabelAndTooltip(format_, tooltipFormat_, fmt::arg("profile", profile.name),
// Legacy placeholder, kept for backward compatibility with existing
// configs.
fmt::arg("driver", profile.driver),
fmt::arg("cpu_driver", profile.cpuDriver),
fmt::arg("platform_driver", profile.platformDriver),
fmt::arg("icon", getIcon(0, profile.name)));
// Set CSS class
if (!currentStyle_.empty()) {
@@ -176,6 +212,10 @@ auto PowerProfilesDaemon::update() -> void {
bool PowerProfilesDaemon::handleToggle(GdkEventButton* const& e) {
if (e->type == GdkEventType::GDK_BUTTON_PRESS && connected_) {
if (availableProfiles_.empty()) return true;
if (activeProfile_ == availableProfiles_.end()) {
activeProfile_ = availableProfiles_.begin();
}
if (e->button == 1) /* left click */ {
activeProfile_++;
if (activeProfile_ == availableProfiles_.end()) {
@@ -204,7 +244,7 @@ void PowerProfilesDaemon::setPropCb(Glib::RefPtr<Gio::AsyncResult>& r) {
auto _ = powerProfilesProxy_->call_finish(r);
dp.emit();
} catch (const std::exception& e) {
spdlog::error("Failed to set the the active power profile: {}", e.what());
spdlog::error("Failed to set the active power profile: {}", e.what());
} catch (const Glib::Error& e) {
spdlog::error("Failed to set the active power profile: {}", std::string(e.what()));
}
+76 -46
View File
@@ -1,16 +1,11 @@
#include "modules/privacy/privacy.hpp"
#include <fmt/core.h>
#include <json/value.h>
#include <pipewire/pipewire.h>
#include <spdlog/spdlog.h>
#include <cstdio>
#include <cstring>
#include <string>
#include "AModule.hpp"
#include "gtkmm/image.h"
#include "modules/privacy/privacy_item.hpp"
namespace waybar::modules::privacy {
@@ -20,13 +15,14 @@ using util::PipewireBackend::PRIVACY_NODE_TYPE_AUDIO_OUTPUT;
using util::PipewireBackend::PRIVACY_NODE_TYPE_NONE;
using util::PipewireBackend::PRIVACY_NODE_TYPE_VIDEO_INPUT;
Privacy::Privacy(const std::string& id, const Json::Value& config, const std::string& pos)
Privacy::Privacy(const std::string& id, const Json::Value& config, Gtk::Orientation orientation,
const std::string& pos)
: AModule(config, "privacy", id),
nodes_screenshare(),
nodes_audio_in(),
nodes_audio_out(),
visibility_conn(),
box_(Gtk::ORIENTATION_HORIZONTAL, 0) {
box_(orientation, 0) {
box_.set_name(name_);
event_box_.add(box_);
@@ -50,36 +46,53 @@ Privacy::Privacy(const std::string& id, const Json::Value& config, const std::st
// Initialize each privacy module
Json::Value modules = config_["modules"];
// Add Screenshare and Mic usage as default modules if none are specified
if (!modules.isArray() || modules.size() == 0) {
if (!modules.isArray() || modules.empty()) {
modules = Json::Value(Json::arrayValue);
for (auto& type : {"screenshare", "audio-in"}) {
for (const auto& type : {"screenshare", "audio-in"}) {
Json::Value obj = Json::Value(Json::objectValue);
obj["type"] = type;
modules.append(obj);
}
}
for (uint i = 0; i < modules.size(); i++) {
const Json::Value& module_config = modules[i];
if (!module_config.isObject() || !module_config["type"].isString()) continue;
const std::string type = module_config["type"].asString();
if (type == "screenshare") {
auto item =
Gtk::make_managed<PrivacyItem>(module_config, PRIVACY_NODE_TYPE_VIDEO_INPUT,
&nodes_screenshare, pos, iconSize, transition_duration);
box_.add(*item);
} else if (type == "audio-in") {
auto item =
Gtk::make_managed<PrivacyItem>(module_config, PRIVACY_NODE_TYPE_AUDIO_INPUT,
&nodes_audio_in, pos, iconSize, transition_duration);
box_.add(*item);
} else if (type == "audio-out") {
auto item =
Gtk::make_managed<PrivacyItem>(module_config, PRIVACY_NODE_TYPE_AUDIO_OUTPUT,
&nodes_audio_out, pos, iconSize, transition_duration);
std::map<std::string, std::tuple<decltype(&nodes_audio_in), PrivacyNodeType> > typeMap = {
{"screenshare", {&nodes_screenshare, PRIVACY_NODE_TYPE_VIDEO_INPUT}},
{"audio-in", {&nodes_audio_in, PRIVACY_NODE_TYPE_AUDIO_INPUT}},
{"audio-out", {&nodes_audio_out, PRIVACY_NODE_TYPE_AUDIO_OUTPUT}},
};
for (const auto& module : modules) {
if (!module.isObject() || !module["type"].isString()) continue;
const std::string type = module["type"].asString();
auto iter = typeMap.find(type);
if (iter != typeMap.end()) {
auto& [nodePtr, nodeType] = iter->second;
auto* item = Gtk::make_managed<PrivacyItem>(module, nodeType, nodePtr, orientation, pos,
iconSize, transition_duration);
box_.add(*item);
modules_.push_back(item);
}
}
for (const auto& ignore_item : config_["ignore"]) {
if (!ignore_item.isObject() || !ignore_item["type"].isString() ||
!ignore_item["name"].isString())
continue;
const std::string type = ignore_item["type"].asString();
const std::string name = ignore_item["name"].asString();
auto iter = typeMap.find(type);
if (iter != typeMap.end()) {
auto& [_, nodeType] = iter->second;
ignore.emplace(nodeType, std::move(name));
}
}
if (config_["ignore-monitor"].isBool()) {
ignore_monitor = config_["ignore-monitor"].asBool();
}
backend = util::PipewireBackend::PipewireBackend::getInstance();
backend->privacy_nodes_changed_signal_event.connect(
sigc::mem_fun(*this, &Privacy::onPrivacyNodesChanged));
@@ -94,6 +107,11 @@ void Privacy::onPrivacyNodesChanged() {
nodes_screenshare.clear();
for (auto& node : backend->privacy_nodes) {
if (ignore_monitor && node.second->is_monitor) continue;
auto iter = ignore.find(std::pair(node.second->type, node.second->node_name));
if (iter != ignore.end()) continue;
switch (node.second->state) {
case PW_NODE_STATE_RUNNING:
switch (node.second->type) {
@@ -120,24 +138,33 @@ void Privacy::onPrivacyNodesChanged() {
}
auto Privacy::update() -> void {
mutex_.lock();
bool screenshare, audio_in, audio_out;
// set in modules or not
bool setScreenshare = false;
bool setAudioIn = false;
bool setAudioOut = false;
for (Gtk::Widget* widget : box_.get_children()) {
PrivacyItem* module = dynamic_cast<PrivacyItem*>(widget);
if (!module) continue;
// used or not
bool useScreenshare = false;
bool useAudioIn = false;
bool useAudioOut = false;
mutex_.lock();
for (PrivacyItem* module : modules_) {
switch (module->privacy_type) {
case util::PipewireBackend::PRIVACY_NODE_TYPE_VIDEO_INPUT:
screenshare = !nodes_screenshare.empty();
module->set_in_use(screenshare);
setScreenshare = true;
useScreenshare = !nodes_screenshare.empty();
module->set_in_use(useScreenshare);
break;
case util::PipewireBackend::PRIVACY_NODE_TYPE_AUDIO_INPUT:
audio_in = !nodes_audio_in.empty();
module->set_in_use(audio_in);
setAudioIn = true;
useAudioIn = !nodes_audio_in.empty();
module->set_in_use(useAudioIn);
break;
case util::PipewireBackend::PRIVACY_NODE_TYPE_AUDIO_OUTPUT:
audio_out = !nodes_audio_out.empty();
module->set_in_use(audio_out);
setAudioOut = true;
useAudioOut = !nodes_audio_out.empty();
module->set_in_use(useAudioOut);
break;
case util::PipewireBackend::PRIVACY_NODE_TYPE_NONE:
break;
@@ -146,25 +173,28 @@ auto Privacy::update() -> void {
mutex_.unlock();
// Hide the whole widget if none are in use
bool is_visible = screenshare || audio_in || audio_out;
if (is_visible != event_box_.get_visible()) {
bool isVisible = (setScreenshare && useScreenshare) || (setAudioIn && useAudioIn) ||
(setAudioOut && useAudioOut);
if (isVisible != event_box_.get_visible()) {
// Disconnect any previous connection so that it doesn't get activated in
// the future, hiding the module when it should be visible
visibility_conn.disconnect();
if (is_visible) {
if (isVisible) {
event_box_.set_visible(true);
} else {
// Hides the widget when all of the privacy_item revealers animations
// have finished animating
visibility_conn = Glib::signal_timeout().connect(
sigc::track_obj(
[this] {
[this, setScreenshare, setAudioOut, setAudioIn]() {
mutex_.lock();
bool screenshare = !nodes_screenshare.empty();
bool audio_in = !nodes_audio_in.empty();
bool audio_out = !nodes_audio_out.empty();
bool visible = false;
visible |= setScreenshare && !nodes_screenshare.empty();
visible |= setAudioIn && !nodes_audio_in.empty();
visible |= setAudioOut && !nodes_audio_out.empty();
mutex_.unlock();
event_box_.set_visible(screenshare || audio_in || audio_out);
event_box_.set_visible(visible);
return false;
},
*this),
+26 -25
View File
@@ -1,30 +1,21 @@
#include "modules/privacy/privacy_item.hpp"
#include <fmt/core.h>
#include <pipewire/pipewire.h>
#include <spdlog/spdlog.h>
#include <cstdio>
#include <cstring>
#include <string>
#include <thread>
#include "AModule.hpp"
#include "glibmm/main.h"
#include "glibmm/priorities.h"
#include "gtkmm/enums.h"
#include "gtkmm/label.h"
#include "gtkmm/revealer.h"
#include "gtkmm/tooltip.h"
#include "sigc++/adaptors/bind.h"
#include "util/gtk_icon.hpp"
#include "util/pipewire/privacy_node_info.hpp"
namespace waybar::modules::privacy {
PrivacyItem::PrivacyItem(const Json::Value &config_, enum PrivacyNodeType privacy_type_,
std::list<PrivacyNodeInfo *> *nodes_, const std::string &pos,
const uint icon_size, const uint transition_duration)
PrivacyItem::PrivacyItem(const Json::Value& config_, enum PrivacyNodeType privacy_type_,
std::list<PrivacyNodeInfo*>* nodes_, Gtk::Orientation orientation,
const std::string& pos, const uint icon_size,
const uint transition_duration)
: Gtk::Revealer(),
privacy_type(privacy_type_),
nodes(nodes_),
@@ -52,16 +43,24 @@ PrivacyItem::PrivacyItem(const Json::Value &config_, enum PrivacyNodeType privac
// Set the reveal transition to not look weird when sliding in
if (pos == "modules-left") {
set_transition_type(Gtk::REVEALER_TRANSITION_TYPE_SLIDE_RIGHT);
set_transition_type(orientation == Gtk::ORIENTATION_HORIZONTAL
? Gtk::REVEALER_TRANSITION_TYPE_SLIDE_RIGHT
: Gtk::REVEALER_TRANSITION_TYPE_SLIDE_DOWN);
} else if (pos == "modules-center") {
set_transition_type(Gtk::REVEALER_TRANSITION_TYPE_CROSSFADE);
} else if (pos == "modules-right") {
set_transition_type(Gtk::REVEALER_TRANSITION_TYPE_SLIDE_LEFT);
set_transition_type(orientation == Gtk::ORIENTATION_HORIZONTAL
? Gtk::REVEALER_TRANSITION_TYPE_SLIDE_LEFT
: Gtk::REVEALER_TRANSITION_TYPE_SLIDE_UP);
}
set_transition_duration(transition_duration);
box_.set_name("privacy-item");
box_.add(icon_);
// We use `set_center_widget` instead of `add` to make sure the icon is
// centered even if the orientation is vertical
box_.set_center_widget(icon_);
icon_.set_pixel_size(icon_size);
add(box_);
@@ -84,7 +83,7 @@ PrivacyItem::PrivacyItem(const Json::Value &config_, enum PrivacyNodeType privac
// Sets the window to use when showing the tooltip
update_tooltip();
this->signal_query_tooltip().connect(sigc::track_obj(
[this](int x, int y, bool keyboard_tooltip, const Glib::RefPtr<Gtk::Tooltip> &tooltip) {
[this](int x, int y, bool keyboard_tooltip, const Glib::RefPtr<Gtk::Tooltip>& tooltip) {
tooltip->set_custom(tooltip_window);
return true;
},
@@ -98,22 +97,24 @@ PrivacyItem::PrivacyItem(const Json::Value &config_, enum PrivacyNodeType privac
void PrivacyItem::update_tooltip() {
// Removes all old nodes
for (auto child : tooltip_window.get_children()) {
for (auto* child : tooltip_window.get_children()) {
tooltip_window.remove(*child);
// despite the remove, still needs a delete to prevent memory leak. Speculating that this might
// work differently in GTK4.
delete child;
}
for (auto *node : *nodes) {
Gtk::Box *box = new Gtk::Box(Gtk::ORIENTATION_HORIZONTAL, 4);
for (auto* node : *nodes) {
auto* box = Gtk::make_managed<Gtk::Box>(Gtk::ORIENTATION_HORIZONTAL, 4);
// Set device icon
Gtk::Image *node_icon = new Gtk::Image();
auto* node_icon = Gtk::make_managed<Gtk::Image>();
node_icon->set_pixel_size(tooltipIconSize);
node_icon->set_from_icon_name(node->get_icon_name(), Gtk::ICON_SIZE_INVALID);
node_icon->set_from_icon_name(node->getIconName(), Gtk::ICON_SIZE_INVALID);
box->add(*node_icon);
// Set model
Gtk::Label *node_name = new Gtk::Label(node->get_name());
box->add(*node_name);
auto* nodeName = Gtk::make_managed<Gtk::Label>(node->getName());
box->add(*nodeName);
tooltip_window.add(*box);
}
+38 -22
View File
@@ -1,15 +1,20 @@
#include "modules/pulseaudio.hpp"
waybar::modules::Pulseaudio::Pulseaudio(const std::string &id, const Json::Value &config)
waybar::modules::Pulseaudio::Pulseaudio(const std::string& id, const Json::Value& config)
: ALabel(config, "pulseaudio", id, "{volume}%") {
event_box_.add_events(Gdk::SCROLL_MASK | Gdk::SMOOTH_SCROLL_MASK);
event_box_.signal_scroll_event().connect(sigc::mem_fun(*this, &Pulseaudio::handleScroll));
backend = util::AudioBackend::getInstance([this] { this->dp.emit(); });
backend->setIgnoredSinks(config_["ignored-sinks"]);
backend->setSinkMapping(config_["sink-mapping"]);
if (config_["target"].isString() && config_["target"].asString() == "source") {
target = util::PulseaudioTarget::Source;
}
}
bool waybar::modules::Pulseaudio::handleScroll(GdkEventScroll *e) {
bool waybar::modules::Pulseaudio::handleScroll(GdkEventScroll* e) {
// change the pulse volume only when no user provided
// events are configured
if (config_["on-scroll-up"].isString() || config_["on-scroll-down"].isString()) {
@@ -33,7 +38,7 @@ bool waybar::modules::Pulseaudio::handleScroll(GdkEventScroll *e) {
? util::ChangeType::Increase
: util::ChangeType::Decrease;
backend->changeVolume(change_type, step, max_volume);
backend->changeVolume(change_type, step, max_volume, target);
return true;
}
@@ -42,21 +47,32 @@ static const std::array<std::string, 9> ports = {
};
const std::vector<std::string> waybar::modules::Pulseaudio::getPulseIcon() const {
std::vector<std::string> res = {backend->getCurrentSinkName(), backend->getDefaultSourceName()};
std::vector<std::string> res;
auto sink_muted = backend->getSinkMuted();
if (sink_muted) {
res.emplace_back(backend->getCurrentSinkName() + "-muted");
}
res.push_back(backend->getCurrentSinkName());
res.push_back(backend->getDefaultSourceName());
std::string nameLC = backend->getSinkPortName() + backend->getFormFactor();
std::transform(nameLC.begin(), nameLC.end(), nameLC.begin(), ::tolower);
for (auto const &port : ports) {
for (auto const& port : ports) {
if (nameLC.find(port) != std::string::npos) {
if (sink_muted) {
res.emplace_back(port + "-muted");
}
res.push_back(port);
return res;
break;
}
}
if (sink_muted) {
res.emplace_back("default-muted");
}
return res;
}
auto waybar::modules::Pulseaudio::update() -> void {
auto format = format_;
std::string tooltip_format;
auto sink_volume = backend->getSinkVolume();
if (!alt_) {
std::string format_name = "format";
@@ -94,7 +110,7 @@ auto waybar::modules::Pulseaudio::update() -> void {
}
} else {
label_.get_style_context()->remove_class("source-muted");
if (config_["format-source-muted"].isString()) {
if (config_["format-source"].isString()) {
format_source = config_["format-source"].asString();
}
}
@@ -104,29 +120,29 @@ auto waybar::modules::Pulseaudio::update() -> void {
auto source_desc = backend->getSourceDesc();
format_source = fmt::format(fmt::runtime(format_source), fmt::arg("volume", source_volume));
auto text = fmt::format(
fmt::runtime(format), fmt::arg("desc", sink_desc), fmt::arg("volume", sink_volume),
fmt::arg("format_source", format_source), fmt::arg("source_volume", source_volume),
fmt::arg("source_desc", source_desc), fmt::arg("icon", getIcon(sink_volume, getPulseIcon())));
fmt::dynamic_format_arg_store<fmt::format_context> store;
store.push_back(fmt::arg("desc", sink_desc));
store.push_back(fmt::arg("volume", sink_volume));
store.push_back(fmt::arg("format_source", format_source));
store.push_back(fmt::arg("source_volume", source_volume));
store.push_back(fmt::arg("source_desc", source_desc));
store.push_back(fmt::arg("icon", getIcon(sink_volume, getPulseIcon())));
auto text = fmt::vformat(format, store);
if (text.empty()) {
label_.hide();
} else {
label_.set_markup(text);
setLabelMarkup(text);
label_.show();
}
if (tooltipEnabled()) {
if (tooltip_format.empty() && config_["tooltip-format"].isString()) {
tooltip_format = config_["tooltip-format"].asString();
}
auto tooltip_format = resolveTooltipFormat("");
if (!tooltip_format.empty()) {
label_.set_tooltip_text(fmt::format(
fmt::runtime(tooltip_format), fmt::arg("desc", sink_desc),
fmt::arg("volume", sink_volume), fmt::arg("format_source", format_source),
fmt::arg("source_volume", source_volume), fmt::arg("source_desc", source_desc),
fmt::arg("icon", getIcon(sink_volume, getPulseIcon()))));
setTooltipMarkup(fmt::vformat(tooltip_format, store));
} else {
label_.set_tooltip_text(sink_desc);
setTooltipMarkup(sink_desc);
}
}
+33 -54
View File
@@ -6,77 +6,56 @@ PulseaudioSlider::PulseaudioSlider(const std::string& id, const Json::Value& con
: ASlider(config, "pulseaudio-slider", id) {
backend = util::AudioBackend::getInstance([this] { this->dp.emit(); });
backend->setIgnoredSinks(config_["ignored-sinks"]);
backend->setSinkMapping(config_["sink-mapping"]);
if (config_["target"].isString()) {
std::string target = config_["target"].asString();
if (target == "sink") {
this->target = PulseaudioSliderTarget::Sink;
this->target = util::PulseaudioTarget::Sink;
} else if (target == "source") {
this->target = PulseaudioSliderTarget::Source;
this->target = util::PulseaudioTarget::Source;
}
}
if (config_["zero-on-mute"].isBool()) {
zero_on_mute = config_["zero-on-mute"].asBool();
}
if (config_["unmute-on-volume-change"].isBool()) {
unmute_on_volume_change = config_["unmute-on-volume-change"].asBool();
}
}
void PulseaudioSlider::update() {
switch (target) {
case PulseaudioSliderTarget::Sink:
if (backend->getSinkMuted()) {
scale_.set_value(min_);
} else {
scale_.set_value(backend->getSinkVolume());
}
break;
uint16_t display_value = backend->getVolume(target);
bool is_muted = backend->getMuted(target);
case PulseaudioSliderTarget::Source:
if (backend->getSourceMuted()) {
scale_.set_value(min_);
} else {
scale_.set_value(backend->getSourceVolume());
}
break;
if (is_muted) {
if (zero_on_mute) {
display_value = min_;
}
if (!previously_muted) {
scale_.get_style_context()->add_class("muted");
}
} else if (previously_muted) {
scale_.get_style_context()->remove_class("muted");
}
scale_.set_value(display_value);
previously_muted = is_muted;
}
void PulseaudioSlider::onValueChanged() {
bool is_mute = false;
uint16_t slider_value = scale_.get_value();
switch (target) {
case PulseaudioSliderTarget::Sink:
if (backend->getSinkMuted()) {
is_mute = true;
}
break;
case PulseaudioSliderTarget::Source:
if (backend->getSourceMuted()) {
is_mute = true;
}
break;
}
uint16_t volume = scale_.get_value();
if (is_mute) {
// Avoid setting sink/source to volume 0 if the user muted if via another mean.
if (volume == 0) {
return;
}
// If the sink/source is mute, but the user clicked the slider, unmute it!
else {
switch (target) {
case PulseaudioSliderTarget::Sink:
backend->toggleSinkMute(false);
break;
case PulseaudioSliderTarget::Source:
backend->toggleSourceMute(false);
break;
}
// Avoid setting sink/source to volume 0 if the user muted it via other means.
if (!backend->getMuted(target) || slider_value != 0) {
if (unmute_on_volume_change) {
backend->unmute(target);
}
backend->changeVolume(slider_value, min_, max_, target);
}
backend->changeVolume(volume, min_, max_);
}
} // namespace waybar::modules
} // namespace waybar::modules
+40 -33
View File
@@ -7,48 +7,48 @@
namespace waybar::modules::river {
static void listen_focused_tags(void *data, struct zriver_output_status_v1 *zriver_output_status_v1,
static void listen_focused_tags(void* data, struct zriver_output_status_v1* zriver_output_status_v1,
uint32_t tags) {
// Intentionally empty
}
static void listen_view_tags(void *data, struct zriver_output_status_v1 *zriver_output_status_v1,
struct wl_array *tags) {
static void listen_view_tags(void* data, struct zriver_output_status_v1* zriver_output_status_v1,
struct wl_array* tags) {
// Intentionally empty
}
static void listen_urgent_tags(void *data, struct zriver_output_status_v1 *zriver_output_status_v1,
static void listen_urgent_tags(void* data, struct zriver_output_status_v1* zriver_output_status_v1,
uint32_t tags) {
// Intentionally empty
}
static void listen_layout_name(void *data, struct zriver_output_status_v1 *zriver_output_status_v1,
const char *layout) {
static_cast<Layout *>(data)->handle_name(layout);
static void listen_layout_name(void* data, struct zriver_output_status_v1* zriver_output_status_v1,
const char* layout) {
static_cast<Layout*>(data)->handle_name(layout);
}
static void listen_layout_name_clear(void *data,
struct zriver_output_status_v1 *zriver_output_status_v1) {
static_cast<Layout *>(data)->handle_clear();
static void listen_layout_name_clear(void* data,
struct zriver_output_status_v1* zriver_output_status_v1) {
static_cast<Layout*>(data)->handle_clear();
}
static void listen_focused_output(void *data, struct zriver_seat_status_v1 *zriver_seat_status_v1,
struct wl_output *output) {
static_cast<Layout *>(data)->handle_focused_output(output);
static void listen_focused_output(void* data, struct zriver_seat_status_v1* zriver_seat_status_v1,
struct wl_output* output) {
static_cast<Layout*>(data)->handle_focused_output(output);
}
static void listen_unfocused_output(void *data, struct zriver_seat_status_v1 *zriver_seat_status_v1,
struct wl_output *output) {
static_cast<Layout *>(data)->handle_unfocused_output(output);
static void listen_unfocused_output(void* data, struct zriver_seat_status_v1* zriver_seat_status_v1,
struct wl_output* output) {
static_cast<Layout*>(data)->handle_unfocused_output(output);
}
static void listen_focused_view(void *data, struct zriver_seat_status_v1 *zriver_seat_status_v1,
const char *title) {
static void listen_focused_view(void* data, struct zriver_seat_status_v1* zriver_seat_status_v1,
const char* title) {
// Intentionally empty
}
static void listen_mode(void *data, struct zriver_seat_status_v1 *zriver_seat_status_v1,
const char *mode) {
static void listen_mode(void* data, struct zriver_seat_status_v1* zriver_seat_status_v1,
const char* mode) {
// Intentionally empty
}
@@ -67,8 +67,8 @@ static const zriver_seat_status_v1_listener seat_status_listener_impl{
.mode = listen_mode,
};
static void handle_global(void *data, struct wl_registry *registry, uint32_t name,
const char *interface, uint32_t version) {
static void handle_global(void* data, struct wl_registry* registry, uint32_t name,
const char* interface, uint32_t version) {
if (std::strcmp(interface, zriver_status_manager_v1_interface.name) == 0) {
version = std::min<uint32_t>(version, 4);
@@ -80,32 +80,32 @@ static void handle_global(void *data, struct wl_registry *registry, uint32_t nam
std::to_string(version));
return;
}
static_cast<Layout *>(data)->status_manager_ = static_cast<struct zriver_status_manager_v1 *>(
static_cast<Layout*>(data)->status_manager_ = static_cast<struct zriver_status_manager_v1*>(
wl_registry_bind(registry, name, &zriver_status_manager_v1_interface, version));
}
if (std::strcmp(interface, wl_seat_interface.name) == 0) {
version = std::min<uint32_t>(version, 1);
static_cast<Layout *>(data)->seat_ = static_cast<struct wl_seat *>(
wl_registry_bind(registry, name, &wl_seat_interface, version));
static_cast<Layout*>(data)->seat_ =
static_cast<struct wl_seat*>(wl_registry_bind(registry, name, &wl_seat_interface, version));
}
}
static void handle_global_remove(void *data, struct wl_registry *registry, uint32_t name) {
static void handle_global_remove(void* data, struct wl_registry* registry, uint32_t name) {
// Nobody cares
}
static const wl_registry_listener registry_listener_impl = {.global = handle_global,
.global_remove = handle_global_remove};
Layout::Layout(const std::string &id, const waybar::Bar &bar, const Json::Value &config)
Layout::Layout(const std::string& id, const waybar::Bar& bar, const Json::Value& config)
: waybar::ALabel(config, "layout", id, "{}"),
status_manager_{nullptr},
seat_{nullptr},
bar_(bar),
output_status_{nullptr} {
struct wl_display *display = Client::inst()->wl_display;
struct wl_registry *registry = wl_display_get_registry(display);
struct wl_display* display = Client::inst()->wl_display;
struct wl_registry* registry = wl_display_get_registry(display);
wl_registry_add_listener(registry, &registry_listener_impl, this);
wl_display_roundtrip(display);
@@ -118,6 +118,7 @@ Layout::Layout(const std::string &id, const waybar::Bar &bar, const Json::Value
if (!seat_) {
spdlog::error("wl_seat not advertised");
return;
}
label_.hide();
@@ -141,13 +142,19 @@ Layout::~Layout() {
}
}
void Layout::handle_name(const char *name) {
void Layout::handle_name(const char* name) {
if (std::strcmp(name, "") == 0 || format_.empty()) {
label_.hide(); // hide empty labels or labels with empty format
} else {
label_.show();
if (!name_.empty()) {
label_.get_style_context()->remove_class(name_);
}
label_.get_style_context()->add_class(name);
label_.set_markup(fmt::format(fmt::runtime(format_), Glib::Markup::escape_text(name).raw()));
label_.show();
}
name_ = name;
ALabel::update();
}
@@ -156,7 +163,7 @@ void Layout::handle_clear() {
ALabel::update();
}
void Layout::handle_focused_output(struct wl_output *output) {
void Layout::handle_focused_output(struct wl_output* output) {
if (output_ == output) { // if we focused the output this bar belongs to
label_.get_style_context()->add_class("focused");
ALabel::update();
@@ -164,7 +171,7 @@ void Layout::handle_focused_output(struct wl_output *output) {
focused_output_ = output;
}
void Layout::handle_unfocused_output(struct wl_output *output) {
void Layout::handle_unfocused_output(struct wl_output* output) {
if (output_ == output) { // if we unfocused the output this bar belongs to
label_.get_style_context()->remove_class("focused");
ALabel::update();
+19 -18
View File
@@ -7,23 +7,23 @@
namespace waybar::modules::river {
static void listen_focused_output(void *data, struct zriver_seat_status_v1 *seat_status,
struct wl_output *output) {
static void listen_focused_output(void* data, struct zriver_seat_status_v1* seat_status,
struct wl_output* output) {
// Intentionally empty
}
static void listen_unfocused_output(void *data, struct zriver_seat_status_v1 *seat_status,
struct wl_output *output) {
static void listen_unfocused_output(void* data, struct zriver_seat_status_v1* seat_status,
struct wl_output* output) {
// Intentionally empty
}
static void listen_focused_view(void *data, struct zriver_seat_status_v1 *seat_status,
const char *title) {
static void listen_focused_view(void* data, struct zriver_seat_status_v1* seat_status,
const char* title) {
// Intentionally empty
}
static void listen_mode(void *data, struct zriver_seat_status_v1 *seat_status, const char *mode) {
static_cast<Mode *>(data)->handle_mode(mode);
static void listen_mode(void* data, struct zriver_seat_status_v1* seat_status, const char* mode) {
static_cast<Mode*>(data)->handle_mode(mode);
}
static const zriver_seat_status_v1_listener seat_status_listener_impl = {
@@ -33,8 +33,8 @@ static const zriver_seat_status_v1_listener seat_status_listener_impl = {
.mode = listen_mode,
};
static void handle_global(void *data, struct wl_registry *registry, uint32_t name,
const char *interface, uint32_t version) {
static void handle_global(void* data, struct wl_registry* registry, uint32_t name,
const char* interface, uint32_t version) {
if (std::strcmp(interface, zriver_status_manager_v1_interface.name) == 0) {
version = std::min<uint32_t>(version, 3);
if (version < ZRIVER_SEAT_STATUS_V1_MODE_SINCE_VERSION) {
@@ -42,31 +42,31 @@ static void handle_global(void *data, struct wl_registry *registry, uint32_t nam
"river server does not support the \"mode\" event; the module will be disabled");
return;
}
static_cast<Mode *>(data)->status_manager_ = static_cast<struct zriver_status_manager_v1 *>(
static_cast<Mode*>(data)->status_manager_ = static_cast<struct zriver_status_manager_v1*>(
wl_registry_bind(registry, name, &zriver_status_manager_v1_interface, version));
} else if (std::strcmp(interface, wl_seat_interface.name) == 0) {
version = std::min<uint32_t>(version, 1);
static_cast<Mode *>(data)->seat_ = static_cast<struct wl_seat *>(
wl_registry_bind(registry, name, &wl_seat_interface, version));
static_cast<Mode*>(data)->seat_ =
static_cast<struct wl_seat*>(wl_registry_bind(registry, name, &wl_seat_interface, version));
}
}
static void handle_global_remove(void *data, struct wl_registry *registry, uint32_t name) {
static void handle_global_remove(void* data, struct wl_registry* registry, uint32_t name) {
// Nobody cares
}
static const wl_registry_listener registry_listener_impl = {.global = handle_global,
.global_remove = handle_global_remove};
Mode::Mode(const std::string &id, const waybar::Bar &bar, const Json::Value &config)
Mode::Mode(const std::string& id, const waybar::Bar& bar, const Json::Value& config)
: waybar::ALabel(config, "mode", id, "{}"),
status_manager_{nullptr},
seat_{nullptr},
bar_(bar),
mode_{""},
seat_status_{nullptr} {
struct wl_display *display = Client::inst()->wl_display;
struct wl_registry *registry = wl_display_get_registry(display);
struct wl_display* display = Client::inst()->wl_display;
struct wl_registry* registry = wl_display_get_registry(display);
wl_registry_add_listener(registry, &registry_listener_impl, this);
wl_display_roundtrip(display);
@@ -77,6 +77,7 @@ Mode::Mode(const std::string &id, const waybar::Bar &bar, const Json::Value &con
if (!seat_) {
spdlog::error("wl_seat not advertised");
return;
}
label_.hide();
@@ -94,7 +95,7 @@ Mode::~Mode() {
}
}
void Mode::handle_mode(const char *mode) {
void Mode::handle_mode(const char* mode) {
if (format_.empty()) {
label_.hide();
} else {
+134 -35
View File
@@ -12,19 +12,19 @@
namespace waybar::modules::river {
static void listen_focused_tags(void *data, struct zriver_output_status_v1 *zriver_output_status_v1,
static void listen_focused_tags(void* data, struct zriver_output_status_v1* zriver_output_status_v1,
uint32_t tags) {
static_cast<Tags *>(data)->handle_focused_tags(tags);
static_cast<Tags*>(data)->handle_focused_tags(tags);
}
static void listen_view_tags(void *data, struct zriver_output_status_v1 *zriver_output_status_v1,
struct wl_array *tags) {
static_cast<Tags *>(data)->handle_view_tags(tags);
static void listen_view_tags(void* data, struct zriver_output_status_v1* zriver_output_status_v1,
struct wl_array* tags) {
static_cast<Tags*>(data)->handle_view_tags(tags);
}
static void listen_urgent_tags(void *data, struct zriver_output_status_v1 *zriver_output_status_v1,
static void listen_urgent_tags(void* data, struct zriver_output_status_v1* zriver_output_status_v1,
uint32_t tags) {
static_cast<Tags *>(data)->handle_urgent_tags(tags);
static_cast<Tags*>(data)->handle_urgent_tags(tags);
}
static const zriver_output_status_v1_listener output_status_listener_impl{
@@ -33,15 +33,42 @@ static const zriver_output_status_v1_listener output_status_listener_impl{
.urgent_tags = listen_urgent_tags,
};
static void listen_command_success(void *data,
struct zriver_command_callback_v1 *zriver_command_callback_v1,
const char *output) {
static void listen_focused_output(void* data, struct zriver_seat_status_v1* zriver_seat_status_v1,
struct wl_output* output) {
static_cast<Tags*>(data)->handle_focused_output(output);
}
static void listen_unfocused_output(void* data, struct zriver_seat_status_v1* zriver_seat_status_v1,
struct wl_output* output) {
static_cast<Tags*>(data)->handle_unfocused_output(output);
}
static void listen_focused_view(void* data, struct zriver_seat_status_v1* zriver_seat_status_v1,
const char* title) {
// This module doesn't care
}
static void listen_mode(void* data, struct zriver_seat_status_v1* zriver_seat_status_v1,
const char* mode) {
// This module doesn't care
}
static const zriver_seat_status_v1_listener seat_status_listener_impl{
.focused_output = listen_focused_output,
.unfocused_output = listen_unfocused_output,
.focused_view = listen_focused_view,
.mode = listen_mode,
};
static void listen_command_success(void* data,
struct zriver_command_callback_v1* zriver_command_callback_v1,
const char* output) {
// Do nothing but keep listener to avoid crashing when command was successful
}
static void listen_command_failure(void *data,
struct zriver_command_callback_v1 *zriver_command_callback_v1,
const char *output) {
static void listen_command_failure(void* data,
struct zriver_command_callback_v1* zriver_command_callback_v1,
const char* output) {
spdlog::error("failure when selecting/toggling tags {}", output);
}
@@ -50,47 +77,49 @@ static const zriver_command_callback_v1_listener command_callback_listener_impl{
.failure = listen_command_failure,
};
static void handle_global(void *data, struct wl_registry *registry, uint32_t name,
const char *interface, uint32_t version) {
static void handle_global(void* data, struct wl_registry* registry, uint32_t name,
const char* interface, uint32_t version) {
if (std::strcmp(interface, zriver_status_manager_v1_interface.name) == 0) {
version = std::min(version, 2u);
if (version < ZRIVER_OUTPUT_STATUS_V1_URGENT_TAGS_SINCE_VERSION) {
spdlog::warn("river server does not support urgent tags");
}
static_cast<Tags *>(data)->status_manager_ = static_cast<struct zriver_status_manager_v1 *>(
static_cast<Tags*>(data)->status_manager_ = static_cast<struct zriver_status_manager_v1*>(
wl_registry_bind(registry, name, &zriver_status_manager_v1_interface, version));
}
if (std::strcmp(interface, zriver_control_v1_interface.name) == 0) {
version = std::min(version, 1u);
static_cast<Tags *>(data)->control_ = static_cast<struct zriver_control_v1 *>(
static_cast<Tags*>(data)->control_ = static_cast<struct zriver_control_v1*>(
wl_registry_bind(registry, name, &zriver_control_v1_interface, version));
}
if (std::strcmp(interface, wl_seat_interface.name) == 0) {
version = std::min(version, 1u);
static_cast<Tags *>(data)->seat_ = static_cast<struct wl_seat *>(
wl_registry_bind(registry, name, &wl_seat_interface, version));
static_cast<Tags*>(data)->seat_ =
static_cast<struct wl_seat*>(wl_registry_bind(registry, name, &wl_seat_interface, version));
}
}
static void handle_global_remove(void *data, struct wl_registry *registry, uint32_t name) {
static void handle_global_remove(void* data, struct wl_registry* registry, uint32_t name) {
/* Ignore event */
}
static const wl_registry_listener registry_listener_impl = {.global = handle_global,
.global_remove = handle_global_remove};
Tags::Tags(const std::string &id, const waybar::Bar &bar, const Json::Value &config)
Tags::Tags(const std::string& id, const waybar::Bar& bar, const Json::Value& config)
: waybar::AModule(config, "tags", id, false, false),
status_manager_{nullptr},
control_{nullptr},
seat_{nullptr},
bar_(bar),
output_{nullptr},
box_{bar.orientation, 0},
output_status_{nullptr} {
struct wl_display *display = Client::inst()->wl_display;
struct wl_registry *registry = wl_display_get_registry(display);
output_status_{nullptr},
seat_status_{nullptr} {
struct wl_display* display = Client::inst()->wl_display;
struct wl_registry* registry = wl_display_get_registry(display);
wl_registry_add_listener(registry, &registry_listener_impl, this);
wl_display_roundtrip(display);
@@ -101,12 +130,19 @@ Tags::Tags(const std::string &id, const waybar::Bar &bar, const Json::Value &con
if (!control_) {
spdlog::error("river_control_v1 not advertised");
return;
}
if (!seat_) {
spdlog::error("wl_seat not advertised");
return;
}
// Store the output this module belongs to; the river_output_status and
// river_seat_status objects (and their listeners) are created lazily in
// handle_show() to avoid leaking objects without listeners here.
output_ = gdk_wayland_monitor_get_wl_output(bar_.output->monitor->gobj());
box_.set_name("tags");
if (!id.empty()) {
box_.get_style_context()->add_class(id);
@@ -129,7 +165,7 @@ Tags::Tags(const std::string &id, const waybar::Bar &bar, const Json::Value &con
buttons_.emplace_back(std::to_string(tag + 1));
}
auto &button = buttons_[tag];
auto& button = buttons_[tag];
button.set_relief(Gtk::RELIEF_NONE);
box_.pack_start(button, false, false, 0);
@@ -147,14 +183,11 @@ Tags::Tags(const std::string &id, const waybar::Bar &bar, const Json::Value &con
button.signal_button_press_event().connect(
sigc::bind(sigc::mem_fun(*this, &Tags::handle_button_press), (1 << tag)));
}
button.get_style_context()->add_class("tag-" + std::to_string(tag + 1));
button.show();
}
struct wl_output *output = gdk_wayland_monitor_get_wl_output(bar_.output->monitor->gobj());
output_status_ = zriver_status_manager_v1_get_river_output_status(status_manager_, output);
zriver_output_status_v1_add_listener(output_status_, &output_status_listener_impl, this);
zriver_status_manager_v1_destroy(status_manager_);
box_.signal_show().connect(sigc::mem_fun(*this, &Tags::handle_show));
}
Tags::~Tags() {
@@ -162,24 +195,44 @@ Tags::~Tags() {
zriver_output_status_v1_destroy(output_status_);
}
if (seat_status_) {
zriver_seat_status_v1_destroy(seat_status_);
}
if (control_) {
zriver_control_v1_destroy(control_);
}
if (status_manager_) {
zriver_status_manager_v1_destroy(status_manager_);
}
}
void Tags::handle_show() {
if (!status_manager_) return;
output_status_ = zriver_status_manager_v1_get_river_output_status(status_manager_, output_);
zriver_output_status_v1_add_listener(output_status_, &output_status_listener_impl, this);
seat_status_ = zriver_status_manager_v1_get_river_seat_status(status_manager_, seat_);
zriver_seat_status_v1_add_listener(seat_status_, &seat_status_listener_impl, this);
zriver_status_manager_v1_destroy(status_manager_);
status_manager_ = nullptr;
}
void Tags::handle_primary_clicked(uint32_t tag) {
// Send river command to select tag on left mouse click
zriver_command_callback_v1 *callback;
zriver_command_callback_v1* callback;
zriver_control_v1_add_argument(control_, "set-focused-tags");
zriver_control_v1_add_argument(control_, std::to_string(tag).c_str());
callback = zriver_control_v1_run_command(control_, seat_);
zriver_command_callback_v1_add_listener(callback, &command_callback_listener_impl, nullptr);
}
bool Tags::handle_button_press(GdkEventButton *event_button, uint32_t tag) {
bool Tags::handle_button_press(GdkEventButton* event_button, uint32_t tag) {
if (event_button->type == GDK_BUTTON_PRESS && event_button->button == 3) {
// Send river command to toggle tag on right mouse click
zriver_command_callback_v1 *callback;
zriver_command_callback_v1* callback;
zriver_control_v1_add_argument(control_, "toggle-focused-tags");
zriver_control_v1_add_argument(control_, std::to_string(tag).c_str());
callback = zriver_control_v1_run_command(control_, seat_);
@@ -189,39 +242,85 @@ bool Tags::handle_button_press(GdkEventButton *event_button, uint32_t tag) {
}
void Tags::handle_focused_tags(uint32_t tags) {
auto hide_vacant = config_["hide-vacant"].asBool();
for (size_t i = 0; i < buttons_.size(); ++i) {
bool visible = buttons_[i].is_visible();
bool occupied = buttons_[i].get_style_context()->has_class("occupied");
bool urgent = buttons_[i].get_style_context()->has_class("urgent");
if ((1 << i) & tags) {
if (hide_vacant && !visible) {
buttons_[i].set_visible(true);
}
buttons_[i].get_style_context()->add_class("focused");
} else {
if (hide_vacant && !(occupied || urgent)) {
buttons_[i].set_visible(false);
}
buttons_[i].get_style_context()->remove_class("focused");
}
}
}
void Tags::handle_view_tags(struct wl_array *view_tags) {
void Tags::handle_view_tags(struct wl_array* view_tags) {
uint32_t tags = 0;
auto view_tag = reinterpret_cast<uint32_t *>(view_tags->data);
auto view_tag = reinterpret_cast<uint32_t*>(view_tags->data);
auto end = view_tag + (view_tags->size / sizeof(uint32_t));
for (; view_tag < end; ++view_tag) {
tags |= *view_tag;
}
auto hide_vacant = config_["hide-vacant"].asBool();
for (size_t i = 0; i < buttons_.size(); ++i) {
bool visible = buttons_[i].is_visible();
bool focused = buttons_[i].get_style_context()->has_class("focused");
bool urgent = buttons_[i].get_style_context()->has_class("urgent");
if ((1 << i) & tags) {
if (hide_vacant && !visible) {
buttons_[i].set_visible(true);
}
buttons_[i].get_style_context()->add_class("occupied");
} else {
if (hide_vacant && !(focused || urgent)) {
buttons_[i].set_visible(false);
}
buttons_[i].get_style_context()->remove_class("occupied");
}
}
}
void Tags::handle_urgent_tags(uint32_t tags) {
auto hide_vacant = config_["hide-vacant"].asBool();
for (size_t i = 0; i < buttons_.size(); ++i) {
bool visible = buttons_[i].is_visible();
bool occupied = buttons_[i].get_style_context()->has_class("occupied");
bool focused = buttons_[i].get_style_context()->has_class("focused");
if ((1 << i) & tags) {
if (hide_vacant && !visible) {
buttons_[i].set_visible(true);
}
buttons_[i].get_style_context()->add_class("urgent");
} else {
if (hide_vacant && !(occupied || focused)) {
buttons_[i].set_visible(false);
}
buttons_[i].get_style_context()->remove_class("urgent");
}
}
}
void Tags::handle_focused_output(struct wl_output* output) {
if (output_ == output) {
for (size_t i = 0; i < buttons_.size(); ++i) {
buttons_[i].get_style_context()->add_class("output");
}
}
}
void Tags::handle_unfocused_output(struct wl_output* output) {
if (output_ == output) {
for (size_t i = 0; i < buttons_.size(); ++i) {
buttons_[i].get_style_context()->remove_class("output");
}
}
}
} /* namespace waybar::modules::river */
+24 -23
View File
@@ -9,23 +9,23 @@
namespace waybar::modules::river {
static void listen_focused_view(void *data, struct zriver_seat_status_v1 *zriver_seat_status_v1,
const char *title) {
static_cast<Window *>(data)->handle_focused_view(title);
static void listen_focused_view(void* data, struct zriver_seat_status_v1* zriver_seat_status_v1,
const char* title) {
static_cast<Window*>(data)->handle_focused_view(title);
}
static void listen_focused_output(void *data, struct zriver_seat_status_v1 *zriver_seat_status_v1,
struct wl_output *output) {
static_cast<Window *>(data)->handle_focused_output(output);
static void listen_focused_output(void* data, struct zriver_seat_status_v1* zriver_seat_status_v1,
struct wl_output* output) {
static_cast<Window*>(data)->handle_focused_output(output);
}
static void listen_unfocused_output(void *data, struct zriver_seat_status_v1 *zriver_seat_status_v1,
struct wl_output *output) {
static_cast<Window *>(data)->handle_unfocused_output(output);
static void listen_unfocused_output(void* data, struct zriver_seat_status_v1* zriver_seat_status_v1,
struct wl_output* output) {
static_cast<Window*>(data)->handle_unfocused_output(output);
}
static void listen_mode(void *data, struct zriver_seat_status_v1 *zriver_seat_status_v1,
const char *mode) {
static void listen_mode(void* data, struct zriver_seat_status_v1* zriver_seat_status_v1,
const char* mode) {
// This module doesn't care
}
@@ -36,36 +36,36 @@ static const zriver_seat_status_v1_listener seat_status_listener_impl{
.mode = listen_mode,
};
static void handle_global(void *data, struct wl_registry *registry, uint32_t name,
const char *interface, uint32_t version) {
static void handle_global(void* data, struct wl_registry* registry, uint32_t name,
const char* interface, uint32_t version) {
if (std::strcmp(interface, zriver_status_manager_v1_interface.name) == 0) {
version = std::min<uint32_t>(version, 2);
static_cast<Window *>(data)->status_manager_ = static_cast<struct zriver_status_manager_v1 *>(
static_cast<Window*>(data)->status_manager_ = static_cast<struct zriver_status_manager_v1*>(
wl_registry_bind(registry, name, &zriver_status_manager_v1_interface, version));
}
if (std::strcmp(interface, wl_seat_interface.name) == 0) {
version = std::min<uint32_t>(version, 1);
static_cast<Window *>(data)->seat_ = static_cast<struct wl_seat *>(
wl_registry_bind(registry, name, &wl_seat_interface, version));
static_cast<Window*>(data)->seat_ =
static_cast<struct wl_seat*>(wl_registry_bind(registry, name, &wl_seat_interface, version));
}
}
static void handle_global_remove(void *data, struct wl_registry *registry, uint32_t name) {
static void handle_global_remove(void* data, struct wl_registry* registry, uint32_t name) {
/* Ignore event */
}
static const wl_registry_listener registry_listener_impl = {.global = handle_global,
.global_remove = handle_global_remove};
Window::Window(const std::string &id, const waybar::Bar &bar, const Json::Value &config)
Window::Window(const std::string& id, const waybar::Bar& bar, const Json::Value& config)
: waybar::ALabel(config, "window", id, "{}", 30),
status_manager_{nullptr},
seat_{nullptr},
bar_(bar),
seat_status_{nullptr} {
struct wl_display *display = Client::inst()->wl_display;
struct wl_registry *registry = wl_display_get_registry(display);
struct wl_display* display = Client::inst()->wl_display;
struct wl_registry* registry = wl_display_get_registry(display);
wl_registry_add_listener(registry, &registry_listener_impl, this);
wl_display_roundtrip(display);
@@ -78,6 +78,7 @@ Window::Window(const std::string &id, const waybar::Bar &bar, const Json::Value
if (!seat_) {
spdlog::error("wl_seat not advertised");
return;
}
label_.hide(); // hide the label until populated
@@ -95,7 +96,7 @@ Window::~Window() {
}
}
void Window::handle_focused_view(const char *title) {
void Window::handle_focused_view(const char* title) {
// don't change the label on unfocused outputs.
// this makes the current output report its currently focused view, and unfocused outputs will
// report their last focused views. when freshly starting the bar, unfocused outputs don't have a
@@ -116,7 +117,7 @@ void Window::handle_focused_view(const char *title) {
ALabel::update();
}
void Window::handle_focused_output(struct wl_output *output) {
void Window::handle_focused_output(struct wl_output* output) {
if (output_ == output) { // if we focused the output this bar belongs to
label_.get_style_context()->add_class("focused");
ALabel::update();
@@ -124,7 +125,7 @@ void Window::handle_focused_output(struct wl_output *output) {
focused_output_ = output;
}
void Window::handle_unfocused_output(struct wl_output *output) {
void Window::handle_unfocused_output(struct wl_output* output) {
if (output_ == output) { // if we unfocused the output this bar belongs to
label_.get_style_context()->remove_class("focused");
ALabel::update();
+1 -12
View File
@@ -18,18 +18,7 @@ auto waybar::modules::Clock::update() -> void {
tzset(); // Update timezone information
auto now = std::chrono::system_clock::now();
auto localtime = fmt::localtime(std::chrono::system_clock::to_time_t(now));
auto text = fmt::format(fmt::runtime(format_), localtime);
label_.set_markup(text);
if (tooltipEnabled()) {
if (config_["tooltip-format"].isString()) {
auto tooltip_format = config_["tooltip-format"].asString();
auto tooltip_text = fmt::format(fmt::runtime(tooltip_format), localtime);
label_.set_tooltip_text(tooltip_text);
} else {
label_.set_tooltip_text(text);
}
}
updateLabelAndTooltip(format_, format_, localtime);
// Call parent update
ALabel::update();
}
+12 -10
View File
@@ -9,8 +9,8 @@
namespace waybar::modules {
void ondesc(void *arg, struct sioctl_desc *d, int curval) {
auto self = static_cast<Sndio *>(arg);
void ondesc(void* arg, struct sioctl_desc* d, int curval) {
auto self = static_cast<Sndio*>(arg);
if (d == NULL) {
// d is NULL when the list is done
return;
@@ -18,8 +18,8 @@ void ondesc(void *arg, struct sioctl_desc *d, int curval) {
self->set_desc(d, curval);
}
void onval(void *arg, unsigned int addr, unsigned int val) {
auto self = static_cast<Sndio *>(arg);
void onval(void* arg, unsigned int addr, unsigned int val) {
auto self = static_cast<Sndio*>(arg);
self->put_val(addr, val);
}
@@ -40,7 +40,7 @@ auto Sndio::connect_to_sndio() -> void {
pfds_.reserve(sioctl_nfds(hdl_));
}
Sndio::Sndio(const std::string &id, const Json::Value &config)
Sndio::Sndio(const std::string& id, const Json::Value& config)
: ALabel(config, "sndio", id, "{volume}%", 1, false, true),
hdl_(nullptr),
pfds_(0),
@@ -80,7 +80,7 @@ Sndio::Sndio(const std::string &id, const Json::Value &config)
while (thread_.isRunning()) {
try {
connect_to_sndio();
} catch (std::runtime_error const &e) {
} catch (std::runtime_error const& e) {
// avoid leaking hdl_
if (hdl_) {
sioctl_close(hdl_);
@@ -102,7 +102,9 @@ Sndio::~Sndio() { sioctl_close(hdl_); }
auto Sndio::update() -> void {
auto format = format_;
unsigned int vol = 100. * static_cast<double>(volume_) / static_cast<double>(maxval_);
unsigned int vol = (maxval_ > 0) ? static_cast<unsigned int>(100. * static_cast<double>(volume_) /
static_cast<double>(maxval_))
: 0;
if (volume_ == 0) {
label_.get_style_context()->add_class("muted");
@@ -122,7 +124,7 @@ auto Sndio::update() -> void {
ALabel::update();
}
auto Sndio::set_desc(struct sioctl_desc *d, unsigned int val) -> void {
auto Sndio::set_desc(struct sioctl_desc* d, unsigned int val) -> void {
std::string name{d->func};
std::string node_name{d->node0.name};
@@ -140,7 +142,7 @@ auto Sndio::put_val(unsigned int addr, unsigned int val) -> void {
}
}
bool Sndio::handleScroll(GdkEventScroll *e) {
bool Sndio::handleScroll(GdkEventScroll* e) {
// change the volume only when no user provided
// events are configured
if (config_["on-scroll-up"].isString() || config_["on-scroll-down"].isString()) {
@@ -180,7 +182,7 @@ bool Sndio::handleScroll(GdkEventScroll *e) {
return true;
}
bool Sndio::handleToggle(GdkEventButton *const &e) {
bool Sndio::handleToggle(GdkEventButton* const& e) {
// toggle mute only when no user provided events are configured
if (config_["on-click"].isString()) {
return AModule::handleToggle(e);
+130 -11
View File
@@ -6,9 +6,14 @@
namespace waybar::modules::SNI {
static const unsigned RETRY_DELAY_MS = 200;
static const unsigned MAX_RETRIES = 10;
Host::Host(const std::size_t id, const Json::Value& config, const Bar& bar,
const std::vector<std::string>& ignore_list,
const std::function<void(std::unique_ptr<Item>&)>& on_add,
const std::function<void(std::unique_ptr<Item>&)>& on_remove)
const std::function<void(std::unique_ptr<Item>&)>& on_remove,
const std::function<void()>& on_update)
: bus_name_("org.kde.StatusNotifierHost-" + std::to_string(getpid()) + "-" +
std::to_string(id)),
object_path_("/StatusNotifierHost/" + std::to_string(id)),
@@ -16,10 +21,13 @@ Host::Host(const std::size_t id, const Json::Value& config, const Bar& bar,
sigc::mem_fun(*this, &Host::busAcquired))),
config_(config),
bar_(bar),
ignore_list_(ignore_list),
on_add_(on_add),
on_remove_(on_remove) {}
on_remove_(on_remove),
on_update_(on_update) {}
Host::~Host() {
retry_connection_.disconnect();
if (bus_name_id_ > 0) {
Gio::DBus::unown_name(bus_name_id_);
bus_name_id_ = 0;
@@ -33,6 +41,42 @@ Host::~Host() {
g_clear_object(&watcher_);
}
void Host::checkIgnoreList(const std::vector<std::string>& ignore_list,
const std::function<void(std::unique_ptr<Item>&)>& on_remove) {
spdlog::debug("Host::checkIgnoreList - checking {} items against {} patterns", items_.size(),
ignore_list.size());
for (auto it = items_.begin(); it != items_.end();) {
auto& item = *it;
spdlog::debug(" Checking item: bus_name='{}', category='{}', icon_name='{}', title='{}'",
item->bus_name, item->category, item->icon_name, item->title);
bool should_remove = false;
for (const auto& ignored : ignore_list) {
if (item->bus_name.find(ignored) != std::string::npos ||
item->category.find(ignored) != std::string::npos ||
item->icon_name.find(ignored) != std::string::npos ||
item->id.find(ignored) != std::string::npos ||
item->title.find(ignored) != std::string::npos) {
spdlog::info(
"Host: Ignoring item bus_name='{}', category='{}', icon_name='{}', title='{}' - "
"matched pattern '{}'",
item->bus_name, item->category, item->icon_name, item->title, ignored);
on_remove(item);
should_remove = true;
break;
}
}
if (should_remove) {
it = items_.erase(it);
} else {
++it;
}
}
}
void Host::busAcquired(const Glib::RefPtr<Gio::DBus::Connection>& conn, Glib::ustring name) {
watcher_id_ = Gio::DBus::watch_name(conn, "org.kde.StatusNotifierWatcher",
sigc::mem_fun(*this, &Host::nameAppeared),
@@ -51,15 +95,17 @@ void Host::nameAppeared(const Glib::RefPtr<Gio::DBus::Connection>& conn, const G
}
void Host::nameVanished(const Glib::RefPtr<Gio::DBus::Connection>& conn, const Glib::ustring name) {
retry_connection_.disconnect();
retry_count_ = 0;
g_cancellable_cancel(cancellable_);
g_clear_object(&cancellable_);
g_clear_object(&watcher_);
items_.clear();
clearItems();
}
void Host::proxyReady(GObject* src, GAsyncResult* res, gpointer data) {
GError* error = nullptr;
waybar::util::ScopeGuard error_deleter([error]() {
waybar::util::ScopeGuard error_deleter([&error]() {
if (error != nullptr) {
g_error_free(error);
}
@@ -70,18 +116,38 @@ void Host::proxyReady(GObject* src, GAsyncResult* res, gpointer data) {
return;
}
auto host = static_cast<SNI::Host*>(data);
host->watcher_ = watcher;
if (error != nullptr) {
spdlog::error("Host: {}", error->message);
g_clear_object(&host->cancellable_);
if (host->retry_count_ >= MAX_RETRIES) {
spdlog::warn("Host: giving up on watcher proxy creation after {} retries",
host->retry_count_);
return;
}
host->retry_count_ += 1;
// Store the timeout connection so it is disconnected in ~Host, avoiding a
// use-after-free if the Host is destroyed before the retry fires.
host->retry_connection_ = Glib::signal_timeout().connect(
[host]() {
if (host->watcher_ != nullptr) {
return false;
}
auto conn = Gio::DBus::Connection::get_sync(Gio::DBus::BusType::BUS_TYPE_SESSION);
host->nameAppeared(conn, "org.kde.StatusNotifierWatcher", "");
return false;
},
RETRY_DELAY_MS);
return;
}
host->retry_count_ = 0;
host->watcher_ = watcher;
sn_watcher_call_register_host(host->watcher_, host->object_path_.c_str(), host->cancellable_,
&Host::registerHost, data);
}
void Host::registerHost(GObject* src, GAsyncResult* res, gpointer data) {
GError* error = nullptr;
waybar::util::ScopeGuard error_deleter([error]() {
waybar::util::ScopeGuard error_deleter([&error]() {
if (error != nullptr) {
g_error_free(error);
}
@@ -100,7 +166,9 @@ void Host::registerHost(GObject* src, GAsyncResult* res, gpointer data) {
g_signal_connect(host->watcher_, "item-unregistered", G_CALLBACK(&Host::itemUnregistered), data);
auto items = sn_watcher_dup_registered_items(host->watcher_);
if (items != nullptr) {
spdlog::info("Host: Found {} pre-registered SNI items", g_strv_length(items));
for (uint32_t i = 0; items[i] != nullptr; i += 1) {
spdlog::info("Host: Processing pre-registered item: {}", items[i]);
host->addRegisteredItem(items[i]);
}
}
@@ -109,7 +177,10 @@ void Host::registerHost(GObject* src, GAsyncResult* res, gpointer data) {
void Host::itemRegistered(SnWatcher* watcher, const gchar* service, gpointer data) {
auto host = static_cast<SNI::Host*>(data);
spdlog::info("Host::itemRegistered called with service: {}", service);
host->addRegisteredItem(service);
// host->checkIgnoreList(host->ignore_list_, std::bind(&Host::itemUnregistered, host,
// std::placeholders::_1, std::placeholders::_2, data));
}
void Host::itemUnregistered(SnWatcher* watcher, const gchar* service, gpointer data) {
@@ -117,13 +188,50 @@ void Host::itemUnregistered(SnWatcher* watcher, const gchar* service, gpointer d
auto [bus_name, object_path] = host->getBusNameAndObjectPath(service);
for (auto it = host->items_.begin(); it != host->items_.end(); ++it) {
if ((*it)->bus_name == bus_name && (*it)->object_path == object_path) {
host->on_remove_(*it);
host->items_.erase(it);
host->removeItem(it);
break;
}
}
}
void Host::itemReady(Item& item) {
auto it = std::find_if(items_.begin(), items_.end(),
[&item](const auto& candidate) { return candidate.get() == &item; });
if (it != items_.end() && (*it)->isReady()) {
on_add_(*it);
}
}
void Host::itemInvalidated(Item& item) {
auto it = std::find_if(items_.begin(), items_.end(),
[&item](const auto& candidate) { return candidate.get() == &item; });
if (it != items_.end()) {
removeItem(it);
}
}
void Host::removeItem(std::vector<std::unique_ptr<Item>>::iterator it) {
if ((*it)->isReady()) {
on_remove_(*it);
}
items_.erase(it);
}
void Host::clearItems() {
bool removed_ready_item = false;
for (auto& item : items_) {
if (item->isReady()) {
on_remove_(item);
removed_ready_item = true;
}
}
bool had_items = !items_.empty();
items_.clear();
if (had_items && !removed_ready_item) {
on_update_();
}
}
std::tuple<std::string, std::string> Host::getBusNameAndObjectPath(const std::string service) {
auto it = service.find('/');
if (it != std::string::npos) {
@@ -132,15 +240,26 @@ std::tuple<std::string, std::string> Host::getBusNameAndObjectPath(const std::st
return {service, "/StatusNotifierItem"};
}
void Host::addRegisteredItem(std::string service) {
void Host::addRegisteredItem(const std::string& service) {
// Check service string directly before parsing
for (const auto& ignored : ignore_list_) {
if (service.find(ignored) != std::string::npos) {
spdlog::info("Host: Ignoring service '{}' - matched pattern '{}'", service, ignored);
return;
}
}
std::string bus_name, object_path;
std::tie(bus_name, object_path) = getBusNameAndObjectPath(service);
spdlog::debug("SNI item registered: bus_name={}, object_path={}, full_service={}", bus_name,
object_path, service);
auto it = std::find_if(items_.begin(), items_.end(), [&bus_name, &object_path](const auto& item) {
return bus_name == item->bus_name && object_path == item->object_path;
});
if (it == items_.end()) {
items_.emplace_back(new Item(bus_name, object_path, config_, bar_));
on_add_(items_.back());
spdlog::debug("Adding SNI item: {}", bus_name);
items_.emplace_back(std::make_unique<Item>(
bus_name, object_path, config_, bar_, [this](Item& item) { itemReady(item); },
[this](Item& item) { itemInvalidated(item); }, on_update_));
}
}
+234 -59
View File
@@ -5,22 +5,26 @@
#include <gtkmm/tooltip.h>
#include <spdlog/spdlog.h>
#include <algorithm>
#include <filesystem>
#include <fstream>
#include <map>
#include "gdk/gdk.h"
#include "modules/sni/icon_manager.hpp"
#include "util/format.hpp"
#include "util/gtk_icon.hpp"
template <>
struct fmt::formatter<Glib::VariantBase> : formatter<std::string> {
bool is_printable(const Glib::VariantBase& value) {
bool is_printable(const Glib::VariantBase& value) const {
auto type = value.get_type_string();
/* Print only primitive (single character excluding 'v') and short complex types */
return (type.length() == 1 && islower(type[0]) && type[0] != 'v') || value.get_size() <= 32;
}
template <typename FormatContext>
auto format(const Glib::VariantBase& value, FormatContext& ctx) {
auto format(const Glib::VariantBase& value, FormatContext& ctx) const {
if (is_printable(value)) {
return formatter<std::string>::format(static_cast<std::string>(value.print()), ctx);
} else {
@@ -34,13 +38,18 @@ namespace waybar::modules::SNI {
static const Glib::ustring SNI_INTERFACE_NAME = sn_item_interface_info()->name;
static const unsigned UPDATE_DEBOUNCE_TIME = 10;
Item::Item(const std::string& bn, const std::string& op, const Json::Value& config, const Bar& bar)
Item::Item(const std::string& bn, const std::string& op, const Json::Value& config, const Bar& bar,
const std::function<void(Item&)>& on_ready,
const std::function<void(Item&)>& on_invalidate, const std::function<void()>& on_updated)
: bus_name(bn),
object_path(op),
icon_size(16),
effective_icon_size(0),
icon_theme(Gtk::IconTheme::create()),
bar_(bar) {
bar_(bar),
on_ready_(on_ready),
on_invalidate_(on_invalidate),
on_updated_(on_updated) {
if (config["icon-size"].isUInt()) {
icon_size = config["icon-size"].asUInt();
}
@@ -57,6 +66,8 @@ Item::Item(const std::string& bn, const std::string& op, const Json::Value& conf
event_box.add_events(Gdk::BUTTON_PRESS_MASK | Gdk::SCROLL_MASK | Gdk::SMOOTH_SCROLL_MASK);
event_box.signal_button_press_event().connect(sigc::mem_fun(*this, &Item::handleClick));
event_box.signal_scroll_event().connect(sigc::mem_fun(*this, &Item::handleScroll));
event_box.signal_enter_notify_event().connect(sigc::mem_fun(*this, &Item::handleMouseEnter));
event_box.signal_leave_notify_event().connect(sigc::mem_fun(*this, &Item::handleMouseLeave));
// initial visibility
event_box.show_all();
event_box.set_visible(show_passive_);
@@ -69,6 +80,29 @@ Item::Item(const std::string& bn, const std::string& op, const Json::Value& conf
cancellable_, interface);
}
Item::~Item() {
if (this->gtk_menu != nullptr) {
this->gtk_menu->popdown();
this->gtk_menu->detach();
}
if (this->dbus_menu != nullptr) {
g_object_weak_unref(G_OBJECT(this->dbus_menu), (GWeakNotify)onMenuDestroyed, this);
this->dbus_menu = nullptr;
}
}
bool Item::isReady() const { return ready_; }
bool Item::handleMouseEnter(GdkEventCrossing* const& e) {
event_box.set_state_flags(Gtk::StateFlags::STATE_FLAG_PRELIGHT);
return false;
}
bool Item::handleMouseLeave(GdkEventCrossing* const& e) {
event_box.unset_state_flags(Gtk::StateFlags::STATE_FLAG_PRELIGHT);
return false;
}
void Item::onConfigure(GdkEventConfigure* ev) { this->updateImage(); }
void Item::proxyReady(Glib::RefPtr<Gio::AsyncResult>& result) {
@@ -86,14 +120,18 @@ void Item::proxyReady(Glib::RefPtr<Gio::AsyncResult>& result) {
if (this->id.empty() || this->category.empty()) {
spdlog::error("Invalid Status Notifier Item: {}, {}", bus_name, object_path);
invalidate();
return;
}
this->updateImage();
setReady();
} catch (const Glib::Error& err) {
spdlog::error("Failed to create DBus Proxy for {} {}: {}", bus_name, object_path, err.what());
invalidate();
} catch (const std::exception& err) {
spdlog::error("Failed to create DBus Proxy for {} {}: {}", bus_name, object_path, err.what());
invalidate();
}
}
@@ -111,7 +149,8 @@ ToolTip get_variant<ToolTip>(const Glib::VariantBase& value) {
result.text = get_variant<Glib::ustring>(container.get_child(2));
auto description = get_variant<Glib::ustring>(container.get_child(3));
if (!description.empty()) {
result.text = fmt::format("<b>{}</b>\n{}", result.text, description);
auto escapedDescription = Glib::Markup::escape_text(description);
result.text = fmt::format("<b>{}</b>\n{}", result.text, escapedDescription);
}
return result;
}
@@ -124,6 +163,25 @@ void Item::setProperty(const Glib::ustring& name, Glib::VariantBase& value) {
category = get_variant<std::string>(value);
} else if (name == "Id") {
id = get_variant<std::string>(value);
/*
* HACK: Electron apps seem to have the same ID, but tooltip seems correct, so use that as ID
* to pass as the custom icon option. I'm avoiding being disruptive and setting that to the ID
* itself as I've no idea what this would affect.
* The tooltip text is converted to lowercase since that's what (most?) themes expect?
* I still haven't found a way for it to pick from theme automatically, although
* it might be my theme.
*/
if (id == "chrome_status_icon_1") {
Glib::VariantBase value;
this->proxy_->get_cached_property(value, "ToolTip");
tooltip = get_variant<ToolTip>(value);
if (!tooltip.text.empty()) {
setCustomIcon(tooltip.text.lowercase());
}
} else {
setCustomIcon(id);
}
} else if (name == "Title") {
title = get_variant<std::string>(value);
if (tooltip.text.empty()) {
@@ -132,17 +190,25 @@ void Item::setProperty(const Glib::ustring& name, Glib::VariantBase& value) {
} else if (name == "Status") {
setStatus(get_variant<Glib::ustring>(value));
} else if (name == "IconName") {
icon_name = get_variant<std::string>(value);
if (has_custom_icon_) {
spdlog::trace("Item '{}': ignoring IconName update, custom icon is set", id);
} else {
icon_name = get_variant<std::string>(value);
}
} else if (name == "IconPixmap") {
icon_pixmap = this->extractPixBuf(value.gobj());
if (has_custom_icon_) {
spdlog::trace("Item '{}': ignoring IconPixmap update, custom icon is set", id);
} else {
icon_pixmap = this->extractPixBuf(value.gobj());
}
} else if (name == "OverlayIconName") {
overlay_icon_name = get_variant<std::string>(value);
} else if (name == "OverlayIconPixmap") {
// TODO: overlay_icon_pixmap
overlay_icon_pixmap = extractPixBuf(value.gobj());
} else if (name == "AttentionIconName") {
attention_icon_name = get_variant<std::string>(value);
} else if (name == "AttentionIconPixmap") {
// TODO: attention_icon_pixmap
attention_icon_pixmap = extractPixBuf(value.gobj());
} else if (name == "AttentionMovieName") {
attention_movie_name = get_variant<std::string>(value);
} else if (name == "ToolTip") {
@@ -171,18 +237,56 @@ void Item::setProperty(const Glib::ustring& name, Glib::VariantBase& value) {
}
void Item::setStatus(const Glib::ustring& value) {
Glib::ustring lower = value.lowercase();
event_box.set_visible(show_passive_ || lower.compare("passive") != 0);
status_ = value.lowercase();
event_box.set_visible(show_passive_ || status_.compare("passive") != 0);
auto style = event_box.get_style_context();
for (const auto& class_name : style->list_classes()) {
style->remove_class(class_name);
}
if (lower.compare("needsattention") == 0) {
auto css_class = status_;
if (css_class.compare("needsattention") == 0) {
// convert status to dash-case for CSS
lower = "needs-attention";
css_class = "needs-attention";
}
style->add_class(css_class);
on_updated_();
}
void Item::setReady() {
if (ready_) {
return;
}
ready_ = true;
on_ready_(*this);
}
void Item::invalidate() {
if (ready_) {
ready_ = false;
}
on_invalidate_(*this);
}
void Item::setCustomIcon(const std::string& id) {
spdlog::debug("SNI tray id: {}", id);
std::string custom_icon = IconManager::instance().getIconForApp(id);
if (!custom_icon.empty()) {
if (std::filesystem::exists(custom_icon)) {
try {
Glib::RefPtr<Gdk::Pixbuf> custom_pixbuf = Gdk::Pixbuf::create_from_file(custom_icon);
icon_name = ""; // icon_name has priority over pixmap
icon_pixmap = custom_pixbuf;
has_custom_icon_ = true;
} catch (const Glib::Error& e) {
spdlog::error("Failed to load custom icon {}: {}", custom_icon, e.what());
}
} else { // if file doesn't exist it's most likely an icon_name
icon_name = custom_icon;
has_custom_icon_ = true;
}
}
style->add_class(lower);
}
void Item::getUpdatedProperties() {
@@ -222,8 +326,8 @@ void Item::processUpdatedProperties(Glib::RefPtr<Gio::AsyncResult>& _result) {
static const std::map<std::string_view, std::set<std::string_view>> signal2props = {
{"NewTitle", {"Title"}},
{"NewIcon", {"IconName", "IconPixmap"}},
// {"NewAttentionIcon", {"AttentionIconName", "AttentionIconPixmap", "AttentionMovieName"}},
// {"NewOverlayIcon", {"OverlayIconName", "OverlayIconPixmap"}},
{"NewAttentionIcon", {"AttentionIconName", "AttentionIconPixmap", "AttentionMovieName"}},
{"NewOverlayIcon", {"OverlayIconName", "OverlayIconPixmap"}},
{"NewIconThemePath", {"IconThemePath"}},
{"NewToolTip", {"ToolTip"}},
{"NewStatus", {"Status"}},
@@ -271,11 +375,20 @@ Glib::RefPtr<Gdk::Pixbuf> Item::extractPixBuf(GVariant* variant) {
if (array != nullptr) {
g_free(array);
}
#if GLIB_MAJOR_VERSION >= 2 && GLIB_MINOR_VERSION >= 68
array = static_cast<guchar*>(g_memdup2(data, size));
#else
array = static_cast<guchar*>(g_memdup(data, size));
#endif
// We must allocate our own array because the data from GVariant is read-only
// and we need to modify it to convert ARGB to RGBA.
array = static_cast<guchar*>(g_malloc(size));
// Copy and convert ARGB to RGBA in one pass to avoid g_memdup2 overhead
const guchar* src = static_cast<const guchar*>(data);
for (gsize i = 0; i < size; i += 4) {
guchar alpha = src[i];
array[i] = src[i + 1];
array[i + 1] = src[i + 2];
array[i + 2] = src[i + 3];
array[i + 3] = alpha;
}
lwidth = width;
lheight = height;
}
@@ -284,14 +397,6 @@ Glib::RefPtr<Gdk::Pixbuf> Item::extractPixBuf(GVariant* variant) {
}
g_variant_iter_free(it);
if (array != nullptr) {
/* argb to rgba */
for (uint32_t i = 0; i < 4U * lwidth * lheight; i += 4) {
guchar alpha = array[i];
array[i] = array[i + 1];
array[i + 1] = array[i + 2];
array[i + 2] = array[i + 3];
array[i + 3] = alpha;
}
return Gdk::Pixbuf::create_from_data(array, Gdk::Colorspace::COLORSPACE_RGB, true, 8, lwidth,
lheight, 4 * lwidth, &pixbuf_data_deleter);
}
@@ -300,47 +405,36 @@ Glib::RefPtr<Gdk::Pixbuf> Item::extractPixBuf(GVariant* variant) {
void Item::updateImage() {
auto pixbuf = getIconPixbuf();
if (!pixbuf) return;
auto scaled_icon_size = getScaledIconSize();
// If the loaded icon is not square, assume that the icon height should match the
// requested icon size, but the width is allowed to be different. As such, if the
// height of the image does not match the requested icon size, resize the icon such that
// the aspect ratio is maintained, but the height matches the requested icon size.
if (pixbuf->get_height() != scaled_icon_size) {
if (pixbuf->get_height() > 0 && pixbuf->get_height() != scaled_icon_size) {
int width = scaled_icon_size * pixbuf->get_width() / pixbuf->get_height();
pixbuf = pixbuf->scale_simple(width, scaled_icon_size, Gdk::InterpType::INTERP_BILINEAR);
}
pixbuf = overlayPixbufs(pixbuf, getOverlayIconPixbuf());
auto surface =
Gdk::Cairo::create_surface_from_pixbuf(pixbuf, image.get_scale_factor(), image.get_window());
image.set(surface);
}
Glib::RefPtr<Gdk::Pixbuf> Item::getIconPixbuf() {
if (!icon_name.empty()) {
try {
std::ifstream temp(icon_name);
if (temp.is_open()) {
return Gdk::Pixbuf::create_from_file(icon_name);
}
} catch (Glib::Error& e) {
// Ignore because we want to also try different methods of getting an icon.
//
// But a warning is logged, as the file apparently exists, but there was
// a failure in creating a pixbuf out of it.
spdlog::warn("Item '{}': {}", id, static_cast<std::string>(e.what()));
}
try {
// Will throw if it can not find an icon.
return getIconByName(icon_name, getScaledIconSize());
} catch (Glib::Error& e) {
spdlog::trace("Item '{}': {}", id, static_cast<std::string>(e.what()));
if (status_ == "needsattention") {
if (auto attention_pixbuf = getAttentionIconPixbuf()) {
return attention_pixbuf;
}
}
// Return the pixmap only if an icon for the given name could not be found.
if (auto pixbuf = loadIconFromNameOrFile(icon_name, true)) {
return pixbuf;
}
if (icon_pixmap) {
return icon_pixmap;
}
@@ -355,17 +449,89 @@ Glib::RefPtr<Gdk::Pixbuf> Item::getIconPixbuf() {
return getIconByName("image-missing", getScaledIconSize());
}
Glib::RefPtr<Gdk::Pixbuf> Item::getIconByName(const std::string& name, int request_size) {
icon_theme->rescan_if_needed();
Glib::RefPtr<Gdk::Pixbuf> Item::getAttentionIconPixbuf() {
if (auto pixbuf = loadIconFromNameOrFile(attention_icon_name, false)) {
return pixbuf;
}
if (auto pixbuf = loadIconFromNameOrFile(attention_movie_name, false)) {
return pixbuf;
}
return attention_icon_pixmap;
}
if (!icon_theme_path.empty() &&
icon_theme->lookup_icon(name.c_str(), request_size,
Gtk::IconLookupFlags::ICON_LOOKUP_FORCE_SIZE)) {
return icon_theme->load_icon(name.c_str(), request_size,
Gtk::IconLookupFlags::ICON_LOOKUP_FORCE_SIZE);
Glib::RefPtr<Gdk::Pixbuf> Item::getOverlayIconPixbuf() {
if (auto pixbuf = loadIconFromNameOrFile(overlay_icon_name, false)) {
return pixbuf;
}
return overlay_icon_pixmap;
}
Glib::RefPtr<Gdk::Pixbuf> Item::loadIconFromNameOrFile(const std::string& name, bool log_failure) {
if (name.empty()) {
return {};
}
try {
std::ifstream temp(name);
if (temp.is_open()) {
return Gdk::Pixbuf::create_from_file(name);
}
} catch (const Glib::Error& e) {
if (log_failure) {
spdlog::warn("Item '{}': {}", id, static_cast<std::string>(e.what()));
}
}
try {
return getIconByName(name, getScaledIconSize());
} catch (const Glib::Error& e) {
if (log_failure) {
spdlog::trace("Item '{}': {}", id, static_cast<std::string>(e.what()));
}
}
return {};
}
Glib::RefPtr<Gdk::Pixbuf> Item::overlayPixbufs(const Glib::RefPtr<Gdk::Pixbuf>& base,
const Glib::RefPtr<Gdk::Pixbuf>& overlay) {
if (!base || !overlay) {
return base;
}
auto composed = base->copy();
if (!composed) {
return base;
}
int overlay_target_size =
std::max(1, std::min(composed->get_width(), composed->get_height()) / 2);
auto scaled_overlay = overlay->scale_simple(overlay_target_size, overlay_target_size,
Gdk::InterpType::INTERP_BILINEAR);
if (!scaled_overlay) {
return composed;
}
int dest_x = std::max(0, composed->get_width() - scaled_overlay->get_width());
int dest_y = std::max(0, composed->get_height() - scaled_overlay->get_height());
scaled_overlay->composite(composed, dest_x, dest_y, scaled_overlay->get_width(),
scaled_overlay->get_height(), dest_x, dest_y, 1.0, 1.0,
Gdk::InterpType::INTERP_BILINEAR, 255);
return composed;
}
Glib::RefPtr<Gdk::Pixbuf> Item::getIconByName(const std::string& name, int request_size) {
if (!icon_theme_path.empty()) {
auto icon_info = icon_theme->lookup_icon(name.c_str(), request_size,
Gtk::IconLookupFlags::ICON_LOOKUP_FORCE_SIZE);
if (icon_info) {
bool is_sym = false;
return icon_info.load_symbolic(event_box.get_style_context(), is_sym);
}
}
return DefaultGtkIconThemeWrapper::load_icon(name.c_str(), request_size,
Gtk::IconLookupFlags::ICON_LOOKUP_FORCE_SIZE);
Gtk::IconLookupFlags::ICON_LOOKUP_FORCE_SIZE,
event_box.get_style_context());
}
double Item::getScaledIconSize() {
@@ -390,9 +556,15 @@ void Item::makeMenu() {
gtk_menu->attach_to_widget(event_box);
}
}
// Manually reset prelight to make sure the tray item doesn't stay in a hover state even though
// the menu is focused
event_box.unset_state_flags(Gtk::StateFlags::STATE_FLAG_PRELIGHT);
}
bool Item::handleClick(GdkEventButton* const& ev) {
if (!proxy_) {
return false;
}
auto parameters = Glib::VariantContainerBase::create_tuple(
{Glib::Variant<int>::create(ev->x_root + bar_.x_global),
Glib::Variant<int>::create(ev->y_root + bar_.y_global)});
@@ -420,6 +592,9 @@ bool Item::handleClick(GdkEventButton* const& ev) {
}
bool Item::handleScroll(GdkEventScroll* const& ev) {
if (!proxy_) {
return false;
}
int dx = 0, dy = 0;
switch (ev->direction) {
case GDK_SCROLL_UP:
+53 -5
View File
@@ -2,14 +2,37 @@
#include <spdlog/spdlog.h>
#include <algorithm>
#include "modules/sni/icon_manager.hpp"
namespace waybar::modules::SNI {
std::vector<std::string> Tray::parseIgnoreList(const Json::Value& config) {
std::vector<std::string> ignore_list;
if (config["ignore-list"].isArray()) {
spdlog::info("Tray: Found ignore-list with {} items", config["ignore-list"].size());
for (const auto& item : config["ignore-list"]) {
if (item.isString()) {
ignore_list.push_back(item.asString());
spdlog::info("Tray: Adding to ignore list: {}", item.asString());
}
}
} else {
spdlog::info("Tray: No ignore-list configured");
}
return ignore_list;
}
Tray::Tray(const std::string& id, const Bar& bar, const Json::Value& config)
: AModule(config, "tray", id),
box_(bar.orientation, 0),
watcher_(SNI::Watcher::getInstance()),
host_(nb_hosts_, config, bar, std::bind(&Tray::onAdd, this, std::placeholders::_1),
std::bind(&Tray::onRemove, this, std::placeholders::_1)) {
ignore_list_(parseIgnoreList(config)),
host_(nb_hosts_, config, bar, ignore_list_,
std::bind(&Tray::onAdd, this, std::placeholders::_1),
std::bind(&Tray::onRemove, this, std::placeholders::_1),
std::bind(&Tray::queueUpdate, this)) {
box_.set_name("tray");
event_box_.add(box_);
if (!id.empty()) {
@@ -20,15 +43,34 @@ Tray::Tray(const std::string& id, const Bar& bar, const Json::Value& config)
box_.set_spacing(config_["spacing"].asUInt());
}
nb_hosts_ += 1;
if (config_["icons"].isObject()) {
IconManager::instance().setIconsConfig(config_["icons"]);
}
dp.emit();
}
void Tray::checkIgnoreList(std::unique_ptr<Item>* item_ptr) {
// Delegate to Host's checkIgnoreList method
host_.checkIgnoreList(ignore_list_, std::bind(&Tray::onRemove, this, std::placeholders::_1));
}
void Tray::queueUpdate() { dp.emit(); }
void Tray::onAdd(std::unique_ptr<Item>& item) {
spdlog::info("Tray::onAdd - item bus_name='{}', category='{}', icon_name='{}', title='{}'",
item->bus_name, item->category, item->icon_name, item->title);
if (config_["reverse-direction"].isBool() && config_["reverse-direction"].asBool()) {
box_.pack_end(item->event_box);
} else {
box_.pack_start(item->event_box);
}
spdlog::debug("Tray::onAdd deferred check - checking ignore list");
host_.checkIgnoreList(ignore_list_, std::bind(&Tray::onRemove, this, std::placeholders::_1));
item->event_box.signal_show().connect([this] { dp.emit(); });
item->event_box.signal_hide().connect([this] { dp.emit(); });
dp.emit();
}
@@ -38,9 +80,15 @@ void Tray::onRemove(std::unique_ptr<Item>& item) {
}
auto Tray::update() -> void {
// Show tray only when items are available
event_box_.set_visible(!box_.get_children().empty());
// Call parent update
// Check if any items should be ignored now that properties have loaded
if (!ignore_list_.empty()) {
spdlog::debug("Tray::update() - checking ignore list");
host_.checkIgnoreList(ignore_list_, std::bind(&Tray::onRemove, this, std::placeholders::_1));
}
std::vector<Gtk::Widget*> children = box_.get_children();
event_box_.set_visible(std::any_of(children.begin(), children.end(),
[](Gtk::Widget* child) { return child->get_visible(); }));
AModule::update();
}
+8 -8
View File
@@ -31,7 +31,7 @@ Watcher::~Watcher() {
void Watcher::busAcquired(const Glib::RefPtr<Gio::DBus::Connection>& conn, Glib::ustring name) {
GError* error = nullptr;
waybar::util::ScopeGuard error_deleter([error]() {
waybar::util::ScopeGuard error_deleter([&error]() {
if (error) {
g_error_free(error);
}
@@ -67,10 +67,9 @@ gboolean Watcher::handleRegisterHost(Watcher* obj, GDBusMethodInvocation* invoca
}
auto watch = gfWatchFind(obj->hosts_, bus_name, object_path);
if (watch != nullptr) {
g_dbus_method_invocation_return_error(
invocation, G_DBUS_ERROR, G_DBUS_ERROR_INVALID_ARGS,
"Status Notifier Host with bus name '%s' and object path '%s' is already registered",
bus_name, object_path);
g_warning("Status Notifier Host with bus name '%s' and object path '%s' is already registered",
bus_name, object_path);
sn_watcher_complete_register_host(obj->watcher_, invocation);
return TRUE;
}
watch = gfWatchNew(GF_WATCH_TYPE_HOST, service, bus_name, object_path, obj);
@@ -99,8 +98,8 @@ gboolean Watcher::handleRegisterItem(Watcher* obj, GDBusMethodInvocation* invoca
}
auto watch = gfWatchFind(obj->items_, bus_name, object_path);
if (watch != nullptr) {
g_warning("Status Notifier Item with bus name '%s' and object path '%s' is already registered",
bus_name, object_path);
spdlog::debug("Ignoring duplicate Status Notifier Item registration for '{}' at '{}'", bus_name,
object_path);
sn_watcher_complete_register_item(obj->watcher_, invocation);
return TRUE;
}
@@ -159,7 +158,7 @@ void Watcher::nameVanished(GDBusConnection* connection, const char* name, gpoint
watch->watcher->hosts_ = g_slist_remove(watch->watcher->hosts_, watch);
if (watch->watcher->hosts_ == nullptr) {
sn_watcher_set_is_host_registered(watch->watcher->watcher_, FALSE);
sn_watcher_emit_host_registered(watch->watcher->watcher_);
sn_watcher_emit_host_unregistered(watch->watcher->watcher_);
}
} else if (watch->type == GF_WATCH_TYPE_ITEM) {
watch->watcher->items_ = g_slist_remove(watch->watcher->items_, watch);
@@ -168,6 +167,7 @@ void Watcher::nameVanished(GDBusConnection* connection, const char* name, gpoint
sn_watcher_emit_item_unregistered(watch->watcher->watcher_, tmp);
g_free(tmp);
}
gfWatchFree(watch);
}
void Watcher::updateRegisteredItems(SnWatcher* obj) {
+1 -1
View File
@@ -60,7 +60,7 @@ BarIpcClient::BarIpcClient(waybar::Bar& bar) : bar_{bar} {
});
}
bool BarIpcClient::isModuleEnabled(std::string name) {
bool BarIpcClient::isModuleEnabled(const std::string& name) {
for (const auto& section : {"modules-left", "modules-center", "modules-right"}) {
if (const auto& modules = bar_.config.get(section, {}); modules.isArray()) {
for (const auto& module : modules) {
+103 -59
View File
@@ -2,15 +2,48 @@
#include <fcntl.h>
#include <spdlog/spdlog.h>
#include <sys/socket.h>
#include <sys/types.h>
#include <sys/un.h>
#include <unistd.h>
#include <cerrno>
#include <cstdio>
#include <cstdlib>
#include <cstring>
#include <limits>
#include <stdexcept>
#include <string_view>
#include <utility>
#include "modules/sway/ipc/ipc.hpp"
namespace waybar::modules::sway {
namespace {
void sendAll(int fd, const char* data, size_t size, const char* what) {
size_t total = 0;
while (total < size) {
const auto res = ::send(fd, data + total, size - total, 0);
if (res < 0) {
if (errno == EINTR || errno == EAGAIN) {
continue;
}
throw std::runtime_error(what);
}
if (res == 0) {
throw std::runtime_error(what);
}
total += static_cast<size_t>(res);
}
}
} // namespace
Ipc::Ipc() {
const std::string& socketPath = getSocketPath();
fd_ = open(socketPath);
fd_event_ = open(socketPath);
const std::string socketPath = getSocketPath();
fd_ = util::ScopedFd(open(socketPath));
fd_event_ = util::ScopedFd(open(socketPath));
}
Ipc::~Ipc() {
@@ -21,118 +54,129 @@ Ipc::~Ipc() {
if (write(fd_, "close-sway-ipc", 14) == -1) {
spdlog::error("Failed to close sway IPC");
}
close(fd_);
fd_ = -1;
}
if (fd_event_ > 0) {
if (write(fd_event_, "close-sway-ipc", 14) == -1) {
spdlog::error("Failed to close sway IPC event handler");
}
close(fd_event_);
fd_event_ = -1;
}
}
void Ipc::setWorker(std::function<void()>&& func) { thread_ = func; }
void Ipc::setWorker(std::function<void()>&& func) { thread_ = std::move(func); }
const std::string Ipc::getSocketPath() const {
std::string Ipc::getSocketPath() {
const char* env = getenv("SWAYSOCK");
if (env != nullptr) {
return std::string(env);
if (env != nullptr && env[0] != '\0') {
return {env};
}
FILE* in = popen("sway --get-socketpath 2>/dev/null", "r");
if (in == nullptr) {
throw std::runtime_error("Failed to get socket path");
}
std::string str;
{
std::string str_buf;
FILE* in;
char buf[512] = {0};
if ((in = popen("sway --get-socketpath 2>/dev/null", "r")) == nullptr) {
throw std::runtime_error("Failed to get socket path");
}
while (fgets(buf, sizeof(buf), in) != nullptr) {
str_buf.append(buf, sizeof(buf));
}
pclose(in);
str = str_buf;
if (str.empty()) {
throw std::runtime_error("Socket path is empty");
}
char buf[512] = {0};
while (fgets(buf, sizeof(buf), in) != nullptr) {
str.append(buf);
}
if (str.back() == '\n') {
if (pclose(in) == -1) {
throw std::runtime_error("Failed to get socket path");
}
if (str.ends_with('\n')) {
str.pop_back();
}
if (str.empty()) {
throw std::runtime_error("Socket path is empty");
}
return str;
}
int Ipc::open(const std::string& socketPath) const {
int32_t fd = socket(AF_UNIX, SOCK_STREAM, 0);
int Ipc::open(const std::string& socketPath) {
util::ScopedFd fd(socket(AF_UNIX, SOCK_STREAM, 0));
if (fd == -1) {
throw std::runtime_error("Unable to open Unix socket");
}
(void)fcntl(fd, F_SETFD, FD_CLOEXEC);
struct sockaddr_un addr;
memset(&addr, 0, sizeof(struct sockaddr_un));
addr.sun_family = AF_UNIX;
struct sockaddr_un addr{.sun_family = AF_UNIX};
strncpy(addr.sun_path, socketPath.c_str(), sizeof(addr.sun_path) - 1);
addr.sun_path[sizeof(addr.sun_path) - 1] = 0;
int l = sizeof(struct sockaddr_un);
if (::connect(fd, reinterpret_cast<struct sockaddr*>(&addr), l) == -1) {
if (::connect(fd, reinterpret_cast<struct sockaddr*>(&addr), sizeof addr) == -1) {
throw std::runtime_error("Unable to connect to Sway");
}
return fd;
return fd.release();
}
struct Ipc::ipc_response Ipc::recv(int fd) {
std::string header;
header.resize(ipc_header_size_);
auto data32 = reinterpret_cast<uint32_t*>(header.data() + ipc_magic_.size());
size_t total = 0;
size_t total = 0;
while (total < ipc_header_size_) {
auto res = ::recv(fd, header.data() + total, ipc_header_size_ - total, 0);
const ssize_t res = ::recv(fd, header.data() + total, ipc_header_size_ - total, 0);
if (fd_event_ == -1 || fd_ == -1) {
// IPC is closed so just return an empty response
return {0, 0, ""};
return {.size = 0, .type = 0, .payload = ""};
}
if (res <= 0) {
if (res < 0) {
if (errno == EINTR || errno == EAGAIN) {
continue;
}
throw std::runtime_error("Unable to receive IPC header");
}
total += res;
if (res == 0) {
throw std::runtime_error("Unable to receive IPC header");
}
total += static_cast<size_t>(res);
}
auto magic = std::string(header.data(), header.data() + ipc_magic_.size());
if (magic != ipc_magic_) {
if (std::string_view(header.data(), ipc_magic_.size()) != ipc_magic_) {
throw std::runtime_error("Invalid IPC magic");
}
total = 0;
uint32_t payload_size = 0;
uint32_t payload_type = 0;
memcpy(&payload_size, header.data() + ipc_magic_.size(), sizeof payload_size);
memcpy(&payload_type, header.data() + ipc_magic_.size() + sizeof payload_size,
sizeof payload_type);
std::string payload;
payload.resize(data32[0]);
while (total < data32[0]) {
auto res = ::recv(fd, payload.data() + total, data32[0] - total, 0);
payload.resize(payload_size);
total = 0;
while (total < payload_size) {
const ssize_t res = ::recv(fd, payload.data() + total, payload_size - total, 0);
if (res < 0) {
if (errno == EINTR || errno == EAGAIN) {
continue;
}
throw std::runtime_error("Unable to receive IPC payload");
}
total += res;
if (res == 0) {
throw std::runtime_error("Unable to receive IPC payload");
}
total += static_cast<size_t>(res);
}
return {data32[0], data32[1], &payload.front()};
return {.size = payload_size, .type = payload_type, .payload = std::move(payload)};
}
struct Ipc::ipc_response Ipc::send(int fd, uint32_t type, const std::string& payload) {
std::string header;
header.resize(ipc_header_size_);
auto data32 = reinterpret_cast<uint32_t*>(header.data() + ipc_magic_.size());
memcpy(header.data(), ipc_magic_.c_str(), ipc_magic_.size());
data32[0] = payload.size();
data32[1] = type;
memcpy(header.data(), ipc_magic_.data(), ipc_magic_.size());
if (payload.size() > std::numeric_limits<uint32_t>::max()) {
throw std::runtime_error("IPC payload is too large");
}
const auto payload_size = static_cast<uint32_t>(payload.size());
memcpy(header.data() + ipc_magic_.size(), &payload_size, sizeof payload_size);
memcpy(header.data() + ipc_magic_.size() + sizeof payload_size, &type, sizeof type);
sendAll(fd, header.data(), ipc_header_size_, "Unable to send IPC header");
sendAll(fd, payload.data(), payload.size(), "Unable to send IPC payload");
if (::send(fd, header.data(), ipc_header_size_, 0) == -1) {
throw std::runtime_error("Unable to send IPC header");
}
if (::send(fd, payload.c_str(), payload.size(), 0) == -1) {
throw std::runtime_error("Unable to send IPC payload");
}
return Ipc::recv(fd);
}
+48 -21
View File
@@ -22,10 +22,10 @@ Language::Language(const std::string& id, const Json::Value& config)
hide_single_ = config["hide-single-layout"].isBool() && config["hide-single-layout"].asBool();
is_variant_displayed = format_.find("{variant}") != std::string::npos;
if (format_.find("{}") != std::string::npos || format_.find("{short}") != std::string::npos) {
displayed_short_flag |= static_cast<std::byte>(DispayedShortFlag::ShortName);
displayed_short_flag |= static_cast<std::byte>(DisplayedShortFlag::ShortName);
}
if (format_.find("{shortDescription}") != std::string::npos) {
displayed_short_flag |= static_cast<std::byte>(DispayedShortFlag::ShortDescription);
displayed_short_flag |= static_cast<std::byte>(DisplayedShortFlag::ShortDescription);
}
if (config.isMember("tooltip-format")) {
tooltip_format_ = config["tooltip-format"].asString();
@@ -54,21 +54,28 @@ void Language::onCmd(const struct Ipc::ipc_response& res) {
std::lock_guard<std::mutex> lock(mutex_);
auto payload = parser_.parse(res.payload);
std::vector<std::string> used_layouts;
// Display current layout of a device with a maximum count of layouts, expecting that all will
// be OK
// Union layout names across every keyboard input so hot-plugged devices contribute their
// layouts to the map. Track the device with the most layouts to seed the initially displayed
// layout, matching the previous behaviour at startup.
Json::ArrayIndex max_id = 0, max = 0;
for (Json::ArrayIndex i = 0; i < payload.size(); i++) {
auto size = payload[i][XKB_LAYOUT_NAMES_KEY].size();
if (size > max) {
max = size;
if (payload[i]["type"].asString() != "keyboard") continue;
const auto& names = payload[i][XKB_LAYOUT_NAMES_KEY];
if (names.size() > max) {
max = names.size();
max_id = i;
}
for (const auto& layout : names) {
const auto name = layout.asString();
if (std::find(used_layouts.begin(), used_layouts.end(), name) == used_layouts.end()) {
used_layouts.push_back(name);
}
}
}
for (const auto& layout : payload[max_id][XKB_LAYOUT_NAMES_KEY]) {
used_layouts.push_back(layout.asString());
}
// Rebuild from scratch so init_layouts_map's duplicate-suffix pass doesn't compound across
// refreshes (e.g. "us" -> "us1" -> "us11").
layouts_map_.clear();
init_layouts_map(used_layouts);
set_current_layout(payload[max_id][XKB_ACTIVE_LAYOUT_NAME_KEY].asString());
dp.emit();
@@ -82,13 +89,26 @@ void Language::onEvent(const struct Ipc::ipc_response& res) {
return;
}
bool refresh_inputs = false;
try {
std::lock_guard<std::mutex> lock(mutex_);
auto payload = parser_.parse(res.payload)["input"];
if (payload["type"].asString() == "keyboard") {
set_current_layout(payload[XKB_ACTIVE_LAYOUT_NAME_KEY].asString());
{
std::lock_guard<std::mutex> lock(mutex_);
auto root = parser_.parse(res.payload);
auto change = root["change"].asString();
auto payload = root["input"];
if (payload["type"].asString() == "keyboard") {
// A device was added or its keymap changed - the layout set may have grown, so refresh
// layouts_map_ via IPC_GET_INPUTS once we've released mutex_.
refresh_inputs = (change == "added" || change == "xkb_keymap");
set_current_layout(payload[XKB_ACTIVE_LAYOUT_NAME_KEY].asString());
}
dp.emit();
}
// sendCmd is synchronous: it blocks on the IPC reply and then emits signal_cmd on this same
// thread, which lands in onCmd and re-locks mutex_. Must call it with mutex_ released.
if (refresh_inputs) {
ipc_.sendCmd(IPC_GET_INPUTS);
}
dp.emit();
} catch (const std::exception& e) {
spdlog::error("Language: {}", e.what());
}
@@ -104,7 +124,7 @@ auto Language::update() -> void {
fmt::runtime(format_), fmt::arg("short", layout_.short_name),
fmt::arg("shortDescription", layout_.short_description), fmt::arg("long", layout_.full_name),
fmt::arg("variant", layout_.variant), fmt::arg("flag", layout_.country_flag())));
label_.set_markup(display_layout);
setLabelMarkup(display_layout);
if (tooltipEnabled()) {
if (tooltip_format_ != "") {
auto tooltip_display_layout = trim(
@@ -112,9 +132,9 @@ auto Language::update() -> void {
fmt::arg("shortDescription", layout_.short_description),
fmt::arg("long", layout_.full_name), fmt::arg("variant", layout_.variant),
fmt::arg("flag", layout_.country_flag())));
label_.set_tooltip_markup(tooltip_display_layout);
setTooltipMarkup(tooltip_display_layout);
} else {
label_.set_tooltip_markup(display_layout);
setTooltipMarkup(display_layout);
}
}
@@ -124,9 +144,16 @@ auto Language::update() -> void {
ALabel::update();
}
auto Language::set_current_layout(std::string current_layout) -> void {
auto Language::set_current_layout(const std::string& current_layout) -> void {
// Guard against unknown / empty layout names: transient virtual keyboards (e.g. wtype) and
// hot-plugged devices whose layouts haven't made it into the map yet would otherwise blank out
// layout_ via map::operator[]'s default-construct-on-miss.
auto it = layouts_map_.find(current_layout);
if (it == layouts_map_.end()) {
return;
}
label_.get_style_context()->remove_class(layout_.short_name);
layout_ = layouts_map_[current_layout];
layout_ = it->second;
label_.get_style_context()->add_class(layout_.short_name);
}
+1 -1
View File
@@ -44,7 +44,7 @@ auto Mode::update() -> void {
} else {
label_.set_markup(fmt::format(fmt::runtime(format_), mode_));
if (tooltipEnabled()) {
label_.set_tooltip_text(mode_);
label_.set_tooltip_markup(mode_);
}
event_box_.show();
}
+2 -2
View File
@@ -31,12 +31,12 @@ Scratchpad::Scratchpad(const std::string& id, const Json::Value& config)
auto Scratchpad::update() -> void {
if (count_ || show_empty_) {
event_box_.show();
label_.set_markup(
setLabelMarkup(
fmt::format(fmt::runtime(format_),
fmt::arg("icon", getIcon(count_, "", config_["format-icons"].size())),
fmt::arg("count", count_)));
if (tooltip_enabled_) {
label_.set_tooltip_markup(tooltip_text_);
setTooltipMarkup(tooltip_text_);
}
} else {
event_box_.hide();
+41 -20
View File
@@ -41,8 +41,8 @@ void Window::onCmd(const struct Ipc::ipc_response& res) {
std::lock_guard<std::mutex> lock(mutex_);
auto payload = parser_.parse(res.payload);
auto output = payload["output"].isString() ? payload["output"].asString() : "";
std::tie(app_nb_, floating_count_, windowId_, window_, app_id_, app_class_, shell_, layout_) =
getFocusedNode(payload["nodes"], output);
std::tie(app_nb_, floating_count_, windowId_, window_, app_id_, app_class_, shell_, layout_,
marks_) = getFocusedNode(payload["nodes"], output);
updateAppIconName(app_id_, app_class_);
dp.emit();
} catch (const std::exception& e) {
@@ -94,12 +94,12 @@ auto Window::update() -> void {
old_app_id_ = app_id_;
}
label_.set_markup(waybar::util::rewriteString(
setLabelMarkup(waybar::util::rewriteString(
fmt::format(fmt::runtime(format_), fmt::arg("title", window_), fmt::arg("app_id", app_id_),
fmt::arg("shell", shell_)),
fmt::arg("shell", shell_), fmt::arg("marks", marks_)),
config_["rewrite"]));
if (tooltipEnabled()) {
label_.set_tooltip_text(window_);
setTooltipMarkup(window_);
}
updateAppIcon();
@@ -108,7 +108,7 @@ auto Window::update() -> void {
AAppIconLabel::update();
}
void Window::setClass(std::string classname, bool enable) {
void Window::setClass(const std::string& classname, bool enable) {
if (enable) {
if (!bar_.window.get_style_context()->has_class(classname)) {
bar_.window.get_style_context()->add_class(classname);
@@ -169,17 +169,31 @@ std::optional<std::reference_wrapper<const Json::Value>> getSingleChildNode(
return {getSingleChildNode(child)};
}
std::tuple<std::string, std::string, std::string> getWindowInfo(const Json::Value& node) {
std::tuple<std::string, std::string, std::string, std::string> getWindowInfo(
const Json::Value& node, bool showHidden) {
const auto app_id = node["app_id"].isString() ? node["app_id"].asString()
: node["window_properties"]["instance"].asString();
const auto app_class = node["window_properties"]["class"].isString()
? node["window_properties"]["class"].asString()
: "";
const auto shell = node["shell"].isString() ? node["shell"].asString() : "";
return {app_id, app_class, shell};
std::string marks = "";
if (node["marks"].isArray()) {
for (const auto& m : node["marks"]) {
if (!m.isString() || (!showHidden && m.asString().at(0) == '_')) {
continue;
}
if (!marks.empty()) {
marks.append(",");
}
marks.append(m.asString());
}
}
return {app_id, app_class, shell, marks};
}
std::tuple<std::size_t, int, int, std::string, std::string, std::string, std::string, std::string>
std::tuple<std::size_t, int, int, std::string, std::string, std::string, std::string, std::string,
std::string>
gfnWithWorkspace(const Json::Value& nodes, std::string& output, const Json::Value& config_,
const Bar& bar_, Json::Value& parentWorkspace,
const Json::Value& immediateParent) {
@@ -207,7 +221,8 @@ gfnWithWorkspace(const Json::Value& nodes, std::string& output, const Json::Valu
"",
"",
"",
node["layout"].asString()};
node["layout"].asString(),
""};
}
parentWorkspace = node;
} else if ((node["type"].asString() == "con" || node["type"].asString() == "floating_con") &&
@@ -215,7 +230,8 @@ gfnWithWorkspace(const Json::Value& nodes, std::string& output, const Json::Valu
// found node
spdlog::trace("actual output {}, output found {}, node (focused) found {}", bar_.output->name,
output, node["name"].asString());
const auto [app_id, app_class, shell] = getWindowInfo(node);
const auto [app_id, app_class, shell, marks] =
getWindowInfo(node, config_["show-hidden-marks"].asBool());
int nb = node.size();
int floating_count = 0;
std::string workspace_layout = "";
@@ -232,20 +248,21 @@ gfnWithWorkspace(const Json::Value& nodes, std::string& output, const Json::Valu
app_id,
app_class,
shell,
workspace_layout};
workspace_layout,
marks};
}
// iterate
auto [nb, f, id, name, app_id, app_class, shell, workspace_layout] =
auto [nb, f, id, name, app_id, app_class, shell, workspace_layout, marks] =
gfnWithWorkspace(node["nodes"], output, config_, bar_, parentWorkspace, node);
auto [nb2, f2, id2, name2, app_id2, app_class2, shell2, workspace_layout2] =
auto [nb2, f2, id2, name2, app_id2, app_class2, shell2, workspace_layout2, marks2] =
gfnWithWorkspace(node["floating_nodes"], output, config_, bar_, parentWorkspace, node);
// if ((id > 0 || ((id2 < 0 || name2.empty()) && id > -1)) && !name.empty()) {
if ((id > 0) || (id2 < 0 && id > -1)) {
return {nb, f, id, name, app_id, app_class, shell, workspace_layout};
return {nb, f, id, name, app_id, app_class, shell, workspace_layout, marks};
} else if (id2 > 0 && !name2.empty()) {
return {nb2, f2, id2, name2, app_id2, app_class, shell2, workspace_layout2};
return {nb2, f2, id2, name2, app_id2, app_class, shell2, workspace_layout2, marks2};
}
}
@@ -258,10 +275,12 @@ gfnWithWorkspace(const Json::Value& nodes, std::string& output, const Json::Valu
std::string app_id = "";
std::string app_class = "";
std::string workspace_layout = "";
std::string marks = "";
if (all_leaf_nodes.first == 1) {
const auto single_child = getSingleChildNode(immediateParent);
if (single_child.has_value()) {
std::tie(app_id, app_class, workspace_layout) = getWindowInfo(single_child.value());
std::tie(app_id, app_class, workspace_layout, marks) =
getWindowInfo(single_child.value(), config_["show-hidden-marks"].asBool());
}
}
return {all_leaf_nodes.first,
@@ -273,13 +292,15 @@ gfnWithWorkspace(const Json::Value& nodes, std::string& output, const Json::Valu
app_id,
app_class,
workspace_layout,
immediateParent["layout"].asString()};
immediateParent["layout"].asString(),
marks};
}
return {0, 0, -1, "", "", "", "", ""};
return {0, 0, -1, "", "", "", "", "", ""};
}
std::tuple<std::size_t, int, int, std::string, std::string, std::string, std::string, std::string>
std::tuple<std::size_t, int, int, std::string, std::string, std::string, std::string, std::string,
std::string>
Window::getFocusedNode(const Json::Value& nodes, std::string& output) {
Json::Value placeholder = Json::Value::null;
return gfnWithWorkspace(nodes, output, config_, bar_, placeholder, placeholder);
+206 -105
View File
@@ -10,21 +10,20 @@ namespace waybar::modules::sway {
// Helper function to assign a number to a workspace, just like sway. In fact
// this is taken quite verbatim from `sway/ipc-json.c`.
int Workspaces::convertWorkspaceNameToNum(std::string name) {
if (isdigit(name[0])) {
int Workspaces::convertWorkspaceNameToNum(const std::string& name) {
if (isdigit(name[0]) != 0) {
errno = 0;
char *endptr = NULL;
char* endptr = nullptr;
long long parsed_num = strtoll(name.c_str(), &endptr, 10);
if (errno != 0 || parsed_num > INT32_MAX || parsed_num < 0 || endptr == name.c_str()) {
return -1;
} else {
return (int)parsed_num;
}
return (int)parsed_num;
}
return -1;
}
int Workspaces::windowRewritePriorityFunction(std::string const &window_rule) {
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;
@@ -42,12 +41,12 @@ int Workspaces::windowRewritePriorityFunction(std::string const &window_rule) {
return 0;
}
Workspaces::Workspaces(const std::string &id, const Bar &bar, const Json::Value &config)
Workspaces::Workspaces(const std::string& id, const Bar& bar, const Json::Value& config)
: AModule(config, "workspaces", id, false, !config["disable-scroll"].asBool()),
bar_(bar),
box_(bar.orientation, 0) {
if (config["format-icons"]["high-priority-named"].isArray()) {
for (auto &it : config["format-icons"]["high-priority-named"]) {
for (const auto& it : config["format-icons"]["high-priority-named"]) {
high_priority_named_.push_back(it.asString());
}
}
@@ -58,26 +57,26 @@ Workspaces::Workspaces(const std::string &id, const Bar &bar, const Json::Value
box_.get_style_context()->add_class(MODULE_CLASS);
event_box_.add(box_);
if (config_["format-window-separator"].isString()) {
m_formatWindowSeperator = config_["format-window-separator"].asString();
m_formatWindowSeparator = config_["format-window-separator"].asString();
} else {
m_formatWindowSeperator = " ";
m_formatWindowSeparator = " ";
}
const Json::Value &windowRewrite = config["window-rewrite"];
const Json::Value &windowRewriteDefaultConfig = config["window-rewrite-default"];
m_windowRewriteDefault =
windowRewriteDefaultConfig.isString() ? windowRewriteDefaultConfig.asString() : "?";
m_windowRewriteRules = waybar::util::RegexCollection(
windowRewrite, m_windowRewriteDefault,
[this](std::string &window_rule) { return windowRewritePriorityFunction(window_rule); });
const Json::Value& windowRewrite = config["window-rewrite"];
if (windowRewrite.isObject()) {
const Json::Value& windowRewriteDefaultConfig = config["window-rewrite-default"];
std::string windowRewriteDefault =
windowRewriteDefaultConfig.isString() ? windowRewriteDefaultConfig.asString() : "?";
m_windowRewriteRules = waybar::util::RegexCollection(
windowRewrite, std::move(windowRewriteDefault), windowRewritePriorityFunction);
}
populateIgnoreWorkspacesConfig(config);
ipc_.subscribe(R"(["workspace"])");
ipc_.subscribe(R"(["window"])");
ipc_.signal_event.connect(sigc::mem_fun(*this, &Workspaces::onEvent));
ipc_.signal_cmd.connect(sigc::mem_fun(*this, &Workspaces::onCmd));
ipc_.sendCmd(IPC_GET_TREE);
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.signal_scroll_event().connect(sigc::mem_fun(*this, &Workspaces::handleScroll));
}
@@ -85,21 +84,51 @@ Workspaces::Workspaces(const std::string &id, const Bar &bar, const Json::Value
ipc_.setWorker([this] {
try {
ipc_.handleEvent();
} catch (const std::exception &e) {
} catch (const std::exception& e) {
spdlog::error("Workspaces: {}", e.what());
}
});
}
void Workspaces::onEvent(const struct Ipc::ipc_response &res) {
void Workspaces::onEvent(const struct Ipc::ipc_response& res) {
try {
ipc_.sendCmd(IPC_GET_TREE);
} catch (const std::exception &e) {
} catch (const std::exception& e) {
spdlog::error("Workspaces: {}", e.what());
}
}
void Workspaces::onCmd(const struct Ipc::ipc_response &res) {
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);
}
}
}
}
bool Workspaces::isWorkspaceIgnored(std::string const& name) {
for (auto& rule : m_ignoreWorkspaces) {
if (std::regex_match(name, rule)) {
return true;
break;
}
}
return false;
}
void Workspaces::onCmd(const struct Ipc::ipc_response& res) {
if (res.type == IPC_GET_TREE) {
try {
{
@@ -109,7 +138,7 @@ void Workspaces::onCmd(const struct Ipc::ipc_response &res) {
std::vector<Json::Value> outputs;
bool alloutputs = config_["all-outputs"].asBool();
std::copy_if(payload["nodes"].begin(), payload["nodes"].end(), std::back_inserter(outputs),
[&](const auto &output) {
[&](const auto& output) {
if (alloutputs && output["name"].asString() != "__i3") {
return true;
}
@@ -119,30 +148,23 @@ void Workspaces::onCmd(const struct Ipc::ipc_response &res) {
return false;
});
for (auto &output : outputs) {
std::copy(output["nodes"].begin(), output["nodes"].end(),
std::back_inserter(workspaces_));
for (auto& output : outputs) {
std::copy_if(
output["nodes"].begin(), output["nodes"].end(), std::back_inserter(workspaces_),
[&](const auto& node) { return !(isWorkspaceIgnored(node["name"].asString())); });
std::copy(output["floating_nodes"].begin(), output["floating_nodes"].end(),
std::back_inserter(workspaces_));
}
if (config_["persistent_workspaces"].isObject()) {
spdlog::warn(
"persistent_workspaces is deprecated. Please change config to use "
"persistent-workspaces.");
}
// adding persistent workspaces (as per the config file)
if (config_["persistent-workspaces"].isObject() ||
config_["persistent_workspaces"].isObject()) {
const Json::Value &p_workspaces = config_["persistent-workspaces"].isObject()
? config_["persistent-workspaces"]
: config_["persistent_workspaces"];
if (config_["persistent-workspaces"].isObject()) {
const Json::Value& p_workspaces = config_["persistent-workspaces"];
const std::vector<std::string> p_workspaces_names = p_workspaces.getMemberNames();
for (const std::string &p_w_name : p_workspaces_names) {
const Json::Value &p_w = p_workspaces[p_w_name];
for (const std::string& p_w_name : p_workspaces_names) {
const Json::Value& p_w = p_workspaces[p_w_name];
auto it = std::find_if(workspaces_.begin(), workspaces_.end(),
[&p_w_name](const Json::Value &node) {
[&p_w_name](const Json::Value& node) {
return node["name"].asString() == p_w_name;
});
@@ -152,8 +174,9 @@ void Workspaces::onCmd(const struct Ipc::ipc_response &res) {
if (p_w.isArray() && !p_w.empty()) {
// Adding to target outputs
for (const Json::Value &output : p_w) {
if (output.asString() == bar_.output->name) {
for (const Json::Value& output : p_w) {
auto output_name = output.asString();
if (output_name == bar_.output->name || output_name == bar_.output->identifier) {
Json::Value v;
v["name"] = p_w_name;
v["target_output"] = bar_.output->name;
@@ -195,10 +218,10 @@ void Workspaces::onCmd(const struct Ipc::ipc_response &res) {
// agreement that the "workspace prev/next" commands may not follow
// the order displayed in Waybar.
int max_num = -1;
for (auto &workspace : workspaces_) {
for (auto& workspace : workspaces_) {
max_num = std::max(workspace["num"].asInt(), max_num);
}
for (auto &workspace : workspaces_) {
for (auto& workspace : workspaces_) {
auto workspace_num = workspace["num"].asInt();
if (workspace_num > -1) {
workspace["sort"] = workspace_num;
@@ -207,7 +230,7 @@ void Workspaces::onCmd(const struct Ipc::ipc_response &res) {
}
}
std::sort(workspaces_.begin(), workspaces_.end(),
[this](const Json::Value &lhs, const Json::Value &rhs) {
[this](const Json::Value& lhs, const Json::Value& rhs) {
auto lname = lhs["name"].asString();
auto rname = rhs["name"].asString();
int l = lhs["sort"].asInt();
@@ -227,7 +250,7 @@ void Workspaces::onCmd(const struct Ipc::ipc_response &res) {
});
}
dp.emit();
} catch (const std::exception &e) {
} catch (const std::exception& e) {
spdlog::error("Workspaces: {}", e.what());
}
}
@@ -237,9 +260,12 @@ bool Workspaces::filterButtons() {
bool needReorder = false;
for (auto it = buttons_.begin(); it != buttons_.end();) {
auto ws = std::find_if(workspaces_.begin(), workspaces_.end(),
[it](const auto &node) { return node["name"].asString() == it->first; });
[it](const auto& node) { return node["name"].asString() == it->first; });
if (ws == workspaces_.end() ||
(!config_["all-outputs"].asBool() && (*ws)["output"].asString() != bar_.output->name)) {
((*ws).isMember("target_output") ? (*ws)["target_output"].asString() != bar_.output->name &&
(*ws)["target_output"].asString() != ""
: !config_["all-outputs"].asBool() &&
(*ws)["output"].asString() != bar_.output->name)) {
it = buttons_.erase(it);
needReorder = true;
} else {
@@ -249,43 +275,45 @@ bool Workspaces::filterButtons() {
return needReorder;
}
bool Workspaces::hasFlag(const Json::Value &node, const std::string &flag) {
if (!node[flag].isBool()) {
return false;
}
bool Workspaces::hasFlag(const Json::Value& node, const std::string& flag) {
if (node[flag].asBool()) {
return true;
}
if (std::any_of(node["nodes"].begin(), node["nodes"].end(),
[&](auto const &e) { return hasFlag(e, flag); })) {
[&](auto const& e) { return hasFlag(e, flag); })) {
return true;
}
if (std::any_of(node["floating_nodes"].begin(), node["floating_nodes"].end(),
[&](auto const &e) { return hasFlag(e, flag); })) {
[&](auto const& e) { return hasFlag(e, flag); })) {
return true;
}
return false;
}
void Workspaces::updateWindows(const Json::Value &node, std::string &windows) {
auto format = config_["window-format"].asString();
void Workspaces::updateWindows(const Json::Value& node, std::string& windows) {
if ((node["type"].asString() == "con" || node["type"].asString() == "floating_con") &&
node["name"].isString()) {
std::string title = g_markup_escape_text(node["name"].asString().c_str(), -1);
std::string windowClass = node["app_id"].asString();
std::string windowReprKey = fmt::format("class<{}> title<{}>", windowClass, title);
std::string window = m_windowRewriteRules.get(windowReprKey);
// allow result to have formatting
window =
fmt::format(fmt::runtime(window), fmt::arg("name", title), fmt::arg("class", windowClass));
windows.append(window);
windows.append(m_formatWindowSeperator);
std::string windowClass = node["app_id"].isString()
? node["app_id"].asString()
: node["window_properties"]["class"].asString();
// Only add window rewrites that can be looked up
if (!windowClass.empty()) {
std::string windowReprKey = fmt::format("class<{}> title<{}>", windowClass, title);
std::string window = m_windowRewriteRules.get(windowReprKey);
// allow result to have formatting
window = fmt::format(fmt::runtime(window), fmt::arg("name", title),
fmt::arg("class", windowClass));
windows.append(window);
windows.append(m_formatWindowSeparator);
}
}
for (const Json::Value &child : node["nodes"]) {
for (const Json::Value& child : node["nodes"]) {
updateWindows(child, windows);
}
for (const Json::Value &child : node["floating_nodes"]) {
for (const Json::Value& child : node["floating_nodes"]) {
updateWindows(child, windows);
}
}
@@ -298,16 +326,17 @@ auto Workspaces::update() -> void {
if (bit == buttons_.end()) {
needReorder = true;
}
auto &button = bit == buttons_.end() ? addButton(*it) : bit->second;
auto& button = bit == buttons_.end() ? addButton(*it) : bit->second;
if (needReorder) {
box_.reorder_child(button, it - workspaces_.begin());
}
bool noNodes = (*it)["nodes"].empty() && (*it)["floating_nodes"].empty();
if (hasFlag((*it), "focused")) {
button.get_style_context()->add_class("focused");
} else {
button.get_style_context()->remove_class("focused");
}
if (hasFlag((*it), "visible")) {
if (hasFlag((*it), "visible") || ((*it)["output"].isString() && noNodes)) {
button.get_style_context()->add_class("visible");
} else {
button.get_style_context()->remove_class("visible");
@@ -322,7 +351,24 @@ auto Workspaces::update() -> void {
} else {
button.get_style_context()->remove_class("persistent");
}
if (noNodes) {
button.get_style_context()->add_class("empty");
} else {
button.get_style_context()->remove_class("empty");
}
if ((*it)["output"].isString()) {
// Simply attempt to remove all output classes every time to reset output classes. This works
// even if a class has not been previously added to the style context.
for (const auto &oclass : config_["output-classes"]) {
button.get_style_context()->remove_class(oclass.asString());
}
// If output-classes contains a class for output associated with current workspace button, add
// the class to its style context.
std::string output_name = (*it)["output"].asString();
if (config_["output-classes"].isMember(output_name) &&
config_["output-classes"][output_name].isString()) {
button.get_style_context()->add_class(config_["output-classes"][output_name].asString());
}
if (((*it)["output"].asString()) == bar_.output->name) {
button.get_style_context()->add_class("current_output");
} else {
@@ -331,24 +377,44 @@ auto Workspaces::update() -> void {
} else {
button.get_style_context()->remove_class("current_output");
}
std::string output = (*it)["name"].asString();
std::string full_name;
if (!config_["disable-markup"].asBool()) {
full_name = g_markup_escape_text((*it)["name"].asString().c_str(), -1);
} else {
full_name = (*it)["name"].asString();
}
std::string windows = "";
if (config_["window-format"].isString()) {
if (config_["window-rewrite"].isObject()) {
updateWindows((*it), windows);
}
auto index = (*it)["num"].asInt();
if (config_["format"].isString()) {
auto format = config_["format"].asString();
output = fmt::format(
fmt::runtime(format), fmt::arg("icon", getIcon(output, *it)), fmt::arg("value", output),
fmt::arg("name", trimWorkspaceName(output)), fmt::arg("index", (*it)["num"].asString()),
fmt::arg("windows",
windows.substr(0, windows.length() - m_formatWindowSeperator.length())),
fmt::arg("output", (*it)["output"].asString()));
std::string format;
if (config_["format-for-negative-index"].isString() && index < 0) {
format = config_["format-for-negative-index"].asString();
} else {
format = config_["format"].asString();
}
auto name = trimWorkspaceName(full_name);
auto output = (*it)["output"].asString();
auto icon = getIcon(full_name, *it);
auto separated_windows =
windows.substr(0, windows.length() - m_formatWindowSeparator.length());
full_name =
fmt::format(fmt::runtime(format), fmt::arg("index", index), fmt::arg("name", name),
fmt::arg("value", full_name), fmt::arg("output", output),
fmt::arg("icon", icon), fmt::arg("windows", separated_windows));
}
if (!config_["disable-markup"].asBool()) {
static_cast<Gtk::Label *>(button.get_children()[0])->set_markup(output);
static_cast<Gtk::Label*>(button.get_children()[0])->set_markup(full_name);
} else {
button.set_label(output);
button.set_label(full_name);
}
onButtonReady(*it, button);
}
@@ -356,9 +422,9 @@ auto Workspaces::update() -> void {
AModule::update();
}
Gtk::Button &Workspaces::addButton(const Json::Value &node) {
Gtk::Button& Workspaces::addButton(const Json::Value& node) {
auto pair = buttons_.emplace(node["name"].asString(), node["name"].asString());
auto &&button = pair.first->second;
auto&& button = pair.first->second;
box_.pack_start(button, false, false, 0);
button.set_name("sway-workspace-" + node["name"].asString());
button.set_relief(Gtk::RELIEF_NONE);
@@ -371,13 +437,16 @@ Gtk::Button &Workspaces::addButton(const Json::Value &node) {
node["name"].asString(), node["target_output"].asString(),
"--no-auto-back-and-forth", node["name"].asString()));
} else {
ipc_.sendCmd(IPC_COMMAND, fmt::format("workspace {} \"{}\"",
config_["disable-auto-back-and-forth"].asBool()
? "--no-auto-back-and-forth"
: "",
node["name"].asString()));
std::string flag = config_["disable-auto-back-and-forth"].asBool()
? "--no-auto-back-and-forth"
: "";
if (node["num"].asInt() >= 0) {
ipc_.sendCmd(IPC_COMMAND, fmt::format(workspace_switch_number_cmd_, flag, node["num"].asInt()));
} else {
ipc_.sendCmd(IPC_COMMAND, fmt::format(workspace_switch_cmd_, flag, node["name"].asString()));
}
}
} catch (const std::exception &e) {
} catch (const std::exception& e) {
spdlog::error("Workspaces: {}", e.what());
}
});
@@ -385,18 +454,18 @@ Gtk::Button &Workspaces::addButton(const Json::Value &node) {
return button;
}
std::string Workspaces::getIcon(const std::string &name, const Json::Value &node) {
std::string Workspaces::getIcon(const std::string& name, const Json::Value& node) {
std::vector<std::string> keys = {"high-priority-named", "urgent", "focused", name, "default"};
for (auto const &key : keys) {
for (auto const& key : keys) {
if (key == "high-priority-named") {
auto it = std::find_if(high_priority_named_.begin(), high_priority_named_.end(),
[&](const std::string &member) { return member == name; });
[&](const std::string& member) { return member == name; });
if (it != high_priority_named_.end()) {
return config_["format-icons"][name].asString();
}
it = std::find_if(high_priority_named_.begin(), high_priority_named_.end(),
[&](const std::string &member) {
[&](const std::string& member) {
return trimWorkspaceName(member) == trimWorkspaceName(name);
});
if (it != high_priority_named_.end()) {
@@ -404,7 +473,7 @@ std::string Workspaces::getIcon(const std::string &name, const Json::Value &node
}
}
if (key == "focused" || key == "urgent") {
if (config_["format-icons"][key].isString() && node[key].asBool()) {
if (config_["format-icons"][key].isString() && hasFlag(node, key)) {
return config_["format-icons"][key].asString();
}
} else if (config_["format-icons"]["persistent"].isString() &&
@@ -419,8 +488,8 @@ std::string Workspaces::getIcon(const std::string &name, const Json::Value &node
return name;
}
bool Workspaces::handleScroll(GdkEventScroll *e) {
if (gdk_event_get_pointer_emulated((GdkEvent *)e)) {
bool Workspaces::handleScroll(GdkEventScroll* e) {
if (gdk_event_get_pointer_emulated((GdkEvent*)e) != 0) {
/**
* Ignore emulated scroll events on window
*/
@@ -432,16 +501,24 @@ bool Workspaces::handleScroll(GdkEventScroll *e) {
}
std::string name;
{
bool alloutputs = config_["all-outputs"].asBool();
std::lock_guard<std::mutex> lock(mutex_);
auto it = std::find_if(workspaces_.begin(), workspaces_.end(),
[](const auto &workspace) { return hasFlag(workspace, "focused"); });
auto it =
std::find_if(workspaces_.begin(), workspaces_.end(), [alloutputs](const auto& workspace) {
if (alloutputs) {
return hasFlag(workspace, "focused");
}
bool noNodes = workspace["nodes"].empty() && workspace["floating_nodes"].empty();
return hasFlag(workspace, "visible") || (workspace["output"].isString() && noNodes);
});
if (it == workspaces_.end()) {
return true;
}
bool reverse_scroll = config_["reverse-scroll"].isBool() && config_["reverse-scroll"].asBool();
if (dir == SCROLL_DIR::DOWN || dir == SCROLL_DIR::RIGHT) {
name = getCycleWorkspace(it, false);
name = getCycleWorkspace(it, reverse_scroll ? true : false);
} else if (dir == SCROLL_DIR::UP || dir == SCROLL_DIR::LEFT) {
name = getCycleWorkspace(it, true);
name = getCycleWorkspace(it, reverse_scroll ? false : true);
} else {
return true;
}
@@ -454,7 +531,7 @@ bool Workspaces::handleScroll(GdkEventScroll *e) {
}
try {
ipc_.sendCmd(IPC_COMMAND, fmt::format(workspace_switch_cmd_, "--no-auto-back-and-forth", name));
} catch (const std::exception &e) {
} catch (const std::exception& e) {
spdlog::error("Workspaces: {}", e.what());
}
if (!config_["warp-on-scroll"].isNull() && !config_["warp-on-scroll"].asBool()) {
@@ -463,8 +540,7 @@ bool Workspaces::handleScroll(GdkEventScroll *e) {
return true;
}
const std::string Workspaces::getCycleWorkspace(std::vector<Json::Value>::iterator it,
bool prev) const {
std::string Workspaces::getCycleWorkspace(std::vector<Json::Value>::iterator it, bool prev) const {
if (prev && it == workspaces_.begin() && !config_["disable-scroll-wraparound"].asBool()) {
return (*(--workspaces_.end()))["name"].asString();
}
@@ -482,7 +558,7 @@ const std::string Workspaces::getCycleWorkspace(std::vector<Json::Value>::iterat
return (*it)["name"].asString();
}
std::string Workspaces::trimWorkspaceName(std::string name) {
std::string Workspaces::trimWorkspaceName(const std::string& name) {
std::size_t found = name.find(':');
if (found != std::string::npos) {
return name.substr(found + 1);
@@ -490,9 +566,34 @@ std::string Workspaces::trimWorkspaceName(std::string name) {
return name;
}
void Workspaces::onButtonReady(const Json::Value &node, Gtk::Button &button) {
bool is_focused_recursive(const Json::Value& node) {
// If a workspace has a focused container then get_tree will say
// that the workspace itself isn't focused. Therefore we need to
// check if any of its nodes are focused as well.
// some layouts like tabbed have many nested nodes
// all nested nodes must be checked for focused flag
if (node["focused"].asBool()) {
return true;
}
for (const auto& child : node["nodes"]) {
if (is_focused_recursive(child)) {
return true;
}
}
for (const auto& child : node["floating_nodes"]) {
if (is_focused_recursive(child)) {
return true;
}
}
return false;
}
void Workspaces::onButtonReady(const Json::Value& node, Gtk::Button& button) {
if (config_["current-only"].asBool()) {
if (node["focused"].asBool()) {
if (is_focused_recursive(node)) {
button.show();
} else {
button.hide();
+222 -46
View File
@@ -1,10 +1,15 @@
#include "modules/systemd_failed_units.hpp"
#include <fmt/format.h>
#include <giomm/dbusproxy.h>
#include <glibmm/markup.h>
#include <glibmm/variant.h>
#include <spdlog/spdlog.h>
#include <cstdint>
#include <exception>
#include <stdexcept>
#include <tuple>
static const unsigned UPDATE_DEBOUNCE_TIME_MS = 1000;
@@ -12,76 +17,134 @@ namespace waybar::modules {
SystemdFailedUnits::SystemdFailedUnits(const std::string& id, const Json::Value& config)
: ALabel(config, "systemd-failed-units", id, "{nr_failed} failed", 1),
hide_on_ok(true),
update_pending(false),
nr_failed_system(0),
nr_failed_user(0),
last_status() {
hide_on_ok_(true),
tooltip_format_(
"System: {system_state}\nUser: {user_state}\nFailed units ({nr_failed}):\n"
"{failed_units_list}"),
tooltip_format_ok_("System: {system_state}\nUser: {user_state}"),
tooltip_unit_format_("{name}: {description}"),
update_pending_(false),
nr_failed_system_(0),
nr_failed_user_(0),
nr_failed_(0),
last_status_() {
if (config["hide-on-ok"].isBool()) {
hide_on_ok = config["hide-on-ok"].asBool();
hide_on_ok_ = config["hide-on-ok"].asBool();
}
if (config["format-ok"].isString()) {
format_ok = config["format-ok"].asString();
format_ok_ = config["format-ok"].asString();
} else {
format_ok = format_;
format_ok_ = format_;
}
if (config["tooltip-format"].isString()) {
tooltip_format_ = config["tooltip-format"].asString();
}
if (config["tooltip-format-ok"].isString()) {
tooltip_format_ok_ = config["tooltip-format-ok"].asString();
}
if (config["tooltip-unit-format"].isString()) {
tooltip_unit_format_ = config["tooltip-unit-format"].asString();
}
/* Default to enable both "system" and "user". */
if (!config["system"].isBool() || config["system"].asBool()) {
system_proxy = Gio::DBus::Proxy::create_for_bus_sync(
system_props_proxy_ = Gio::DBus::Proxy::create_for_bus_sync(
Gio::DBus::BusType::BUS_TYPE_SYSTEM, "org.freedesktop.systemd1",
"/org/freedesktop/systemd1", "org.freedesktop.DBus.Properties");
if (!system_proxy) {
if (!system_props_proxy_) {
throw std::runtime_error("Unable to connect to systemwide systemd DBus!");
}
system_proxy->signal_signal().connect(sigc::mem_fun(*this, &SystemdFailedUnits::notify_cb));
system_props_proxy_->signal_signal().connect(
sigc::mem_fun(*this, &SystemdFailedUnits::notify_cb));
try {
system_manager_proxy_ = Gio::DBus::Proxy::create_for_bus_sync(
Gio::DBus::BusType::BUS_TYPE_SYSTEM, "org.freedesktop.systemd1",
"/org/freedesktop/systemd1", "org.freedesktop.systemd1.Manager");
} catch (const Glib::Error& e) {
spdlog::warn("Unable to connect to systemwide systemd Manager interface: {}",
e.what().c_str());
}
}
if (!config["user"].isBool() || config["user"].asBool()) {
user_proxy = Gio::DBus::Proxy::create_for_bus_sync(
user_props_proxy_ = Gio::DBus::Proxy::create_for_bus_sync(
Gio::DBus::BusType::BUS_TYPE_SESSION, "org.freedesktop.systemd1",
"/org/freedesktop/systemd1", "org.freedesktop.DBus.Properties");
if (!user_proxy) {
if (!user_props_proxy_) {
throw std::runtime_error("Unable to connect to user systemd DBus!");
}
user_proxy->signal_signal().connect(sigc::mem_fun(*this, &SystemdFailedUnits::notify_cb));
user_props_proxy_->signal_signal().connect(
sigc::mem_fun(*this, &SystemdFailedUnits::notify_cb));
try {
user_manager_proxy_ = Gio::DBus::Proxy::create_for_bus_sync(
Gio::DBus::BusType::BUS_TYPE_SESSION, "org.freedesktop.systemd1",
"/org/freedesktop/systemd1", "org.freedesktop.systemd1.Manager");
} catch (const Glib::Error& e) {
spdlog::warn("Unable to connect to user systemd Manager interface: {}", e.what().c_str());
}
}
if (!user_props_proxy_ && !system_props_proxy_)
throw std::runtime_error("Neither system nor user status is requested.");
updateData();
/* Always update for the first time. */
dp.emit();
}
SystemdFailedUnits::~SystemdFailedUnits() {
if (system_proxy) system_proxy.reset();
if (user_proxy) user_proxy.reset();
}
auto SystemdFailedUnits::notify_cb(const Glib::ustring& sender_name,
const Glib::ustring& signal_name,
const Glib::VariantContainerBase& arguments) -> void {
if (signal_name == "PropertiesChanged" && !update_pending) {
update_pending = true;
if (signal_name == "PropertiesChanged" && !update_pending_) {
update_pending_ = true;
/* The fail count may fluctuate due to restarting. */
Glib::signal_timeout().connect_once(sigc::mem_fun(*this, &SystemdFailedUnits::updateData),
UPDATE_DEBOUNCE_TIME_MS);
}
}
void SystemdFailedUnits::updateData() {
update_pending = false;
void SystemdFailedUnits::RequestSystemState() {
auto load = [](const char* kind, Glib::RefPtr<Gio::DBus::Proxy>& proxy) -> std::string {
try {
if (!proxy) return "ignored";
auto parameters = Glib::VariantContainerBase(
g_variant_new("(ss)", "org.freedesktop.systemd1.Manager", "SystemState"));
Glib::VariantContainerBase data = proxy->call_sync("Get", parameters);
if (data && data.is_of_type(Glib::VariantType("(v)"))) {
Glib::VariantBase variant;
g_variant_get(const_cast<GVariant*>(data.gobj()), "(v)", &variant);
if (variant && variant.is_of_type(Glib::VARIANT_TYPE_STRING)) {
return g_variant_get_string(const_cast<GVariant*>(variant.gobj()), NULL);
}
}
} catch (Glib::Error& e) {
spdlog::error("Failed to get {} state: {}", kind, e.what().c_str());
}
return "unknown";
};
system_state_ = load("systemwide", system_props_proxy_);
user_state_ = load("user", user_props_proxy_);
if ((system_state_ == "running" || system_state_ == "ignored") &&
(user_state_ == "running" || user_state_ == "ignored"))
overall_state_ = "ok";
else
overall_state_ = "degraded";
}
void SystemdFailedUnits::RequestFailedUnits() {
auto load = [](const char* kind, Glib::RefPtr<Gio::DBus::Proxy>& proxy) -> uint32_t {
try {
if (!proxy) return 0;
auto parameters = Glib::VariantContainerBase(
g_variant_new("(ss)", "org.freedesktop.systemd1.Manager", "NFailedUnits"));
Glib::VariantContainerBase data = proxy->call_sync("Get", parameters);
if (data && data.is_of_type(Glib::VariantType("(v)"))) {
Glib::VariantBase variant;
g_variant_get(data.gobj_copy(), "(v)", &variant);
g_variant_get(const_cast<GVariant*>(data.gobj()), "(v)", &variant);
if (variant && variant.is_of_type(Glib::VARIANT_TYPE_UINT32)) {
uint32_t value = 0;
g_variant_get(variant.gobj_copy(), "u", &value);
return value;
return g_variant_get_uint32(const_cast<GVariant*>(variant.gobj()));
}
}
} catch (Glib::Error& e) {
@@ -90,40 +153,153 @@ void SystemdFailedUnits::updateData() {
return 0;
};
if (system_proxy) {
nr_failed_system = load("systemwide", system_proxy);
nr_failed_system_ = load("systemwide", system_props_proxy_);
nr_failed_user_ = load("user", user_props_proxy_);
nr_failed_ = nr_failed_system_ + nr_failed_user_;
}
void SystemdFailedUnits::RequestFailedUnitsList() {
failed_units_.clear();
if (!tooltipEnabled() || nr_failed_ == 0) {
return;
}
if (user_proxy) {
nr_failed_user = load("user", user_proxy);
if (system_manager_proxy_) {
auto units = LoadFailedUnitsList("systemwide", system_manager_proxy_, "system");
failed_units_.insert(failed_units_.end(), units.begin(), units.end());
}
if (user_manager_proxy_) {
auto units = LoadFailedUnitsList("user", user_manager_proxy_, "user");
failed_units_.insert(failed_units_.end(), units.begin(), units.end());
}
}
auto SystemdFailedUnits::LoadFailedUnitsList(const char* kind,
Glib::RefPtr<Gio::DBus::Proxy>& proxy,
const std::string& scope) -> std::vector<FailedUnit> {
// org.freedesktop.systemd1.Manager.ListUnits returns
// (name, description, load_state, active_state, sub_state, followed, unit_path, job_id,
// job_type, job_path).
using UnitRow = std::tuple<Glib::ustring, Glib::ustring, Glib::ustring, Glib::ustring,
Glib::ustring, Glib::ustring, Glib::DBusObjectPathString, guint32,
Glib::ustring, Glib::DBusObjectPathString>;
using ListUnitsReply = Glib::Variant<std::tuple<std::vector<UnitRow>>>;
std::vector<FailedUnit> units;
if (!proxy) {
return units;
}
try {
auto data = proxy->call_sync("ListUnits");
if (!data) return units;
if (!data.is_of_type(ListUnitsReply::variant_type())) {
spdlog::warn("Unexpected DBus signature for ListUnits: {}", data.get_type_string());
return units;
}
auto [rows] = Glib::VariantBase::cast_dynamic<ListUnitsReply>(data).get();
for (const auto& row : rows) {
const auto& name = std::get<0>(row);
const auto& description = std::get<1>(row);
const auto& load_state = std::get<2>(row);
const auto& active_state = std::get<3>(row);
const auto& sub_state = std::get<4>(row);
if (active_state == "failed" || sub_state == "failed") {
units.push_back({name, description, load_state, active_state, sub_state, scope});
}
}
} catch (const Glib::Error& e) {
spdlog::error("Failed to list {} units: {}", kind, e.what().c_str());
}
return units;
}
std::string SystemdFailedUnits::BuildTooltipFailedList() const {
if (failed_units_.empty()) {
return "";
}
std::string list;
list.reserve(failed_units_.size() * 16);
bool first = true;
for (const auto& unit : failed_units_) {
try {
auto line = fmt::format(
fmt::runtime(tooltip_unit_format_),
fmt::arg("name", Glib::Markup::escape_text(unit.name).raw()),
fmt::arg("description", Glib::Markup::escape_text(unit.description).raw()),
fmt::arg("load_state", unit.load_state), fmt::arg("active_state", unit.active_state),
fmt::arg("sub_state", unit.sub_state), fmt::arg("scope", unit.scope));
if (!first) {
list += "\n";
}
first = false;
list += "- ";
list += line;
} catch (const std::exception& e) {
spdlog::warn("Failed to format tooltip for unit {}: {}", unit.name, e.what());
}
}
return list;
}
void SystemdFailedUnits::updateData() {
update_pending_ = false;
RequestSystemState();
if (overall_state_ == "degraded") {
RequestFailedUnits();
RequestFailedUnitsList();
} else {
nr_failed_system_ = nr_failed_user_ = nr_failed_ = 0;
failed_units_.clear();
}
dp.emit();
}
auto SystemdFailedUnits::update() -> void {
uint32_t nr_failed = nr_failed_system + nr_failed_user;
// Hide if needed.
if (nr_failed == 0 && hide_on_ok) {
if (overall_state_ == "ok" && hide_on_ok_) {
event_box_.set_visible(false);
last_status_ = overall_state_;
return;
}
if (!event_box_.get_visible()) {
event_box_.set_visible(true);
}
event_box_.set_visible(true);
// Set state class.
const std::string status = nr_failed == 0 ? "ok" : "degraded";
if (!last_status.empty() && label_.get_style_context()->has_class(last_status)) {
label_.get_style_context()->remove_class(last_status);
if (!last_status_.empty() && label_.get_style_context()->has_class(last_status_)) {
label_.get_style_context()->remove_class(last_status_);
}
if (!label_.get_style_context()->has_class(status)) {
label_.get_style_context()->add_class(status);
if (!label_.get_style_context()->has_class(overall_state_)) {
label_.get_style_context()->add_class(overall_state_);
}
last_status = status;
label_.set_markup(fmt::format(
fmt::runtime(nr_failed == 0 ? format_ok : format_), fmt::arg("nr_failed", nr_failed),
fmt::arg("nr_failed_system", nr_failed_system), fmt::arg("nr_failed_user", nr_failed_user)));
last_status_ = overall_state_;
setLabelMarkup(fmt::format(
fmt::runtime(nr_failed_ == 0 ? format_ok_ : format_), fmt::arg("nr_failed", nr_failed_),
fmt::arg("nr_failed_system", nr_failed_system_), fmt::arg("nr_failed_user", nr_failed_user_),
fmt::arg("system_state", system_state_), fmt::arg("user_state", user_state_),
fmt::arg("overall_state", overall_state_)));
if (tooltipEnabled()) {
std::string failed_list = BuildTooltipFailedList();
auto tooltip_template = overall_state_ == "ok" ? tooltip_format_ok_ : tooltip_format_;
if (!tooltip_template.empty()) {
setTooltipMarkup(fmt::format(
fmt::runtime(tooltip_template), fmt::arg("nr_failed", nr_failed_),
fmt::arg("nr_failed_system", nr_failed_system_),
fmt::arg("nr_failed_user", nr_failed_user_), fmt::arg("system_state", system_state_),
fmt::arg("user_state", user_state_), fmt::arg("overall_state", overall_state_),
fmt::arg("failed_units_list", failed_list)));
} else {
setTooltipMarkup("");
}
}
ALabel::update();
}
+120 -39
View File
@@ -1,6 +1,9 @@
#include "modules/temperature.hpp"
#include <filesystem>
#include <optional>
#include <stdexcept>
#include <string>
#if defined(__FreeBSD__)
#include <sys/sysctl.h>
@@ -11,26 +14,93 @@ waybar::modules::Temperature::Temperature(const std::string& id, const Json::Val
#if defined(__FreeBSD__)
// FreeBSD uses sysctlbyname instead of read from a file
#else
auto& hwmon_path = config_["hwmon-path"];
if (hwmon_path.isString()) {
file_path_ = hwmon_path.asString();
} else if (hwmon_path.isArray()) {
auto traverseAsArray = [](const Json::Value& value, auto&& check_set_path) {
if (value.isString())
check_set_path(value.asString());
else if (value.isArray())
for (const auto& item : value)
if (check_set_path(item.asString())) break;
};
if (config_["hwmon-by-name"].isString() && config_["input-filename"].isString()) {
auto name = config_["hwmon-by-name"].asString();
auto input_filename = config_["input-filename"].asString();
for (const auto& entry : std::filesystem::directory_iterator("/sys/class/hwmon/")) {
if (std::filesystem::is_directory(entry) && file_path_.empty()) {
auto name_filepath = entry.path().string() + "/name";
auto input_filepath = entry.path().string() + "/" + input_filename;
if (std::filesystem::exists(name_filepath) && std::filesystem::exists(input_filepath)) {
std::ifstream name_file(name_filepath);
if (!name_file.is_open()) {
throw std::runtime_error("Error: Could not open file " + name_filepath);
}
std::string line;
while (std::getline(name_file, line)) {
if (line.find(name) != std::string::npos) {
file_path_ = input_filepath;
break;
}
}
}
}
}
if (file_path_.empty()) throw std::runtime_error("Could not find hwmon by name " + name);
}
auto find_hwmon_by_name = [](const std::string& name) -> std::optional<std::filesystem::path> {
for (const auto& entry : std::filesystem::directory_iterator("/sys/class/hwmon")) {
std::ifstream f(entry.path() / "name");
std::string hwname;
if (f >> hwname && hwname == name) {
return entry.path();
}
}
return std::nullopt;
};
// ensure either hwmon-name OR old paths are used, not both
if (config_["hwmon-name"].isString() &&
(!config_["hwmon-path"].isNull() || !config_["hwmon-path-abs"].isNull())) {
throw std::runtime_error(
"hwmon-name cannot be used together with hwmon-path or hwmon-path-abs");
}
if (file_path_.empty()) {
// if hwmon_path is an array, loop to find first valid item
for (auto& item : hwmon_path) {
auto path = item.asString();
if (std::filesystem::exists(path)) {
file_path_ = path;
break;
}
traverseAsArray(config_["hwmon-path"], [this](const std::string& path) {
if (!std::filesystem::exists(path)) return false;
file_path_ = path;
return true;
});
}
if (file_path_.empty() && config_["input-filename"].isString()) {
// fallback to hwmon_paths-abs
traverseAsArray(config_["hwmon-path-abs"], [this](const std::string& path) {
if (!std::filesystem::is_directory(path)) return false;
return std::ranges::any_of(
std::filesystem::directory_iterator(path), [this](const auto& hwmon) {
if (!hwmon.path().filename().string().starts_with("hwmon")) return false;
file_path_ = hwmon.path().string() + "/" + config_["input-filename"].asString();
return true;
});
});
}
if (file_path_.empty() && config_["hwmon-name"].isString()) {
if (!config_["input-filename"].isString()) {
throw std::runtime_error("hwmon-name requires input-filename to be set");
}
} else if (config_["hwmon-path-abs"].isString() && config_["input-filename"].isString()) {
for (const auto& hwmon :
std::filesystem::directory_iterator(config_["hwmon-path-abs"].asString())) {
if (hwmon.path().filename().string().starts_with("hwmon")) {
file_path_ = hwmon.path().string() + "/" + config_["input-filename"].asString();
break;
}
auto hwmon = find_hwmon_by_name(config_["hwmon-name"].asString());
if (!hwmon) {
throw std::runtime_error("hwmon-name '" + config_["hwmon-name"].asString() + "' not found");
}
file_path_ = hwmon->string() + "/" + config_["input-filename"].asString();
}
if (file_path_.empty()) {
@@ -38,7 +108,7 @@ waybar::modules::Temperature::Temperature(const std::string& id, const Json::Val
file_path_ = fmt::format("/sys/class/thermal/thermal_zone{}/temp", zone);
}
// check if file_path_ can be used to retrive the temperature
// check if file_path_ can be used to retrieve the temperature
std::ifstream temp(file_path_);
if (!temp.is_open()) {
throw std::runtime_error("Can't open " + file_path_);
@@ -62,35 +132,33 @@ auto waybar::modules::Temperature::update() -> void {
uint16_t temperature_f = std::round(temperature * 1.8 + 32);
uint16_t temperature_k = std::round(temperature + 273.15);
auto critical = isCritical(temperature_c);
auto warning = isWarning(temperature_c);
auto format = format_;
if (critical) {
format = config_["format-critical"].isString() ? config_["format-critical"].asString() : format;
label_.get_style_context()->add_class("critical");
} else {
label_.get_style_context()->remove_class("critical");
if (warning) {
format = config_["format-warning"].isString() ? config_["format-warning"].asString() : format;
label_.get_style_context()->add_class("warning");
} else {
label_.get_style_context()->remove_class("warning");
}
}
if (format.empty()) {
event_box_.hide();
return;
} else {
event_box_.show();
}
event_box_.show();
auto max_temp = config_["critical-threshold"].isInt() ? config_["critical-threshold"].asInt() : 0;
label_.set_markup(fmt::format(fmt::runtime(format), fmt::arg("temperatureC", temperature_c),
fmt::arg("temperatureF", temperature_f),
fmt::arg("temperatureK", temperature_k),
fmt::arg("icon", getIcon(temperature_c, "", max_temp))));
if (tooltipEnabled()) {
std::string tooltip_format = "{temperatureC}°C";
if (config_["tooltip-format"].isString()) {
tooltip_format = config_["tooltip-format"].asString();
}
label_.set_tooltip_text(fmt::format(
fmt::runtime(tooltip_format), fmt::arg("temperatureC", temperature_c),
fmt::arg("temperatureF", temperature_f), fmt::arg("temperatureK", temperature_k)));
}
updateLabelAndTooltip(format, "{temperatureC}°C", fmt::arg("temperatureC", temperature_c),
fmt::arg("temperatureF", temperature_f),
fmt::arg("temperatureK", temperature_k),
fmt::arg("icon", getIcon(temperature_c, "", max_temp)));
// Call parent update
ALabel::update();
}
@@ -102,13 +170,17 @@ float waybar::modules::Temperature::getTemperature() {
auto zone = config_["thermal-zone"].isInt() ? config_["thermal-zone"].asInt() : 0;
if (sysctlbyname(fmt::format("hw.acpi.thermal.tz{}.temperature", zone).c_str(), &temp, &size,
NULL, 0) != 0) {
throw std::runtime_error(fmt::format(
"sysctl hw.acpi.thermal.tz{}.temperature or dev.cpu.{}.temperature failed", zone, zone));
// First, try with dev.cpu
if ((sysctlbyname(fmt::format("dev.cpu.{}.temperature", zone).c_str(), &temp, &size, NULL, 0) ==
0) ||
(sysctlbyname(fmt::format("hw.acpi.thermal.tz{}.temperature", zone).c_str(), &temp, &size,
NULL, 0) == 0)) {
auto temperature_c = ((float)temp - 2732) / 10;
return temperature_c;
}
auto temperature_c = ((float)temp - 2732) / 10;
return temperature_c;
throw std::runtime_error(fmt::format(
"sysctl hw.acpi.thermal.tz{}.temperature and dev.cpu.{}.temperature failed", zone, zone));
#else // Linux
std::ifstream temp(file_path_);
@@ -128,7 +200,16 @@ float waybar::modules::Temperature::getTemperature() {
#endif
}
bool waybar::modules::Temperature::isWarning(uint16_t temperature_c) {
return config_["warning-threshold"].isInt() &&
temperature_c >= config_["warning-threshold"].asInt();
}
bool waybar::modules::Temperature::isCritical(uint16_t temperature_c) {
return config_["critical-threshold"].isInt() &&
temperature_c >= config_["critical-threshold"].asInt();
}
void waybar::modules::Temperature::suspend() { thread_.pause(); }
void waybar::modules::Temperature::resume() { thread_.resume(); }
+503
View File
@@ -0,0 +1,503 @@
#include "modules/upower.hpp"
#include <giomm/dbuswatchname.h>
#include <gtkmm/tooltip.h>
#include <spdlog/spdlog.h>
namespace waybar::modules {
UPower::UPower(const std::string& id, const Json::Value& config)
: AIconLabel(config, "upower", id, "{percentage}", 0, true, true, true), sleeping_{false} {
box_.set_name(name_);
box_.set_spacing(0);
// Tooltip box
contentBox_.set_orientation((box_.get_orientation() == Gtk::ORIENTATION_HORIZONTAL)
? Gtk::ORIENTATION_VERTICAL
: Gtk::ORIENTATION_HORIZONTAL);
// Get current theme
gtkTheme_ = Gtk::IconTheme::get_default();
// Icon Size
if (config_["icon-size"].isInt()) {
iconSize_ = config_["icon-size"].asInt();
}
image_.set_pixel_size(iconSize_);
// Show icon only when "show-icon" isn't set to false
if (config_["show-icon"].isBool()) showIcon_ = config_["show-icon"].asBool();
if (!showIcon_) box_.remove(image_);
// Device user wants
if (config_["native-path"].isString()) nativePath_ = config_["native-path"].asString();
// Device model user wants
if (config_["model"].isString()) model_ = config_["model"].asString();
// Hide If Empty
if (config_["hide-if-empty"].isBool()) hideIfEmpty_ = config_["hide-if-empty"].asBool();
// Tooltip Spacing
if (config_["tooltip-spacing"].isInt()) tooltip_spacing_ = config_["tooltip-spacing"].asInt();
// Tooltip Padding
if (config_["tooltip-padding"].isInt()) {
tooltip_padding_ = config_["tooltip-padding"].asInt();
contentBox_.set_margin_top(tooltip_padding_);
contentBox_.set_margin_bottom(tooltip_padding_);
contentBox_.set_margin_left(tooltip_padding_);
contentBox_.set_margin_right(tooltip_padding_);
}
// Tooltip Format
if (config_["tooltip-format"].isString()) tooltipFormat_ = config_["tooltip-format"].asString();
// Start watching DBUS
watcherID_ = Gio::DBus::watch_name(
Gio::DBus::BusType::BUS_TYPE_SYSTEM, "org.freedesktop.UPower",
sigc::mem_fun(*this, &UPower::onAppear), sigc::mem_fun(*this, &UPower::onVanished),
Gio::DBus::BusNameWatcherFlags::BUS_NAME_WATCHER_FLAGS_AUTO_START);
// Get DBus async connect
Gio::DBus::Connection::get(Gio::DBus::BusType::BUS_TYPE_SYSTEM,
sigc::mem_fun(*this, &UPower::getConn_cb));
// Make UPower client
GError* gErr = NULL;
upClient_ = up_client_new_full(NULL, &gErr);
if (upClient_ == NULL) {
spdlog::error("Upower. UPower client connection error. {}",
gErr ? gErr->message : "unknown error");
if (gErr) g_error_free(gErr);
}
// Subscribe UPower events
g_signal_connect(upClient_, "device-added", G_CALLBACK(deviceAdded_cb), this);
g_signal_connect(upClient_, "device-removed", G_CALLBACK(deviceRemoved_cb), this);
// Subscribe tooltip query events
box_.set_has_tooltip(AModule::tooltipEnabled());
if (AModule::tooltipEnabled()) {
box_.signal_query_tooltip().connect(sigc::mem_fun(*this, &UPower::queryTooltipCb), false);
}
resetDevices();
setDisplayDevice();
// Update the widget
dp.emit();
}
UPower::~UPower() {
if (upDevice_.upDevice != NULL) g_object_unref(upDevice_.upDevice);
if (upClient_ != NULL) g_object_unref(upClient_);
if (subscrID_ > 0u) {
conn_->signal_unsubscribe(subscrID_);
subscrID_ = 0u;
}
Gio::DBus::unwatch_name(watcherID_);
watcherID_ = 0u;
removeDevices();
}
static std::string_view getDeviceStatus(UpDeviceState& state) {
switch (state) {
case UP_DEVICE_STATE_CHARGING:
case UP_DEVICE_STATE_PENDING_CHARGE:
return "charging";
case UP_DEVICE_STATE_DISCHARGING:
case UP_DEVICE_STATE_PENDING_DISCHARGE:
return "discharging";
case UP_DEVICE_STATE_FULLY_CHARGED:
return "full";
case UP_DEVICE_STATE_EMPTY:
return "empty";
default:
return "unknown-status";
}
}
static std::string_view getDeviceIcon(UpDeviceKind& kind) {
switch (kind) {
case UP_DEVICE_KIND_LINE_POWER:
return "ac-adapter-symbolic";
case UP_DEVICE_KIND_BATTERY:
return "battery-symbolic";
case UP_DEVICE_KIND_UPS:
return "uninterruptible-power-supply-symbolic";
case UP_DEVICE_KIND_MONITOR:
return "video-display-symbolic";
case UP_DEVICE_KIND_MOUSE:
return "input-mouse-symbolic";
case UP_DEVICE_KIND_KEYBOARD:
return "input-keyboard-symbolic";
case UP_DEVICE_KIND_PDA:
return "pda-symbolic";
case UP_DEVICE_KIND_PHONE:
return "phone-symbolic";
case UP_DEVICE_KIND_MEDIA_PLAYER:
return "multimedia-player-symbolic";
case UP_DEVICE_KIND_TABLET:
return "computer-apple-ipad-symbolic";
case UP_DEVICE_KIND_COMPUTER:
return "computer-symbolic";
case UP_DEVICE_KIND_GAMING_INPUT:
return "input-gaming-symbolic";
case UP_DEVICE_KIND_PEN:
return "input-tablet-symbolic";
case UP_DEVICE_KIND_TOUCHPAD:
return "input-touchpad-symbolic";
case UP_DEVICE_KIND_MODEM:
return "modem-symbolic";
case UP_DEVICE_KIND_NETWORK:
return "network-wired-symbolic";
case UP_DEVICE_KIND_HEADSET:
return "audio-headset-symbolic";
case UP_DEVICE_KIND_HEADPHONES:
return "audio-headphones-symbolic";
case UP_DEVICE_KIND_OTHER_AUDIO:
case UP_DEVICE_KIND_SPEAKERS:
return "audio-speakers-symbolic";
case UP_DEVICE_KIND_VIDEO:
return "camera-web-symbolic";
case UP_DEVICE_KIND_PRINTER:
return "printer-symbolic";
case UP_DEVICE_KIND_SCANNER:
return "scanner-symbolic";
case UP_DEVICE_KIND_CAMERA:
return "camera-photo-symbolic";
case UP_DEVICE_KIND_BLUETOOTH_GENERIC:
return "bluetooth-active-symbolic";
case UP_DEVICE_KIND_TOY:
case UP_DEVICE_KIND_REMOTE_CONTROL:
case UP_DEVICE_KIND_WEARABLE:
case UP_DEVICE_KIND_LAST:
default:
return "battery-symbolic";
}
}
static std::string secondsToString(const std::chrono::seconds sec) {
const auto ds{std::chrono::duration_cast<std::chrono::days>(sec)};
const auto hrs{std::chrono::duration_cast<std::chrono::hours>(sec - ds)};
const auto min{std::chrono::duration_cast<std::chrono::minutes>(sec - ds - hrs)};
std::string_view strRet{(ds.count() > 0) ? "{D}d {H}h {M}min"
: (hrs.count() > 0) ? "{H}h {M}min"
: (min.count() > 0) ? "{M}min"
: ""};
spdlog::debug(
"UPower::secondsToString(). seconds: \"{0}\", minutes: \"{1}\", hours: \"{2}\", \
days: \"{3}\", strRet: \"{4}\"",
sec.count(), min.count(), hrs.count(), ds.count(), strRet);
return fmt::format(fmt::runtime(strRet), fmt::arg("D", ds.count()), fmt::arg("H", hrs.count()),
fmt::arg("M", min.count()));
}
auto UPower::update() -> void {
std::lock_guard<std::mutex> guard{mutex_};
// Don't update widget if the UPower service isn't running
if (!upRunning_ || sleeping_) {
if (hideIfEmpty_) box_.hide();
return;
}
getUpDeviceInfo(upDevice_);
if (upDevice_.upDevice == NULL && hideIfEmpty_) {
box_.hide();
return;
}
/* Every Device which is handled by Upower and which is not
* UP_DEVICE_KIND_UNKNOWN (0) or UP_DEVICE_KIND_LINE_POWER (1) is a Battery
*/
const bool upDeviceValid{upDevice_.kind != UpDeviceKind::UP_DEVICE_KIND_UNKNOWN &&
upDevice_.kind != UpDeviceKind::UP_DEVICE_KIND_LINE_POWER};
// Get CSS status
const auto status{getDeviceStatus(upDevice_.state)};
// Remove last status if it exists
if (!lastStatus_.empty() && box_.get_style_context()->has_class(lastStatus_))
box_.get_style_context()->remove_class(lastStatus_);
if (!box_.get_style_context()->has_class(std::string(status)))
box_.get_style_context()->add_class(std::string(status));
lastStatus_ = status;
if (devices_.size() == 0 && !upDeviceValid && hideIfEmpty_) {
box_.hide();
// Call parent update
AModule::update();
return;
}
setLabelMarkup(getText(upDevice_, format_));
// Set icon
if (upDevice_.icon_name == NULL || !gtkTheme_->has_icon(upDevice_.icon_name))
upDevice_.icon_name = (char*)NO_BATTERY.c_str();
image_.set_from_icon_name(upDevice_.icon_name, Gtk::ICON_SIZE_INVALID);
box_.show();
// Call parent update
ALabel::update();
}
void UPower::getConn_cb(Glib::RefPtr<Gio::AsyncResult>& result) {
try {
conn_ = Gio::DBus::Connection::get_finish(result);
// Subscribe DBUs events
subscrID_ = conn_->signal_subscribe(sigc::mem_fun(*this, &UPower::prepareForSleep_cb),
"org.freedesktop.login1", "org.freedesktop.login1.Manager",
"PrepareForSleep", "/org/freedesktop/login1");
} catch (const Glib::Error& e) {
spdlog::error("Upower. DBus connection error. {}", e.what().c_str());
}
}
void UPower::onAppear(const Glib::RefPtr<Gio::DBus::Connection>& conn, const Glib::ustring& name,
const Glib::ustring& name_owner) {
upRunning_ = true;
}
void UPower::onVanished(const Glib::RefPtr<Gio::DBus::Connection>& conn,
const Glib::ustring& name) {
upRunning_ = false;
}
void UPower::prepareForSleep_cb(const Glib::RefPtr<Gio::DBus::Connection>& connection,
const Glib::ustring& sender_name, const Glib::ustring& object_path,
const Glib::ustring& interface_name,
const Glib::ustring& signal_name,
const Glib::VariantContainerBase& parameters) {
if (parameters.is_of_type(Glib::VariantType("(b)"))) {
Glib::Variant<bool> sleeping;
parameters.get_child(sleeping, 0);
if (!sleeping.get()) {
resetDevices();
setDisplayDevice();
sleeping_ = false;
// Update the widget
dp.emit();
} else
sleeping_ = true;
}
}
void UPower::deviceAdded_cb(UpClient* client, UpDevice* device, gpointer data) {
UPower* up{static_cast<UPower*>(data)};
up->addDevice(device);
up->setDisplayDevice();
// Update the widget
up->dp.emit();
}
void UPower::deviceRemoved_cb(UpClient* client, const gchar* objectPath, gpointer data) {
UPower* up{static_cast<UPower*>(data)};
up->removeDevice(objectPath);
up->setDisplayDevice();
// Update the widget
up->dp.emit();
}
void UPower::deviceNotify_cb(UpDevice* device, GParamSpec* pspec, gpointer data) {
UPower* up{static_cast<UPower*>(data)};
// Update the widget
up->dp.emit();
}
void UPower::addDevice(UpDevice* device) {
std::lock_guard<std::mutex> guard{mutex_};
if (G_IS_OBJECT(device)) {
const gchar* objectPath{up_device_get_object_path(device)};
// Due to the device getting cleared after this event is fired, we
// create a new object pointing to its objectPath
device = up_device_new();
upDevice_output upDevice{.upDevice = device};
gboolean ret{up_device_set_object_path_sync(device, objectPath, NULL, NULL)};
if (!ret) {
g_object_unref(G_OBJECT(device));
return;
}
if (devices_.find(objectPath) != devices_.cend()) {
auto upDevice{devices_[objectPath]};
if (G_IS_OBJECT(upDevice.upDevice)) g_object_unref(upDevice.upDevice);
devices_.erase(objectPath);
}
g_signal_connect(device, "notify", G_CALLBACK(deviceNotify_cb), this);
devices_.emplace(Devices::value_type(objectPath, upDevice));
}
}
void UPower::removeDevice(const gchar* objectPath) {
std::lock_guard<std::mutex> guard{mutex_};
if (devices_.find(objectPath) != devices_.cend()) {
auto upDevice{devices_[objectPath]};
if (G_IS_OBJECT(upDevice.upDevice)) g_object_unref(upDevice.upDevice);
devices_.erase(objectPath);
}
}
void UPower::removeDevices() {
std::lock_guard<std::mutex> guard{mutex_};
if (!devices_.empty()) {
auto it{devices_.cbegin()};
while (it != devices_.cend()) {
if (G_IS_OBJECT(it->second.upDevice)) g_object_unref(it->second.upDevice);
devices_.erase(it++);
}
}
}
// Removes all devices and adds the current devices
void UPower::resetDevices() {
// Remove all devices
removeDevices();
// Adds all devices
GPtrArray* newDevices = up_client_get_devices2(upClient_);
if (newDevices != NULL)
for (guint i{0}; i < newDevices->len; ++i) {
UpDevice* device{(UpDevice*)g_ptr_array_index(newDevices, i)};
if (device && G_IS_OBJECT(device)) addDevice(device);
}
}
void UPower::setDisplayDevice() {
std::lock_guard<std::mutex> guard{mutex_};
if (upDevice_.upDevice != NULL) {
g_object_unref(upDevice_.upDevice);
upDevice_.upDevice = NULL;
}
if (nativePath_.empty() && model_.empty()) {
upDevice_.upDevice = up_client_get_display_device(upClient_);
getUpDeviceInfo(upDevice_);
} else {
g_ptr_array_foreach(
up_client_get_devices2(upClient_),
[](gpointer data, gpointer user_data) {
upDevice_output upDevice;
auto thisPtr{static_cast<UPower*>(user_data)};
upDevice.upDevice = static_cast<UpDevice*>(data);
thisPtr->getUpDeviceInfo(upDevice);
upDevice_output displayDevice{NULL};
if (!thisPtr->nativePath_.empty()) {
if (upDevice.nativePath == nullptr) return;
if (0 == std::strcmp(upDevice.nativePath, thisPtr->nativePath_.c_str())) {
displayDevice = upDevice;
}
} else {
if (upDevice.model == nullptr) return;
if (0 == std::strcmp(upDevice.model, thisPtr->model_.c_str())) {
displayDevice = upDevice;
}
}
// Unref current upDevice if it exists
if (displayDevice.upDevice != NULL) {
thisPtr->upDevice_ = displayDevice;
}
},
this);
}
if (upDevice_.upDevice != NULL)
g_signal_connect(upDevice_.upDevice, "notify", G_CALLBACK(deviceNotify_cb), this);
}
void UPower::getUpDeviceInfo(upDevice_output& upDevice_) {
if (upDevice_.upDevice != NULL && G_IS_OBJECT(upDevice_.upDevice)) {
g_object_get(upDevice_.upDevice, "kind", &upDevice_.kind, "state", &upDevice_.state,
"percentage", &upDevice_.percentage, "icon-name", &upDevice_.icon_name,
"time-to-empty", &upDevice_.time_empty, "time-to-full", &upDevice_.time_full,
"temperature", &upDevice_.temperature, "native-path", &upDevice_.nativePath,
"model", &upDevice_.model, NULL);
spdlog::debug(
"UPower. getUpDeviceInfo. kind: \"{0}\". state: \"{1}\". percentage: \"{2}\". \
icon_name: \"{3}\". time-to-empty: \"{4}\". time-to-full: \"{5}\". temperature: \"{6}\". \
native_path: \"{7}\". model: \"{8}\"",
fmt::format_int(upDevice_.kind).str(), fmt::format_int(upDevice_.state).str(),
upDevice_.percentage, upDevice_.icon_name, upDevice_.time_empty, upDevice_.time_full,
upDevice_.temperature, upDevice_.nativePath, upDevice_.model);
}
}
const Glib::ustring UPower::getText(const upDevice_output& upDevice_, const std::string& format) {
Glib::ustring ret{""};
if (upDevice_.upDevice != NULL) {
std::string timeStr{""};
switch (upDevice_.state) {
case UP_DEVICE_STATE_CHARGING:
case UP_DEVICE_STATE_PENDING_CHARGE:
timeStr = secondsToString(std::chrono::seconds(upDevice_.time_full));
break;
case UP_DEVICE_STATE_DISCHARGING:
case UP_DEVICE_STATE_PENDING_DISCHARGE:
timeStr = secondsToString(std::chrono::seconds(upDevice_.time_empty));
break;
default:
break;
}
ret = fmt::format(
fmt::runtime(format),
fmt::arg("percentage", std::to_string((int)std::round(upDevice_.percentage)) + '%'),
fmt::arg("time", timeStr),
fmt::arg("temperature", fmt::format("{:-.2g}C", upDevice_.temperature)),
fmt::arg("model", upDevice_.model), fmt::arg("native-path", upDevice_.nativePath));
}
return ret;
}
bool UPower::queryTooltipCb(int x, int y, bool keyboard_tooltip,
const Glib::RefPtr<Gtk::Tooltip>& tooltip) {
std::lock_guard<std::mutex> guard{mutex_};
// Clear content box
contentBox_.forall([this](Gtk::Widget& wg) { contentBox_.remove(wg); });
// Fill content box with the content
for (auto pairDev : devices_) {
// Get device info
getUpDeviceInfo(pairDev.second);
if (pairDev.second.kind != UpDeviceKind::UP_DEVICE_KIND_UNKNOWN &&
pairDev.second.kind != UpDeviceKind::UP_DEVICE_KIND_LINE_POWER) {
// Make box record
Gtk::Box* boxRec{new Gtk::Box{box_.get_orientation(), tooltip_spacing_}};
contentBox_.add(*boxRec);
Gtk::Box* boxDev{new Gtk::Box{box_.get_orientation()}};
Gtk::Box* boxUsr{new Gtk::Box{box_.get_orientation()}};
boxRec->add(*boxDev);
boxRec->add(*boxUsr);
// Construct device box
// Set icon from kind
std::string iconNameDev{getDeviceIcon(pairDev.second.kind)};
if (!gtkTheme_->has_icon(iconNameDev)) iconNameDev = (char*)NO_BATTERY.c_str();
Gtk::Image* iconDev{new Gtk::Image{}};
iconDev->set_from_icon_name(iconNameDev, Gtk::ICON_SIZE_INVALID);
iconDev->set_pixel_size(iconSize_);
boxDev->add(*iconDev);
// Set label from model
Gtk::Label* labelDev{new Gtk::Label{pairDev.second.model}};
boxDev->add(*labelDev);
// Construct user box
// Set icon from icon state
if (pairDev.second.icon_name == NULL || !gtkTheme_->has_icon(pairDev.second.icon_name))
pairDev.second.icon_name = (char*)NO_BATTERY.c_str();
Gtk::Image* iconTooltip{new Gtk::Image{}};
iconTooltip->set_from_icon_name(pairDev.second.icon_name, Gtk::ICON_SIZE_INVALID);
iconTooltip->set_pixel_size(iconSize_);
boxUsr->add(*iconTooltip);
// Set markup text
Gtk::Label* labelTooltip{new Gtk::Label{}};
labelTooltip->set_markup(getText(pairDev.second, tooltipFormat_));
boxUsr->add(*labelTooltip);
}
}
tooltip->set_custom(contentBox_);
contentBox_.show_all();
return true;
}
} // namespace waybar::modules
-397
View File
@@ -1,397 +0,0 @@
#include "modules/upower/upower.hpp"
#include <fmt/core.h>
#include <cstring>
#include <string>
#include "gtkmm/tooltip.h"
#include "util/gtk_icon.hpp"
namespace waybar::modules::upower {
UPower::UPower(const std::string& id, const Json::Value& config)
: AModule(config, "upower", id),
box_(Gtk::ORIENTATION_HORIZONTAL, 0),
icon_(),
label_(),
devices(),
m_Mutex(),
client(),
showAltText(false) {
// Show icon only when "show-icon" isn't set to false
if (config_["show-icon"].isBool()) {
showIcon = config_["show-icon"].asBool();
}
if (showIcon) {
box_.pack_start(icon_);
}
box_.pack_start(label_);
box_.set_name(name_);
event_box_.add(box_);
// Device user wants
if (config_["native-path"].isString()) nativePath_ = config_["native-path"].asString();
// Icon Size
if (config_["icon-size"].isUInt()) {
iconSize = config_["icon-size"].asUInt();
}
icon_.set_pixel_size(iconSize);
// Hide If Empty
if (config_["hide-if-empty"].isBool()) {
hideIfEmpty = config_["hide-if-empty"].asBool();
}
// Format
if (config_["format"].isString()) {
format = config_["format"].asString();
}
// Format Alt
if (config_["format-alt"].isString()) {
format_alt = config_["format-alt"].asString();
}
// Tooltip Spacing
if (config_["tooltip-spacing"].isUInt()) {
tooltip_spacing = config_["tooltip-spacing"].asUInt();
}
// Tooltip Padding
if (config_["tooltip-padding"].isUInt()) {
tooltip_padding = config_["tooltip-padding"].asUInt();
}
// Tooltip
if (config_["tooltip"].isBool()) {
tooltip_enabled = config_["tooltip"].asBool();
}
box_.set_has_tooltip(tooltip_enabled);
if (tooltip_enabled) {
// Sets the window to use when showing the tooltip
upower_tooltip = std::make_unique<UPowerTooltip>(iconSize, tooltip_spacing, tooltip_padding);
box_.set_tooltip_window(*upower_tooltip);
box_.signal_query_tooltip().connect(sigc::mem_fun(*this, &UPower::show_tooltip_callback));
}
upowerWatcher_id = g_bus_watch_name(G_BUS_TYPE_SYSTEM, "org.freedesktop.UPower",
G_BUS_NAME_WATCHER_FLAGS_AUTO_START, upowerAppear,
upowerDisappear, this, NULL);
client = up_client_new_full(NULL, NULL);
if (client == NULL) {
throw std::runtime_error("Unable to create UPower client!");
}
// Connect to Login1 PrepareForSleep signal
login1_connection = g_bus_get_sync(G_BUS_TYPE_SYSTEM, NULL, NULL);
if (!login1_connection) {
throw std::runtime_error("Unable to connect to the SYSTEM Bus!...");
} else {
login1_id = g_dbus_connection_signal_subscribe(
login1_connection, "org.freedesktop.login1", "org.freedesktop.login1.Manager",
"PrepareForSleep", "/org/freedesktop/login1", NULL, G_DBUS_SIGNAL_FLAGS_NONE,
prepareForSleep_cb, this, NULL);
}
event_box_.signal_button_press_event().connect(sigc::mem_fun(*this, &UPower::handleToggle));
g_signal_connect(client, "device-added", G_CALLBACK(deviceAdded_cb), this);
g_signal_connect(client, "device-removed", G_CALLBACK(deviceRemoved_cb), this);
resetDevices();
setDisplayDevice();
}
UPower::~UPower() {
if (displayDevice != NULL) g_object_unref(displayDevice);
if (client != NULL) g_object_unref(client);
if (login1_id > 0) {
g_dbus_connection_signal_unsubscribe(login1_connection, login1_id);
login1_id = 0;
}
g_bus_unwatch_name(upowerWatcher_id);
removeDevices();
}
void UPower::deviceAdded_cb(UpClient* client, UpDevice* device, gpointer data) {
UPower* up = static_cast<UPower*>(data);
up->addDevice(device);
up->setDisplayDevice();
// Update the widget
up->dp.emit();
}
void UPower::deviceRemoved_cb(UpClient* client, const gchar* objectPath, gpointer data) {
UPower* up = static_cast<UPower*>(data);
up->removeDevice(objectPath);
up->setDisplayDevice();
// Update the widget
up->dp.emit();
}
void UPower::deviceNotify_cb(UpDevice* device, GParamSpec* pspec, gpointer data) {
UPower* up = static_cast<UPower*>(data);
// Update the widget
up->dp.emit();
}
void UPower::prepareForSleep_cb(GDBusConnection* system_bus, const gchar* sender_name,
const gchar* object_path, const gchar* interface_name,
const gchar* signal_name, GVariant* parameters, gpointer data) {
if (g_variant_is_of_type(parameters, G_VARIANT_TYPE("(b)"))) {
gboolean sleeping;
g_variant_get(parameters, "(b)", &sleeping);
if (!sleeping) {
UPower* up = static_cast<UPower*>(data);
up->resetDevices();
up->setDisplayDevice();
}
}
}
void UPower::upowerAppear(GDBusConnection* conn, const gchar* name, const gchar* name_owner,
gpointer data) {
UPower* up = static_cast<UPower*>(data);
up->upowerRunning = true;
up->event_box_.set_visible(true);
}
void UPower::upowerDisappear(GDBusConnection* conn, const gchar* name, gpointer data) {
UPower* up = static_cast<UPower*>(data);
up->upowerRunning = false;
up->event_box_.set_visible(false);
}
void UPower::removeDevice(const gchar* objectPath) {
std::lock_guard<std::mutex> guard(m_Mutex);
if (devices.find(objectPath) != devices.end()) {
UpDevice* device = devices[objectPath];
if (G_IS_OBJECT(device)) {
g_object_unref(device);
}
devices.erase(objectPath);
}
}
void UPower::addDevice(UpDevice* device) {
if (G_IS_OBJECT(device)) {
const gchar* objectPath = up_device_get_object_path(device);
// Due to the device getting cleared after this event is fired, we
// create a new object pointing to its objectPath
gboolean ret;
device = up_device_new();
ret = up_device_set_object_path_sync(device, objectPath, NULL, NULL);
if (!ret) {
g_object_unref(G_OBJECT(device));
return;
}
std::lock_guard<std::mutex> guard(m_Mutex);
if (devices.find(objectPath) != devices.end()) {
UpDevice* device = devices[objectPath];
if (G_IS_OBJECT(device)) {
g_object_unref(device);
}
devices.erase(objectPath);
}
g_signal_connect(device, "notify", G_CALLBACK(deviceNotify_cb), this);
devices.emplace(Devices::value_type(objectPath, device));
}
}
void UPower::setDisplayDevice() {
std::lock_guard<std::mutex> guard(m_Mutex);
if (nativePath_.empty())
displayDevice = up_client_get_display_device(client);
else {
g_ptr_array_foreach(
up_client_get_devices2(client),
[](gpointer data, gpointer user_data) {
UpDevice* device{static_cast<UpDevice*>(data)};
UPower* thisPtr{static_cast<UPower*>(user_data)};
gchar* nativePath;
if (!thisPtr->displayDevice) {
g_object_get(device, "native-path", &nativePath, NULL);
if (!std::strcmp(nativePath, thisPtr->nativePath_.c_str()))
thisPtr->displayDevice = device;
}
},
this);
}
if (displayDevice) g_signal_connect(displayDevice, "notify", G_CALLBACK(deviceNotify_cb), this);
}
void UPower::removeDevices() {
std::lock_guard<std::mutex> guard(m_Mutex);
if (!devices.empty()) {
auto it = devices.cbegin();
while (it != devices.cend()) {
if (G_IS_OBJECT(it->second)) {
g_object_unref(it->second);
}
devices.erase(it++);
}
}
}
/** Removes all devices and adds the current devices */
void UPower::resetDevices() {
// Removes all devices
removeDevices();
// Adds all devices
GPtrArray* newDevices = up_client_get_devices2(client);
for (guint i = 0; i < newDevices->len; i++) {
UpDevice* device = (UpDevice*)g_ptr_array_index(newDevices, i);
if (device && G_IS_OBJECT(device)) addDevice(device);
}
// Update the widget
dp.emit();
}
bool UPower::show_tooltip_callback(int, int, bool, const Glib::RefPtr<Gtk::Tooltip>& tooltip) {
return true;
}
const std::string UPower::getDeviceStatus(UpDeviceState& state) {
switch (state) {
case UP_DEVICE_STATE_CHARGING:
case UP_DEVICE_STATE_PENDING_CHARGE:
return "charging";
case UP_DEVICE_STATE_DISCHARGING:
case UP_DEVICE_STATE_PENDING_DISCHARGE:
return "discharging";
case UP_DEVICE_STATE_FULLY_CHARGED:
return "full";
case UP_DEVICE_STATE_EMPTY:
return "empty";
default:
return "unknown-status";
}
}
bool UPower::handleToggle(GdkEventButton* const& event) {
std::lock_guard<std::mutex> guard(m_Mutex);
showAltText = !showAltText;
return AModule::handleToggle(event);
}
std::string UPower::timeToString(gint64 time) {
if (time == 0) return "";
float hours = (float)time / 3600;
float hours_fixed = static_cast<float>(static_cast<int>(hours * 10)) / 10;
float minutes = static_cast<float>(static_cast<int>(hours * 60 * 10)) / 10;
if (hours_fixed >= 1) {
return fmt::format("{H} h", fmt::arg("H", hours_fixed));
} else {
return fmt::format("{M} min", fmt::arg("M", minutes));
}
}
auto UPower::update() -> void {
std::lock_guard<std::mutex> guard(m_Mutex);
// Don't update widget if the UPower service isn't running
if (!upowerRunning) {
if (hideIfEmpty) {
event_box_.set_visible(false);
}
return;
}
UpDeviceKind kind;
UpDeviceState state;
double percentage;
gint64 time_empty;
gint64 time_full;
gchar* icon_name{(char*)'\0'};
std::string percentString{""};
std::string time_format{""};
bool displayDeviceValid{false};
if (displayDevice) {
g_object_get(displayDevice, "kind", &kind, "state", &state, "percentage", &percentage,
"icon-name", &icon_name, "time-to-empty", &time_empty, "time-to-full", &time_full,
NULL);
/* Every Device which is handled by Upower and which is not
* UP_DEVICE_KIND_UNKNOWN (0) or UP_DEVICE_KIND_LINE_POWER (1) is a Battery
*/
displayDeviceValid = (kind != UpDeviceKind::UP_DEVICE_KIND_UNKNOWN &&
kind != UpDeviceKind::UP_DEVICE_KIND_LINE_POWER);
}
// CSS status class
const std::string status = getDeviceStatus(state);
// Remove last status if it exists
if (!lastStatus.empty() && box_.get_style_context()->has_class(lastStatus)) {
box_.get_style_context()->remove_class(lastStatus);
}
// Add the new status class to the Box
if (!box_.get_style_context()->has_class(status)) {
box_.get_style_context()->add_class(status);
}
lastStatus = status;
if (devices.size() == 0 && !displayDeviceValid && hideIfEmpty) {
event_box_.set_visible(false);
// Call parent update
AModule::update();
return;
}
event_box_.set_visible(true);
if (displayDeviceValid) {
// Tooltip
if (tooltip_enabled) {
uint tooltipCount = upower_tooltip->updateTooltip(devices);
// Disable the tooltip if there aren't any devices in the tooltip
box_.set_has_tooltip(!devices.empty() && tooltipCount > 0);
}
// Set percentage
percentString = std::to_string(int(percentage + 0.5)) + "%";
// Label format
switch (state) {
case UP_DEVICE_STATE_CHARGING:
case UP_DEVICE_STATE_PENDING_CHARGE:
time_format = timeToString(time_full);
break;
case UP_DEVICE_STATE_DISCHARGING:
case UP_DEVICE_STATE_PENDING_DISCHARGE:
time_format = timeToString(time_empty);
break;
default:
break;
}
}
std::string label_format =
fmt::format(fmt::runtime(showAltText ? format_alt : format),
fmt::arg("percentage", percentString), fmt::arg("time", time_format));
// Only set the label text if it doesn't only contain spaces
bool onlySpaces = true;
for (auto& character : label_format) {
if (character == ' ') continue;
onlySpaces = false;
break;
}
label_.set_markup(onlySpaces ? "" : label_format);
// Set icon
if (icon_name == NULL || !DefaultGtkIconThemeWrapper::has_icon(icon_name)) {
icon_name = (char*)"battery-missing-symbolic";
}
icon_.set_from_icon_name(icon_name, Gtk::ICON_SIZE_INVALID);
// Call parent update
AModule::update();
}
} // namespace waybar::modules::upower
-160
View File
@@ -1,160 +0,0 @@
#include "modules/upower/upower_tooltip.hpp"
#include "gtkmm/box.h"
#include "gtkmm/enums.h"
#include "gtkmm/image.h"
#include "gtkmm/label.h"
#include "util/gtk_icon.hpp"
namespace waybar::modules::upower {
UPowerTooltip::UPowerTooltip(uint iconSize_, uint tooltipSpacing_, uint tooltipPadding_)
: Gtk::Window(),
contentBox(std::make_unique<Gtk::Box>(Gtk::ORIENTATION_VERTICAL)),
iconSize(iconSize_),
tooltipSpacing(tooltipSpacing_),
tooltipPadding(tooltipPadding_) {
// Sets the Tooltip Padding
contentBox->set_margin_top(tooltipPadding);
contentBox->set_margin_bottom(tooltipPadding);
contentBox->set_margin_left(tooltipPadding);
contentBox->set_margin_right(tooltipPadding);
add(*contentBox);
contentBox->show();
}
UPowerTooltip::~UPowerTooltip() {}
uint UPowerTooltip::updateTooltip(Devices& devices) {
// Removes all old devices
for (auto child : contentBox->get_children()) {
delete child;
}
uint deviceCount = 0;
// Adds all valid devices
for (auto pair : devices) {
UpDevice* device = pair.second;
std::string objectPath = pair.first;
if (!G_IS_OBJECT(device)) continue;
Gtk::Box* box = new Gtk::Box(Gtk::ORIENTATION_HORIZONTAL, tooltipSpacing);
UpDeviceKind kind;
double percentage;
gchar* native_path;
gchar* model;
gchar* icon_name;
g_object_get(device, "kind", &kind, "percentage", &percentage, "native-path", &native_path,
"model", &model, "icon-name", &icon_name, NULL);
// Skip Line_Power and BAT0 devices
if (kind == UP_DEVICE_KIND_LINE_POWER || native_path == NULL || strlen(native_path) == 0 ||
strcmp(native_path, "BAT0") == 0)
continue;
Gtk::Box* modelBox = new Gtk::Box(Gtk::ORIENTATION_HORIZONTAL);
box->add(*modelBox);
// Set device icon
std::string deviceIconName = getDeviceIcon(kind);
Gtk::Image* deviceIcon = new Gtk::Image();
deviceIcon->set_pixel_size(iconSize);
if (!DefaultGtkIconThemeWrapper::has_icon(deviceIconName)) {
deviceIconName = "battery-missing-symbolic";
}
deviceIcon->set_from_icon_name(deviceIconName, Gtk::ICON_SIZE_INVALID);
modelBox->add(*deviceIcon);
// Set model
if (model == NULL) model = (gchar*)"";
Gtk::Label* modelLabel = new Gtk::Label(model);
modelBox->add(*modelLabel);
Gtk::Box* chargeBox = new Gtk::Box(Gtk::ORIENTATION_HORIZONTAL);
box->add(*chargeBox);
// Set icon
Gtk::Image* icon = new Gtk::Image();
icon->set_pixel_size(iconSize);
if (icon_name == NULL || !DefaultGtkIconThemeWrapper::has_icon(icon_name)) {
icon_name = (char*)"battery-missing-symbolic";
}
icon->set_from_icon_name(icon_name, Gtk::ICON_SIZE_INVALID);
chargeBox->add(*icon);
// Set percentage
std::string percentString = std::to_string(int(percentage + 0.5)) + "%";
Gtk::Label* percentLabel = new Gtk::Label(percentString);
chargeBox->add(*percentLabel);
contentBox->add(*box);
deviceCount++;
}
contentBox->show_all();
return deviceCount;
}
const std::string UPowerTooltip::getDeviceIcon(UpDeviceKind& kind) {
switch (kind) {
case UP_DEVICE_KIND_LINE_POWER:
return "ac-adapter-symbolic";
case UP_DEVICE_KIND_BATTERY:
return "battery";
case UP_DEVICE_KIND_UPS:
return "uninterruptible-power-supply-symbolic";
case UP_DEVICE_KIND_MONITOR:
return "video-display-symbolic";
case UP_DEVICE_KIND_MOUSE:
return "input-mouse-symbolic";
case UP_DEVICE_KIND_KEYBOARD:
return "input-keyboard-symbolic";
case UP_DEVICE_KIND_PDA:
return "pda-symbolic";
case UP_DEVICE_KIND_PHONE:
return "phone-symbolic";
case UP_DEVICE_KIND_MEDIA_PLAYER:
return "multimedia-player-symbolic";
case UP_DEVICE_KIND_TABLET:
return "computer-apple-ipad-symbolic";
case UP_DEVICE_KIND_COMPUTER:
return "computer-symbolic";
case UP_DEVICE_KIND_GAMING_INPUT:
return "input-gaming-symbolic";
case UP_DEVICE_KIND_PEN:
return "input-tablet-symbolic";
case UP_DEVICE_KIND_TOUCHPAD:
return "input-touchpad-symbolic";
case UP_DEVICE_KIND_MODEM:
return "modem-symbolic";
case UP_DEVICE_KIND_NETWORK:
return "network-wired-symbolic";
case UP_DEVICE_KIND_HEADSET:
return "audio-headset-symbolic";
case UP_DEVICE_KIND_HEADPHONES:
return "audio-headphones-symbolic";
case UP_DEVICE_KIND_OTHER_AUDIO:
case UP_DEVICE_KIND_SPEAKERS:
return "audio-speakers-symbolic";
case UP_DEVICE_KIND_VIDEO:
return "camera-web-symbolic";
case UP_DEVICE_KIND_PRINTER:
return "printer-symbolic";
case UP_DEVICE_KIND_SCANNER:
return "scanner-symbolic";
case UP_DEVICE_KIND_CAMERA:
return "camera-photo-symbolic";
case UP_DEVICE_KIND_BLUETOOTH_GENERIC:
return "bluetooth-active-symbolic";
case UP_DEVICE_KIND_TOY:
case UP_DEVICE_KIND_REMOTE_CONTROL:
case UP_DEVICE_KIND_WEARABLE:
case UP_DEVICE_KIND_LAST:
default:
return "battery-symbolic";
}
}
} // namespace waybar::modules::upower
+7 -2
View File
@@ -7,7 +7,6 @@
#include <algorithm>
#include <chrono>
#include "gdkmm/cursor.h"
#include "gdkmm/event.h"
#include "gdkmm/types.h"
#include "glibmm/fileutils.h"
@@ -62,7 +61,13 @@ long User::uptime_as_seconds() {
#if HAVE_CPU_BSD
struct timespec s_info;
if (0 == clock_gettime(CLOCK_UPTIME_PRECISE, &s_info)) {
int flags = 0;
#ifndef __OpenBSD__
flags = CLOCK_UPTIME_PRECISE;
#else
flags = CLOCK_UPTIME;
#endif
if (0 == clock_gettime(flags, &s_info)) {
uptime = s_info.tv_sec;
}
#endif
+462
View File
@@ -0,0 +1,462 @@
#include "modules/wayfire/backend.hpp"
#include <json/json.h>
#include <spdlog/spdlog.h>
#include <sys/socket.h>
#include <sys/un.h>
#include <algorithm>
#include <bit>
#include <cstdint>
#include <cstdlib>
#include <cstring>
#include <exception>
#include <ranges>
#include <thread>
namespace waybar::modules::wayfire {
std::weak_ptr<IPC> IPC::instance;
// C++23: std::byteswap
inline auto byteswap(uint32_t x) -> uint32_t {
return (x & 0xff000000) >> 24 | (x & 0x00ff0000) >> 8 | (x & 0x0000ff00) << 8 |
(x & 0x000000ff) << 24;
}
auto pack_and_write(Sock& sock, std::string&& buf) -> void {
uint32_t len = buf.size();
if constexpr (std::endian::native != std::endian::little) len = byteswap(len);
(void)write(sock, &len, 4);
(void)write(sock, buf.data(), buf.size());
}
auto read_exact(Sock& sock, size_t n) -> std::string {
auto buf = std::string(n, 0);
for (size_t i = 0; i < n;) {
auto r = read(sock, &buf[i], n - i);
if (r <= 0) {
throw std::runtime_error("Wayfire IPC: read failed");
}
i += static_cast<size_t>(r);
}
return buf;
}
// https://github.com/WayfireWM/pywayfire/blob/69b7c21/wayfire/ipc.py#L438
inline auto is_mapped_toplevel_view(const Json::Value& view) -> bool {
return view["mapped"].asBool() && view["role"] != "desktop-environment" &&
view["pid"].asInt() != -1;
}
auto State::Wset::count_ws(const Json::Value& pos) -> Workspace& {
auto x = pos["x"].asInt();
auto y = pos["y"].asInt();
return wss.at(ws_w * y + x);
}
auto State::Wset::locate_ws(const Json::Value& geo) -> Workspace& {
return const_cast<Workspace&>(std::as_const(*this).locate_ws(geo));
}
auto State::Wset::locate_ws(const Json::Value& geo) const -> const Workspace& {
if (!output.has_value()) {
throw std::runtime_error("Wayfire IPC: wset has no output assigned");
}
const auto& out = output.value().get();
auto [qx, rx] = std::div(geo["x"].asInt(), out.w);
auto [qy, ry] = std::div(geo["y"].asInt(), out.h);
auto x = std::max(0, (int)ws_x + qx - int{rx < 0});
auto y = std::max(0, (int)ws_y + qy - int{ry < 0});
return wss.at(ws_w * y + x);
}
auto State::update_view(const Json::Value& view) -> void {
auto id = view["id"].asUInt();
// erase old view information
if (views.contains(id)) {
auto& old_view = views.at(id);
auto& ws = wsets.at(old_view["wset-index"].asUInt()).locate_ws(old_view["geometry"]);
ws.num_views--;
if (old_view["sticky"].asBool()) ws.num_sticky_views--;
views.erase(id);
}
// insert or assign new view information
if (is_mapped_toplevel_view(view)) {
try {
// view["wset-index"] could be messed up
auto& ws = wsets.at(view["wset-index"].asUInt()).locate_ws(view["geometry"]);
ws.num_views++;
if (view["sticky"].asBool()) ws.num_sticky_views++;
views.emplace(id, view);
} catch (const std::exception&) {
}
}
}
auto IPC::get_instance() -> std::shared_ptr<IPC> {
auto p = instance.lock();
if (!p) {
instance = p = std::shared_ptr<IPC>(new IPC);
p->start();
}
return p;
}
auto IPC::connect() -> Sock {
auto* path = std::getenv("WAYFIRE_SOCKET");
if (path == nullptr) {
throw std::runtime_error{"Wayfire IPC: ipc not available"};
}
util::ScopedFd sock(socket(AF_UNIX, SOCK_STREAM, 0));
if (sock == -1) {
throw std::runtime_error{"Wayfire IPC: socket() failed"};
}
auto addr = sockaddr_un{.sun_family = AF_UNIX};
std::strncpy(addr.sun_path, path, sizeof(addr.sun_path) - 1);
addr.sun_path[sizeof(addr.sun_path) - 1] = 0;
if (::connect(sock, (const sockaddr*)&addr, sizeof(addr)) == -1) {
throw std::runtime_error{"Wayfire IPC: connect() failed"};
}
return sock;
}
auto IPC::receive(Sock& sock) -> Json::Value {
auto len_buf = read_exact(sock, 4);
uint32_t len;
std::memcpy(&len, len_buf.data(), sizeof(len));
if constexpr (std::endian::native != std::endian::little) len = byteswap(len);
auto buf = read_exact(sock, len);
Json::Value json;
std::string err;
auto* reader = reader_builder.newCharReader();
if (!reader->parse(&*buf.begin(), &*buf.end(), &json, &err)) {
throw std::runtime_error{"Wayfire IPC: parse json failed: " + err};
}
return json;
}
auto IPC::send(const std::string& method, Json::Value&& data) -> Json::Value {
spdlog::debug("Wayfire IPC: send method \"{}\"", method);
auto sock = connect();
Json::Value json;
json["method"] = method;
json["data"] = std::move(data);
pack_and_write(sock, Json::writeString(writer_builder, json));
auto res = receive(sock);
root_event_handler(method, res);
return res;
}
auto IPC::start() -> void {
spdlog::info("Wayfire IPC: starting");
// init state
send("window-rules/list-outputs", {});
send("window-rules/list-wsets", {});
send("window-rules/list-views", {});
send("window-rules/get-focused-view", {});
send("window-rules/get-focused-output", {});
std::thread([self = shared_from_this()] {
auto sock = connect();
{
Json::Value json;
json["method"] = "window-rules/events/watch";
pack_and_write(sock, Json::writeString(self->writer_builder, json));
if (self->receive(sock)["result"] != "ok") {
spdlog::error(
"Wayfire IPC: method \"window-rules/events/watch\""
" have failed");
return;
}
}
while (auto json = self->receive(sock)) {
auto ev = json["event"].asString();
spdlog::debug("Wayfire IPC: received event \"{}\"", ev);
self->root_event_handler(ev, json);
}
}).detach();
}
auto IPC::register_handler(const std::string& event, const EventHandler& handler) -> void {
auto _ = std::lock_guard{handlers_mutex};
handlers.emplace_back(event, handler);
}
auto IPC::unregister_handler(EventHandler& handler) -> void {
auto _ = std::lock_guard{handlers_mutex};
handlers.remove_if([&](auto& e) { return &e.second.get() == &handler; });
}
auto IPC::root_event_handler(const std::string& event, const Json::Value& data) -> void {
bool new_output_detected;
{
auto _ = lock_state();
update_state_handler(event, data);
new_output_detected = state.new_output_detected;
state.new_output_detected = false;
}
if (new_output_detected) {
send("window-rules/list-outputs", {});
send("window-rules/list-wsets", {});
}
{
auto _ = std::lock_guard{handlers_mutex};
for (const auto& [_event, handler] : handlers)
if (_event == event) handler(event);
}
}
auto IPC::update_state_handler(const std::string& event, const Json::Value& data) -> void {
// IPC events
// https://github.com/WayfireWM/wayfire/blob/053b222/plugins/ipc-rules/ipc-events.hpp#L108-L125
/*
[x] view-mapped
[x] view-unmapped
[-] view-set-output // for detect new output
[ ] view-geometry-changed // -> view-workspace-changed
[x] view-wset-changed
[x] view-focused
[x] view-title-changed
[x] view-app-id-changed
[x] plugin-activation-state-changed
[x] output-gain-focus
[ ] view-tiled
[ ] view-minimized
[ ] view-fullscreened
[x] view-sticky
[x] view-workspace-changed
[x] output-wset-changed
[x] wset-workspace-changed
*/
if (event == "view-mapped") {
// data: { event, view }
state.update_view(data["view"]);
return;
}
if (event == "view-unmapped") {
// data: { event, view }
try {
// data["view"]["wset-index"] could be messed up
state.update_view(data["view"]);
state.maybe_empty_focus_wset_idx = data["view"]["wset-index"].asUInt();
} catch (const std::exception&) {
}
return;
}
if (event == "view-set-output") {
// data: { event, output?, view }
// new output event
if (!state.outputs.contains(data["view"]["output-name"].asString())) {
state.new_output_detected = true;
}
return;
}
if (event == "view-wset-changed") {
// data: { event, old-wset: wset, new-wset: wset, view }
state.maybe_empty_focus_wset_idx = data["old-wset"]["index"].asUInt();
state.update_view(data["view"]);
return;
}
if (event == "view-focused") {
// data: { event, view? }
if (const auto& view = data["view"]) {
try {
// view["wset-index"] could be messed up
auto& wset = state.wsets.at(view["wset-index"].asUInt());
wset.focused_view_id = view["id"].asUInt();
} catch (const std::exception&) {
}
} else {
// focused to null
if (state.wsets.contains(state.maybe_empty_focus_wset_idx))
state.wsets.at(state.maybe_empty_focus_wset_idx).focused_view_id = {};
}
return;
}
if (event == "view-title-changed" || event == "view-app-id-changed" || event == "view-sticky") {
// data: { event, view }
state.update_view(data["view"]);
return;
}
if (event == "plugin-activation-state-changed") {
// data: { event, plugin: name, state: bool, output: id, output-data: output }
auto plugin = data["plugin"].asString();
auto plugin_state = data["state"].asBool();
if (plugin == "vswitch") {
state.vswitching = plugin_state;
if (plugin_state) {
state.maybe_empty_focus_wset_idx = data["output-data"]["wset-index"].asUInt();
}
}
return;
}
if (event == "output-gain-focus") {
// data: { event, output }
state.focused_output_name = data["output"]["name"].asString();
return;
}
if (event == "view-workspace-changed") {
// data: { event, from: point, to: point, view }
if (state.vswitching) {
if (state.vswitch_sticky_view_id == 0) {
auto& wset = state.wsets.at(data["view"]["wset-index"].asUInt());
auto& old_ws = wset.locate_ws(state.views.at(data["view"]["id"].asUInt())["geometry"]);
auto& new_ws = wset.count_ws(data["to"]);
old_ws.num_views--;
new_ws.num_views++;
if (data["view"]["sticky"].asBool()) {
old_ws.num_sticky_views--;
new_ws.num_sticky_views++;
}
state.update_view(data["view"]);
state.vswitch_sticky_view_id = data["view"]["id"].asUInt();
} else {
state.vswitch_sticky_view_id = {};
}
return;
}
state.update_view(data["view"]);
return;
}
if (event == "output-wset-changed") {
// data: { event, new-wset: wset.name, output: id, new-wset-data: wset, output-data: output }
try {
auto& output = state.outputs.at(data["output-data"]["name"].asString());
auto wset_idx = data["new-wset-data"]["index"].asUInt();
state.wsets.at(wset_idx).output = output;
output.wset_idx = wset_idx;
} catch (const std::exception&) {
}
return;
}
if (event == "wset-workspace-changed") {
// data: { event, previous-workspace: point, new-workspace: point,
// output: id, wset: wset.name, output-data: output, wset-data: wset }
auto wset_idx = data["wset-data"]["index"].asUInt();
auto& wset = state.wsets.at(wset_idx);
wset.ws_x = data["new-workspace"]["x"].asUInt();
wset.ws_y = data["new-workspace"]["y"].asUInt();
// correct existing views geometry
auto& out = wset.output.value().get();
auto dx = (int)out.w * ((int)wset.ws_x - data["previous-workspace"]["x"].asInt());
auto dy = (int)out.h * ((int)wset.ws_y - data["previous-workspace"]["y"].asInt());
for (auto& [_, view] : state.views) {
if (view["wset-index"].asUInt() == wset_idx &&
view["id"].asUInt() != state.vswitch_sticky_view_id) {
view["geometry"]["x"] = view["geometry"]["x"].asInt() - dx;
view["geometry"]["y"] = view["geometry"]["y"].asInt() - dy;
}
}
return;
}
// IPC responses
// https://github.com/WayfireWM/wayfire/blob/053b222/plugins/ipc-rules/ipc-rules.cpp#L27-L37
if (event == "window-rules/list-views") {
// data: [ view ]
state.views.clear();
for (auto& [_, wset] : state.wsets) std::ranges::fill(wset.wss, State::Workspace{});
for (const auto& view : data | std::views::filter(is_mapped_toplevel_view)) {
state.update_view(view);
}
return;
}
if (event == "window-rules/list-outputs") {
// data: [ output ]
state.outputs.clear();
for (const auto& output_data : data) {
state.outputs.emplace(output_data["name"].asString(),
State::Output{
.id = output_data["id"].asUInt(),
.w = output_data["geometry"]["width"].asUInt(),
.h = output_data["geometry"]["height"].asUInt(),
.wset_idx = output_data["wset-index"].asUInt(),
});
}
return;
}
if (event == "window-rules/list-wsets") {
// data: [ wset ]
std::unordered_map<size_t, State::Wset> wsets;
for (const auto& wset_data : data) {
auto wset_idx = wset_data["index"].asUInt();
auto output_name = wset_data["output-name"].asString();
auto output = state.outputs.contains(output_name)
? std::optional{std::ref(state.outputs.at(output_name))}
: std::nullopt;
const auto& ws_data = wset_data["workspace"];
auto ws_w = ws_data["grid_width"].asUInt();
auto ws_h = ws_data["grid_height"].asUInt();
wsets.emplace(wset_idx, State::Wset{
.output = output,
.wss = std::vector<State::Workspace>(ws_w * ws_h),
.ws_w = ws_w,
.ws_h = ws_h,
.ws_x = ws_data["x"].asUInt(),
.ws_y = ws_data["y"].asUInt(),
});
if (state.wsets.contains(wset_idx)) {
auto& old_wset = state.wsets.at(wset_idx);
auto& new_wset = wsets.at(wset_idx);
new_wset.wss = std::move(old_wset.wss);
new_wset.focused_view_id = old_wset.focused_view_id;
}
}
state.wsets = std::move(wsets);
return;
}
if (event == "window-rules/get-focused-view") {
// data: { ok, info: view? }
if (const auto& view = data["info"]) {
auto& wset = state.wsets.at(view["wset-index"].asUInt());
wset.focused_view_id = view["id"].asUInt();
state.update_view(view);
}
return;
}
if (event == "window-rules/get-focused-output") {
// data: { ok, info: output }
state.focused_output_name = data["info"]["name"].asString();
return;
}
}
} // namespace waybar::modules::wayfire
+81
View File
@@ -0,0 +1,81 @@
#include "modules/wayfire/window.hpp"
#include <gtkmm/button.h>
#include <gtkmm/label.h>
#include <spdlog/spdlog.h>
#include "util/rewrite_string.hpp"
#include "util/sanitize_str.hpp"
namespace waybar::modules::wayfire {
Window::Window(const std::string& id, const Bar& bar, const Json::Value& config)
: AAppIconLabel(config, "window", id, "{title}", 0, true),
ipc{IPC::get_instance()},
handler{[this](const auto&) { dp.emit(); }},
bar_{bar} {
ipc->register_handler("view-unmapped", handler);
ipc->register_handler("view-focused", handler);
ipc->register_handler("view-title-changed", handler);
ipc->register_handler("view-app-id-changed", handler);
ipc->register_handler("window-rules/get-focused-view", handler);
dp.emit();
}
Window::~Window() { ipc->unregister_handler(handler); }
auto Window::update() -> void {
update_icon_label();
AAppIconLabel::update();
}
auto Window::update_icon_label() -> void {
auto _ = ipc->lock_state();
auto out_it = ipc->get_outputs().find(bar_.output->name);
if (out_it == ipc->get_outputs().end()) return;
const auto& output = out_it->second;
auto wset_it = ipc->get_wsets().find(output.wset_idx);
if (wset_it == ipc->get_wsets().end()) return;
const auto& wset = wset_it->second;
const auto& views = ipc->get_views();
auto ctx = bar_.window.get_style_context();
if (views.contains(wset.focused_view_id)) {
const auto& view = views.at(wset.focused_view_id);
auto title = view["title"].asString();
auto app_id = view["app-id"].asString();
// update label
label_.set_markup(waybar::util::rewriteString(
fmt::format(fmt::runtime(format_), fmt::arg("title", waybar::util::sanitize_string(title)),
fmt::arg("app_id", waybar::util::sanitize_string(app_id))),
config_["rewrite"]));
// update window#waybar.solo
if (wset.locate_ws(view["geometry"]).num_views > 1)
ctx->remove_class("solo");
else
ctx->add_class("solo");
// update window#waybar.<app_id>
ctx->remove_class(old_app_id_);
ctx->add_class(old_app_id_ = app_id);
// update window#waybar.empty
ctx->remove_class("empty");
//
updateAppIconName(app_id, "");
label_.show();
} else {
ctx->add_class("empty");
updateAppIconName("", "");
label_.hide();
}
}
} // namespace waybar::modules::wayfire
+191
View File
@@ -0,0 +1,191 @@
#include "modules/wayfire/workspaces.hpp"
#include <gtkmm/button.h>
#include <gtkmm/label.h>
#include <spdlog/spdlog.h>
#include <string>
#include <utility>
#include "modules/wayfire/backend.hpp"
namespace waybar::modules::wayfire {
Workspaces::Workspaces(const std::string& id, const Bar& bar, const Json::Value& config)
: AModule{config, "workspaces", id, false, !config["disable-scroll"].asBool()},
ipc{IPC::get_instance()},
handler{[this](const auto&) { dp.emit(); }},
bar_{bar} {
// init box_
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_);
// scroll events
if (!config_["disable-scroll"].asBool()) {
auto& target = config_["enable-bar-scroll"].asBool() ? const_cast<Bar&>(bar_).window
: dynamic_cast<Gtk::Widget&>(box_);
target.add_events(Gdk::SCROLL_MASK | Gdk::SMOOTH_SCROLL_MASK);
target.signal_scroll_event().connect(sigc::mem_fun(*this, &Workspaces::handleScroll));
}
// listen events
ipc->register_handler("view-mapped", handler);
ipc->register_handler("view-unmapped", handler);
ipc->register_handler("view-wset-changed", handler);
ipc->register_handler("output-gain-focus", handler);
ipc->register_handler("view-sticky", handler);
ipc->register_handler("view-workspace-changed", handler);
ipc->register_handler("output-wset-changed", handler);
ipc->register_handler("wset-workspace-changed", handler);
ipc->register_handler("window-rules/list-views", handler);
ipc->register_handler("window-rules/list-outputs", handler);
ipc->register_handler("window-rules/list-wsets", handler);
ipc->register_handler("window-rules/get-focused-output", handler);
// initial render
dp.emit();
}
Workspaces::~Workspaces() { ipc->unregister_handler(handler); }
auto Workspaces::handleScroll(GdkEventScroll* e) -> bool {
// Ignore emulated scroll events on window
if (gdk_event_get_pointer_emulated((GdkEvent*)e) != 0) return false;
auto dir = AModule::getScrollDir(e);
if (dir == SCROLL_DIR::NONE) return true;
int delta;
if (dir == SCROLL_DIR::DOWN || dir == SCROLL_DIR::RIGHT)
delta = 1;
else if (dir == SCROLL_DIR::UP || dir == SCROLL_DIR::LEFT)
delta = -1;
else
return true;
// cycle workspace
Json::Value data;
{
auto _ = ipc->lock_state();
auto out_it = ipc->get_outputs().find(bar_.output->name);
if (out_it == ipc->get_outputs().end()) return true;
const auto& output = out_it->second;
auto wset_it = ipc->get_wsets().find(output.wset_idx);
if (wset_it == ipc->get_wsets().end()) return true;
const auto& wset = wset_it->second;
auto n = wset.ws_w * wset.ws_h;
auto i = (wset.ws_idx() + delta + n) % n;
data["x"] = Json::Value((uint64_t)i % wset.ws_w);
data["y"] = Json::Value((uint64_t)i / wset.ws_h);
data["output-id"] = Json::Value((uint64_t)output.id);
}
ipc->send("vswitch/set-workspace", std::move(data));
return true;
}
auto Workspaces::update() -> void {
update_box();
AModule::update();
}
auto Workspaces::update_box() -> void {
auto _ = ipc->lock_state();
const auto& output_name = bar_.output->name;
auto out_it = ipc->get_outputs().find(output_name);
if (out_it == ipc->get_outputs().end()) return;
const auto& output = out_it->second;
auto wset_it = ipc->get_wsets().find(output.wset_idx);
if (wset_it == ipc->get_wsets().end()) return;
const auto& wset = wset_it->second;
auto output_focused = ipc->get_focused_output_name() == output_name;
auto ws_w = wset.ws_w;
auto ws_h = wset.ws_h;
auto num_wss = ws_w * ws_h;
// add buttons for new workspaces
for (auto i = buttons_.size(); i < num_wss; i++) {
auto& btn = buttons_.emplace_back("");
box_.pack_start(btn, false, false, 0);
btn.set_relief(Gtk::RELIEF_NONE);
if (!config_["disable-click"].asBool()) {
btn.signal_pressed().connect([=, this] {
Json::Value data;
data["x"] = Json::Value((uint64_t)i % ws_w);
data["y"] = Json::Value((uint64_t)i / ws_h);
data["output-id"] = Json::Value((uint64_t)output.id);
ipc->send("vswitch/set-workspace", std::move(data));
});
}
}
// remove buttons for removed workspaces
buttons_.resize(num_wss);
// update buttons
for (size_t i = 0; i < num_wss; i++) {
const auto& ws = wset.wss[i];
auto& btn = buttons_[i];
auto ctx = btn.get_style_context();
auto ws_focused = i == wset.ws_idx();
auto ws_empty = ws.num_views == 0;
// update #workspaces button.focused
if (ws_focused)
ctx->add_class("focused");
else
ctx->remove_class("focused");
// update #workspaces button.empty
if (ws_empty)
ctx->add_class("empty");
else
ctx->remove_class("empty");
// update #workspaces button.current_output
if (output_focused)
ctx->add_class("current_output");
else
ctx->remove_class("current_output");
// update label
auto label = std::to_string(i + 1);
if (config_["format"].isString()) {
auto format = config_["format"].asString();
auto ws_idx = std::to_string(i + 1);
const auto& icons = config_["format-icons"];
std::string icon;
if (!icons)
icon = ws_idx;
else if (ws_focused && icons["focused"])
icon = icons["focused"].asString();
else if (icons[ws_idx])
icon = icons[ws_idx].asString();
else if (icons["default"])
icon = icons["default"].asString();
else
icon = ws_idx;
label = fmt::format(fmt::runtime(format), fmt::arg("icon", icon), fmt::arg("index", ws_idx),
fmt::arg("output", output_name));
}
if (!config_["disable-markup"].asBool())
static_cast<Gtk::Label*>(btn.get_children()[0])->set_markup(label);
else
btn.set_label(label);
//
if (config_["current-only"].asBool() && i != wset.ws_idx())
btn.hide();
else
btn.show();
}
}
} // namespace waybar::modules::wayfire
+447 -153
View File
@@ -2,8 +2,13 @@
#include <spdlog/spdlog.h>
#include <cmath>
#include <string>
bool isValidNodeId(uint32_t id) { return id > 0 && id < G_MAXUINT32; }
std::list<waybar::modules::Wireplumber*> waybar::modules::Wireplumber::modules;
waybar::modules::Wireplumber::Wireplumber(const std::string& id, const Json::Value& config)
: ALabel(config, "wireplumber", id, "{volume}%"),
wp_core_(nullptr),
@@ -16,36 +21,52 @@ waybar::modules::Wireplumber::Wireplumber(const std::string& id, const Json::Val
muted_(false),
volume_(0.0),
min_step_(0.0),
node_id_(0) {
node_id_(0),
node_name_(""),
source_name_(""),
type_(nullptr),
source_node_id_(0),
source_muted_(false),
source_volume_(0.0),
default_source_name_(nullptr),
form_factor_("") {
waybar::modules::Wireplumber::modules.push_back(this);
wp_init(WP_INIT_PIPEWIRE);
wp_core_ = wp_core_new(NULL, NULL);
wp_core_ = wp_core_new(nullptr, nullptr, nullptr);
apis_ = g_ptr_array_new_with_free_func(g_object_unref);
om_ = wp_object_manager_new();
prepare();
type_ = g_strdup(config_["node-type"].isString() ? config_["node-type"].asString().c_str()
: "Audio/Sink");
loadRequiredApiModules();
prepare(this);
spdlog::debug("[{}]: connecting to pipewire...", this->name_);
spdlog::debug("[{}]: connecting to pipewire: '{}'...", name_, type_);
if (!wp_core_connect(wp_core_)) {
spdlog::error("[{}]: Could not connect to PipeWire", this->name_);
if (wp_core_connect(wp_core_) == 0) {
spdlog::error("[{}]: Could not connect to PipeWire: '{}'", name_, type_);
throw std::runtime_error("Could not connect to PipeWire\n");
}
spdlog::debug("[{}]: connected!", this->name_);
spdlog::debug("[{}]: {} connected!", name_, type_);
g_signal_connect_swapped(om_, "installed", (GCallback)onObjectManagerInstalled, this);
activatePlugins();
dp.emit();
event_box_.add_events(Gdk::SCROLL_MASK | Gdk::SMOOTH_SCROLL_MASK);
event_box_.signal_scroll_event().connect(sigc::mem_fun(*this, &Wireplumber::handleScroll));
asyncLoadRequiredApiModules();
}
waybar::modules::Wireplumber::~Wireplumber() {
waybar::modules::Wireplumber::modules.remove(this);
if (mixer_api_ != nullptr) {
g_signal_handlers_disconnect_by_data(mixer_api_, this);
}
if (def_nodes_api_ != nullptr) {
g_signal_handlers_disconnect_by_data(def_nodes_api_, this);
}
if (om_ != nullptr) {
g_signal_handlers_disconnect_by_data(om_, this);
}
wp_core_disconnect(wp_core_);
g_clear_pointer(&apis_, g_ptr_array_unref);
g_clear_object(&om_);
@@ -53,54 +74,114 @@ waybar::modules::Wireplumber::~Wireplumber() {
g_clear_object(&mixer_api_);
g_clear_object(&def_nodes_api_);
g_free(default_node_name_);
g_free(default_source_name_);
g_free(type_);
}
void waybar::modules::Wireplumber::updateNodeName(waybar::modules::Wireplumber* self, uint32_t id) {
spdlog::debug("[{}]: updating node name with node.id {}", self->name_, id);
spdlog::debug("[{}]: updating '{}' node name with node.id {}", self->name_, self->type_, id);
if (!isValidNodeId(id)) {
spdlog::warn("[{}]: '{}' is not a valid node ID. Ignoring node name update.", self->name_, id);
spdlog::warn("[{}]: '{}' is not a valid node ID. Ignoring '{}' node name update.", self->name_,
id, self->type_);
return;
}
auto proxy = static_cast<WpProxy*>(wp_object_manager_lookup(
self->om_, WP_TYPE_GLOBAL_PROXY, WP_CONSTRAINT_TYPE_G_PROPERTY, "bound-id", "=u", id, NULL));
auto* proxy = static_cast<WpProxy*>(wp_object_manager_lookup(self->om_, WP_TYPE_GLOBAL_PROXY,
WP_CONSTRAINT_TYPE_G_PROPERTY,
"bound-id", "=u", id, nullptr));
if (!proxy) {
if (proxy == nullptr) {
auto err = fmt::format("Object '{}' not found\n", id);
spdlog::error("[{}]: {}", self->name_, err);
throw std::runtime_error(err);
return;
}
g_autoptr(WpProperties) properties =
WP_IS_PIPEWIRE_OBJECT(proxy) ? wp_pipewire_object_get_properties(WP_PIPEWIRE_OBJECT(proxy))
: wp_properties_new_empty();
g_autoptr(WpProperties) global_p = wp_global_proxy_get_global_properties(WP_GLOBAL_PROXY(proxy));
WP_IS_PIPEWIRE_OBJECT(proxy) != 0
? wp_pipewire_object_get_properties(WP_PIPEWIRE_OBJECT(proxy))
: wp_properties_new_empty();
g_autoptr(WpProperties) globalP = wp_global_proxy_get_global_properties(WP_GLOBAL_PROXY(proxy));
properties = wp_properties_ensure_unique_owner(properties);
wp_properties_add(properties, global_p);
wp_properties_set(properties, "object.id", NULL);
auto nick = wp_properties_get(properties, "node.nick");
auto description = wp_properties_get(properties, "node.description");
wp_properties_add(properties, globalP);
wp_properties_set(properties, "object.id", nullptr);
const auto* nick = wp_properties_get(properties, "node.nick");
const auto* description = wp_properties_get(properties, "node.description");
self->node_name_ = nick ? nick : description ? description : "Unknown node name";
spdlog::debug("[{}]: Updating node name to: {}", self->name_, self->node_name_);
self->node_name_ = nick != nullptr ? nick
: description != nullptr ? description
: "Unknown node name";
spdlog::debug("[{}]: Updating '{}' node name to: {}", self->name_, self->type_, self->node_name_);
// find form-factor
const auto* devid = wp_properties_get(properties, "device.id");
spdlog::debug("[{}]: '{}' device.id is {}", self->name_, self->type_, devid);
auto* dev = static_cast<WpDevice*>(wp_object_manager_lookup(
self->om_, WP_TYPE_DEVICE, WP_CONSTRAINT_TYPE_G_PROPERTY, "bound-id", "=s", devid, nullptr));
if (const auto* ff =
wp_pipewire_object_get_property(WP_PIPEWIRE_OBJECT(dev), "device.form-factor")) {
self->form_factor_ = ff;
spdlog::debug("[{}]: Updating node form factor to: {}", self->name_, self->form_factor_);
} else {
self->form_factor_ = "";
}
}
void waybar::modules::Wireplumber::updateSourceName(waybar::modules::Wireplumber* self,
uint32_t id) {
spdlog::debug("[{}]: updating source name with node.id {}", self->name_, id);
if (!isValidNodeId(id)) {
spdlog::warn("[{}]: '{}' is not a valid source node ID. Ignoring source name update.",
self->name_, id);
return;
}
auto* proxy = static_cast<WpProxy*>(wp_object_manager_lookup(self->om_, WP_TYPE_GLOBAL_PROXY,
WP_CONSTRAINT_TYPE_G_PROPERTY,
"bound-id", "=u", id, nullptr));
if (proxy == nullptr) {
auto err = fmt::format("Source object '{}' not found\n", id);
spdlog::error("[{}]: {}", self->name_, err);
return;
}
g_autoptr(WpProperties) properties =
WP_IS_PIPEWIRE_OBJECT(proxy) != 0
? wp_pipewire_object_get_properties(WP_PIPEWIRE_OBJECT(proxy))
: wp_properties_new_empty();
g_autoptr(WpProperties) globalP = wp_global_proxy_get_global_properties(WP_GLOBAL_PROXY(proxy));
properties = wp_properties_ensure_unique_owner(properties);
wp_properties_add(properties, globalP);
wp_properties_set(properties, "object.id", nullptr);
const auto* nick = wp_properties_get(properties, "node.nick");
const auto* description = wp_properties_get(properties, "node.description");
self->source_name_ = nick != nullptr ? nick
: description != nullptr ? description
: "Unknown source name";
spdlog::debug("[{}]: Updating source name to: {}", self->name_, self->source_name_);
}
void waybar::modules::Wireplumber::updateVolume(waybar::modules::Wireplumber* self, uint32_t id) {
spdlog::debug("[{}]: updating volume", self->name_);
GVariant* variant = NULL;
GVariant* variant = nullptr;
if (!isValidNodeId(id)) {
spdlog::error("[{}]: '{}' is not a valid node ID. Ignoring volume update.", self->name_, id);
spdlog::error("[{}]: '{}' is not a valid '{}' node ID. Ignoring volume update.", self->name_,
id, self->type_);
return;
}
g_signal_emit_by_name(self->mixer_api_, "get-volume", id, &variant);
if (!variant) {
if (variant == nullptr) {
auto err = fmt::format("Node {} does not support volume\n", id);
spdlog::error("[{}]: {}", self->name_, err);
throw std::runtime_error(err);
return;
}
g_variant_lookup(variant, "volume", "d", &self->volume_);
@@ -111,78 +192,121 @@ void waybar::modules::Wireplumber::updateVolume(waybar::modules::Wireplumber* se
self->dp.emit();
}
void waybar::modules::Wireplumber::updateSourceVolume(waybar::modules::Wireplumber* self,
uint32_t id) {
spdlog::debug("[{}]: updating source volume", self->name_);
GVariant* variant = nullptr;
if (!isValidNodeId(id)) {
spdlog::error("[{}]: '{}' is not a valid source node ID. Ignoring source volume update.",
self->name_, id);
return;
}
g_signal_emit_by_name(self->mixer_api_, "get-volume", id, &variant);
if (variant == nullptr) {
spdlog::debug("[{}]: Source node {} does not support volume", self->name_, id);
return;
}
g_variant_lookup(variant, "volume", "d", &self->source_volume_);
g_variant_lookup(variant, "mute", "b", &self->source_muted_);
g_clear_pointer(&variant, g_variant_unref);
self->dp.emit();
}
void waybar::modules::Wireplumber::onMixerChanged(waybar::modules::Wireplumber* self, uint32_t id) {
spdlog::debug("[{}]: (onMixerChanged) - id: {}", self->name_, id);
g_autoptr(WpNode) node = static_cast<WpNode*>(wp_object_manager_lookup(
self->om_, WP_TYPE_NODE, WP_CONSTRAINT_TYPE_G_PROPERTY, "bound-id", "=u", id, NULL));
self->om_, WP_TYPE_NODE, WP_CONSTRAINT_TYPE_G_PROPERTY, "bound-id", "=u", id, nullptr));
if (!node) {
spdlog::warn("[{}]: (onMixerChanged) - Object with id {} not found", self->name_, id);
if (node == nullptr) {
// log a warning only if no other widget is targeting the id.
// this reduces log spam when multiple instances of the module are used on different node types.
if (id != self->node_id_ && id != self->source_node_id_) {
for (auto const& module : waybar::modules::Wireplumber::modules) {
if (module->node_id_ == id || module->source_node_id_ == id) {
return;
}
}
return;
}
spdlog::warn("[{}]: (onMixerChanged: {}) - Object with id {} not found", self->name_,
self->type_, id);
return;
}
const gchar* name = wp_pipewire_object_get_property(WP_PIPEWIRE_OBJECT(node), "node.name");
if (self->node_id_ != id) {
spdlog::debug(
"[{}]: (onMixerChanged) - ignoring mixer update for node: id: {}, name: {} as it is not "
"the default node: {} with id: {}",
self->name_, id, name, self->default_node_name_, self->node_id_);
return;
if (self->node_id_ == id) {
spdlog::debug("[{}]: (onMixerChanged: {}) - updating sink volume for node: {}", self->name_,
self->type_, name);
updateVolume(self, id);
} else if (self->source_node_id_ == id) {
spdlog::debug("[{}]: (onMixerChanged: {}) - updating source volume for node: {}", self->name_,
self->type_, name);
updateSourceVolume(self, id);
}
spdlog::debug("[{}]: (onMixerChanged) - Need to update volume for node with id {} and name {}",
self->name_, id, name);
updateVolume(self, id);
}
void waybar::modules::Wireplumber::onDefaultNodesApiChanged(waybar::modules::Wireplumber* self) {
spdlog::debug("[{}]: (onDefaultNodesApiChanged)", self->name_);
spdlog::debug("[{}]: (onDefaultNodesApiChanged: {})", self->name_, self->type_);
uint32_t default_node_id;
g_signal_emit_by_name(self->def_nodes_api_, "get-default-node", "Audio/Sink", &default_node_id);
// Handle sink
uint32_t defaultNodeId;
g_signal_emit_by_name(self->def_nodes_api_, "get-default-node", self->type_, &defaultNodeId);
if (!isValidNodeId(default_node_id)) {
spdlog::warn("[{}]: '{}' is not a valid node ID. Ignoring node change.", self->name_,
default_node_id);
return;
if (isValidNodeId(defaultNodeId)) {
g_autoptr(WpNode) node = static_cast<WpNode*>(
wp_object_manager_lookup(self->om_, WP_TYPE_NODE, WP_CONSTRAINT_TYPE_G_PROPERTY, "bound-id",
"=u", defaultNodeId, nullptr));
if (node != nullptr) {
const gchar* defaultNodeName =
wp_pipewire_object_get_property(WP_PIPEWIRE_OBJECT(node), "node.name");
if (g_strcmp0(self->default_node_name_, defaultNodeName) != 0 ||
self->node_id_ != defaultNodeId) {
spdlog::debug("[{}]: Default sink changed to -> Node(name: {}, id: {})", self->name_,
defaultNodeName, defaultNodeId);
g_free(self->default_node_name_);
self->default_node_name_ = g_strdup(defaultNodeName);
self->node_id_ = defaultNodeId;
updateVolume(self, defaultNodeId);
updateNodeName(self, defaultNodeId);
}
}
}
g_autoptr(WpNode) node = static_cast<WpNode*>(
wp_object_manager_lookup(self->om_, WP_TYPE_NODE, WP_CONSTRAINT_TYPE_G_PROPERTY, "bound-id",
"=u", default_node_id, NULL));
// Handle source
uint32_t defaultSourceId;
g_signal_emit_by_name(self->def_nodes_api_, "get-default-node", "Audio/Source", &defaultSourceId);
if (!node) {
spdlog::warn("[{}]: (onDefaultNodesApiChanged) - Object with id {} not found", self->name_,
default_node_id);
return;
if (isValidNodeId(defaultSourceId)) {
g_autoptr(WpNode) sourceNode = static_cast<WpNode*>(
wp_object_manager_lookup(self->om_, WP_TYPE_NODE, WP_CONSTRAINT_TYPE_G_PROPERTY, "bound-id",
"=u", defaultSourceId, nullptr));
if (sourceNode != nullptr) {
const gchar* defaultSourceName =
wp_pipewire_object_get_property(WP_PIPEWIRE_OBJECT(sourceNode), "node.name");
if (g_strcmp0(self->default_source_name_, defaultSourceName) != 0 ||
self->source_node_id_ != defaultSourceId) {
spdlog::debug("[{}]: Default source changed to -> Node(name: {}, id: {})", self->name_,
defaultSourceName, defaultSourceId);
g_free(self->default_source_name_);
self->default_source_name_ = g_strdup(defaultSourceName);
self->source_node_id_ = defaultSourceId;
updateSourceVolume(self, defaultSourceId);
updateSourceName(self, defaultSourceId);
}
}
}
const gchar* default_node_name =
wp_pipewire_object_get_property(WP_PIPEWIRE_OBJECT(node), "node.name");
spdlog::debug(
"[{}]: (onDefaultNodesApiChanged) - got the following default node: Node(name: {}, id: {})",
self->name_, default_node_name, default_node_id);
if (g_strcmp0(self->default_node_name_, default_node_name) == 0) {
spdlog::debug(
"[{}]: (onDefaultNodesApiChanged) - Default node has not changed. Node(name: {}, id: {}). "
"Ignoring.",
self->name_, self->default_node_name_, default_node_id);
return;
}
spdlog::debug(
"[{}]: (onDefaultNodesApiChanged) - Default node changed to -> Node(name: {}, id: {})",
self->name_, default_node_name, default_node_id);
g_free(self->default_node_name_);
self->default_node_name_ = g_strdup(default_node_name);
self->node_id_ = default_node_id;
updateVolume(self, default_node_id);
updateNodeName(self, default_node_id);
}
void waybar::modules::Wireplumber::onObjectManagerInstalled(waybar::modules::Wireplumber* self) {
@@ -190,29 +314,43 @@ void waybar::modules::Wireplumber::onObjectManagerInstalled(waybar::modules::Wir
self->def_nodes_api_ = wp_plugin_find(self->wp_core_, "default-nodes-api");
if (!self->def_nodes_api_) {
if (self->def_nodes_api_ == nullptr) {
spdlog::error("[{}]: default nodes api is not loaded.", self->name_);
throw std::runtime_error("Default nodes API is not loaded\n");
return;
}
self->mixer_api_ = wp_plugin_find(self->wp_core_, "mixer-api");
if (!self->mixer_api_) {
if (self->mixer_api_ == nullptr) {
spdlog::error("[{}]: mixer api is not loaded.", self->name_);
throw std::runtime_error("Mixer api is not loaded\n");
return;
}
g_signal_emit_by_name(self->def_nodes_api_, "get-default-configured-node-name", "Audio/Sink",
// Get default sink
g_signal_emit_by_name(self->def_nodes_api_, "get-default-configured-node-name", self->type_,
&self->default_node_name_);
g_signal_emit_by_name(self->def_nodes_api_, "get-default-node", "Audio/Sink", &self->node_id_);
g_signal_emit_by_name(self->def_nodes_api_, "get-default-node", self->type_, &self->node_id_);
if (self->default_node_name_) {
spdlog::debug("[{}]: (onObjectManagerInstalled) - default configured node name: {} and id: {}",
self->name_, self->default_node_name_, self->node_id_);
// Get default source
g_signal_emit_by_name(self->def_nodes_api_, "get-default-configured-node-name", "Audio/Source",
&self->default_source_name_);
g_signal_emit_by_name(self->def_nodes_api_, "get-default-node", "Audio/Source",
&self->source_node_id_);
if (self->default_node_name_ != nullptr) {
spdlog::debug(
"[{}]: (onObjectManagerInstalled: {}) - default configured node name: {} and id: {}",
self->name_, self->type_, self->default_node_name_, self->node_id_);
}
if (self->default_source_name_ != nullptr) {
spdlog::debug("[{}]: default source: {} (id: {})", self->name_, self->default_source_name_,
self->source_node_id_);
}
updateVolume(self, self->node_id_);
updateNodeName(self, self->node_id_);
updateSourceVolume(self, self->source_node_id_);
updateSourceName(self, self->source_node_id_);
g_signal_connect_swapped(self->mixer_api_, "changed", (GCallback)onMixerChanged, self);
g_signal_connect_swapped(self->def_nodes_api_, "changed", (GCallback)onDefaultNodesApiChanged,
@@ -221,13 +359,13 @@ void waybar::modules::Wireplumber::onObjectManagerInstalled(waybar::modules::Wir
void waybar::modules::Wireplumber::onPluginActivated(WpObject* p, GAsyncResult* res,
waybar::modules::Wireplumber* self) {
auto plugin_name = wp_plugin_get_name(WP_PLUGIN(p));
spdlog::debug("[{}]: onPluginActivated: {}", self->name_, plugin_name);
g_autoptr(GError) error = NULL;
const auto* pluginName = wp_plugin_get_name(WP_PLUGIN(p));
spdlog::debug("[{}]: onPluginActivated: {}", self->name_, pluginName);
g_autoptr(GError) error = nullptr;
if (!wp_object_activate_finish(p, res, &error)) {
if (wp_object_activate_finish(p, res, &error) == 0) {
spdlog::error("[{}]: error activating plugin: {}", self->name_, error->message);
throw std::runtime_error(error->message);
return;
}
if (--self->pending_plugins_ == 0) {
@@ -240,68 +378,192 @@ void waybar::modules::Wireplumber::activatePlugins() {
for (uint16_t i = 0; i < apis_->len; i++) {
WpPlugin* plugin = static_cast<WpPlugin*>(g_ptr_array_index(apis_, i));
pending_plugins_++;
wp_object_activate(WP_OBJECT(plugin), WP_PLUGIN_FEATURE_ENABLED, NULL,
wp_object_activate(WP_OBJECT(plugin), WP_PLUGIN_FEATURE_ENABLED, nullptr,
(GAsyncReadyCallback)onPluginActivated, this);
}
}
void waybar::modules::Wireplumber::prepare() {
spdlog::debug("[{}]: preparing object manager", name_);
void waybar::modules::Wireplumber::prepare(waybar::modules::Wireplumber* self) {
spdlog::debug("[{}]: preparing object manager: '{}'", name_, self->type_);
wp_object_manager_add_interest(om_, WP_TYPE_NODE, WP_CONSTRAINT_TYPE_PW_PROPERTY, "media.class",
"=s", "Audio/Sink", NULL);
"=s", self->type_, nullptr);
wp_object_manager_add_interest(om_, WP_TYPE_NODE, WP_CONSTRAINT_TYPE_PW_PROPERTY, "media.class",
"=s", "Audio/Source", nullptr);
wp_object_manager_add_interest(om_, WP_TYPE_DEVICE, WP_CONSTRAINT_TYPE_PW_PROPERTY, "media.class",
"=s", "Audio/Device", nullptr);
}
void waybar::modules::Wireplumber::loadRequiredApiModules() {
spdlog::debug("[{}]: loading required modules", name_);
g_autoptr(GError) error = NULL;
void waybar::modules::Wireplumber::onDefaultNodesApiLoaded(WpObject* p, GAsyncResult* res,
waybar::modules::Wireplumber* self) {
gboolean success = FALSE;
g_autoptr(GError) error = nullptr;
if (!wp_core_load_component(wp_core_, "libwireplumber-module-default-nodes-api", "module", NULL,
&error)) {
throw std::runtime_error(error->message);
spdlog::debug("[{}]: callback loading default node api module", self->name_);
success = wp_core_load_component_finish(self->wp_core_, res, &error);
if (success == FALSE) {
spdlog::error("[{}]: default nodes API load failed", self->name_);
return;
}
spdlog::debug("[{}]: loaded default nodes api", self->name_);
g_ptr_array_add(self->apis_, wp_plugin_find(self->wp_core_, "default-nodes-api"));
spdlog::debug("[{}]: loading mixer api module", self->name_);
wp_core_load_component(self->wp_core_, "libwireplumber-module-mixer-api", "module", nullptr,
"mixer-api", nullptr, (GAsyncReadyCallback)onMixerApiLoaded, self);
}
void waybar::modules::Wireplumber::onMixerApiLoaded(WpObject* p, GAsyncResult* res,
waybar::modules::Wireplumber* self) {
gboolean success = FALSE;
g_autoptr(GError) error = nullptr;
success = wp_core_load_component_finish(self->wp_core_, res, &error);
if (success == FALSE) {
spdlog::error("[{}]: mixer API load failed", self->name_);
return;
}
if (!wp_core_load_component(wp_core_, "libwireplumber-module-mixer-api", "module", NULL,
&error)) {
throw std::runtime_error(error->message);
}
g_ptr_array_add(apis_, wp_plugin_find(wp_core_, "default-nodes-api"));
g_ptr_array_add(apis_, ({
WpPlugin* p = wp_plugin_find(wp_core_, "mixer-api");
g_object_set(G_OBJECT(p), "scale", 1 /* cubic */, NULL);
spdlog::debug("[{}]: loaded mixer API", self->name_);
g_ptr_array_add(self->apis_, ({
WpPlugin* p = wp_plugin_find(self->wp_core_, "mixer-api");
g_object_set(G_OBJECT(p), "scale", 0 /* linear */, nullptr);
p;
}));
self->activatePlugins();
self->dp.emit();
self->event_box_.add_events(Gdk::SCROLL_MASK | Gdk::SMOOTH_SCROLL_MASK);
self->event_box_.signal_scroll_event().connect(sigc::mem_fun(*self, &Wireplumber::handleScroll));
}
void waybar::modules::Wireplumber::asyncLoadRequiredApiModules() {
spdlog::debug("[{}]: loading default nodes api module", name_);
wp_core_load_component(wp_core_, "libwireplumber-module-default-nodes-api", "module", nullptr,
"default-nodes-api", nullptr, (GAsyncReadyCallback)onDefaultNodesApiLoaded,
this);
}
static const std::array<std::string, 7> ports = {
"headphone", "speaker", "headset", "hands-free", "portable", "car", "hifi",
};
std::vector<std::string> waybar::modules::Wireplumber::getWPIcon() {
std::vector<std::string> res;
if (muted_) {
res.emplace_back(node_name_ + "-muted");
}
res.push_back(node_name_);
res.push_back(source_name_);
std::transform(form_factor_.begin(), form_factor_.end(), form_factor_.begin(), ::tolower);
for (auto const& port : ports) {
if (form_factor_.find(port) != std::string::npos) {
if (muted_) {
res.emplace_back(port + "-muted");
}
res.push_back(port);
break;
}
}
if (muted_) {
res.emplace_back("default-muted");
}
return res;
}
auto waybar::modules::Wireplumber::update() -> void {
auto format = format_;
std::string tooltip_format;
std::string format_name = "format";
if (muted_) {
format = config_["format-muted"].isString() ? config_["format-muted"].asString() : format;
label_.get_style_context()->add_class("muted");
// Handle sink bluetooth state
const std::string name = default_node_name_ != nullptr ? default_node_name_ : "";
auto bt = name.find("bluez") != std::string::npos || name.find("a2dp-sink") != std::string::npos;
if (bt) {
format_name += "-bluetooth";
label_.get_style_context()->add_class("bluetooth");
} else {
label_.get_style_context()->remove_class("muted");
label_.get_style_context()->remove_class("bluetooth");
}
int vol = round(volume_ * 100.0);
std::string markup = fmt::format(fmt::runtime(format), fmt::arg("node_name", node_name_),
fmt::arg("volume", vol), fmt::arg("icon", getIcon(vol)));
label_.set_markup(markup);
// Handle sink mute state
if (muted_) {
// Check muted bluetooth format exists, otherwise fall back to default muted format.
if (format_name != "format" && !config_[format_name + "-muted"].isString())
format_name = "format";
format_name += "-muted";
label_.get_style_context()->add_class("muted");
label_.get_style_context()->add_class("sink-muted");
} else {
label_.get_style_context()->remove_class("muted");
label_.get_style_context()->remove_class("sink-muted");
}
getState(vol);
// Handle source mute state
if (source_muted_) {
label_.get_style_context()->add_class("source-muted");
} else {
label_.get_style_context()->remove_class("source-muted");
}
double vol_cube = pow(volume_, 3);
double source_vol_cube = pow(source_volume_, 3);
int vol = round(vol_cube * 100.0);
int source_vol = round(source_vol_cube * 100.0);
double vol_db = 20.0 * log10(volume_);
double source_vol_db = 20.0 * log10(source_volume_);
// Get the state and apply state-specific format if available
auto state = getState(vol);
if (!state.empty() && config_[format_name + "-" + state].isString())
format = config_[format_name + "-" + state].asString();
else if (config_[format_name].isString())
format = config_[format_name].asString();
// Prepare source format string (similar to PulseAudio)
std::string format_source = "{volume}%";
if (source_muted_) {
if (config_["format-source-muted"].isString()) {
format_source = config_["format-source-muted"].asString();
}
} else {
if (config_["format-source"].isString()) {
format_source = config_["format-source"].asString();
}
}
// Format the source string with actual volume
std::string formatted_source =
fmt::format(fmt::runtime(format_source), fmt::arg("volume", source_vol));
fmt::dynamic_format_arg_store<fmt::format_context> store;
store.push_back(fmt::arg("node_name", node_name_));
store.push_back(fmt::arg("volume", vol));
store.push_back(fmt::arg("icon", getIcon(vol, getWPIcon())));
store.push_back(fmt::arg("format_source", formatted_source));
store.push_back(fmt::arg("source_volume", source_vol));
store.push_back(fmt::arg("source_desc", source_name_));
store.push_back(fmt::arg("volume_linear", volume_));
store.push_back(fmt::arg("volume_cubic", vol_cube));
store.push_back(fmt::arg("volume_db", vol_db));
store.push_back(fmt::arg("source_volume_linear", source_volume_));
store.push_back(fmt::arg("source_volume_cubic", source_vol_cube));
store.push_back(fmt::arg("source_volume_db", source_vol_db));
setLabelMarkup(fmt::vformat(format, store));
if (tooltipEnabled()) {
if (tooltip_format.empty() && config_["tooltip-format"].isString()) {
tooltip_format = config_["tooltip-format"].asString();
}
if (!tooltip_format.empty()) {
label_.set_tooltip_text(fmt::format(fmt::runtime(tooltip_format),
fmt::arg("node_name", node_name_),
fmt::arg("volume", vol), fmt::arg("icon", getIcon(vol))));
auto tooltipFormat = resolveTooltipFormat("");
if (!tooltipFormat.empty()) {
setTooltipMarkup(fmt::vformat(tooltipFormat, store));
} else {
label_.set_tooltip_text(node_name_);
setTooltipMarkup(node_name_);
}
}
@@ -317,33 +579,65 @@ bool waybar::modules::Wireplumber::handleScroll(GdkEventScroll* e) {
if (dir == SCROLL_DIR::NONE) {
return true;
}
double max_volume = 1;
double step = 1.0 / 100.0;
double maxVolume = 1;
double step = 1.0;
if (config_["scroll-step"].isDouble()) {
step = config_["scroll-step"].asDouble() / 100.0;
step = config_["scroll-step"].asDouble();
}
if (config_["max-volume"].isDouble()) {
max_volume = config_["max-volume"].asDouble() / 100.0;
maxVolume = config_["max-volume"].asDouble();
}
if (step < min_step_) step = min_step_;
double vol = volume_;
std::string scale = "cubic_percent";
if (config_["scroll-scale"].isString()) {
scale = config_["scroll-scale"].asString();
}
double new_vol = volume_;
if (scale == "cubic") {
vol = pow(vol, 3);
} else if (scale == "db") {
vol = log10(vol) * 20.0;
} else if (scale == "cubic_percent") {
vol = pow(vol, 3) * 100.0;
}
double newVol = vol;
if (dir == SCROLL_DIR::UP) {
if (volume_ < max_volume) {
new_vol = volume_ + step;
if (new_vol > max_volume) new_vol = max_volume;
newVol = vol + step;
} else if (dir == SCROLL_DIR::DOWN) {
newVol = vol - step;
}
if (scale == "cubic") {
newVol = cbrt(newVol);
} else if (scale == "db") {
newVol = exp10(newVol / 20.0);
} else if (scale == "cubic_percent") {
newVol = cbrt(newVol / 100.0);
}
if (dir == SCROLL_DIR::UP) {
if (volume_ + min_step_ > newVol) {
newVol = volume_ + min_step_;
}
} else if (dir == SCROLL_DIR::DOWN) {
if (volume_ > 0) {
new_vol = volume_ - step;
if (new_vol < 0) new_vol = 0;
if (volume_ - min_step_ < newVol) {
newVol = volume_ - min_step_;
}
}
if (new_vol != volume_) {
GVariant* variant = g_variant_new_double(new_vol);
if (newVol < 0)
newVol = 0;
else if (newVol > maxVolume)
newVol = maxVolume;
if (newVol != volume_) {
if (mixer_api_ == nullptr) return true;
GVariant* variant = g_variant_new_double(newVol);
gboolean ret;
g_signal_emit_by_name(mixer_api_, "set-volume", node_id_, variant, &ret);
g_variant_unref(variant);
}
return true;
}
File diff suppressed because it is too large Load Diff

Some files were not shown because too many files have changed in this diff Show More