Merge branch 'master' into master

This commit is contained in:
Alexis Rouillard
2026-07-03 22:22:19 +02:00
committed by GitHub
76 changed files with 1269 additions and 393 deletions
+67
View File
@@ -13,6 +13,7 @@
#include <cerrno>
#include <cstring>
#include <filesystem>
#include <optional>
#include <string>
#include "util/scoped_fd.hpp"
@@ -20,6 +21,7 @@
namespace waybar::modules::hyprland {
std::filesystem::path IPC::socketFolder_;
std::optional<bool> IPC::s_luaProtocolDetected_;
std::filesystem::path IPC::getSocketFolder(const char* instanceSig) {
static std::mutex folderMutex;
@@ -290,4 +292,69 @@ Json::Value IPC::getSocket1JsonReply(const std::string& rq) {
return parser_.parse(reply);
}
bool IPC::isLuaProtocol() {
if (s_luaProtocolDetected_.has_value()) {
return *s_luaProtocolDetected_;
}
// Probe: send a harmless old-style dispatch and check the error.
// In Lua-based Hyprland (>= 0.54) the error contains "hl.dispatch".
// In older versions it returns "ok" or a different error.
auto reply = getSocket1Reply("dispatch workspace __waybar_probe__");
bool luaProto = reply.find("hl.dispatch") != std::string::npos;
if (luaProto) {
spdlog::info("Hyprland IPC: detected Lua-based dispatch protocol (Hyprland >= 0.54)");
} else {
spdlog::info("Hyprland IPC: detected legacy dispatch protocol");
}
s_luaProtocolDetected_ = luaProto;
return luaProto;
}
std::string IPC::buildLuaDispatch(const std::string& dispatcher, const std::string& arg) {
// Map old-style dispatchers to the new Lua hl.dsp API.
//
// Old format: dispatch workspace 1
// New format: /dispatch hl.dsp.focus({ workspace = "1" })
//
// Old format: dispatch focusworkspaceoncurrentmonitor 2
// New format: /dispatch hl.dsp.focus({ workspace = "2", on_current_monitor = true })
//
// Old format: dispatch togglespecialworkspace name
// New format: /dispatch hl.dsp.workspace.toggle_special("name")
if (dispatcher == "workspace") {
return "/dispatch hl.dsp.focus({ workspace = \"" + arg + "\" })";
}
if (dispatcher == "focusworkspaceoncurrentmonitor") {
return "/dispatch hl.dsp.focus({ workspace = \"" + arg + "\", on_current_monitor = true })";
}
if (dispatcher == "togglespecialworkspace") {
if (arg.empty()) {
return "/dispatch hl.dsp.workspace.toggle_special()";
}
return "/dispatch hl.dsp.workspace.toggle_special(\"" + arg + "\")";
}
// Fallback for any other dispatcher: try the old format wrapped in dispatch().
// This may not work for all dispatchers, but it's a reasonable default.
spdlog::warn("Hyprland IPC: unknown dispatcher '{}' in Lua mode, attempting generic format",
dispatcher);
return "/dispatch hl.dsp." + dispatcher + "(\"" + arg + "\")";
}
std::string IPC::dispatch(const std::string& dispatcher, const std::string& arg) {
if (isLuaProtocol()) {
return getSocket1Reply(buildLuaDispatch(dispatcher, arg));
}
// Legacy format: "dispatch <dispatcher> <arg>"
std::string cmd = "dispatch " + dispatcher;
if (!arg.empty()) {
cmd += " " + arg;
}
return getSocket1Reply(cmd);
}
} // namespace waybar::modules::hyprland
+37 -5
View File
@@ -48,12 +48,39 @@ auto Language::update() -> void {
fmt::arg("shortDescription", layout_.short_description),
fmt::arg("variant", layout_.variant)));
}
spdlog::debug("hyprland language formatted layout name {}", layoutName);
std::string tooltipContent = std::string{};
bool tooltip_enabled = tooltipEnabled();
if (tooltip_enabled) {
if (config_.isMember("tooltip-format")) {
auto tooltip_format = config_["tooltip-format"].asString();
if (config_.isMember("tooltip-format-" + layout_.short_description + "-" + layout_.variant)) {
const auto propName = "tooltip-format-" + layout_.short_description + "-" + layout_.variant;
tooltipContent = fmt::format(fmt::runtime(tooltip_format), config_[propName].asString());
} else if (config_.isMember("tooltip-format-" + layout_.short_description)) {
const auto propName = "tooltip-format-" + layout_.short_description;
tooltipContent = fmt::format(fmt::runtime(tooltip_format), config_[propName].asString());
} else {
tooltipContent =
trim(fmt::format(fmt::runtime(tooltip_format), fmt::arg("long", layout_.full_name),
fmt::arg("short", layout_.short_name),
fmt::arg("shortDescription", layout_.short_description),
fmt::arg("variant", layout_.variant)));
}
} else {
// if no tooltip format is provided, use the same text as the module
tooltipContent = layoutName;
}
spdlog::debug("hyprland language formatted tooltip content {}", tooltipContent);
}
if (!format_.empty()) {
label_.show();
label_.set_markup(layoutName);
if (tooltip_enabled) {
label_.set_tooltip_markup(tooltipContent);
}
} else {
label_.hide();
}
@@ -74,8 +101,6 @@ void Language::onEvent(const std::string& ev) {
spdlog::warn("hyprland language received malformed event payload: {}", ev);
return;
}
std::string kbName = payload.substr(0, kbSeparator);
// Last comma before variants parenthesis, eg:
// activelayout>>micro-star-int'l-co.,-ltd.-msi-gk50-elite-gaming-keyboard,English (US, intl.,
// with dead keys)
@@ -93,8 +118,15 @@ void Language::onEvent(const std::string& ev) {
}
auto layoutName = payload.substr(layoutSeparator + 1);
if (config_.isMember("keyboard-name") && kbName != config_["keyboard-name"].asString())
return; // ignore
if (config_.isMember("keyboard-name")) {
const auto keyboardName = config_["keyboard-name"].asString();
// The keyboard name itself can contain commas, so match it as a full prefix
// (followed by the ',' separator) rather than comparing against the substring
// before the first comma, which would truncate such names and drop the event.
if (payload.size() <= keyboardName.size() || payload[keyboardName.size()] != ',' ||
payload.compare(0, keyboardName.size(), keyboardName) != 0)
return; // ignore
}
layoutName = waybar::util::sanitize_string(layoutName);
+15 -13
View File
@@ -1,5 +1,6 @@
#include "modules/hyprland/window.hpp"
#include <fmt/format.h>
#include <glibmm/fileutils.h>
#include <glibmm/keyfile.h>
#include <glibmm/miscutils.h>
@@ -53,8 +54,15 @@ auto Window::update() -> void {
std::string label_text;
if (!format_.empty()) {
label_.show();
// If the focused window name is empty and fallback is configured, use fallback text
std::string displayTitle = windowName;
if (displayTitle.empty() && config_["fallback"].isString()) {
displayTitle = config_["fallback"].asString();
}
label_text = waybar::util::rewriteString(
fmt::format(fmt::runtime(format_), fmt::arg("title", windowName),
fmt::format(fmt::runtime(format_), fmt::arg("title", displayTitle),
fmt::arg("initialTitle", windowData_.initial_title),
fmt::arg("class", windowData_.class_name),
fmt::arg("initialClass", windowData_.initial_class_name)),
@@ -108,21 +116,14 @@ auto Window::update() -> void {
AAppIconLabel::update();
}
auto Window::getActiveWorkspace() -> Workspace {
const auto workspace = IPC::inst().getSocket1JsonReply("activeworkspace");
if (workspace.isObject()) {
return Workspace::parse(workspace);
}
return {};
}
auto Window::getActiveWorkspace() -> Workspace { return getActiveWorkspace(""); }
auto Window::getActiveWorkspace(const std::string& monitorName) -> Workspace {
const auto monitors = IPC::inst().getSocket1JsonReply("monitors");
if (monitors.isArray()) {
auto monitor = std::ranges::find_if(
monitors, [&](const Json::Value& monitor) { return monitor["name"] == monitorName; });
auto monitor = std::ranges::find_if(monitors, [&](const Json::Value& monitor) {
return monitorName.empty() ? monitor["focused"].asBool() : monitor["name"] == monitorName;
});
if (monitor == std::end(monitors)) {
spdlog::warn("Monitor not found: {}", monitorName);
return Workspace{
@@ -132,7 +133,8 @@ auto Window::getActiveWorkspace(const std::string& monitorName) -> Workspace {
.last_window_title = "",
};
}
const int id = (*monitor)["activeWorkspace"]["id"].asInt();
const int special_id = (*monitor)["specialWorkspace"]["id"].asInt();
const int id = special_id != 0 ? special_id : (*monitor)["activeWorkspace"]["id"].asInt();
const auto workspaces = IPC::inst().getSocket1JsonReply("workspaces");
if (workspaces.isArray()) {
+153 -19
View File
@@ -1,5 +1,6 @@
#include <json/value.h>
#include <spdlog/spdlog.h>
#include <glibmm/main.h>
#include <memory>
#include <string>
@@ -30,6 +31,11 @@ Workspace::Workspace(const Json::Value& workspace_data, Workspaces& workspace_ma
}
m_button.add_events(Gdk::BUTTON_PRESS_MASK);
m_button.add_events(Gdk::ENTER_NOTIFY_MASK | Gdk::LEAVE_NOTIFY_MASK);
m_button.signal_enter_notify_event().connect(sigc::mem_fun(*this, &Workspace::handleEnter));
m_button.signal_leave_notify_event().connect(sigc::mem_fun(*this, &Workspace::handleLeave));
m_button.signal_button_press_event().connect(sigc::mem_fun(*this, &Workspace::handleClicked),
false);
@@ -45,6 +51,12 @@ Workspace::Workspace(const Json::Value& workspace_data, Workspaces& workspace_ma
initializeWindowMap(clients_data);
}
Workspace::~Workspace() {
// Disconnect the hover-check timeout so it can't fire on this destroyed
// instance (Workspaces are removed at runtime while a check may be armed).
stopHoverCheck();
}
void addOrRemoveClass(const Glib::RefPtr<Gtk::StyleContext>& context, bool condition,
const std::string& class_name) {
if (condition) {
@@ -66,25 +78,121 @@ std::optional<WindowRepr> Workspace::closeWindow(WindowAddress const& addr) {
return std::nullopt;
}
bool Workspace::pointerInsideButton() {
auto display = Gdk::Display::get_default();
if (!display) {
return false;
}
auto seat = display->get_default_seat();
if (!seat) {
return false;
}
auto pointer = seat->get_pointer();
if (!pointer) {
return false;
}
Glib::RefPtr<Gdk::Screen> screen;
int pointerRootX = 0;
int pointerRootY = 0;
pointer->get_position(screen, pointerRootX, pointerRootY);
Gtk::Widget* toplevel = m_button.get_toplevel();
if (toplevel == nullptr || !toplevel->get_window()) {
return false;
}
int buttonX = 0;
int buttonY = 0;
if (!m_button.translate_coordinates(*toplevel, 0, 0, buttonX, buttonY)) {
return false;
}
int windowRootX = 0;
int windowRootY = 0;
toplevel->get_window()->get_root_origin(windowRootX, windowRootY);
const auto allocation = m_button.get_allocation();
const int buttonRootX = windowRootX + buttonX;
const int buttonRootY = windowRootY + buttonY;
const int buttonWidth = allocation.get_width();
const int buttonHeight = allocation.get_height();
return pointerRootX >= buttonRootX && pointerRootY >= buttonRootY &&
pointerRootX < buttonRootX + buttonWidth &&
pointerRootY < buttonRootY + buttonHeight;
}
bool Workspace::syncHoverClass() {
auto styleContext = m_button.get_style_context();
if (pointerInsideButton()) {
styleContext->add_class("workspace-hover");
return true;
}
styleContext->remove_class("workspace-hover");
stopHoverCheck();
return false;
}
void Workspace::startHoverCheck() {
if (m_hoverCheckConnection.connected()) {
return;
}
m_hoverCheckConnection = Glib::signal_timeout().connect(
sigc::mem_fun(*this, &Workspace::syncHoverClass),
50);
}
void Workspace::stopHoverCheck() {
if (m_hoverCheckConnection.connected()) {
m_hoverCheckConnection.disconnect();
}
}
bool Workspace::handleEnter(GdkEventCrossing* /*event*/) {
m_button.get_style_context()->add_class("workspace-hover");
startHoverCheck();
return false;
}
bool Workspace::handleLeave(GdkEventCrossing* /*event*/) {
/*
* Do not remove immediately.
* Workspace taskbar children can fire misleading leave events while the
* pointer is still visually inside the workspace button.
*
* The polling check will remove the class once the pointer really leaves.
*/
startHoverCheck();
return false;
}
bool Workspace::handleClicked(GdkEventButton* bt) const {
if (bt->type == GDK_BUTTON_PRESS) {
try {
if (id() > 0) { // normal
if (m_workspaceManager.moveToMonitor()) {
m_ipc.getSocket1Reply("dispatch focusworkspaceoncurrentmonitor " + std::to_string(id()));
IPC::dispatch("focusworkspaceoncurrentmonitor", std::to_string(id()));
} else {
m_ipc.getSocket1Reply("dispatch workspace " + std::to_string(id()));
IPC::dispatch("workspace", std::to_string(id()));
}
} else if (!isSpecial()) { // named (this includes persistent)
if (m_workspaceManager.moveToMonitor()) {
m_ipc.getSocket1Reply("dispatch focusworkspaceoncurrentmonitor name:" + name());
IPC::dispatch("focusworkspaceoncurrentmonitor", "name:" + name());
} else {
m_ipc.getSocket1Reply("dispatch workspace name:" + name());
IPC::dispatch("workspace", "name:" + name());
}
} else if (id() != -99) { // named special
m_ipc.getSocket1Reply("dispatch togglespecialworkspace " + name());
IPC::dispatch("togglespecialworkspace", name());
} else { // special
m_ipc.getSocket1Reply("dispatch togglespecialworkspace");
IPC::dispatch("togglespecialworkspace", "");
}
return true;
} catch (const std::exception& e) {
@@ -115,7 +223,11 @@ void Workspace::setActiveWindow(WindowAddress const& addr) {
}
auto activeWindowPos = m_workspaceManager.activeWindowPosition();
if (activeIdx.has_value() && activeWindowPos != Workspaces::ActiveWindowPosition::NONE) {
const bool has_active_window =
activeIdx.has_value() &&
activeWindowPos != Workspaces::ActiveWindowPosition::NONE;
if (has_active_window) {
auto window = std::move(m_windowMap[*activeIdx]);
m_windowMap.erase(m_windowMap.begin() + *activeIdx);
if (activeWindowPos == Workspaces::ActiveWindowPosition::FIRST) {
@@ -130,7 +242,10 @@ void Workspace::insertWindow(WindowCreationPayload create_window_payload) {
if (!create_window_payload.isEmpty(m_workspaceManager)) {
auto repr = create_window_payload.repr(m_workspaceManager);
if (!repr.empty() || m_workspaceManager.enableTaskbar()) {
const bool should_display =
!repr.empty() || m_workspaceManager.enableTaskbar();
if (should_display) {
auto addr = create_window_payload.getAddress();
auto it = std::ranges::find_if(
m_windowMap, [&addr](const auto& window) { return window.address == addr; });
@@ -142,7 +257,7 @@ void Workspace::insertWindow(WindowCreationPayload create_window_payload) {
}
}
}
};
}
bool Workspace::onWindowOpened(WindowCreationPayload const& create_window_payload) {
if (create_window_payload.getWorkspaceName() == name()) {
@@ -166,6 +281,17 @@ std::string& Workspace::selectIcon(std::map<std::string, std::string>& icons_map
}
}
if (isActive() && isSpecial()) {
auto activeIconIt = icons_map.find("active:" + name());
if (activeIconIt != icons_map.end()) {
return activeIconIt->second;
}
auto namedIconIt = icons_map.find(name());
if (namedIconIt != icons_map.end()) {
return namedIconIt->second;
}
}
if (isActive()) {
auto activeNamedIconIt = icons_map.find("active:" + name());
if (activeNamedIconIt != icons_map.end()) {
@@ -249,6 +375,7 @@ void Workspace::update(const std::string& workspace_icon) {
auto styleContext = m_button.get_style_context();
addOrRemoveClass(styleContext, isActive(), "active");
addOrRemoveClass(styleContext, isSpecial(), "special");
addOrRemoveClass(styleContext, isSpecial(), name());
addOrRemoveClass(styleContext, isEmpty(), "empty");
addOrRemoveClass(styleContext, isPersistent(), "persistent");
addOrRemoveClass(styleContext, isUrgent(), "urgent");
@@ -285,7 +412,9 @@ void Workspace::update(const std::string& workspace_icon) {
bool Workspace::isEmpty() const {
auto ignore_list = m_workspaceManager.getIgnoredWindows();
if (ignore_list.empty()) {
const bool no_ignore_rules = ignore_list.empty();
if (no_ignore_rules) {
return m_windows == 0;
}
// If there are windows but they are all ignored, consider the workspace empty
@@ -316,15 +445,18 @@ void Workspace::updateTaskbar(const std::string& workspace_icon) {
auto window_box = Gtk::make_managed<Gtk::Box>(Gtk::ORIENTATION_HORIZONTAL);
window_box->set_tooltip_markup(window_repr.window_title);
window_box->get_style_context()->add_class("taskbar-window");
auto button = Gtk::manage(new Gtk::Button());
button->set_relief(Gtk::RELIEF_NONE);
button->add(*window_box);
button->get_style_context()->add_class("taskbar-window");
if (window_repr.isActive) {
window_box->get_style_context()->add_class("active");
button->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));
button->signal_button_press_event().connect(
sigc::bind(sigc::mem_fun(*this, &Workspace::handleClick), window_repr.address),
false);
}
auto text_before = fmt::format(fmt::runtime(m_workspaceManager.taskbarFormatBefore()),
@@ -349,8 +481,8 @@ void Workspace::updateTaskbar(const std::string& workspace_icon) {
window_box->pack_start(*window_label_after, true, true);
}
m_content.pack_start(*event_box, true, false);
event_box->show_all();
m_content.pack_start(*button, true, false);
button->show_all();
};
if (m_workspaceManager.taskbarReverseDirection()) {
@@ -364,7 +496,9 @@ void Workspace::updateTaskbar(const std::string& workspace_icon) {
}
auto formatAfter = m_workspaceManager.formatAfter();
if (!formatAfter.empty()) {
const bool has_format_after = !formatAfter.empty();
if (has_format_after) {
m_labelAfter.set_markup(fmt::format(fmt::runtime(formatAfter), fmt::arg("id", id()),
fmt::arg("name", name()),
fmt::arg("icon", workspace_icon)));
+16 -5
View File
@@ -50,7 +50,8 @@ void Workspaces::init() {
if (m_scrollEventConnection_.connected()) {
m_scrollEventConnection_.disconnect();
}
if (barScroll()) {
bool hasScrollConfig = config_["on-scroll-up"].isString() || config_["on-scroll-down"].isString();
if (barScroll() || hasScrollConfig) {
auto& window = const_cast<Bar&>(m_bar).window;
window.add_events(Gdk::SCROLL_MASK | Gdk::SMOOTH_SCROLL_MASK);
m_scrollEventConnection_ =
@@ -326,6 +327,10 @@ void Workspaces::loadPersistentWorkspacesFromWorkspaceRules(const Json::Value& c
// 2. the rule's monitor is the current monitor
// 3. no monitor is specified in the rule => assume it needs to be persistent on every monitor
if (allOutputs() || m_bar.output->name == monitor || monitor.empty()) {
// => skip ignore-workspaces even if its a persistent
if(isWorkspaceIgnored(workspace)) {
continue;
}
// => persistent workspace should be shown on this monitor
auto workspaceData = createMonitorWorkspaceData(workspace, m_bar.output->name);
workspaceData["persistent-rule"] = true;
@@ -1188,6 +1193,12 @@ bool Workspaces::handleScroll(GdkEventScroll* e) {
if (gdk_event_get_pointer_emulated((GdkEvent*)e)) {
return false;
}
// Check for custom scroll commands first; delegate to base class
if (config_["on-scroll-up"].isString() || config_["on-scroll-down"].isString()) {
return AModule::handleScroll(e);
}
auto dir = AModule::getScrollDir(e);
if (dir == SCROLL_DIR::NONE) {
return true;
@@ -1195,15 +1206,15 @@ bool Workspaces::handleScroll(GdkEventScroll* e) {
if (dir == SCROLL_DIR::DOWN || dir == SCROLL_DIR::RIGHT) {
if (allOutputs()) {
m_ipc.getSocket1Reply("dispatch workspace e+1");
IPC::dispatch("workspace", "e+1");
} else {
m_ipc.getSocket1Reply("dispatch workspace m+1");
IPC::dispatch("workspace", "m+1");
}
} else if (dir == SCROLL_DIR::UP || dir == SCROLL_DIR::LEFT) {
if (allOutputs()) {
m_ipc.getSocket1Reply("dispatch workspace e-1");
IPC::dispatch("workspace", "e-1");
} else {
m_ipc.getSocket1Reply("dispatch workspace m-1");
IPC::dispatch("workspace", "m-1");
}
}