diff --git a/include/factory.hpp b/include/factory.hpp index 7afc89f..90d0ac1 100644 --- a/include/factory.hpp +++ b/include/factory.hpp @@ -31,6 +31,7 @@ #include "modules/hyprland/language.hpp" #include "modules/hyprland/submap.hpp" #include "modules/hyprland/window.hpp" +#include "modules/hyprland/workspaces.hpp" #endif #if defined(__FreeBSD__) || (defined(__linux__) && !defined(NO_FILESYSTEM)) #include "modules/battery.hpp" diff --git a/include/modules/hyprland/backend.hpp b/include/modules/hyprland/backend.hpp index d876781..179c82f 100644 --- a/include/modules/hyprland/backend.hpp +++ b/include/modules/hyprland/backend.hpp @@ -5,6 +5,7 @@ #include #include #include +#include "util/json.hpp" namespace waybar::modules::hyprland { @@ -22,12 +23,14 @@ class IPC { void unregisterForIPC(EventHandler*); std::string getSocket1Reply(const std::string& rq); + Json::Value getSocket1JsonReply(const std::string& rq); private: void startIPC(); void parseIPC(const std::string&); std::mutex callbackMutex; + util::JsonParser parser_; std::list> callbacks; }; diff --git a/include/modules/hyprland/workspaces.hpp b/include/modules/hyprland/workspaces.hpp new file mode 100644 index 0000000..0b8a452 --- /dev/null +++ b/include/modules/hyprland/workspaces.hpp @@ -0,0 +1,58 @@ +#include +#include + +#include "AModule.hpp" +#include "bar.hpp" +#include "modules/hyprland/backend.hpp" + +namespace waybar::modules::hyprland { + +struct WorkspaceDto { + int id; + + static WorkspaceDto parse(const Json::Value& value); +}; + +class Workspace { + public: + Workspace(int id); + Workspace(WorkspaceDto dto); + int id() const { return id_; }; + int active() const { return active_; }; + std::string& select_icon(std::map& icons_map); + void set_active(bool value = true) { active_ = value; }; + Gtk::Button& button() { return button_; }; + + void update(const std::string& format, const std::string& icon); + + private: + int id_; + bool active_; + + Gtk::Button button_; + Gtk::Box content_; + Gtk::Label label_; +}; + +class Workspaces : public AModule, public EventHandler { + public: + Workspaces(const std::string&, const waybar::Bar&, const Json::Value&); + virtual ~Workspaces(); + void update() override; + void init(); + + private: + void onEvent(const std::string&) override; + void sort_workspaces(); + + std::string format_; + std::map icons_map_; + bool with_icon_; + int active_workspace_id; + std::vector workspaces_; + std::mutex mutex_; + const Bar& bar_; + Gtk::Box box_; +}; + +} // namespace waybar::modules::hyprland diff --git a/man/waybar-hyprland-workspaces.5.scd b/man/waybar-hyprland-workspaces.5.scd new file mode 100644 index 0000000..0678fb2 --- /dev/null +++ b/man/waybar-hyprland-workspaces.5.scd @@ -0,0 +1,59 @@ +waybar-wlr-workspaces(5) + +# NAME + +waybar - hyprland workspaces module + +# DESCRIPTION + +The *workspaces* module displays the currently used workspaces in hyprland compositor. + +# CONFIGURATION + +Addressed by *hyprland/workspaces* + +*format*: ++ + typeof: string ++ + default: {id} ++ + The format, how information should be displayed. + +*format-icons*: ++ + typeof: array ++ + Based on the workspace id and state, the corresponding icon gets selected. See *icons*. + +# FORMAT REPLACEMENTS + +*{id}*: id of workspace assigned by compositor + +*{icon}*: Icon, as defined in *format-icons*. + +# ICONS + +Additional to workspace name matching, the following *format-icons* can be set. + +- *default*: Will be shown, when no string match is found. +- *active*: Will be shown, when workspace is active + +# EXAMPLES + +``` +"wlr/workspaces": { + "format": "{name}: {icon}", + "format-icons": { + "1": "", + "2": "", + "3": "", + "4": "", + "5": "", + "active": "", + "default": "" + }, + "sort-by-number": true +} +``` + +# Style + +- *#workspaces* +- *#workspaces button* +- *#workspaces button.active* diff --git a/man/waybar-wlr-workspaces.5.scd b/man/waybar-wlr-workspaces.5.scd index 169112f..4a256f0 100644 --- a/man/waybar-wlr-workspaces.5.scd +++ b/man/waybar-wlr-workspaces.5.scd @@ -65,7 +65,7 @@ Addressed by *wlr/workspaces* Additional to workspace name matching, the following *format-icons* can be set. - *default*: Will be shown, when no string match is found. -- *focused*: Will be shown, when workspace is focused +- *active*: Will be shown, when workspace is active # EXAMPLES @@ -78,7 +78,7 @@ Additional to workspace name matching, the following *format-icons* can be set. "3": "", "4": "", "5": "", - "focused": "", + "active": "", "default": "" }, "sort-by-number": true diff --git a/meson.build b/meson.build index beee053..085dcd0 100644 --- a/meson.build +++ b/meson.build @@ -240,6 +240,7 @@ if true src_files += 'src/modules/hyprland/window.cpp' src_files += 'src/modules/hyprland/language.cpp' src_files += 'src/modules/hyprland/submap.cpp' + src_files += 'src/modules/hyprland/workspaces.cpp' endif if libnl.found() and libnlgen.found() diff --git a/src/factory.cpp b/src/factory.cpp index bd2e7a2..1d7a00b 100644 --- a/src/factory.cpp +++ b/src/factory.cpp @@ -83,6 +83,9 @@ waybar::AModule* waybar::Factory::makeModule(const std::string& name) const { if (ref == "hyprland/submap") { return new waybar::modules::hyprland::Submap(id, bar_, config_[name]); } + if (ref == "hyprland/workspaces") { + return new waybar::modules::hyprland::Workspaces(id, bar_, config_[name]); + } #endif if (ref == "idle_inhibitor") { return new waybar::modules::IdleInhibitor(id, bar_, config_[name]); diff --git a/src/modules/hyprland/backend.cpp b/src/modules/hyprland/backend.cpp index 997fe11..79bc637 100644 --- a/src/modules/hyprland/backend.cpp +++ b/src/modules/hyprland/backend.cpp @@ -198,4 +198,8 @@ std::string IPC::getSocket1Reply(const std::string& rq) { return response; } +Json::Value IPC::getSocket1JsonReply(const std::string& rq) { + return parser_.parse(getSocket1Reply("j/" + rq)); +} + } // namespace waybar::modules::hyprland diff --git a/src/modules/hyprland/workspaces.cpp b/src/modules/hyprland/workspaces.cpp new file mode 100644 index 0000000..cdcb54e --- /dev/null +++ b/src/modules/hyprland/workspaces.cpp @@ -0,0 +1,167 @@ +#include "modules/hyprland/workspaces.hpp" + +#include +#include + +#include +#include +#include + +namespace waybar::modules::hyprland { + +Workspaces::Workspaces(const std::string &id, const Bar &bar, const Json::Value &config) + : AModule(config, "workspaces", id, false, false), + bar_(bar), + box_(bar.vertical ? Gtk::ORIENTATION_VERTICAL : Gtk::ORIENTATION_HORIZONTAL, 0) { + Json::Value config_format = config["format"]; + + format_ = config_format.isString() ? config_format.asString() : "{id}"; + with_icon_ = format_.find("{icon}") != std::string::npos; + + if (with_icon_ && icons_map_.empty()) { + Json::Value format_icons = config["format-icons"]; + for (std::string &name : format_icons.getMemberNames()) { + icons_map_.emplace(name, format_icons[name].asString()); + } + + icons_map_.emplace("", ""); + } + + box_.set_name("workspaces"); + if (!id.empty()) { + box_.get_style_context()->add_class(id); + } + event_box_.add(box_); + modulesReady = true; + if (!gIPC.get()) { + gIPC = std::make_unique(); + } + + init(); + + gIPC->registerForIPC("workspace", this); + gIPC->registerForIPC("createworkspace", this); + gIPC->registerForIPC("destroyworkspace", this); +} + +auto Workspaces::update() -> void { + std::lock_guard lock(mutex_); + for (Workspace &workspace : workspaces_) { + workspace.set_active(workspace.id() == active_workspace_id); + + std::string &workspace_icon = icons_map_[""]; + if (with_icon_) { + workspace_icon = workspace.select_icon(icons_map_); + } + + workspace.update(format_, workspace_icon); + } + + AModule::update(); +} + +void Workspaces::onEvent(const std::string &ev) { + std::string eventName(begin(ev), begin(ev) + ev.find_first_of('>')); + std::string payload = ev.substr(eventName.size() + 2); + if (eventName == "workspace") { + std::from_chars(payload.data(), payload.data() + payload.size(), active_workspace_id); + } else if (eventName == "destroyworkspace") { + int deleted_workspace_id; + std::from_chars(payload.data(), payload.data() + payload.size(), deleted_workspace_id); + workspaces_.erase(std::remove_if(workspaces_.begin(), workspaces_.end(), + [&](Workspace &x) { return x.id() == deleted_workspace_id; })); + } else if (eventName == "createworkspace") { + int new_workspace_id; + std::from_chars(payload.data(), payload.data() + payload.size(), new_workspace_id); + workspaces_.push_back(new_workspace_id); + Gtk::Button &new_workspace_button = workspaces_.back().button(); + box_.pack_end(new_workspace_button, false, false); + sort_workspaces(); + new_workspace_button.show_all(); + } + + dp.emit(); +} + +void Workspaces::init() { + const auto activeWorkspace = WorkspaceDto::parse(gIPC->getSocket1JsonReply("activeworkspace")); + active_workspace_id = activeWorkspace.id; + const Json::Value workspaces_json = gIPC->getSocket1JsonReply("workspaces"); + for (const Json::Value &workspace_json : workspaces_json) { + workspaces_.push_back(Workspace(WorkspaceDto::parse(workspace_json))); + } + + for (auto &workspace : workspaces_) { + box_.pack_start(workspace.button(), false, false); + } + + sort_workspaces(); + + dp.emit(); +} + +Workspaces::~Workspaces() { + gIPC->unregisterForIPC(this); + // wait for possible event handler to finish + std::lock_guard lg(mutex_); +} + +WorkspaceDto WorkspaceDto::parse(const Json::Value &value) { + return WorkspaceDto{value["id"].asInt()}; +} + +Workspace::Workspace(WorkspaceDto dto) : Workspace(dto.id){}; + +Workspace::Workspace(int id) : id_(id) { + button_.set_relief(Gtk::RELIEF_NONE); + content_.set_center_widget(label_); + button_.add(content_); +}; + +void add_or_remove_class(Glib::RefPtr context, bool condition, + const std::string &class_name) { + if (condition) { + context->add_class(class_name); + } else { + context->remove_class(class_name); + } +} + +void Workspace::update(const std::string &format, const std::string &icon) { + Glib::RefPtr style_context = button_.get_style_context(); + add_or_remove_class(style_context, active(), "active"); + + label_.set_markup( + fmt::format(fmt::runtime(format), fmt::arg("id", id()), fmt::arg("icon", icon))); +} + +void Workspaces::sort_workspaces() { + std::sort(workspaces_.begin(), workspaces_.end(), + [](Workspace &lhs, Workspace &rhs) { return lhs.id() < rhs.id(); }); + + for (size_t i = 0; i < workspaces_.size(); ++i) { + box_.reorder_child(workspaces_[i].button(), i); + } +} + +std::string &Workspace::select_icon(std::map &icons_map) { + if (active()) { + auto active_icon_it = icons_map.find("active"); + if (active_icon_it != icons_map.end()) { + return active_icon_it->second; + } + } + + auto named_icon_it = icons_map.find(std::to_string(id())); + if (named_icon_it != icons_map.end()) { + return named_icon_it->second; + } + + auto default_icon_it = icons_map.find("default"); + if (default_icon_it != icons_map.end()) { + return default_icon_it->second; + } + + return icons_map[""]; +} +} // namespace waybar::modules::hyprland