Compare commits

...

23 Commits

Author SHA1 Message Date
Zander671 e12366d64a Merge branch 'master' of https://github.com/Alexays/Waybar
clang-format / lint (push) Has been cancelled
freebsd / build (push) Has been cancelled
linux / build (c++20, alpine) (push) Has been cancelled
linux / build (c++20, archlinux) (push) Has been cancelled
linux / build (c++20, debian) (push) Has been cancelled
linux / build (c++20, fedora) (push) Has been cancelled
linux / build (c++20, gentoo) (push) Has been cancelled
linux / build (c++20, opensuse) (push) Has been cancelled
Nix-Tests / nix-flake-check (push) Has been cancelled
2026-05-07 22:44:09 -07:00
Alexis Rouillard 05945748dc Merge pull request #5013 from higorprado/fix/hyprland-lua-dispatch-protocol
fix(hyprland/workspaces): adapt dispatch commands for Lua IPC protocol
2026-05-04 23:17:38 +02:00
Alexis Rouillard 4c105f7cd7 Merge pull request #5007 from nschloe/toctou-fixes
Various toctou fixes
2026-05-03 13:27:52 +02:00
Higor Prado 97917db593 test(hyprland): expose dispatch internals for unit tests
Move buildLuaDispatch and isLuaProtocol from private to
protected/public so IPCTestHelper can access them.

Add 7 tests covering all buildLuaDispatch branches, dispatch
error path, and isLuaProtocol cache behavior.
2026-05-02 20:25:43 -03:00
Higor Prado e17c0d9f0a fix(hyprland/workspaces): adapt dispatch commands for Lua IPC protocol
Hyprland 0.54 replaced the text-based dispatch socket protocol with a
Lua-based one. Commands like "dispatch workspace 1" are now interpreted
as invalid Lua (return hl.dispatch(workspace 1)), breaking workspace
clicks and scroll navigation.

Add IPC::dispatch() that probes the running Hyprland on first call and
routes commands through the new hl.dsp Lua API when the Lua protocol is
detected, falling back to the old text format otherwise.
2026-04-29 16:41:11 -03:00
Nico Schlömer 5af324f375 two more toctou bugs 2026-04-27 11:41:09 +02:00
Nico Schlömer 59d09c2c12 fix various toctou bugs in battery.cpp 2026-04-27 11:29:03 +02:00
Alexis Rouillard cca8dc38b6 Merge pull request #4990 from B2krobbery/fix-sni-make-unique
fix(sni): use std::make_unique for Item allocation
2026-04-22 20:50:48 +02:00
B2krobbery 7e9c46e4d1 fix(sni): use std::make_unique for Item allocation 2026-04-16 18:34:39 +05:30
Alexis Rouillard 3a321d934d Merge pull request #4982 from Mrpaoo/bug-fix
fix the io failure for hotplug-in device
2026-04-13 11:02:50 +02:00
yubo ac62754b28 fix the lint problem 2026-04-11 13:26:50 +08:00
yubo 906a589715 fix the io failure for hotplug-in device 2026-04-09 21:20:45 +08:00
Alexis Rouillard 202ae4bd5f Merge pull request #4975 from pix-code/master
memory: feature - add "unit" config option
2026-04-07 12:45:24 +02:00
Alexis Rouillard 6acc94ca37 Merge pull request #4967 from argentite/net-addr-del-fix
Network: fix: delete correct address type
2026-04-07 12:45:07 +02:00
Alexis Rouillard 66c09bc5b7 Merge pull request #4964 from adrianlzt/fix/network-bandwidth-event-race
fix(network): prevent near-zero bandwidth on rapid event-driven updates
2026-04-07 12:41:31 +02:00
Alexis Rouillard c30d4ce3e6 Merge pull request #4970 from B2krobbery/fix-group-module-leak
fix(bar): ensure exception safety for group module allocation
2026-04-07 12:39:42 +02:00
Alexis Rouillard cead1a0543 Merge pull request #4973 from cebem1nt/group-ref-fix
Group ref fix
2026-04-07 12:38:44 +02:00
Duke B fc11789a4f add unit config option to memory module 2026-04-05 19:01:36 -04:00
cebem1nt ae11954398 fix: ensure passing group_config as reference 2026-04-04 22:11:19 -03:00
Visal Vijay 36518e4eca fix(bar): ensure exception safety when creating group modules using std::unique_ptr 2026-04-03 19:47:17 +05:30
Anubhab Ghosh 16886117b3 Network: fix: delete correct address type
Only delete the corresponding address type (IPv4 or IPv6) when an event
about a specific type (AF_INET or AF_INET6) is received

