diff --git a/README.md b/README.md index 2b57b139..b6ecd962 100644 --- a/README.md +++ b/README.md @@ -9,6 +9,7 @@ - Sway (Workspaces, Binding mode, Focused window name) - River (Mapping mode, Tags, Focused window name) - Hyprland (Focused window name) +- DWL (Tags) [requires dwl ipc patch](https://github.com/djpohly/dwl/wiki/ipc) - Tray [#21](https://github.com/Alexays/Waybar/issues/21) - Local time - Battery diff --git a/include/factory.hpp b/include/factory.hpp index 89eea0e9..a33eac37 100644 --- a/include/factory.hpp +++ b/include/factory.hpp @@ -23,6 +23,9 @@ #include "modules/river/tags.hpp" #include "modules/river/window.hpp" #endif +#ifdef HAVE_DWL +#include "modules/dwl/tags.hpp" +#endif #ifdef HAVE_HYPRLAND #include "modules/hyprland/backend.hpp" #include "modules/hyprland/language.hpp" diff --git a/include/modules/dwl/tags.hpp b/include/modules/dwl/tags.hpp new file mode 100644 index 00000000..6e6d086f --- /dev/null +++ b/include/modules/dwl/tags.hpp @@ -0,0 +1,34 @@ +#pragma once + +#include +#include + +#include "AModule.hpp" +#include "bar.hpp" +#include "dwl-bar-ipc-unstable-v1-client-protocol.h" +#include "xdg-output-unstable-v1-client-protocol.h" + +namespace waybar::modules::dwl { + +class Tags : public waybar::AModule { + public: + Tags(const std::string &, const waybar::Bar &, const Json::Value &); + virtual ~Tags(); + + // Handlers for wayland events + void handle_view_tags(uint32_t tag, uint32_t state, uint32_t clients, uint32_t focused); + + void handle_primary_clicked(uint32_t tag); + bool handle_button_press(GdkEventButton *event_button, uint32_t tag); + + struct zdwl_manager_v1 *status_manager_; + struct wl_seat *seat_; + + private: + const waybar::Bar &bar_; + Gtk::Box box_; + std::vector buttons_; + struct zdwl_output_v1 *output_status_; +}; + +} /* namespace waybar::modules::dwl */ diff --git a/man/waybar-dwl-tags.5.scd b/man/waybar-dwl-tags.5.scd new file mode 100644 index 00000000..06fb577f --- /dev/null +++ b/man/waybar-dwl-tags.5.scd @@ -0,0 +1,49 @@ +waybar-dwl-tags(5) + +# NAME + +waybar - dwl tags module + +# DESCRIPTION + +The *tags* module displays the current state of tags in dwl. + +# CONFIGURATION + +Addressed by *dwl/tags* + +*num-tags*: ++ + typeof: uint ++ + default: 9 ++ + The number of tags that should be displayed. Max 32. + +*tag-labels*: ++ + typeof: array ++ + The label to display for each tag. + +*disable-click*: ++ + typeof: bool ++ + default: false ++ + If set to false, you can left click to set focused tag. Right click to toggle tag focus. If set to true this behaviour is disabled. + +# EXAMPLE + +``` +"dwl/tags": { + "num-tags": 5 +} +``` + +# STYLE + +- *#tags button* +- *#tags button.occupied* +- *#tags button.focused* +- *#tags button.urgent* + +Note that occupied/focused/urgent status may overlap. That is, a tag may be +both occupied and focused at the same time. + +# SEE ALSO + +waybar(5), dwl(1) diff --git a/meson.build b/meson.build index 58e1c672..3ab19626 100644 --- a/meson.build +++ b/meson.build @@ -227,6 +227,11 @@ if true src_files += 'src/modules/river/layout.cpp' endif +if true + add_project_arguments('-DHAVE_DWL', language: 'cpp') + src_files += 'src/modules/dwl/tags.cpp' +endif + if true add_project_arguments('-DHAVE_HYPRLAND', language: 'cpp') src_files += 'src/modules/hyprland/backend.cpp' diff --git a/protocol/dwl-bar-ipc-unstable-v1.xml b/protocol/dwl-bar-ipc-unstable-v1.xml new file mode 100644 index 00000000..0dcec4f4 --- /dev/null +++ b/protocol/dwl-bar-ipc-unstable-v1.xml @@ -0,0 +1,167 @@ + + + + + This protocol allows clients to get updates from dwl and vice versa. + + Warning! The protocol described in this file is experimental and + backward incompatible changes may be made. Backward compatible + changes may be added together with the corresponding interface + version bump. + Backward incompatible changes are done by bumping the version + number in the protocol and interface names and resetting the + interface version. Once the protocol is to be declared stable, + the 'z' prefix and the version number in the protocol and + interface names are removed and the interface version number is + reset. + + + + + This interface is exposed as a global in wl_registry. + + Clients can use this interface to get a dwl_output. + After binding the client will revieve dwl_manager.tag and dwl_manager.layout events. + The dwl_manager.tag and dwl_manager.layout events expose tags and layouts to the client. + + + + + Indicates that the client will not the dwl_manager object anymore. + Objects created through this instance are not affected. + + + + + + Get a dwl_output for the specified wl_output. + + + + + + + + This event is sent after binding. + A roundtrip after binding guarantees the client recieved all tags. + + + + + + + This event is sent after binding. + A roundtrip after binding guarantees the client recieved all layouts. + + + + + + + + Observe and control a dwl output. + + Events are double-buffered: + Clients should cache events and redraw when a dwl_output.done event is sent. + + Request are not double-buffered: + The compositor will update immediately upon request. + + + + + + + + + + + Indicates to that the client no longer needs this dwl_output. + + + + + + Indicates the client should hide or show themselves. + If the client is visible then hide, if hidden then show. + + + + + + Indicates if the output is active. Zero is invalid, nonzero is valid. + + + + + + + Indicates that a tag has been updated. + + + + + + + + + + Indicates a new layout is selected. + + + + + + + Indicates the title has changed. + + + + + + + Indicates the appid has changed. + + + + + + + Indicates the layout has changed. Since layout symbols are now dynamic. + As opposed to the zdwl_manager_v1.layout event, this should take precendence when displaying. + This also means ignoring the zdwl_output_v1.layout event. + + + + + + + + Indicates that a sequence of status updates have finished and the client should redraw. + + + + + + + + + + + + + + + + + The tags are updated as follows: + new_tags = (current_tags AND and_tags) XOR xor_tags + + + + + + diff --git a/protocol/meson.build b/protocol/meson.build index 6e82d63d..9c1b0193 100644 --- a/protocol/meson.build +++ b/protocol/meson.build @@ -30,6 +30,7 @@ client_protocols = [ ['ext-workspace-unstable-v1.xml'], ['river-status-unstable-v1.xml'], ['river-control-unstable-v1.xml'], + ['dwl-bar-ipc-unstable-v1.xml'], ] client_protos_src = [] diff --git a/src/factory.cpp b/src/factory.cpp index dd5c142d..8102fee8 100644 --- a/src/factory.cpp +++ b/src/factory.cpp @@ -68,6 +68,11 @@ waybar::AModule* waybar::Factory::makeModule(const std::string& name) const { return new waybar::modules::river::Layout(id, bar_, config_[name]); } #endif +#ifdef HAVE_DWL + if (ref == "dwl/tags") { + return new waybar::modules::dwl::Tags(id, bar_, config_[name]); + } +#endif #ifdef HAVE_HYPRLAND if (ref == "hyprland/window") { return new waybar::modules::hyprland::Window(id, bar_, config_[name]); diff --git a/src/modules/dwl/tags.cpp b/src/modules/dwl/tags.cpp new file mode 100644 index 00000000..a38e7094 --- /dev/null +++ b/src/modules/dwl/tags.cpp @@ -0,0 +1,224 @@ +#include "modules/dwl/tags.hpp" + +#include +#include +#include +#include + +#include + +#include "client.hpp" +#include "dwl-bar-ipc-unstable-v1-client-protocol.h" + +#define TAG_INACTIVE 0 +#define TAG_ACTIVE 1 +#define TAG_URGENT 2 + +namespace waybar::modules::dwl { + +/* dwl stuff */ +wl_array tags, layouts; + +static uint num_tags = 0; + +void toggle_visibility(void* data, zdwl_output_v1* zdwl_output_v1) { + // Intentionally empty +} + +void active(void* data, zdwl_output_v1* zdwl_output_v1, uint32_t active) { + // Intentionally empty +} + +static void set_tag(void* data, zdwl_output_v1* zdwl_output_v1, uint32_t tag, uint32_t state, uint32_t clients, uint32_t focused) { + static_cast(data)->handle_view_tags(tag, state, clients, focused); + + num_tags = (state & ZDWL_OUTPUT_V1_TAG_STATE_ACTIVE) ? num_tags | (1 << tag) : num_tags & ~(1 << tag); +} + +void set_layout_symbol(void* data, zdwl_output_v1* zdwl_output_v1, const char *layout) { + // Intentionally empty +} + +void title(void* data, zdwl_output_v1* zdwl_output_v1, const char* title) { + // Intentionally empty +} + +void dwl_frame(void* data, zdwl_output_v1* zdwl_output_v1) { + // Intentionally empty +} + +static void set_layout(void* data, zdwl_output_v1* zdwl_output_v1, uint32_t layout) { + // Intentionally empty +} + +static void appid(void *data, zdwl_output_v1 *zdwl_output_v1, const char *appid) { + // Intentionally empty +}; + +static const zdwl_output_v1_listener output_status_listener_impl { + .toggle_visibility = toggle_visibility, + .active = active, + .tag = set_tag, + .layout = set_layout, + .title = title, + .appid = appid, + .layout_symbol = set_layout_symbol, + .frame = dwl_frame, +}; + +void add_layout(void* data, zdwl_manager_v1* zdwl_manager_v1, const char* name) { + void* temp = wl_array_add(&layouts, sizeof(char**)); + if (!temp) + return; + + char* dup = strdup(name); + + memcpy(temp, &dup, sizeof(char**)); +} + +void add_tag(void* data, zdwl_manager_v1* zdwl_manager_v1, const char* name) { + void* temp = wl_array_add(&tags, sizeof(char**)); + if (!temp) + return; + + char* dup = strdup(name); /* Gain ownership of name */ + + memcpy(temp, &dup, sizeof(char**)); /* Copy a pointer of it into the array */; +} + +static const struct zdwl_manager_v1_listener dwl_listener = { + .tag = add_tag, + .layout = add_layout, +}; + +static void handle_global(void *data, struct wl_registry *registry, uint32_t name, + const char *interface, uint32_t version) { + if (std::strcmp(interface, zdwl_manager_v1_interface.name) == 0) { + static_cast(data)->status_manager_ = static_cast( + (zdwl_manager_v1*)wl_registry_bind(registry, name, &zdwl_manager_v1_interface, 3)); + zdwl_manager_v1_add_listener(static_cast(data)->status_manager_, &dwl_listener, NULL); + } + if (std::strcmp(interface, wl_seat_interface.name) == 0) { + version = std::min(version, 1); + static_cast(data)->seat_ = static_cast( + wl_registry_bind(registry, name, &wl_seat_interface, version)); + } +} + +static void handle_global_remove(void *data, struct wl_registry *registry, uint32_t name) { + /* Ignore event */ +} + +static const wl_registry_listener registry_listener_impl = {.global = handle_global, + .global_remove = handle_global_remove}; + +Tags::Tags(const std::string &id, const waybar::Bar &bar, const Json::Value &config) + : waybar::AModule(config, "tags", id, false, false), + status_manager_{nullptr}, + seat_{nullptr}, + bar_(bar), + box_{bar.vertical ? Gtk::ORIENTATION_VERTICAL : Gtk::ORIENTATION_HORIZONTAL, 0}, + output_status_{nullptr} { + struct wl_display *display = Client::inst()->wl_display; + struct wl_registry *registry = wl_display_get_registry(display); + wl_registry_add_listener(registry, ®istry_listener_impl, this); + wl_display_roundtrip(display); + + if (!status_manager_) { + spdlog::error("dwl_status_manager_v1 not advertised"); + return; + } + + if (!seat_) { + spdlog::error("wl_seat not advertised"); + } + + box_.set_name("tags"); + if (!id.empty()) { + box_.get_style_context()->add_class(id); + } + event_box_.add(box_); + + // Default to 9 tags, cap at 32 + const uint32_t num_tags = + config["num-tags"].isUInt() ? std::min(32, config_["num-tags"].asUInt()) : 9; + + std::vector tag_labels(num_tags); + for (uint32_t tag = 0; tag < num_tags; ++tag) { + tag_labels[tag] = std::to_string(tag + 1); + } + const Json::Value custom_labels = config["tag-labels"]; + if (custom_labels.isArray() && !custom_labels.empty()) { + for (uint32_t tag = 0; tag < std::min(num_tags, custom_labels.size()); ++tag) { + tag_labels[tag] = custom_labels[tag].asString(); + } + } + + uint32_t i = 1; + for (const auto &tag_label : tag_labels) { + Gtk::Button &button = buttons_.emplace_back(tag_label); + button.set_relief(Gtk::RELIEF_NONE); + box_.pack_start(button, false, false, 0); + if (!config_["disable-click"].asBool()) { + button.signal_clicked().connect( + sigc::bind(sigc::mem_fun(*this, &Tags::handle_primary_clicked), i)); + button.signal_button_press_event().connect( + sigc::bind(sigc::mem_fun(*this, &Tags::handle_button_press), i)); + } + button.show(); + i <<= 1; + } + + struct wl_output *output = gdk_wayland_monitor_get_wl_output(bar_.output->monitor->gobj()); + output_status_ = zdwl_manager_v1_get_output(status_manager_, output); + zdwl_output_v1_add_listener(output_status_, &output_status_listener_impl, this); + + zdwl_manager_v1_destroy(status_manager_); +} + +Tags::~Tags() { + if (output_status_) { + zdwl_manager_v1_destroy(status_manager_); + } +} + +void Tags::handle_primary_clicked(uint32_t tag) { + if (!output_status_) return; + + zdwl_output_v1_set_tags(output_status_, tag, 1); +} + +bool Tags::handle_button_press(GdkEventButton *event_button, uint32_t tag) { + if (event_button->type == GDK_BUTTON_PRESS && event_button->button == 3) { + + if (!output_status_) return true; + zdwl_output_v1_set_tags(output_status_, num_tags ^ tag, 0); + } + return true; +} + + +void Tags::handle_view_tags(uint32_t tag, uint32_t state, uint32_t clients, uint32_t focused) { + // First clear all occupied state + auto &button = buttons_[tag]; + if (clients) { + button.get_style_context()->add_class("occupied"); + } else { + button.get_style_context()->remove_class("occupied"); + } + + if (state & TAG_ACTIVE) { + button.get_style_context()->add_class("focused"); + } else { + button.get_style_context()->remove_class("focused"); + } + + if (state & TAG_URGENT) { + button.get_style_context()->add_class("urgent"); + } else { + button.get_style_context()->remove_class("urgent"); + } + +} + +} /* namespace waybar::modules::dwl */