Merge remote-tracking branch 'origin/master' into refactor/generic-tooltip

# Conflicts:
#	src/modules/wireplumber.cpp
This commit is contained in:
Alex
2026-07-04 00:29:26 +02:00
51 changed files with 1903 additions and 84 deletions
+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
+8
View File
@@ -3,10 +3,12 @@
#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"
@@ -39,6 +41,12 @@ void waybar::Client::handleGlobal(void* data, struct wl_registry* registry, uint
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);
}
}
+10 -2
View File
@@ -43,11 +43,11 @@
#include "modules/niri/workspaces.hpp"
#endif
#ifdef HAVE_MANGO
#include "modules/mango/language.hpp"
#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"
#include "modules/mango/layout.hpp"
#endif
#ifdef HAVE_WAYFIRE
#include "modules/wayfire/window.hpp"
@@ -59,6 +59,7 @@
#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
@@ -124,6 +125,7 @@
#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"
@@ -275,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]);
@@ -382,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]);
}
+14 -6
View File
@@ -62,6 +62,10 @@ 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();
@@ -73,10 +77,11 @@ Group::Group(const std::string& name, const std::string& id, const Json::Value&
revealer.set_transition_type(transition_type);
revealer.set_transition_duration(transition_duration);
revealer.set_reveal_child(start_expanded);
if (start_expanded) {
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");
@@ -110,11 +115,14 @@ bool Group::handleMouseEnter(GdkEventCrossing* const& e) {
if (reveal_delay > 0) {
if (reveal_timeout_.connected()) {
reveal_timeout_.disconnect();
}
}
reveal_timeout_ = Glib::signal_timeout().connect(
[this]() { show_group(); return false; },
reveal_delay);
[this]() {
show_group();
return false;
},
reveal_delay);
} else {
show_group();
}
+151 -3
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)
@@ -232,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()) {
@@ -258,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();
@@ -278,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_)));
}
@@ -398,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"));
@@ -418,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;
}
+7
View File
@@ -9,6 +9,7 @@
#include <regex>
#include <sstream>
#include "util/command.hpp"
#include "util/ustring_clen.hpp"
#ifdef HAVE_LANGINFO_1STDAY
@@ -516,6 +517,8 @@ auto waybar::modules::Clock::local_zone() -> const time_zone* {
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);
}
@@ -542,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>
+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();
}
+26 -3
View File
@@ -8,7 +8,7 @@
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),
@@ -28,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() {
@@ -184,7 +193,8 @@ auto waybar::modules::Custom::update() -> void {
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()) {
if ((config_["hide-empty-text"].asBool() && text_.empty()) ||
(str.empty() && image_path_.empty() && image_name_.empty())) {
event_box_.hide();
} else {
setLabelMarkup(str);
@@ -216,7 +226,19 @@ auto waybar::modules::Custom::update() -> void {
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());
}
} catch (const fmt::format_error& e) {
if (std::strcmp(e.what(), "cannot switch from manual to automatic argument indexing") != 0)
@@ -228,7 +250,7 @@ auto waybar::modules::Custom::update() -> void {
}
}
// Call parent update
ALabel::update();
AIconLabel::update();
}
void waybar::modules::Custom::parseOutputRaw() {
@@ -294,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;
}
}
+62 -23
View File
@@ -1,7 +1,8 @@
#include <glibmm/main.h>
#include <json/value.h>
#include <spdlog/spdlog.h>
#include <glibmm/main.h>
#include <algorithm>
#include <cctype>
#include <memory>
#include <string>
@@ -152,8 +153,7 @@ bool Workspace::pointerInsideButton() {
const int buttonHeight = allocation.get_height();
return pointerRootX >= buttonRootX && pointerRootY >= buttonRootY &&
pointerRootX < buttonRootX + buttonWidth &&
pointerRootY < buttonRootY + buttonHeight;
pointerRootX < buttonRootX + buttonWidth && pointerRootY < buttonRootY + buttonHeight;
}
bool Workspace::syncHoverClass() {
@@ -174,9 +174,8 @@ void Workspace::startHoverCheck() {
return;
}
m_hoverCheckConnection = Glib::signal_timeout().connect(
sigc::mem_fun(*this, &Workspace::syncHoverClass),
50);
m_hoverCheckConnection =
Glib::signal_timeout().connect(sigc::mem_fun(*this, &Workspace::syncHoverClass), 50);
}
void Workspace::stopHoverCheck() {
@@ -252,8 +251,7 @@ void Workspace::setActiveWindow(WindowAddress const& addr) {
auto activeWindowPos = m_workspaceManager.activeWindowPosition();
const bool has_active_window =
activeIdx.has_value() &&
activeWindowPos != Workspaces::ActiveWindowPosition::NONE;
activeIdx.has_value() && activeWindowPos != Workspaces::ActiveWindowPosition::NONE;
if (has_active_window) {
auto window = std::move(m_windowMap[*activeIdx]);
@@ -270,8 +268,7 @@ 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();
const bool should_display = !repr.empty() || m_workspaceManager.enableTaskbar();
if (should_display) {
auto addr = create_window_payload.getAddress();
@@ -337,7 +334,7 @@ std::string& Workspace::selectIcon(std::map<std::string, std::string>& icons_map
if (specialNamedIconIt != icons_map.end()) {
return specialNamedIconIt->second;
}
auto specialIconIt = icons_map.find("special");
if (specialIconIt != icons_map.end()) {
return specialIconIt->second;
@@ -378,7 +375,6 @@ std::string& Workspace::selectIcon(std::map<std::string, std::string>& icons_map
return m_name;
}
void Workspace::update(const std::string& workspace_icon) {
if (this->m_workspaceManager.persistentOnly() && !this->isPersistent()) {
m_button.hide();
@@ -436,16 +432,56 @@ void Workspace::update(const std::string& workspace_icon) {
// need to compute this if enableTaskbar() is true
if (!m_workspaceManager.enableTaskbar()) {
auto windowSeparator = m_workspaceManager.getWindowSeparator();
auto groupThreshold = m_workspaceManager.windowRewriteGroupThreshold();
bool isNotFirst = false;
auto end_it = m_workspaceManager.maxWindows() == 0 ? m_windowMap.end() : m_windowMap.begin() + m_workspaceManager.maxWindows();
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) {
if (isNotFirst) {
windows.append(windowSeparator);
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);
}
isNotFirst = true;
windows.append(it->repr_rewrite);
}
}
@@ -505,8 +541,7 @@ void Workspace::updateTaskbar(const std::string& workspace_icon) {
}
if (m_workspaceManager.onClickWindow() != "") {
button->signal_button_press_event().connect(
sigc::bind(sigc::mem_fun(*this, &Workspace::handleClick), window_repr.address),
false);
sigc::bind(sigc::mem_fun(*this, &Workspace::handleClick), window_repr.address), false);
}
auto text_before = fmt::format(fmt::runtime(m_workspaceManager.taskbarFormatBefore()),
@@ -536,13 +571,17 @@ void Workspace::updateTaskbar(const std::string& workspace_icon) {
};
if (m_workspaceManager.taskbarReverseDirection()) {
auto rend_it = m_workspaceManager.maxWindows() == 0 ? m_windowMap.rend() : m_windowMap.rbegin() + m_workspaceManager.maxWindows();
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();
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);
+10
View File
@@ -670,6 +670,16 @@ auto Workspaces::parseConfig(const Json::Value& config) -> void {
populateSortByConfig(config);
populateIgnoreWorkspacesConfig(config);
populateFormatWindowSeparatorConfig(config);
const auto& groupThreshold = config["window-rewrite-group-threshold"];
if (groupThreshold.isInt()) {
m_windowRewriteGroupThreshold = groupThreshold.asInt();
}
const auto& groupFormat = config["window-rewrite-group-format"];
if (groupFormat.isString()) {
m_windowRewriteGroupFormat = groupFormat.asString();
}
populateWindowRewriteConfig(config);
populateMaxWindowsConfig(config);
+148 -15
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;
@@ -70,6 +86,17 @@ auto waybar::modules::IdleInhibitor::update() -> void {
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;
@@ -81,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();
}
}
@@ -114,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;
}
}
+6
View File
@@ -23,6 +23,8 @@ 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),
@@ -35,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_);
+3 -3
View File
@@ -115,7 +115,7 @@ bool Idle::on_io(Glib::IOCondition const&) {
void Playing::entry() noexcept {
timer();
idle();
spdlog::debug("mpd: Playing: enabled 1 second periodic timer.");
spdlog::debug("mpd: Playing: enabled {}ms periodic timer.", ctx_->playing_interval());
}
void Playing::exit() noexcept {
@@ -126,7 +126,7 @@ void Playing::exit() noexcept {
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());
}
}
@@ -136,7 +136,7 @@ void Playing::timer() noexcept {
}
sigc::slot<bool> timer_slot = sigc::mem_fun(*this, &Playing::on_timer);
timer_connection_ = Glib::signal_timeout().connect_seconds(timer_slot, 1);
timer_connection_ = Glib::signal_timeout().connect(timer_slot, ctx_->playing_interval());
}
void Playing::idle() noexcept {
+9 -3
View File
@@ -315,19 +315,26 @@ auto waybar::modules::Network::update() -> void {
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)) {
@@ -336,7 +343,6 @@ auto waybar::modules::Network::update() -> void {
format_ = default_format_;
state_ = state;
}
getState(signal_strength_);
std::string final_ipaddr_;
if (addr_pref_ == ip_addr_pref::IPV4) {
+49 -4
View File
@@ -46,6 +46,12 @@ Workspaces::Workspaces(const std::string& id, const Bar& bar, const Json::Value&
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();
}
@@ -123,10 +129,10 @@ void Workspaces::doUpdate() {
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()));
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()));
@@ -225,6 +231,45 @@ std::string Workspaces::getIcon(const std::string& value, const Json::Value& ws)
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();
+1
View File
@@ -7,6 +7,7 @@ waybar::modules::Pulseaudio::Pulseaudio(const std::string& id, const Json::Value
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;
+1
View File
@@ -6,6 +6,7 @@ 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();
+5 -4
View File
@@ -35,12 +35,12 @@ static const zriver_output_status_v1_listener output_status_listener_impl{
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_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_cast<Tags*>(data)->handle_unfocused_output(output);
}
static void listen_focused_view(void* data, struct zriver_seat_status_v1* zriver_seat_status_v1,
@@ -183,6 +183,7 @@ 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();
}
@@ -306,7 +307,7 @@ void Tags::handle_urgent_tags(uint32_t tags) {
}
}
void Tags::handle_focused_output(struct wl_output *output) {
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");
@@ -314,7 +315,7 @@ void Tags::handle_focused_output(struct wl_output *output) {
}
}
void Tags::handle_unfocused_output(struct wl_output *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");
+67 -10
View File
@@ -28,7 +28,8 @@ waybar::modules::Wireplumber::Wireplumber(const std::string& id, const Json::Val
source_node_id_(0),
source_muted_(false),
source_volume_(0.0),
default_source_name_(nullptr) {
default_source_name_(nullptr),
form_factor_("") {
waybar::modules::Wireplumber::modules.push_back(this);
wp_init(WP_INIT_PIPEWIRE);
@@ -111,6 +112,21 @@ void waybar::modules::Wireplumber::updateNodeName(waybar::modules::Wireplumber*
: 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,
@@ -373,6 +389,8 @@ void waybar::modules::Wireplumber::prepare(waybar::modules::Wireplumber* self) {
"=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::onDefaultNodesApiLoaded(WpObject* p, GAsyncResult* res,
@@ -430,12 +448,54 @@ void waybar::modules::Wireplumber::asyncLoadRequiredApiModules() {
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 format_name = "format";
// 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("bluetooth");
}
// Handle sink mute state
if (muted_) {
format = config_["format-muted"].isString() ? config_["format-muted"].asString() : format;
// 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 {
@@ -461,13 +521,10 @@ auto waybar::modules::Wireplumber::update() -> void {
// Get the state and apply state-specific format if available
auto state = getState(vol);
if (!state.empty()) {
std::string format_name = muted_ ? "format-muted" : "format";
std::string state_format_name = format_name + "-" + state;
if (config_[state_format_name].isString()) {
format = config_[state_format_name].asString();
}
}
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}%";
@@ -488,7 +545,7 @@ auto waybar::modules::Wireplumber::update() -> void {
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)));
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_));
+19 -1
View File
@@ -198,10 +198,18 @@ void AudioBackend::sinkInfoCb(pa_context* /*context*/, const pa_sink_info* i, in
}
}
if (const auto mapping = backend->sink_mapping_.find(backend->current_sink_name_);
mapping != backend->sink_mapping_.end()) {
if (i->name == mapping->second) {
backend->current_sink_name_ = i->name;
}
}
backend->default_sink_running_ = backend->default_sink_name == i->name &&
(i->state == PA_SINK_RUNNING || i->state == PA_SINK_IDLE);
if (i->name != backend->default_sink_name && !backend->default_sink_running_) {
if (i->name != backend->default_sink_name && i->name != backend->current_sink_name_ &&
!backend->default_sink_running_) {
return;
}
@@ -493,4 +501,14 @@ void AudioBackend::setIgnoredSinks(const Json::Value& config) {
}
}
void AudioBackend::setSinkMapping(const Json::Value& config) {
if (config.isObject()) {
for (auto it = config.begin(); it != config.end(); ++it) {
if (it.key().isString() && it->isString()) {
sink_mapping_.emplace(it.key().asString(), it->asString());
}
}
}
}
} // namespace waybar::util