Merge branch 'master' into compact-bandwidth

This commit is contained in:
Alexis Rouillard
2026-07-03 22:10:59 +02:00
committed by GitHub
76 changed files with 1271 additions and 452 deletions
+13 -13
View File
@@ -33,21 +33,21 @@ std::string toLowerCase(const std::string& input) {
std::optional<std::string> getFileBySuffix(const std::string& dir, const std::string& suffix,
bool check_lower_case) {
if (!std::filesystem::exists(dir)) {
return {};
}
for (const auto& entry : std::filesystem::recursive_directory_iterator(dir)) {
if (entry.is_regular_file()) {
std::string filename = entry.path().filename().string();
if (filename.size() < suffix.size()) {
continue;
}
if ((filename.compare(filename.size() - suffix.size(), suffix.size(), suffix) == 0) ||
(check_lower_case && filename.compare(filename.size() - suffix.size(), suffix.size(),
toLowerCase(suffix)) == 0)) {
return entry.path().string();
try {
for (const auto& entry : std::filesystem::recursive_directory_iterator(dir)) {
if (entry.is_regular_file()) {
std::string filename = entry.path().filename().string();
if (filename.size() < suffix.size()) {
continue;
}
if ((filename.compare(filename.size() - suffix.size(), suffix.size(), suffix) == 0) ||
(check_lower_case && filename.compare(filename.size() - suffix.size(), suffix.size(),
toLowerCase(suffix)) == 0)) {
return entry.path().string();
}
}
}
} catch (const std::filesystem::filesystem_error&) {
}
return {};
+20
View File
@@ -139,6 +139,26 @@ ALabel::ALabel(const Json::Value& config, const std::string& name, const std::st
auto ALabel::update() -> void { AModule::update(); }
bool ALabel::setLabelMarkup(const Glib::ustring& markup) {
if (last_label_markup_ == markup) {
return false;
}
label_.set_markup(markup);
last_label_markup_ = markup;
return true;
}
bool ALabel::setTooltipMarkup(const Glib::ustring& markup) {
if (last_tooltip_markup_ == markup) {
return false;
}
label_.set_tooltip_markup(markup);
last_tooltip_markup_ = markup;
return true;
}
std::string ALabel::getIcon(uint16_t percentage, const std::string& alt, uint16_t max) {
auto format_icons = config_["format-icons"];
if (format_icons.isObject()) {
+15 -4
View File
@@ -17,10 +17,14 @@ AModule::AModule(const Json::Value& config, const std::string& name, const std::
isTooltip{config_["tooltip"].isBool() ? config_["tooltip"].asBool() : true},
isExpand{config_["expand"].isBool() ? config_["expand"].asBool() : false},
distance_scrolled_y_(0.0),
distance_scrolled_x_(0.0) {
distance_scrolled_x_(0.0),
cursor_timeout_conn_() {
// Configure module action Map
const Json::Value actions{config_["actions"]};
disable_on_sleep_ =
config_["disable-on-sleep"].isBool() ? config_["disable-on-sleep"].asBool() : false;
for (Json::Value::const_iterator it = actions.begin(); it != actions.end(); ++it) {
if (it.key().isString() && it->isString())
if (!eventActionMap_.contains(it.key().asString())) {
@@ -72,8 +76,12 @@ AModule::AModule(const Json::Value& config, const std::string& name, const std::
// Respect user configuration of cursor
if (config_.isMember("cursor")) {
if (config_["cursor"].isBool() && config_["cursor"].asBool()) {
setCursor(Gdk::HAND2);
if (config_["cursor"].isBool()) {
if (config_["cursor"].asBool()) {
setCursor(Gdk::HAND2);
} else {
setCursor(Gdk::ARROW);
}
} else if (config_["cursor"].isInt()) {
setCursor(Gdk::CursorType(config_["cursor"].asInt()));
} else {
@@ -83,6 +91,9 @@ AModule::AModule(const Json::Value& config, const std::string& name, const std::
}
AModule::~AModule() {
if (cursor_timeout_conn_.connected()) {
cursor_timeout_conn_.disconnect();
}
for (const auto& pid : pid_children_) {
if (pid != -1) {
killpg(pid, SIGTERM);
@@ -118,7 +129,7 @@ void AModule::setCursor(Gdk::CursorType const& c) {
} else {
// window may not be accessible yet, in this case,
// schedule another call for setting the cursor in 1 sec
Glib::signal_timeout().connect_seconds(
cursor_timeout_conn_ = Glib::signal_timeout().connect_seconds(
[this, c]() {
setCursor(c);
return false;
+67 -3
View File
@@ -172,6 +172,10 @@ waybar::Bar::Bar(struct waybar_output* w_output, const Json::Value& w_config)
right_.set_spacing(spacing);
}
if (config.isMember("height") && !config["height"].isUInt()) {
spdlog::warn("Invalid type for 'height', expected unsigned integer");
}
height_ = config["height"].isUInt() ? config["height"].asUInt() : 0;
width_ = config["width"].isUInt() ? config["width"].asUInt() : 0;
@@ -263,6 +267,16 @@ waybar::Bar::Bar(struct waybar_output* w_output, const Json::Value& w_config)
window.signal_map_event().connect_notify(sigc::mem_fun(*this, &Bar::onMap));
window.signal_unmap().connect([this]() {
spdlog::debug("Output {} unmapped (DPMS off), suspending modules", output->name);
toggleSuspend(true);
});
window.signal_map().connect([this]() {
spdlog::debug("Output {} mapped (DPMS on), resuming modules", output->name);
toggleSuspend(false);
});
#if HAVE_SWAY
if (auto ipc = config["ipc"]; ipc.isBool() && ipc.asBool()) {
bar_id = Client::inst()->bar_id;
@@ -309,6 +323,19 @@ waybar::Bar::Bar(struct waybar_output* w_output, const Json::Value& w_config)
setupWidgets();
window.show_all();
/*
* If gtk-layer-shell's synchronous wait for the initial configure timed out, show_all() can
* return with a configured but not-yet-presented surface. Kick GTK/layer-shell once control has
* returned to the main loop, when any late initial configure has been dispatched and widgets have
* had a chance to allocate/draw.
*/
Glib::signal_idle().connect(sigc::track_obj([this] {
window.queue_resize();
window.queue_draw();
forceLayerCommit();
return false;
}, *this));
if (spdlog::should_log(spdlog::level::debug)) {
// Unfortunately, this function isn't in the C++ bindings, so we have to call the C version.
char* gtk_tree = gtk_style_context_to_string(
@@ -374,7 +401,14 @@ void waybar::Bar::setMode(const struct bar_mode& mode) {
* gtk-layer-shell schedules a commit on the next frame event in GTK, but this could fail in
* certain scenarios, such as fully occluded bar.
*/
gtk_layer_try_force_commit(gtk_window);
forceLayerCommit();
}
void waybar::Bar::forceLayerCommit() {
auto* gtk_window = window.gobj();
if (gtk_window != nullptr && gtk_widget_get_realized(GTK_WIDGET(gtk_window))) {
gtk_layer_try_force_commit(gtk_window);
}
wl_display_flush(Client::inst()->wl_display);
}
@@ -545,8 +579,8 @@ void waybar::Bar::getModules(const Factory& factory, const std::string& pos,
if (group_config["modules"].isNull()) {
spdlog::warn("Group definition '{}' has not been found, group will be hidden", ref);
}
auto group_module = std::make_unique<waybar::Group>(
id_name, class_name, group_config, vertical);
auto group_module =
std::make_unique<waybar::Group>(id_name, class_name, group_config, vertical);
getModules(factory, ref, group_module.get());
module = group_module.release();
@@ -652,6 +686,17 @@ void waybar::Bar::onConfigure(GdkEventConfigure* ev) {
configureGlobalOffset(ev->width, ev->height);
spdlog::info(BAR_SIZE_MSG, ev->width, ev->height, output->name);
/*
* gtk-layer-shell waits for the compositor's initial configure while realizing the window. On a
* busy compositor (common during session startup) that wait can time out even though the initial
* configure arrives shortly afterwards. In that case GTK may not schedule the frame commit that
* presents the first layer-surface buffer, leaving an otherwise configured bar invisible. Force a
* commit after every configure so late initial configures, and later compositor-driven resizes,
* always result in a submitted surface state.
*/
window.queue_draw();
forceLayerCommit();
}
void waybar::Bar::configureGlobalOffset(int width, int height) {
@@ -696,3 +741,22 @@ void waybar::Bar::configureGlobalOffset(int width, int height) {
void waybar::Bar::onOutputGeometryChanged() {
configureGlobalOffset(window.get_width(), window.get_height());
}
void waybar::Bar::toggleSuspend(bool suspend) {
auto process_modules = [suspend](Gtk::Box& module_box) {
for (auto* widget : module_box.get_children()) {
auto* module = dynamic_cast<waybar::AModule*>(widget);
if (module && module->shouldSuspend()) {
if (suspend) {
module->suspend();
} else {
module->resume();
}
}
}
};
process_modules(left_);
process_modules(center_);
process_modules(right_);
}
+29 -6
View File
@@ -91,11 +91,14 @@ void waybar::Client::handleOutputDone(void* data, struct zxdg_output_v1* /*xdg_o
output.xdg_output.reset();
spdlog::debug("Output detection done: {} ({})", output.name, output.identifier);
auto configs = client->getOutputConfigs(output);
if (!configs.empty()) {
for (const auto& config : configs) {
client->bars.emplace_back(std::make_unique<Bar>(&output, config));
}
client->pending_outputs_.push_back(&output);
if (!client->bars_scheduled_) {
client->bars_scheduled_ = true;
Glib::signal_idle().connect_once([client]() {
client->createBarsBatch();
}, Glib::PRIORITY_HIGH_IDLE);
}
}
} catch (const std::exception& e) {
@@ -103,6 +106,25 @@ void waybar::Client::handleOutputDone(void* data, struct zxdg_output_v1* /*xdg_o
}
}
void waybar::Client::createBarsBatch() {
pending_outputs_.remove_if([this](auto* output) { return std::none_of(outputs_.begin(), outputs_.end(), [&output](const auto& o) { return &o == output; }); });
for (auto* output : pending_outputs_) {
try {
auto configs = getOutputConfigs(*output);
if (!configs.empty()) {
for (const auto& config : configs) {
bars.emplace_back(std::make_unique<Bar>(output, config));
}
}
} catch (const std::exception& e) {
spdlog::warn("Error creating bar: {}", e.what());
}
}
pending_outputs_.clear();
bars_scheduled_ = false;
}
void waybar::Client::handleOutputName(void* data, struct zxdg_output_v1* /*xdg_output*/,
const char* name) {
auto* client = waybar::Client::inst();
@@ -306,9 +328,10 @@ int waybar::Client::main(int argc, char* argv[]) {
}
m_cssFile = getStyle(style_opt);
setupCss(m_cssFile);
m_cssReloadHelper = std::make_unique<CssReloadHelper>(m_cssFile, [&]() { setupCss(m_cssFile); });
m_cssReloadHelper = std::make_unique<CssReloadHelper>(m_cssFile, [&](const std::string& css_file) { setupCss(css_file); });
portal->signal_appearance_changed().connect([&](waybar::Appearance appearance) {
auto css_file = getStyle(style_opt, appearance);
m_cssReloadHelper->changeCssFile(css_file);
setupCss(css_file);
});
+5 -1
View File
@@ -55,9 +55,11 @@ static void catchSignals(waybar::SafeSignal<int>& signal_handler) {
std::signal(SIGINT, writeSignalToPipe);
std::signal(SIGCHLD, writeSignalToPipe);
#ifdef SIGRTMIN
for (int sig = SIGRTMIN + 1; sig <= SIGRTMAX; ++sig) {
std::signal(sig, writeSignalToPipe);
}
#endif
while (true) {
int signum;
@@ -119,13 +121,15 @@ void handleUserSignal(int signal, bool& reload) {
// If this signal should restart or close the bar, this function will write
// `true` or `false`, respectively, into `reload`.
static void handleSignalMainThread(int signum, bool& reload) {
#ifdef SIGRTMIN
if (signum >= SIGRTMIN + 1 && signum <= SIGRTMAX) {
for (auto& bar : waybar::Client::inst()->bars) {
bar->handleSignal(signum);
}
return;
}
#endif
switch (signum) {
case SIGUSR1:
handleUserSignal(SIGUSR1, reload);
+83 -69
View File
@@ -117,24 +117,16 @@ void waybar::modules::Battery::refreshBatteries() {
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() / "uevent") &&
(fs::exists(node.path() / "status") || bat_compatibility) &&
fs::exists(node.path() / "type")) {
(fs::exists(node.path() / "status") || bat_compatibility)) {
std::string type;
std::ifstream(node.path() / "type") >> type;
if (!type.compare("Battery")) {
if (std::ifstream{node.path() / "type"} >> type && !type.compare("Battery")) {
// Ignore non-system power supplies unless explicitly requested
if (!bat_defined && fs::exists(node.path() / "scope")) {
if (!bat_defined) {
std::string scope;
try {
// for hotplug-in device, access it is always unstable because you may remove the
// device anytime so just allow failure happen and do nothing
std::ifstream(node.path() / "scope") >> scope;
} catch (const std::ifstream::failure& e) {
scope.clear();
continue;
}
if (g_ascii_strcasecmp(scope.data(), "device") == 0) {
// for hotplug-in device, access it is always unstable because you may remove the
// 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;
}
}
@@ -161,7 +153,7 @@ void waybar::modules::Battery::refreshBatteries() {
}
}
} catch (fs::filesystem_error& e) {
throw std::runtime_error(e.what());
spdlog::warn("Battery directory tracking failed: {}", e.what());
}
if (warnFirstTime_ && batteries_.empty()) {
if (config_["bat"].isString()) {
@@ -282,11 +274,11 @@ waybar::modules::Battery::getInfos() {
auto bat = item.first;
std::string _status;
/* Check for adapter status if battery is not available */
if (!std::ifstream(bat / "status")) {
std::getline(std::ifstream(adapter_ / "status"), _status);
} else {
std::getline(std::ifstream(bat / "status"), _status);
{
std::ifstream f{bat / "status"};
if (!std::getline(f, _status)) {
std::getline(std::ifstream(adapter_ / "status"), _status);
}
}
// Some battery will report current and charge in μA/μAh.
@@ -295,64 +287,74 @@ waybar::modules::Battery::getInfos() {
uint32_t current_now = 0;
int32_t _current_now_int = 0;
bool current_now_exists = false;
if (fs::exists(bat / "current_now")) {
current_now_exists = true;
std::ifstream(bat / "current_now") >> _current_now_int;
} else if (fs::exists(bat / "current_avg")) {
current_now_exists = true;
std::ifstream(bat / "current_avg") >> _current_now_int;
if (std::ifstream current_now_f{bat / "current_now"}) {
if (current_now_f >> _current_now_int) {
current_now_exists = true;
}
} else if (std::ifstream current_avg_f{bat / "current_avg"}) {
if (current_avg_f >> _current_now_int) {
current_now_exists = true;
}
}
// Documentation ABI allows a negative value when discharging, positive
// value when charging.
current_now = std::abs(_current_now_int);
if (fs::exists(bat / "time_to_empty_now")) {
time_to_empty_now_exists = true;
std::ifstream(bat / "time_to_empty_now") >> time_to_empty_now;
if (std::ifstream f{bat / "time_to_empty_now"}) {
if (f >> time_to_empty_now) {
time_to_empty_now_exists = true;
}
}
if (fs::exists(bat / "time_to_full_now")) {
time_to_full_now_exists = true;
std::ifstream(bat / "time_to_full_now") >> time_to_full_now;
if (std::ifstream f{bat / "time_to_full_now"}) {
if (f >> time_to_full_now) {
time_to_full_now_exists = true;
}
}
uint32_t voltage_now = 0;
bool voltage_now_exists = false;
if (fs::exists(bat / "voltage_now")) {
voltage_now_exists = true;
std::ifstream(bat / "voltage_now") >> voltage_now;
} else if (fs::exists(bat / "voltage_avg")) {
voltage_now_exists = true;
std::ifstream(bat / "voltage_avg") >> voltage_now;
if (std::ifstream voltage_now_f{bat / "voltage_now"}) {
if (voltage_now_f >> voltage_now) {
voltage_now_exists = true;
}
} else if (std::ifstream voltage_avg_f{bat / "voltage_avg"}) {
if (voltage_avg_f >> voltage_now) {
voltage_now_exists = true;
}
}
uint32_t charge_full = 0;
bool charge_full_exists = false;
if (fs::exists(bat / "charge_full")) {
charge_full_exists = true;
std::ifstream(bat / "charge_full") >> charge_full;
if (std::ifstream f{bat / "charge_full"}) {
if (f >> charge_full) {
charge_full_exists = true;
}
}
uint32_t charge_full_design = 0;
bool charge_full_design_exists = false;
if (fs::exists(bat / "charge_full_design")) {
charge_full_design_exists = true;
std::ifstream(bat / "charge_full_design") >> charge_full_design;
if (std::ifstream f{bat / "charge_full_design"}) {
if (f >> charge_full_design) {
charge_full_design_exists = true;
}
}
uint32_t charge_now = 0;
bool charge_now_exists = false;
if (fs::exists(bat / "charge_now")) {
charge_now_exists = true;
std::ifstream(bat / "charge_now") >> charge_now;
if (std::ifstream f{bat / "charge_now"}) {
if (f >> charge_now) {
charge_now_exists = true;
}
}
uint32_t power_now = 0;
int32_t _power_now_int = 0;
bool power_now_exists = false;
if (fs::exists(bat / "power_now")) {
power_now_exists = true;
std::ifstream(bat / "power_now") >> _power_now_int;
if (std::ifstream f{bat / "power_now"}) {
if (f >> _power_now_int) {
power_now_exists = true;
}
}
// Some drivers (example: Qualcomm) exposes use a negative value when
// discharging, positive value when charging.
@@ -360,28 +362,31 @@ waybar::modules::Battery::getInfos() {
uint32_t energy_now = 0;
bool energy_now_exists = false;
if (fs::exists(bat / "energy_now")) {
energy_now_exists = true;
std::ifstream(bat / "energy_now") >> energy_now;
if (std::ifstream f{bat / "energy_now"}) {
if (f >> energy_now) {
energy_now_exists = true;
}
}
uint32_t energy_full = 0;
bool energy_full_exists = false;
if (fs::exists(bat / "energy_full")) {
energy_full_exists = true;
std::ifstream(bat / "energy_full") >> energy_full;
if (std::ifstream f{bat / "energy_full"}) {
if (f >> energy_full) {
energy_full_exists = true;
}
}
uint32_t energy_full_design = 0;
bool energy_full_design_exists = false;
if (fs::exists(bat / "energy_full_design")) {
energy_full_design_exists = true;
std::ifstream(bat / "energy_full_design") >> energy_full_design;
if (std::ifstream f{bat / "energy_full_design"}) {
if (f >> energy_full_design) {
energy_full_design_exists = true;
}
}
uint16_t cycleCount = 0;
if (fs::exists(bat / "cycle_count")) {
std::ifstream(bat / "cycle_count") >> cycleCount;
if (std::ifstream f{bat / "cycle_count"}) {
f >> cycleCount;
}
if (charge_full_design >= largestDesignCapacity) {
largestDesignCapacity = charge_full_design;
@@ -411,9 +416,10 @@ waybar::modules::Battery::getInfos() {
} else if (energy_now_exists && energy_full_exists && energy_full != 0) {
capacity_exists = true;
capacity = 100 * (uint64_t)energy_now / (uint64_t)energy_full;
} else if (fs::exists(bat / "capacity")) {
capacity_exists = true;
std::ifstream(bat / "capacity") >> capacity;
} else if (std::ifstream f{bat / "capacity"}) {
if (f >> capacity) {
capacity_exists = true;
}
}
if (!voltage_now_exists) {
@@ -593,11 +599,12 @@ waybar::modules::Battery::getInfos() {
float calculated_capacity{0.0f};
if (total_capacity_exists) {
if (total_capacity > 0.0f)
if (total_capacity > 0.0f) {
calculated_capacity = (float)total_capacity / batteries_.size();
else if (total_energy_full_exists && total_energy_exists) {
if (total_energy_full > 0.0f)
} else if (total_energy_full_exists && total_energy_exists) {
if (total_energy_full > 0.0f) {
calculated_capacity = ((float)total_energy * 100.0f / (float)total_energy_full);
}
}
}
@@ -694,13 +701,20 @@ auto waybar::modules::Battery::update() -> void {
if (status == "Unknown") {
status = getAdapterStatus(capacity);
}
auto state = getState(capacity, true);
bool adapter_online = false;
if (!adapter_.empty()) {
std::ifstream(adapter_ / "online") >> adapter_online;
}
if (config_["full-at-plugged"].asBool() && status == "Full" && adapter_online) {
status = "Plugged";
}
auto status_pretty = status;
// Transform to lowercase and replace space with dash
std::ranges::transform(status.begin(), status.end(), status.begin(),
[](char ch) { return ch == ' ' ? '-' : std::tolower(ch); });
auto format = format_;
auto state = getState(capacity, true);
processEvents(state, status, capacity);
setBarClass(state);
auto time_remaining_formatted = formatTimeRemaining(time_remaining);
+6 -1
View File
@@ -402,8 +402,13 @@ auto waybar::modules::Clock::get_calendar(const year_month_day& today, const yea
data = g_utf8_find_next_char(data, end);
}
}
// Note: the stream's default fill character is already a space (L' ' on
// libstdc++'s wide FormatStream, ' ' on libc++'s narrow one), so no
// std::setfill is needed. Passing std::setfill(' ')/std::setfill(L' ')
// here is not portable because the fill char type must match the
// FormatStream's char type, which differs between standard libraries.
os << Glib::ustring::format(
(cldWPos_ != WS::LEFT || line == 0) ? std::left : std::right, std::setfill(L' '),
(cldWPos_ != WS::LEFT || line == 0) ? std::left : std::right,
std::setw(cldMonColLen_ + ((line < 2) ? cldWnLen_ - wideCharCount : 0)),
calendarLine);
+8 -3
View File
@@ -58,9 +58,14 @@ auto waybar::modules::Cpu::update() -> void {
label_.set_markup(fmt::vformat(format, store));
if (tooltipEnabled()) {
if (config_["tooltip-format"].isString()) {
tooltip = config_["tooltip-format"].asString();
label_.set_tooltip_markup(fmt::vformat(tooltip, store));
std::string tooltip_format;
if (!state.empty() && config_["tooltip-format-" + state].isString()) {
tooltip_format = config_["tooltip-format-" + state].asString();
} else if (config_["tooltip-format"].isString()) {
tooltip_format = config_["tooltip-format"].asString();
}
if (!tooltip_format.empty()) {
label_.set_tooltip_markup(fmt::vformat(tooltip_format, store));
} else {
label_.set_tooltip_markup(tooltip);
}
+8 -10
View File
@@ -23,23 +23,21 @@ std::vector<float> waybar::modules::CpuFrequency::parseCpuFrequencies() {
if (frequencies.size() <= 0) {
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"};
for (auto& p : std::filesystem::directory_iterator(cpufreq_dir)) {
for (const auto& freq_file : frequency_files) {
std::string freq_file_path = p.path().string() + freq_file;
if (std::filesystem::exists(freq_file_path)) {
std::string freq_value;
std::ifstream freq(freq_file_path);
if (freq.is_open()) {
getline(freq, freq_value);
float frequency = std::strtol(freq_value.c_str(), nullptr, 10);
frequencies.push_back(frequency / 1000);
freq.close();
}
std::string freq_value;
std::ifstream freq(freq_file_path);
if (freq.is_open()) {
getline(freq, freq_value);
float frequency = std::strtol(freq_value.c_str(), nullptr, 10);
frequencies.push_back(frequency / 1000);
}
}
}
} catch (const std::filesystem::filesystem_error&) {
}
}
+7
View File
@@ -39,6 +39,11 @@ waybar::modules::Custom::~Custom() {
}
void waybar::modules::Custom::delayWorker() {
if (!config_["exec"].isString() && !config_["exec-if"].isString()) {
dp.emit();
return;
}
thread_ = [this] {
for (int i : this->pid_children_) {
int status;
@@ -138,9 +143,11 @@ void waybar::modules::Custom::waitingWorker() {
}
void waybar::modules::Custom::refresh(int sig) {
#ifdef SIGRTMIN
if (config_["signal"].isInt() && sig == SIGRTMIN + config_["signal"].asInt()) {
thread_.wake_up();
}
#endif
}
void waybar::modules::Custom::handleEvent() {
+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()) {
@@ -161,6 +276,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 activeIconIt = icons_map.find("active");
if (activeIconIt != icons_map.end()) {
@@ -234,6 +360,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");
@@ -270,7 +397,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
@@ -301,15 +430,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()),
@@ -334,8 +466,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()) {
@@ -349,7 +481,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");
}
}
+2
View File
@@ -46,9 +46,11 @@ void waybar::modules::Image::delayWorker() {
}
void waybar::modules::Image::refresh(int sig) {
#ifdef SIGRTMIN
if (config_["signal"].isInt() && sig == SIGRTMIN + config_["signal"].asInt()) {
thread_.wake_up();
}
#endif
}
auto waybar::modules::Image::update() -> void {
+20 -24
View File
@@ -1,5 +1,18 @@
#include "modules/memory.hpp"
namespace {
const std::unordered_map<std::string, float> kUnits = {
{"kB", 1.000},
{"kiB", 1.024},
{"MB", 1.000 * 1000.0},
{"MiB", 1.024 * 1024.0},
{"GB", 1.000 * 1000.0 * 1000.0},
{"GiB", 1.024 * 1024.0 * 1024.0},
{"TB", 1.000 * 1000.0 * 1000.0 * 1000.0},
{"TiB", 1.024 * 1024.0 * 1024.0 * 1024.0}
};
}
waybar::modules::Memory::Memory(const std::string& id, const Json::Value& config)
: ALabel(config, "memory", id, "{}%", 30) {
thread_ = [this] {
@@ -8,6 +21,11 @@ waybar::modules::Memory::Memory(const std::string& id, const Json::Value& config
};
if (config["unit"].isString()) {
unit_ = config["unit"].asString();
if (!kUnits.contains(unit_)) {
unit_ = "GiB";
}
} else {
unit_ = "GiB";
}
}
@@ -40,7 +58,7 @@ auto waybar::modules::Memory::update() -> void {
used_swap_percentage = 100 * (swaptotal - swapfree) / swaptotal;
}
float divisor = calc_divisor(unit_);
float divisor = kUnits.at(unit_);
float total_ram = memtotal / divisor;
float total_swap = swaptotal / divisor;
float used_ram = (memtotal - memfree) / divisor;
@@ -82,7 +100,7 @@ auto waybar::modules::Memory::update() -> void {
fmt::arg("swapUsed", used_swap), fmt::arg("avail", available_ram),
fmt::arg("swapAvail", available_swap)));
} else {
label_.set_tooltip_markup(fmt::format("{:.{}f}GiB used", used_ram, 1));
label_.set_tooltip_markup(fmt::format("{:.{}f}{} used", used_ram, 1, unit_));
}
}
} else {
@@ -91,25 +109,3 @@ auto waybar::modules::Memory::update() -> void {
// Call parent 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;
}
}
+51 -40
View File
@@ -6,6 +6,7 @@
#include <system_error>
#include <util/sanitize_str.hpp>
#include <util/utf8_string.hpp>
using namespace waybar::util;
#include "modules/mpd/state.hpp"
@@ -24,7 +25,8 @@ waybar::modules::MPD::MPD(const std::string& id, const Json::Value& config)
timeout_(config_["timeout"].isUInt() ? config_["timeout"].asUInt() * 1'000 : 30'000),
connection_(nullptr, &mpd_connection_free),
status_(nullptr, &mpd_status_free),
song_(nullptr, &mpd_song_free) {
song_(nullptr, &mpd_song_free),
ellipsis_(config_["ellipsis"].isString() ? config_["ellipsis"].asString() : "\u2026") {
if (!config_["port"].isNull() && !config_["port"].isUInt()) {
spdlog::warn("{}: `port` configuration should be an unsigned int", module_name_);
}
@@ -51,21 +53,6 @@ auto waybar::modules::MPD::update() -> void {
ALabel::update();
}
void waybar::modules::MPD::queryMPD() {
if (connection_ != nullptr) {
spdlog::trace("{}: fetching state information", module_name_);
try {
fetchState();
spdlog::trace("{}: fetch complete", module_name_);
} catch (std::exception const& e) {
spdlog::error("{}: {}", module_name_, e.what());
state_ = MPD_STATE_UNKNOWN;
}
dp.emit();
}
}
std::string waybar::modules::MPD::getTag(mpd_tag_type type, unsigned idx) const {
std::string result =
config_["unknown-tag"].isString() ? config_["unknown-tag"].asString() : "N/A";
@@ -98,7 +85,7 @@ void waybar::modules::MPD::setLabel() {
? config_["format-disconnected"].asString()
: "disconnected";
if (format.empty()) {
label_.set_markup(format);
setLabelMarkup(format);
label_.show();
} else {
label_.hide();
@@ -110,22 +97,22 @@ void waybar::modules::MPD::setLabel() {
? config_["tooltip-format-disconnected"].asString()
: "MPD (disconnected)";
// Nothing to format
label_.set_tooltip_markup(tooltip_format);
setTooltipMarkup(tooltip_format);
}
return;
}
label_.get_style_context()->remove_class("disconnected");
auto format = format_;
Glib::ustring artist, album_artist, album, title;
std::string date, filename, uri;
int song_pos = 0, queue_length = 0, volume = 0;
std::chrono::seconds elapsedTime, totalTime;
std::string stateIcon = "";
bool no_song = song_.get() == nullptr;
if (stopped() || no_song) {
if (no_song) spdlog::warn("Bug in mpd: no current song but state is not stopped.");
bool is_stopped = stopped();
if (is_stopped || no_song) {
if (no_song && !is_stopped) spdlog::warn("mpd: no current song while state is not stopped");
format =
config_["format-stopped"].isString() ? config_["format-stopped"].asString() : "stopped";
label_.get_style_context()->add_class("stopped");
@@ -137,18 +124,15 @@ void waybar::modules::MPD::setLabel() {
label_.get_style_context()->add_class("playing");
label_.get_style_context()->remove_class("paused");
} else if (paused()) {
format = config_["format-paused"].isString() ? config_["format-paused"].asString()
: config_["format"].asString();
if (config_["format-paused"].isString()) {
format = config_["format-paused"].asString();
}
label_.get_style_context()->add_class("paused");
label_.get_style_context()->remove_class("playing");
}
stateIcon = getStateIcon();
artist = sanitize_string(getTag(MPD_TAG_ARTIST));
album_artist = sanitize_string(getTag(MPD_TAG_ALBUM_ARTIST));
album = sanitize_string(getTag(MPD_TAG_ALBUM));
title = sanitize_string(getTag(MPD_TAG_TITLE));
date = sanitize_string(getTag(MPD_TAG_DATE));
filename = sanitize_string(getFilename());
uri = mpd_song_get_uri(song_.get());
@@ -170,17 +154,12 @@ void waybar::modules::MPD::setLabel() {
std::string repeatIcon = getOptionIcon("repeat", repeatActivated);
bool singleActivated = mpd_status_get_single(status_.get());
std::string singleIcon = getOptionIcon("single", singleActivated);
if (config_["artist-len"].isInt()) artist = artist.substr(0, config_["artist-len"].asInt());
if (config_["album-artist-len"].isInt())
album_artist = album_artist.substr(0, config_["album-artist-len"].asInt());
if (config_["album-len"].isInt()) album = album.substr(0, config_["album-len"].asInt());
if (config_["title-len"].isInt()) title = title.substr(0, config_["title-len"].asInt());
try {
auto text = fmt::format(
fmt::runtime(format), fmt::arg("artist", artist.raw()),
fmt::arg("albumArtist", album_artist.raw()), fmt::arg("album", album.raw()),
fmt::arg("title", title.raw()), fmt::arg("date", date), fmt::arg("volume", volume),
fmt::runtime(format), fmt::arg("artist", getArtistStr(true)),
fmt::arg("albumArtist", getAlbumArtistStr(true)), fmt::arg("album", getAlbumStr(true)),
fmt::arg("title", getTitleStr(true)), fmt::arg("date", date), fmt::arg("volume", volume),
fmt::arg("elapsedTime", elapsedTime), fmt::arg("totalTime", totalTime),
fmt::arg("songPosition", song_pos), fmt::arg("queueLength", queue_length),
fmt::arg("stateIcon", stateIcon), fmt::arg("consumeIcon", consumeIcon),
@@ -190,7 +169,7 @@ void waybar::modules::MPD::setLabel() {
label_.hide();
} else {
label_.show();
label_.set_markup(text);
setLabelMarkup(text);
}
} catch (fmt::format_error const& e) {
spdlog::warn("mpd: format error: {}", e.what());
@@ -202,15 +181,15 @@ void waybar::modules::MPD::setLabel() {
: "MPD (connected)";
try {
auto tooltip_text = fmt::format(
fmt::runtime(tooltip_format), fmt::arg("artist", artist.raw()),
fmt::arg("albumArtist", album_artist.raw()), fmt::arg("album", album.raw()),
fmt::arg("title", title.raw()), fmt::arg("date", date), fmt::arg("volume", volume),
fmt::runtime(tooltip_format), fmt::arg("artist", getArtistStr(false)),
fmt::arg("albumArtist", getAlbumArtistStr(false)), fmt::arg("album", getAlbumStr(false)),
fmt::arg("title", getTitleStr(false)), fmt::arg("date", date), fmt::arg("volume", volume),
fmt::arg("elapsedTime", elapsedTime), fmt::arg("totalTime", totalTime),
fmt::arg("songPosition", song_pos), fmt::arg("queueLength", queue_length),
fmt::arg("stateIcon", stateIcon), fmt::arg("consumeIcon", consumeIcon),
fmt::arg("randomIcon", randomIcon), fmt::arg("repeatIcon", repeatIcon),
fmt::arg("singleIcon", singleIcon), fmt::arg("filename", filename), fmt::arg("uri", uri));
label_.set_tooltip_markup(tooltip_text);
setTooltipMarkup(tooltip_text);
} catch (fmt::format_error const& e) {
spdlog::warn("mpd: format error (tooltip): {}", e.what());
}
@@ -257,6 +236,38 @@ std::string waybar::modules::MPD::getOptionIcon(const std::string& optionName,
}
}
std::string waybar::modules::MPD::getArtistStr(bool truncated) const {
std::string artist = getTag(MPD_TAG_ARTIST);
if (truncated && config_["artist-len"].isInt()) {
waybar::util::utf8_truncate(artist, ellipsis_, config_["artist-len"].asInt());
}
return sanitize_string(artist);
}
std::string waybar::modules::MPD::getAlbumArtistStr(bool truncated) const {
std::string album_artist = getTag(MPD_TAG_ALBUM_ARTIST);
if (truncated && config_["album-artist-len"].isInt()) {
waybar::util::utf8_truncate(album_artist, ellipsis_, config_["album-artist-len"].asInt());
}
return sanitize_string(album_artist);
}
std::string waybar::modules::MPD::getAlbumStr(bool truncated) const {
std::string album = getTag(MPD_TAG_ALBUM);
if (truncated && config_["album-len"].isInt()) {
waybar::util::utf8_truncate(album, ellipsis_, config_["album-len"].asInt());
}
return sanitize_string(album);
}
std::string waybar::modules::MPD::getTitleStr(bool truncated) const {
std::string title = getTag(MPD_TAG_TITLE);
if (truncated && config_["title-len"].isInt()) {
waybar::util::utf8_truncate(title, ellipsis_, config_["title-len"].asInt());
}
return sanitize_string(title);
}
static bool isServerUnavailable(const std::error_code& ec) {
if (ec.category() == std::system_category()) {
switch (ec.value()) {
-1
View File
@@ -152,7 +152,6 @@ bool Playing::on_timer() {
return false;
}
ctx_->queryMPD();
ctx_->emit();
} catch (std::exception const& e) {
spdlog::warn("mpd: Playing: error: {}", e.what());
+14 -57
View File
@@ -7,6 +7,7 @@
#include <string>
#include "util/scope_guard.hpp"
#include "util/utf8_string.hpp"
extern "C" {
#include <playerctl/playerctl.h>
@@ -202,69 +203,22 @@ auto Mpris::getIconFromJson(const Json::Value& icons, const std::string& key) ->
// Wide characters count as two, zero-width characters count as zero
// Modifies str in-place (unless width = std::string::npos)
// Returns the total width of the string pre-truncating
size_t utf8_truncate(std::string& str, size_t width = std::string::npos) {
if (str.length() == 0) return 0;
const gchar* trunc_end = nullptr;
size_t total_width = 0;
for (gchar *data = str.data(), *end = data + str.size(); data != nullptr;) {
gunichar c = g_utf8_get_char_validated(data, end - data);
if (c == -1U || c == -2U) {
// invalid unicode, treat string as ascii
if (width != std::string::npos && str.length() > width) str.resize(width);
return str.length();
} else if (g_unichar_iswide(c)) {
total_width += 2;
} else if (!g_unichar_iszerowidth(c) && c != 0xAD) { // neither zero-width nor soft hyphen
total_width += 1;
}
data = g_utf8_find_next_char(data, end);
if (width != std::string::npos && total_width <= width && !g_unichar_isspace(c))
trunc_end = data;
}
if (trunc_end) str.resize(trunc_end - str.data());
return total_width;
}
size_t utf8_width(const std::string& str) { return utf8_truncate(const_cast<std::string&>(str)); }
void truncate(std::string& s, const std::string& ellipsis, size_t max_len) {
if (max_len == 0) {
s.resize(0);
return;
}
size_t len = utf8_truncate(s, max_len);
if (len > max_len) {
size_t ellipsis_len = utf8_width(ellipsis);
if (max_len >= ellipsis_len) {
if (ellipsis_len) utf8_truncate(s, max_len - ellipsis_len);
s += ellipsis;
} else {
s.resize(0);
}
}
}
auto Mpris::getArtistStr(const PlayerInfo& info, bool truncated) -> std::string {
auto artist = info.artist.value_or(std::string());
if (truncated && artist_len_ >= 0) truncate(artist, ellipsis_, artist_len_);
if (truncated && artist_len_ >= 0) waybar::util::utf8_truncate(artist, ellipsis_, artist_len_);
return artist;
}
auto Mpris::getAlbumStr(const PlayerInfo& info, bool truncated) -> std::string {
auto album = info.album.value_or(std::string());
if (truncated && album_len_ >= 0) truncate(album, ellipsis_, album_len_);
if (truncated && album_len_ >= 0) waybar::util::utf8_truncate(album, ellipsis_, album_len_);
return album;
}
auto Mpris::getTitleStr(const PlayerInfo& info, bool truncated) -> std::string {
auto title = info.title.value_or(std::string());
if (truncated && title_len_ >= 0) truncate(title, ellipsis_, title_len_);
if (truncated && title_len_ >= 0) waybar::util::utf8_truncate(title, ellipsis_, title_len_);
return title;
}
@@ -292,9 +246,9 @@ auto Mpris::getDynamicStr(const PlayerInfo& info, bool truncated, bool html) ->
// keep position format same as length format
auto position = getPositionStr(info, truncated && truncate_hours_ && length.length() < 6);
size_t artistLen = utf8_width(artist);
size_t albumLen = utf8_width(album);
size_t titleLen = utf8_width(title);
size_t artistLen = waybar::util::utf8_width(artist);
size_t albumLen = waybar::util::utf8_width(album);
size_t titleLen = waybar::util::utf8_width(title);
size_t lengthLen = length.length();
size_t posLen = position.length();
@@ -313,7 +267,7 @@ auto Mpris::getDynamicStr(const PlayerInfo& info, bool truncated, bool html) ->
// Since the first element doesn't present a separator and we don't know a priori which one
// it will be, we add a "virtual separatorLen" to the dynamicLen, since we are adding the
// separatorLen to all the other lengths.
size_t separatorLen = utf8_width(dynamic_separator_);
size_t separatorLen = waybar::util::utf8_width(dynamic_separator_);
size_t dynamicLen = dynamic_len_ + separatorLen;
if (showArtist) artistLen += separatorLen;
if (showAlbum) albumLen += separatorLen;
@@ -775,9 +729,12 @@ auto Mpris::update() -> void {
auto tooltip_text = fmt::format(
fmt::runtime(tooltipstr), fmt::arg("player", info.name),
fmt::arg("status", info.status_string),
fmt::arg("artist", getArtistStr(info, tooltip_len_limits_)),
fmt::arg("title", getTitleStr(info, tooltip_len_limits_)),
fmt::arg("album", getAlbumStr(info, tooltip_len_limits_)),
fmt::arg("artist",
std::string(Glib::Markup::escape_text(getArtistStr(info, tooltip_len_limits_)))),
fmt::arg("title",
std::string(Glib::Markup::escape_text(getTitleStr(info, tooltip_len_limits_)))),
fmt::arg("album",
std::string(Glib::Markup::escape_text(getAlbumStr(info, tooltip_len_limits_)))),
fmt::arg("length", tooltipLength), fmt::arg("position", tooltipPosition),
fmt::arg("dynamic", getDynamicStr(info, tooltip_len_limits_, false)),
fmt::arg("player_icon", getIconFromJson(config_["player-icons"], info.name)),
+1 -1
View File
@@ -85,7 +85,7 @@ void Language::doUpdate() {
spdlog::debug("niri language formatted layout name {}", layoutName);
if (!format_.empty()) {
if (!layoutName.empty()) {
label_.show();
label_.set_markup(layoutName);
} else {
+3 -1
View File
@@ -103,7 +103,9 @@ void Workspaces::doUpdate() {
fmt::arg("output", ws["output"].asString()));
}
if (!config_["disable-markup"].asBool()) {
static_cast<Gtk::Label*>(button.get_children()[0])->set_markup(name);
auto* child = gtk_bin_get_child(GTK_BIN(button.gobj()));
if (child != nullptr && GTK_IS_LABEL(child))
gtk_label_set_markup(GTK_LABEL(child), name.c_str());
} else {
button.set_label(name);
}
+2 -3
View File
@@ -71,6 +71,7 @@ Privacy::Privacy(const std::string& id, const Json::Value& config, Gtk::Orientat
auto* item = Gtk::make_managed<PrivacyItem>(module, nodeType, nodePtr, orientation, pos,
iconSize, transition_duration);
box_.add(*item);
modules_.push_back(item);
}
}
@@ -148,9 +149,7 @@ auto Privacy::update() -> void {
bool useAudioOut = false;
mutex_.lock();
for (Gtk::Widget* widget : box_.get_children()) {
auto* module = dynamic_cast<PrivacyItem*>(widget);
if (module == nullptr) continue;
for (PrivacyItem* module : modules_) {
switch (module->privacy_type) {
case util::PipewireBackend::PRIVACY_NODE_TYPE_VIDEO_INPUT:
setScreenshare = true;
+32 -54
View File
@@ -10,73 +10,51 @@ PulseaudioSlider::PulseaudioSlider(const std::string& id, const Json::Value& con
if (config_["target"].isString()) {
std::string target = config_["target"].asString();
if (target == "sink") {
this->target = PulseaudioSliderTarget::Sink;
this->target = util::PulseaudioTarget::Sink;
} else if (target == "source") {
this->target = PulseaudioSliderTarget::Source;
this->target = util::PulseaudioTarget::Source;
}
}
if (config_["zero-on-mute"].isBool()) {
zero_on_mute = config_["zero-on-mute"].asBool();
}
if (config_["unmute-on-volume-change"].isBool()) {
unmute_on_volume_change = config_["unmute-on-volume-change"].asBool();
}
}
void PulseaudioSlider::update() {
switch (target) {
case PulseaudioSliderTarget::Sink:
if (backend->getSinkMuted()) {
scale_.set_value(min_);
} else {
scale_.set_value(backend->getSinkVolume());
}
break;
uint16_t display_value = backend->getVolume(target);
bool is_muted = backend->getMuted(target);
case PulseaudioSliderTarget::Source:
if (backend->getSourceMuted()) {
scale_.set_value(min_);
} else {
scale_.set_value(backend->getSourceVolume());
}
break;
if (is_muted) {
if (zero_on_mute) {
display_value = min_;
}
if (!previously_muted) {
scale_.get_style_context()->add_class("muted");
}
} else if (previously_muted) {
scale_.get_style_context()->remove_class("muted");
}
scale_.set_value(display_value);
previously_muted = is_muted;
}
void PulseaudioSlider::onValueChanged() {
bool is_mute = false;
uint16_t slider_value = scale_.get_value();
switch (target) {
case PulseaudioSliderTarget::Sink:
if (backend->getSinkMuted()) {
is_mute = true;
}
break;
case PulseaudioSliderTarget::Source:
if (backend->getSourceMuted()) {
is_mute = true;
}
break;
}
uint16_t volume = scale_.get_value();
if (is_mute) {
// Avoid setting sink/source to volume 0 if the user muted if via another mean.
if (volume == 0) {
return;
}
// If the sink/source is mute, but the user clicked the slider, unmute it!
else {
switch (target) {
case PulseaudioSliderTarget::Sink:
backend->toggleSinkMute(false);
break;
case PulseaudioSliderTarget::Source:
backend->toggleSourceMute(false);
break;
}
// Avoid setting sink/source to volume 0 if the user muted it via other means.
if (!backend->getMuted(target) || slider_value != 0) {
if (unmute_on_volume_change) {
backend->unmute(target);
}
backend->changeVolume(slider_value, min_, max_);
}
backend->changeVolume(volume, min_, max_);
}
} // namespace waybar::modules
} // namespace waybar::modules
+26 -1
View File
@@ -6,6 +6,9 @@
namespace waybar::modules::SNI {
static const unsigned RETRY_DELAY_MS = 200;
static const unsigned MAX_RETRIES = 10;
Host::Host(const std::size_t id, const Json::Value& config, const Bar& bar,
const std::function<void(std::unique_ptr<Item>&)>& on_add,
const std::function<void(std::unique_ptr<Item>&)>& on_remove,
@@ -22,6 +25,7 @@ Host::Host(const std::size_t id, const Json::Value& config, const Bar& bar,
on_update_(on_update) {}
Host::~Host() {
retry_connection_.disconnect();
if (bus_name_id_ > 0) {
Gio::DBus::unown_name(bus_name_id_);
bus_name_id_ = 0;
@@ -53,6 +57,8 @@ void Host::nameAppeared(const Glib::RefPtr<Gio::DBus::Connection>& conn, const G
}
void Host::nameVanished(const Glib::RefPtr<Gio::DBus::Connection>& conn, const Glib::ustring name) {
retry_connection_.disconnect();
retry_count_ = 0;
g_cancellable_cancel(cancellable_);
g_clear_object(&cancellable_);
g_clear_object(&watcher_);
@@ -72,11 +78,30 @@ void Host::proxyReady(GObject* src, GAsyncResult* res, gpointer data) {
return;
}
auto host = static_cast<SNI::Host*>(data);
host->watcher_ = watcher;
if (error != nullptr) {
spdlog::error("Host: {}", error->message);
g_clear_object(&host->cancellable_);
if (host->retry_count_ >= MAX_RETRIES) {
spdlog::warn("Host: giving up on watcher proxy creation after {} retries", host->retry_count_);
return;
}
host->retry_count_ += 1;
// Store the timeout connection so it is disconnected in ~Host, avoiding a
// use-after-free if the Host is destroyed before the retry fires.
host->retry_connection_ = Glib::signal_timeout().connect(
[host]() {
if (host->watcher_ != nullptr) {
return false;
}
auto conn = Gio::DBus::Connection::get_sync(Gio::DBus::BusType::BUS_TYPE_SESSION);
host->nameAppeared(conn, "org.kde.StatusNotifierWatcher", "");
return false;
},
RETRY_DELAY_MS);
return;
}
host->retry_count_ = 0;
host->watcher_ = watcher;
sn_watcher_call_register_host(host->watcher_, host->object_path_.c_str(), host->cancellable_,
&Host::registerHost, data);
}
+12 -2
View File
@@ -190,9 +190,17 @@ void Item::setProperty(const Glib::ustring& name, Glib::VariantBase& value) {
} else if (name == "Status") {
setStatus(get_variant<Glib::ustring>(value));
} else if (name == "IconName") {
icon_name = get_variant<std::string>(value);
if (has_custom_icon_) {
spdlog::trace("Item '{}': ignoring IconName update, custom icon is set", id);
} else {
icon_name = get_variant<std::string>(value);
}
} else if (name == "IconPixmap") {
icon_pixmap = this->extractPixBuf(value.gobj());
if (has_custom_icon_) {
spdlog::trace("Item '{}': ignoring IconPixmap update, custom icon is set", id);
} else {
icon_pixmap = this->extractPixBuf(value.gobj());
}
} else if (name == "OverlayIconName") {
overlay_icon_name = get_variant<std::string>(value);
} else if (name == "OverlayIconPixmap") {
@@ -270,11 +278,13 @@ void Item::setCustomIcon(const std::string& id) {
Glib::RefPtr<Gdk::Pixbuf> custom_pixbuf = Gdk::Pixbuf::create_from_file(custom_icon);
icon_name = ""; // icon_name has priority over pixmap
icon_pixmap = custom_pixbuf;
has_custom_icon_ = true;
} catch (const Glib::Error& e) {
spdlog::error("Failed to load custom icon {}: {}", custom_icon, e.what());
}
} else { // if file doesn't exist it's most likely an icon_name
icon_name = custom_icon;
has_custom_icon_ = true;
}
}
}
+4 -13
View File
@@ -24,9 +24,6 @@ Tray::Tray(const std::string& id, const Bar& bar, const Json::Value& config)
if (config_["spacing"].isUInt()) {
box_.set_spacing(config_["spacing"].asUInt());
}
if (config["show-passive-items"].isBool()) {
show_passive_ = config["show-passive-items"].asBool();
}
nb_hosts_ += 1;
if (config_["icons"].isObject()) {
IconManager::instance().setIconsConfig(config_["icons"]);
@@ -42,6 +39,8 @@ void Tray::onAdd(std::unique_ptr<Item>& item) {
} else {
box_.pack_start(item->event_box);
}
item->event_box.signal_show().connect([this] { dp.emit(); });
item->event_box.signal_hide().connect([this] { dp.emit(); });
dp.emit();
}
@@ -51,17 +50,9 @@ void Tray::onRemove(std::unique_ptr<Item>& item) {
}
auto Tray::update() -> void {
// Show tray only when items are available
std::vector<Gtk::Widget*> children = box_.get_children();
if (show_passive_) {
event_box_.set_visible(!children.empty());
} else {
event_box_.set_visible(!std::all_of(children.begin(), children.end(), [](Gtk::Widget* child) {
return child->get_style_context()->has_class("passive");
}));
}
// Call parent update
event_box_.set_visible(std::any_of(children.begin(), children.end(),
[](Gtk::Widget* child) { return child->get_visible(); }));
AModule::update();
}
+42 -15
View File
@@ -54,21 +54,28 @@ void Language::onCmd(const struct Ipc::ipc_response& res) {
std::lock_guard<std::mutex> lock(mutex_);
auto payload = parser_.parse(res.payload);
std::vector<std::string> used_layouts;
// Display current layout of a device with a maximum count of layouts, expecting that all will
// be OK
// Union layout names across every keyboard input so hot-plugged devices contribute their
// layouts to the map. Track the device with the most layouts to seed the initially displayed
// layout, matching the previous behaviour at startup.
Json::ArrayIndex max_id = 0, max = 0;
for (Json::ArrayIndex i = 0; i < payload.size(); i++) {
auto size = payload[i][XKB_LAYOUT_NAMES_KEY].size();
if (size > max) {
max = size;
if (payload[i]["type"].asString() != "keyboard") continue;
const auto& names = payload[i][XKB_LAYOUT_NAMES_KEY];
if (names.size() > max) {
max = names.size();
max_id = i;
}
for (const auto& layout : names) {
const auto name = layout.asString();
if (std::find(used_layouts.begin(), used_layouts.end(), name) == used_layouts.end()) {
used_layouts.push_back(name);
}
}
}
for (const auto& layout : payload[max_id][XKB_LAYOUT_NAMES_KEY]) {
used_layouts.push_back(layout.asString());
}
// Rebuild from scratch so init_layouts_map's duplicate-suffix pass doesn't compound across
// refreshes (e.g. "us" -> "us1" -> "us11").
layouts_map_.clear();
init_layouts_map(used_layouts);
set_current_layout(payload[max_id][XKB_ACTIVE_LAYOUT_NAME_KEY].asString());
dp.emit();
@@ -82,13 +89,26 @@ void Language::onEvent(const struct Ipc::ipc_response& res) {
return;
}
bool refresh_inputs = false;
try {
std::lock_guard<std::mutex> lock(mutex_);
auto payload = parser_.parse(res.payload)["input"];
if (payload["type"].asString() == "keyboard") {
set_current_layout(payload[XKB_ACTIVE_LAYOUT_NAME_KEY].asString());
{
std::lock_guard<std::mutex> lock(mutex_);
auto root = parser_.parse(res.payload);
auto change = root["change"].asString();
auto payload = root["input"];
if (payload["type"].asString() == "keyboard") {
// A device was added or its keymap changed - the layout set may have grown, so refresh
// layouts_map_ via IPC_GET_INPUTS once we've released mutex_.
refresh_inputs = (change == "added" || change == "xkb_keymap");
set_current_layout(payload[XKB_ACTIVE_LAYOUT_NAME_KEY].asString());
}
dp.emit();
}
// sendCmd is synchronous: it blocks on the IPC reply and then emits signal_cmd on this same
// thread, which lands in onCmd and re-locks mutex_. Must call it with mutex_ released.
if (refresh_inputs) {
ipc_.sendCmd(IPC_GET_INPUTS);
}
dp.emit();
} catch (const std::exception& e) {
spdlog::error("Language: {}", e.what());
}
@@ -125,8 +145,15 @@ auto Language::update() -> void {
}
auto Language::set_current_layout(const std::string& current_layout) -> void {
// Guard against unknown / empty layout names: transient virtual keyboards (e.g. wtype) and
// hot-plugged devices whose layouts haven't made it into the map yet would otherwise blank out
// layout_ via map::operator[]'s default-construct-on-miss.
auto it = layouts_map_.find(current_layout);
if (it == layouts_map_.end()) {
return;
}
label_.get_style_context()->remove_class(layout_.short_name);
layout_ = layouts_map_[current_layout];
layout_ = it->second;
label_.get_style_context()->add_class(layout_.short_name);
}
+2 -2
View File
@@ -94,12 +94,12 @@ auto Window::update() -> void {
old_app_id_ = app_id_;
}
label_.set_markup(waybar::util::rewriteString(
setLabelMarkup(waybar::util::rewriteString(
fmt::format(fmt::runtime(format_), fmt::arg("title", window_), fmt::arg("app_id", app_id_),
fmt::arg("shell", shell_), fmt::arg("marks", marks_)),
config_["rewrite"]));
if (tooltipEnabled()) {
label_.set_tooltip_markup(window_);
setTooltipMarkup(window_);
}
updateAppIcon();
+18 -7
View File
@@ -229,7 +229,10 @@ bool Workspaces::filterButtons() {
auto ws = std::find_if(workspaces_.begin(), workspaces_.end(),
[it](const auto& node) { return node["name"].asString() == it->first; });
if (ws == workspaces_.end() ||
(!config_["all-outputs"].asBool() && (*ws)["output"].asString() != bar_.output->name)) {
((*ws).isMember("target_output") ? (*ws)["target_output"].asString() != bar_.output->name &&
(*ws)["target_output"].asString() != ""
: !config_["all-outputs"].asBool() &&
(*ws)["output"].asString() != bar_.output->name)) {
it = buttons_.erase(it);
needReorder = true;
} else {
@@ -329,7 +332,12 @@ auto Workspaces::update() -> void {
} else {
button.get_style_context()->remove_class("current_output");
}
std::string output = (*it)["name"].asString();
std::string output;
if (!config_["disable-markup"].asBool()) {
output = g_markup_escape_text((*it)["name"].asString().c_str(), -1);
} else {
output = (*it)["name"].asString();
}
std::string windows = "";
if (config_["window-rewrite"].isObject()) {
updateWindows((*it), windows);
@@ -369,11 +377,14 @@ Gtk::Button& Workspaces::addButton(const Json::Value& node) {
node["name"].asString(), node["target_output"].asString(),
"--no-auto-back-and-forth", node["name"].asString()));
} else {
ipc_.sendCmd(IPC_COMMAND, fmt::format("workspace {} \"{}\"",
config_["disable-auto-back-and-forth"].asBool()
? "--no-auto-back-and-forth"
: "",
node["name"].asString()));
std::string flag = config_["disable-auto-back-and-forth"].asBool()
? "--no-auto-back-and-forth"
: "";
if (node["num"].asInt() >= 0) {
ipc_.sendCmd(IPC_COMMAND, fmt::format(workspace_switch_number_cmd_, flag, node["num"].asInt()));
} else {
ipc_.sendCmd(IPC_COMMAND, fmt::format(workspace_switch_cmd_, flag, node["name"].asString()));
}
}
} catch (const std::exception& e) {
spdlog::error("Workspaces: {}", e.what());
+7 -2
View File
@@ -84,6 +84,9 @@ SystemdFailedUnits::SystemdFailedUnits(const std::string& id, const Json::Value&
}
}
if (!user_props_proxy_ && !system_props_proxy_)
throw std::runtime_error("Neither system nor user status is requested.");
updateData();
/* Always update for the first time. */
dp.emit();
@@ -103,7 +106,7 @@ auto SystemdFailedUnits::notify_cb(const Glib::ustring& sender_name,
void SystemdFailedUnits::RequestSystemState() {
auto load = [](const char* kind, Glib::RefPtr<Gio::DBus::Proxy>& proxy) -> std::string {
try {
if (!proxy) return "unknown";
if (!proxy) return "ignored";
auto parameters = Glib::VariantContainerBase(
g_variant_new("(ss)", "org.freedesktop.systemd1.Manager", "SystemState"));
Glib::VariantContainerBase data = proxy->call_sync("Get", parameters);
@@ -122,7 +125,9 @@ void SystemdFailedUnits::RequestSystemState() {
system_state_ = load("systemwide", system_props_proxy_);
user_state_ = load("user", user_props_proxy_);
if (system_state_ == "running" && user_state_ == "running")
if ((system_state_ == "running" || system_state_ == "ignored") &&
(user_state_ == "running" || user_state_ == "ignored"))
overall_state_ = "ok";
else
overall_state_ = "degraded";
+4
View File
@@ -155,3 +155,7 @@ bool waybar::modules::Temperature::isCritical(uint16_t temperature_c) {
return config_["critical-threshold"].isInt() &&
temperature_c >= config_["critical-threshold"].asInt();
}
void waybar::modules::Temperature::suspend() { thread_.pause(); }
void waybar::modules::Temperature::resume() { thread_.resume(); }
+7 -4
View File
@@ -347,10 +347,13 @@ auto IPC::update_state_handler(const std::string& event, const Json::Value& data
if (event == "output-wset-changed") {
// data: { event, new-wset: wset.name, output: id, new-wset-data: wset, output-data: output }
auto& output = state.outputs.at(data["output-data"]["name"].asString());
auto wset_idx = data["new-wset-data"]["index"].asUInt();
state.wsets.at(wset_idx).output = output;
output.wset_idx = wset_idx;
try {
auto& output = state.outputs.at(data["output-data"]["name"].asString());
auto wset_idx = data["new-wset-data"]["index"].asUInt();
state.wsets.at(wset_idx).output = output;
output.wset_idx = wset_idx;
} catch (const std::exception&) {
}
return;
}
+1
View File
@@ -211,6 +211,7 @@ void waybar::modules::Wireplumber::onMixerChanged(waybar::modules::Wireplumber*
return;
}
}
return;
}
spdlog::warn("[{}]: (onMixerChanged: {}) - Object with id {} not found", self->name_,
+38 -4
View File
@@ -94,9 +94,35 @@ Task::Task(const waybar::Bar& bar, const Json::Value& config, Taskbar* tbar,
button.set_relief(Gtk::RELIEF_NONE);
content_.add(text_before_);
content_.add(icon_);
content_.add(text_after_);
/* When "expand" is enabled the buttons stretch to fill the taskbar and the
* titles ellipsize to fit within the available space. This only makes sense
* on a horizontal bar; on a vertical bar the box grows along the vertical
* axis, so ellipsizing/forcing width_chars(1) would truncate every title to
* "…". Keep the historical behavior (buttons sized to their content) as the
* default and when the bar is vertical. */
bool expand = config_["expand"].isBool() && config_["expand"].asBool();
bool horizontal = bar.orientation == Gtk::ORIENTATION_HORIZONTAL;
if (expand && horizontal) {
button.set_hexpand(true);
content_.set_hexpand(true);
text_before_.set_ellipsize(Pango::ELLIPSIZE_END);
text_before_.set_single_line_mode(true);
text_before_.set_width_chars(1);
text_before_.set_xalign(0.0);
text_after_.set_ellipsize(Pango::ELLIPSIZE_END);
text_after_.set_single_line_mode(true);
text_after_.set_width_chars(1);
text_after_.set_xalign(0.0);
content_.pack_start(text_before_, true, true, 0);
content_.pack_start(icon_, false, false, 0);
content_.pack_start(text_after_, true, true, 0);
} else {
content_.add(text_before_);
content_.add(icon_);
content_.add(text_after_);
}
content_.show();
button.add(content_);
@@ -718,7 +744,15 @@ void Taskbar::handle_finished() {
}
void Taskbar::add_button(Gtk::Button& bt) {
box_.pack_start(bt, false, false);
/* Only let the buttons expand to fill the taskbar when "expand" is enabled
* and the bar is horizontal (see the Task constructor for details). */
bool expand = config_["expand"].isBool() && config_["expand"].asBool();
bool horizontal = bar_.orientation == Gtk::ORIENTATION_HORIZONTAL;
if (expand && horizontal) {
box_.pack_start(bt, true, true);
} else {
box_.pack_start(bt, false, false);
}
box_.get_style_context()->remove_class("empty");
}
+34 -8
View File
@@ -99,15 +99,17 @@ void AudioBackend::contextStateCb(pa_context* c, void* data) {
nullptr, nullptr);
break;
case PA_CONTEXT_FAILED:
// When pulseaudio server restarts, the connection is "failed". Try to reconnect.
// pa_threaded_mainloop_lock is already acquired in callback threads.
// So there is no need to lock it again.
if (backend->context_ != nullptr) {
pa_context_disconnect(backend->context_);
pa_context_unref(backend->context_);
backend->context_ = nullptr;
if (pa_context_errno(c) != PA_ERR_CONNECTIONREFUSED) {
// When pulseaudio server restarts, the connection is "failed". Try to reconnect.
// pa_threaded_mainloop_lock is already acquired in callback threads.
// So there is no need to lock it again.
if (backend->context_ != nullptr) {
pa_context_disconnect(backend->context_);
pa_context_unref(backend->context_);
backend->context_ = nullptr;
}
backend->connectContext();
}
backend->connectContext();
break;
case PA_CONTEXT_CONNECTING:
case PA_CONTEXT_AUTHORIZING:
@@ -259,6 +261,14 @@ void AudioBackend::serverInfoCb(pa_context* context, const pa_server_info* i, vo
pa_context_get_source_info_list(context, sourceInfoCb, data);
}
uint16_t AudioBackend::getVolume(PulseaudioTarget target) const {
if (target == PulseaudioTarget::Source) {
return source_volume_;
} else {
return volume_;
}
}
void AudioBackend::changeVolume(uint16_t volume, uint16_t min_volume, uint16_t max_volume) {
// Early return if context is not ready
if ((context_ == nullptr) || pa_context_get_state(context_) != PA_CONTEXT_READY) {
@@ -368,6 +378,14 @@ void AudioBackend::changeVolume(ChangeType change_type, double step, uint16_t ma
pa_threaded_mainloop_unlock(mainloop_);
}
bool AudioBackend::getMuted(PulseaudioTarget target) const {
if (target == PulseaudioTarget::Source) {
return source_muted_;
} else {
return muted_;
}
}
void AudioBackend::toggleSinkMute() {
if (context_ == nullptr || pa_context_get_state(context_) != PA_CONTEXT_READY) return;
muted_ = !muted_;
@@ -404,6 +422,14 @@ void AudioBackend::toggleSourceMute(bool mute) {
pa_threaded_mainloop_unlock(mainloop_);
}
void AudioBackend::unmute(PulseaudioTarget target) {
if (target == PulseaudioTarget::Source) {
toggleSourceMute(false);
} else {
toggleSinkMute(false);
}
}
bool AudioBackend::isBluetooth() {
return monitor_.find("a2dp_sink") != std::string::npos || // PulseAudio
monitor_.find("a2dp-sink") != std::string::npos || // PipeWire
+9 -2
View File
@@ -23,7 +23,8 @@ namespace {
const std::regex IMPORT_REGEX(R"(@import\s+(?:url\()?(?:"|')([^"')]+)(?:"|')\)?;)");
}
waybar::CssReloadHelper::CssReloadHelper(std::string cssFile, std::function<void()> callback)
waybar::CssReloadHelper::CssReloadHelper(std::string cssFile,
std::function<void(const std::string&)> callback)
: m_cssFile(std::move(cssFile)), m_callback(std::move(callback)) {}
std::string waybar::CssReloadHelper::getFileContents(const std::string& filename) {
@@ -88,6 +89,12 @@ void waybar::CssReloadHelper::monitorChanges() {
}
}
void waybar::CssReloadHelper::changeCssFile(const std::string& newCssFile) {
m_fileMonitors.clear();
m_cssFile = newCssFile;
monitorChanges();
}
void waybar::CssReloadHelper::handleFileChange(Glib::RefPtr<Gio::File> const& file,
Glib::RefPtr<Gio::File> const& other_type,
Gio::FileMonitorEvent event_type) {
@@ -95,7 +102,7 @@ void waybar::CssReloadHelper::handleFileChange(Glib::RefPtr<Gio::File> const& fi
// fire for one
if (event_type == Gio::FileMonitorEvent::FILE_MONITOR_EVENT_CHANGES_DONE_HINT) {
spdlog::debug("Reloading style, file changed: {}", file->get_path());
m_callback();
m_callback(m_cssFile);
}
}
+1 -1
View File
@@ -18,7 +18,7 @@ std::string rewriteString(const std::string& value, const Json::Value& rules) {
// malformated regexes will cause an exception.
// in this case, log error and try the next rule.
const std::regex rule{it.key().asString(), std::regex_constants::icase};
if (std::regex_match(value, rule)) {
if (std::regex_match(res, rule)) {
res = std::regex_replace(res, rule, it->asString());
}
} catch (const std::regex_error& e) {
+62
View File
@@ -0,0 +1,62 @@
#include <glib.h>
#include <string>
#include <util/utf8_string.hpp>
namespace waybar::util {
namespace {
// Wide characters count as two, zero-width characters count as zero
// Modifies str in-place (unless width = std::string::npos)
// Returns the total width of the string pre-truncating
size_t measure_and_truncate(std::string& str, size_t width = std::string::npos) {
if (str.length() == 0) return 0;
const gchar* trunc_end = nullptr;
size_t total_width = 0;
for (gchar *data = str.data(), *end = data + str.size(); data != nullptr;) {
gunichar c = g_utf8_get_char_validated(data, end - data);
if (c == -1U || c == -2U) {
// invalid unicode, treat string as ascii
if (width != std::string::npos && str.length() > width) str.resize(width);
return str.length();
} else if (g_unichar_iswide(c)) {
total_width += 2;
} else if (!g_unichar_iszerowidth(c) && c != 0xAD) { // neither zero-width nor soft hyphen
total_width += 1;
}
data = g_utf8_find_next_char(data, end);
if (width != std::string::npos && total_width <= width && !g_unichar_isspace(c))
trunc_end = data;
}
if (trunc_end) str.resize(trunc_end - str.data());
return total_width;
}
} // namespace
size_t utf8_width(const std::string& str) {
return measure_and_truncate(const_cast<std::string&>(str));
}
void utf8_truncate(std::string& s, const std::string& ellipsis, size_t max_len) {
if (max_len == 0) {
s.resize(0);
return;
}
size_t len = measure_and_truncate(s, max_len);
if (len > max_len) {
size_t ellipsis_len = utf8_width(ellipsis);
if (max_len >= ellipsis_len) {
if (ellipsis_len) measure_and_truncate(s, max_len - ellipsis_len);
s += ellipsis;
} else {
s.resize(0);
}
}
}
} // namespace waybar::util