Merge pull request #4588 from Gazer/feat/basic-cpu-graph

feat: add customgraph module
This commit is contained in:
Alexis Rouillard
2026-07-04 00:09:00 +02:00
committed by GitHub
10 changed files with 1047 additions and 2 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
+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]);
}
+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();
}
+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;
}
}