This fixes situations where only one type of the address is deleted (and
possibly added again) but Waybar still thinks the interface is in
"linked" (no IP) state.
2026-04-02 19:54:19 +05:30
cebem1nt 8b1e574063 niri/workspaces: feature - add "hide-empty" config option (#4965) 2026-03-31 17:49:30 -03:00
Adrian Lopez 753294dbf4 fix(network): prevent near-zero bandwidth on rapid event-driven updates
When netlink events (link/addr/route changes) fire between timer
intervals, dp.emit() triggers update() which consumes the byte delta
and resets bandwidth_down_total_. A subsequent timer update sees
near-zero delta, displaying very small bandwidth.

Cache the last computed bandwidth values and skip recalculation
when update() is called within half the interval. Event-driven
updates reuse the cached values instead.

Ultraworked with [Sisyphus](https://github.com/code-yeongyu/oh-my-openagent)

Co-authored-by: Sisyphus <clio-agent@sisyphuslabs.ai>
2026-03-30 19:38:24 +02:00
16 changed files with 350 additions and 127 deletions
+14
View File
@@ -4,6 +4,7 @@
#include <filesystem> #include <filesystem>
#include <list> #include <list>
#include <mutex> #include <mutex>
#include <optional>
#include <string> #include <string>
#include <thread> #include <thread>
#include <utility> #include <utility>
@@ -35,9 +36,22 @@ class IPC {
Json::Value getSocket1JsonReply(const std::string& rq); Json::Value getSocket1JsonReply(const std::string& rq);
static std::filesystem::path getSocketFolder(const char* instanceSig); static std::filesystem::path getSocketFolder(const char* instanceSig);
/// Dispatch a Hyprland command. Automatically uses the correct protocol
/// (legacy text or Lua-based) depending on the running Hyprland version.
static std::string dispatch(const std::string& dispatcher, const std::string& arg);
/// Build a Lua-format dispatch command string.
static std::string buildLuaDispatch(const std::string& dispatcher, const std::string& arg);
protected: protected:
static std::filesystem::path socketFolder_; static std::filesystem::path socketFolder_;
/// Detect whether the running Hyprland uses the Lua-based IPC protocol.
/// Returns true for Hyprland >= 0.54 (Lua config), false for older versions.
static bool isLuaProtocol();
static std::optional<bool> s_luaProtocolDetected_; // cached detection result
private: private:
void socketListener(); void socketListener();
void parseIPC(const std::string&); void parseIPC(const std::string&);
+4
View File
@@ -19,9 +19,13 @@ class Memory : public ALabel {
private: private:
void parseMeminfo(); void parseMeminfo();
static float calc_divisor(const std::string& divisor);
std::unordered_map<std::string, unsigned long> meminfo_; std::unordered_map<std::string, unsigned long> meminfo_;
util::SleeperThread thread_; util::SleeperThread thread_;
std::string unit_;
}; };
} // namespace waybar::modules } // namespace waybar::modules
+2
View File
@@ -70,6 +70,8 @@ class Network : public ALabel {
unsigned long long bandwidth_down_total_{0}; unsigned long long bandwidth_down_total_{0};
unsigned long long bandwidth_up_total_{0}; unsigned long long bandwidth_up_total_{0};
unsigned long long bandwidth_down_prev_{0};
unsigned long long bandwidth_up_prev_{0};
std::chrono::steady_clock::time_point bandwidth_last_sample_time_; std::chrono::steady_clock::time_point bandwidth_last_sample_time_;
std::string state_; std::string state_;
+12 -6
View File
@@ -102,23 +102,29 @@ Addressed by *memory*
default: false ++ default: false ++
Enables this module to consume all left over space dynamically. Enables this module to consume all left over space dynamically.
*unit*: ++
typeof: string ++
default: GiB ++
Used to specify unit for total, swapTotal, used, swapUsed, avail, swapAvail,
and swapState. Accepts B, kB, kiB, MB, MiB, GB, GiB, TB, and TiB.
# FORMAT REPLACEMENTS # FORMAT REPLACEMENTS
*{percentage}*: Percentage of memory in use. *{percentage}*: Percentage of memory in use.
*{swapPercentage}*: Percentage of swap in use. *{swapPercentage}*: Percentage of swap in use.
*{total}*: Amount of total memory available in GiB. *{total}*: Amount of total memory available. Defaults to GiB.
*{swapTotal}*: Amount of total swap available in GiB. *{swapTotal}*: Amount of total swap available. Defaults to GiB.
*{used}*: Amount of used memory in GiB. *{used}*: Amount of used memory. Defaults to GiB.
*{swapUsed}*: Amount of used swap in GiB. *{swapUsed}*: Amount of used swap. Defaults to GiB.
*{avail}*: Amount of available memory in GiB. *{avail}*: Amount of available memory. Defaults to GiB.
*{swapAvail}*: Amount of available swap in GiB. *{swapAvail}*: Amount of available swap. Defaults to GiB.
*{swapState}*: Signals if swap is activated or not *{swapState}*: Signals if swap is activated or not
+3 -3
View File
@@ -33,9 +33,7 @@ std::string toLowerCase(const std::string& input) {
std::optional<std::string> getFileBySuffix(const std::string& dir, const std::string& suffix, std::optional<std::string> getFileBySuffix(const std::string& dir, const std::string& suffix,
bool check_lower_case) { bool check_lower_case) {
if (!std::filesystem::exists(dir)) { try {
return {};
}
for (const auto& entry : std::filesystem::recursive_directory_iterator(dir)) { for (const auto& entry : std::filesystem::recursive_directory_iterator(dir)) {
if (entry.is_regular_file()) { if (entry.is_regular_file()) {
std::string filename = entry.path().filename().string(); std::string filename = entry.path().filename().string();
@@ -49,6 +47,8 @@ std::optional<std::string> getFileBySuffix(const std::string& dir, const std::st
} }
} }
} }
} catch (const std::filesystem::filesystem_error&) {
}
return {}; return {};
} }
+6 -4
View File
@@ -541,13 +541,15 @@ void waybar::Bar::getModules(const Factory& factory, const std::string& pos,
auto vertical = (group != nullptr ? group->getBox().get_orientation() auto vertical = (group != nullptr ? group->getBox().get_orientation()
: box_.get_orientation()) == Gtk::ORIENTATION_VERTICAL; : box_.get_orientation()) == Gtk::ORIENTATION_VERTICAL;
auto group_config = config[ref]; const Json::Value& group_config = config[ref];
if (group_config["modules"].isNull()) { if (group_config["modules"].isNull()) {
spdlog::warn("Group definition '{}' has not been found, group will be hidden", ref); spdlog::warn("Group definition '{}' has not been found, group will be hidden", ref);
} }
auto* group_module = new waybar::Group(id_name, class_name, group_config, vertical); auto group_module = std::make_unique<waybar::Group>(
getModules(factory, ref, group_module); id_name, class_name, group_config, vertical);
module = group_module;
getModules(factory, ref, group_module.get());
module = group_module.release();
} else { } else {
module = factory.makeModule(ref, pos); module = factory.makeModule(ref, pos);
} }
+41 -42
View File
@@ -117,17 +117,16 @@ void waybar::modules::Battery::refreshBatteries() {
if (((bat_defined && dir_name == config_["bat"].asString()) || !bat_defined) && if (((bat_defined && dir_name == config_["bat"].asString()) || !bat_defined) &&
(fs::exists(node.path() / "capacity") || fs::exists(node.path() / "charge_now")) && (fs::exists(node.path() / "capacity") || fs::exists(node.path() / "charge_now")) &&
fs::exists(node.path() / "uevent") && fs::exists(node.path() / "uevent") &&
(fs::exists(node.path() / "status") || bat_compatibility) && (fs::exists(node.path() / "status") || bat_compatibility)) {
fs::exists(node.path() / "type")) {
std::string type; std::string type;
std::ifstream(node.path() / "type") >> type; if (std::ifstream{node.path() / "type"} >> type && !type.compare("Battery")) {
if (!type.compare("Battery")) {
// Ignore non-system power supplies unless explicitly requested // Ignore non-system power supplies unless explicitly requested
if (!bat_defined && fs::exists(node.path() / "scope")) { if (!bat_defined) {
std::string scope; std::string scope;
std::ifstream(node.path() / "scope") >> scope; // for hotplug-in device, access it is always unstable because you may remove the
if (g_ascii_strcasecmp(scope.data(), "device") == 0) { // device anytime so just allow failure happen and do nothing
if (std::ifstream{node.path() / "scope"} >> scope &&
g_ascii_strcasecmp(scope.data(), "device") == 0) {
continue; continue;
} }
} }
@@ -275,11 +274,11 @@ waybar::modules::Battery::getInfos() {
auto bat = item.first; auto bat = item.first;
std::string _status; std::string _status;
/* Check for adapter status if battery is not available */ {
if (!std::ifstream(bat / "status")) { std::ifstream f{bat / "status"};
if (!std::getline(f, _status)) {
std::getline(std::ifstream(adapter_ / "status"), _status); std::getline(std::ifstream(adapter_ / "status"), _status);
} else { }
std::getline(std::ifstream(bat / "status"), _status);
} }
// Some battery will report current and charge in μA/μAh. // Some battery will report current and charge in μA/μAh.
@@ -288,64 +287,64 @@ waybar::modules::Battery::getInfos() {
uint32_t current_now = 0; uint32_t current_now = 0;
int32_t _current_now_int = 0; int32_t _current_now_int = 0;
bool current_now_exists = false; bool current_now_exists = false;
if (fs::exists(bat / "current_now")) { if (std::ifstream current_now_f{bat / "current_now"}) {
current_now_exists = true; current_now_exists = true;
std::ifstream(bat / "current_now") >> _current_now_int; current_now_f >> _current_now_int;
} else if (fs::exists(bat / "current_avg")) { } else if (std::ifstream current_avg_f{bat / "current_avg"}) {
current_now_exists = true; current_now_exists = true;
std::ifstream(bat / "current_avg") >> _current_now_int; current_avg_f >> _current_now_int;
} }
// Documentation ABI allows a negative value when discharging, positive // Documentation ABI allows a negative value when discharging, positive
// value when charging. // value when charging.
current_now = std::abs(_current_now_int); current_now = std::abs(_current_now_int);
if (fs::exists(bat / "time_to_empty_now")) { if (std::ifstream f{bat / "time_to_empty_now"}) {
time_to_empty_now_exists = true; time_to_empty_now_exists = true;
std::ifstream(bat / "time_to_empty_now") >> time_to_empty_now; f >> time_to_empty_now;
} }
if (fs::exists(bat / "time_to_full_now")) { if (std::ifstream f{bat / "time_to_full_now"}) {
time_to_full_now_exists = true; time_to_full_now_exists = true;
std::ifstream(bat / "time_to_full_now") >> time_to_full_now; f >> time_to_full_now;
} }
uint32_t voltage_now = 0; uint32_t voltage_now = 0;
bool voltage_now_exists = false; bool voltage_now_exists = false;
if (fs::exists(bat / "voltage_now")) { if (std::ifstream voltage_now_f{bat / "voltage_now"}) {
voltage_now_exists = true; voltage_now_exists = true;
std::ifstream(bat / "voltage_now") >> voltage_now; voltage_now_f >> voltage_now;
} else if (fs::exists(bat / "voltage_avg")) { } else if (std::ifstream voltage_avg_f{bat / "voltage_avg"}) {
voltage_now_exists = true; voltage_now_exists = true;
std::ifstream(bat / "voltage_avg") >> voltage_now; voltage_avg_f >> voltage_now;
} }
uint32_t charge_full = 0; uint32_t charge_full = 0;
bool charge_full_exists = false; bool charge_full_exists = false;
if (fs::exists(bat / "charge_full")) { if (std::ifstream f{bat / "charge_full"}) {
charge_full_exists = true; charge_full_exists = true;
std::ifstream(bat / "charge_full") >> charge_full; f >> charge_full;
} }
uint32_t charge_full_design = 0; uint32_t charge_full_design = 0;
bool charge_full_design_exists = false; bool charge_full_design_exists = false;
if (fs::exists(bat / "charge_full_design")) { if (std::ifstream f{bat / "charge_full_design"}) {
charge_full_design_exists = true; charge_full_design_exists = true;
std::ifstream(bat / "charge_full_design") >> charge_full_design; f >> charge_full_design;
} }
uint32_t charge_now = 0; uint32_t charge_now = 0;
bool charge_now_exists = false; bool charge_now_exists = false;
if (fs::exists(bat / "charge_now")) { if (std::ifstream f{bat / "charge_now"}) {
charge_now_exists = true; charge_now_exists = true;
std::ifstream(bat / "charge_now") >> charge_now; f >> charge_now;
} }
uint32_t power_now = 0; uint32_t power_now = 0;
int32_t _power_now_int = 0; int32_t _power_now_int = 0;
bool power_now_exists = false; bool power_now_exists = false;
if (fs::exists(bat / "power_now")) { if (std::ifstream f{bat / "power_now"}) {
power_now_exists = true; power_now_exists = true;
std::ifstream(bat / "power_now") >> _power_now_int; f >> _power_now_int;
} }
// Some drivers (example: Qualcomm) exposes use a negative value when // Some drivers (example: Qualcomm) exposes use a negative value when
// discharging, positive value when charging. // discharging, positive value when charging.
@@ -353,28 +352,28 @@ waybar::modules::Battery::getInfos() {
uint32_t energy_now = 0; uint32_t energy_now = 0;
bool energy_now_exists = false; bool energy_now_exists = false;
if (fs::exists(bat / "energy_now")) { if (std::ifstream f{bat / "energy_now"}) {
energy_now_exists = true; energy_now_exists = true;
std::ifstream(bat / "energy_now") >> energy_now; f >> energy_now;
} }
uint32_t energy_full = 0; uint32_t energy_full = 0;
bool energy_full_exists = false; bool energy_full_exists = false;
if (fs::exists(bat / "energy_full")) { if (std::ifstream f{bat / "energy_full"}) {
energy_full_exists = true; energy_full_exists = true;
std::ifstream(bat / "energy_full") >> energy_full; f >> energy_full;
} }
uint32_t energy_full_design = 0; uint32_t energy_full_design = 0;
bool energy_full_design_exists = false; bool energy_full_design_exists = false;
if (fs::exists(bat / "energy_full_design")) { if (std::ifstream f{bat / "energy_full_design"}) {
energy_full_design_exists = true; energy_full_design_exists = true;
std::ifstream(bat / "energy_full_design") >> energy_full_design; f >> energy_full_design;
} }
uint16_t cycleCount = 0; uint16_t cycleCount = 0;
if (fs::exists(bat / "cycle_count")) { if (std::ifstream f{bat / "cycle_count"}) {
std::ifstream(bat / "cycle_count") >> cycleCount; f >> cycleCount;
} }
if (charge_full_design >= largestDesignCapacity) { if (charge_full_design >= largestDesignCapacity) {
largestDesignCapacity = charge_full_design; largestDesignCapacity = charge_full_design;
@@ -404,9 +403,9 @@ waybar::modules::Battery::getInfos() {
} else if (energy_now_exists && energy_full_exists && energy_full != 0) { } else if (energy_now_exists && energy_full_exists && energy_full != 0) {
capacity_exists = true; capacity_exists = true;
capacity = 100 * (uint64_t)energy_now / (uint64_t)energy_full; capacity = 100 * (uint64_t)energy_now / (uint64_t)energy_full;
} else if (fs::exists(bat / "capacity")) { } else if (std::ifstream f{bat / "capacity"}) {
capacity_exists = true; capacity_exists = true;
std::ifstream(bat / "capacity") >> capacity; f >> capacity;
} }
if (!voltage_now_exists) { if (!voltage_now_exists) {
+2 -4
View File
@@ -23,23 +23,21 @@ std::vector<float> waybar::modules::CpuFrequency::parseCpuFrequencies() {
if (frequencies.size() <= 0) { if (frequencies.size() <= 0) {
std::string cpufreq_dir = "/sys/devices/system/cpu/cpufreq"; std::string cpufreq_dir = "/sys/devices/system/cpu/cpufreq";
if (std::filesystem::exists(cpufreq_dir)) { try {
std::vector<std::string> frequency_files = {"/cpuinfo_min_freq", "/cpuinfo_max_freq"}; std::vector<std::string> frequency_files = {"/cpuinfo_min_freq", "/cpuinfo_max_freq"};
for (auto& p : std::filesystem::directory_iterator(cpufreq_dir)) { for (auto& p : std::filesystem::directory_iterator(cpufreq_dir)) {
for (const auto& freq_file : frequency_files) { for (const auto& freq_file : frequency_files) {
std::string freq_file_path = p.path().string() + freq_file; std::string freq_file_path = p.path().string() + freq_file;
if (std::filesystem::exists(freq_file_path)) {
std::string freq_value; std::string freq_value;
std::ifstream freq(freq_file_path); std::ifstream freq(freq_file_path);
if (freq.is_open()) { if (freq.is_open()) {
getline(freq, freq_value); getline(freq, freq_value);
float frequency = std::strtol(freq_value.c_str(), nullptr, 10); float frequency = std::strtol(freq_value.c_str(), nullptr, 10);
frequencies.push_back(frequency / 1000); frequencies.push_back(frequency / 1000);
freq.close();
}
} }
} }
} }
} catch (const std::filesystem::filesystem_error&) {
} }
} }
+67
View File
@@ -13,6 +13,7 @@
#include <cerrno> #include <cerrno>
#include <cstring> #include <cstring>
#include <filesystem> #include <filesystem>
#include <optional>
#include <string> #include <string>
#include "util/scoped_fd.hpp" #include "util/scoped_fd.hpp"
@@ -20,6 +21,7 @@
namespace waybar::modules::hyprland { namespace waybar::modules::hyprland {
std::filesystem::path IPC::socketFolder_; std::filesystem::path IPC::socketFolder_;
std::optional<bool> IPC::s_luaProtocolDetected_;
std::filesystem::path IPC::getSocketFolder(const char* instanceSig) { std::filesystem::path IPC::getSocketFolder(const char* instanceSig) {
static std::mutex folderMutex; static std::mutex folderMutex;
@@ -290,4 +292,69 @@ Json::Value IPC::getSocket1JsonReply(const std::string& rq) {
return parser_.parse(reply); 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", monitor = "current" })
//
// 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 + "\", monitor = \"current\" })";
}
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 } // namespace waybar::modules::hyprland
+6 -6
View File
@@ -71,20 +71,20 @@ bool Workspace::handleClicked(GdkEventButton* bt) const {
try { try {
if (id() > 0) { // normal if (id() > 0) { // normal
if (m_workspaceManager.moveToMonitor()) { if (m_workspaceManager.moveToMonitor()) {
m_ipc.getSocket1Reply("dispatch focusworkspaceoncurrentmonitor " + std::to_string(id())); IPC::dispatch("focusworkspaceoncurrentmonitor", std::to_string(id()));
} else { } else {
m_ipc.getSocket1Reply("dispatch workspace " + std::to_string(id())); IPC::dispatch("workspace", std::to_string(id()));
} }
} else if (!isSpecial()) { // named (this includes persistent) } else if (!isSpecial()) { // named (this includes persistent)
if (m_workspaceManager.moveToMonitor()) { if (m_workspaceManager.moveToMonitor()) {
m_ipc.getSocket1Reply("dispatch focusworkspaceoncurrentmonitor name:" + name()); IPC::dispatch("focusworkspaceoncurrentmonitor", "name:" + name());
} else { } else {
m_ipc.getSocket1Reply("dispatch workspace name:" + name()); IPC::dispatch("workspace", "name:" + name());
} }
} else if (id() != -99) { // named special } else if (id() != -99) { // named special
m_ipc.getSocket1Reply("dispatch togglespecialworkspace " + name()); IPC::dispatch("togglespecialworkspace", name());
} else { // special } else { // special
m_ipc.getSocket1Reply("dispatch togglespecialworkspace"); IPC::dispatch("togglespecialworkspace", "");
} }
return true; return true;
} catch (const std::exception& e) { } catch (const std::exception& e) {
+4 -4
View File
@@ -1195,15 +1195,15 @@ bool Workspaces::handleScroll(GdkEventScroll* e) {
if (dir == SCROLL_DIR::DOWN || dir == SCROLL_DIR::RIGHT) { if (dir == SCROLL_DIR::DOWN || dir == SCROLL_DIR::RIGHT) {
if (allOutputs()) { if (allOutputs()) {
m_ipc.getSocket1Reply("dispatch workspace e+1"); IPC::dispatch("workspace", "e+1");
} else { } else {
m_ipc.getSocket1Reply("dispatch workspace m+1"); IPC::dispatch("workspace", "m+1");
} }
} else if (dir == SCROLL_DIR::UP || dir == SCROLL_DIR::LEFT) { } else if (dir == SCROLL_DIR::UP || dir == SCROLL_DIR::LEFT) {
if (allOutputs()) { if (allOutputs()) {
m_ipc.getSocket1Reply("dispatch workspace e-1"); IPC::dispatch("workspace", "e-1");
} else { } else {
m_ipc.getSocket1Reply("dispatch workspace m-1"); IPC::dispatch("workspace", "m-1");
} }
} }
+46 -20
View File
@@ -6,6 +6,9 @@ waybar::modules::Memory::Memory(const std::string& id, const Json::Value& config
dp.emit(); dp.emit();
thread_.sleep_for(interval_); thread_.sleep_for(interval_);
}; };
if (config["unit"].isString()) {
unit_ = config["unit"].asString();
}
} }
auto waybar::modules::Memory::update() -> void { auto waybar::modules::Memory::update() -> void {
@@ -13,15 +16,15 @@ auto waybar::modules::Memory::update() -> void {
unsigned long memtotal = meminfo_["MemTotal"]; unsigned long memtotal = meminfo_["MemTotal"];
unsigned long swaptotal = 0; unsigned long swaptotal = 0;
if (meminfo_.count("SwapTotal")) { if (meminfo_.contains("SwapTotal")) {
swaptotal = meminfo_["SwapTotal"]; swaptotal = meminfo_["SwapTotal"];
} }
unsigned long memfree; unsigned long memfree;
unsigned long swapfree = 0; unsigned long swapfree = 0;
if (meminfo_.count("SwapFree")) { if (meminfo_.contains("SwapFree")) {
swapfree = meminfo_["SwapFree"]; swapfree = meminfo_["SwapFree"];
} }
if (meminfo_.count("MemAvailable")) { if (meminfo_.contains("MemAvailable")) {
// New kernels (3.4+) have an accurate available memory field. // New kernels (3.4+) have an accurate available memory field.
memfree = meminfo_["MemAvailable"] + meminfo_["zfs_size"]; memfree = meminfo_["MemAvailable"] + meminfo_["zfs_size"];
} else { } else {
@@ -31,18 +34,19 @@ auto waybar::modules::Memory::update() -> void {
} }
if (memtotal > 0 && memfree >= 0) { if (memtotal > 0 && memfree >= 0) {
float total_ram_gigabytes =
0.01 * round(memtotal / 10485.76); // 100*10485.76 = 2^20 = 1024^2 = GiB/KiB
float total_swap_gigabytes = 0.01 * round(swaptotal / 10485.76);
int used_ram_percentage = 100 * (memtotal - memfree) / memtotal; int used_ram_percentage = 100 * (memtotal - memfree) / memtotal;
int used_swap_percentage = 0; int used_swap_percentage = 0;
if (swaptotal) { if ((bool) swaptotal) {
used_swap_percentage = 100 * (swaptotal - swapfree) / swaptotal; used_swap_percentage = 100 * (swaptotal - swapfree) / swaptotal;
} }
float used_ram_gigabytes = 0.01 * round((memtotal - memfree) / 10485.76);
float used_swap_gigabytes = 0.01 * round((swaptotal - swapfree) / 10485.76); float divisor = calc_divisor(unit_);
float available_ram_gigabytes = 0.01 * round(memfree / 10485.76); float total_ram = memtotal / divisor;
float available_swap_gigabytes = 0.01 * round(swapfree / 10485.76); float total_swap = swaptotal / divisor;
float used_ram = (memtotal - memfree) / divisor;
float used_swap = (swaptotal - swapfree) / divisor;
float available_ram = memfree / divisor;
float available_swap = swapfree / divisor;
auto format = format_; auto format = format_;
auto state = getState(used_ram_percentage); auto state = getState(used_ram_percentage);
@@ -58,12 +62,12 @@ auto waybar::modules::Memory::update() -> void {
label_.set_markup(fmt::format( label_.set_markup(fmt::format(
fmt::runtime(format), used_ram_percentage, fmt::runtime(format), used_ram_percentage,
fmt::arg("icon", getIcon(used_ram_percentage, icons)), fmt::arg("icon", getIcon(used_ram_percentage, icons)),
fmt::arg("total", total_ram_gigabytes), fmt::arg("swapTotal", total_swap_gigabytes), fmt::arg("total", total_ram), fmt::arg("swapTotal", total_swap),
fmt::arg("percentage", used_ram_percentage), fmt::arg("percentage", used_ram_percentage),
fmt::arg("swapState", swaptotal == 0 ? "Off" : "On"), fmt::arg("swapState", swaptotal == 0 ? "Off" : "On"),
fmt::arg("swapPercentage", used_swap_percentage), fmt::arg("used", used_ram_gigabytes), fmt::arg("swapPercentage", used_swap_percentage), fmt::arg("used", used_ram),
fmt::arg("swapUsed", used_swap_gigabytes), fmt::arg("avail", available_ram_gigabytes), fmt::arg("swapUsed", used_swap), fmt::arg("avail", available_ram),
fmt::arg("swapAvail", available_swap_gigabytes))); fmt::arg("swapAvail", available_swap)));
} }
if (tooltipEnabled()) { if (tooltipEnabled()) {
@@ -71,14 +75,14 @@ auto waybar::modules::Memory::update() -> void {
auto tooltip_format = config_["tooltip-format"].asString(); auto tooltip_format = config_["tooltip-format"].asString();
label_.set_tooltip_markup(fmt::format( label_.set_tooltip_markup(fmt::format(
fmt::runtime(tooltip_format), used_ram_percentage, fmt::runtime(tooltip_format), used_ram_percentage,
fmt::arg("total", total_ram_gigabytes), fmt::arg("swapTotal", total_swap_gigabytes), fmt::arg("total", total_ram), fmt::arg("swapTotal", total_swap),
fmt::arg("percentage", used_ram_percentage), fmt::arg("percentage", used_ram_percentage),
fmt::arg("swapState", swaptotal == 0 ? "Off" : "On"), fmt::arg("swapState", swaptotal == 0 ? "Off" : "On"),
fmt::arg("swapPercentage", used_swap_percentage), fmt::arg("used", used_ram_gigabytes), fmt::arg("swapPercentage", used_swap_percentage), fmt::arg("used", used_ram),
fmt::arg("swapUsed", used_swap_gigabytes), fmt::arg("avail", available_ram_gigabytes), fmt::arg("swapUsed", used_swap), fmt::arg("avail", available_ram),
fmt::arg("swapAvail", available_swap_gigabytes))); fmt::arg("swapAvail", available_swap)));
} else { } else {
label_.set_tooltip_markup(fmt::format("{:.{}f}GiB used", used_ram_gigabytes, 1)); label_.set_tooltip_markup(fmt::format("{:.{}f}GiB used", used_ram, 1));
} }
} }
} else { } else {
@@ -87,3 +91,25 @@ auto waybar::modules::Memory::update() -> void {
// Call parent update // Call parent update
ALabel::update(); ALabel::update();
} }
float waybar::modules::Memory::calc_divisor(const std::string& divisor) {
if (divisor == "kB") {
return 1.0;
} else if (divisor == "kiB") {
return 1.024;
} else if (divisor == "MB") {
return 1.000 * 1000.0;
} else if (divisor == "MiB") {
return 1.024 * 1024.0;
} else if (divisor == "GB") {
return 1.000 * 1000.0 * 1000.0;
} else if (divisor == "GiB") {
return 1.024 * 1024.0 * 1024.0;
} else if (divisor == "TB") {
return 1.000 * 1000.0 * 1000.0 * 1000.0;
} else if (divisor == "TiB") {
return 1.024 * 1024.0 * 1024.0 * 1024.0;
} else { // default to GiB if it is anything that we don't recongnise
return 1.024 * 1024.0 * 1024.0;
}
}
+23 -4
View File
@@ -280,14 +280,22 @@ auto waybar::modules::Network::update() -> void {
std::string tooltip_format; std::string tooltip_format;
auto now = std::chrono::steady_clock::now(); auto now = std::chrono::steady_clock::now();
auto elapsed_seconds = std::chrono::duration<double>(now - bandwidth_last_sample_time_).count(); auto elapsed_seconds = std::chrono::duration<double>(now - bandwidth_last_sample_time_).count();
auto bandwidth_down = 0ull;
auto bandwidth_up = 0ull;
// Only recalculate bandwidth when enough time has elapsed since the last
// sample. Event-driven dp.emit() calls (link/addr/route changes) can
// trigger update() between timer intervals, which would consume the byte
// delta prematurely and show near-zero bandwidth.
auto min_elapsed = std::chrono::duration<double>(interval_).count() * 0.5;
if (elapsed_seconds >= min_elapsed) {
if (elapsed_seconds <= 0.0) { if (elapsed_seconds <= 0.0) {
elapsed_seconds = std::chrono::duration<double>(interval_).count(); elapsed_seconds = std::chrono::duration<double>(interval_).count();
} }
bandwidth_last_sample_time_ = now; bandwidth_last_sample_time_ = now;
auto bandwidth = readBandwidthUsage(); auto bandwidth = readBandwidthUsage();
auto bandwidth_down = 0ull;
auto bandwidth_up = 0ull;
if (bandwidth.has_value()) { if (bandwidth.has_value()) {
auto down_octets = (*bandwidth).first; auto down_octets = (*bandwidth).first;
auto up_octets = (*bandwidth).second; auto up_octets = (*bandwidth).second;
@@ -297,6 +305,14 @@ auto waybar::modules::Network::update() -> void {
bandwidth_up = up_octets - bandwidth_up_total_; bandwidth_up = up_octets - bandwidth_up_total_;
bandwidth_up_total_ = up_octets; bandwidth_up_total_ = up_octets;
bandwidth_down_prev_ = bandwidth_down;
bandwidth_up_prev_ = bandwidth_up;
}
} else {
bandwidth_down = bandwidth_down_prev_;
bandwidth_up = bandwidth_up_prev_;
elapsed_seconds = std::chrono::duration<double>(interval_).count();
} }
if (!alt_) { if (!alt_) {
@@ -677,12 +693,15 @@ int waybar::modules::Network::handleEvents(struct nl_msg* msg, void* data) {
changed_cidr); changed_cidr);
} }
} else { } else {
if (ifa->ifa_family == AF_INET) {
net->ipaddr_.clear(); net->ipaddr_.clear();
net->ipaddr6_.clear();
net->cidr_ = 0; net->cidr_ = 0;
net->cidr6_ = 0;
net->netmask_.clear(); net->netmask_.clear();
} else if (ifa->ifa_family == AF_INET6) {
net->ipaddr6_.clear();
net->cidr6_ = 0;
net->netmask6_.clear(); net->netmask6_.clear();
}
spdlog::debug("network: {} addr deleted {}/{}", net->ifname_, spdlog::debug("network: {} addr deleted {}/{}", net->ifname_,
inet_ntop(ifa->ifa_family, RTA_DATA(ifa_rta), ipaddr, sizeof(ipaddr)), inet_ntop(ifa->ifa_family, RTA_DATA(ifa_rta), ipaddr, sizeof(ipaddr)),
ifa->ifa_prefixlen); ifa->ifa_prefixlen);
+5
View File
@@ -114,6 +114,11 @@ void Workspaces::doUpdate() {
button.show(); button.show();
else else
button.hide(); button.hide();
} else if (config_["hide-empty"].asBool()) {
if (ws["active_window_id"].isNull() && !ws["is_focused"].asBool())
button.hide();
else
button.show();
} else { } else {
button.show(); button.show();
} }
+5 -3
View File
@@ -178,9 +178,11 @@ void Host::addRegisteredItem(const std::string& service) {
return bus_name == item->bus_name && object_path == item->object_path; return bus_name == item->bus_name && object_path == item->object_path;
}); });
if (it == items_.end()) { if (it == items_.end()) {
items_.emplace_back(new Item( items_.emplace_back(std::make_unique<Item>(
bus_name, object_path, config_, bar_, [this](Item& item) { itemReady(item); }, bus_name, object_path, config_, bar_,
[this](Item& item) { itemInvalidated(item); }, on_update_)); [this](Item& item) { itemReady(item); },
[this](Item& item) { itemInvalidated(item); },
on_update_));
} }
} }
+79
View File
@@ -15,6 +15,10 @@ namespace {
class IPCTestHelper : public hyprland::IPC { class IPCTestHelper : public hyprland::IPC {
public: public:
static void resetSocketFolder() { socketFolder_.clear(); } static void resetSocketFolder() { socketFolder_.clear(); }
static void resetLuaProtocolDetection() { s_luaProtocolDetected_.reset(); }
static void setLuaProtocolDetected(bool value) { s_luaProtocolDetected_ = value; }
using hyprland::IPC::buildLuaDispatch;
using hyprland::IPC::isLuaProtocol;
}; };
std::size_t countOpenFds() { std::size_t countOpenFds() {
@@ -133,3 +137,78 @@ TEST_CASE("getSocket1Reply failure paths do not leak fds", "[getSocket1Reply][fd
REQUIRE(after_connect_failures == baseline); REQUIRE(after_connect_failures == baseline);
} }
#endif #endif
// --- Tests for new Lua IPC dispatch functions ---
TEST_CASE("buildLuaDispatch workspace", "[buildLuaDispatch]") {
SECTION("numeric workspace") {
auto result = IPCTestHelper::buildLuaDispatch("workspace", "1");
REQUIRE(result == "/dispatch hl.dsp.focus({ workspace = \"1\" })");
}
SECTION("named workspace") {
auto result = IPCTestHelper::buildLuaDispatch("workspace", "name:term");
REQUIRE(result == "/dispatch hl.dsp.focus({ workspace = \"name:term\" })");
}
SECTION("relative workspace") {
auto result = IPCTestHelper::buildLuaDispatch("workspace", "e+1");
REQUIRE(result == "/dispatch hl.dsp.focus({ workspace = \"e+1\" })");
}
}
TEST_CASE("buildLuaDispatch focusworkspaceoncurrentmonitor", "[buildLuaDispatch]") {
auto result =
IPCTestHelper::buildLuaDispatch("focusworkspaceoncurrentmonitor", "3");
REQUIRE(
result ==
"/dispatch hl.dsp.focus({ workspace = \"3\", monitor = \"current\" })");
}
TEST_CASE("buildLuaDispatch togglespecialworkspace", "[buildLuaDispatch]") {
SECTION("with name") {
auto result =
IPCTestHelper::buildLuaDispatch("togglespecialworkspace", "scratchpad");
REQUIRE(result ==
"/dispatch hl.dsp.workspace.toggle_special(\"scratchpad\")");
}
SECTION("empty arg") {
auto result =
IPCTestHelper::buildLuaDispatch("togglespecialworkspace", "");
REQUIRE(result == "/dispatch hl.dsp.workspace.toggle_special()");
}
}
TEST_CASE("buildLuaDispatch unknown dispatcher fallback", "[buildLuaDispatch]") {
auto result =
IPCTestHelper::buildLuaDispatch("unknown_dispatcher", "some_arg");
REQUIRE(result ==
"/dispatch hl.dsp.unknown_dispatcher(\"some_arg\")");
}
TEST_CASE("dispatch throws when Hyprland is not running", "[dispatch]") {
unsetenv("HYPRLAND_INSTANCE_SIGNATURE");
IPCTestHelper::resetSocketFolder();
IPCTestHelper::resetLuaProtocolDetection();
CHECK_THROWS(hyprland::IPC::dispatch("workspace", "1"));
}
TEST_CASE("isLuaProtocol uses cached value and avoids socket call",
"[isLuaProtocol]") {
unsetenv("HYPRLAND_INSTANCE_SIGNATURE");
IPCTestHelper::resetSocketFolder();
SECTION("cached false") {
IPCTestHelper::setLuaProtocolDetected(false);
// Should return false without throwing (no socket call needed)
REQUIRE(IPCTestHelper::isLuaProtocol() == false);
}
SECTION("cached true") {
IPCTestHelper::setLuaProtocolDetected(true);
// Should return true without throwing (no socket call needed)
REQUIRE(IPCTestHelper::isLuaProtocol() == true);
}
// Cleanup: reset detection so other tests aren't affected
IPCTestHelper::resetLuaProtocolDetection();
}