diff --git a/include/AGraph.hpp b/include/AGraph.hpp index 3466e577..a49b215d 100644 --- a/include/AGraph.hpp +++ b/include/AGraph.hpp @@ -11,11 +11,12 @@ namespace waybar { +enum class GraphType { LINE, BAR, GAUGE }; + class AGraph : public AModule { public: - AGraph(const Json::Value &, const std::string &, const std::string &, - uint16_t interval = 0, bool enable_click = false, - bool enable_scroll = false); + AGraph(const Json::Value &, const std::string &, const std::string &, uint16_t interval = 0, + bool enable_click = false, bool enable_scroll = false); virtual ~AGraph() = default; auto update() -> void override; @@ -24,6 +25,7 @@ class AGraph : public AModule { std::deque values_; uint16_t datapoints_ = 20; uint16_t y_offset_ = 0; + GraphType graph_type_ = GraphType::LINE; void addValue(const int n); @@ -37,14 +39,21 @@ class AGraph : public AModule { private: void drawFilledArea(const Cairo::RefPtr &cr, - const std::vector> &points, - double height, const Gdk::RGBA &bg_color); + const std::vector> &points, double height, + const Gdk::RGBA &bg_color); void drawLine(const Cairo::RefPtr &cr, const std::vector> &points, const Gdk::RGBA &fg_color); void drawPath(const Cairo::RefPtr &cr, const std::vector> &points); + + void drawBars(const Cairo::RefPtr &cr, + double width, double height, int current_value, + const Gdk::RGBA &fg_color); + + void drawGauge(const Cairo::RefPtr &cr, double width, double height, + int current_value, const Gdk::RGBA &fg_color); }; } // namespace waybar diff --git a/include/modules/custom_graph.hpp b/include/modules/custom_graph.hpp new file mode 100644 index 00000000..a081f376 --- /dev/null +++ b/include/modules/custom_graph.hpp @@ -0,0 +1,49 @@ +#pragma once + +#include + +#include +#include + +#include "AGraph.hpp" +#include "util/command.hpp" +#include "util/json.hpp" +#include "util/sleeper_thread.hpp" + +namespace waybar::modules { + +class CustomGraph : public AGraph { + public: + CustomGraph(const std::string&, const std::string&, const Json::Value&, const std::string&); + virtual ~CustomGraph(); + auto update() -> void override; + void refresh(int /*signal*/) override; + + private: + void delayWorker(); + void continuousWorker(); + void waitingWorker(); + void parseOutputRaw(); + void parseOutputJson(); + void handleEvent(); + bool handleScroll(GdkEventScroll* e) override; + bool handleToggle(GdkEventButton* const& e) override; + + const std::string name_; + const std::string output_name_; + std::string text_; + std::string id_; + std::string alt_; + std::string tooltip_; + const bool tooltip_format_enabled_; + std::vector class_; + int percentage_; + FILE* fp_; + int pid_; + util::command::res output_; + util::JsonParser parser_; + + util::SleeperThread thread_; +}; + +} // namespace waybar::modules diff --git a/meson.build b/meson.build index 713a723f..8413dbe1 100644 --- a/meson.build +++ b/meson.build @@ -165,6 +165,7 @@ src_files = files( 'src/AIconLabel.cpp', 'src/AAppIconLabel.cpp', 'src/modules/custom.cpp', + 'src/modules/custom_graph.cpp', 'src/modules/disk.cpp', 'src/modules/idle_inhibitor.cpp', 'src/modules/image.cpp', diff --git a/src/AGraph.cpp b/src/AGraph.cpp index 529cdb66..b8076ecc 100644 --- a/src/AGraph.cpp +++ b/src/AGraph.cpp @@ -3,6 +3,7 @@ #include #include +#include #include #include #include @@ -42,6 +43,17 @@ AGraph::AGraph(const Json::Value& config, const std::string& name, const std::st y_offset_ = config_["y_offset"].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 @@ -137,19 +149,26 @@ bool AGraph::onDraw(const Cairo::RefPtr& cr) { double y = height - (static_cast(value) / 100.0 * height); points.emplace_back(x, y); } - if (!points.empty()) { - - drawFilledArea(cr, points, height, bg_color); - - drawLine(cr, points, fg_color); + 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& cr, - const std::vector>& points, - double height, const Gdk::RGBA& bg_color) { + const std::vector>& points, double height, + const Gdk::RGBA& bg_color) { if (points.empty()) return; double first_x = points.front().first; @@ -194,4 +213,93 @@ void AGraph::drawPath(const Cairo::RefPtr& cr, } } +void AGraph::drawBars(const Cairo::RefPtr& 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(0.0, 1.0, 0.0, 1.0); + 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(1.0, 1.0, 0.0, 1.0); + 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(1.0, 0.5, 0.0, 1.0); + 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(1.0, 0.0, 0.0, 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& 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(0.0, 1.0, 0.0, 1.0); + 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(1.0, 1.0, 0.0, 1.0); + cr->arc(center_x, center_y, radius, angle1, angle2); + cr->stroke(); + + // Red section (66-100%) + angle1 = angle2; + angle2 = 0.0; + cr->set_source_rgba(1.0, 0.0, 0.0, 0.8); + 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 diff --git a/src/factory.cpp b/src/factory.cpp index e82711b5..dfb5caf4 100644 --- a/src/factory.cpp +++ b/src/factory.cpp @@ -118,6 +118,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" @@ -362,6 +363,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() > 7) { + return new waybar::modules::CustomGraph(ref.substr(7), 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]); } diff --git a/src/modules/custom_graph.cpp b/src/modules/custom_graph.cpp new file mode 100644 index 00000000..288d97f2 --- /dev/null +++ b/src/modules/custom_graph.cpp @@ -0,0 +1,283 @@ +#include "modules/custom_graph.hpp" + +#include + +#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; + } + 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; + } +}