Post-0.15.0 review of the 0.15.0..HEAD range surfaced regressions and
bugs. This restores backward compatibility for existing configs/CSS,
fixes confirmed defects, and repairs the scdoc man-page build break on
master. Pango-markup tooltips are intentional and were kept.
Backward-compat restorations:
- AModule: honor legacy numeric Gdk::CursorType cursor values (int overload)
- memory: correct GiB divisor (was ~2.3% low); round bare {} placeholders
- wireplumber: scale max-volume into the linear domain so the cap works again
- idle_inhibitor: gate right/middle-click deactivate & scroll on dynamic-timeouts;
accept both dynamic-timeout(s); widen timeout to double (no fractional truncation)
- custom: keep #custom-<name>.<class> CSS selectors working (classes on box_)
- image: don't wordexp-split a single path; fall back to the literal path
- niri/window: restore hide-when-empty (new show-empty opt-in); escape tooltip
- wlr/taskbar: plain-text tooltip when markup is disabled
Bug fixes:
- tray: fix use-after-free in onAdd; guard the watcher retry timeout
- hyprland: clamp max-windows iterator (OOB); drop duplicate language tooltip block
- niri/window: supply {col}/{max_col} args in the empty branch (fmt::format_error)
- mpris: escape {dynamic}/{player} tooltip; fix dangling player; albumArtist source
- mango: fix use-after-free race (dispatch under callback_mutex_)
- mpd: contain throwing checkErrors in noexcept idle paths (no std::terminate/UAF)
- keyboard_state: always render every lock label, with guarded defaults
- bluetooth: bound GATT ReadValue timeout, opt-in + services-resolved gating,
preserve authoritative Battery1 percentage
- wireplumber: fix WpDevice reference leak / NULL handling
- battery, clock, dwl, wayfire, graph, custom_graph, transform, river: assorted
crash/logic fixes
Man page / build:
- niri-workspaces: fix scdoc "indented by an amount greater than 1"
(workspace-taskbar sub-options were mis-indented; breaks man-page build)
- document new show-empty (niri/window); correct network {txBitrate}/{rxBitrate}
Not compiled locally (no gtkmm on this host); C++ build relies on CI.
Man pages validated with scdoc 1.11.4.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
303 lines
10 KiB
C++
303 lines
10 KiB
C++
#include "AGraph.hpp"
|
|
|
|
#include <cairomm/context.h>
|
|
#include <fmt/format.h>
|
|
|
|
#include <algorithm>
|
|
#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::milliseconds::max()
|
|
: std::chrono::milliseconds(
|
|
config_["interval"].isNumeric()
|
|
? std::max(1L, static_cast<long>(config_["interval"].asDouble() * 1000))
|
|
: 1000L * static_cast<long>(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
|