diff --git a/include/factory.hpp b/include/factory.hpp index 06cd4d90..fd741ce6 100644 --- a/include/factory.hpp +++ b/include/factory.hpp @@ -73,6 +73,7 @@ #endif #include "bar.hpp" #include "modules/custom.hpp" +#include "modules/custom_list.hpp" #include "modules/temperature.hpp" namespace waybar { diff --git a/include/modules/custom_list.hpp b/include/modules/custom_list.hpp new file mode 100644 index 00000000..ee5be87e --- /dev/null +++ b/include/modules/custom_list.hpp @@ -0,0 +1,58 @@ +#pragma once + +#include + +#include +#include + +#include +#include +#include +#include "bar.hpp" +#include "AModule.hpp" +#include "util/command.hpp" +#include "util/json.hpp" +#include "util/sleeper_thread.hpp" + +#include + +namespace waybar::modules { + +class CustomList : public AModule { + public: + CustomList(const std::string&, const waybar::Bar&, const std::string&,const Json::Value&); + ~CustomList(); + auto update() -> void; + void refresh(int /*signal*/); + + private: + void delayWorker(); + void continuousWorker(); + void parseOutputRaw(); + void parseOutputJson(); + void handleEvent(); + bool handleScroll(GdkEventScroll* e); + bool handleToggle(GdkEventButton* const& e); + + Gtk::Button &addButton(const Json::Value &node); + void handleClick(std::string); + + std::unordered_map buttons_; + std::vector results_; + std::vector prev_; + const std::string name_; + const Bar& bar_; + Gtk::Box box_; + std::chrono::seconds interval_; + + std::string id_; + std::vector class_; + 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 420606aa..743d291d 100644 --- a/meson.build +++ b/meson.build @@ -146,6 +146,7 @@ src_files = files( 'src/ALabel.cpp', 'src/AIconLabel.cpp', 'src/modules/custom.cpp', + 'src/modules/custom_list.cpp', 'src/modules/disk.cpp', 'src/modules/idle_inhibitor.cpp', 'src/modules/temperature.cpp', diff --git a/src/factory.cpp b/src/factory.cpp index 13b7803f..6cfa35d6 100644 --- a/src/factory.cpp +++ b/src/factory.cpp @@ -138,6 +138,9 @@ waybar::AModule* waybar::Factory::makeModule(const std::string& name) const { if (ref.compare(0, 7, "custom/") == 0 && ref.size() > 7) { return new waybar::modules::Custom(ref.substr(7), id, config_[name]); } + if(ref.compare(0, 11, "customlist/") == 0 && ref.size() > 11) { + return new waybar::modules::CustomList(ref.substr(12), bar_, id, config_[name]); + } } catch (const std::exception& e) { auto err = fmt::format("Disabling module \"{}\", {}", name, e.what()); throw std::runtime_error(err); diff --git a/src/modules/custom_list.cpp b/src/modules/custom_list.cpp new file mode 100644 index 00000000..263a6754 --- /dev/null +++ b/src/modules/custom_list.cpp @@ -0,0 +1,262 @@ +#include "modules/custom_list.hpp" + +#include + +waybar::modules::CustomList::CustomList(const std::string& name, + const Bar &bar, + const std::string& id, + const Json::Value& config) + : AModule(config, "custom-list-" + name, id, false, !config["disable-scroll"].asBool()), + name_(name), + bar_(bar), + box_(bar.vertical ? Gtk::ORIENTATION_VERTICAL : Gtk::ORIENTATION_HORIZONTAL, 0), + interval_(config["interval"].isUInt() ? config_["interval"].asUInt() : 0), + fp_(nullptr), + pid_(-1) { + box_.set_name("custom-list-" + name); + event_box_.add(box_); + if(!id.empty()) + box_.get_style_context()->add_class(id); + if (interval_.count() > 0) { + delayWorker(); + } else if (config_["exec"].isString()) { + continuousWorker(); + } +} + +waybar::modules::CustomList::~CustomList() { + if (pid_ != -1) { + killpg(pid_, SIGTERM); + pid_ = -1; + } +} + +void waybar::modules::CustomList::delayWorker() { + 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()); + } + dp.emit(); + } + thread_.sleep_for(interval_); + }; +} + +void waybar::modules::CustomList::continuousWorker() { + auto cmd = config_["exec"].asString(); + pid_ = -1; + fp_ = util::command::open(cmd, pid_); + if (!fp_) { + throw std::runtime_error("Unable to open " + cmd); + } + thread_ = [this, cmd] { + char* buff = nullptr; + 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_); + 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::CustomList::refresh(int sig) { + if (sig == SIGRTMIN + config_["signal"].asInt()) { + thread_.wake_up(); + } +} + +void waybar::modules::CustomList::handleEvent() { + if (!config_["exec-on-event"].isBool() || config_["exec-on-event"].asBool()) { + thread_.wake_up(); + } +} + +bool waybar::modules::CustomList::handleScroll(GdkEventScroll* e) { + //auto ret = ALabel::handleScroll(e); + ///* TODO */ + handleEvent(); + return false; +} + +bool waybar::modules::CustomList::handleToggle(GdkEventButton* const& e) { + /* TODO */ + //auto ret = ALabel::handleToggle(e); + handleEvent(); + return false; +} + +auto waybar::modules::CustomList::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 { + parseOutputJson(); + } + bool needReorder = false; + for(auto res = results_.begin(); res != results_.end(); res++) { + + std::string name = (*res)["name"].asString(); + auto bit = buttons_.find(name); + if(bit == buttons_.end()) { + needReorder = true; + } + auto &button = bit == buttons_.end() ? addButton(*res) : bit->second; + + auto prev = std::find_if(prev_.begin(), prev_.end(), [name](auto p) { + return p["name"] == name; + }); + + // Classes + std::vector prev_classes; + if(prev != prev_.end()) { + if ((*prev)["class"].isString()) { + prev_classes.push_back((*prev)["class"].asString()); + } else if ((*prev)["class"].isArray()) { + for (auto const& c : (*prev)["class"]) { + prev_classes.push_back(c.asString()); + } + } + } + std::vector new_classes; + if ((*res)["class"].isString()) { + new_classes.push_back((*res)["class"].asString()); + } else if ((*res)["class"].isArray()) { + for (auto const& c : (*res)["class"]) { + new_classes.push_back(c.asString()); + } + } + + for(auto it : prev_classes) { + if(std::find(new_classes.begin(), new_classes.end(), it) == new_classes.end()) + button.get_style_context()->remove_class(it); + } + for(auto it : new_classes) { + if(std::find(prev_classes.begin(), prev_classes.end(), it) == prev_classes.end()) + button.get_style_context()->add_class(it); + } + + // Reorder to match results order + if(needReorder) { + box_.reorder_child(button, res - results_.begin()); + } + + if(!(*res)["disable-markup"].asBool()) { + static_cast(button.get_children()[0])->set_markup((*res)["text"].asString()); + } else { + button.set_label((*res)["text"].asString()); + } + + if((*res)["hide"].asBool()) { + button.hide(); + } else { + button.show(); + } + + if(button.get_tooltip_markup() != (*res)["tooltip"].asString()) { + button.set_tooltip_markup((*res)["tooltip"].asString()); + } + + } + for(auto prev : prev_) { + auto res_it = std::find_if(results_.begin(), results_.end(), [prev](auto it) { + return it["name"] == prev["name"]; + }); + if(res_it == results_.end()) { + // Node has been removed + auto bit = buttons_.find(prev["name"].asString()); + if(bit != buttons_.end()) { + auto &button = bit->second; + box_.remove(button); + buttons_.erase(bit); + } + } + + } + prev_ = results_; + + // Call parent update + AModule::update(); +} + +void waybar::modules::CustomList::handleClick(std::string name) { + auto node = std::find_if(results_.begin(), results_.end(), [name](auto it) { + return it["name"] == name; + }); + + if(node == results_.end()) + return; + auto cmd = (*node)["onclick"]; + + if(cmd.isString()) { + util::command::execNoRead(cmd.asString()); + } +} + +Gtk::Button &waybar::modules::CustomList::addButton(const Json::Value &node) { + auto pair = buttons_.emplace(node["name"].asString(), node["name"].asString()); + auto &&button = pair.first->second; + box_.pack_start(button, false, false, 0); + button.set_name(name_ + node["name"].asString()); + button.set_relief(Gtk::RELIEF_NONE); + if(!config_["disable-click"].asBool()) { + button.signal_pressed().connect([this, node] { + handleClick(node["name"].asString()); + }); + } + return button; +} + +void waybar::modules::CustomList::parseOutputJson() { + std::istringstream output(output_.out); + std::string line; + class_.clear(); + while (getline(output, line)) { + auto parsed = parser_.parse(line); + if(!parsed["data"].isArray()) { + throw std::runtime_error("Output should be a list"); + } + results_.clear(); + for(auto it : parsed["data"]) { + results_.push_back(it); + } + break; + } +}