#include "modules/wlr/taskbar.hpp" #include "glibmm/refptr.h" #include "util/format.hpp" #include #include #include #include #include #include #include #include #include #include namespace waybar::modules::wlr { /* String manipulation methods */ const std::string WHITESPACE = " \n\r\t\f\v"; static std::string ltrim(const std::string& s) { size_t start = s.find_first_not_of(WHITESPACE); return (start == std::string::npos) ? "" : s.substr(start); } static std::string rtrim(const std::string& s) { size_t end = s.find_last_not_of(WHITESPACE); return (end == std::string::npos) ? "" : s.substr(0, end + 1); } static std::string trim(const std::string& s) { return rtrim(ltrim(s)); } /* Icon loading functions */ static std::vector search_prefix() { std::vector prefixes = {""}; auto xdg_data_dirs = std::getenv("XDG_DATA_DIRS"); if (!xdg_data_dirs) { prefixes.emplace_back("/usr/share/"); prefixes.emplace_back("/usr/local/share/"); } else { std::string xdg_data_dirs_str(xdg_data_dirs); size_t start = 0, end = 0; do { end = xdg_data_dirs_str.find(':', start); auto p = xdg_data_dirs_str.substr(start, end-start); prefixes.push_back(trim(p) + "/"); start = end == std::string::npos ? end : end + 1; } while(end != std::string::npos); } for (auto& p : prefixes) spdlog::debug("Using 'desktop' search path prefix: {}", p); return prefixes; } /* Method 1 - get the correct icon name from the desktop file */ static std::string get_from_desktop_app_info(const std::string &app_id) { static std::vector prefixes = search_prefix(); std::vector app_folders = { "", "applications/", "applications/kde/", "applications/org.kde." }; std::vector suffixes = { "", ".desktop" }; Glib::RefPtr app_info; for (auto& prefix : prefixes) for (auto& folder : app_folders) for (auto& suffix : suffixes) if (!app_info) app_info = Gio::DesktopAppInfo::create_from_filename(prefix + folder + app_id + suffix); if (app_info && app_info->get_icon()) return app_info->get_icon()->to_string(); return ""; } /* Method 2 - use the app_id and check whether there is an icon with this name in the icon theme */ static std::string get_from_icon_theme(const Glib::RefPtr& icon_theme, const std::string &app_id) { if (icon_theme->lookup_icon(app_id, 24)) return app_id; return ""; } static bool image_load_icon(Gtk::Image& image, const Glib::RefPtr& icon_theme, const std::string &app_id_list, int size) { std::string app_id; std::istringstream stream(app_id_list); bool found = false; /* Wayfire sends a list of app-id's in space separated format, other compositors * send a single app-id, but in any case this works fine */ while (stream >> app_id) { auto lower_app_id = app_id; std::transform(lower_app_id.begin(), lower_app_id.end(), lower_app_id.begin(), [](char c){ return std::tolower(c); }); std::string icon_name = get_from_icon_theme(icon_theme, app_id); if (icon_name.empty()) icon_name = get_from_icon_theme(icon_theme, lower_app_id); if (icon_name.empty()) icon_name = get_from_desktop_app_info(app_id); if (icon_name.empty()) icon_name = get_from_desktop_app_info(lower_app_id); if (icon_name.empty()) continue; auto pixbuf = icon_theme->load_icon(icon_name, size, Gtk::ICON_LOOKUP_FORCE_SIZE); if (pixbuf) { image.set(pixbuf); found = true; break; } } return found; } /* Task class implementation */ uint32_t Task::global_id = 0; static void tl_handle_title(void *data, struct zwlr_foreign_toplevel_handle_v1 *handle, const char *title) { return static_cast(data)->handle_title(title); } static void tl_handle_app_id(void *data, struct zwlr_foreign_toplevel_handle_v1 *handle, const char *app_id) { return static_cast(data)->handle_app_id(app_id); } static void tl_handle_output_enter(void *data, struct zwlr_foreign_toplevel_handle_v1 *handle, struct wl_output *output) { return static_cast(data)->handle_output_enter(output); } static void tl_handle_output_leave(void *data, struct zwlr_foreign_toplevel_handle_v1 *handle, struct wl_output *output) { return static_cast(data)->handle_output_leave(output); } static void tl_handle_state(void *data, struct zwlr_foreign_toplevel_handle_v1 *handle, struct wl_array *state) { return static_cast(data)->handle_state(state); } static void tl_handle_done(void *data, struct zwlr_foreign_toplevel_handle_v1 *handle) { return static_cast(data)->handle_done(); } static void tl_handle_parent(void *data, struct zwlr_foreign_toplevel_handle_v1 *handle, struct zwlr_foreign_toplevel_handle_v1 *parent) { /* This is explicitly left blank */ } static void tl_handle_closed(void *data, struct zwlr_foreign_toplevel_handle_v1 *handle) { return static_cast(data)->handle_closed(); } static const struct zwlr_foreign_toplevel_handle_v1_listener toplevel_handle_impl = { .title = tl_handle_title, .app_id = tl_handle_app_id, .output_enter = tl_handle_output_enter, .output_leave = tl_handle_output_leave, .state = tl_handle_state, .done = tl_handle_done, .closed = tl_handle_closed, .parent = tl_handle_parent, }; Task::Task(const waybar::Bar &bar, const Json::Value &config, Taskbar *tbar, struct zwlr_foreign_toplevel_handle_v1 *tl_handle, struct wl_seat *seat) : bar_{bar}, config_{config}, tbar_{tbar}, handle_{tl_handle}, seat_{seat}, id_{global_id++}, content_{bar.vertical ? Gtk::ORIENTATION_VERTICAL : Gtk::ORIENTATION_HORIZONTAL, 0}, button_visible_{false} { zwlr_foreign_toplevel_handle_v1_add_listener(handle_, &toplevel_handle_impl, this); button_.set_relief(Gtk::RELIEF_NONE); content_.add(text_before_); content_.add(icon_); content_.add(text_after_); content_.show(); button_.add(content_); with_icon_ = false; format_before_.clear(); format_after_.clear(); if (config_["format"].isString()) { /* The user defined a format string, use it */ auto format = config_["format"].asString(); auto icon_pos = format.find("{icon}"); if (icon_pos == 0) { with_icon_ = true; format_after_ = format.substr(6); } else if (icon_pos == std::string::npos) { format_before_ = format; } else { with_icon_ = true; format_before_ = format.substr(0, icon_pos); format_after_ = format.substr(icon_pos + 6); } } else { /* The default is to only show the icon */ with_icon_ = true; } /* Strip spaces at the beginning and end of the format strings */ format_tooltip_.clear(); if (!config_["tooltip"].isBool() || config_["tooltip"].asBool()) { if (config_["tooltip-format"].isString()) format_tooltip_ = config_["tooltip-format"].asString(); else format_tooltip_ = "{title}"; } /* Handle click events if configured */ if (config_["on-click"].isString() || config_["on-click-middle"].isString() || config_["on-click-right"].isString()) { button_.add_events(Gdk::BUTTON_PRESS_MASK); button_.signal_button_press_event().connect( sigc::mem_fun(*this, &Task::handle_clicked), false); } } Task::~Task() { if (handle_) { zwlr_foreign_toplevel_handle_v1_destroy(handle_); handle_ = nullptr; } if (button_visible_) { tbar_->remove_button(button_); button_visible_ = false; } } std::string Task::repr() const { std::stringstream ss; ss << "Task (" << id_ << ") " << title_ << " [" << app_id_ << "] <" << (active() ? "A" : "a") << (maximized() ? "M" : "m") << (minimized() ? "I" : "i") << (fullscreen() ? "F" : "f") << ">"; return ss.str(); } std::string Task::state_string(bool shortened) const { std::stringstream ss; if (shortened) ss << (minimized() ? "m" : "") << (maximized() ? "M" : "") << (active() ? "A" : "") << (fullscreen() ? "F" : ""); else ss << (minimized() ? "minimized " : "") << (maximized() ? "maximized " : "") << (active() ? "active " : "") << (fullscreen() ? "fullscreen " : ""); std::string res = ss.str(); if (shortened || res.empty()) return res; else return res.substr(0, res.size() - 1); } void Task::handle_title(const char *title) { title_ = title; } void Task::handle_app_id(const char *app_id) { app_id_ = app_id; if (!with_icon_) return; int icon_size = config_["icon-size"].isInt() ? config_["icon-size"].asInt() : 16; bool found = false; for (auto& icon_theme : tbar_->icon_themes()) { if (image_load_icon(icon_, icon_theme, app_id_, icon_size)) { found = true; break; } } if (found) icon_.show(); else spdlog::debug("Couldn't find icon for {}", app_id_); } void Task::handle_output_enter(struct wl_output *output) { spdlog::debug("{} entered output {}", repr(), (void*)output); if (!button_visible_ && (tbar_->all_outputs() || tbar_->show_output(output))) { /* The task entered the output of the current bar make the button visible */ tbar_->add_button(button_); button_.show(); button_visible_ = true; spdlog::debug("{} now visible on {}", repr(), bar_.output->name); } } void Task::handle_output_leave(struct wl_output *output) { spdlog::debug("{} left output {}", repr(), (void*)output); if (button_visible_ && !tbar_->all_outputs() && tbar_->show_output(output)) { /* The task left the output of the current bar, make the button invisible */ tbar_->remove_button(button_); button_.hide(); button_visible_ = false; spdlog::debug("{} now invisible on {}", repr(), bar_.output->name); } } void Task::handle_state(struct wl_array *state) { state_ = 0; for (auto* entry = static_cast(state->data); entry < static_cast(state->data) + state->size; entry++) { if (*entry == ZWLR_FOREIGN_TOPLEVEL_HANDLE_V1_STATE_MAXIMIZED) state_ |= MAXIMIZED; if (*entry == ZWLR_FOREIGN_TOPLEVEL_HANDLE_V1_STATE_MINIMIZED) state_ |= MINIMIZED; if (*entry == ZWLR_FOREIGN_TOPLEVEL_HANDLE_V1_STATE_ACTIVATED) state_ |= ACTIVE; if (*entry == ZWLR_FOREIGN_TOPLEVEL_HANDLE_V1_STATE_FULLSCREEN) state_ |= FULLSCREEN; } } void Task::handle_done() { spdlog::debug("{} changed", repr()); if (state_ & MAXIMIZED) { button_.get_style_context()->add_class("maximized"); } else if (!(state_ & MAXIMIZED)) { button_.get_style_context()->remove_class("maximized"); } if (state_ & MINIMIZED) { button_.get_style_context()->add_class("minimized"); } else if (!(state_ & MINIMIZED)) { button_.get_style_context()->remove_class("minimized"); } if (state_ & ACTIVE) { button_.get_style_context()->add_class("active"); } else if (!(state_ & ACTIVE)) { button_.get_style_context()->remove_class("active"); } if (state_ & FULLSCREEN) { button_.get_style_context()->add_class("fullscreen"); } else if (!(state_ & FULLSCREEN)) { button_.get_style_context()->remove_class("fullscreen"); } if (config_["active-first"].isBool() && config_["active-first"].asBool() && active()) tbar_->move_button(button_, 0); tbar_->dp.emit(); } void Task::handle_closed() { spdlog::debug("{} closed", repr()); zwlr_foreign_toplevel_handle_v1_destroy(handle_); handle_ = nullptr; if (button_visible_) { tbar_->remove_button(button_); button_visible_ = false; } tbar_->remove_task(id_); } bool Task::handle_clicked(GdkEventButton *bt) { std::string action; if (config_["on-click"].isString() && bt->button == 1) action = config_["on-click"].asString(); else if (config_["on-click-middle"].isString() && bt->button == 2) action = config_["on-click-middle"].asString(); else if (config_["on-click-right"].isString() && bt->button == 3) action = config_["on-click-right"].asString(); if (action.empty()) return true; else if (action == "activate") activate(); else if (action == "minimize") minimize(!minimized()); else if (action == "minimize-raise"){ if (minimized()) minimize(false); else if (active()) minimize(true); else activate(); } else if (action == "maximize") maximize(!maximized()); else if (action == "fullscreen") fullscreen(!fullscreen()); else if (action == "close") close(); else spdlog::warn("Unknown action {}", action); return true; } bool Task::operator==(const Task &o) const { return o.id_ == id_; } bool Task::operator!=(const Task &o) const { return o.id_ != id_; } void Task::update() { bool markup = config_["markup"].isBool() ? config_["markup"].asBool() : false; std::string title = title_; std::string app_id = app_id_; if (markup) { title = Glib::Markup::escape_text(title); app_id = Glib::Markup::escape_text(app_id); } if (!format_before_.empty()) { auto txt = fmt::format(format_before_, fmt::arg("title", title), fmt::arg("app_id", app_id), fmt::arg("state", state_string()), fmt::arg("short_state", state_string(true)) ); if (markup) text_before_.set_markup(txt); else text_before_.set_label(txt); text_before_.show(); } if (!format_after_.empty()) { auto txt = fmt::format(format_after_, fmt::arg("title", title), fmt::arg("app_id", app_id), fmt::arg("state", state_string()), fmt::arg("short_state", state_string(true)) ); if (markup) text_after_.set_markup(txt); else text_after_.set_label(txt); text_after_.show(); } if (!format_tooltip_.empty()) { auto txt = fmt::format(format_tooltip_, fmt::arg("title", title), fmt::arg("app_id", app_id), fmt::arg("state", state_string()), fmt::arg("short_state", state_string(true)) ); if (markup) button_.set_tooltip_markup(txt); else button_.set_tooltip_text(txt); } } void Task::maximize(bool set) { if (set) zwlr_foreign_toplevel_handle_v1_set_maximized(handle_); else zwlr_foreign_toplevel_handle_v1_unset_maximized(handle_); } void Task::minimize(bool set) { if (set) zwlr_foreign_toplevel_handle_v1_set_minimized(handle_); else zwlr_foreign_toplevel_handle_v1_unset_minimized(handle_); } void Task::activate() { zwlr_foreign_toplevel_handle_v1_activate(handle_, seat_); } void Task::fullscreen(bool set) { if (set) zwlr_foreign_toplevel_handle_v1_set_fullscreen(handle_, nullptr); else zwlr_foreign_toplevel_handle_v1_unset_fullscreen(handle_); } void Task::close() { zwlr_foreign_toplevel_handle_v1_close(handle_); } /* Taskbar class implementation */ static void handle_global(void *data, struct wl_registry *registry, uint32_t name, const char *interface, uint32_t version) { if (std::strcmp(interface, zwlr_foreign_toplevel_manager_v1_interface.name) == 0) { static_cast(data)->register_manager(registry, name, version); } else if (std::strcmp(interface, wl_seat_interface.name) == 0) { static_cast(data)->register_seat(registry, name, version); } } static void handle_global_remove(void *data, struct wl_registry *registry, uint32_t name) { /* Nothing to do here */ } static const wl_registry_listener registry_listener_impl = { .global = handle_global, .global_remove = handle_global_remove }; Taskbar::Taskbar(const std::string &id, const waybar::Bar &bar, const Json::Value &config) : waybar::AModule(config, "taskbar", id, false, false), bar_(bar), box_{bar.vertical ? Gtk::ORIENTATION_VERTICAL : Gtk::ORIENTATION_HORIZONTAL, 0}, manager_{nullptr}, seat_{nullptr} { box_.set_name("taskbar"); if (!id.empty()) { box_.get_style_context()->add_class(id); } event_box_.add(box_); 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 (!manager_) { spdlog::error("Failed to register as toplevel manager"); return; } if (!seat_) { spdlog::error("Failed to get wayland seat"); return; } /* Get the configured icon theme if specified */ if (config_["icon-theme"].isArray()) { for (auto& c : config_["icon-theme"]) { auto it_name = c.asString(); auto it = Gtk::IconTheme::create(); it->set_custom_theme(it_name); spdlog::debug("Use custom icon theme: {}", it_name); icon_themes_.push_back(it); } } else if (config_["icon-theme"].isString()) { auto it_name = config_["icon-theme"].asString(); auto it = Gtk::IconTheme::create(); it->set_custom_theme(it_name); spdlog::debug("Use custom icon theme: {}", it_name); icon_themes_.push_back(it); } icon_themes_.push_back(Gtk::IconTheme::get_default()); } Taskbar::~Taskbar() { if (manager_) { zwlr_foreign_toplevel_manager_v1_destroy(manager_); manager_ = nullptr; } } void Taskbar::update() { for (auto& t : tasks_) { t->update(); } AModule::update(); } static void tm_handle_toplevel(void *data, struct zwlr_foreign_toplevel_manager_v1 *manager, struct zwlr_foreign_toplevel_handle_v1 *tl_handle) { return static_cast(data)->handle_toplevel_create(tl_handle); } static void tm_handle_finished(void *data, struct zwlr_foreign_toplevel_manager_v1 *manager) { return static_cast(data)->handle_finished(); } static const struct zwlr_foreign_toplevel_manager_v1_listener toplevel_manager_impl = { .toplevel = tm_handle_toplevel, .finished = tm_handle_finished, }; void Taskbar::register_manager(struct wl_registry *registry, uint32_t name, uint32_t version) { if (manager_) { spdlog::warn("Register foreign toplevel manager again although already existing!"); return; } if (version < ZWLR_FOREIGN_TOPLEVEL_HANDLE_V1_SET_FULLSCREEN_SINCE_VERSION) { spdlog::warn("Foreign toplevel manager server does not have the appropriate version." " To be able to use all features, you need at least version 2, but server is version {}", version); } // limit version to a highest supported by the client protocol file version = std::min(version, zwlr_foreign_toplevel_manager_v1_interface.version); manager_ = static_cast(wl_registry_bind(registry, name, &zwlr_foreign_toplevel_manager_v1_interface, version)); if (manager_) zwlr_foreign_toplevel_manager_v1_add_listener(manager_, &toplevel_manager_impl, this); else spdlog::debug("Failed to register manager"); } void Taskbar::register_seat(struct wl_registry *registry, uint32_t name, uint32_t version) { if (seat_) { spdlog::warn("Register seat again although already existing!"); return; } version = std::min(version, wl_seat_interface.version); seat_ = static_cast(wl_registry_bind(registry, name, &wl_seat_interface, version)); } void Taskbar::handle_toplevel_create(struct zwlr_foreign_toplevel_handle_v1 *tl_handle) { tasks_.push_back(std::make_unique(bar_, config_, this, tl_handle, seat_)); } void Taskbar::handle_finished() { zwlr_foreign_toplevel_manager_v1_destroy(manager_); manager_ = nullptr; } void Taskbar::add_button(Gtk::Button &bt) { box_.pack_start(bt, false, false); } void Taskbar::move_button(Gtk::Button &bt, int pos) { box_.reorder_child(bt, pos); } void Taskbar::remove_button(Gtk::Button &bt) { box_.remove(bt); } void Taskbar::remove_task(uint32_t id) { auto it = std::find_if(std::begin(tasks_), std::end(tasks_), [id](const TaskPtr &p) { return p->id() == id; }); if (it == std::end(tasks_)) { spdlog::warn("Can't find task with id {}", id); return; } tasks_.erase(it); } bool Taskbar::show_output(struct wl_output *output) const { return output == gdk_wayland_monitor_get_wl_output(bar_.output->monitor->gobj()); } bool Taskbar::all_outputs() const { return config_["all-outputs"].isBool() && config_["all-outputs"].asBool(); } std::vector> Taskbar::icon_themes() const { return icon_themes_; } } /* namespace waybar::modules::wlr */