Merge pull request #3868 from pol-rivero/master

[hyprland/workspaces] Implement workspace taskbars
This commit is contained in:
Alexis Rouillard
2025-08-07 23:00:22 +02:00
committed by GitHub
13 changed files with 638 additions and 297 deletions

View File

@ -26,18 +26,31 @@ namespace waybar::modules::hyprland {
class Workspaces; class Workspaces;
struct WindowRepr {
std::string address;
std::string window_class;
std::string window_title;
std::string repr_rewrite;
bool isActive = false;
public:
bool empty() const { return address.empty(); }
void setActive(bool value) { isActive = value; }
};
class WindowCreationPayload { class WindowCreationPayload {
public: public:
WindowCreationPayload(std::string workspace_name, WindowAddress window_address, WindowCreationPayload(std::string workspace_name, WindowAddress window_address,
std::string window_repr); WindowRepr window_repr);
WindowCreationPayload(std::string workspace_name, WindowAddress window_address, WindowCreationPayload(std::string workspace_name, WindowAddress window_address,
std::string window_class, std::string window_title); std::string window_class, std::string window_title, bool is_active);
WindowCreationPayload(Json::Value const& client_data); WindowCreationPayload(Json::Value const& client_data);
int incrementTimeSpentUncreated(); int incrementTimeSpentUncreated();
bool isEmpty(Workspaces& workspace_manager); bool isEmpty(Workspaces& workspace_manager);
bool reprIsReady() const { return std::holds_alternative<Repr>(m_window); } bool reprIsReady() const { return std::holds_alternative<Repr>(m_window); }
std::string repr(Workspaces& workspace_manager); WindowRepr repr(Workspaces& workspace_manager);
void setActive(bool value) { m_isActive = value; }
std::string getWorkspaceName() const { return m_workspaceName; } std::string getWorkspaceName() const { return m_workspaceName; }
WindowAddress getAddress() const { return m_windowAddress; } WindowAddress getAddress() const { return m_windowAddress; }
@ -48,12 +61,13 @@ class WindowCreationPayload {
void clearAddr(); void clearAddr();
void clearWorkspaceName(); void clearWorkspaceName();
using Repr = std::string; using Repr = WindowRepr;
using ClassAndTitle = std::pair<std::string, std::string>; using ClassAndTitle = std::pair<std::string, std::string>;
std::variant<Repr, ClassAndTitle> m_window; std::variant<Repr, ClassAndTitle> m_window;
WindowAddress m_windowAddress; WindowAddress m_windowAddress;
std::string m_workspaceName; std::string m_workspaceName;
bool m_isActive = false;
int m_timeSpentUncreated = 0; int m_timeSpentUncreated = 0;
}; };

View File

@ -54,15 +54,18 @@ class Workspace {
void setWindows(uint value) { m_windows = value; }; void setWindows(uint value) { m_windows = value; };
void setName(std::string const& value) { m_name = value; }; void setName(std::string const& value) { m_name = value; };
void setOutput(std::string const& value) { m_output = 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_payload); void insertWindow(WindowCreationPayload create_window_payload);
std::string removeWindow(WindowAddress const& addr);
void initializeWindowMap(const Json::Value& clients_data); void initializeWindowMap(const Json::Value& clients_data);
void setActiveWindow(WindowAddress const& addr);
bool onWindowOpened(WindowCreationPayload const& create_window_payload); bool onWindowOpened(WindowCreationPayload const& create_window_payload);
std::optional<std::string> closeWindow(WindowAddress const& addr); std::optional<WindowRepr> closeWindow(WindowAddress const& addr);
void update(const std::string& format, const std::string& icon); void update(const std::string& workspace_icon);
private: private:
Workspaces& m_workspaceManager; Workspaces& m_workspaceManager;
@ -78,11 +81,16 @@ class Workspace {
bool m_isUrgent = false; bool m_isUrgent = false;
bool m_isVisible = false; bool m_isVisible = false;
std::map<WindowAddress, std::string> m_windowMap; std::vector<WindowRepr> m_windowMap;
Gtk::Button m_button; Gtk::Button m_button;
Gtk::Box m_content; 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;
bool shouldSkipWindow(const WindowRepr& window_repr) const;
IPC& m_ipc; IPC& m_ipc;
}; };

View File

@ -1,6 +1,7 @@
#pragma once #pragma once
#include <gtkmm/button.h> #include <gtkmm/button.h>
#include <gtkmm/enums.h>
#include <gtkmm/label.h> #include <gtkmm/label.h>
#include <json/value.h> #include <json/value.h>
@ -18,6 +19,7 @@
#include "modules/hyprland/windowcreationpayload.hpp" #include "modules/hyprland/windowcreationpayload.hpp"
#include "modules/hyprland/workspace.hpp" #include "modules/hyprland/workspace.hpp"
#include "util/enum.hpp" #include "util/enum.hpp"
#include "util/icon_loader.hpp"
#include "util/regex_collection.hpp" #include "util/regex_collection.hpp"
using WindowAddress = std::string; using WindowAddress = std::string;
@ -39,14 +41,25 @@ class Workspaces : public AModule, public EventHandler {
auto specialVisibleOnly() const -> bool { return m_specialVisibleOnly; } auto specialVisibleOnly() const -> bool { return m_specialVisibleOnly; }
auto persistentOnly() const -> bool { return m_persistentOnly; } auto persistentOnly() const -> bool { return m_persistentOnly; }
auto moveToMonitor() const -> bool { return m_moveToMonitor; } 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 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; }
auto getIgnoredWindows() const -> std::vector<std::regex> { return m_ignoreWindows; }
std::string getRewrite(std::string window_class, std::string window_title); std::string getRewrite(std::string window_class, std::string window_title);
std::string& getWindowSeparator() { return m_formatWindowSeparator; } std::string& getWindowSeparator() { return m_formatWindowSeparator; }
bool isWorkspaceIgnored(std::string const& workspace_name); bool isWorkspaceIgnored(std::string const& workspace_name);
bool windowRewriteConfigUsesTitle() const { return m_anyWindowRewriteRuleUsesTitle; } bool windowRewriteConfigUsesTitle() const { return m_anyWindowRewriteRuleUsesTitle; }
const IconLoader& iconLoader() const { return m_iconLoader; }
private: private:
void onEvent(const std::string& e) override; void onEvent(const std::string& e) override;
@ -70,6 +83,7 @@ class Workspaces : public AModule, public EventHandler {
auto populateIgnoreWorkspacesConfig(const Json::Value& config) -> void; auto populateIgnoreWorkspacesConfig(const Json::Value& config) -> void;
auto populateFormatWindowSeparatorConfig(const Json::Value& config) -> void; auto populateFormatWindowSeparatorConfig(const Json::Value& config) -> void;
auto populateWindowRewriteConfig(const Json::Value& config) -> void; auto populateWindowRewriteConfig(const Json::Value& config) -> void;
auto populateWorkspaceTaskbarConfig(const Json::Value& config) -> void;
void registerIpc(); void registerIpc();
@ -92,6 +106,7 @@ class Workspaces : public AModule, public EventHandler {
void onWindowMoved(std::string const& payload); void onWindowMoved(std::string const& payload);
void onWindowTitleEvent(std::string const& payload); void onWindowTitleEvent(std::string const& payload);
void onActiveWindowChanged(WindowAddress const& payload);
void onConfigReloaded(); void onConfigReloaded();
@ -131,7 +146,7 @@ class Workspaces : public AModule, public EventHandler {
// Map for windows stored in workspaces not present in the current bar. // Map for windows stored in workspaces not present in the current bar.
// This happens when the user has multiple monitors (hence, multiple bars) // This happens when the user has multiple monitors (hence, multiple bars)
// and doesn't share windows across bars (a.k.a `all-outputs` = false) // and doesn't share windows across bars (a.k.a `all-outputs` = false)
std::map<WindowAddress, std::string> m_orphanWindowMap; std::map<WindowAddress, WindowRepr, std::less<>> m_orphanWindowMap;
enum class SortMethod { ID, NAME, NUMBER, SPECIAL_CENTERED, DEFAULT }; enum class SortMethod { ID, NAME, NUMBER, SPECIAL_CENTERED, DEFAULT };
util::EnumParser<SortMethod> m_enumParser; util::EnumParser<SortMethod> m_enumParser;
@ -142,7 +157,8 @@ class Workspaces : public AModule, public EventHandler {
{"SPECIAL-CENTERED", SortMethod::SPECIAL_CENTERED}, {"SPECIAL-CENTERED", SortMethod::SPECIAL_CENTERED},
{"DEFAULT", SortMethod::DEFAULT}}; {"DEFAULT", SortMethod::DEFAULT}};
std::string m_format; std::string m_formatBefore;
std::string m_formatAfter;
std::map<std::string, std::string> m_iconsMap; std::map<std::string, std::string> m_iconsMap;
util::RegexCollection m_windowRewriteRules; util::RegexCollection m_windowRewriteRules;
@ -158,7 +174,20 @@ class Workspaces : public AModule, public EventHandler {
std::vector<std::string> m_workspacesToRemove; std::vector<std::string> m_workspacesToRemove;
std::vector<WindowCreationPayload> m_windowsToCreate; std::vector<WindowCreationPayload> m_windowsToCreate;
IconLoader m_iconLoader;
bool m_enableTaskbar = false;
bool m_updateActiveWindow = 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::string m_currentActiveWindowAddress;
std::vector<std::regex> m_ignoreWorkspaces; std::vector<std::regex> m_ignoreWorkspaces;
std::vector<std::regex> m_ignoreWindows;
std::mutex m_mutex; std::mutex m_mutex;
const Bar& m_bar; const Bar& m_bar;

View File

@ -19,6 +19,7 @@
#include "bar.hpp" #include "bar.hpp"
#include "client.hpp" #include "client.hpp"
#include "giomm/desktopappinfo.h" #include "giomm/desktopappinfo.h"
#include "util/icon_loader.hpp"
#include "util/json.hpp" #include "util/json.hpp"
#include "wlr-foreign-toplevel-management-unstable-v1-client-protocol.h" #include "wlr-foreign-toplevel-management-unstable-v1-client-protocol.h"
@ -89,9 +90,6 @@ class Task {
std::string state_string(bool = false) const; std::string state_string(bool = false) const;
void set_minimize_hint(); void set_minimize_hint();
void on_button_size_allocated(Gtk::Allocation &alloc); 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<Gtk::IconTheme> &icon_theme,
Glib::RefPtr<Gio::DesktopAppInfo> app_info, int size);
void hide_if_ignored(); void hide_if_ignored();
public: public:
@ -153,7 +151,7 @@ class Taskbar : public waybar::AModule {
Gtk::Box box_; Gtk::Box box_;
std::vector<TaskPtr> tasks_; std::vector<TaskPtr> tasks_;
std::vector<Glib::RefPtr<Gtk::IconTheme>> icon_themes_; IconLoader icon_loader_;
std::unordered_set<std::string> ignore_list_; std::unordered_set<std::string> ignore_list_;
std::map<std::string, std::string> app_ids_replace_map_; std::map<std::string, std::string> app_ids_replace_map_;
@ -178,7 +176,7 @@ class Taskbar : public waybar::AModule {
bool show_output(struct wl_output *) const; bool show_output(struct wl_output *) const;
bool all_outputs() const; bool all_outputs() const;
const std::vector<Glib::RefPtr<Gtk::IconTheme>> &icon_themes() const; const IconLoader &icon_loader() const;
const std::unordered_set<std::string> &ignore_list() const; const std::unordered_set<std::string> &ignore_list() const;
const std::map<std::string, std::string> &app_ids_replace_map() const; const std::map<std::string, std::string> &app_ids_replace_map() const;
}; };

View File

@ -0,0 +1,34 @@
#pragma once
#include <gdkmm/general.h>
#include <gio/gdesktopappinfo.h>
#include <giomm/desktopappinfo.h>
#include <glibmm/fileutils.h>
#include <gtkmm/image.h>
#include <spdlog/spdlog.h>
#include <string>
#include <vector>
#include "util/gtk_icon.hpp"
class IconLoader {
private:
std::vector<Glib::RefPtr<Gtk::IconTheme>> custom_icon_themes_;
Glib::RefPtr<Gtk::IconTheme> default_icon_theme_ = Gtk::IconTheme::get_default();
static std::vector<std::string> search_prefix();
static Glib::RefPtr<Gio::DesktopAppInfo> get_app_info_by_name(const std::string &app_id);
static Glib::RefPtr<Gio::DesktopAppInfo> get_desktop_app_info(const std::string &app_id);
static Glib::RefPtr<Gdk::Pixbuf> load_icon_from_file(std::string const &icon_path, int size);
static std::string get_icon_name_from_icon_theme(const Glib::RefPtr<Gtk::IconTheme> &icon_theme,
const std::string &app_id);
static bool image_load_icon(Gtk::Image &image, const Glib::RefPtr<Gtk::IconTheme> &icon_theme,
Glib::RefPtr<Gio::DesktopAppInfo> app_info, int size);
public:
void add_custom_icon_theme(const std::string &theme_name);
bool image_load_icon(Gtk::Image &image, Glib::RefPtr<Gio::DesktopAppInfo> app_info,
int size) const;
static Glib::RefPtr<Gio::DesktopAppInfo> get_app_info_from_app_id_list(
const std::string &app_id_list);
};

View File

@ -23,3 +23,26 @@ inline std::string capitalize(const std::string& str) {
[](unsigned char c) { return std::toupper(c); }); [](unsigned char c) { return std::toupper(c); });
return result; 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<std::string> split(std::string_view s, std::string_view delimiter,
int max_splits = -1) {
std::vector<std::string> 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<size_t>(max_splits)) {
break;
}
}
result.push_back(std::string(s.substr(pos)));
return result;
}

View File

@ -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. 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. 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. 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 ++ typeof: string ++
default: "?" ++ 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*: ++ *format-window-separator*: ++
typeof: string ++ typeof: string ++
default: " " ++ 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*: ++ *show-special*: ++
typeof: bool ++ typeof: bool ++
@ -184,3 +216,4 @@ Additional to workspace name matching, the following *format-icons* can be set.
- *#workspaces button.special* - *#workspaces button.special*
- *#workspaces button.urgent* - *#workspaces button.urgent*
- *#workspaces button.hosting-monitor* (gets applied if workspace-monitor == waybar-monitor) - *#workspaces button.hosting-monitor* (gets applied if workspace-monitor == waybar-monitor)
- *#workspaces .taskbar-window* (each window in the taskbar)

View File

@ -183,6 +183,7 @@ src_files = files(
'src/util/sanitize_str.cpp', 'src/util/sanitize_str.cpp',
'src/util/rewrite_string.cpp', 'src/util/rewrite_string.cpp',
'src/util/gtk_icon.cpp', 'src/util/gtk_icon.cpp',
'src/util/icon_loader.cpp',
'src/util/regex_collection.cpp', 'src/util/regex_collection.cpp',
'src/util/css_reload_helper.cpp' 'src/util/css_reload_helper.cpp'
) )

View File

@ -20,7 +20,7 @@ WindowCreationPayload::WindowCreationPayload(Json::Value const &client_data)
} }
WindowCreationPayload::WindowCreationPayload(std::string workspace_name, 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_window(std::move(window_repr)),
m_windowAddress(std::move(window_address)), m_windowAddress(std::move(window_address)),
m_workspaceName(std::move(workspace_name)) { m_workspaceName(std::move(workspace_name)) {
@ -30,10 +30,11 @@ WindowCreationPayload::WindowCreationPayload(std::string workspace_name,
WindowCreationPayload::WindowCreationPayload(std::string workspace_name, WindowCreationPayload::WindowCreationPayload(std::string workspace_name,
WindowAddress window_address, std::string window_class, WindowAddress window_address, std::string window_class,
std::string window_title) std::string window_title, bool is_active)
: m_window(std::make_pair(std::move(window_class), std::move(window_title))), : m_window(std::make_pair(std::move(window_class), std::move(window_title))),
m_windowAddress(std::move(window_address)), m_windowAddress(std::move(window_address)),
m_workspaceName(std::move(workspace_name)) { m_workspaceName(std::move(workspace_name)),
m_isActive(is_active) {
clearAddr(); clearAddr();
clearWorkspaceName(); clearWorkspaceName();
} }
@ -92,13 +93,14 @@ void WindowCreationPayload::moveToWorkspace(std::string &new_workspace_name) {
m_workspaceName = 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<Repr>(m_window)) { if (std::holds_alternative<Repr>(m_window)) {
return std::get<Repr>(m_window); return std::get<Repr>(m_window);
} }
if (std::holds_alternative<ClassAndTitle>(m_window)) { if (std::holds_alternative<ClassAndTitle>(m_window)) {
auto [window_class, window_title] = std::get<ClassAndTitle>(m_window); auto const &[window_class, window_title] = std::get<ClassAndTitle>(m_window);
return workspace_manager.getRewrite(window_class, window_title); return {m_windowAddress, window_class, window_title,
workspace_manager.getRewrite(window_class, window_title), m_isActive};
} }
// Unreachable // Unreachable
spdlog::error("WorkspaceWindow::repr: Unreachable"); spdlog::error("WorkspaceWindow::repr: Unreachable");

View File

@ -6,6 +6,8 @@
#include <utility> #include <utility>
#include "modules/hyprland/workspaces.hpp" #include "modules/hyprland/workspaces.hpp"
#include "util/command.hpp"
#include "util/icon_loader.hpp"
namespace waybar::modules::hyprland { namespace waybar::modules::hyprland {
@ -32,7 +34,12 @@ Workspace::Workspace(const Json::Value &workspace_data, Workspaces &workspace_ma
false); false);
m_button.set_relief(Gtk::RELIEF_NONE); 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); m_button.add(m_content);
initializeWindowMap(clients_data); initializeWindowMap(clients_data);
@ -47,9 +54,14 @@ void addOrRemoveClass(const Glib::RefPtr<Gtk::StyleContext> &context, bool condi
} }
} }
std::optional<std::string> Workspace::closeWindow(WindowAddress const &addr) { std::optional<WindowRepr> Workspace::closeWindow(WindowAddress const &addr) {
if (m_windowMap.contains(addr)) { auto it = std::ranges::find_if(m_windowMap,
return removeWindow(addr); [&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; return std::nullopt;
} }
@ -91,12 +103,26 @@ void Workspace::initializeWindowMap(const Json::Value &clients_data) {
} }
} }
void Workspace::setActiveWindow(WindowAddress const &addr) {
for (auto &window : m_windowMap) {
window.setActive(window.address == addr);
}
}
void Workspace::insertWindow(WindowCreationPayload create_window_payload) { void Workspace::insertWindow(WindowCreationPayload create_window_payload) {
if (!create_window_payload.isEmpty(m_workspaceManager)) { if (!create_window_payload.isEmpty(m_workspaceManager)) {
auto repr = create_window_payload.repr(m_workspaceManager); auto repr = create_window_payload.repr(m_workspaceManager);
if (!repr.empty()) { if (!repr.empty() || m_workspaceManager.enableTaskbar()) {
m_windowMap[create_window_payload.getAddress()] = repr; auto addr = create_window_payload.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 +135,6 @@ bool Workspace::onWindowOpened(WindowCreationPayload const &create_window_payloa
return false; 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<std::string, std::string> &icons_map) { std::string &Workspace::selectIcon(std::map<std::string, std::string> &icons_map) {
spdlog::trace("Selecting icon for workspace {}", name()); spdlog::trace("Selecting icon for workspace {}", name());
if (isUrgent()) { if (isUrgent()) {
@ -172,7 +192,7 @@ std::string &Workspace::selectIcon(std::map<std::string, std::string> &icons_map
return m_name; return m_name;
} }
void Workspace::update(const std::string &format, const std::string &icon) { void Workspace::update(const std::string &workspace_icon) {
if (this->m_workspaceManager.persistentOnly() && !this->isPersistent()) { if (this->m_workspaceManager.persistentOnly() && !this->isPersistent()) {
m_button.hide(); m_button.hide();
return; return;
@ -204,21 +224,122 @@ void Workspace::update(const std::string &format, const std::string &icon) {
addOrRemoveClass(styleContext, m_workspaceManager.getBarOutput() == output(), "hosting-monitor"); addOrRemoveClass(styleContext, m_workspaceManager.getBarOutput() == output(), "hosting-monitor");
std::string windows; 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; bool isNotFirst = false;
for (auto &[_pid, window_repr] : m_windowMap) { for (const auto &window_repr : m_windowMap) {
if (isNotFirst) { if (isNotFirst) {
windows.append(windowSeparator); windows.append(windowSeparator);
}
isNotFirst = true;
windows.append(window_repr.repr_rewrite);
} }
isNotFirst = true;
windows.append(window_repr);
} }
m_label.set_markup(fmt::format(fmt::runtime(format), fmt::arg("id", id()), auto formatBefore = m_workspaceManager.formatBefore();
fmt::arg("name", name()), fmt::arg("icon", icon), m_labelBefore.set_markup(fmt::format(fmt::runtime(formatBefore), fmt::arg("id", id()),
fmt::arg("windows", windows))); fmt::arg("name", name()), fmt::arg("icon", workspace_icon),
fmt::arg("windows", windows)));
m_labelBefore.get_style_context()->add_class("workspace-label");
if (m_workspaceManager.enableTaskbar()) {
updateTaskbar(workspace_icon);
}
}
void Workspace::updateTaskbar(const std::string &workspace_icon) {
for (auto child : m_content.get_children()) {
if (child != &m_labelBefore) {
m_content.remove(*child);
}
}
bool isFirst = true;
for (const auto &window_repr : m_windowMap) {
if (shouldSkipWindow(window_repr)) {
continue;
}
if (isFirst) {
isFirst = false;
} else if (m_workspaceManager.getWindowSeparator() != "") {
auto windowSeparator = Gtk::make_managed<Gtk::Label>(m_workspaceManager.getWindowSeparator());
m_content.pack_start(*windowSeparator, false, false);
windowSeparator->show();
}
auto window_box = Gtk::make_managed<Gtk::Box>(Gtk::ORIENTATION_HORIZONTAL);
window_box->set_tooltip_text(window_repr.window_title);
window_box->get_style_context()->add_class("taskbar-window");
if (window_repr.isActive) {
window_box->get_style_context()->add_class("active");
}
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<Gtk::Label>(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<Gtk::Image>();
m_workspaceManager.iconLoader().image_load_icon(*window_icon, app_info_, icon_size);
window_box->pack_start(*window_icon, false, false);
}
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<Gtk::Label>(text_after);
window_box->pack_start(*window_label_after, true, true);
}
m_content.pack_start(*event_box, true, false);
event_box->show_all();
}
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;
}
bool Workspace::shouldSkipWindow(const WindowRepr &window_repr) const {
auto ignore_list = m_workspaceManager.getIgnoredWindows();
auto it = std::ranges::find_if(ignore_list, [&window_repr](const auto &ignoreItem) {
return std::regex_match(window_repr.window_class, ignoreItem) ||
std::regex_match(window_repr.window_title, ignoreItem);
});
return it != ignore_list.end();
} }
} // namespace waybar::modules::hyprland } // namespace waybar::modules::hyprland

View File

@ -10,6 +10,7 @@
#include <utility> #include <utility>
#include "util/regex_collection.hpp" #include "util/regex_collection.hpp"
#include "util/string.hpp"
namespace waybar::modules::hyprland { namespace waybar::modules::hyprland {
@ -346,6 +347,8 @@ void Workspaces::onEvent(const std::string &ev) {
onWorkspaceRenamed(payload); onWorkspaceRenamed(payload);
} else if (eventName == "windowtitlev2") { } else if (eventName == "windowtitlev2") {
onWindowTitleEvent(payload); onWindowTitleEvent(payload);
} else if (eventName == "activewindowv2") {
onActiveWindowChanged(payload);
} else if (eventName == "configreloaded") { } else if (eventName == "configreloaded") {
onConfigReloaded(); onConfigReloaded();
} }
@ -495,12 +498,14 @@ void Workspaces::onWindowOpened(std::string const &payload) {
std::string windowTitle = payload.substr(nextCommaIdx + 1, payload.length() - nextCommaIdx); std::string windowTitle = payload.substr(nextCommaIdx + 1, payload.length() - nextCommaIdx);
m_windowsToCreate.emplace_back(workspaceName, windowAddress, windowClass, windowTitle); bool isActive = m_currentActiveWindowAddress == windowAddress;
m_windowsToCreate.emplace_back(workspaceName, windowAddress, windowClass, windowTitle, isActive);
} }
void Workspaces::onWindowClosed(std::string const &addr) { void Workspaces::onWindowClosed(std::string const &addr) {
spdlog::trace("Window closed: {}", addr); spdlog::trace("Window closed: {}", addr);
updateWindowCount(); updateWindowCount();
m_orphanWindowMap.erase(addr);
for (auto &workspace : m_workspaces) { for (auto &workspace : m_workspaces) {
if (workspace->closeWindow(addr)) { if (workspace->closeWindow(addr)) {
break; break;
@ -513,7 +518,7 @@ void Workspaces::onWindowMoved(std::string const &payload) {
updateWindowCount(); updateWindowCount();
auto [windowAddress, _, workspaceName] = splitTriplePayload(payload); auto [windowAddress, _, workspaceName] = splitTriplePayload(payload);
std::string windowRepr; WindowRepr windowRepr;
// If the window was still queued to be created, just change its destination // If the window was still queued to be created, just change its destination
// and exit // and exit
@ -539,6 +544,7 @@ void Workspaces::onWindowMoved(std::string const &payload) {
// ...and then add it to the new workspace // ...and then add it to the new workspace
if (!windowRepr.empty()) { if (!windowRepr.empty()) {
m_orphanWindowMap.erase(windowAddress);
m_windowsToCreate.emplace_back(workspaceName, windowAddress, windowRepr); m_windowsToCreate.emplace_back(workspaceName, windowAddress, windowRepr);
} }
} }
@ -564,9 +570,10 @@ void Workspaces::onWindowTitleEvent(std::string const &payload) {
(*windowWorkspace)->insertWindow(std::move(wcp)); (*windowWorkspace)->insertWindow(std::move(wcp));
}; };
} else { } else {
auto queuedWindow = std::ranges::find_if(m_windowsToCreate, [payload](auto &windowPayload) { auto queuedWindow =
return windowPayload.getAddress() == payload; std::ranges::find_if(m_windowsToCreate, [&windowAddress](auto &windowPayload) {
}); return windowPayload.getAddress() == windowAddress;
});
// If the window was queued, rename it in the queue // If the window was queued, rename it in the queue
if (queuedWindow != m_windowsToCreate.end()) { if (queuedWindow != m_windowsToCreate.end()) {
@ -577,7 +584,7 @@ void Workspaces::onWindowTitleEvent(std::string const &payload) {
if (inserter.has_value()) { if (inserter.has_value()) {
Json::Value clientsData = m_ipc.getSocket1JsonReply("clients"); Json::Value clientsData = m_ipc.getSocket1JsonReply("clients");
std::string jsonWindowAddress = fmt::format("0x{}", payload); std::string jsonWindowAddress = fmt::format("0x{}", windowAddress);
auto client = std::ranges::find_if(clientsData, [jsonWindowAddress](auto &client) { auto client = std::ranges::find_if(clientsData, [jsonWindowAddress](auto &client) {
return client["address"].asString() == jsonWindowAddress; return client["address"].asString() == jsonWindowAddress;
@ -589,6 +596,21 @@ void Workspaces::onWindowTitleEvent(std::string const &payload) {
} }
} }
void Workspaces::onActiveWindowChanged(WindowAddress const &activeWindowAddress) {
spdlog::trace("Active window changed: {}", activeWindowAddress);
m_currentActiveWindowAddress = activeWindowAddress;
for (auto &[address, window] : m_orphanWindowMap) {
window.setActive(address == activeWindowAddress);
}
for (auto const &workspace : m_workspaces) {
workspace->setActiveWindow(activeWindowAddress);
}
for (auto &window : m_windowsToCreate) {
window.setActive(window.getAddress() == activeWindowAddress);
}
}
void Workspaces::onConfigReloaded() { void Workspaces::onConfigReloaded() {
spdlog::info("Hyprland config reloaded, reinitializing hyprland/workspaces module..."); spdlog::info("Hyprland config reloaded, reinitializing hyprland/workspaces module...");
init(); init();
@ -596,8 +618,9 @@ void Workspaces::onConfigReloaded() {
auto Workspaces::parseConfig(const Json::Value &config) -> void { auto Workspaces::parseConfig(const Json::Value &config) -> void {
const auto &configFormat = config["format"]; const auto &configFormat = config["format"];
m_format = configFormat.isString() ? configFormat.asString() : "{name}"; m_formatBefore = configFormat.isString() ? configFormat.asString() : "{name}";
m_withIcon = m_format.find("{icon}") != std::string::npos; m_withIcon = m_formatBefore.find("{icon}") != std::string::npos;
auto withWindows = m_formatBefore.find("{windows}") != std::string::npos;
if (m_withIcon && m_iconsMap.empty()) { if (m_withIcon && m_iconsMap.empty()) {
populateIconsMap(config["format-icons"]); populateIconsMap(config["format-icons"]);
@ -615,6 +638,15 @@ auto Workspaces::parseConfig(const Json::Value &config) -> void {
populateIgnoreWorkspacesConfig(config); populateIgnoreWorkspacesConfig(config);
populateFormatWindowSeparatorConfig(config); populateFormatWindowSeparatorConfig(config);
populateWindowRewriteConfig(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 { auto Workspaces::populateIconsMap(const Json::Value &formatIcons) -> void {
@ -687,6 +719,64 @@ auto Workspaces::populateWindowRewriteConfig(const Json::Value &config) -> void
[this](std::string &window_rule) { return windowRewritePriorityFunction(window_rule); }); [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);
populateBoolConfig(workspaceTaskbar, "update-active-window", m_updateActiveWindow);
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();
}
if (workspaceTaskbar["ignore-list"].isArray()) {
for (auto &windowRegex : workspaceTaskbar["ignore-list"]) {
std::string ruleString = windowRegex.asString();
try {
m_ignoreWindows.emplace_back(ruleString, std::regex_constants::icase);
} catch (const std::regex_error &e) {
spdlog::error("Invalid rule {}: {}", ruleString, e.what());
}
}
}
}
void Workspaces::registerOrphanWindow(WindowCreationPayload create_window_payload) { void Workspaces::registerOrphanWindow(WindowCreationPayload create_window_payload) {
if (!create_window_payload.isEmpty(*this)) { if (!create_window_payload.isEmpty(*this)) {
m_orphanWindowMap[create_window_payload.getAddress()] = create_window_payload.repr(*this); m_orphanWindowMap[create_window_payload.getAddress()] = create_window_payload.repr(*this);
@ -707,12 +797,18 @@ auto Workspaces::registerIpc() -> void {
m_ipc.registerForIPC("urgent", this); m_ipc.registerForIPC("urgent", this);
m_ipc.registerForIPC("configreloaded", this); m_ipc.registerForIPC("configreloaded", this);
if (windowRewriteConfigUsesTitle()) { if (windowRewriteConfigUsesTitle() || m_taskbarWithTitle) {
spdlog::info( spdlog::info(
"Registering for Hyprland's 'windowtitlev2' events because a user-defined window " "Registering for Hyprland's 'windowtitlev2' events because a user-defined window "
"rewrite rule uses the 'title' field."); "rewrite rule uses the 'title' field.");
m_ipc.registerForIPC("windowtitlev2", this); m_ipc.registerForIPC("windowtitlev2", this);
} }
if (m_updateActiveWindow) {
spdlog::info(
"Registering for Hyprland's 'activewindowv2' events because 'update-active-window' is set "
"to true.");
m_ipc.registerForIPC("activewindowv2", this);
}
} }
void Workspaces::removeWorkspacesToRemove() { void Workspaces::removeWorkspacesToRemove() {
@ -904,7 +1000,7 @@ auto Workspaces::update() -> void {
void Workspaces::updateWindowCount() { void Workspaces::updateWindowCount() {
const Json::Value workspacesJson = m_ipc.getSocket1JsonReply("workspaces"); const Json::Value workspacesJson = m_ipc.getSocket1JsonReply("workspaces");
for (auto &workspace : m_workspaces) { for (auto const &workspace : m_workspaces) {
auto workspaceJson = std::ranges::find_if(workspacesJson, [&](Json::Value const &x) { auto workspaceJson = std::ranges::find_if(workspacesJson, [&](Json::Value const &x) {
return x["name"].asString() == workspace->name() || return x["name"].asString() == workspace->name() ||
(workspace->isSpecial() && x["name"].asString() == "special:" + workspace->name()); (workspace->isSpecial() && x["name"].asString() == "special:" + workspace->name());
@ -979,7 +1075,7 @@ void Workspaces::updateWorkspaceStates() {
if (updatedWorkspace != updatedWorkspaces.end()) { if (updatedWorkspace != updatedWorkspaces.end()) {
workspace->setOutput((*updatedWorkspace)["monitor"].asString()); workspace->setOutput((*updatedWorkspace)["monitor"].asString());
} }
workspace->update(m_format, workspaceIcon); workspace->update(workspaceIcon);
} }
} }

View File

@ -26,194 +26,6 @@
namespace waybar::modules::wlr { namespace waybar::modules::wlr {
/* Icon loading functions */
static std::vector<std::string> search_prefix() {
std::vector<std::string> 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<Gdk::Pixbuf> 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<Gio::DesktopAppInfo> get_app_info_by_name(const std::string &app_id) {
static std::vector<std::string> prefixes = search_prefix();
std::vector<std::string> app_folders = {"", "applications/", "applications/kde/",
"applications/org.kde."};
std::vector<std::string> 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<Gio::DesktopAppInfo> 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<Gtk::IconTheme> &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<Gtk::IconTheme> &icon_theme,
Glib::RefPtr<Gio::DesktopAppInfo> 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<Gdk::Pixbuf> 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 */ /* Task class implementation */
uint32_t Task::global_id = 0; 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; with_name_ = true;
} }
auto icon_pos = format.find("{icon}"); auto parts = split(format, "{icon}", 1);
if (icon_pos == 0) { format_before_ = parts[0];
if (parts.size() > 1) {
with_icon_ = true; with_icon_ = true;
format_after_ = format.substr(6); format_after_ = parts[1];
} 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 { } else {
/* The default is to only show the icon */ /* The default is to only show the icon */
@ -395,7 +202,7 @@ void Task::handle_title(const char *title) {
return; return;
} }
set_app_info_from_app_id_list(title_); app_info_ = IconLoader::get_app_info_from_app_id_list(title_);
name_ = app_info_ ? app_info_->get_display_name() : title; name_ = app_info_ ? app_info_->get_display_name() : title;
if (!with_icon_) { if (!with_icon_) {
@ -403,15 +210,7 @@ void Task::handle_title(const char *title) {
} }
int icon_size = config_["icon-size"].isInt() ? config_["icon-size"].asInt() : 16; int icon_size = config_["icon-size"].isInt() ? config_["icon-size"].asInt() : 16;
bool found = false; if (tbar_->icon_loader().image_load_icon(icon_, app_info_, icon_size))
for (auto &icon_theme : tbar_->icon_themes()) {
if (image_load_icon(icon_, icon_theme, app_info_, icon_size)) {
found = true;
break;
}
}
if (found)
icon_.show(); icon_.show();
else else
spdlog::debug("Couldn't find icon for {}", title_); spdlog::debug("Couldn't find icon for {}", title_);
@ -460,7 +259,7 @@ void Task::handle_app_id(const char *app_id) {
return; 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; name_ = app_info_ ? app_info_->get_display_name() : app_id;
if (!with_icon_) { if (!with_icon_) {
@ -468,15 +267,7 @@ void Task::handle_app_id(const char *app_id) {
} }
int icon_size = config_["icon-size"].isInt() ? config_["icon-size"].asInt() : 16; int icon_size = config_["icon-size"].isInt() ? config_["icon-size"].asInt() : 16;
bool found = false; if (tbar_->icon_loader().image_load_icon(icon_, app_info_, icon_size))
for (auto &icon_theme : tbar_->icon_themes()) {
if (image_load_icon(icon_, icon_theme, app_info_, icon_size)) {
found = true;
break;
}
}
if (found)
icon_.show(); icon_.show();
else else
spdlog::debug("Couldn't find icon for {}", app_id_); spdlog::debug("Couldn't find icon for {}", app_id_);
@ -802,22 +593,10 @@ Taskbar::Taskbar(const std::string &id, const waybar::Bar &bar, const Json::Valu
/* Get the configured icon theme if specified */ /* Get the configured icon theme if specified */
if (config_["icon-theme"].isArray()) { if (config_["icon-theme"].isArray()) {
for (auto &c : config_["icon-theme"]) { for (auto &c : config_["icon-theme"]) {
auto it_name = c.asString(); icon_loader_.add_custom_icon_theme(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()) { } else if (config_["icon-theme"].isString()) {
auto it_name = config_["icon-theme"].asString(); icon_loader_.add_custom_icon_theme(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);
} }
// Load ignore-list // Load ignore-list
@ -836,8 +615,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_) { for (auto &t : tasks_) {
t->handle_app_id(t->app_id().c_str()); t->handle_app_id(t->app_id().c_str());
} }
@ -972,9 +749,7 @@ bool Taskbar::all_outputs() const {
return config_["all-outputs"].isBool() && config_["all-outputs"].asBool(); return config_["all-outputs"].isBool() && config_["all-outputs"].asBool();
} }
const std::vector<Glib::RefPtr<Gtk::IconTheme>> &Taskbar::icon_themes() const { const IconLoader &Taskbar::icon_loader() const { return icon_loader_; }
return icon_themes_;
}
const std::unordered_set<std::string> &Taskbar::ignore_list() const { return ignore_list_; } const std::unordered_set<std::string> &Taskbar::ignore_list() const { return ignore_list_; }

207
src/util/icon_loader.cpp Normal file
View File

@ -0,0 +1,207 @@
#include "util/icon_loader.hpp"
#include "util/string.hpp"
std::vector<std::string> IconLoader::search_prefix() {
std::vector<std::string> 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<Gio::DesktopAppInfo> IconLoader::get_app_info_by_name(const std::string &app_id) {
static std::vector<std::string> prefixes = search_prefix();
std::vector<std::string> app_folders = {"", "applications/", "applications/kde/",
"applications/org.kde."};
std::vector<std::string> 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<Gio::DesktopAppInfo> 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<Gdk::Pixbuf> 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<Gtk::IconTheme> &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<Gtk::IconTheme> &icon_theme,
Glib::RefPtr<Gio::DesktopAppInfo> 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<Gdk::Pixbuf> 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<Gio::DesktopAppInfo> 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<Gio::DesktopAppInfo> 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<Gio::DesktopAppInfo> 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_;
}