diff --git a/include/modules/hyprland/windowcreationpayload.hpp b/include/modules/hyprland/windowcreationpayload.hpp index 45229ed42..3bd30c8e6 100644 --- a/include/modules/hyprland/windowcreationpayload.hpp +++ b/include/modules/hyprland/windowcreationpayload.hpp @@ -26,10 +26,20 @@ namespace waybar::modules::hyprland { class Workspaces; +struct WindowRepr { + std::string address; + std::string window_class; + std::string window_title; + std::string repr_rewrite; + + public: + bool empty() const { return address.empty(); } +}; + class WindowCreationPayload { public: WindowCreationPayload(std::string workspace_name, WindowAddress window_address, - std::string window_repr); + WindowRepr window_repr); WindowCreationPayload(std::string workspace_name, WindowAddress window_address, std::string window_class, std::string window_title); WindowCreationPayload(Json::Value const& client_data); @@ -37,7 +47,7 @@ class WindowCreationPayload { int incrementTimeSpentUncreated(); bool isEmpty(Workspaces& workspace_manager); bool reprIsReady() const { return std::holds_alternative(m_window); } - std::string repr(Workspaces& workspace_manager); + WindowRepr repr(Workspaces& workspace_manager); std::string getWorkspaceName() const { return m_workspaceName; } WindowAddress getAddress() const { return m_windowAddress; } @@ -48,7 +58,7 @@ class WindowCreationPayload { void clearAddr(); void clearWorkspaceName(); - using Repr = std::string; + using Repr = WindowRepr; using ClassAndTitle = std::pair; std::variant m_window; diff --git a/include/modules/hyprland/workspace.hpp b/include/modules/hyprland/workspace.hpp index 3dedba4c5..4280cc390 100644 --- a/include/modules/hyprland/workspace.hpp +++ b/include/modules/hyprland/workspace.hpp @@ -54,15 +54,17 @@ class Workspace { void setWindows(uint value) { m_windows = value; }; void setName(std::string const& value) { m_name = value; }; void setOutput(std::string const& value) { m_output = value; }; - bool containsWindow(WindowAddress const& addr) const { return m_windowMap.contains(addr); } + bool containsWindow(WindowAddress const& addr) const { + return std::ranges::any_of(m_windowMap, + [&addr](const auto& window) { return window.address == addr; }); + }; void insertWindow(WindowCreationPayload create_window_paylod); - std::string removeWindow(WindowAddress const& addr); void initializeWindowMap(const Json::Value& clients_data); bool onWindowOpened(WindowCreationPayload const& create_window_paylod); - std::optional closeWindow(WindowAddress const& addr); + std::optional closeWindow(WindowAddress const& addr); - void update(const std::string& format, const std::string& icon); + void update(const std::string& workspace_icon); private: Workspaces& m_workspaceManager; @@ -78,11 +80,15 @@ class Workspace { bool m_isUrgent = false; bool m_isVisible = false; - std::map m_windowMap; + std::vector m_windowMap; Gtk::Button m_button; Gtk::Box m_content; - Gtk::Label m_label; + Gtk::Label m_labelBefore; + Gtk::Label m_labelAfter; + + void updateTaskbar(const std::string& workspace_icon); + bool handleClick(const GdkEventButton* event_button, WindowAddress const& addr) const; IPC& m_ipc; }; diff --git a/include/modules/hyprland/workspaces.hpp b/include/modules/hyprland/workspaces.hpp index 3f0252c87..4a6aa04a5 100644 --- a/include/modules/hyprland/workspaces.hpp +++ b/include/modules/hyprland/workspaces.hpp @@ -1,6 +1,7 @@ #pragma once #include +#include #include #include @@ -17,6 +18,7 @@ #include "modules/hyprland/windowcreationpayload.hpp" #include "modules/hyprland/workspace.hpp" #include "util/enum.hpp" +#include "util/icon_loader.hpp" #include "util/regex_collection.hpp" using WindowAddress = std::string; @@ -37,14 +39,24 @@ class Workspaces : public AModule, public EventHandler { auto activeOnly() const -> bool { return m_activeOnly; } auto specialVisibleOnly() const -> bool { return m_specialVisibleOnly; } auto moveToMonitor() const -> bool { return m_moveToMonitor; } + auto enableTaskbar() const -> bool { return m_enableTaskbar; } + auto taskbarWithIcon() const -> bool { return m_taskbarWithIcon; } auto getBarOutput() const -> std::string { return m_bar.output->name; } + auto formatBefore() const -> std::string { return m_formatBefore; } + auto formatAfter() const -> std::string { return m_formatAfter; } + auto taskbarFormatBefore() const -> std::string { return m_taskbarFormatBefore; } + auto taskbarFormatAfter() const -> std::string { return m_taskbarFormatAfter; } + auto taskbarIconSize() const -> int { return m_taskbarIconSize; } + auto taskbarOrientation() const -> Gtk::Orientation { return m_taskbarOrientation; } + auto onClickWindow() const -> std::string { return m_onClickWindow; } std::string getRewrite(std::string window_class, std::string window_title); std::string& getWindowSeparator() { return m_formatWindowSeparator; } bool isWorkspaceIgnored(std::string const& workspace_name); bool windowRewriteConfigUsesTitle() const { return m_anyWindowRewriteRuleUsesTitle; } + const IconLoader& iconLoader() const { return m_iconLoader; } private: void onEvent(const std::string& e) override; @@ -67,6 +79,7 @@ class Workspaces : public AModule, public EventHandler { auto populateIgnoreWorkspacesConfig(const Json::Value& config) -> void; auto populateFormatWindowSeparatorConfig(const Json::Value& config) -> void; auto populateWindowRewriteConfig(const Json::Value& config) -> void; + auto populateWorkspaceTaskbarConfig(const Json::Value& config) -> void; void registerIpc(); @@ -119,7 +132,7 @@ class Workspaces : public AModule, public EventHandler { // Map for windows stored in workspaces not present in the current bar. // This happens when the user has multiple monitors (hence, multiple bars) // and doesn't share windows accross bars (a.k.a `all-outputs` = false) - std::map m_orphanWindowMap; + std::map> m_orphanWindowMap; enum class SortMethod { ID, NAME, NUMBER, DEFAULT }; util::EnumParser m_enumParser; @@ -129,7 +142,8 @@ class Workspaces : public AModule, public EventHandler { {"NUMBER", SortMethod::NUMBER}, {"DEFAULT", SortMethod::DEFAULT}}; - std::string m_format; + std::string m_formatBefore; + std::string m_formatAfter; std::map m_iconsMap; util::RegexCollection m_windowRewriteRules; @@ -145,6 +159,16 @@ class Workspaces : public AModule, public EventHandler { std::vector m_workspacesToRemove; std::vector m_windowsToCreate; + IconLoader m_iconLoader; + bool m_enableTaskbar = false; + bool m_taskbarWithIcon = false; + bool m_taskbarWithTitle = false; + std::string m_taskbarFormatBefore; + std::string m_taskbarFormatAfter; + int m_taskbarIconSize = 16; + Gtk::Orientation m_taskbarOrientation = Gtk::ORIENTATION_HORIZONTAL; + std::string m_onClickWindow; + std::vector m_ignoreWorkspaces; std::mutex m_mutex; diff --git a/include/modules/wlr/taskbar.hpp b/include/modules/wlr/taskbar.hpp index 07110ddee..8dc4dadd3 100644 --- a/include/modules/wlr/taskbar.hpp +++ b/include/modules/wlr/taskbar.hpp @@ -19,6 +19,7 @@ #include "bar.hpp" #include "client.hpp" #include "giomm/desktopappinfo.h" +#include "util/icon_loader.hpp" #include "util/json.hpp" #include "wlr-foreign-toplevel-management-unstable-v1-client-protocol.h" @@ -89,9 +90,6 @@ class Task { std::string state_string(bool = false) const; void set_minimize_hint(); void on_button_size_allocated(Gtk::Allocation &alloc); - void set_app_info_from_app_id_list(const std::string &app_id_list); - bool image_load_icon(Gtk::Image &image, const Glib::RefPtr &icon_theme, - Glib::RefPtr app_info, int size); void hide_if_ignored(); public: @@ -153,7 +151,7 @@ class Taskbar : public waybar::AModule { Gtk::Box box_; std::vector tasks_; - std::vector> icon_themes_; + IconLoader icon_loader_; std::unordered_set ignore_list_; std::map app_ids_replace_map_; @@ -178,7 +176,7 @@ class Taskbar : public waybar::AModule { bool show_output(struct wl_output *) const; bool all_outputs() const; - const std::vector> &icon_themes() const; + const IconLoader &icon_loader() const; const std::unordered_set &ignore_list() const; const std::map &app_ids_replace_map() const; }; diff --git a/include/util/icon_loader.hpp b/include/util/icon_loader.hpp new file mode 100644 index 000000000..664e510c2 --- /dev/null +++ b/include/util/icon_loader.hpp @@ -0,0 +1,34 @@ +#pragma once + +#include +#include +#include +#include +#include +#include + +#include +#include + +#include "util/gtk_icon.hpp" + +class IconLoader { + private: + std::vector> custom_icon_themes_; + Glib::RefPtr default_icon_theme_ = Gtk::IconTheme::get_default(); + static std::vector search_prefix(); + static Glib::RefPtr get_app_info_by_name(const std::string &app_id); + static Glib::RefPtr get_desktop_app_info(const std::string &app_id); + static Glib::RefPtr load_icon_from_file(std::string const &icon_path, int size); + static std::string get_icon_name_from_icon_theme(const Glib::RefPtr &icon_theme, + const std::string &app_id); + static bool image_load_icon(Gtk::Image &image, const Glib::RefPtr &icon_theme, + Glib::RefPtr app_info, int size); + + public: + void add_custom_icon_theme(const std::string &theme_name); + bool image_load_icon(Gtk::Image &image, Glib::RefPtr app_info, + int size) const; + static Glib::RefPtr get_app_info_from_app_id_list( + const std::string &app_id_list); +}; diff --git a/include/util/string.hpp b/include/util/string.hpp index d06557c1d..cb8a6892e 100644 --- a/include/util/string.hpp +++ b/include/util/string.hpp @@ -23,3 +23,26 @@ inline std::string capitalize(const std::string& str) { [](unsigned char c) { return std::toupper(c); }); return result; } + +inline std::string toLower(const std::string& str) { + std::string result = str; + std::transform(result.begin(), result.end(), result.begin(), + [](unsigned char c) { return std::tolower(c); }); + return result; +} + +inline std::vector split(std::string_view s, std::string_view delimiter, + int max_splits = -1) { + std::vector result; + size_t pos = 0; + size_t next_pos = 0; + while ((next_pos = s.find(delimiter, pos)) != std::string::npos) { + result.push_back(std::string(s.substr(pos, next_pos - pos))); + pos = next_pos + delimiter.size(); + if (max_splits > 0 && result.size() == static_cast(max_splits)) { + break; + } + } + result.push_back(std::string(s.substr(pos))); + return result; +} diff --git a/man/waybar-hyprland-workspaces.5.scd b/man/waybar-hyprland-workspaces.5.scd index 18c39898c..01374a2c1 100644 --- a/man/waybar-hyprland-workspaces.5.scd +++ b/man/waybar-hyprland-workspaces.5.scd @@ -26,17 +26,49 @@ Addressed by *hyprland/workspaces* Regex rules to map window class to an icon or preferred method of representation for a workspace's window. Keys are the rules, while the values are the methods of representation. Values may use the placeholders {class} and {title} to use the window's original class and/or title respectively. Rules may specify `class<...>`, `title<...>`, or both in order to fine-tune the matching. - You may assign an empty value to a rule to have it ignored from generating any representation in workspaces. + You may assign an empty value to a rule to have it ignored from generating any representation in workspaces. ++ +This setting is ignored if *workspace-taskbar.enable* is set to true. -*window-rewrite-default*: +*window-rewrite-default*: ++ typeof: string ++ default: "?" ++ - The default method of representation for a workspace's window. This will be used for windows whose classes do not match any of the rules in *window-rewrite*. + The default method of representation for a workspace's window. This will be used for windows whose classes do not match any of the rules in *window-rewrite*. ++ + This setting is ignored if *workspace-taskbar.enable* is set to true. *format-window-separator*: ++ typeof: string ++ default: " " ++ - The separator to be used between windows in a workspace. + The separator to be used between windows in a workspace. ++ + This setting is ignored if *workspace-taskbar.enable* is set to true. + +*workspace-taskbar*: ++ + typeof: object ++ + Contains settings for the workspace taskbar, an alternative mode for the workspaces module which displays the window icons as images instead of text. + + *enable*: ++ + typeof: bool ++ + default: false ++ + Enables the workspace taskbar mode. + + *format*: ++ + typeof: string ++ + default: {icon} ++ + Format to use for each window in the workspace taskbar. Available placeholders are {icon} and {title}. + + *icon-size*: ++ + typeof: int ++ + default: 16 ++ + Size of the icons in the workspace taskbar. + + *icon-theme*: ++ + typeof: string | array ++ + default: [] ++ + Icon theme to use for the workspace taskbar. If an array is provided, the first theme that is found for a given icon will be used. If no theme is found (or the array is empty), the default icon theme is used. + + *orientation*: ++ + typeof: "horizontal" | "vertical" ++ + default: horizontal ++ + Direction in which the workspace taskbar is displayed. *show-special*: ++ typeof: bool ++ @@ -178,3 +210,4 @@ Additional to workspace name matching, the following *format-icons* can be set. - *#workspaces button.special* - *#workspaces button.urgent* - *#workspaces button.hosting-monitor* (gets applied if workspace-monitor == waybar-monitor) +- *#workspaces .taskbar-window* (each window in the taskbar) diff --git a/meson.build b/meson.build index 5524b49cc..4b3f35e73 100644 --- a/meson.build +++ b/meson.build @@ -182,6 +182,7 @@ src_files = files( 'src/util/sanitize_str.cpp', 'src/util/rewrite_string.cpp', 'src/util/gtk_icon.cpp', + 'src/util/icon_loader.cpp', 'src/util/regex_collection.cpp', 'src/util/css_reload_helper.cpp' ) diff --git a/src/modules/hyprland/windowcreationpayload.cpp b/src/modules/hyprland/windowcreationpayload.cpp index df7fe784e..5e587d510 100644 --- a/src/modules/hyprland/windowcreationpayload.cpp +++ b/src/modules/hyprland/windowcreationpayload.cpp @@ -20,7 +20,7 @@ WindowCreationPayload::WindowCreationPayload(Json::Value const &client_data) } WindowCreationPayload::WindowCreationPayload(std::string workspace_name, - WindowAddress window_address, std::string window_repr) + WindowAddress window_address, WindowRepr window_repr) : m_window(std::move(window_repr)), m_windowAddress(std::move(window_address)), m_workspaceName(std::move(workspace_name)) { @@ -92,13 +92,14 @@ void WindowCreationPayload::moveToWorksace(std::string &new_workspace_name) { m_workspaceName = new_workspace_name; } -std::string WindowCreationPayload::repr(Workspaces &workspace_manager) { +WindowRepr WindowCreationPayload::repr(Workspaces &workspace_manager) { if (std::holds_alternative(m_window)) { return std::get(m_window); } if (std::holds_alternative(m_window)) { auto [window_class, window_title] = std::get(m_window); - return workspace_manager.getRewrite(window_class, window_title); + return {m_windowAddress, window_class, window_title, + workspace_manager.getRewrite(window_class, window_title)}; } // Unreachable spdlog::error("WorkspaceWindow::repr: Unreachable"); diff --git a/src/modules/hyprland/workspace.cpp b/src/modules/hyprland/workspace.cpp index 4655096f4..70953247f 100644 --- a/src/modules/hyprland/workspace.cpp +++ b/src/modules/hyprland/workspace.cpp @@ -6,6 +6,8 @@ #include #include "modules/hyprland/workspaces.hpp" +#include "util/command.hpp" +#include "util/icon_loader.hpp" namespace waybar::modules::hyprland { @@ -32,7 +34,12 @@ Workspace::Workspace(const Json::Value &workspace_data, Workspaces &workspace_ma false); m_button.set_relief(Gtk::RELIEF_NONE); - m_content.set_center_widget(m_label); + if (m_workspaceManager.enableTaskbar()) { + m_content.set_orientation(m_workspaceManager.taskbarOrientation()); + m_content.pack_start(m_labelBefore, false, false); + } else { + m_content.set_center_widget(m_labelBefore); + } m_button.add(m_content); initializeWindowMap(clients_data); @@ -47,9 +54,14 @@ void addOrRemoveClass(const Glib::RefPtr &context, bool condi } } -std::optional Workspace::closeWindow(WindowAddress const &addr) { - if (m_windowMap.contains(addr)) { - return removeWindow(addr); +std::optional Workspace::closeWindow(WindowAddress const &addr) { + auto it = std::ranges::find_if(m_windowMap, + [&addr](const auto &window) { return window.address == addr; }); + // If the vector contains the address, remove it and return the window representation + if (it != m_windowMap.end()) { + WindowRepr windowRepr = *it; + m_windowMap.erase(it); + return windowRepr; } return std::nullopt; } @@ -95,8 +107,16 @@ void Workspace::insertWindow(WindowCreationPayload create_window_paylod) { if (!create_window_paylod.isEmpty(m_workspaceManager)) { auto repr = create_window_paylod.repr(m_workspaceManager); - if (!repr.empty()) { - m_windowMap[create_window_paylod.getAddress()] = repr; + if (!repr.empty() || m_workspaceManager.enableTaskbar()) { + auto addr = create_window_paylod.getAddress(); + auto it = std::ranges::find_if( + m_windowMap, [&addr](const auto &window) { return window.address == addr; }); + // If the vector contains the address, update the window representation, otherwise insert it + if (it != m_windowMap.end()) { + *it = repr; + } else { + m_windowMap.emplace_back(repr); + } } } }; @@ -109,12 +129,6 @@ bool Workspace::onWindowOpened(WindowCreationPayload const &create_window_paylod return false; } -std::string Workspace::removeWindow(WindowAddress const &addr) { - std::string windowRepr = m_windowMap[addr]; - m_windowMap.erase(addr); - return windowRepr; -} - std::string &Workspace::selectIcon(std::map &icons_map) { spdlog::trace("Selecting icon for workspace {}", name()); if (isUrgent()) { @@ -172,7 +186,7 @@ std::string &Workspace::selectIcon(std::map &icons_map return m_name; } -void Workspace::update(const std::string &format, const std::string &icon) { +void Workspace::update(const std::string &workspace_icon) { // clang-format off if (this->m_workspaceManager.activeOnly() && \ !this->isActive() && \ @@ -200,21 +214,98 @@ void Workspace::update(const std::string &format, const std::string &icon) { addOrRemoveClass(styleContext, m_workspaceManager.getBarOutput() == output(), "hosting-monitor"); std::string windows; - auto windowSeparator = m_workspaceManager.getWindowSeparator(); + // Optimization: The {windows} substitution string is only possible if the taskbar is disabled, no + // need to compute this if enableTaskbar() is true + if (!m_workspaceManager.enableTaskbar()) { + auto windowSeparator = m_workspaceManager.getWindowSeparator(); + + bool isNotFirst = false; + + for (const auto &window_repr : m_windowMap) { + if (isNotFirst) { + windows.append(windowSeparator); + } + isNotFirst = true; + windows.append(window_repr.repr_rewrite); + } + } + + auto formatBefore = m_workspaceManager.formatBefore(); + m_labelBefore.set_markup(fmt::format(fmt::runtime(formatBefore), fmt::arg("id", id()), + fmt::arg("name", name()), fmt::arg("icon", workspace_icon), + fmt::arg("windows", windows))); + + if (m_workspaceManager.enableTaskbar()) { + updateTaskbar(workspace_icon); + } +} - bool isNotFirst = false; +void Workspace::updateTaskbar(const std::string &workspace_icon) { + for (auto child : m_content.get_children()) { + if (child != &m_labelBefore) { + m_content.remove(*child); + } + } + + for (const auto &window_repr : m_windowMap) { + auto window_box = Gtk::make_managed(Gtk::ORIENTATION_HORIZONTAL); + window_box->set_tooltip_text(window_repr.window_title); + window_box->get_style_context()->add_class("taskbar-window"); + auto event_box = Gtk::manage(new Gtk::EventBox()); + event_box->add(*window_box); + if (m_workspaceManager.onClickWindow() != "") { + event_box->signal_button_press_event().connect( + sigc::bind(sigc::mem_fun(*this, &Workspace::handleClick), window_repr.address)); + } + + auto text_before = fmt::format(fmt::runtime(m_workspaceManager.taskbarFormatBefore()), + fmt::arg("title", window_repr.window_title)); + if (!text_before.empty()) { + auto window_label_before = Gtk::make_managed(text_before); + window_box->pack_start(*window_label_before, true, true); + } + + if (m_workspaceManager.taskbarWithIcon()) { + auto app_info_ = IconLoader::get_app_info_from_app_id_list(window_repr.window_class); + int icon_size = m_workspaceManager.taskbarIconSize(); + auto window_icon = Gtk::make_managed(); + m_workspaceManager.iconLoader().image_load_icon(*window_icon, app_info_, icon_size); + window_box->pack_start(*window_icon, false, false); + } - for (auto &[_pid, window_repr] : m_windowMap) { - if (isNotFirst) { - windows.append(windowSeparator); + auto text_after = fmt::format(fmt::runtime(m_workspaceManager.taskbarFormatAfter()), + fmt::arg("title", window_repr.window_title)); + if (!text_after.empty()) { + auto window_label_after = Gtk::make_managed(text_after); + window_box->pack_start(*window_label_after, true, true); } - isNotFirst = true; - windows.append(window_repr); + + m_content.pack_start(*event_box, true, false); + event_box->show_all(); } - m_label.set_markup(fmt::format(fmt::runtime(format), fmt::arg("id", id()), - fmt::arg("name", name()), fmt::arg("icon", icon), - fmt::arg("windows", windows))); + auto formatAfter = m_workspaceManager.formatAfter(); + if (!formatAfter.empty()) { + m_labelAfter.set_markup(fmt::format(fmt::runtime(formatAfter), fmt::arg("id", id()), + fmt::arg("name", name()), + fmt::arg("icon", workspace_icon))); + m_content.pack_end(m_labelAfter, false, false); + m_labelAfter.show(); + } +} + +bool Workspace::handleClick(const GdkEventButton *event_button, WindowAddress const &addr) const { + if (event_button->type == GDK_BUTTON_PRESS) { + std::string command = std::regex_replace(m_workspaceManager.onClickWindow(), + std::regex("\\{address\\}"), "0x" + addr); + command = std::regex_replace(command, std::regex("\\{button\\}"), + std::to_string(event_button->button)); + auto res = util::command::execNoRead(command); + if (res.exit_code != 0) { + spdlog::error("Failed to execute {}: {}", command, res.out); + } + } + return true; } } // namespace waybar::modules::hyprland diff --git a/src/modules/hyprland/workspaces.cpp b/src/modules/hyprland/workspaces.cpp index ef057d6d4..c77aafb1e 100644 --- a/src/modules/hyprland/workspaces.cpp +++ b/src/modules/hyprland/workspaces.cpp @@ -9,6 +9,7 @@ #include #include "util/regex_collection.hpp" +#include "util/string.hpp" namespace waybar::modules::hyprland { @@ -468,6 +469,7 @@ void Workspaces::onWindowOpened(std::string const &payload) { void Workspaces::onWindowClosed(std::string const &addr) { spdlog::trace("Window closed: {}", addr); updateWindowCount(); + m_orphanWindowMap.erase(addr); for (auto &workspace : m_workspaces) { if (workspace->closeWindow(addr)) { break; @@ -484,7 +486,7 @@ void Workspaces::onWindowMoved(std::string const &payload) { std::string workspaceName = payload.substr(nextCommaIdx + 1, payload.length() - nextCommaIdx); - std::string windowRepr; + WindowRepr windowRepr; // If the window was still queued to be created, just change its destination // and exit @@ -510,6 +512,7 @@ void Workspaces::onWindowMoved(std::string const &payload) { // ...and then add it to the new workspace if (!windowRepr.empty()) { + m_orphanWindowMap.erase(windowAddress); m_windowsToCreate.emplace_back(workspaceName, windowAddress, windowRepr); } } @@ -548,12 +551,11 @@ void Workspaces::onWindowTitleEvent(std::string const &payload) { Json::Value clientsData = m_ipc.getSocket1JsonReply("clients"); std::string jsonWindowAddress = fmt::format("0x{}", payload); - auto client = - std::find_if(clientsData.begin(), clientsData.end(), [jsonWindowAddress](auto &client) { - return client["address"].asString() == jsonWindowAddress; - }); + auto client = std::ranges::find_if(clientsData, [&jsonWindowAddress](auto &c) { + return c["address"].asString() == jsonWindowAddress; + }); - if (!client->empty()) { + if (client != clientsData.end() && !client->empty()) { (*inserter)({*client}); } } @@ -566,8 +568,9 @@ void Workspaces::onConfigReloaded() { auto Workspaces::parseConfig(const Json::Value &config) -> void { const auto &configFormat = config["format"]; - m_format = configFormat.isString() ? configFormat.asString() : "{name}"; - m_withIcon = m_format.find("{icon}") != std::string::npos; + m_formatBefore = configFormat.isString() ? configFormat.asString() : "{name}"; + m_withIcon = m_formatBefore.find("{icon}") != std::string::npos; + auto withWindows = m_formatBefore.find("{windows}") != std::string::npos; if (m_withIcon && m_iconsMap.empty()) { populateIconsMap(config["format-icons"]); @@ -584,6 +587,15 @@ auto Workspaces::parseConfig(const Json::Value &config) -> void { populateIgnoreWorkspacesConfig(config); populateFormatWindowSeparatorConfig(config); populateWindowRewriteConfig(config); + + if (withWindows) { + populateWorkspaceTaskbarConfig(config); + } + if (m_enableTaskbar) { + auto parts = split(m_formatBefore, "{windows}", 1); + m_formatBefore = parts[0]; + m_formatAfter = parts.size() > 1 ? parts[1] : ""; + } } auto Workspaces::populateIconsMap(const Json::Value &formatIcons) -> void { @@ -656,6 +668,52 @@ auto Workspaces::populateWindowRewriteConfig(const Json::Value &config) -> void [this](std::string &window_rule) { return windowRewritePriorityFunction(window_rule); }); } +auto Workspaces::populateWorkspaceTaskbarConfig(const Json::Value &config) -> void { + const auto &workspaceTaskbar = config["workspace-taskbar"]; + if (!workspaceTaskbar.isObject()) { + spdlog::debug("workspace-taskbar is not defined or is not an object, using default rules."); + return; + } + + populateBoolConfig(workspaceTaskbar, "enable", m_enableTaskbar); + + if (workspaceTaskbar["format"].isString()) { + /* The user defined a format string, use it */ + std::string format = workspaceTaskbar["format"].asString(); + m_taskbarWithTitle = format.find("{title") != std::string::npos; /* {title} or {title.length} */ + auto parts = split(format, "{icon}", 1); + m_taskbarFormatBefore = parts[0]; + if (parts.size() > 1) { + m_taskbarWithIcon = true; + m_taskbarFormatAfter = parts[1]; + } + } else { + /* The default is to only show the icon */ + m_taskbarWithIcon = true; + } + + auto iconTheme = workspaceTaskbar["icon-theme"]; + if (iconTheme.isArray()) { + for (auto &c : iconTheme) { + m_iconLoader.add_custom_icon_theme(c.asString()); + } + } else if (iconTheme.isString()) { + m_iconLoader.add_custom_icon_theme(iconTheme.asString()); + } + + if (workspaceTaskbar["icon-size"].isInt()) { + m_taskbarIconSize = workspaceTaskbar["icon-size"].asInt(); + } + if (workspaceTaskbar["orientation"].isString() && + toLower(workspaceTaskbar["orientation"].asString()) == "vertical") { + m_taskbarOrientation = Gtk::ORIENTATION_VERTICAL; + } + + if (workspaceTaskbar["on-click-window"].isString()) { + m_onClickWindow = workspaceTaskbar["on-click-window"].asString(); + } +} + void Workspaces::registerOrphanWindow(WindowCreationPayload create_window_payload) { if (!create_window_payload.isEmpty(*this)) { m_orphanWindowMap[create_window_payload.getAddress()] = create_window_payload.repr(*this); @@ -676,7 +734,7 @@ auto Workspaces::registerIpc() -> void { m_ipc.registerForIPC("urgent", this); m_ipc.registerForIPC("configreloaded", this); - if (windowRewriteConfigUsesTitle()) { + if (windowRewriteConfigUsesTitle() || m_taskbarWithTitle) { spdlog::info( "Registering for Hyprland's 'windowtitle' events because a user-defined window " "rewrite rule uses the 'title' field."); @@ -884,7 +942,7 @@ void Workspaces::updateWorkspaceStates() { if (updatedWorkspace != updatedWorkspaces.end()) { workspace->setOutput((*updatedWorkspace)["monitor"].asString()); } - workspace->update(m_format, workspaceIcon); + workspace->update(workspaceIcon); } } diff --git a/src/modules/wlr/taskbar.cpp b/src/modules/wlr/taskbar.cpp index 30e4ee488..a2ddafdbd 100644 --- a/src/modules/wlr/taskbar.cpp +++ b/src/modules/wlr/taskbar.cpp @@ -26,194 +26,6 @@ namespace waybar::modules::wlr { -/* Icon loading functions */ -static std::vector search_prefix() { - std::vector prefixes = {""}; - - std::string home_dir = std::getenv("HOME"); - prefixes.push_back(home_dir + "/.local/share/"); - - 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; -} - -static Glib::RefPtr load_icon_from_file(std::string icon_path, int size) { - try { - auto pb = Gdk::Pixbuf::create_from_file(icon_path, size, size); - return pb; - } catch (...) { - return {}; - } -} - -static Glib::RefPtr get_app_info_by_name(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"}; - - for (auto &prefix : prefixes) { - for (auto &folder : app_folders) { - for (auto &suffix : suffixes) { - auto app_info_ = - Gio::DesktopAppInfo::create_from_filename(prefix + folder + app_id + suffix); - if (!app_info_) { - continue; - } - - return app_info_; - } - } - } - - return {}; -} - -Glib::RefPtr get_desktop_app_info(const std::string &app_id) { - auto app_info = get_app_info_by_name(app_id); - if (app_info) { - return app_info; - } - - std::string desktop_file = ""; - - gchar ***desktop_list = g_desktop_app_info_search(app_id.c_str()); - if (desktop_list != nullptr && desktop_list[0] != nullptr) { - for (size_t i = 0; desktop_list[0][i]; i++) { - if (desktop_file == "") { - desktop_file = desktop_list[0][i]; - } else { - auto tmp_info = Gio::DesktopAppInfo::create(desktop_list[0][i]); - if (!tmp_info) - // see https://github.com/Alexays/Waybar/issues/1446 - continue; - - auto startup_class = tmp_info->get_startup_wm_class(); - if (startup_class == app_id) { - desktop_file = desktop_list[0][i]; - break; - } - } - } - g_strfreev(desktop_list[0]); - } - g_free(desktop_list); - - return get_app_info_by_name(desktop_file); -} - -void Task::set_app_info_from_app_id_list(const std::string &app_id_list) { - std::string app_id; - std::istringstream stream(app_id_list); - - /* 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) { - app_info_ = get_desktop_app_info(app_id); - if (app_info_) { - return; - } - - 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); }); - app_info_ = get_desktop_app_info(lower_app_id); - if (app_info_) { - return; - } - - size_t start = 0, end = app_id.size(); - start = app_id.rfind(".", end); - std::string app_name = app_id.substr(start + 1, app_id.size()); - app_info_ = get_desktop_app_info(app_name); - if (app_info_) { - return; - } - - start = app_id.find("-"); - app_name = app_id.substr(0, start); - app_info_ = get_desktop_app_info(app_name); - } -} - -static std::string get_icon_name_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 ""; -} - -bool Task::image_load_icon(Gtk::Image &image, const Glib::RefPtr &icon_theme, - Glib::RefPtr app_info, int size) { - std::string ret_icon_name = "unknown"; - if (app_info) { - std::string icon_name = - get_icon_name_from_icon_theme(icon_theme, app_info->get_startup_wm_class()); - if (!icon_name.empty()) { - ret_icon_name = icon_name; - } else { - if (app_info->get_icon()) { - ret_icon_name = app_info->get_icon()->to_string(); - } - } - } - - Glib::RefPtr pixbuf; - auto scaled_icon_size = size * image.get_scale_factor(); - - try { - pixbuf = icon_theme->load_icon(ret_icon_name, scaled_icon_size, Gtk::ICON_LOOKUP_FORCE_SIZE); - spdlog::debug("{} Loaded icon '{}'", repr(), ret_icon_name); - } catch (...) { - if (Glib::file_test(ret_icon_name, Glib::FILE_TEST_EXISTS)) { - pixbuf = load_icon_from_file(ret_icon_name, scaled_icon_size); - spdlog::debug("{} Loaded icon from file '{}'", repr(), ret_icon_name); - } else { - try { - pixbuf = DefaultGtkIconThemeWrapper::load_icon( - "image-missing", scaled_icon_size, Gtk::IconLookupFlags::ICON_LOOKUP_FORCE_SIZE); - spdlog::debug("{} Loaded icon from resource", repr()); - } catch (...) { - pixbuf = {}; - spdlog::debug("{} Unable to load icon.", repr()); - } - } - } - - if (pixbuf) { - if (pixbuf->get_width() != scaled_icon_size) { - int width = scaled_icon_size * pixbuf->get_width() / pixbuf->get_height(); - pixbuf = pixbuf->scale_simple(width, scaled_icon_size, Gdk::InterpType::INTERP_BILINEAR); - } - auto surface = Gdk::Cairo::create_surface_from_pixbuf(pixbuf, image.get_scale_factor(), - image.get_window()); - image.set(surface); - return true; - } - - return false; -} - /* Task class implementation */ uint32_t Task::global_id = 0; @@ -299,16 +111,11 @@ Task::Task(const waybar::Bar &bar, const Json::Value &config, Taskbar *tbar, with_name_ = true; } - auto icon_pos = format.find("{icon}"); - if (icon_pos == 0) { + auto parts = split(format, "{icon}", 1); + format_before_ = parts[0]; + if (parts.size() > 1) { 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); + format_after_ = parts[1]; } } else { /* The default is to only show the icon */ @@ -430,7 +237,7 @@ void Task::handle_app_id(const char *app_id) { return; } - set_app_info_from_app_id_list(app_id_); + app_info_ = IconLoader::get_app_info_from_app_id_list(app_id_); name_ = app_info_ ? app_info_->get_display_name() : app_id; if (!with_icon_) { @@ -438,15 +245,7 @@ void Task::handle_app_id(const char *app_id) { } 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_info_, icon_size)) { - found = true; - break; - } - } - - if (found) + if (tbar_->icon_loader().image_load_icon(icon_, app_info_, icon_size)) icon_.show(); else spdlog::debug("Couldn't find icon for {}", app_id_); @@ -769,22 +568,10 @@ Taskbar::Taskbar(const std::string &id, const waybar::Bar &bar, const Json::Valu /* 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); + icon_loader_.add_custom_icon_theme(c.asString()); } } 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_loader_.add_custom_icon_theme(config_["icon-theme"].asString()); } // Load ignore-list @@ -803,8 +590,6 @@ Taskbar::Taskbar(const std::string &id, const waybar::Bar &bar, const Json::Valu } } - icon_themes_.push_back(Gtk::IconTheme::get_default()); - for (auto &t : tasks_) { t->handle_app_id(t->app_id().c_str()); } @@ -939,9 +724,7 @@ bool Taskbar::all_outputs() const { return config_["all-outputs"].isBool() && config_["all-outputs"].asBool(); } -const std::vector> &Taskbar::icon_themes() const { - return icon_themes_; -} +const IconLoader &Taskbar::icon_loader() const { return icon_loader_; } const std::unordered_set &Taskbar::ignore_list() const { return ignore_list_; } diff --git a/src/util/icon_loader.cpp b/src/util/icon_loader.cpp new file mode 100644 index 000000000..69e25dbce --- /dev/null +++ b/src/util/icon_loader.cpp @@ -0,0 +1,207 @@ +#include "util/icon_loader.hpp" + +#include "util/string.hpp" + +std::vector IconLoader::search_prefix() { + std::vector prefixes = {""}; + + std::string home_dir = std::getenv("HOME"); + prefixes.push_back(home_dir + "/.local/share/"); + + 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; + size_t 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; +} + +Glib::RefPtr IconLoader::get_app_info_by_name(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"}; + + for (auto const &prefix : prefixes) { + for (auto const &folder : app_folders) { + for (auto const &suffix : suffixes) { + auto app_info_ = + Gio::DesktopAppInfo::create_from_filename(prefix + folder + app_id + suffix); + if (!app_info_) { + continue; + } + + return app_info_; + } + } + } + + return {}; +} + +Glib::RefPtr IconLoader::get_desktop_app_info(const std::string &app_id) { + auto app_info = get_app_info_by_name(app_id); + if (app_info) { + return app_info; + } + + std::string desktop_file = ""; + + gchar ***desktop_list = g_desktop_app_info_search(app_id.c_str()); + if (desktop_list != nullptr && desktop_list[0] != nullptr) { + for (size_t i = 0; desktop_list[0][i]; i++) { + if (desktop_file == "") { + desktop_file = desktop_list[0][i]; + } else { + auto tmp_info = Gio::DesktopAppInfo::create(desktop_list[0][i]); + if (!tmp_info) + // see https://github.com/Alexays/Waybar/issues/1446 + continue; + + auto startup_class = tmp_info->get_startup_wm_class(); + if (startup_class == app_id) { + desktop_file = desktop_list[0][i]; + break; + } + } + } + g_strfreev(desktop_list[0]); + } + g_free(desktop_list); + + return get_app_info_by_name(desktop_file); +} + +Glib::RefPtr IconLoader::load_icon_from_file(std::string const &icon_path, int size) { + try { + auto pb = Gdk::Pixbuf::create_from_file(icon_path, size, size); + return pb; + } catch (...) { + return {}; + } +} + +std::string IconLoader::get_icon_name_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 ""; +} + +bool IconLoader::image_load_icon(Gtk::Image &image, const Glib::RefPtr &icon_theme, + Glib::RefPtr app_info, int size) { + std::string ret_icon_name = "unknown"; + if (app_info) { + std::string icon_name = + get_icon_name_from_icon_theme(icon_theme, app_info->get_startup_wm_class()); + if (!icon_name.empty()) { + ret_icon_name = icon_name; + } else { + if (app_info->get_icon()) { + ret_icon_name = app_info->get_icon()->to_string(); + } + } + } + + Glib::RefPtr pixbuf; + auto scaled_icon_size = size * image.get_scale_factor(); + + try { + pixbuf = icon_theme->load_icon(ret_icon_name, scaled_icon_size, Gtk::ICON_LOOKUP_FORCE_SIZE); + } catch (...) { + if (Glib::file_test(ret_icon_name, Glib::FILE_TEST_EXISTS)) { + pixbuf = load_icon_from_file(ret_icon_name, scaled_icon_size); + } else { + try { + pixbuf = DefaultGtkIconThemeWrapper::load_icon( + "image-missing", scaled_icon_size, Gtk::IconLookupFlags::ICON_LOOKUP_FORCE_SIZE); + } catch (...) { + pixbuf = {}; + } + } + } + + if (pixbuf) { + if (pixbuf->get_width() != scaled_icon_size) { + int width = scaled_icon_size * pixbuf->get_width() / pixbuf->get_height(); + pixbuf = pixbuf->scale_simple(width, scaled_icon_size, Gdk::InterpType::INTERP_BILINEAR); + } + auto surface = Gdk::Cairo::create_surface_from_pixbuf(pixbuf, image.get_scale_factor(), + image.get_window()); + image.set(surface); + return true; + } + + return false; +} + +void IconLoader::add_custom_icon_theme(const std::string &theme_name) { + auto icon_theme = Gtk::IconTheme::create(); + icon_theme->set_custom_theme(theme_name); + custom_icon_themes_.push_back(icon_theme); + spdlog::debug("Use custom icon theme: {}", theme_name); +} + +bool IconLoader::image_load_icon(Gtk::Image &image, Glib::RefPtr app_info, + int size) const { + for (auto &icon_theme : custom_icon_themes_) { + if (image_load_icon(image, icon_theme, app_info, size)) { + return true; + } + } + return image_load_icon(image, default_icon_theme_, app_info, size); +} + +Glib::RefPtr IconLoader::get_app_info_from_app_id_list( + const std::string &app_id_list) { + std::string app_id; + std::istringstream stream(app_id_list); + Glib::RefPtr app_info_; + + /* 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) { + app_info_ = get_desktop_app_info(app_id); + if (app_info_) { + return app_info_; + } + + auto lower_app_id = app_id; + std::ranges::transform(lower_app_id, lower_app_id.begin(), + [](char c) { return std::tolower(c); }); + app_info_ = get_desktop_app_info(lower_app_id); + if (app_info_) { + return app_info_; + } + + size_t start = 0, end = app_id.size(); + start = app_id.rfind(".", end); + std::string app_name = app_id.substr(start + 1, app_id.size()); + app_info_ = get_desktop_app_info(app_name); + if (app_info_) { + return app_info_; + } + + start = app_id.find("-"); + app_name = app_id.substr(0, start); + app_info_ = get_desktop_app_info(app_name); + } + return app_info_; +}