Merge remote-tracking branch 'origin/master' into pr-4265
# Conflicts: # src/modules/network.cpp
This commit is contained in:
+10
-16
@@ -36,9 +36,15 @@ auto waybar::modules::Backlight::update() -> void {
|
||||
|
||||
if (best->get_powered()) {
|
||||
event_box_.show();
|
||||
|
||||
const uint8_t percent =
|
||||
best->get_max() == 0 ? 100 : round(best->get_actual() * 100.0f / best->get_max());
|
||||
|
||||
const uint8_t percent_exp =
|
||||
best->get_max() == 0
|
||||
? 100
|
||||
: roundf(powf((float)best->get_actual() / best->get_max(), 1.0f / 2.718f) * 100);
|
||||
|
||||
// Get the state and apply state-specific format if available
|
||||
auto state = getState(percent);
|
||||
std::string current_format = format_;
|
||||
@@ -49,22 +55,10 @@ auto waybar::modules::Backlight::update() -> void {
|
||||
}
|
||||
}
|
||||
|
||||
std::string desc = fmt::format(fmt::runtime(current_format), fmt::arg("percent", percent),
|
||||
fmt::arg("icon", getIcon(percent)));
|
||||
label_.set_markup(desc);
|
||||
if (tooltipEnabled()) {
|
||||
std::string tooltip_format;
|
||||
if (config_["tooltip-format"].isString()) {
|
||||
tooltip_format = config_["tooltip-format"].asString();
|
||||
}
|
||||
if (!tooltip_format.empty()) {
|
||||
label_.set_tooltip_markup(fmt::format(fmt::runtime(tooltip_format),
|
||||
fmt::arg("percent", percent),
|
||||
fmt::arg("icon", getIcon(percent))));
|
||||
} else {
|
||||
label_.set_tooltip_markup(desc);
|
||||
}
|
||||
}
|
||||
updateLabelAndTooltip(current_format, current_format, fmt::arg("percent", percent),
|
||||
fmt::arg("percent_exp", percent_exp),
|
||||
fmt::arg("icon", getIcon(percent)),
|
||||
fmt::arg("icon_exp", getIcon(percent_exp)));
|
||||
} else {
|
||||
event_box_.hide();
|
||||
}
|
||||
|
||||
+114
-65
@@ -2,6 +2,9 @@
|
||||
|
||||
#include <algorithm>
|
||||
#include <cctype>
|
||||
#include <cmath>
|
||||
#include <cstdio>
|
||||
#include <string>
|
||||
|
||||
#include "util/command.hpp"
|
||||
#if defined(__FreeBSD__)
|
||||
@@ -33,6 +36,11 @@ waybar::modules::Battery::Battery(const std::string& id, const Bar& bar, const J
|
||||
}
|
||||
udev_monitor_enable_receiving(mon_.get());
|
||||
|
||||
if (config_["smooth-power"].isBool()) {
|
||||
smoothPowerEnable_ = config_["smooth-power"].asBool();
|
||||
if (smoothPowerEnable_ && config_["smooth-power-time-constant"].isNumeric())
|
||||
time_constant_s_ = std::max(1.0, config_["smooth-power-time-constant"].asDouble());
|
||||
}
|
||||
if (config_["weighted-average"].isBool()) weightedAverage_ = config_["weighted-average"].asBool();
|
||||
#endif
|
||||
spdlog::debug("battery: worker interval is {}", interval_.count());
|
||||
@@ -117,17 +125,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;
|
||||
std::ifstream(node.path() / "scope") >> scope;
|
||||
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;
|
||||
}
|
||||
}
|
||||
@@ -154,7 +161,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()) {
|
||||
@@ -275,11 +282,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.
|
||||
@@ -288,64 +295,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.
|
||||
@@ -353,28 +370,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;
|
||||
@@ -404,9 +424,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) {
|
||||
@@ -564,11 +585,31 @@ waybar::modules::Battery::getInfos() {
|
||||
if (online && current_status != "Discharging") status = "Plugged";
|
||||
}
|
||||
|
||||
if (total_energy_exists && total_power_exists && total_power != 0) {
|
||||
if (!smoothPowerEnable_) {
|
||||
smooth_power_ = total_power;
|
||||
} else {
|
||||
if (status != old_status_raw_) {
|
||||
smooth_power_ = total_power;
|
||||
last_t_ = std::chrono::steady_clock::now();
|
||||
} else {
|
||||
std::chrono::steady_clock::time_point now = std::chrono::steady_clock::now();
|
||||
double dt_s =
|
||||
std::chrono::duration_cast<std::chrono::duration<double> >(now - last_t_).count();
|
||||
smooth_power_ = smooth_power_ + ((1 - std::exp(-dt_s / time_constant_s_)) *
|
||||
(total_power - smooth_power_));
|
||||
last_t_ = now;
|
||||
}
|
||||
|
||||
old_status_raw_ = status;
|
||||
}
|
||||
}
|
||||
|
||||
float time_remaining{0.0f};
|
||||
if (status == "Discharging" && time_to_empty_now_exists) {
|
||||
if (time_to_empty_now != 0) time_remaining = (float)time_to_empty_now / 3600.0f;
|
||||
} else if (status == "Discharging" && total_power_exists && total_energy_exists) {
|
||||
if (total_power != 0) time_remaining = (float)total_energy / total_power;
|
||||
if (smooth_power_ != 0) time_remaining = (float)total_energy / smooth_power_;
|
||||
} else if (status == "Charging" && time_to_full_now_exists) {
|
||||
if (time_to_full_now_exists && (time_to_full_now != 0))
|
||||
time_remaining = -(float)time_to_full_now / 3600.0f;
|
||||
@@ -586,11 +627,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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -687,13 +729,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);
|
||||
@@ -718,7 +767,7 @@ auto waybar::modules::Battery::update() -> void {
|
||||
} else if (config_["tooltip-format"].isString()) {
|
||||
tooltip_format = config_["tooltip-format"].asString();
|
||||
}
|
||||
label_.set_tooltip_markup(
|
||||
setTooltipMarkup(
|
||||
fmt::format(fmt::runtime(tooltip_format), fmt::arg("timeTo", tooltip_text_default),
|
||||
fmt::arg("power", power), fmt::arg("capacity", capacity),
|
||||
fmt::arg("time", time_remaining_formatted), fmt::arg("cycles", cycles),
|
||||
@@ -741,7 +790,7 @@ auto waybar::modules::Battery::update() -> void {
|
||||
} else {
|
||||
event_box_.show();
|
||||
auto icons = std::vector<std::string>{status + "-" + state, status, state};
|
||||
label_.set_markup(fmt::format(
|
||||
setLabelMarkup(fmt::format(
|
||||
fmt::runtime(format), fmt::arg("capacity", capacity), fmt::arg("power", power),
|
||||
fmt::arg("icon", getIcon(capacity, icons)), fmt::arg("time", time_remaining_formatted),
|
||||
fmt::arg("cycles", cycles), fmt::arg("health", fmt::format("{:.3}", health))));
|
||||
|
||||
+153
-5
@@ -83,6 +83,68 @@ auto getUcharProperty(GDBusProxy* proxy, const char* property_name) -> unsigned
|
||||
return 0;
|
||||
}
|
||||
|
||||
auto isChildPath(const std::string& child, const std::string& parent) -> bool {
|
||||
return child.starts_with(parent);
|
||||
}
|
||||
|
||||
auto readBatteryCharacteristicValue(GDBusProxy* proxy_char) -> std::optional<unsigned char> {
|
||||
GVariantBuilder builder;
|
||||
g_variant_builder_init(&builder, G_VARIANT_TYPE("a{sv}"));
|
||||
|
||||
GError* error = nullptr;
|
||||
GVariant* gvar =
|
||||
g_dbus_proxy_call_sync(proxy_char, "ReadValue", g_variant_new("(a{sv})", &builder),
|
||||
G_DBUS_CALL_FLAGS_NONE, -1, nullptr, &error);
|
||||
if (error != nullptr) {
|
||||
g_error_free(error);
|
||||
return std::nullopt;
|
||||
}
|
||||
if (gvar == nullptr) {
|
||||
return std::nullopt;
|
||||
}
|
||||
|
||||
GVariant* value_array = g_variant_get_child_value(gvar, 0);
|
||||
gsize n_elements;
|
||||
const auto* data = static_cast<const guchar*>(
|
||||
g_variant_get_fixed_array(value_array, &n_elements, sizeof(guchar)));
|
||||
|
||||
std::optional<unsigned char> result;
|
||||
if (data != nullptr && n_elements > 0) {
|
||||
result = data[0];
|
||||
}
|
||||
|
||||
g_variant_unref(value_array);
|
||||
g_variant_unref(gvar);
|
||||
return result;
|
||||
}
|
||||
|
||||
auto hasUserDescriptionDescriptor(GList* objects, const std::string& char_path,
|
||||
const std::string& user_description_uuid) -> bool {
|
||||
for (GList* n = objects; n != nullptr; n = n->next) {
|
||||
GDBusObject* desc_object = G_DBUS_OBJECT(n->data);
|
||||
std::string desc_path = g_dbus_object_get_object_path(desc_object);
|
||||
|
||||
if (!isChildPath(desc_path, char_path)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
GDBusProxy* proxy_desc =
|
||||
G_DBUS_PROXY(g_dbus_object_get_interface(desc_object, "org.bluez.GattDescriptor1"));
|
||||
if (proxy_desc == nullptr) {
|
||||
continue;
|
||||
}
|
||||
|
||||
auto desc_uuid = getOptionalStringProperty(proxy_desc, "UUID");
|
||||
g_object_unref(proxy_desc);
|
||||
|
||||
if (desc_uuid.has_value() &&
|
||||
desc_uuid.value().find(user_description_uuid) != std::string::npos) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
waybar::modules::Bluetooth::Bluetooth(const std::string& id, const Json::Value& config)
|
||||
@@ -222,7 +284,7 @@ auto waybar::modules::Bluetooth::update() -> void {
|
||||
event_box_.hide();
|
||||
} else {
|
||||
event_box_.show();
|
||||
label_.set_markup(fmt::format(
|
||||
setLabelMarkup(fmt::format(
|
||||
fmt::runtime(format_), fmt::arg("status", state_),
|
||||
fmt::arg("num_connections", connected_devices_.size()),
|
||||
fmt::arg("controller_address", cur_controller_ ? cur_controller_->address : "null"),
|
||||
@@ -232,8 +294,9 @@ auto waybar::modules::Bluetooth::update() -> void {
|
||||
fmt::arg("device_address", cur_focussed_device_.address),
|
||||
fmt::arg("device_address_type", cur_focussed_device_.address_type),
|
||||
fmt::arg("device_alias", cur_focussed_device_.alias), fmt::arg("icon", icon_label),
|
||||
fmt::arg("device_battery_percentage",
|
||||
cur_focussed_device_.battery_percentage.value_or(0))));
|
||||
fmt::arg("device_battery_percentage", cur_focussed_device_.battery_percentage.value_or(0)),
|
||||
fmt::arg("device_battery_percentage_peripheral",
|
||||
cur_focussed_device_.battery_percentage_peripheral.value_or(0))));
|
||||
}
|
||||
|
||||
if (tooltipEnabled()) {
|
||||
@@ -258,7 +321,9 @@ auto waybar::modules::Bluetooth::update() -> void {
|
||||
fmt::runtime(enumerate_format), fmt::arg("device_address", dev.address),
|
||||
fmt::arg("device_address_type", dev.address_type),
|
||||
fmt::arg("device_alias", dev.alias), fmt::arg("icon", enumerate_icon),
|
||||
fmt::arg("device_battery_percentage", dev.battery_percentage.value_or(0)));
|
||||
fmt::arg("device_battery_percentage", dev.battery_percentage.value_or(0)),
|
||||
fmt::arg("device_battery_percentage_peripheral",
|
||||
dev.battery_percentage_peripheral.value_or(0)));
|
||||
}
|
||||
}
|
||||
device_enumerate_ = ss.str();
|
||||
@@ -267,7 +332,7 @@ auto waybar::modules::Bluetooth::update() -> void {
|
||||
device_enumerate_.erase(0, 1);
|
||||
}
|
||||
}
|
||||
label_.set_tooltip_markup(fmt::format(
|
||||
setTooltipMarkup(fmt::format(
|
||||
fmt::runtime(tooltip_format), fmt::arg("status", state_),
|
||||
fmt::arg("num_connections", connected_devices_.size()),
|
||||
fmt::arg("controller_address", cur_controller_ ? cur_controller_->address : "null"),
|
||||
@@ -278,6 +343,8 @@ auto waybar::modules::Bluetooth::update() -> void {
|
||||
fmt::arg("device_address_type", cur_focussed_device_.address_type),
|
||||
fmt::arg("device_alias", cur_focussed_device_.alias), fmt::arg("icon", icon_tooltip),
|
||||
fmt::arg("device_battery_percentage", cur_focussed_device_.battery_percentage.value_or(0)),
|
||||
fmt::arg("device_battery_percentage_peripheral",
|
||||
cur_focussed_device_.battery_percentage_peripheral.value_or(0)),
|
||||
fmt::arg("device_enumerate", device_enumerate_)));
|
||||
}
|
||||
|
||||
@@ -398,6 +465,85 @@ auto waybar::modules::Bluetooth::getDeviceBatteryPercentage(GDBusObject* object)
|
||||
return std::nullopt;
|
||||
}
|
||||
|
||||
auto waybar::modules::Bluetooth::getDeviceGattBatteryLevels(
|
||||
GDBusObject* device_object, std::optional<unsigned char>& central_battery,
|
||||
std::optional<unsigned char>& peripheral_battery) -> void {
|
||||
const std::string BATTERY_SERVICE_UUID = "0000180f-0000-1000-8000-00805f9b34fb";
|
||||
const std::string BATTERY_LEVEL_UUID = "00002a19-0000-1000-8000-00805f9b34fb";
|
||||
const std::string USER_DESCRIPTION_UUID = "00002901-0000-1000-8000-00805f9b34fb";
|
||||
|
||||
GList* objects = g_dbus_object_manager_get_objects(manager_.get());
|
||||
std::string device_path = g_dbus_object_get_object_path(device_object);
|
||||
|
||||
for (GList* l = objects; l != nullptr; l = l->next) {
|
||||
GDBusObject* service_object = G_DBUS_OBJECT(l->data);
|
||||
std::string service_path = g_dbus_object_get_object_path(service_object);
|
||||
|
||||
if (!isChildPath(service_path, device_path)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
GDBusProxy* proxy_service =
|
||||
G_DBUS_PROXY(g_dbus_object_get_interface(service_object, "org.bluez.GattService1"));
|
||||
if (proxy_service == nullptr) {
|
||||
continue;
|
||||
}
|
||||
|
||||
auto service_uuid = getOptionalStringProperty(proxy_service, "UUID");
|
||||
g_object_unref(proxy_service);
|
||||
|
||||
if (!service_uuid.has_value() ||
|
||||
service_uuid.value().find(BATTERY_SERVICE_UUID) == std::string::npos) {
|
||||
continue;
|
||||
}
|
||||
|
||||
processBatteryServiceCharacteristics(objects, service_path, BATTERY_LEVEL_UUID,
|
||||
USER_DESCRIPTION_UUID, central_battery,
|
||||
peripheral_battery);
|
||||
}
|
||||
|
||||
g_list_free_full(objects, g_object_unref);
|
||||
}
|
||||
|
||||
auto waybar::modules::Bluetooth::processBatteryServiceCharacteristics(
|
||||
GList* objects, const std::string& service_path, const std::string& battery_level_uuid,
|
||||
const std::string& user_description_uuid, std::optional<unsigned char>& central_battery,
|
||||
std::optional<unsigned char>& peripheral_battery) -> void {
|
||||
for (GList* m = objects; m != nullptr; m = m->next) {
|
||||
GDBusObject* char_object = G_DBUS_OBJECT(m->data);
|
||||
std::string char_path = g_dbus_object_get_object_path(char_object);
|
||||
|
||||
if (!isChildPath(char_path, service_path)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
GDBusProxy* proxy_char =
|
||||
G_DBUS_PROXY(g_dbus_object_get_interface(char_object, "org.bluez.GattCharacteristic1"));
|
||||
if (proxy_char == nullptr) {
|
||||
continue;
|
||||
}
|
||||
|
||||
auto char_uuid = getOptionalStringProperty(proxy_char, "UUID");
|
||||
if (!char_uuid.has_value() || char_uuid.value().find(battery_level_uuid) == std::string::npos) {
|
||||
g_object_unref(proxy_char);
|
||||
continue;
|
||||
}
|
||||
|
||||
auto battery_value = readBatteryCharacteristicValue(proxy_char);
|
||||
g_object_unref(proxy_char);
|
||||
|
||||
if (!battery_value.has_value()) {
|
||||
continue;
|
||||
}
|
||||
|
||||
if (hasUserDescriptionDescriptor(objects, char_path, user_description_uuid)) {
|
||||
peripheral_battery = battery_value.value();
|
||||
} else {
|
||||
central_battery = battery_value.value();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
auto waybar::modules::Bluetooth::getDeviceProperties(GDBusObject* object, DeviceInfo& device_info)
|
||||
-> bool {
|
||||
GDBusProxy* proxy_device = G_DBUS_PROXY(g_dbus_object_get_interface(object, "org.bluez.Device1"));
|
||||
@@ -418,6 +564,8 @@ auto waybar::modules::Bluetooth::getDeviceProperties(GDBusObject* object, Device
|
||||
g_object_unref(proxy_device);
|
||||
|
||||
device_info.battery_percentage = getDeviceBatteryPercentage(object);
|
||||
getDeviceGattBatteryLevels(object, device_info.battery_percentage,
|
||||
device_info.battery_percentage_peripheral);
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
+65
-2
@@ -9,6 +9,7 @@
|
||||
#include <regex>
|
||||
#include <sstream>
|
||||
|
||||
#include "util/command.hpp"
|
||||
#include "util/ustring_clen.hpp"
|
||||
|
||||
#ifdef HAVE_LANGINFO_1STDAY
|
||||
@@ -162,7 +163,7 @@ auto waybar::modules::Clock::update() -> void {
|
||||
const auto* tz = tzList_[tzCurrIdx_] != nullptr ? tzList_[tzCurrIdx_] : local_zone();
|
||||
const zoned_time now{tz, floor<seconds>(system_clock::now())};
|
||||
|
||||
label_.set_markup(fmt_lib::vformat(m_locale_, format_, fmt_lib::make_format_args(now)));
|
||||
setLabelMarkup(fmt_lib::vformat(m_locale_, format_, fmt_lib::make_format_args(now)));
|
||||
|
||||
if (tooltipEnabled()) {
|
||||
const year_month_day today{floor<days>(now.get_local_time())};
|
||||
@@ -187,6 +188,45 @@ auto waybar::modules::Clock::update() -> void {
|
||||
}
|
||||
|
||||
m_tlpText_ = fmt_lib::vformat(m_locale_, m_tlpText_, fmt_lib::make_format_args(now));
|
||||
|
||||
// Pango doesn't support CSS classes but to continue using it while staying
|
||||
// backwards compatible this approach uses post-posting to replace fake
|
||||
// classes with attributes Pango does understand.
|
||||
//
|
||||
// The benefit of this approach is anyone using the original styling choices
|
||||
// can continue doing that and folks can optionally opt into using classes.
|
||||
//
|
||||
// It's also forwards compatible to where if this implemention ever changes
|
||||
// to support proper classes anyone using them will continue to work.
|
||||
auto context = label_.get_style_context();
|
||||
|
||||
static const std::vector<std::pair<std::string, std::string>> calendar_class_map = {
|
||||
{"calendar-today", "class='today'"},
|
||||
{"calendar-days", "class='days'"},
|
||||
{"calendar-weeks", "class='weeks'"},
|
||||
{"calendar-weekdays", "class='weekdays'"},
|
||||
{"calendar-months", "class='months'"}};
|
||||
|
||||
for (const auto& [css_class, search_str] : calendar_class_map) {
|
||||
try {
|
||||
context->add_class(css_class);
|
||||
const Gdk::RGBA color = context->get_color();
|
||||
context->remove_class(css_class);
|
||||
|
||||
const std::string replace_str = fmt::format(
|
||||
"color='#{:02x}{:02x}{:02x}'", static_cast<int>(color.get_red() * 255),
|
||||
static_cast<int>(color.get_green() * 255), static_cast<int>(color.get_blue() * 255));
|
||||
|
||||
m_tlpText_ = std::regex_replace(m_tlpText_, std::regex(search_str), replace_str);
|
||||
} catch (const Glib::Error& e) {
|
||||
spdlog::warn("Clock: Failed to fetch CSS color for {}: {}", css_class, e.what().raw());
|
||||
continue;
|
||||
} catch (...) {
|
||||
// Catch-all for any other weirdness.
|
||||
continue;
|
||||
}
|
||||
}
|
||||
|
||||
m_tooltip_->set_markup(m_tlpText_);
|
||||
label_.trigger_tooltip_query();
|
||||
}
|
||||
@@ -402,8 +442,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);
|
||||
|
||||
@@ -472,6 +517,8 @@ auto waybar::modules::Clock::local_zone() -> const time_zone* {
|
||||
auto waybar::modules::Clock::doAction(const std::string& name) -> void {
|
||||
if (actionMap_[name]) {
|
||||
(this->*actionMap_[name])();
|
||||
} else if (auto key = name.substr(0, name.find(" ")); actionWithArgsMap_[key]) {
|
||||
(this->*actionWithArgsMap_[key])(name);
|
||||
} else
|
||||
spdlog::error("Clock. Unsupported action \"{0}\"", name);
|
||||
}
|
||||
@@ -498,6 +545,10 @@ void waybar::modules::Clock::tz_down() {
|
||||
if (tzSize == 1) return;
|
||||
tzCurrIdx_ = (tzCurrIdx_ == 0) ? tzSize - 1 : tzCurrIdx_ - 1;
|
||||
}
|
||||
void waybar::modules::Clock::action_exec(const std::string& action) {
|
||||
auto cmd = action.substr(strlen("exec "));
|
||||
pid_children_.push_back(util::command::forkExec(cmd));
|
||||
}
|
||||
|
||||
#ifdef HAVE_LANGINFO_1STDAY
|
||||
template <auto fn>
|
||||
@@ -509,6 +560,18 @@ using deleting_unique_ptr = std::unique_ptr<T, deleter_from_fn<fn>>;
|
||||
|
||||
// Computations done similarly to Linux cal utility.
|
||||
auto waybar::modules::Clock::first_day_of_week() -> weekday {
|
||||
const auto firstdow = config_[kCldPlaceholder]["first-day-of-week"];
|
||||
if (firstdow.isInt()) {
|
||||
const int firstDay = firstdow.asInt();
|
||||
if (!(firstDay >= 0 && firstDay <= 6)) {
|
||||
spdlog::warn(
|
||||
"Clock calender configuration first-day-of-week = {0} must be in range [0, 6]. Default "
|
||||
"value is used instead",
|
||||
firstDay);
|
||||
} else {
|
||||
return weekday{static_cast<unsigned>(firstDay)};
|
||||
}
|
||||
}
|
||||
if (iso8601Calendar_) {
|
||||
return Monday;
|
||||
}
|
||||
|
||||
+9
-11
@@ -41,6 +41,9 @@ auto waybar::modules::Cpu::update() -> void {
|
||||
auto icons = std::vector<std::string>{state};
|
||||
fmt::dynamic_format_arg_store<fmt::format_context> store;
|
||||
store.push_back(fmt::arg("load", load1));
|
||||
store.push_back(fmt::arg("load1", load1));
|
||||
store.push_back(fmt::arg("load5", load5));
|
||||
store.push_back(fmt::arg("load15", load15));
|
||||
store.push_back(fmt::arg("usage", total_usage));
|
||||
store.push_back(fmt::arg("icon", getIcon(total_usage, icons)));
|
||||
store.push_back(fmt::arg("max_frequency", max_frequency));
|
||||
@@ -48,23 +51,18 @@ auto waybar::modules::Cpu::update() -> void {
|
||||
store.push_back(fmt::arg("avg_frequency", avg_frequency));
|
||||
std::vector<std::string> arg_names;
|
||||
arg_names.reserve(cpu_usage.size() * 2);
|
||||
std::string all_icons;
|
||||
for (size_t i = 1; i < cpu_usage.size(); ++i) {
|
||||
auto core_i = i - 1;
|
||||
arg_names.push_back(fmt::format("usage{}", core_i));
|
||||
store.push_back(fmt::arg(arg_names.back().c_str(), cpu_usage[i]));
|
||||
auto core_icon = getIcon(cpu_usage[i], icons);
|
||||
all_icons += core_icon;
|
||||
arg_names.push_back(fmt::format("icon{}", core_i));
|
||||
store.push_back(fmt::arg(arg_names.back().c_str(), getIcon(cpu_usage[i], icons)));
|
||||
}
|
||||
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));
|
||||
} else {
|
||||
label_.set_tooltip_markup(tooltip);
|
||||
}
|
||||
store.push_back(fmt::arg(arg_names.back().c_str(), core_icon));
|
||||
}
|
||||
store.push_back(fmt::arg("icons", all_icons));
|
||||
updateLabelAndTooltipForState(state, format, tooltip, store);
|
||||
}
|
||||
|
||||
// Call parent update
|
||||
|
||||
@@ -32,24 +32,12 @@ auto waybar::modules::CpuFrequency::update() -> void {
|
||||
} else {
|
||||
event_box_.show();
|
||||
auto icons = std::vector<std::string>{state};
|
||||
fmt::dynamic_format_arg_store<fmt::format_context> store;
|
||||
store.push_back(fmt::arg("icon", getIcon(avg_frequency, icons)));
|
||||
store.push_back(fmt::arg("max_frequency", max_frequency));
|
||||
store.push_back(fmt::arg("min_frequency", min_frequency));
|
||||
store.push_back(fmt::arg("avg_frequency", avg_frequency));
|
||||
label_.set_markup(fmt::vformat(format, store));
|
||||
|
||||
if (tooltipEnabled()) {
|
||||
std::string tooltip;
|
||||
if (config_["tooltip-format"].isString()) {
|
||||
tooltip = config_["tooltip-format"].asString();
|
||||
label_.set_tooltip_markup(fmt::vformat(tooltip, store));
|
||||
} else {
|
||||
tooltip = "Minimum frequency: {}\nAverage frequency: {}\nMaximum frequency: {}\n";
|
||||
label_.set_tooltip_markup(
|
||||
fmt::format(fmt::runtime(tooltip), min_frequency, avg_frequency, max_frequency));
|
||||
}
|
||||
}
|
||||
updateLabelAndTooltip(
|
||||
format,
|
||||
"Minimum frequency: {min_frequency}\nAverage frequency: {avg_frequency}\nMaximum "
|
||||
"frequency: {max_frequency}\n",
|
||||
fmt::arg("icon", getIcon(avg_frequency, icons)), fmt::arg("max_frequency", max_frequency),
|
||||
fmt::arg("min_frequency", min_frequency), fmt::arg("avg_frequency", avg_frequency));
|
||||
}
|
||||
|
||||
// Call parent update
|
||||
|
||||
@@ -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&) {
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,47 @@
|
||||
#include "modules/cpu_graph.hpp"
|
||||
|
||||
#include "modules/cpu_frequency.hpp"
|
||||
#include "modules/cpu_usage.hpp"
|
||||
#include "modules/load.hpp"
|
||||
|
||||
// In the 80000 version of fmt library authors decided to optimize imports
|
||||
// and moved declarations required for fmt::dynamic_format_arg_store in new
|
||||
// header fmt/args.h
|
||||
#if (FMT_VERSION >= 80000)
|
||||
#include <fmt/args.h>
|
||||
#else
|
||||
#include <fmt/core.h>
|
||||
#endif
|
||||
|
||||
waybar::modules::CpuGraph::CpuGraph(const std::string& id, const Json::Value& config)
|
||||
: AGraph(config, "cpu_graph", id, 5) {
|
||||
thread_ = [this] {
|
||||
dp.emit();
|
||||
thread_.sleep_for(interval_);
|
||||
};
|
||||
}
|
||||
|
||||
auto waybar::modules::CpuGraph::update() -> void {
|
||||
// TODO: as creating dynamic fmt::arg arrays is buggy we have to calc both
|
||||
auto [cpu_usage, tooltip] = CpuUsage::getCpuUsage(prev_times_);
|
||||
if (tooltipEnabled()) {
|
||||
graph_.set_tooltip_text(tooltip);
|
||||
}
|
||||
auto total_usage = cpu_usage.empty() ? 0 : cpu_usage[0];
|
||||
addValue(total_usage);
|
||||
|
||||
graph_.get_style_context()->remove_class(MODERATE_CLASS);
|
||||
graph_.get_style_context()->remove_class(HIGH_CLASS);
|
||||
graph_.get_style_context()->remove_class(INTENSIVE_CLASS);
|
||||
|
||||
if (total_usage > 90) {
|
||||
graph_.get_style_context()->add_class(INTENSIVE_CLASS);
|
||||
} else if (total_usage > 70) {
|
||||
graph_.get_style_context()->add_class(HIGH_CLASS);
|
||||
} else if (total_usage > 30) {
|
||||
graph_.get_style_context()->add_class(MODERATE_CLASS);
|
||||
}
|
||||
|
||||
// Call parent update
|
||||
AGraph::update();
|
||||
}
|
||||
@@ -38,23 +38,18 @@ auto waybar::modules::CpuUsage::update() -> void {
|
||||
store.push_back(fmt::arg("icon", getIcon(total_usage, icons)));
|
||||
std::vector<std::string> arg_names;
|
||||
arg_names.reserve(cpu_usage.size() * 2);
|
||||
std::string all_icons;
|
||||
for (size_t i = 1; i < cpu_usage.size(); ++i) {
|
||||
auto core_i = i - 1;
|
||||
arg_names.push_back(fmt::format("usage{}", core_i));
|
||||
store.push_back(fmt::arg(arg_names.back().c_str(), cpu_usage[i]));
|
||||
auto core_icon = getIcon(cpu_usage[i], icons);
|
||||
all_icons += core_icon;
|
||||
arg_names.push_back(fmt::format("icon{}", core_i));
|
||||
store.push_back(fmt::arg(arg_names.back().c_str(), getIcon(cpu_usage[i], icons)));
|
||||
}
|
||||
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));
|
||||
} else {
|
||||
label_.set_tooltip_markup(tooltip);
|
||||
}
|
||||
store.push_back(fmt::arg(arg_names.back().c_str(), core_icon));
|
||||
}
|
||||
store.push_back(fmt::arg("icons", all_icons));
|
||||
updateLabelAndTooltip(format, tooltip, store);
|
||||
}
|
||||
|
||||
// Call parent update
|
||||
|
||||
+35
-8
@@ -8,7 +8,7 @@
|
||||
|
||||
waybar::modules::Custom::Custom(const std::string& name, const std::string& id,
|
||||
const Json::Value& config, const std::string& output_name)
|
||||
: ALabel(config, "custom-" + name, id, "{}"),
|
||||
: AIconLabel(config, "custom-" + name, id, "{}"),
|
||||
name_(name),
|
||||
output_name_(output_name),
|
||||
id_(id),
|
||||
@@ -28,6 +28,15 @@ waybar::modules::Custom::Custom(const std::string& name, const std::string& id,
|
||||
} else if (config_["exec"].isString()) {
|
||||
continuousWorker();
|
||||
}
|
||||
if (config_["image-path"].isString()) {
|
||||
image_path_ = config_["image-path"].asString();
|
||||
}
|
||||
if (config_["image-name"].isString()) {
|
||||
image_name_ = config_["image-name"].asString();
|
||||
}
|
||||
if (config["icon-size"].isUInt()) {
|
||||
app_icon_size_ = config["icon-size"].asUInt();
|
||||
}
|
||||
}
|
||||
|
||||
waybar::modules::Custom::~Custom() {
|
||||
@@ -39,6 +48,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 +152,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() {
|
||||
@@ -177,10 +193,11 @@ auto waybar::modules::Custom::update() -> void {
|
||||
auto str = fmt::format(fmt::runtime(format_), fmt::arg("text", text_), fmt::arg("alt", alt_),
|
||||
fmt::arg("icon", getIcon(percentage_, alt_)),
|
||||
fmt::arg("percentage", percentage_));
|
||||
if ((config_["hide-empty-text"].asBool() && text_.empty()) || str.empty()) {
|
||||
if ((config_["hide-empty-text"].asBool() && text_.empty()) ||
|
||||
(str.empty() && image_path_.empty() && image_name_.empty())) {
|
||||
event_box_.hide();
|
||||
} else {
|
||||
label_.set_markup(str);
|
||||
setLabelMarkup(str);
|
||||
if (tooltipEnabled()) {
|
||||
std::string tooltip_markup;
|
||||
if (tooltip_format_enabled_) {
|
||||
@@ -195,10 +212,7 @@ auto waybar::modules::Custom::update() -> void {
|
||||
tooltip_markup = tooltip_;
|
||||
}
|
||||
|
||||
if (last_tooltip_markup_ != tooltip_markup) {
|
||||
label_.set_tooltip_markup(tooltip_markup);
|
||||
last_tooltip_markup_ = std::move(tooltip_markup);
|
||||
}
|
||||
setTooltipMarkup(tooltip_markup);
|
||||
}
|
||||
auto style = label_.get_style_context();
|
||||
auto classes = style->list_classes();
|
||||
@@ -212,7 +226,19 @@ auto waybar::modules::Custom::update() -> void {
|
||||
style->add_class("flat");
|
||||
style->add_class("text-button");
|
||||
style->add_class(MODULE_CLASS);
|
||||
auto image_style = image_.get_style_context();
|
||||
image_style->add_class("image-button");
|
||||
event_box_.show();
|
||||
if (!image_path_.empty()) {
|
||||
auto pixbuf = Gdk::Pixbuf::create_from_file(image_path_, app_icon_size_, app_icon_size_);
|
||||
image_.set(pixbuf);
|
||||
} else if (!image_name_.empty()) {
|
||||
image_.set_from_icon_name(image_name_, Gtk::ICON_SIZE_INVALID);
|
||||
image_.set_pixel_size(app_icon_size_);
|
||||
}
|
||||
|
||||
image_.set_visible(!image_name_.empty() || !image_path_.empty());
|
||||
label_.set_visible(!str.empty());
|
||||
}
|
||||
} catch (const fmt::format_error& e) {
|
||||
if (std::strcmp(e.what(), "cannot switch from manual to automatic argument indexing") != 0)
|
||||
@@ -224,7 +250,7 @@ auto waybar::modules::Custom::update() -> void {
|
||||
}
|
||||
}
|
||||
// Call parent update
|
||||
ALabel::update();
|
||||
AIconLabel::update();
|
||||
}
|
||||
|
||||
void waybar::modules::Custom::parseOutputRaw() {
|
||||
@@ -290,6 +316,7 @@ void waybar::modules::Custom::parseOutputJson() {
|
||||
class_.push_back(c.asString());
|
||||
}
|
||||
}
|
||||
|
||||
if (!parsed["percentage"].asString().empty() && parsed["percentage"].isNumeric()) {
|
||||
percentage_ = (int)lround(parsed["percentage"].asFloat());
|
||||
} else {
|
||||
|
||||
@@ -0,0 +1,281 @@
|
||||
#include "modules/custom_graph.hpp"
|
||||
|
||||
#include <spdlog/spdlog.h>
|
||||
|
||||
#include "util/scope_guard.hpp"
|
||||
|
||||
waybar::modules::CustomGraph::CustomGraph(const std::string& name, const std::string& id,
|
||||
const Json::Value& config, const std::string& output_name)
|
||||
: AGraph(config, "custom-graph-" + name, id),
|
||||
name_(name),
|
||||
output_name_(output_name),
|
||||
id_(id),
|
||||
tooltip_format_enabled_{config_["tooltip-format"].isString()},
|
||||
percentage_(0),
|
||||
fp_(nullptr),
|
||||
pid_(-1) {
|
||||
if (config.isNull()) {
|
||||
spdlog::warn("There is no configuration for 'custom-graph/{}', element will be hidden", name);
|
||||
}
|
||||
dp.emit();
|
||||
if (!config_["signal"].empty() && config_["interval"].empty() &&
|
||||
config_["restart-interval"].empty()) {
|
||||
waitingWorker();
|
||||
} else if (interval_.count() > 0) {
|
||||
delayWorker();
|
||||
} else if (config_["exec"].isString()) {
|
||||
continuousWorker();
|
||||
}
|
||||
}
|
||||
|
||||
waybar::modules::CustomGraph::~CustomGraph() {
|
||||
if (pid_ != -1) {
|
||||
killpg(pid_, SIGTERM);
|
||||
waitpid(pid_, NULL, 0);
|
||||
pid_ = -1;
|
||||
}
|
||||
}
|
||||
|
||||
void waybar::modules::CustomGraph::delayWorker() {
|
||||
thread_ = [this] {
|
||||
for (int i : this->pid_children_) {
|
||||
int status;
|
||||
waitpid(i, &status, 0);
|
||||
}
|
||||
|
||||
this->pid_children_.clear();
|
||||
|
||||
bool can_update = true;
|
||||
if (config_["exec-if"].isString()) {
|
||||
output_ = util::command::execNoRead(config_["exec-if"].asString());
|
||||
if (output_.exit_code != 0) {
|
||||
can_update = false;
|
||||
dp.emit();
|
||||
}
|
||||
}
|
||||
if (can_update) {
|
||||
if (config_["exec"].isString()) {
|
||||
output_ = util::command::exec(config_["exec"].asString(), output_name_);
|
||||
}
|
||||
dp.emit();
|
||||
}
|
||||
thread_.sleep_for(interval_);
|
||||
};
|
||||
}
|
||||
|
||||
void waybar::modules::CustomGraph::continuousWorker() {
|
||||
auto cmd = config_["exec"].asString();
|
||||
pid_ = -1;
|
||||
fp_ = util::command::open(cmd, pid_, output_name_);
|
||||
if (!fp_) {
|
||||
throw std::runtime_error("Unable to open " + cmd);
|
||||
}
|
||||
thread_ = [this, cmd] {
|
||||
char* buff = nullptr;
|
||||
waybar::util::ScopeGuard buff_deleter([&buff]() {
|
||||
if (buff) {
|
||||
free(buff);
|
||||
}
|
||||
});
|
||||
size_t len = 0;
|
||||
if (getline(&buff, &len, fp_) == -1) {
|
||||
int exit_code = 1;
|
||||
if (fp_) {
|
||||
exit_code = WEXITSTATUS(util::command::close(fp_, pid_));
|
||||
fp_ = nullptr;
|
||||
}
|
||||
if (exit_code != 0) {
|
||||
output_ = {exit_code, ""};
|
||||
dp.emit();
|
||||
spdlog::error("{} stopped unexpectedly, is it endless?", name_);
|
||||
}
|
||||
if (config_["restart-interval"].isUInt()) {
|
||||
pid_ = -1;
|
||||
thread_.sleep_for(std::chrono::seconds(config_["restart-interval"].asUInt()));
|
||||
fp_ = util::command::open(cmd, pid_, output_name_);
|
||||
if (!fp_) {
|
||||
throw std::runtime_error("Unable to open " + cmd);
|
||||
}
|
||||
} else {
|
||||
thread_.stop();
|
||||
return;
|
||||
}
|
||||
} else {
|
||||
std::string output = buff;
|
||||
|
||||
// Remove last newline
|
||||
if (!output.empty() && output[output.length() - 1] == '\n') {
|
||||
output.erase(output.length() - 1);
|
||||
}
|
||||
output_ = {0, output};
|
||||
dp.emit();
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
void waybar::modules::CustomGraph::waitingWorker() {
|
||||
thread_ = [this] {
|
||||
bool can_update = true;
|
||||
if (config_["exec-if"].isString()) {
|
||||
output_ = util::command::execNoRead(config_["exec-if"].asString());
|
||||
if (output_.exit_code != 0) {
|
||||
can_update = false;
|
||||
dp.emit();
|
||||
}
|
||||
}
|
||||
if (can_update) {
|
||||
if (config_["exec"].isString()) {
|
||||
output_ = util::command::exec(config_["exec"].asString(), output_name_);
|
||||
}
|
||||
dp.emit();
|
||||
}
|
||||
thread_.sleep();
|
||||
};
|
||||
}
|
||||
|
||||
void waybar::modules::CustomGraph::refresh(int sig) {
|
||||
if (sig == SIGRTMIN + config_["signal"].asInt()) {
|
||||
thread_.wake_up();
|
||||
}
|
||||
}
|
||||
|
||||
void waybar::modules::CustomGraph::handleEvent() {
|
||||
if (!config_["exec-on-event"].isBool() || config_["exec-on-event"].asBool()) {
|
||||
thread_.wake_up();
|
||||
}
|
||||
}
|
||||
|
||||
bool waybar::modules::CustomGraph::handleScroll(GdkEventScroll* e) {
|
||||
auto ret = AGraph::handleScroll(e);
|
||||
handleEvent();
|
||||
return ret;
|
||||
}
|
||||
|
||||
bool waybar::modules::CustomGraph::handleToggle(GdkEventButton* const& e) {
|
||||
auto ret = AGraph::handleToggle(e);
|
||||
handleEvent();
|
||||
return ret;
|
||||
}
|
||||
|
||||
auto waybar::modules::CustomGraph::update() -> void {
|
||||
// Hide label if output is empty
|
||||
if ((config_["exec"].isString() || config_["exec-if"].isString()) &&
|
||||
(output_.out.empty() || output_.exit_code != 0)) {
|
||||
event_box_.hide();
|
||||
} else {
|
||||
if (config_["return-type"].asString() == "json") {
|
||||
parseOutputJson();
|
||||
} else {
|
||||
parseOutputRaw();
|
||||
}
|
||||
|
||||
try {
|
||||
addValue(percentage_);
|
||||
|
||||
if (tooltipEnabled()) {
|
||||
if (tooltip_format_enabled_) {
|
||||
auto tooltip = config_["tooltip-format"].asString();
|
||||
tooltip = fmt::format(fmt::runtime(tooltip), fmt::arg("text", text_),
|
||||
fmt::arg("alt", alt_), fmt::arg("percentage", percentage_));
|
||||
graph_.set_tooltip_markup(tooltip);
|
||||
} else {
|
||||
if (graph_.get_tooltip_markup() != tooltip_) {
|
||||
graph_.set_tooltip_markup(tooltip_);
|
||||
}
|
||||
}
|
||||
}
|
||||
auto style = graph_.get_style_context();
|
||||
auto classes = style->list_classes();
|
||||
for (auto const& c : classes) {
|
||||
if (c == id_) continue;
|
||||
style->remove_class(c);
|
||||
}
|
||||
for (auto const& c : class_) {
|
||||
style->add_class(c);
|
||||
}
|
||||
style->add_class("flat");
|
||||
style->add_class(MODULE_CLASS);
|
||||
event_box_.show();
|
||||
} catch (const fmt::format_error& e) {
|
||||
if (std::strcmp(e.what(), "cannot switch from manual to automatic argument indexing") != 0)
|
||||
throw;
|
||||
|
||||
throw fmt::format_error(
|
||||
"mixing manual and automatic argument indexing is no longer supported; "
|
||||
"try replacing \"{}\" with \"{text}\" in your format specifier");
|
||||
}
|
||||
}
|
||||
// Call parent update
|
||||
AGraph::update();
|
||||
}
|
||||
|
||||
void waybar::modules::CustomGraph::parseOutputRaw() {
|
||||
std::istringstream output(output_.out);
|
||||
std::string line;
|
||||
int i = 0;
|
||||
while (getline(output, line)) {
|
||||
Glib::ustring validated_line = line;
|
||||
if (!validated_line.validate()) {
|
||||
validated_line = validated_line.make_valid();
|
||||
}
|
||||
|
||||
if (i == 0) {
|
||||
if (config_["escape"].isBool() && config_["escape"].asBool()) {
|
||||
text_ = Glib::Markup::escape_text(validated_line);
|
||||
tooltip_ = Glib::Markup::escape_text(validated_line);
|
||||
} else {
|
||||
text_ = validated_line;
|
||||
tooltip_ = validated_line;
|
||||
}
|
||||
class_.clear();
|
||||
} else if (i == 1) {
|
||||
if (config_["escape"].isBool() && config_["escape"].asBool()) {
|
||||
tooltip_ = Glib::Markup::escape_text(validated_line);
|
||||
} else {
|
||||
tooltip_ = validated_line;
|
||||
}
|
||||
} else if (i == 2) {
|
||||
class_.push_back(validated_line);
|
||||
} else {
|
||||
break;
|
||||
}
|
||||
i++;
|
||||
}
|
||||
}
|
||||
|
||||
void waybar::modules::CustomGraph::parseOutputJson() {
|
||||
std::istringstream output(output_.out);
|
||||
std::string line;
|
||||
class_.clear();
|
||||
while (getline(output, line)) {
|
||||
auto parsed = parser_.parse(line);
|
||||
if (config_["escape"].isBool() && config_["escape"].asBool()) {
|
||||
text_ = Glib::Markup::escape_text(parsed["text"].asString());
|
||||
} else {
|
||||
text_ = parsed["text"].asString();
|
||||
}
|
||||
if (config_["escape"].isBool() && config_["escape"].asBool()) {
|
||||
alt_ = Glib::Markup::escape_text(parsed["alt"].asString());
|
||||
} else {
|
||||
alt_ = parsed["alt"].asString();
|
||||
}
|
||||
if (config_["escape"].isBool() && config_["escape"].asBool()) {
|
||||
tooltip_ = Glib::Markup::escape_text(parsed["tooltip"].asString());
|
||||
} else {
|
||||
tooltip_ = parsed["tooltip"].asString();
|
||||
}
|
||||
if (parsed["class"].isString()) {
|
||||
class_.push_back(parsed["class"].asString());
|
||||
} else if (parsed["class"].isArray()) {
|
||||
for (auto const& c : parsed["class"]) {
|
||||
class_.push_back(c.asString());
|
||||
}
|
||||
}
|
||||
if (!parsed["percentage"].asString().empty() && parsed["percentage"].isNumeric()) {
|
||||
percentage_ = (int)lround(parsed["percentage"].asFloat());
|
||||
} else {
|
||||
percentage_ = 0;
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
+111
-62
@@ -1,15 +1,32 @@
|
||||
#include "modules/disk.hpp"
|
||||
|
||||
#include <spdlog/spdlog.h>
|
||||
|
||||
using namespace waybar::util;
|
||||
|
||||
waybar::modules::Disk::Disk(const std::string& id, const Json::Value& config)
|
||||
: ALabel(config, "disk", id, "{}%", 30), path_("/") {
|
||||
: ALabel(config, "disk", id, "{}%", 30), header_(""), paths_(), separator_(" ") {
|
||||
thread_ = [this] {
|
||||
dp.emit();
|
||||
thread_.sleep_for(interval_);
|
||||
};
|
||||
if (config["path"].isString()) {
|
||||
path_ = config["path"].asString();
|
||||
if (config["header"].isString()) {
|
||||
header_ = config["header"].asString();
|
||||
}
|
||||
if (config["path"].isString() && !config["paths"].isArray()) {
|
||||
spdlog::warn("Disk: path is deprecated use paths instead!");
|
||||
paths_.push_back(config["path"].asString());
|
||||
}
|
||||
if (config["paths"].isArray()) {
|
||||
for (const auto& path : config["paths"]) {
|
||||
paths_.push_back(path.asString());
|
||||
}
|
||||
}
|
||||
if (!config["path"].isString() && !config["paths"].isArray()) {
|
||||
paths_.emplace_back("/");
|
||||
}
|
||||
if (config["separator"].isString()) {
|
||||
separator_ = config["separator"].asString();
|
||||
}
|
||||
if (config["unit"].isString()) {
|
||||
unit_ = config["unit"].asString();
|
||||
@@ -17,76 +34,108 @@ waybar::modules::Disk::Disk(const std::string& id, const Json::Value& config)
|
||||
}
|
||||
|
||||
auto waybar::modules::Disk::update() -> void {
|
||||
struct statvfs /* {
|
||||
unsigned long f_bsize; // filesystem block size
|
||||
unsigned long f_frsize; // fragment size
|
||||
fsblkcnt_t f_blocks; // size of fs in f_frsize units
|
||||
fsblkcnt_t f_bfree; // # free blocks
|
||||
fsblkcnt_t f_bavail; // # free blocks for unprivileged users
|
||||
fsfilcnt_t f_files; // # inodes
|
||||
fsfilcnt_t f_ffree; // # free inodes
|
||||
fsfilcnt_t f_favail; // # free inodes for unprivileged users
|
||||
unsigned long f_fsid; // filesystem ID
|
||||
unsigned long f_flag; // mount flags
|
||||
unsigned long f_namemax; // maximum filename length
|
||||
}; */
|
||||
stats;
|
||||
int err = statvfs(path_.c_str(), &stats);
|
||||
std::string tooltip_label;
|
||||
std::string label = header_;
|
||||
|
||||
/* Conky options
|
||||
fs_bar - Bar that shows how much space is used
|
||||
fs_free - Free space on a file system
|
||||
fs_free_perc - Free percentage of space
|
||||
fs_size - File system size
|
||||
fs_used - File system used space
|
||||
*/
|
||||
bool had_valid_disk = false;
|
||||
|
||||
if (err != 0 || stats.f_blocks == 0) {
|
||||
event_box_.hide();
|
||||
return;
|
||||
}
|
||||
for (size_t i = 0; i < paths_.size(); ++i) {
|
||||
const auto& path = paths_[i];
|
||||
|
||||
float specific_free, specific_used, specific_total, divisor;
|
||||
struct statvfs /* {
|
||||
unsigned long f_bsize; // filesystem block size
|
||||
unsigned long f_frsize; // fragment size
|
||||
fsblkcnt_t f_blocks; // size of fs in f_frsize units
|
||||
fsblkcnt_t f_bfree; // # free blocks
|
||||
fsblkcnt_t f_bavail; // # free blocks for unprivileged users
|
||||
fsfilcnt_t f_files; // # inodes
|
||||
fsfilcnt_t f_ffree; // # free inodes
|
||||
fsfilcnt_t f_favail; // # free inodes for unprivileged users
|
||||
unsigned long f_fsid; // filesystem ID
|
||||
unsigned long f_flag; // mount flags
|
||||
unsigned long f_namemax; // maximum filename length
|
||||
}; */
|
||||
stats;
|
||||
|
||||
divisor = calc_specific_divisor(unit_);
|
||||
specific_free = (stats.f_bavail * stats.f_frsize) / divisor;
|
||||
specific_used = ((stats.f_blocks - stats.f_bfree) * stats.f_frsize) / divisor;
|
||||
specific_total = (stats.f_blocks * stats.f_frsize) / divisor;
|
||||
int err = statvfs(path.c_str(), &stats);
|
||||
|
||||
auto free = pow_format(stats.f_bavail * stats.f_frsize, "B", true);
|
||||
auto used = pow_format((stats.f_blocks - stats.f_bfree) * stats.f_frsize, "B", true);
|
||||
auto total = pow_format(stats.f_blocks * stats.f_frsize, "B", true);
|
||||
auto percentage_used = (stats.f_blocks - stats.f_bfree) * 100 / stats.f_blocks;
|
||||
/* Conky options
|
||||
fs_bar - Bar that shows how much space is used
|
||||
fs_free - Free space on a file system
|
||||
fs_free_perc - Free percentage of space
|
||||
fs_size - File system size
|
||||
fs_used - File system used space
|
||||
*/
|
||||
|
||||
auto format = format_;
|
||||
auto state = getState(percentage_used);
|
||||
if (!state.empty() && config_["format-" + state].isString()) {
|
||||
format = config_["format-" + state].asString();
|
||||
}
|
||||
if (err != 0 || stats.f_blocks == 0) {
|
||||
spdlog::warn("Disk: statvfs failed for path '{}' (errno={})", path, errno);
|
||||
continue;
|
||||
}
|
||||
|
||||
if (format.empty()) {
|
||||
event_box_.hide();
|
||||
} else {
|
||||
event_box_.show();
|
||||
label_.set_markup(fmt::format(
|
||||
fmt::runtime(format), stats.f_bavail * 100 / stats.f_blocks, fmt::arg("free", free),
|
||||
fmt::arg("percentage_free", stats.f_bavail * 100 / stats.f_blocks), fmt::arg("used", used),
|
||||
fmt::arg("percentage_used", percentage_used), fmt::arg("total", total),
|
||||
fmt::arg("path", path_), fmt::arg("specific_free", specific_free),
|
||||
fmt::arg("specific_used", specific_used), fmt::arg("specific_total", specific_total)));
|
||||
}
|
||||
float specific_free, specific_used, specific_total, divisor;
|
||||
|
||||
divisor = calc_specific_divisor(unit_);
|
||||
specific_free = (stats.f_bavail * stats.f_frsize) / divisor;
|
||||
specific_used = ((stats.f_blocks - stats.f_bfree) * stats.f_frsize) / divisor;
|
||||
specific_total = (stats.f_blocks * stats.f_frsize) / divisor;
|
||||
|
||||
auto free = pow_format(stats.f_bavail * stats.f_frsize, "B", true);
|
||||
auto used = pow_format((stats.f_blocks - stats.f_bfree) * stats.f_frsize, "B", true);
|
||||
auto total = pow_format(stats.f_blocks * stats.f_frsize, "B", true);
|
||||
auto percentage_used = (stats.f_blocks - stats.f_bfree) * 100 / stats.f_blocks;
|
||||
|
||||
std::string disk_format = format_;
|
||||
auto state = getState(percentage_used);
|
||||
if (!state.empty() && config_["format-" + state].isString()) {
|
||||
disk_format = config_["format-" + state].asString();
|
||||
}
|
||||
|
||||
if (!disk_format.empty()) {
|
||||
if (had_valid_disk) {
|
||||
label += separator_;
|
||||
}
|
||||
|
||||
label += fmt::format(
|
||||
fmt::runtime(disk_format), stats.f_bavail * 100 / stats.f_blocks, fmt::arg("free", free),
|
||||
fmt::arg("percentage_free", stats.f_bavail * 100 / stats.f_blocks),
|
||||
fmt::arg("used", used), fmt::arg("percentage_used", percentage_used),
|
||||
fmt::arg("total", total), fmt::arg("path", path),
|
||||
fmt::arg("specific_free", specific_free), fmt::arg("specific_used", specific_used),
|
||||
fmt::arg("specific_total", specific_total));
|
||||
}
|
||||
|
||||
if (tooltipEnabled()) {
|
||||
std::string tooltip_format = "{used} used out of {total} on {path} ({percentage_used}%)";
|
||||
if (config_["tooltip-format"].isString()) {
|
||||
tooltip_format = config_["tooltip-format"].asString();
|
||||
}
|
||||
label_.set_tooltip_markup(fmt::format(
|
||||
fmt::runtime(tooltip_format), stats.f_bavail * 100 / stats.f_blocks, fmt::arg("free", free),
|
||||
fmt::arg("percentage_free", stats.f_bavail * 100 / stats.f_blocks), fmt::arg("used", used),
|
||||
fmt::arg("percentage_used", percentage_used), fmt::arg("total", total),
|
||||
fmt::arg("path", path_), fmt::arg("specific_free", specific_free),
|
||||
fmt::arg("specific_used", specific_used), fmt::arg("specific_total", specific_total)));
|
||||
|
||||
if (!tooltip_format.empty()) {
|
||||
if (had_valid_disk) {
|
||||
tooltip_label += "\n";
|
||||
}
|
||||
|
||||
tooltip_label += fmt::format(
|
||||
fmt::runtime(tooltip_format), stats.f_bavail * 100 / stats.f_blocks,
|
||||
fmt::arg("free", free),
|
||||
fmt::arg("percentage_free", stats.f_bavail * 100 / stats.f_blocks),
|
||||
fmt::arg("used", used), fmt::arg("percentage_used", percentage_used),
|
||||
fmt::arg("total", total), fmt::arg("path", path),
|
||||
fmt::arg("specific_free", specific_free), fmt::arg("specific_used", specific_used),
|
||||
fmt::arg("specific_total", specific_total));
|
||||
}
|
||||
|
||||
had_valid_disk = true;
|
||||
}
|
||||
if (had_valid_disk) {
|
||||
event_box_.show();
|
||||
} else {
|
||||
event_box_.hide();
|
||||
}
|
||||
|
||||
setLabelMarkup(label);
|
||||
|
||||
if (tooltipEnabled() && !tooltip_label.empty()) {
|
||||
setTooltipMarkup(tooltip_label);
|
||||
}
|
||||
// Call parent update
|
||||
ALabel::update();
|
||||
@@ -109,7 +158,7 @@ float waybar::modules::Disk::calc_specific_divisor(const std::string& divisor) {
|
||||
return 1000.0 * 1000.0 * 1000.0 * 1000.0;
|
||||
} else if (divisor == "TiB") {
|
||||
return 1024.0 * 1024.0 * 1024.0 * 1024.0;
|
||||
} else { // default to Bytes if it is anything that we don't recongnise
|
||||
} else { // default to Bytes if it is anything that we don't recognise
|
||||
return 1.0;
|
||||
}
|
||||
}
|
||||
|
||||
+44
-23
@@ -26,7 +26,7 @@ static void toggle_visibility(void* data, zdwl_ipc_output_v2* zdwl_output_v2) {
|
||||
}
|
||||
|
||||
static void active(void* data, zdwl_ipc_output_v2* zdwl_output_v2, uint32_t active) {
|
||||
// Intentionally empty
|
||||
static_cast<Tags*>(data)->handle_active_output(zdwl_output_v2, active);
|
||||
}
|
||||
|
||||
static void set_tag(void* data, zdwl_ipc_output_v2* zdwl_output_v2, uint32_t tag, uint32_t state,
|
||||
@@ -70,32 +70,31 @@ static const zdwl_ipc_output_v2_listener output_status_listener_impl{
|
||||
|
||||
static void handle_global(void* data, struct wl_registry* registry, uint32_t name,
|
||||
const char* interface, uint32_t version) {
|
||||
|
||||
if (std::strcmp(interface, zdwl_ipc_manager_v2_interface.name) == 0) {
|
||||
auto* self = static_cast<Tags*>(data);
|
||||
if (std::strcmp(interface, zdwl_ipc_manager_v2_interface.name) == 0) {
|
||||
auto* self = static_cast<Tags*>(data);
|
||||
|
||||
if (self->status_manager_) {
|
||||
zdwl_ipc_manager_v2_destroy(self->status_manager_);
|
||||
self->status_manager_ = nullptr;
|
||||
}
|
||||
if (self->status_manager_) {
|
||||
zdwl_ipc_manager_v2_destroy(self->status_manager_);
|
||||
self->status_manager_ = nullptr;
|
||||
}
|
||||
|
||||
self->status_manager_ = static_cast<struct zdwl_ipc_manager_v2*>(
|
||||
wl_registry_bind(registry, name, &zdwl_ipc_manager_v2_interface, 1));
|
||||
}
|
||||
|
||||
if (std::strcmp(interface, wl_seat_interface.name) == 0) {
|
||||
auto* self = static_cast<Tags*>(data);
|
||||
|
||||
if (self->seat_) {
|
||||
wl_seat_destroy(self->seat_);
|
||||
self->seat_ = nullptr;
|
||||
self->status_manager_ = static_cast<struct zdwl_ipc_manager_v2*>(
|
||||
wl_registry_bind(registry, name, &zdwl_ipc_manager_v2_interface, 1));
|
||||
}
|
||||
|
||||
version = std::min<uint32_t>(version, 1);
|
||||
if (std::strcmp(interface, wl_seat_interface.name) == 0) {
|
||||
auto* self = static_cast<Tags*>(data);
|
||||
|
||||
self->seat_ = static_cast<struct wl_seat*>(
|
||||
wl_registry_bind(registry, name, &wl_seat_interface, version));
|
||||
}
|
||||
if (self->seat_) {
|
||||
wl_seat_destroy(self->seat_);
|
||||
self->seat_ = nullptr;
|
||||
}
|
||||
|
||||
version = std::min<uint32_t>(version, 1);
|
||||
|
||||
self->seat_ =
|
||||
static_cast<struct wl_seat*>(wl_registry_bind(registry, name, &wl_seat_interface, version));
|
||||
}
|
||||
}
|
||||
static void handle_global_remove(void* data, struct wl_registry* registry, uint32_t name) {
|
||||
/* Ignore event */
|
||||
@@ -110,7 +109,11 @@ Tags::Tags(const std::string& id, const waybar::Bar& bar, const Json::Value& con
|
||||
seat_{nullptr},
|
||||
bar_(bar),
|
||||
box_{bar.orientation, 0},
|
||||
hide_vacant_(false),
|
||||
output_status_{nullptr} {
|
||||
if (config_["hide-vacant"].asBool()) {
|
||||
hide_vacant_ = config_["hide-vacant"].asBool();
|
||||
}
|
||||
struct wl_display* display = Client::inst()->wl_display;
|
||||
struct wl_registry* registry = wl_display_get_registry(display);
|
||||
|
||||
@@ -163,7 +166,7 @@ Tags::Tags(const std::string& id, const waybar::Bar& bar, const Json::Value& con
|
||||
i <<= 1;
|
||||
}
|
||||
|
||||
struct wl_output* output = gdk_wayland_monitor_get_wl_output(bar_.output->monitor->gobj());
|
||||
struct wl_output *output = gdk_wayland_monitor_get_wl_output(bar_.output->monitor->gobj());
|
||||
output_status_ = zdwl_ipc_manager_v2_get_output(status_manager_, output);
|
||||
zdwl_ipc_output_v2_add_listener(output_status_, &output_status_listener_impl, this);
|
||||
|
||||
@@ -221,6 +224,24 @@ void Tags::handle_view_tags(uint32_t tag, uint32_t state, uint32_t clients, uint
|
||||
} else {
|
||||
button.get_style_context()->remove_class("urgent");
|
||||
}
|
||||
|
||||
if (hide_vacant_ && !clients && !(state & TAG_ACTIVE)) {
|
||||
button.set_visible(false);
|
||||
} else {
|
||||
button.set_visible(true);
|
||||
}
|
||||
}
|
||||
|
||||
void Tags::handle_active_output(zdwl_ipc_output_v2* zdwl_output_v2, uint32_t active) {
|
||||
if (output_status_ == zdwl_output_v2) {
|
||||
for (size_t i = 0; i < buttons_.size(); ++i) {
|
||||
if (active == 0) {
|
||||
buttons_[i].get_style_context()->remove_class("output");
|
||||
} else {
|
||||
buttons_[i].get_style_context()->add_class("output");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
} /* namespace waybar::modules::dwl */
|
||||
|
||||
@@ -19,7 +19,7 @@ static void toggle_visibility(void* data, zdwl_ipc_output_v2* zdwl_output_v2) {
|
||||
}
|
||||
|
||||
static void active(void* data, zdwl_ipc_output_v2* zdwl_output_v2, uint32_t active) {
|
||||
// Intentionally empty
|
||||
static_cast<Window*>(data)->handle_active(active);
|
||||
}
|
||||
|
||||
static void set_tag(void* data, zdwl_ipc_output_v2* zdwl_output_v2, uint32_t tag, uint32_t state,
|
||||
@@ -74,7 +74,17 @@ static const wl_registry_listener registry_listener_impl = {.global = handle_glo
|
||||
.global_remove = handle_global_remove};
|
||||
|
||||
Window::Window(const std::string& id, const Bar& bar, const Json::Value& config)
|
||||
: AAppIconLabel(config, "window", id, "{}", 0, true), bar_(bar) {
|
||||
: AAppIconLabel(config, "window", id, "{}", 0, true),
|
||||
bar_(bar),
|
||||
active_(false),
|
||||
hide_inactive_(false),
|
||||
hide_empty_(false) {
|
||||
if (config_["hide-inactive"].isBool()) {
|
||||
hide_inactive_ = config["hide-inactive"].asBool();
|
||||
}
|
||||
if (config_["hide-empty"].isBool()) {
|
||||
hide_empty_ = config["hide-empty"].asBool();
|
||||
}
|
||||
struct wl_display* display = Client::inst()->wl_display;
|
||||
struct wl_registry* registry = wl_display_get_registry(display);
|
||||
|
||||
@@ -102,6 +112,8 @@ void Window::handle_title(const char* title) { title_ = Glib::Markup::escape_tex
|
||||
|
||||
void Window::handle_appid(const char* appid) { appid_ = Glib::Markup::escape_text(appid); }
|
||||
|
||||
void Window::handle_active(const uint32_t active) { active_ = active != 0; }
|
||||
|
||||
void Window::handle_layout_symbol(const char* layout_symbol) {
|
||||
layout_symbol_ = Glib::Markup::escape_text(layout_symbol);
|
||||
}
|
||||
@@ -118,6 +130,19 @@ void Window::handle_frame() {
|
||||
if (tooltipEnabled()) {
|
||||
label_.set_tooltip_markup(title_);
|
||||
}
|
||||
if (hide_empty_ && title_.empty()) {
|
||||
box_.set_visible(false);
|
||||
} else {
|
||||
if (active_) {
|
||||
box_.get_style_context()->add_class("active");
|
||||
box_.set_visible(true);
|
||||
} else {
|
||||
box_.get_style_context()->remove_class("active");
|
||||
if (hide_inactive_) {
|
||||
box_.set_visible(false);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
} // namespace waybar::modules::dwl
|
||||
|
||||
@@ -210,10 +210,7 @@ auto Gamemode::update() -> void {
|
||||
lastStatus = status;
|
||||
|
||||
// Tooltip
|
||||
if (tooltip) {
|
||||
std::string text = fmt::format(fmt::runtime(tooltip_format), fmt::arg("count", gameCount));
|
||||
box_.set_tooltip_markup(text);
|
||||
}
|
||||
updateTooltip(box_, tooltip_format, fmt::arg("count", gameCount));
|
||||
|
||||
// Label format
|
||||
std::string str = fmt::format(fmt::runtime(showAltText ? format_alt : format),
|
||||
|
||||
+13
-43
@@ -141,7 +141,7 @@ auto waybar::modules::Gps::update() -> void {
|
||||
// Show the module
|
||||
if (!event_box_.get_visible()) event_box_.set_visible(true);
|
||||
|
||||
std::string tooltip_format;
|
||||
std::string tooltip_state;
|
||||
|
||||
if (!alt_) {
|
||||
auto state = getFixModeName();
|
||||
@@ -155,57 +155,27 @@ auto waybar::modules::Gps::update() -> void {
|
||||
} else {
|
||||
default_format_ = DEFAULT_FORMAT;
|
||||
}
|
||||
if (config_["tooltip-format-" + state].isString()) {
|
||||
tooltip_format = config_["tooltip-format-" + state].asString();
|
||||
}
|
||||
if (!label_.get_style_context()->has_class(state)) {
|
||||
label_.get_style_context()->add_class(state);
|
||||
}
|
||||
format_ = default_format_;
|
||||
state_ = state;
|
||||
tooltip_state = state;
|
||||
}
|
||||
|
||||
auto format = format_;
|
||||
|
||||
fmt::dynamic_format_arg_store<fmt::format_context> store;
|
||||
store.push_back(fmt::arg("mode", getFixModeString()));
|
||||
store.push_back(fmt::arg("status", getFixStatusString()));
|
||||
|
||||
store.push_back(fmt::arg("latitude", gps_data_.fix.latitude));
|
||||
store.push_back(fmt::arg("latitude_error", gps_data_.fix.epy));
|
||||
|
||||
store.push_back(fmt::arg("longitude", gps_data_.fix.longitude));
|
||||
store.push_back(fmt::arg("longitude_error", gps_data_.fix.epx));
|
||||
|
||||
store.push_back(fmt::arg("altitude_hae", gps_data_.fix.altHAE));
|
||||
store.push_back(fmt::arg("altitude_msl", gps_data_.fix.altMSL));
|
||||
store.push_back(fmt::arg("altitude_error", gps_data_.fix.epv));
|
||||
|
||||
store.push_back(fmt::arg("speed", gps_data_.fix.speed));
|
||||
store.push_back(fmt::arg("speed_error", gps_data_.fix.eps));
|
||||
|
||||
store.push_back(fmt::arg("climb", gps_data_.fix.climb));
|
||||
store.push_back(fmt::arg("climb_error", gps_data_.fix.epc));
|
||||
|
||||
store.push_back(fmt::arg("satellites_used", gps_data_.satellites_used));
|
||||
store.push_back(fmt::arg("satellites_visible", gps_data_.satellites_visible));
|
||||
|
||||
auto text = fmt::vformat(format, store);
|
||||
|
||||
if (tooltipEnabled()) {
|
||||
if (tooltip_format.empty() && config_["tooltip-format"].isString()) {
|
||||
tooltip_format = config_["tooltip-format"].asString();
|
||||
}
|
||||
if (!tooltip_format.empty()) {
|
||||
auto tooltip_text = fmt::vformat(tooltip_format, store);
|
||||
if (label_.get_tooltip_text() != tooltip_text) {
|
||||
label_.set_tooltip_markup(tooltip_text);
|
||||
}
|
||||
} else if (label_.get_tooltip_text() != text) {
|
||||
label_.set_tooltip_markup(text);
|
||||
}
|
||||
}
|
||||
label_.set_markup(text);
|
||||
updateLabelAndTooltipForState(
|
||||
tooltip_state, format, format, fmt::arg("mode", getFixModeString()),
|
||||
fmt::arg("status", getFixStatusString()), fmt::arg("latitude", gps_data_.fix.latitude),
|
||||
fmt::arg("latitude_error", gps_data_.fix.epy), fmt::arg("longitude", gps_data_.fix.longitude),
|
||||
fmt::arg("longitude_error", gps_data_.fix.epx),
|
||||
fmt::arg("altitude_hae", gps_data_.fix.altHAE),
|
||||
fmt::arg("altitude_msl", gps_data_.fix.altMSL), fmt::arg("altitude_error", gps_data_.fix.epv),
|
||||
fmt::arg("speed", gps_data_.fix.speed), fmt::arg("speed_error", gps_data_.fix.eps),
|
||||
fmt::arg("climb", gps_data_.fix.climb), fmt::arg("climb_error", gps_data_.fix.epc),
|
||||
fmt::arg("satellites_used", gps_data_.satellites_used),
|
||||
fmt::arg("satellites_visible", gps_data_.satellites_visible));
|
||||
// Call parent update
|
||||
ALabel::update();
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -48,16 +48,61 @@ 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);
|
||||
setLabelMarkup(layoutName);
|
||||
if (tooltip_enabled) {
|
||||
setTooltipMarkup(tooltipContent);
|
||||
}
|
||||
} else {
|
||||
label_.hide();
|
||||
}
|
||||
|
||||
// Tooltip support
|
||||
if (tooltipEnabled()) {
|
||||
std::string tooltipFormat;
|
||||
if (config_["tooltip-format"].isString()) {
|
||||
tooltipFormat = config_["tooltip-format"].asString();
|
||||
} else {
|
||||
tooltipFormat = "{long}";
|
||||
}
|
||||
auto tooltipText =
|
||||
trim(fmt::format(fmt::runtime(tooltipFormat), fmt::arg("long", layout_.full_name),
|
||||
fmt::arg("short", layout_.short_name),
|
||||
fmt::arg("shortDescription", layout_.short_description),
|
||||
fmt::arg("variant", layout_.variant)));
|
||||
label_.set_tooltip_text(tooltipText);
|
||||
} else {
|
||||
label_.set_tooltip_text("");
|
||||
}
|
||||
|
||||
ALabel::update();
|
||||
}
|
||||
|
||||
@@ -74,8 +119,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,12 +136,21 @@ 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);
|
||||
|
||||
removeXkbLayoutCssClass();
|
||||
layout_ = getLayout(layoutName);
|
||||
addXkbLayoutCssClass();
|
||||
|
||||
spdlog::debug("hyprland language onevent with {}", layoutName);
|
||||
|
||||
@@ -120,6 +172,7 @@ void Language::initLanguage() {
|
||||
searcher = waybar::util::sanitize_string(searcher);
|
||||
|
||||
layout_ = getLayout(searcher);
|
||||
addXkbLayoutCssClass();
|
||||
|
||||
spdlog::debug("hyprland language initLanguage found {}", layout_.full_name);
|
||||
|
||||
@@ -129,6 +182,16 @@ void Language::initLanguage() {
|
||||
}
|
||||
}
|
||||
|
||||
auto Language::removeXkbLayoutCssClass() -> void {
|
||||
label_.get_style_context()->remove_class(layout_.short_name);
|
||||
spdlog::debug("hyprland language try to remove currently short_name css class {}",
|
||||
layout_.short_name);
|
||||
}
|
||||
auto Language::addXkbLayoutCssClass() -> void {
|
||||
label_.get_style_context()->add_class(layout_.short_name);
|
||||
spdlog::debug("hyprland language add new short_name css class {}", layout_.short_name);
|
||||
}
|
||||
|
||||
auto Language::getLayout(const std::string& fullName) -> Layout {
|
||||
auto* const context = rxkb_context_new(RXKB_CONTEXT_LOAD_EXOTIC_RULES);
|
||||
rxkb_context_parse_default_ruleset(context);
|
||||
|
||||
@@ -21,6 +21,18 @@ Submap::Submap(const std::string& id, const Bar& bar, const Json::Value& config)
|
||||
// register for hyprland ipc
|
||||
m_ipc.registerForIPC("submap", this);
|
||||
dp.emit();
|
||||
|
||||
if (config["icons"].isObject()) {
|
||||
const Json::Value& icons = config["icons"];
|
||||
|
||||
for (const std::string& key : icons.getMemberNames()) {
|
||||
const Json::Value& value = icons[key];
|
||||
|
||||
if (value.isString()) {
|
||||
icons_[key] = value.asString();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Submap::~Submap() {
|
||||
@@ -58,7 +70,8 @@ auto Submap::update() -> void {
|
||||
if (submap_.empty()) {
|
||||
event_box_.hide();
|
||||
} else {
|
||||
label_.set_markup(fmt::format(fmt::runtime(format_), submap_));
|
||||
label_.set_markup(
|
||||
fmt::format(fmt::runtime(format_), fmt::arg("submap", submap_), fmt::arg("icon", icon_)));
|
||||
if (tooltipEnabled()) {
|
||||
label_.set_tooltip_markup(submap_);
|
||||
}
|
||||
@@ -84,6 +97,12 @@ void Submap::onEvent(const std::string& ev) {
|
||||
|
||||
submap_ = submapName;
|
||||
|
||||
if (!icons_[submap_].empty()) {
|
||||
icon_ = icons_[submap_];
|
||||
} else {
|
||||
icon_ = "";
|
||||
}
|
||||
|
||||
if (submap_.empty() && always_on_) {
|
||||
submap_ = default_submap_;
|
||||
}
|
||||
|
||||
@@ -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,13 +54,20 @@ 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)),
|
||||
config_["rewrite"]);
|
||||
label_.set_markup(label_text);
|
||||
setLabelMarkup(label_text);
|
||||
} else {
|
||||
label_.hide();
|
||||
}
|
||||
@@ -70,13 +78,12 @@ auto Window::update() -> void {
|
||||
tooltip_format = config_["tooltip-format"].asString();
|
||||
}
|
||||
if (!tooltip_format.empty()) {
|
||||
label_.set_tooltip_markup(
|
||||
fmt::format(fmt::runtime(tooltip_format), fmt::arg("title", windowName),
|
||||
fmt::arg("initialTitle", windowData_.initial_title),
|
||||
fmt::arg("class", windowData_.class_name),
|
||||
fmt::arg("initialClass", windowData_.initial_class_name)));
|
||||
setTooltipMarkup(fmt::format(fmt::runtime(tooltip_format), fmt::arg("title", windowName),
|
||||
fmt::arg("initialTitle", windowData_.initial_title),
|
||||
fmt::arg("class", windowData_.class_name),
|
||||
fmt::arg("initialClass", windowData_.initial_class_name)));
|
||||
} else if (!label_text.empty()) {
|
||||
label_.set_tooltip_markup(label_text);
|
||||
setTooltipMarkup(label_text);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -108,21 +115,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 +132,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()) {
|
||||
@@ -220,9 +221,9 @@ void Window::queryActiveWorkspace() {
|
||||
std::vector<Json::Value> visibleWindows;
|
||||
std::ranges::copy_if(workspaceWindows, std::back_inserter(visibleWindows),
|
||||
[&](const Json::Value& window) { return !window["hidden"].asBool(); });
|
||||
solo_ = 1 == std::count_if(
|
||||
visibleWindows.begin(), visibleWindows.end(),
|
||||
[&](const Json::Value& window) { return !window["floating"].asBool(); });
|
||||
solo_ =
|
||||
1 == std::count_if(visibleWindows.begin(), visibleWindows.end(),
|
||||
[&](const Json::Value& window) { return !window["floating"].asBool(); });
|
||||
allFloating_ = std::ranges::all_of(
|
||||
visibleWindows, [&](const Json::Value& window) { return window["floating"].asBool(); });
|
||||
fullscreen_ = windowData_.fullscreen;
|
||||
|
||||
@@ -1,6 +1,9 @@
|
||||
#include <glibmm/main.h>
|
||||
#include <json/value.h>
|
||||
#include <spdlog/spdlog.h>
|
||||
|
||||
#include <algorithm>
|
||||
#include <cctype>
|
||||
#include <memory>
|
||||
#include <string>
|
||||
#include <utility>
|
||||
@@ -9,6 +12,33 @@
|
||||
#include "util/command.hpp"
|
||||
#include "util/icon_loader.hpp"
|
||||
|
||||
namespace {
|
||||
constexpr std::string_view kCssClassPrefix = "ws-";
|
||||
|
||||
// Convert a workspace name to a valid CSS class name.
|
||||
// Lowercases, replaces non-alphanumeric runs with single hyphens,
|
||||
// and prefixes digit-leading names (CSS classes can't start with a digit).
|
||||
std::string sanitizeCssClass(const std::string& name) {
|
||||
std::string result;
|
||||
result.reserve(name.size() + kCssClassPrefix.size());
|
||||
for (auto c : name) {
|
||||
auto uc = static_cast<unsigned char>(c);
|
||||
if (std::isalnum(uc)) {
|
||||
result += static_cast<char>(std::tolower(uc));
|
||||
} else if (!result.empty() && result.back() != '-') {
|
||||
result += '-';
|
||||
}
|
||||
}
|
||||
if (!result.empty() && result.back() == '-') {
|
||||
result.pop_back();
|
||||
}
|
||||
if (!result.empty() && std::isdigit(static_cast<unsigned char>(result.front()))) {
|
||||
result.insert(0, kCssClassPrefix);
|
||||
}
|
||||
return result;
|
||||
}
|
||||
} // namespace
|
||||
|
||||
namespace waybar::modules::hyprland {
|
||||
|
||||
Workspace::Workspace(const Json::Value& workspace_data, Workspaces& workspace_manager,
|
||||
@@ -30,6 +60,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 +80,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 +107,119 @@ 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 +250,10 @@ 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 +268,9 @@ 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 +282,7 @@ void Workspace::insertWindow(WindowCreationPayload create_window_payload) {
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
bool Workspace::onWindowOpened(WindowCreationPayload const& create_window_payload) {
|
||||
if (create_window_payload.getWorkspaceName() == name()) {
|
||||
@@ -155,13 +295,34 @@ bool Workspace::onWindowOpened(WindowCreationPayload const& create_window_payloa
|
||||
std::string& Workspace::selectIcon(std::map<std::string, std::string>& icons_map) {
|
||||
spdlog::trace("Selecting icon for workspace {}", name());
|
||||
if (isUrgent()) {
|
||||
auto urgentNamedIconIt = icons_map.find("urgent:" + name());
|
||||
if (urgentNamedIconIt != icons_map.end()) {
|
||||
return urgentNamedIconIt->second;
|
||||
}
|
||||
|
||||
auto urgentIconIt = icons_map.find("urgent");
|
||||
if (urgentIconIt != icons_map.end()) {
|
||||
return urgentIconIt->second;
|
||||
}
|
||||
}
|
||||
|
||||
if (isActive() && isSpecial()) {
|
||||
auto activeIconIt = icons_map.find("active:" + name());
|
||||
if (activeIconIt != icons_map.end()) {
|
||||
return activeIconIt->second;
|
||||
}
|
||||
auto namedIconIt = icons_map.find(name());
|
||||
if (namedIconIt != icons_map.end()) {
|
||||
return namedIconIt->second;
|
||||
}
|
||||
}
|
||||
|
||||
if (isActive()) {
|
||||
auto activeNamedIconIt = icons_map.find("active:" + name());
|
||||
if (activeNamedIconIt != icons_map.end()) {
|
||||
return activeNamedIconIt->second;
|
||||
}
|
||||
|
||||
auto activeIconIt = icons_map.find("active");
|
||||
if (activeIconIt != icons_map.end()) {
|
||||
return activeIconIt->second;
|
||||
@@ -169,6 +330,11 @@ std::string& Workspace::selectIcon(std::map<std::string, std::string>& icons_map
|
||||
}
|
||||
|
||||
if (isSpecial()) {
|
||||
auto specialNamedIconIt = icons_map.find("special:" + name());
|
||||
if (specialNamedIconIt != icons_map.end()) {
|
||||
return specialNamedIconIt->second;
|
||||
}
|
||||
|
||||
auto specialIconIt = icons_map.find("special");
|
||||
if (specialIconIt != icons_map.end()) {
|
||||
return specialIconIt->second;
|
||||
@@ -215,6 +381,15 @@ void Workspace::update(const std::string& workspace_icon) {
|
||||
return;
|
||||
}
|
||||
// clang-format off
|
||||
if (this->m_workspaceManager.hideActive() && \
|
||||
this->isActive() && \
|
||||
!this->isPersistent() && \
|
||||
!this->isSpecial()) {
|
||||
// clang-format on
|
||||
m_button.hide();
|
||||
return;
|
||||
}
|
||||
// clang-format off
|
||||
if (this->m_workspaceManager.activeOnly() && \
|
||||
!this->isActive() && \
|
||||
!this->isPersistent() && \
|
||||
@@ -234,26 +409,79 @@ 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");
|
||||
addOrRemoveClass(styleContext, isVisible(), "visible");
|
||||
addOrRemoveClass(styleContext, m_workspaceManager.getBarOutput() == output(), "hosting-monitor");
|
||||
|
||||
// Add workspace name as CSS class for per-workspace styling
|
||||
if (!m_prevNameClass.empty()) {
|
||||
styleContext->remove_class(m_prevNameClass);
|
||||
m_prevNameClass.clear();
|
||||
}
|
||||
auto nameClass = sanitizeCssClass(name());
|
||||
if (!nameClass.empty()) {
|
||||
styleContext->add_class(nameClass);
|
||||
m_prevNameClass = nameClass;
|
||||
}
|
||||
|
||||
std::string windows;
|
||||
// Optimization: The {windows} substitution string is only possible if the taskbar is disabled, no
|
||||
// need to compute this if enableTaskbar() is true
|
||||
if (!m_workspaceManager.enableTaskbar()) {
|
||||
auto windowSeparator = m_workspaceManager.getWindowSeparator();
|
||||
auto groupThreshold = m_workspaceManager.windowRewriteGroupThreshold();
|
||||
|
||||
bool isNotFirst = false;
|
||||
auto end_it = m_workspaceManager.maxWindows() == 0
|
||||
? m_windowMap.end()
|
||||
: m_windowMap.begin() + m_workspaceManager.maxWindows();
|
||||
|
||||
for (const auto& window_repr : m_windowMap) {
|
||||
if (isNotFirst) {
|
||||
windows.append(windowSeparator);
|
||||
if (groupThreshold > 0) {
|
||||
// Build ordered counts of each unique icon (including singular ones when threshold set to 1)
|
||||
std::vector<std::pair<std::string, int>> iconCounts;
|
||||
for (auto it = m_windowMap.begin(); it != end_it; ++it) {
|
||||
const auto& window_repr = *it;
|
||||
auto found = std::ranges::find_if(
|
||||
iconCounts, [&](const auto& p) { return p.first == window_repr.repr_rewrite; });
|
||||
if (found != iconCounts.end()) {
|
||||
found->second++;
|
||||
} else {
|
||||
iconCounts.emplace_back(window_repr.repr_rewrite, 1);
|
||||
}
|
||||
}
|
||||
|
||||
// Format the group string
|
||||
auto groupFormat = m_workspaceManager.getWindowRewriteGroupFormat();
|
||||
bool isNotFirst = false;
|
||||
for (const auto& [icon, count] : iconCounts) {
|
||||
if (count >= groupThreshold) {
|
||||
if (isNotFirst) windows.append(windowSeparator);
|
||||
isNotFirst = true;
|
||||
try {
|
||||
windows.append(fmt::format(fmt::runtime(groupFormat), fmt::arg("icon", icon),
|
||||
fmt::arg("count", count)));
|
||||
} catch (const fmt::format_error& e) {
|
||||
spdlog::warn("Formatting window-rewrite-group-format error: {}", e.what());
|
||||
windows.append(icon);
|
||||
}
|
||||
} else {
|
||||
for (int i = 0; i < count; ++i) {
|
||||
if (isNotFirst) windows.append(windowSeparator);
|
||||
isNotFirst = true;
|
||||
windows.append(icon);
|
||||
}
|
||||
}
|
||||
}
|
||||
} else {
|
||||
// Not grouping icons
|
||||
bool isNotFirst = false;
|
||||
for (auto it = m_windowMap.begin(); it != end_it; ++it) {
|
||||
if (isNotFirst) windows.append(windowSeparator);
|
||||
isNotFirst = true;
|
||||
windows.append(it->repr_rewrite);
|
||||
}
|
||||
isNotFirst = true;
|
||||
windows.append(window_repr.repr_rewrite);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -270,7 +498,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 +531,17 @@ 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,22 +566,32 @@ 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()) {
|
||||
for (auto it = m_windowMap.rbegin(); it != m_windowMap.rend(); ++it) {
|
||||
auto rend_it = m_workspaceManager.maxWindows() == 0
|
||||
? m_windowMap.rend()
|
||||
: m_windowMap.rbegin() + m_workspaceManager.maxWindows();
|
||||
|
||||
for (auto it = m_windowMap.rbegin(); it != rend_it; ++it) {
|
||||
processWindow(*it);
|
||||
}
|
||||
} else {
|
||||
for (const auto& window_repr : m_windowMap) {
|
||||
processWindow(window_repr);
|
||||
auto end_it = m_workspaceManager.maxWindows() == 0
|
||||
? m_windowMap.end()
|
||||
: m_windowMap.begin() + m_workspaceManager.maxWindows();
|
||||
|
||||
for (auto it = m_windowMap.begin(); it != end_it; ++it) {
|
||||
processWindow(*it);
|
||||
}
|
||||
}
|
||||
|
||||
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)));
|
||||
|
||||
@@ -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;
|
||||
@@ -338,46 +343,62 @@ void Workspaces::loadPersistentWorkspacesFromWorkspaceRules(const Json::Value& c
|
||||
}
|
||||
|
||||
void Workspaces::onEvent(const std::string& ev) {
|
||||
std::lock_guard<std::mutex> lock(m_mutex);
|
||||
const auto separator = ev.find(">>");
|
||||
if (separator == std::string::npos) {
|
||||
spdlog::warn("Malformed Hyprland workspace event: {}", ev);
|
||||
return;
|
||||
}
|
||||
std::string eventName = ev.substr(0, separator);
|
||||
std::string payload = ev.substr(separator + 2);
|
||||
{
|
||||
std::lock_guard<std::mutex> lock(m_mutex);
|
||||
const auto separator = ev.find(">>");
|
||||
if (separator == std::string::npos) {
|
||||
spdlog::warn("Malformed Hyprland workspace event: {}", ev);
|
||||
return;
|
||||
}
|
||||
std::string eventName = ev.substr(0, separator);
|
||||
std::string payload = ev.substr(separator + 2);
|
||||
|
||||
if (eventName == "workspacev2") {
|
||||
onWorkspaceActivated(payload);
|
||||
} else if (eventName == "activespecial") {
|
||||
onSpecialWorkspaceActivated(payload);
|
||||
} else if (eventName == "destroyworkspacev2") {
|
||||
onWorkspaceDestroyed(payload);
|
||||
} else if (eventName == "createworkspacev2") {
|
||||
onWorkspaceCreated(payload);
|
||||
} else if (eventName == "focusedmonv2") {
|
||||
onMonitorFocused(payload);
|
||||
} else if (eventName == "moveworkspacev2") {
|
||||
onWorkspaceMoved(payload);
|
||||
} else if (eventName == "openwindow") {
|
||||
onWindowOpened(payload);
|
||||
} else if (eventName == "closewindow") {
|
||||
onWindowClosed(payload);
|
||||
} else if (eventName == "movewindowv2") {
|
||||
onWindowMoved(payload);
|
||||
} else if (eventName == "urgent") {
|
||||
setUrgentWorkspace(payload);
|
||||
} else if (eventName == "renameworkspace") {
|
||||
onWorkspaceRenamed(payload);
|
||||
} else if (eventName == "windowtitlev2") {
|
||||
onWindowTitleEvent(payload);
|
||||
} else if (eventName == "activewindowv2") {
|
||||
onActiveWindowChanged(payload);
|
||||
} else if (eventName == "configreloaded") {
|
||||
onConfigReloaded();
|
||||
if (eventName == "workspacev2") {
|
||||
onWorkspaceActivated(payload);
|
||||
} else if (eventName == "activespecial") {
|
||||
onSpecialWorkspaceActivated(payload);
|
||||
} else if (eventName == "destroyworkspacev2") {
|
||||
onWorkspaceDestroyed(payload);
|
||||
} else if (eventName == "createworkspacev2") {
|
||||
onWorkspaceCreated(payload);
|
||||
} else if (eventName == "focusedmonv2") {
|
||||
onMonitorFocused(payload);
|
||||
} else if (eventName == "moveworkspacev2") {
|
||||
onWorkspaceMoved(payload);
|
||||
} else if (eventName == "openwindow") {
|
||||
onWindowOpened(payload);
|
||||
} else if (eventName == "closewindow") {
|
||||
onWindowClosed(payload);
|
||||
} else if (eventName == "movewindowv2") {
|
||||
onWindowMoved(payload);
|
||||
} else if (eventName == "urgent") {
|
||||
setUrgentWorkspace(payload);
|
||||
} else if (eventName == "renameworkspace") {
|
||||
onWorkspaceRenamed(payload);
|
||||
} else if (eventName == "windowtitlev2") {
|
||||
onWindowTitleEvent(payload);
|
||||
} else if (eventName == "activewindowv2") {
|
||||
onActiveWindowChanged(payload);
|
||||
} else if (eventName == "configreloaded") {
|
||||
onConfigReloaded();
|
||||
}
|
||||
}
|
||||
|
||||
dp.emit();
|
||||
if (m_debounceTimer.connected()) {
|
||||
m_debounceTimer.disconnect();
|
||||
m_updatePending = false;
|
||||
}
|
||||
|
||||
m_updatePending = true;
|
||||
m_debounceTimer = Glib::signal_timeout().connect([this]() {
|
||||
if (!m_updatePending) return false;
|
||||
std::lock_guard<std::mutex> lock(m_mutex);
|
||||
if (m_updatePending) {
|
||||
dp.emit();
|
||||
m_updatePending = false;
|
||||
}
|
||||
return false;
|
||||
}, 7);
|
||||
}
|
||||
|
||||
void Workspaces::onWorkspaceActivated(std::string const& payload) {
|
||||
@@ -657,6 +678,7 @@ auto Workspaces::parseConfig(const Json::Value& config) -> void {
|
||||
populateBoolConfig(config, "special-visible-only", m_specialVisibleOnly);
|
||||
populateBoolConfig(config, "persistent-only", m_persistentOnly);
|
||||
populateBoolConfig(config, "active-only", m_activeOnly);
|
||||
populateBoolConfig(config, "hide-active", m_hideActive);
|
||||
populateBoolConfig(config, "move-to-monitor", m_moveToMonitor);
|
||||
populateBoolConfig(config, "enable-bar-scroll", m_barScroll);
|
||||
|
||||
@@ -664,7 +686,18 @@ auto Workspaces::parseConfig(const Json::Value& config) -> void {
|
||||
populateSortByConfig(config);
|
||||
populateIgnoreWorkspacesConfig(config);
|
||||
populateFormatWindowSeparatorConfig(config);
|
||||
|
||||
const auto& groupThreshold = config["window-rewrite-group-threshold"];
|
||||
if (groupThreshold.isInt()) {
|
||||
m_windowRewriteGroupThreshold = groupThreshold.asInt();
|
||||
}
|
||||
const auto& groupFormat = config["window-rewrite-group-format"];
|
||||
if (groupFormat.isString()) {
|
||||
m_windowRewriteGroupFormat = groupFormat.asString();
|
||||
}
|
||||
|
||||
populateWindowRewriteConfig(config);
|
||||
populateMaxWindowsConfig(config);
|
||||
|
||||
if (withWindows) {
|
||||
populateWorkspaceTaskbarConfig(config);
|
||||
@@ -746,6 +779,15 @@ auto Workspaces::populateWindowRewriteConfig(const Json::Value& config) -> void
|
||||
[this](std::string& window_rule) { return windowRewritePriorityFunction(window_rule); });
|
||||
}
|
||||
|
||||
auto Workspaces::populateMaxWindowsConfig(const Json::Value& config) -> void {
|
||||
if (config["max-windows"].isInt()) {
|
||||
m_maxWindows = config["max-windows"].asInt();
|
||||
if (m_maxWindows < 0) {
|
||||
m_maxWindows = 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
auto Workspaces::populateWorkspaceTaskbarConfig(const Json::Value& config) -> void {
|
||||
const auto& workspaceTaskbar = config["workspace-taskbar"];
|
||||
if (!workspaceTaskbar.isObject()) {
|
||||
@@ -1188,6 +1230,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 +1243,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");
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
+150
-24
@@ -1,5 +1,6 @@
|
||||
#include "modules/idle_inhibitor.hpp"
|
||||
|
||||
#include "ext-idle-notify-v1-client-protocol.h"
|
||||
#include "idle-inhibit-unstable-v1-client-protocol.h"
|
||||
#include "util/command.hpp"
|
||||
|
||||
@@ -11,11 +12,24 @@ waybar::modules::IdleInhibitor::IdleInhibitor(const std::string& id, const Bar&
|
||||
: ALabel(config, "idle_inhibitor", id, "{status}", 0, false, true),
|
||||
bar_(bar),
|
||||
idle_inhibitor_(nullptr),
|
||||
pid_(-1) {
|
||||
idle_notification_(nullptr),
|
||||
idle_timeout_ms_(0),
|
||||
pid_(-1),
|
||||
wait_for_activity_(false) {
|
||||
if (waybar::Client::inst()->idle_inhibit_manager == nullptr) {
|
||||
throw std::runtime_error("idle-inhibit not available");
|
||||
}
|
||||
|
||||
// Read the wait-for-activity config option
|
||||
if (config_["wait-for-activity"].isBool()) {
|
||||
wait_for_activity_ = config_["wait-for-activity"].asBool();
|
||||
|
||||
// Check if ext-idle-notify protocol is available when wait-for-activity is enabled
|
||||
if (wait_for_activity_ && waybar::Client::inst()->idle_notifier == nullptr) {
|
||||
throw std::runtime_error("wait-for-activity requires ext-idle-notify-v1 protocol support");
|
||||
}
|
||||
}
|
||||
|
||||
if (waybar::modules::IdleInhibitor::modules.empty() && config_["start-activated"].isBool() &&
|
||||
config_["start-activated"].asBool() != status) {
|
||||
toggleStatus();
|
||||
@@ -32,6 +46,8 @@ waybar::modules::IdleInhibitor::IdleInhibitor(const std::string& id, const Bar&
|
||||
}
|
||||
|
||||
waybar::modules::IdleInhibitor::~IdleInhibitor() {
|
||||
teardownIdleNotification();
|
||||
|
||||
if (idle_inhibitor_ != nullptr) {
|
||||
zwp_idle_inhibitor_v1_destroy(idle_inhibitor_);
|
||||
idle_inhibitor_ = nullptr;
|
||||
@@ -63,20 +79,24 @@ auto waybar::modules::IdleInhibitor::update() -> void {
|
||||
}
|
||||
|
||||
std::string status_text = status ? "activated" : "deactivated";
|
||||
label_.set_markup(fmt::format(fmt::runtime(format_), fmt::arg("status", status_text),
|
||||
fmt::arg("icon", getIcon(0, status_text))));
|
||||
updateLabelAndTooltipForState(status_text, format_, "{status}", fmt::arg("status", status_text),
|
||||
fmt::arg("icon", getIcon(0, status_text)));
|
||||
label_.get_style_context()->add_class(status_text);
|
||||
if (tooltipEnabled()) {
|
||||
auto config = config_[status ? "tooltip-format-activated" : "tooltip-format-deactivated"];
|
||||
auto tooltip_format = config.isString() ? config.asString() : "{status}";
|
||||
label_.set_tooltip_markup(fmt::format(fmt::runtime(tooltip_format),
|
||||
fmt::arg("status", status_text),
|
||||
fmt::arg("icon", getIcon(0, status_text))));
|
||||
}
|
||||
// Call parent update
|
||||
ALabel::update();
|
||||
}
|
||||
|
||||
auto waybar::modules::IdleInhibitor::refresh(int sig) -> void {
|
||||
if (config_["signal"].isInt() && sig == SIGRTMIN + config_["signal"].asInt()) {
|
||||
toggleStatus();
|
||||
|
||||
// Make all other idle inhibitor modules update
|
||||
for (auto const& module : waybar::modules::IdleInhibitor::modules) {
|
||||
module->update();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void waybar::modules::IdleInhibitor::toggleStatus() {
|
||||
status = !status;
|
||||
|
||||
@@ -88,21 +108,34 @@ void waybar::modules::IdleInhibitor::toggleStatus() {
|
||||
if (status && config_["timeout"].isNumeric()) {
|
||||
auto timeoutMins = config_["timeout"].asDouble();
|
||||
int timeoutSecs = timeoutMins * 60;
|
||||
idle_timeout_ms_ = timeoutSecs * 1000;
|
||||
|
||||
timeout_ = Glib::signal_timeout().connect_seconds(
|
||||
[]() {
|
||||
/* intentionally not tied to a module instance lifetime
|
||||
* as the output with `this` can be disconnected
|
||||
*/
|
||||
spdlog::info("deactivating idle_inhibitor by timeout");
|
||||
status = false;
|
||||
for (auto const& module : waybar::modules::IdleInhibitor::modules) {
|
||||
module->update();
|
||||
}
|
||||
/* disconnect */
|
||||
return false;
|
||||
},
|
||||
timeoutSecs);
|
||||
// If wait-for-activity is enabled, set up idle notification
|
||||
if (wait_for_activity_) {
|
||||
spdlog::debug("idle_inhibitor: wait-for-activity enabled, timeout: {} ms", idle_timeout_ms_);
|
||||
// Tear down any existing notification first to ensure fresh setup
|
||||
teardownIdleNotification();
|
||||
setupIdleNotification();
|
||||
} else {
|
||||
// Original behavior: simple timeout
|
||||
timeout_ = Glib::signal_timeout().connect_seconds(
|
||||
[]() {
|
||||
/* intentionally not tied to a module instance lifetime
|
||||
* as the output with `this` can be disconnected
|
||||
*/
|
||||
spdlog::info("deactivating idle_inhibitor by timeout");
|
||||
status = false;
|
||||
for (auto const& module : waybar::modules::IdleInhibitor::modules) {
|
||||
module->update();
|
||||
}
|
||||
/* disconnect */
|
||||
return false;
|
||||
},
|
||||
timeoutSecs);
|
||||
}
|
||||
} else {
|
||||
// When deactivated, tear down idle notification
|
||||
teardownIdleNotification();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -121,3 +154,96 @@ bool waybar::modules::IdleInhibitor::handleToggle(GdkEventButton* const& e) {
|
||||
ALabel::handleToggle(e);
|
||||
return true;
|
||||
}
|
||||
|
||||
void waybar::modules::IdleInhibitor::handleIdled(void* data,
|
||||
ext_idle_notification_v1* /*notification*/) {
|
||||
spdlog::info("deactivating idle_inhibitor due to user inactivity");
|
||||
status = false;
|
||||
|
||||
// Clean up the notification since we're deactivating
|
||||
auto* self = static_cast<IdleInhibitor*>(data);
|
||||
if (self != nullptr) {
|
||||
self->teardownIdleNotification();
|
||||
}
|
||||
|
||||
for (auto const& module : waybar::modules::IdleInhibitor::modules) {
|
||||
module->update();
|
||||
}
|
||||
}
|
||||
|
||||
void waybar::modules::IdleInhibitor::handleResumed(void* data,
|
||||
ext_idle_notification_v1* /*notification*/) {
|
||||
// User became active again - notification will continue monitoring
|
||||
spdlog::debug("user activity detected, idle_inhibitor still active");
|
||||
}
|
||||
|
||||
void waybar::modules::IdleInhibitor::setupIdleNotification() {
|
||||
spdlog::debug("idle_inhibitor: setting up idle notification");
|
||||
|
||||
// Clean up any existing notification first
|
||||
if (idle_notification_ != nullptr) {
|
||||
spdlog::debug("idle_inhibitor: cleaning up existing notification before setup");
|
||||
teardownIdleNotification();
|
||||
}
|
||||
|
||||
auto* client = waybar::Client::inst();
|
||||
if (client->idle_notifier == nullptr) {
|
||||
spdlog::error("ext-idle-notify protocol not available");
|
||||
return;
|
||||
}
|
||||
|
||||
// Get the wayland seat from the display
|
||||
auto* gdk_seat = gdk_display_get_default_seat(client->gdk_display->gobj());
|
||||
if (gdk_seat == nullptr) {
|
||||
spdlog::error("failed to get default seat");
|
||||
return;
|
||||
}
|
||||
auto* wl_seat = gdk_wayland_seat_get_wl_seat(gdk_seat);
|
||||
|
||||
// Check protocol version to determine which function to use
|
||||
uint32_t version =
|
||||
wl_proxy_get_version(reinterpret_cast<struct wl_proxy*>(client->idle_notifier));
|
||||
|
||||
spdlog::debug("idle_inhibitor: creating notification with timeout {} ms (protocol version {})",
|
||||
idle_timeout_ms_, version);
|
||||
|
||||
if (version >= 2) {
|
||||
// Version 2+: Use get_input_idle_notification which ignores idle inhibitors
|
||||
// This allows us to detect actual user inactivity even while the inhibitor is active
|
||||
spdlog::debug("idle_inhibitor: using get_input_idle_notification (ignores inhibitors)");
|
||||
idle_notification_ = ext_idle_notifier_v1_get_input_idle_notification(
|
||||
client->idle_notifier, idle_timeout_ms_, wl_seat);
|
||||
} else {
|
||||
// Version 1: Fall back to get_idle_notification
|
||||
// WARNING: This respects idle inhibitors, so it won't fire while inhibitor is active
|
||||
spdlog::warn(
|
||||
"idle_inhibitor: ext-idle-notifier-v1 version {} doesn't support "
|
||||
"get_input_idle_notification, "
|
||||
"wait-for-activity may not work correctly",
|
||||
version);
|
||||
idle_notification_ = ext_idle_notifier_v1_get_idle_notification(client->idle_notifier,
|
||||
idle_timeout_ms_, wl_seat);
|
||||
}
|
||||
|
||||
if (idle_notification_ == nullptr) {
|
||||
spdlog::error("idle_inhibitor: failed to create idle notification");
|
||||
return;
|
||||
}
|
||||
|
||||
static const struct ext_idle_notification_v1_listener idle_notification_listener = {
|
||||
.idled = &IdleInhibitor::handleIdled,
|
||||
.resumed = &IdleInhibitor::handleResumed,
|
||||
};
|
||||
|
||||
ext_idle_notification_v1_add_listener(idle_notification_, &idle_notification_listener, this);
|
||||
wl_display_roundtrip(client->wl_display);
|
||||
spdlog::debug("idle_inhibitor: idle notification setup complete");
|
||||
}
|
||||
|
||||
void waybar::modules::IdleInhibitor::teardownIdleNotification() {
|
||||
if (idle_notification_ != nullptr) {
|
||||
spdlog::debug("idle_inhibitor: tearing down idle notification");
|
||||
ext_idle_notification_v1_destroy(idle_notification_);
|
||||
idle_notification_ = nullptr;
|
||||
}
|
||||
}
|
||||
|
||||
+15
-5
@@ -1,5 +1,7 @@
|
||||
#include "modules/image.hpp"
|
||||
|
||||
#include <config.hpp>
|
||||
|
||||
waybar::modules::Image::Image(const std::string& id, const Json::Value& config)
|
||||
: AModule(config, "image", id), box_(Gtk::ORIENTATION_HORIZONTAL, 0) {
|
||||
box_.pack_start(image_);
|
||||
@@ -35,6 +37,13 @@ waybar::modules::Image::Image(const std::string& id, const Json::Value& config)
|
||||
size_ = 16;
|
||||
}
|
||||
|
||||
if (config_["path"].isString()) {
|
||||
auto result = Config::tryExpandPath(config_["path"].asString(), "");
|
||||
path_ = result.empty() ? "" : result.front();
|
||||
} else {
|
||||
path_.clear();
|
||||
}
|
||||
|
||||
delayWorker();
|
||||
}
|
||||
|
||||
@@ -46,19 +55,20 @@ 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 {
|
||||
if (config_["path"].isString()) {
|
||||
path_ = config_["path"].asString();
|
||||
} else if (config_["exec"].isString()) {
|
||||
if (config_["exec"].isString()) {
|
||||
output_ = util::command::exec(config_["exec"].asString(), "");
|
||||
parseOutputRaw();
|
||||
} else {
|
||||
path_ = "";
|
||||
// expand path if "~" or "$HOME" is present in original path
|
||||
auto result = Config::tryExpandPath(path_, "");
|
||||
path_ = result.empty() ? "" : result.front();
|
||||
}
|
||||
|
||||
if (Glib::file_test(path_, Glib::FILE_TEST_EXISTS)) {
|
||||
|
||||
+4
-13
@@ -72,19 +72,10 @@ auto JACK::update() -> void {
|
||||
} else
|
||||
format = "{load}%";
|
||||
|
||||
label_.set_markup(fmt::format(fmt::runtime(format), fmt::arg("load", std::round(load_)),
|
||||
fmt::arg("bufsize", bufsize_), fmt::arg("samplerate", samplerate_),
|
||||
fmt::arg("latency", fmt::format("{:.2f}", latency)),
|
||||
fmt::arg("xruns", xruns_)));
|
||||
|
||||
if (tooltipEnabled()) {
|
||||
std::string tooltip_format = "{bufsize}/{samplerate} {latency}ms";
|
||||
if (config_["tooltip-format"].isString()) tooltip_format = config_["tooltip-format"].asString();
|
||||
label_.set_tooltip_markup(fmt::format(
|
||||
fmt::runtime(tooltip_format), fmt::arg("load", std::round(load_)),
|
||||
fmt::arg("bufsize", bufsize_), fmt::arg("samplerate", samplerate_),
|
||||
fmt::arg("latency", fmt::format("{:.2f}", latency)), fmt::arg("xruns", xruns_)));
|
||||
}
|
||||
updateLabelAndTooltip(
|
||||
format, "{bufsize}/{samplerate} {latency}ms", fmt::arg("load", std::round(load_)),
|
||||
fmt::arg("bufsize", bufsize_), fmt::arg("samplerate", samplerate_),
|
||||
fmt::arg("latency", fmt::format("{:.2f}", latency)), fmt::arg("xruns", xruns_));
|
||||
|
||||
// Call parent update
|
||||
ALabel::update();
|
||||
|
||||
@@ -78,6 +78,53 @@ auto supportsLockStates(const libevdev* dev) -> bool {
|
||||
libevdev_has_event_code(dev, EV_LED, LED_SCROLLL);
|
||||
}
|
||||
|
||||
auto isCommonFormatIcons(const Json::Value& config) -> bool {
|
||||
return config["format-icons"].isObject() && (config["format-icons"]["locked"].isString() ||
|
||||
config["format-icons"]["unlocked"].isString());
|
||||
}
|
||||
|
||||
auto keyStateToIcons(const Json::Value& config)
|
||||
-> std::unordered_map<std::string, std::vector<std::string>> {
|
||||
std::unordered_map<std::string, std::vector<std::string>> key_icon_states;
|
||||
std::vector<std::string> default_icons = {"unlocked", "locked"};
|
||||
|
||||
if (isCommonFormatIcons(config)) {
|
||||
std::vector<std::string> icons = {
|
||||
config["format-icons"]["unlocked"].isString()
|
||||
? config["format-icons"]["unlocked"].asString()
|
||||
: "unlocked",
|
||||
config["format-icons"]["locked"].isString() ? config["format-icons"]["locked"].asString()
|
||||
: "locked",
|
||||
};
|
||||
key_icon_states["Lock"] = icons;
|
||||
return key_icon_states;
|
||||
}
|
||||
|
||||
bool found_any = false;
|
||||
for (const auto& key : std::vector<std::string>{"numlock", "capslock", "scrolllock"}) {
|
||||
std::string map_key = key.substr(0, key.length() - 4);
|
||||
map_key[0] = std::toupper(map_key[0]);
|
||||
if (config["format-icons"].isObject() && config["format-icons"][key].isObject()) {
|
||||
std::string unlocked = config["format-icons"][key]["unlocked"].isString()
|
||||
? config["format-icons"][key]["unlocked"].asString()
|
||||
: "unlocked";
|
||||
std::string locked = config["format-icons"][key]["locked"].isString()
|
||||
? config["format-icons"][key]["locked"].asString()
|
||||
: "locked";
|
||||
key_icon_states[map_key] = {unlocked, locked};
|
||||
found_any = true;
|
||||
}
|
||||
}
|
||||
|
||||
if (!found_any) {
|
||||
key_icon_states["Num"] = default_icons;
|
||||
key_icon_states["Caps"] = default_icons;
|
||||
key_icon_states["Scroll"] = default_icons;
|
||||
}
|
||||
|
||||
return key_icon_states;
|
||||
}
|
||||
|
||||
waybar::modules::KeyboardState::KeyboardState(const std::string& id, const Bar& bar,
|
||||
const Json::Value& config)
|
||||
: AModule(config, "keyboard-state", id, false, !config["disable-scroll"].asBool()),
|
||||
@@ -98,12 +145,7 @@ waybar::modules::KeyboardState::KeyboardState(const std::string& id, const Bar&
|
||||
: "{name} {icon}"),
|
||||
interval_(
|
||||
std::chrono::seconds(config_["interval"].isUInt() ? config_["interval"].asUInt() : 1)),
|
||||
icon_locked_(config_["format-icons"]["locked"].isString()
|
||||
? config_["format-icons"]["locked"].asString()
|
||||
: "locked"),
|
||||
icon_unlocked_(config_["format-icons"]["unlocked"].isString()
|
||||
? config_["format-icons"]["unlocked"].asString()
|
||||
: "unlocked"),
|
||||
key_icon_states_(keyStateToIcons(config_)),
|
||||
devices_path_("/dev/input/"),
|
||||
libinput_(nullptr),
|
||||
libinput_devices_({}) {
|
||||
@@ -290,7 +332,7 @@ auto waybar::modules::KeyboardState::update() -> void {
|
||||
bool state;
|
||||
Gtk::Label& label;
|
||||
const std::string& format;
|
||||
const char* name;
|
||||
const std::string name;
|
||||
} label_states[] = {
|
||||
{(bool)numl, numlock_label_, numlock_format_, "Num"},
|
||||
{(bool)capsl, capslock_label_, capslock_format_, "Caps"},
|
||||
@@ -298,8 +340,21 @@ auto waybar::modules::KeyboardState::update() -> void {
|
||||
};
|
||||
for (auto& label_state : label_states) {
|
||||
std::string text;
|
||||
std::string map_key = isCommonFormatIcons(config_) ? "Lock" : label_state.name;
|
||||
|
||||
if (key_icon_states_.find(map_key) == key_icon_states_.end()) {
|
||||
spdlog::warn("keyboard-state: Missing icon configuration for '{}'", map_key);
|
||||
continue;
|
||||
}
|
||||
|
||||
auto& icons = key_icon_states_[map_key];
|
||||
if (icons.size() < 2) {
|
||||
spdlog::warn("keyboard-state: Invalid icon vector size for '{}'", map_key);
|
||||
continue;
|
||||
}
|
||||
|
||||
text = fmt::format(fmt::runtime(label_state.format),
|
||||
fmt::arg("icon", label_state.state ? icon_locked_ : icon_unlocked_),
|
||||
fmt::arg("icon", label_state.state ? icons[1] : icons[0]),
|
||||
fmt::arg("name", label_state.name));
|
||||
label_state.label.set_markup(text);
|
||||
if (label_state.state) {
|
||||
|
||||
@@ -0,0 +1,317 @@
|
||||
#include "modules/mango/backend.hpp"
|
||||
|
||||
#include <fcntl.h>
|
||||
#include <poll.h>
|
||||
#include <spdlog/spdlog.h>
|
||||
#include <sys/poll.h>
|
||||
#include <sys/socket.h>
|
||||
#include <sys/un.h>
|
||||
#include <unistd.h>
|
||||
|
||||
#include <sstream>
|
||||
#include <thread>
|
||||
#include <vector>
|
||||
|
||||
#include "util/scoped_fd.hpp"
|
||||
|
||||
namespace waybar::modules::mango {
|
||||
|
||||
int IPC::connectToSocket() {
|
||||
const char* socket_path = getenv("MANGO_INSTANCE_SIGNATURE");
|
||||
if (!socket_path) {
|
||||
throw std::runtime_error("Mango IPC: MANGO_INSTANCE_SIGNATURE not set");
|
||||
}
|
||||
|
||||
struct sockaddr_un addr;
|
||||
util::ScopedFd fd(socket(AF_UNIX, SOCK_STREAM, 0));
|
||||
if (fd == -1) throw std::runtime_error("socket() failed");
|
||||
|
||||
addr.sun_family = AF_UNIX;
|
||||
strncpy(addr.sun_path, socket_path, sizeof(addr.sun_path) - 1);
|
||||
addr.sun_path[sizeof(addr.sun_path) - 1] = 0;
|
||||
|
||||
if (connect(fd, (struct sockaddr*)&addr, sizeof(addr)) == -1) {
|
||||
throw std::runtime_error("connect() failed");
|
||||
}
|
||||
return fd.release();
|
||||
}
|
||||
|
||||
Json::Value IPC::sendCommand(const std::string& cmd) {
|
||||
util::ScopedFd fd(IPC::connectToSocket());
|
||||
std::string full_cmd = cmd + "\n";
|
||||
|
||||
ssize_t total_written = 0;
|
||||
while (total_written < (ssize_t)full_cmd.size()) {
|
||||
ssize_t res = write(fd, full_cmd.c_str() + total_written, full_cmd.size() - total_written);
|
||||
if (res < 0) {
|
||||
if (errno == EINTR) continue;
|
||||
throw std::runtime_error("Failed to write command");
|
||||
}
|
||||
total_written += res;
|
||||
}
|
||||
|
||||
char buf[4096];
|
||||
std::string response;
|
||||
while (true) {
|
||||
ssize_t n = read(fd, buf, sizeof(buf) - 1);
|
||||
if (n <= 0) {
|
||||
if (n == 0) break;
|
||||
if (errno == EINTR || errno == EAGAIN || errno == EWOULDBLOCK) continue;
|
||||
throw std::runtime_error("Read error");
|
||||
}
|
||||
buf[n] = '\0';
|
||||
response += buf;
|
||||
if (response.find('\n') != std::string::npos) break;
|
||||
}
|
||||
|
||||
Json::Value root;
|
||||
std::istringstream iss(response);
|
||||
Json::CharReaderBuilder builder;
|
||||
std::string errors;
|
||||
if (!Json::parseFromStream(builder, iss, &root, &errors)) {
|
||||
throw std::runtime_error("JSON parse error: " + errors);
|
||||
}
|
||||
return root;
|
||||
}
|
||||
|
||||
Json::Value IPC::send(const Json::Value& request) {
|
||||
if (!request.isMember("command")) {
|
||||
throw std::runtime_error("Mango IPC: request must have 'command' field");
|
||||
}
|
||||
return sendCommand(request["command"].asString());
|
||||
}
|
||||
|
||||
void IPC::sendAsync(const Json::Value& request) {
|
||||
if (!request.isMember("command")) {
|
||||
spdlog::error("Mango IPC: request must have 'command' field");
|
||||
return;
|
||||
}
|
||||
std::string cmd = request["command"].asString();
|
||||
|
||||
std::thread([cmd]() {
|
||||
try {
|
||||
IPC::sendCommand(cmd);
|
||||
} catch (const std::exception& e) {
|
||||
spdlog::error("IPC async send failed: {}", e.what());
|
||||
}
|
||||
}).detach();
|
||||
}
|
||||
|
||||
IPC::IPC() : sockfd_(-1), active_client_(Json::nullValue) { startIPC(); }
|
||||
|
||||
IPC::~IPC() {
|
||||
if (sockfd_ != -1) close(sockfd_);
|
||||
if (ipc_thread_.joinable()) ipc_thread_.join();
|
||||
}
|
||||
|
||||
void IPC::startIPC() {
|
||||
sockfd_ = IPC::connectToSocket();
|
||||
|
||||
ipc_thread_ = std::thread([this]() {
|
||||
spdlog::info("Mango IPC thread started");
|
||||
|
||||
struct pollfd pfd;
|
||||
pfd.fd = sockfd_;
|
||||
pfd.events = POLLIN;
|
||||
|
||||
const std::vector<std::string> subs = {"watch all-monitors"};
|
||||
for (const auto& cmd : subs) {
|
||||
if (write(sockfd_, cmd.c_str(), cmd.size()) != (ssize_t)cmd.size() ||
|
||||
write(sockfd_, "\n", 1) != 1) {
|
||||
spdlog::error("Failed to subscribe to {}", cmd);
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
char buf[4096];
|
||||
std::string buffer;
|
||||
while (true) {
|
||||
int ret = poll(&pfd, 1, 1000);
|
||||
if (ret == 0) continue;
|
||||
if (ret < 0) {
|
||||
if (errno == EINTR) continue;
|
||||
spdlog::error("IPC poll error: {}", strerror(errno));
|
||||
break;
|
||||
}
|
||||
|
||||
if (pfd.revents & (POLLERR | POLLHUP | POLLNVAL)) {
|
||||
spdlog::info("Mango IPC socket closed or invalid");
|
||||
break;
|
||||
}
|
||||
|
||||
if (pfd.revents & POLLIN) {
|
||||
ssize_t n = read(sockfd_, buf, sizeof(buf));
|
||||
if (n == 0) {
|
||||
spdlog::info("Mango IPC connection closed");
|
||||
break;
|
||||
}
|
||||
if (n < 0) {
|
||||
if (errno == EINTR) continue;
|
||||
spdlog::error("IPC read error: {}", strerror(errno));
|
||||
break;
|
||||
}
|
||||
buffer.append(buf, n);
|
||||
|
||||
size_t pos;
|
||||
while ((pos = buffer.find('\n')) != std::string::npos) {
|
||||
std::string line = buffer.substr(0, pos);
|
||||
buffer.erase(0, pos + 1);
|
||||
if (line.empty()) continue;
|
||||
try {
|
||||
parseIPC(line);
|
||||
} catch (const std::exception& e) {
|
||||
spdlog::warn("Failed to parse IPC line: {} - {}", line, e.what());
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
void IPC::parseIPC(const std::string& line) {
|
||||
Json::Value root;
|
||||
Json::CharReaderBuilder builder;
|
||||
std::string errors;
|
||||
std::istringstream iss(line);
|
||||
if (!Json::parseFromStream(builder, iss, &root, &errors)) {
|
||||
throw std::runtime_error("JSON parse error: " + errors);
|
||||
}
|
||||
|
||||
if (root.isMember("monitors") && root["monitors"].isArray()) {
|
||||
for (const auto& mon : root["monitors"]) {
|
||||
handleMonitorUpdate(mon);
|
||||
}
|
||||
|
||||
Json::Value active_monitor;
|
||||
for (const auto& mon : root["monitors"]) {
|
||||
if (mon["active"].asBool()) {
|
||||
active_monitor = mon;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (!active_monitor.isNull()) {
|
||||
const auto& active_client = active_monitor["active_client"];
|
||||
updateFocusingClient(active_client);
|
||||
|
||||
if (active_monitor.isMember("keyboardlayout")) {
|
||||
updateKeyboardLayout(active_monitor["keyboardlayout"].asString());
|
||||
}
|
||||
|
||||
if (active_monitor.isMember("keymode")) {
|
||||
std::lock_guard<std::mutex> lock(data_mutex_);
|
||||
keymode_ = active_monitor["keymode"].asString();
|
||||
}
|
||||
}
|
||||
|
||||
std::vector<EventHandler*> handlers_to_notify;
|
||||
{
|
||||
std::lock_guard<std::mutex> lock(callback_mutex_);
|
||||
for (auto& [ev, handler] : callbacks_) {
|
||||
if (ev == "monitor") {
|
||||
handlers_to_notify.push_back(handler);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
for (auto* handler : handlers_to_notify) {
|
||||
handler->onEvent(root);
|
||||
}
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
spdlog::debug("Unhandled IPC message: {}", line);
|
||||
}
|
||||
|
||||
std::unordered_map<std::string, Json::Value> IPC::getMonitors() const {
|
||||
std::lock_guard<std::mutex> lock(data_mutex_);
|
||||
return monitors_;
|
||||
}
|
||||
|
||||
IPC& IPC::getInstance() {
|
||||
static IPC instance;
|
||||
return instance;
|
||||
}
|
||||
|
||||
Json::Value IPC::getMonitor(const std::string& name) {
|
||||
std::lock_guard<std::mutex> lock(data_mutex_);
|
||||
auto it = monitors_.find(name);
|
||||
if (it != monitors_.end()) {
|
||||
return it->second;
|
||||
}
|
||||
return Json::nullValue;
|
||||
}
|
||||
|
||||
std::string IPC::getKeyboardLayout() const {
|
||||
std::lock_guard<std::mutex> lock(data_mutex_);
|
||||
return keyboard_layout_;
|
||||
}
|
||||
|
||||
std::string IPC::getKeymode() const {
|
||||
std::lock_guard<std::mutex> lock(data_mutex_);
|
||||
return keymode_;
|
||||
}
|
||||
|
||||
Json::Value IPC::getActiveClientForMonitor(const std::string& name) const {
|
||||
std::lock_guard<std::mutex> lock(data_mutex_);
|
||||
auto it = monitors_.find(name);
|
||||
if (it != monitors_.end() && it->second.isMember("active_client")) {
|
||||
return it->second["active_client"];
|
||||
}
|
||||
return Json::nullValue;
|
||||
}
|
||||
|
||||
std::string IPC::getLayoutSymbolForMonitor(const std::string& name) const {
|
||||
std::lock_guard<std::mutex> lock(data_mutex_);
|
||||
auto it = monitors_.find(name);
|
||||
if (it != monitors_.end() && it->second.isMember("layout_symbol")) {
|
||||
return it->second["layout_symbol"].asString();
|
||||
}
|
||||
return {};
|
||||
}
|
||||
|
||||
void IPC::handleMonitorUpdate(const Json::Value& mon) {
|
||||
std::lock_guard<std::mutex> lock(data_mutex_);
|
||||
monitors_[mon["name"].asString()] = mon;
|
||||
}
|
||||
|
||||
void IPC::updateFocusingClient(const Json::Value& client) {
|
||||
{
|
||||
std::lock_guard<std::mutex> lock(data_mutex_);
|
||||
active_client_ = client;
|
||||
|
||||
if (client.isNull() || !client.isObject() || client["id"].isNull()) {
|
||||
focusing_client_id_ = 0;
|
||||
} else {
|
||||
focusing_client_id_ = client["id"].asUInt64();
|
||||
clients_[focusing_client_id_] = client;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void IPC::updateKeyboardLayout(const std::string& layout) {
|
||||
{
|
||||
std::lock_guard<std::mutex> lock(data_mutex_);
|
||||
keyboard_layout_ = layout;
|
||||
}
|
||||
}
|
||||
|
||||
void IPC::registerForIPC(const std::string& ev, EventHandler* handler) {
|
||||
if (!handler) return;
|
||||
std::lock_guard<std::mutex> lock(callback_mutex_);
|
||||
callbacks_.emplace_back(ev, handler);
|
||||
}
|
||||
|
||||
void IPC::unregisterForIPC(EventHandler* handler) {
|
||||
if (!handler) return;
|
||||
std::lock_guard<std::mutex> lock(callback_mutex_);
|
||||
for (auto it = callbacks_.begin(); it != callbacks_.end();) {
|
||||
if (it->second == handler)
|
||||
it = callbacks_.erase(it);
|
||||
else
|
||||
++it;
|
||||
}
|
||||
}
|
||||
|
||||
} // namespace waybar::modules::mango
|
||||
@@ -0,0 +1,58 @@
|
||||
#include "modules/mango/keymode.hpp"
|
||||
|
||||
#include <spdlog/spdlog.h>
|
||||
|
||||
namespace waybar::modules::mango {
|
||||
|
||||
Keymode::Keymode(const std::string& id, const Bar& bar, const Json::Value& config)
|
||||
: ALabel(config, "keymode", id, "{}", 0, false), bar_(bar) {
|
||||
IPC::getInstance().registerForIPC("monitor", this);
|
||||
dp.emit();
|
||||
}
|
||||
|
||||
Keymode::~Keymode() { IPC::getInstance().unregisterForIPC(this); }
|
||||
|
||||
void Keymode::onEvent(const Json::Value& ev) { dp.emit(); }
|
||||
|
||||
void Keymode::doUpdate() {
|
||||
std::lock_guard<std::mutex> lock(mutex_);
|
||||
|
||||
std::string current = IPC::getInstance().getKeymode();
|
||||
|
||||
// if keymode is empty, hide the label
|
||||
if (current.empty()) {
|
||||
label_.hide();
|
||||
last_keymode_.clear();
|
||||
return;
|
||||
}
|
||||
|
||||
// if keymode is the same as last time, skip style changes
|
||||
if (current != last_keymode_) {
|
||||
if (!last_keymode_.empty()) label_.get_style_context()->remove_class(last_keymode_);
|
||||
label_.get_style_context()->add_class(current);
|
||||
last_keymode_ = current;
|
||||
}
|
||||
|
||||
// support config's format-keymode custom format (such as format-default, format-resize, etc.)
|
||||
std::string text;
|
||||
std::string format_key = "format-" + current;
|
||||
if (config_.isMember(format_key)) {
|
||||
text = fmt::format(fmt::runtime(config_[format_key].asString()), fmt::arg("mode", current));
|
||||
} else {
|
||||
text = fmt::format(fmt::runtime(format_), fmt::arg("mode", current));
|
||||
}
|
||||
|
||||
if (!text.empty()) {
|
||||
label_.show();
|
||||
label_.set_markup(text);
|
||||
} else {
|
||||
label_.hide();
|
||||
}
|
||||
}
|
||||
|
||||
void Keymode::update() {
|
||||
doUpdate();
|
||||
ALabel::update();
|
||||
}
|
||||
|
||||
} // namespace waybar::modules::mango
|
||||
@@ -0,0 +1,148 @@
|
||||
#include "modules/mango/language.hpp"
|
||||
|
||||
#include <spdlog/spdlog.h>
|
||||
#include <xkbcommon/xkbcommon.h>
|
||||
#include <xkbcommon/xkbregistry.h>
|
||||
|
||||
#include "util/string.hpp"
|
||||
|
||||
namespace waybar::modules::mango {
|
||||
|
||||
Language::Language(const std::string& id, const Bar& bar, const Json::Value& config)
|
||||
: ALabel(config, "language", id, "{}", 0, false), bar_(bar), rxkb_ctx_(nullptr) {
|
||||
rxkb_ctx_ = rxkb_context_new(RXKB_CONTEXT_LOAD_EXOTIC_RULES);
|
||||
if (rxkb_ctx_) {
|
||||
rxkb_context_parse_default_ruleset(rxkb_ctx_);
|
||||
}
|
||||
|
||||
IPC::getInstance().registerForIPC("monitor", this);
|
||||
updateFromIPC();
|
||||
dp.emit();
|
||||
}
|
||||
|
||||
Language::~Language() {
|
||||
IPC::getInstance().unregisterForIPC(this);
|
||||
if (rxkb_ctx_) rxkb_context_unref(rxkb_ctx_);
|
||||
}
|
||||
|
||||
void Language::updateFromIPC() {
|
||||
std::lock_guard<std::mutex> lock(mutex_);
|
||||
std::string layout = IPC::getInstance().getKeyboardLayout();
|
||||
|
||||
layouts_.clear();
|
||||
if (!layout.empty()) {
|
||||
Layout l = getLayout(layout);
|
||||
layouts_.push_back(l);
|
||||
current_idx_ = 0;
|
||||
} else {
|
||||
current_idx_ = 0;
|
||||
}
|
||||
}
|
||||
|
||||
void Language::doUpdate() {
|
||||
std::lock_guard<std::mutex> lock(mutex_);
|
||||
if (layouts_.empty() || current_idx_ >= layouts_.size()) {
|
||||
label_.hide();
|
||||
return;
|
||||
}
|
||||
const auto& layout = layouts_[current_idx_];
|
||||
|
||||
if (!last_short_name_.empty()) label_.get_style_context()->remove_class(last_short_name_);
|
||||
if (!layout.short_name.empty()) {
|
||||
label_.get_style_context()->add_class(layout.short_name);
|
||||
last_short_name_ = layout.short_name;
|
||||
}
|
||||
|
||||
std::string layoutName;
|
||||
std::string variant_key = "format-" + layout.short_description + "-" + layout.variant;
|
||||
if (!layout.variant.empty() && config_.isMember(variant_key)) {
|
||||
layoutName =
|
||||
fmt::format(fmt::runtime(config_[variant_key].asString()),
|
||||
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 (config_.isMember("format-" + layout.short_description)) {
|
||||
std::string key = "format-" + layout.short_description;
|
||||
layoutName =
|
||||
fmt::format(fmt::runtime(config_[key].asString()), fmt::arg("long", layout.full_name),
|
||||
fmt::arg("short", layout.short_name),
|
||||
fmt::arg("shortDescription", layout.short_description),
|
||||
fmt::arg("variant", layout.variant));
|
||||
} else {
|
||||
layoutName = fmt::format(fmt::runtime(format_), fmt::arg("long", layout.full_name),
|
||||
fmt::arg("short", layout.short_name),
|
||||
fmt::arg("shortDescription", layout.short_description),
|
||||
fmt::arg("variant", layout.variant));
|
||||
}
|
||||
|
||||
if (!layoutName.empty()) {
|
||||
label_.show();
|
||||
label_.set_markup(layoutName);
|
||||
} else {
|
||||
label_.hide();
|
||||
}
|
||||
}
|
||||
|
||||
void Language::update() {
|
||||
updateFromIPC();
|
||||
doUpdate();
|
||||
ALabel::update();
|
||||
}
|
||||
|
||||
void Language::onEvent(const Json::Value& ev) {
|
||||
updateFromIPC();
|
||||
dp.emit();
|
||||
}
|
||||
|
||||
Language::Layout Language::getLayout(const std::string& fullName) {
|
||||
if (rxkb_ctx_) {
|
||||
rxkb_layout* layout = rxkb_layout_first(rxkb_ctx_);
|
||||
while (layout != nullptr) {
|
||||
std::string desc = rxkb_layout_get_description(layout);
|
||||
if (desc == fullName) {
|
||||
std::string short_name = rxkb_layout_get_name(layout);
|
||||
const char* variant_ptr = rxkb_layout_get_variant(layout);
|
||||
std::string variant = variant_ptr ? variant_ptr : "";
|
||||
const char* brief_ptr = rxkb_layout_get_brief(layout);
|
||||
std::string short_description = brief_ptr ? brief_ptr : "";
|
||||
|
||||
if (short_description.empty()) {
|
||||
short_description = short_name;
|
||||
}
|
||||
|
||||
short_description = short_name;
|
||||
|
||||
Layout info{desc, short_name, variant, short_description};
|
||||
return info;
|
||||
}
|
||||
layout = rxkb_layout_next(layout);
|
||||
}
|
||||
}
|
||||
|
||||
spdlog::warn("mango language: rxkb failed to find layout '{}', using string parsing fallback",
|
||||
fullName);
|
||||
|
||||
Layout l;
|
||||
l.full_name = fullName;
|
||||
l.variant = "";
|
||||
|
||||
size_t paren_start = fullName.find('(');
|
||||
size_t paren_end = fullName.find(')');
|
||||
if (paren_start != std::string::npos && paren_end != std::string::npos &&
|
||||
paren_end > paren_start) {
|
||||
l.short_name = fullName.substr(paren_start + 1, paren_end - paren_start - 1);
|
||||
} else if (fullName.length() >= 2) {
|
||||
l.short_name = fullName.substr(0, 2);
|
||||
} else {
|
||||
l.short_name = fullName;
|
||||
}
|
||||
|
||||
std::transform(l.short_name.begin(), l.short_name.end(), l.short_name.begin(),
|
||||
[](unsigned char c) { return std::tolower(c); });
|
||||
|
||||
l.short_description = l.short_name;
|
||||
|
||||
return l;
|
||||
}
|
||||
|
||||
} // namespace waybar::modules::mango
|
||||
@@ -0,0 +1,56 @@
|
||||
#include "modules/mango/layout.hpp"
|
||||
|
||||
#include <spdlog/spdlog.h>
|
||||
|
||||
namespace waybar::modules::mango {
|
||||
|
||||
Layout::Layout(const std::string& id, const Bar& bar, const Json::Value& config)
|
||||
: ALabel(config, "layout", id, "{}", 0, false), bar_(bar) {
|
||||
IPC::getInstance().registerForIPC("monitor", this);
|
||||
dp.emit();
|
||||
}
|
||||
|
||||
Layout::~Layout() { IPC::getInstance().unregisterForIPC(this); }
|
||||
|
||||
void Layout::onEvent(const Json::Value& ev) { dp.emit(); }
|
||||
|
||||
void Layout::doUpdate() {
|
||||
std::lock_guard<std::mutex> lock(mutex_);
|
||||
|
||||
std::string symbol = IPC::getInstance().getLayoutSymbolForMonitor(bar_.output->name);
|
||||
|
||||
if (symbol.empty()) {
|
||||
label_.hide();
|
||||
last_symbol_.clear();
|
||||
return;
|
||||
}
|
||||
|
||||
if (symbol != last_symbol_) {
|
||||
if (!last_symbol_.empty()) label_.get_style_context()->remove_class(last_symbol_);
|
||||
label_.get_style_context()->add_class(symbol);
|
||||
last_symbol_ = symbol;
|
||||
}
|
||||
|
||||
std::string text;
|
||||
std::string format_key = "format-" + symbol;
|
||||
|
||||
if (config_.isMember(format_key)) {
|
||||
text = fmt::format(fmt::runtime(config_[format_key].asString()), fmt::arg("symbol", symbol));
|
||||
} else {
|
||||
text = fmt::format(fmt::runtime(format_), fmt::arg("symbol", symbol));
|
||||
}
|
||||
|
||||
if (!text.empty()) {
|
||||
label_.show();
|
||||
label_.set_markup(text);
|
||||
} else {
|
||||
label_.hide();
|
||||
}
|
||||
}
|
||||
|
||||
void Layout::update() {
|
||||
doUpdate();
|
||||
ALabel::update();
|
||||
}
|
||||
|
||||
} // namespace waybar::modules::mango
|
||||
@@ -0,0 +1,93 @@
|
||||
#include "modules/mango/window.hpp"
|
||||
|
||||
#include <spdlog/spdlog.h>
|
||||
|
||||
#include "util/rewrite_string.hpp"
|
||||
#include "util/sanitize_str.hpp"
|
||||
|
||||
namespace waybar::modules::mango {
|
||||
|
||||
Window::Window(const std::string& id, const Bar& bar, const Json::Value& config)
|
||||
: AAppIconLabel(config, "window", id, "{title}", 0, true), bar_(bar) {
|
||||
IPC::getInstance().registerForIPC("monitor", this);
|
||||
dp.emit();
|
||||
}
|
||||
|
||||
Window::~Window() { IPC::getInstance().unregisterForIPC(this); }
|
||||
|
||||
void Window::onEvent(const Json::Value& ev) { dp.emit(); }
|
||||
|
||||
void Window::doUpdate() {
|
||||
std::lock_guard<std::mutex> lock(mutex_);
|
||||
|
||||
const Json::Value& client = IPC::getInstance().getActiveClientForMonitor(bar_.output->name);
|
||||
|
||||
// judge whether to hide: active_client is null or title field is null
|
||||
if (client.isNull() || !client.isObject() || client["title"].isNull()) {
|
||||
event_box_.hide();
|
||||
label_.hide();
|
||||
updateAppIconName("", "");
|
||||
setClass("empty", true);
|
||||
if (!oldAppId_.empty()) setClass(oldAppId_, false);
|
||||
oldAppId_.clear();
|
||||
return;
|
||||
}
|
||||
|
||||
// if we have a valid client, show the label and update content
|
||||
event_box_.show();
|
||||
label_.show();
|
||||
setClass("empty", false);
|
||||
|
||||
std::string title = client["title"].asString();
|
||||
std::string appid = client["appid"].asString();
|
||||
std::string sanitized_title = waybar::util::sanitize_string(title);
|
||||
std::string sanitized_appid = waybar::util::sanitize_string(appid);
|
||||
|
||||
label_.set_markup(waybar::util::rewriteString(
|
||||
fmt::format(fmt::runtime(format_), fmt::arg("title", sanitized_title),
|
||||
fmt::arg("app_id", sanitized_appid)),
|
||||
config_["rewrite"]));
|
||||
|
||||
updateAppIconName(appid, "");
|
||||
if (tooltipEnabled()) label_.set_tooltip_markup(title);
|
||||
|
||||
// Solo judgment
|
||||
bool solo = false;
|
||||
if (client.isMember("tags") && client["tags"].isArray() && client["tags"].size() == 1) {
|
||||
int tag_idx = client["tags"][0].asInt();
|
||||
const auto& monitors = IPC::getInstance().getMonitors();
|
||||
auto mon_it = monitors.find(client["monitor"].asString());
|
||||
if (mon_it != monitors.end()) {
|
||||
const auto& tags = mon_it->second["tags"];
|
||||
for (const auto& tag : tags) {
|
||||
if (tag["index"].asInt() == tag_idx) {
|
||||
solo = (tag["client_count"].asInt() == 1);
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
setClass("solo", solo);
|
||||
if (!appid.empty()) setClass(appid, solo);
|
||||
|
||||
if (oldAppId_ != appid) {
|
||||
if (!oldAppId_.empty()) setClass(oldAppId_, false);
|
||||
oldAppId_ = appid;
|
||||
}
|
||||
}
|
||||
|
||||
void Window::update() {
|
||||
doUpdate();
|
||||
AAppIconLabel::update();
|
||||
}
|
||||
|
||||
void Window::setClass(const std::string& className, bool enable) {
|
||||
auto style_context = event_box_.get_style_context();
|
||||
if (enable) {
|
||||
if (!style_context->has_class(className)) style_context->add_class(className);
|
||||
} else {
|
||||
style_context->remove_class(className);
|
||||
}
|
||||
}
|
||||
|
||||
} // namespace waybar::modules::mango
|
||||
@@ -0,0 +1,252 @@
|
||||
#include "modules/mango/workspaces.hpp"
|
||||
|
||||
#include <spdlog/spdlog.h>
|
||||
|
||||
#include <algorithm>
|
||||
|
||||
namespace waybar::modules::mango {
|
||||
|
||||
Workspaces::Workspaces(const std::string& id, const Bar& bar, const Json::Value& config)
|
||||
: AModule(config, "workspaces", id, false, false), bar_(bar), box_(bar.orientation, 0) {
|
||||
box_.set_name("workspaces");
|
||||
if (!id.empty()) box_.get_style_context()->add_class(id);
|
||||
box_.get_style_context()->add_class(MODULE_CLASS);
|
||||
event_box_.add(box_);
|
||||
|
||||
if (config_["on-click"].isString()) on_click_left_ = config_["on-click"].asString();
|
||||
if (config_["on-click-middle"].isString())
|
||||
on_click_middle_ = config_["on-click-middle"].asString();
|
||||
if (config_["on-click-right"].isString()) on_click_right_ = config_["on-click-right"].asString();
|
||||
|
||||
overview_button_ = new Gtk::Button("OVERVIEW");
|
||||
overview_button_->set_relief(Gtk::RELIEF_NONE);
|
||||
box_.pack_start(*overview_button_, false, false, 0);
|
||||
|
||||
if (!on_click_left_.empty() || !on_click_middle_.empty() || !on_click_right_.empty()) {
|
||||
overview_button_->add_events(Gdk::BUTTON_PRESS_MASK);
|
||||
overview_button_->signal_button_press_event().connect(
|
||||
[this](GdkEventButton* event) -> bool { return handleButtonClick(event, 0, true); }, false);
|
||||
}
|
||||
|
||||
IPC::getInstance().registerForIPC("monitor", this);
|
||||
dp.emit();
|
||||
}
|
||||
|
||||
Workspaces::~Workspaces() {
|
||||
IPC::getInstance().unregisterForIPC(this);
|
||||
|
||||
if (overview_button_) {
|
||||
box_.remove(*overview_button_);
|
||||
delete overview_button_;
|
||||
overview_button_ = nullptr;
|
||||
}
|
||||
|
||||
for (auto& [idx, btn] : buttons_) {
|
||||
box_.remove(btn);
|
||||
}
|
||||
buttons_.clear();
|
||||
}
|
||||
|
||||
void Workspaces::onEvent(const Json::Value& ev) { dp.emit(); }
|
||||
|
||||
void Workspaces::doUpdate() {
|
||||
Json::Value monitor = IPC::getInstance().getMonitor(bar_.output->name);
|
||||
if (monitor.isNull()) return;
|
||||
|
||||
const auto& tags = monitor["tags"];
|
||||
|
||||
bool overview_mode = false;
|
||||
if (monitor.isMember("active_tags") && monitor["active_tags"].isArray()) {
|
||||
const auto& active_tags = monitor["active_tags"];
|
||||
if (active_tags.size() == 1 && active_tags[0].asInt() == 0) {
|
||||
overview_mode = true;
|
||||
}
|
||||
}
|
||||
|
||||
for (auto& [idx, btn] : buttons_) {
|
||||
btn.hide();
|
||||
}
|
||||
|
||||
if (overview_mode) {
|
||||
overview_button_->show();
|
||||
auto style = overview_button_->get_style_context();
|
||||
style->add_class("overview");
|
||||
if (monitor["active"].asBool())
|
||||
style->add_class("current_output");
|
||||
else
|
||||
style->remove_class("current_output");
|
||||
|
||||
std::string label =
|
||||
config_["overview-label"].isString() ? config_["overview-label"].asString() : "OVERVIEW";
|
||||
|
||||
if (!config_["disable-markup"].asBool()) {
|
||||
if (auto gtk_label = dynamic_cast<Gtk::Label*>(overview_button_->get_child())) {
|
||||
gtk_label->set_markup(label);
|
||||
}
|
||||
} else {
|
||||
overview_button_->set_label(label);
|
||||
}
|
||||
} else {
|
||||
overview_button_->hide();
|
||||
|
||||
for (auto btn_it = buttons_.begin(); btn_it != buttons_.end();) {
|
||||
uint64_t id = btn_it->first;
|
||||
bool found = std::any_of(tags.begin(), tags.end(), [id](const Json::Value& tag) {
|
||||
return tag["index"].asUInt64() == id;
|
||||
});
|
||||
|
||||
if (!found) {
|
||||
box_.remove(btn_it->second);
|
||||
btn_it = buttons_.erase(btn_it);
|
||||
} else {
|
||||
++btn_it;
|
||||
}
|
||||
}
|
||||
|
||||
for (const auto& tag : tags) {
|
||||
uint64_t idx = tag["index"].asUInt64();
|
||||
auto btn_it = buttons_.find(idx);
|
||||
Gtk::Button& button = (btn_it == buttons_.end()) ? addButton(idx) : btn_it->second;
|
||||
updateButtonState(button, tag, monitor);
|
||||
}
|
||||
|
||||
std::vector<uint64_t> indices;
|
||||
for (const auto& tag : tags) indices.push_back(tag["index"].asUInt64());
|
||||
std::sort(indices.begin(), indices.end());
|
||||
int pos = 0;
|
||||
for (uint64_t idx : indices) {
|
||||
box_.reorder_child(buttons_[idx], pos + 1);
|
||||
pos++;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void Workspaces::update() {
|
||||
doUpdate();
|
||||
AModule::update();
|
||||
}
|
||||
|
||||
Gtk::Button& Workspaces::addButton(uint64_t idx) {
|
||||
auto [it, _] = buttons_.emplace(idx, std::to_string(idx));
|
||||
auto& button = it->second;
|
||||
box_.pack_start(button, false, false, 0);
|
||||
button.set_relief(Gtk::RELIEF_NONE);
|
||||
|
||||
if (!on_click_left_.empty() || !on_click_middle_.empty() || !on_click_right_.empty()) {
|
||||
button.add_events(Gdk::BUTTON_PRESS_MASK);
|
||||
button.signal_button_press_event().connect(
|
||||
[this, idx](GdkEventButton* event) -> bool { return handleButtonClick(event, idx, false); },
|
||||
false);
|
||||
}
|
||||
|
||||
button.show_all();
|
||||
return button;
|
||||
}
|
||||
|
||||
void Workspaces::updateButtonState(Gtk::Button& button, const Json::Value& tag,
|
||||
const Json::Value& monitor) {
|
||||
auto style = button.get_style_context();
|
||||
bool active = tag["is_active"].asBool();
|
||||
bool urgent = tag["is_urgent"].asBool();
|
||||
bool empty = (tag["client_count"].asInt() == 0);
|
||||
|
||||
if (active)
|
||||
style->add_class("active");
|
||||
else
|
||||
style->remove_class("active");
|
||||
|
||||
if (urgent)
|
||||
style->add_class("urgent");
|
||||
else
|
||||
style->remove_class("urgent");
|
||||
|
||||
if (empty)
|
||||
style->add_class("empty");
|
||||
else
|
||||
style->remove_class("empty");
|
||||
|
||||
if (monitor["active"].asBool())
|
||||
style->add_class("current_output");
|
||||
else
|
||||
style->remove_class("current_output");
|
||||
|
||||
uint64_t idx = tag["index"].asUInt64();
|
||||
std::string name = std::to_string(idx);
|
||||
if (config_["format"].isString()) {
|
||||
name = fmt::format(fmt::runtime(config_["format"].asString()),
|
||||
fmt::arg("icon", getIcon(name, tag)), fmt::arg("value", name),
|
||||
fmt::arg("index", idx), fmt::arg("output", monitor["name"].asString()));
|
||||
}
|
||||
|
||||
if (!config_["disable-markup"].asBool()) {
|
||||
if (auto gtk_label = dynamic_cast<Gtk::Label*>(button.get_child())) {
|
||||
gtk_label->set_markup(name);
|
||||
}
|
||||
} else {
|
||||
button.set_label(name);
|
||||
}
|
||||
|
||||
if (config_["current-only"].asBool()) {
|
||||
if (active)
|
||||
button.show();
|
||||
else
|
||||
button.hide();
|
||||
} else if (config_["hide-empty"].asBool() && empty && !active) {
|
||||
button.hide();
|
||||
} else {
|
||||
button.show();
|
||||
}
|
||||
}
|
||||
|
||||
std::string Workspaces::getIcon(const std::string& value, const Json::Value& tag) {
|
||||
const auto& icons = config_["format-icons"];
|
||||
if (!icons) return value;
|
||||
|
||||
if (tag["is_urgent"].asBool() && icons["urgent"]) return icons["urgent"].asString();
|
||||
if (tag["is_active"].asBool() && icons["active"]) return icons["active"].asString();
|
||||
if (tag["client_count"].asInt() == 0 && icons["empty"]) return icons["empty"].asString();
|
||||
|
||||
std::string idx = std::to_string(tag["index"].asUInt());
|
||||
if (icons[idx]) return icons[idx].asString();
|
||||
if (icons["default"]) return icons["default"].asString();
|
||||
return value;
|
||||
}
|
||||
|
||||
bool Workspaces::handleButtonClick(GdkEventButton* event, uint64_t idx, bool isOverview) {
|
||||
std::string action;
|
||||
if (event->button == 1)
|
||||
action = on_click_left_;
|
||||
else if (event->button == 2)
|
||||
action = on_click_middle_;
|
||||
else if (event->button == 3)
|
||||
action = on_click_right_;
|
||||
|
||||
if (action.empty()) return true;
|
||||
|
||||
try {
|
||||
std::string cmd;
|
||||
if (isOverview) {
|
||||
if (action == "activate")
|
||||
cmd = "dispatch overview";
|
||||
else if (action == "toggle")
|
||||
cmd = "dispatch toggleoverview";
|
||||
} else {
|
||||
if (action == "activate")
|
||||
cmd = "dispatch view," + std::to_string(idx);
|
||||
else if (action == "toggle")
|
||||
cmd = "dispatch toggleview," + std::to_string(idx);
|
||||
}
|
||||
|
||||
if (!cmd.empty()) {
|
||||
Json::Value req;
|
||||
req["command"] = cmd;
|
||||
IPC::sendAsync(req);
|
||||
}
|
||||
} catch (const std::exception& e) {
|
||||
spdlog::error("Error sending IPC command: {}", e.what());
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
} // namespace waybar::modules::mango
|
||||
@@ -1,11 +1,30 @@
|
||||
#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] {
|
||||
dp.emit();
|
||||
thread_.sleep_for(interval_);
|
||||
};
|
||||
if (config["unit"].isString()) {
|
||||
unit_ = config["unit"].asString();
|
||||
if (!kUnits.contains(unit_)) {
|
||||
unit_ = "GiB";
|
||||
}
|
||||
} else {
|
||||
unit_ = "GiB";
|
||||
}
|
||||
}
|
||||
|
||||
auto waybar::modules::Memory::update() -> void {
|
||||
@@ -13,15 +32,15 @@ auto waybar::modules::Memory::update() -> void {
|
||||
|
||||
unsigned long memtotal = meminfo_["MemTotal"];
|
||||
unsigned long swaptotal = 0;
|
||||
if (meminfo_.count("SwapTotal")) {
|
||||
if (meminfo_.contains("SwapTotal")) {
|
||||
swaptotal = meminfo_["SwapTotal"];
|
||||
}
|
||||
unsigned long memfree;
|
||||
unsigned long swapfree = 0;
|
||||
if (meminfo_.count("SwapFree")) {
|
||||
if (meminfo_.contains("SwapFree")) {
|
||||
swapfree = meminfo_["SwapFree"];
|
||||
}
|
||||
if (meminfo_.count("MemAvailable")) {
|
||||
if (meminfo_.contains("MemAvailable")) {
|
||||
// New kernels (3.4+) have an accurate available memory field.
|
||||
memfree = meminfo_["MemAvailable"] + meminfo_["zfs_size"];
|
||||
} else {
|
||||
@@ -31,18 +50,19 @@ auto waybar::modules::Memory::update() -> void {
|
||||
}
|
||||
|
||||
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_swap_percentage = 0;
|
||||
if (swaptotal) {
|
||||
if ((bool)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 available_ram_gigabytes = 0.01 * round(memfree / 10485.76);
|
||||
float available_swap_gigabytes = 0.01 * round(swapfree / 10485.76);
|
||||
|
||||
float divisor = kUnits.at(unit_);
|
||||
float total_ram = memtotal / divisor;
|
||||
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 state = getState(used_ram_percentage);
|
||||
@@ -55,31 +75,14 @@ auto waybar::modules::Memory::update() -> void {
|
||||
} else {
|
||||
event_box_.show();
|
||||
auto icons = std::vector<std::string>{state};
|
||||
label_.set_markup(fmt::format(
|
||||
fmt::runtime(format), used_ram_percentage,
|
||||
fmt::arg("icon", getIcon(used_ram_percentage, icons)),
|
||||
fmt::arg("total", total_ram_gigabytes), fmt::arg("swapTotal", total_swap_gigabytes),
|
||||
fmt::arg("percentage", used_ram_percentage),
|
||||
updateLabelAndTooltip(
|
||||
format, fmt::format("{:.{}f}{} used", used_ram, 1, unit_), used_ram_percentage,
|
||||
fmt::arg("icon", getIcon(used_ram_percentage, icons)), fmt::arg("total", total_ram),
|
||||
fmt::arg("swapTotal", total_swap), fmt::arg("percentage", used_ram_percentage),
|
||||
fmt::arg("swapState", swaptotal == 0 ? "Off" : "On"),
|
||||
fmt::arg("swapPercentage", used_swap_percentage), fmt::arg("used", used_ram_gigabytes),
|
||||
fmt::arg("swapUsed", used_swap_gigabytes), fmt::arg("avail", available_ram_gigabytes),
|
||||
fmt::arg("swapAvail", available_swap_gigabytes)));
|
||||
}
|
||||
|
||||
if (tooltipEnabled()) {
|
||||
if (config_["tooltip-format"].isString()) {
|
||||
auto tooltip_format = config_["tooltip-format"].asString();
|
||||
label_.set_tooltip_markup(fmt::format(
|
||||
fmt::runtime(tooltip_format), used_ram_percentage,
|
||||
fmt::arg("total", total_ram_gigabytes), fmt::arg("swapTotal", total_swap_gigabytes),
|
||||
fmt::arg("percentage", used_ram_percentage),
|
||||
fmt::arg("swapState", swaptotal == 0 ? "Off" : "On"),
|
||||
fmt::arg("swapPercentage", used_swap_percentage), fmt::arg("used", used_ram_gigabytes),
|
||||
fmt::arg("swapUsed", used_swap_gigabytes), fmt::arg("avail", available_ram_gigabytes),
|
||||
fmt::arg("swapAvail", available_swap_gigabytes)));
|
||||
} else {
|
||||
label_.set_tooltip_markup(fmt::format("{:.{}f}GiB used", used_ram_gigabytes, 1));
|
||||
}
|
||||
fmt::arg("swapPercentage", used_swap_percentage), fmt::arg("used", used_ram),
|
||||
fmt::arg("swapUsed", used_swap), fmt::arg("avail", available_ram),
|
||||
fmt::arg("swapAvail", available_swap));
|
||||
}
|
||||
} else {
|
||||
event_box_.hide();
|
||||
|
||||
+57
-40
@@ -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"
|
||||
@@ -22,9 +23,12 @@ waybar::modules::MPD::MPD(const std::string& id, const Json::Value& config)
|
||||
port_(config_["port"].isUInt() ? config["port"].asUInt() : 0),
|
||||
password_(config_["password"].empty() ? "" : config_["password"].asString()),
|
||||
timeout_(config_["timeout"].isUInt() ? config_["timeout"].asUInt() * 1'000 : 30'000),
|
||||
playing_interval_(config_["playing-interval"].isUInt() ? config_["playing-interval"].asUInt()
|
||||
: 1'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_);
|
||||
}
|
||||
@@ -33,6 +37,10 @@ waybar::modules::MPD::MPD(const std::string& id, const Json::Value& config)
|
||||
spdlog::warn("{}: `timeout` configuration should be an unsigned int", module_name_);
|
||||
}
|
||||
|
||||
if (!config_["playing-interval"].isNull() && !config_["playing-interval"].isUInt()) {
|
||||
spdlog::warn("{}: `playing-interval` configuration should be an unsigned int", module_name_);
|
||||
}
|
||||
|
||||
if (!config["server"].isNull()) {
|
||||
if (!config_["server"].isString()) {
|
||||
spdlog::warn("{}:`server` configuration should be a string", module_name_);
|
||||
@@ -51,21 +59,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 +91,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 +103,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 +130,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 +160,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 +175,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 +187,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 +242,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()) {
|
||||
|
||||
+108
-35
@@ -19,39 +19,37 @@ auto format_as(enum mpd_idle val) {
|
||||
|
||||
namespace waybar::modules::detail {
|
||||
|
||||
#define IDLE_RUN_NOIDLE_AND_CMD(...) \
|
||||
if (idle_connection_.connected()) { \
|
||||
idle_connection_.disconnect(); \
|
||||
auto conn = ctx_->connection().get(); \
|
||||
if (!mpd_run_noidle(conn)) { \
|
||||
if (mpd_connection_get_error(conn) != MPD_ERROR_SUCCESS) { \
|
||||
spdlog::error("mpd: Idle: failed to unregister for IDLE events"); \
|
||||
ctx_->checkErrors(conn); \
|
||||
} \
|
||||
} \
|
||||
__VA_ARGS__; \
|
||||
#define RUN_NOIDLE_AND_CMD(STATE, ...) \
|
||||
if (idle_connection_.connected()) { \
|
||||
idle_connection_.disconnect(); \
|
||||
auto conn = ctx_->connection().get(); \
|
||||
if (!mpd_run_noidle(conn)) { \
|
||||
if (mpd_connection_get_error(conn) != MPD_ERROR_SUCCESS) { \
|
||||
spdlog::error("mpd: STATE: failed to unregister for IDLE events"); \
|
||||
ctx_->checkErrors(conn); \
|
||||
} \
|
||||
} \
|
||||
__VA_ARGS__; \
|
||||
}
|
||||
|
||||
void Idle::play() {
|
||||
IDLE_RUN_NOIDLE_AND_CMD(mpd_run_play(conn));
|
||||
RUN_NOIDLE_AND_CMD(Idle, mpd_run_play(conn));
|
||||
|
||||
ctx_->setState(std::make_unique<Playing>(ctx_));
|
||||
}
|
||||
|
||||
void Idle::pause() {
|
||||
IDLE_RUN_NOIDLE_AND_CMD(mpd_run_pause(conn, true));
|
||||
RUN_NOIDLE_AND_CMD(Idle, mpd_run_pause(conn, true));
|
||||
|
||||
ctx_->setState(std::make_unique<Paused>(ctx_));
|
||||
}
|
||||
|
||||
void Idle::stop() {
|
||||
IDLE_RUN_NOIDLE_AND_CMD(mpd_run_stop(conn));
|
||||
RUN_NOIDLE_AND_CMD(Idle, mpd_run_stop(conn));
|
||||
|
||||
ctx_->setState(std::make_unique<Stopped>(ctx_));
|
||||
}
|
||||
|
||||
#undef IDLE_RUN_NOIDLE_AND_CMD
|
||||
|
||||
void Idle::update() noexcept {
|
||||
// This is intentionally blank.
|
||||
}
|
||||
@@ -97,19 +95,16 @@ bool Idle::on_io(Glib::IOCondition const&) {
|
||||
}
|
||||
|
||||
ctx_->fetchState();
|
||||
ctx_->emit();
|
||||
mpd_state state = ctx_->state();
|
||||
|
||||
if (state == MPD_STATE_STOP) {
|
||||
ctx_->emit();
|
||||
ctx_->setState(std::make_unique<Stopped>(ctx_));
|
||||
} else if (state == MPD_STATE_PLAY) {
|
||||
ctx_->emit();
|
||||
ctx_->setState(std::make_unique<Playing>(ctx_));
|
||||
} else if (state == MPD_STATE_PAUSE) {
|
||||
ctx_->emit();
|
||||
ctx_->setState(std::make_unique<Paused>(ctx_));
|
||||
} else {
|
||||
ctx_->emit();
|
||||
// self transition
|
||||
ctx_->setState(std::make_unique<Idle>(ctx_));
|
||||
}
|
||||
@@ -118,15 +113,46 @@ bool Idle::on_io(Glib::IOCondition const&) {
|
||||
}
|
||||
|
||||
void Playing::entry() noexcept {
|
||||
sigc::slot<bool> timer_slot = sigc::mem_fun(*this, &Playing::on_timer);
|
||||
timer_connection_ = Glib::signal_timeout().connect_seconds(timer_slot, 1);
|
||||
spdlog::debug("mpd: Playing: enabled 1 second periodic timer.");
|
||||
timer();
|
||||
idle();
|
||||
spdlog::debug("mpd: Playing: enabled {}ms periodic timer.", ctx_->playing_interval());
|
||||
}
|
||||
|
||||
void Playing::exit() noexcept {
|
||||
if (idle_connection_.connected()) {
|
||||
idle_connection_.disconnect();
|
||||
spdlog::debug("mpd: Playing: unwatching FD");
|
||||
}
|
||||
|
||||
if (timer_connection_.connected()) {
|
||||
timer_connection_.disconnect();
|
||||
spdlog::debug("mpd: Playing: disabled 1 second periodic timer.");
|
||||
spdlog::debug("mpd: Playing: disabled {}ms periodic timer.", ctx_->playing_interval());
|
||||
}
|
||||
}
|
||||
|
||||
void Playing::timer() noexcept {
|
||||
if (timer_connection_.connected()) {
|
||||
timer_connection_.disconnect();
|
||||
}
|
||||
|
||||
sigc::slot<bool> timer_slot = sigc::mem_fun(*this, &Playing::on_timer);
|
||||
timer_connection_ = Glib::signal_timeout().connect(timer_slot, ctx_->playing_interval());
|
||||
}
|
||||
|
||||
void Playing::idle() noexcept {
|
||||
auto conn = ctx_->connection().get();
|
||||
assert(conn != nullptr);
|
||||
|
||||
if (!mpd_send_idle_mask(
|
||||
conn, static_cast<mpd_idle>(MPD_IDLE_PLAYER | MPD_IDLE_OPTIONS | MPD_IDLE_QUEUE))) {
|
||||
ctx_->checkErrors(conn);
|
||||
spdlog::error("mpd: Playing: failed to register for IDLE events");
|
||||
} else if (!idle_connection_.connected()) {
|
||||
spdlog::trace("mpd: Playing: watching FD");
|
||||
sigc::slot<bool, Glib::IOCondition const&> idle_slot = sigc::mem_fun(*this, &Playing::on_io);
|
||||
idle_connection_ =
|
||||
Glib::signal_io().connect(idle_slot, mpd_connection_get_fd(conn),
|
||||
Glib::IO_IN | Glib::IO_PRI | Glib::IO_ERR | Glib::IO_HUP);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -141,9 +167,12 @@ bool Playing::on_timer() {
|
||||
return false;
|
||||
}
|
||||
|
||||
RUN_NOIDLE_AND_CMD(Playing);
|
||||
|
||||
ctx_->fetchState();
|
||||
|
||||
if (!ctx_->is_playing()) {
|
||||
ctx_->emit();
|
||||
if (ctx_->is_paused()) {
|
||||
ctx_->setState(std::make_unique<Paused>(ctx_));
|
||||
} else {
|
||||
@@ -152,8 +181,51 @@ bool Playing::on_timer() {
|
||||
return false;
|
||||
}
|
||||
|
||||
ctx_->queryMPD();
|
||||
ctx_->emit();
|
||||
|
||||
idle();
|
||||
} catch (std::exception const& e) {
|
||||
spdlog::warn("mpd: Playing: error: {}", e.what());
|
||||
ctx_->setState(std::make_unique<Disconnected>(ctx_));
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
bool Playing::on_io(Glib::IOCondition const&) {
|
||||
auto conn = ctx_->connection().get();
|
||||
|
||||
// callback should do this:
|
||||
enum mpd_idle events = mpd_recv_idle(conn, /* ignore_timeout?= */ false);
|
||||
spdlog::debug("mpd: Playing: recv_idle events -> {}", events);
|
||||
|
||||
mpd_response_finish(conn);
|
||||
try {
|
||||
ctx_->checkErrors(conn);
|
||||
|
||||
ctx_->fetchState();
|
||||
|
||||
if (!ctx_->is_playing()) {
|
||||
ctx_->emit();
|
||||
if (ctx_->is_paused()) {
|
||||
ctx_->setState(std::make_unique<Paused>(ctx_));
|
||||
} else {
|
||||
ctx_->setState(std::make_unique<Stopped>(ctx_));
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
ctx_->emit();
|
||||
|
||||
if (!mpd_send_idle_mask(
|
||||
conn, static_cast<mpd_idle>(MPD_IDLE_PLAYER | MPD_IDLE_OPTIONS | MPD_IDLE_QUEUE))) {
|
||||
ctx_->checkErrors(conn);
|
||||
spdlog::error("mpd: Playing: failed to register for IDLE events");
|
||||
}
|
||||
|
||||
// Defer the next timer
|
||||
timer();
|
||||
} catch (std::exception const& e) {
|
||||
spdlog::warn("mpd: Playing: error: {}", e.what());
|
||||
ctx_->setState(std::make_unique<Disconnected>(ctx_));
|
||||
@@ -164,21 +236,23 @@ bool Playing::on_timer() {
|
||||
}
|
||||
|
||||
void Playing::stop() {
|
||||
if (timer_connection_.connected()) {
|
||||
timer_connection_.disconnect();
|
||||
RUN_NOIDLE_AND_CMD(
|
||||
Playing, if (timer_connection_.connected()) {
|
||||
timer_connection_.disconnect();
|
||||
|
||||
mpd_run_stop(ctx_->connection().get());
|
||||
}
|
||||
mpd_run_stop(ctx_->connection().get());
|
||||
});
|
||||
|
||||
ctx_->setState(std::make_unique<Stopped>(ctx_));
|
||||
}
|
||||
|
||||
void Playing::pause() {
|
||||
if (timer_connection_.connected()) {
|
||||
timer_connection_.disconnect();
|
||||
RUN_NOIDLE_AND_CMD(
|
||||
Playing, if (timer_connection_.connected()) {
|
||||
timer_connection_.disconnect();
|
||||
|
||||
mpd_run_pause(ctx_->connection().get(), true);
|
||||
}
|
||||
mpd_run_pause(ctx_->connection().get(), true);
|
||||
});
|
||||
|
||||
ctx_->setState(std::make_unique<Paused>(ctx_));
|
||||
}
|
||||
@@ -212,7 +286,6 @@ bool Paused::on_timer() {
|
||||
}
|
||||
|
||||
ctx_->fetchState();
|
||||
|
||||
ctx_->emit();
|
||||
|
||||
if (ctx_->is_paused()) {
|
||||
@@ -283,7 +356,6 @@ bool Stopped::on_timer() {
|
||||
}
|
||||
|
||||
ctx_->fetchState();
|
||||
|
||||
ctx_->emit();
|
||||
|
||||
if (ctx_->is_stopped()) {
|
||||
@@ -387,4 +459,5 @@ bool Disconnected::on_timer() {
|
||||
|
||||
void Disconnected::update() noexcept { ctx_->do_update(); }
|
||||
|
||||
#undef RUN_NOIDLE_AND_CMD
|
||||
} // namespace waybar::modules::detail
|
||||
|
||||
+34
-59
@@ -7,6 +7,7 @@
|
||||
#include <string>
|
||||
|
||||
#include "util/scope_guard.hpp"
|
||||
#include "util/utf8_string.hpp"
|
||||
|
||||
extern "C" {
|
||||
#include <playerctl/playerctl.h>
|
||||
@@ -30,6 +31,7 @@ Mpris::Mpris(const std::string& id, const Json::Value& config)
|
||||
dynamic_separator_(" - "),
|
||||
truncate_hours_(true),
|
||||
tooltip_len_limits_(false),
|
||||
prefer_album_artist_(false),
|
||||
// this character is used in Gnome so it's fine to use it here
|
||||
ellipsis_("\u2026"),
|
||||
player_("playerctld"),
|
||||
@@ -67,6 +69,9 @@ Mpris::Mpris(const std::string& id, const Json::Value& config)
|
||||
if (config_["enable-tooltip-len-limits"].isBool()) {
|
||||
tooltip_len_limits_ = config["enable-tooltip-len-limits"].asBool();
|
||||
}
|
||||
if (config_["prefer-album-artist"].isBool()) {
|
||||
prefer_album_artist_ = config["prefer-album-artist"].asBool();
|
||||
}
|
||||
}
|
||||
|
||||
if (config["artist-len"].isUInt()) {
|
||||
@@ -202,69 +207,28 @@ 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 (prefer_album_artist_) {
|
||||
auto album_artist = info.album_artist.value_or(std::string());
|
||||
if (!album_artist.empty()) {
|
||||
artist = album_artist;
|
||||
}
|
||||
}
|
||||
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 +256,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 +277,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;
|
||||
@@ -567,6 +531,7 @@ auto Mpris::getPlayerInfo() -> std::optional<PlayerInfo> {
|
||||
.status_string = player_status,
|
||||
.artist = std::nullopt,
|
||||
.album = std::nullopt,
|
||||
.album_artist = std::nullopt,
|
||||
.title = std::nullopt,
|
||||
.length = std::nullopt,
|
||||
};
|
||||
@@ -578,6 +543,13 @@ auto Mpris::getPlayerInfo() -> std::optional<PlayerInfo> {
|
||||
}
|
||||
if (error) goto errorexit;
|
||||
|
||||
if (auto* album_artist_ =
|
||||
playerctl_player_print_metadata_prop(player, "xesam:albumArtist", &error)) {
|
||||
spdlog::debug("mpris[{}]: albumArtist = {}", info.name, album_artist_);
|
||||
info.album_artist = album_artist_;
|
||||
g_free(album_artist_);
|
||||
}
|
||||
|
||||
if (auto* album_ = playerctl_player_get_album(last_active_player_, &error)) {
|
||||
spdlog::debug("mpris[{}]: album = {}", info.name, album_);
|
||||
info.album = album_;
|
||||
@@ -763,7 +735,7 @@ auto Mpris::update() -> void {
|
||||
if (label_format.empty()) {
|
||||
label_.hide();
|
||||
} else {
|
||||
label_.set_markup(label_format);
|
||||
setLabelMarkup(label_format);
|
||||
label_.show();
|
||||
}
|
||||
} catch (fmt::format_error const& e) {
|
||||
@@ -775,15 +747,18 @@ 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)),
|
||||
fmt::arg("status_icon", getIconFromJson(config_["status-icons"], info.status_string)));
|
||||
|
||||
label_.set_tooltip_markup(tooltip_text);
|
||||
setTooltipMarkup(tooltip_text);
|
||||
} catch (fmt::format_error const& e) {
|
||||
spdlog::warn("mpris: format error (tooltip): {}", e.what());
|
||||
}
|
||||
|
||||
+89
-71
@@ -294,38 +294,61 @@ auto waybar::modules::Network::update() -> void {
|
||||
std::string tooltip_format;
|
||||
auto now = std::chrono::steady_clock::now();
|
||||
auto elapsed_seconds = std::chrono::duration<double>(now - bandwidth_last_sample_time_).count();
|
||||
if (elapsed_seconds <= 0.0) {
|
||||
elapsed_seconds = std::chrono::duration<double>(interval_).count();
|
||||
}
|
||||
bandwidth_last_sample_time_ = now;
|
||||
|
||||
auto bandwidth = readBandwidthUsage();
|
||||
auto bandwidth_down = 0ull;
|
||||
auto bandwidth_up = 0ull;
|
||||
if (bandwidth.has_value()) {
|
||||
auto down_octets = (*bandwidth).first;
|
||||
auto up_octets = (*bandwidth).second;
|
||||
|
||||
bandwidth_down = down_octets - bandwidth_down_total_;
|
||||
bandwidth_down_total_ = down_octets;
|
||||
// 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) {
|
||||
elapsed_seconds = std::chrono::duration<double>(interval_).count();
|
||||
}
|
||||
bandwidth_last_sample_time_ = now;
|
||||
|
||||
bandwidth_up = up_octets - bandwidth_up_total_;
|
||||
bandwidth_up_total_ = up_octets;
|
||||
auto bandwidth = readBandwidthUsage();
|
||||
if (bandwidth.has_value()) {
|
||||
auto down_octets = (*bandwidth).first;
|
||||
auto up_octets = (*bandwidth).second;
|
||||
|
||||
bandwidth_down = down_octets - bandwidth_down_total_;
|
||||
bandwidth_down_total_ = down_octets;
|
||||
|
||||
bandwidth_up = up_octets - bandwidth_up_total_;
|
||||
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();
|
||||
}
|
||||
|
||||
auto threshold_state = getState(signal_strength_);
|
||||
|
||||
if (!alt_) {
|
||||
auto state = getNetworkState();
|
||||
if (!state_.empty() && label_.get_style_context()->has_class(state_)) {
|
||||
label_.get_style_context()->remove_class(state_);
|
||||
}
|
||||
if (config_["format-" + state].isString()) {
|
||||
if (!threshold_state.empty() && config_["format-" + state + "-" + threshold_state].isString()) {
|
||||
default_format_ = config_["format-" + state + "-" + threshold_state].asString();
|
||||
} else if (config_["format-" + state].isString()) {
|
||||
default_format_ = config_["format-" + state].asString();
|
||||
} else if (config_["format"].isString()) {
|
||||
default_format_ = config_["format"].asString();
|
||||
} else {
|
||||
default_format_ = DEFAULT_FORMAT;
|
||||
}
|
||||
if (config_["tooltip-format-" + state].isString()) {
|
||||
if (!threshold_state.empty() &&
|
||||
config_["tooltip-format-" + state + "-" + threshold_state].isString()) {
|
||||
tooltip_format = config_["tooltip-format-" + state + "-" + threshold_state].asString();
|
||||
} else if (config_["tooltip-format-" + state].isString()) {
|
||||
tooltip_format = config_["tooltip-format-" + state].asString();
|
||||
}
|
||||
if (!label_.get_style_context()->has_class(state)) {
|
||||
@@ -334,7 +357,6 @@ auto waybar::modules::Network::update() -> void {
|
||||
format_ = default_format_;
|
||||
state_ = state;
|
||||
}
|
||||
getState(signal_strength_);
|
||||
|
||||
std::string final_ipaddr_;
|
||||
if (addr_pref_ == ip_addr_pref::IPV4) {
|
||||
@@ -348,30 +370,47 @@ auto waybar::modules::Network::update() -> void {
|
||||
final_ipaddr_ += ipaddr6_;
|
||||
}
|
||||
|
||||
auto text = fmt::format(
|
||||
fmt::runtime(format_), fmt::arg("essid", essid_), fmt::arg("bssid", bssid_),
|
||||
fmt::arg("signaldBm", signal_strength_dbm_), fmt::arg("signalStrength", signal_strength_),
|
||||
fmt::arg("signalStrengthApp", signal_strength_app_), fmt::arg("ifname", ifname_),
|
||||
fmt::arg("netmask", netmask_), fmt::arg("netmask6", netmask6_),
|
||||
fmt::arg("ipaddr", final_ipaddr_), fmt::arg("gwaddr", gwaddr_), fmt::arg("cidr", cidr_),
|
||||
fmt::arg("cidr6", cidr6_), fmt::arg("frequency", fmt::format("{:.1f}", frequency_)),
|
||||
fmt::arg("icon", getIcon(signal_strength_, state_)),
|
||||
fmt::arg("bandwidthDownBits", pow_format(bandwidth_down * 8ull / elapsed_seconds, "b/s")),
|
||||
fmt::arg("bandwidthUpBits", pow_format(bandwidth_up * 8ull / elapsed_seconds, "b/s")),
|
||||
fmt::dynamic_format_arg_store<fmt::format_context> store;
|
||||
store.push_back(fmt::arg("essid", essid_));
|
||||
store.push_back(fmt::arg("bssid", bssid_));
|
||||
store.push_back(fmt::arg("signaldBm", signal_strength_dbm_));
|
||||
store.push_back(fmt::arg("signalStrength", signal_strength_));
|
||||
store.push_back(fmt::arg("signalStrengthApp", signal_strength_app_));
|
||||
store.push_back(fmt::arg("ifname", ifname_));
|
||||
store.push_back(fmt::arg("netmask", netmask_));
|
||||
store.push_back(fmt::arg("netmask6", netmask6_));
|
||||
store.push_back(fmt::arg("ipaddr", final_ipaddr_));
|
||||
store.push_back(fmt::arg("gwaddr", gwaddr_));
|
||||
store.push_back(fmt::arg("cidr", cidr_));
|
||||
store.push_back(fmt::arg("cidr6", cidr6_));
|
||||
store.push_back(fmt::arg("frequency", fmt::format("{:.1f}", frequency_)));
|
||||
store.push_back(fmt::arg("icon", getIcon(signal_strength_, state_)));
|
||||
store.push_back(
|
||||
fmt::arg("bandwidthDownBits", pow_format(bandwidth_down * 8ull / elapsed_seconds, "b/s")));
|
||||
store.push_back(
|
||||
fmt::arg("bandwidthUpBits", pow_format(bandwidth_up * 8ull / elapsed_seconds, "b/s")));
|
||||
store.push_back(
|
||||
fmt::arg("bandwidthTotalBits",
|
||||
pow_format((bandwidth_up + bandwidth_down) * 8ull / elapsed_seconds, "b/s")),
|
||||
fmt::arg("bandwidthDownOctets", pow_format(bandwidth_down / elapsed_seconds, "o/s")),
|
||||
fmt::arg("bandwidthUpOctets", pow_format(bandwidth_up / elapsed_seconds, "o/s")),
|
||||
fmt::arg("bandwidthTotalOctets",
|
||||
pow_format((bandwidth_up + bandwidth_down) / elapsed_seconds, "o/s")),
|
||||
fmt::arg("bandwidthDownBytes", pow_format(bandwidth_down / elapsed_seconds, "B/s")),
|
||||
fmt::arg("bandwidthUpBytes", pow_format(bandwidth_up / elapsed_seconds, "B/s")),
|
||||
fmt::arg("bandwidthTotalBytes",
|
||||
pow_format((bandwidth_up + bandwidth_down) / elapsed_seconds, "B/s")),
|
||||
fmt::arg("rxBitrate", pow_format(rx_bitrate_, "b/s")),
|
||||
fmt::arg("txBitrate", pow_format(tx_bitrate_, "b/s")));
|
||||
if (text.compare(label_.get_label()) != 0) {
|
||||
label_.set_markup(text);
|
||||
pow_format((bandwidth_up + bandwidth_down) * 8ull / elapsed_seconds, "b/s")));
|
||||
store.push_back(
|
||||
fmt::arg("bandwidthDownOctets", pow_format(bandwidth_down / elapsed_seconds, "o/s")));
|
||||
store.push_back(fmt::arg("bandwidthUpOctets", pow_format(bandwidth_up / elapsed_seconds, "o/s")));
|
||||
store.push_back(fmt::arg("bandwidthTotalOctets",
|
||||
pow_format((bandwidth_up + bandwidth_down) / elapsed_seconds, "o/s")));
|
||||
store.push_back(
|
||||
fmt::arg("bandwidthDownBytes", pow_format(bandwidth_down / elapsed_seconds, "B/s")));
|
||||
store.push_back(fmt::arg("bandwidthUpBytes", pow_format(bandwidth_up / elapsed_seconds, "B/s")));
|
||||
store.push_back(fmt::arg("bandwidthDownBytesCompact",
|
||||
pow_format(bandwidth_down / elapsed_seconds, "B", false, 2)));
|
||||
store.push_back(fmt::arg("bandwidthUpBytesCompact",
|
||||
pow_format(bandwidth_up / elapsed_seconds, "B", false, 2)));
|
||||
store.push_back(fmt::arg("bandwidthTotalBytes",
|
||||
pow_format((bandwidth_up + bandwidth_down) / elapsed_seconds, "B/s")));
|
||||
store.push_back(fmt::arg("rxBitrate", pow_format(rx_bitrate_, "b/s")));
|
||||
store.push_back(fmt::arg("txBitrate", pow_format(tx_bitrate_, "b/s")));
|
||||
|
||||
auto text = fmt::vformat(format_, store);
|
||||
if (setLabelMarkup(text)) {
|
||||
if (text.empty()) {
|
||||
event_box_.hide();
|
||||
} else {
|
||||
@@ -383,33 +422,9 @@ auto waybar::modules::Network::update() -> void {
|
||||
tooltip_format = config_["tooltip-format"].asString();
|
||||
}
|
||||
if (!tooltip_format.empty()) {
|
||||
auto tooltip_text = fmt::format(
|
||||
fmt::runtime(tooltip_format), fmt::arg("essid", essid_), fmt::arg("bssid", bssid_),
|
||||
fmt::arg("signaldBm", signal_strength_dbm_), fmt::arg("signalStrength", signal_strength_),
|
||||
fmt::arg("signalStrengthApp", signal_strength_app_), fmt::arg("ifname", ifname_),
|
||||
fmt::arg("netmask", netmask_), fmt::arg("netmask6", netmask6_),
|
||||
fmt::arg("ipaddr", final_ipaddr_), fmt::arg("gwaddr", gwaddr_), fmt::arg("cidr", cidr_),
|
||||
fmt::arg("cidr6", cidr6_), fmt::arg("frequency", fmt::format("{:.1f}", frequency_)),
|
||||
fmt::arg("icon", getIcon(signal_strength_, state_)),
|
||||
fmt::arg("bandwidthDownBits", pow_format(bandwidth_down * 8ull / elapsed_seconds, "b/s")),
|
||||
fmt::arg("bandwidthUpBits", pow_format(bandwidth_up * 8ull / elapsed_seconds, "b/s")),
|
||||
fmt::arg("bandwidthTotalBits",
|
||||
pow_format((bandwidth_up + bandwidth_down) * 8ull / elapsed_seconds, "b/s")),
|
||||
fmt::arg("bandwidthDownOctets", pow_format(bandwidth_down / elapsed_seconds, "o/s")),
|
||||
fmt::arg("bandwidthUpOctets", pow_format(bandwidth_up / elapsed_seconds, "o/s")),
|
||||
fmt::arg("bandwidthTotalOctets",
|
||||
pow_format((bandwidth_up + bandwidth_down) / elapsed_seconds, "o/s")),
|
||||
fmt::arg("bandwidthDownBytes", pow_format(bandwidth_down / elapsed_seconds, "B/s")),
|
||||
fmt::arg("bandwidthUpBytes", pow_format(bandwidth_up / elapsed_seconds, "B/s")),
|
||||
fmt::arg("bandwidthTotalBytes",
|
||||
pow_format((bandwidth_up + bandwidth_down) / elapsed_seconds, "B/s")),
|
||||
fmt::arg("rxBitrate", pow_format(rx_bitrate_, "b/s")),
|
||||
fmt::arg("txBitrate", pow_format(tx_bitrate_, "b/s")));
|
||||
if (label_.get_tooltip_text() != tooltip_text) {
|
||||
label_.set_tooltip_markup(tooltip_text);
|
||||
}
|
||||
} else if (label_.get_tooltip_text() != text) {
|
||||
label_.set_tooltip_markup(text);
|
||||
setTooltipMarkup(fmt::vformat(tooltip_format, store));
|
||||
} else {
|
||||
setTooltipMarkup(text);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -697,12 +712,15 @@ int waybar::modules::Network::handleEvents(struct nl_msg* msg, void* data) {
|
||||
changed_cidr);
|
||||
}
|
||||
} else {
|
||||
net->ipaddr_.clear();
|
||||
net->ipaddr6_.clear();
|
||||
net->cidr_ = 0;
|
||||
net->cidr6_ = 0;
|
||||
net->netmask_.clear();
|
||||
net->netmask6_.clear();
|
||||
if (ifa->ifa_family == AF_INET) {
|
||||
net->ipaddr_.clear();
|
||||
net->cidr_ = 0;
|
||||
net->netmask_.clear();
|
||||
} else if (ifa->ifa_family == AF_INET6) {
|
||||
net->ipaddr6_.clear();
|
||||
net->cidr6_ = 0;
|
||||
net->netmask6_.clear();
|
||||
}
|
||||
spdlog::debug("network: {} addr deleted {}/{}", net->ifname_,
|
||||
inet_ntop(ifa->ifa_family, RTA_DATA(ifa_rta), ipaddr, sizeof(ipaddr)),
|
||||
ifa->ifa_prefixlen);
|
||||
|
||||
@@ -196,6 +196,18 @@ void IPC::parseIPC(const std::string& line) {
|
||||
for (auto& win : windows_) {
|
||||
win["is_focused"] = focused && win["id"].asUInt64() == id;
|
||||
}
|
||||
} else if (const auto &payload = ev["WindowLayoutsChanged"]) {
|
||||
const auto &values = payload["changes"];
|
||||
for (const auto &changed : values) {
|
||||
const auto id = changed[0].asUInt64();
|
||||
const auto &change = changed[1];
|
||||
for (auto &win : windows_) {
|
||||
if (win["id"].asUInt64() == id) {
|
||||
win["layout"] = change;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -17,6 +17,7 @@ Window::Window(const std::string& id, const Bar& bar, const Json::Value& config)
|
||||
gIPC->registerForIPC("WindowOpenedOrChanged", this);
|
||||
gIPC->registerForIPC("WindowClosed", this);
|
||||
gIPC->registerForIPC("WindowFocusChanged", this);
|
||||
gIPC->registerForIPC("WindowLayoutsChanged", this);
|
||||
|
||||
dp.emit();
|
||||
}
|
||||
@@ -54,15 +55,25 @@ void Window::doUpdate() {
|
||||
if (it != windows.cend()) {
|
||||
const auto& window = *it;
|
||||
|
||||
auto max_col = -1;
|
||||
for (const auto &win : windows) {
|
||||
if (win["workspace_id"].asUInt64() != window["workspace_id"].asUInt64()) {
|
||||
continue;
|
||||
}
|
||||
const auto col = win["layout"]["pos_in_scrolling_layout"][0].asInt64();
|
||||
if (col > max_col) max_col = col;
|
||||
}
|
||||
const auto title = window["title"].asString();
|
||||
const auto appId = window["app_id"].asString();
|
||||
const auto col = window["layout"]["pos_in_scrolling_layout"][0].asInt64();
|
||||
const auto sanitizedTitle = waybar::util::sanitize_string(title);
|
||||
const auto sanitizedAppId = waybar::util::sanitize_string(appId);
|
||||
|
||||
label_.show();
|
||||
label_.set_markup(waybar::util::rewriteString(
|
||||
fmt::format(fmt::runtime(format_), fmt::arg("title", sanitizedTitle),
|
||||
fmt::arg("app_id", sanitizedAppId)),
|
||||
fmt::arg("app_id", sanitizedAppId), fmt::arg("col", col),
|
||||
fmt::arg("max_col", max_col)),
|
||||
config_["rewrite"]));
|
||||
|
||||
updateAppIconName(appId, "");
|
||||
@@ -82,7 +93,12 @@ void Window::doUpdate() {
|
||||
oldAppId_ = appId;
|
||||
}
|
||||
} else {
|
||||
label_.hide();
|
||||
label_.show();
|
||||
label_.set_markup(waybar::util::rewriteString(
|
||||
fmt::format(fmt::runtime(format_), fmt::arg("title", ""),
|
||||
fmt::arg("app_id", "")),
|
||||
config_["rewrite"]));
|
||||
|
||||
updateAppIconName("", "");
|
||||
setClass("solo", false);
|
||||
if (!oldAppId_.empty()) setClass(oldAppId_, false);
|
||||
|
||||
@@ -4,10 +4,34 @@
|
||||
#include <gtkmm/label.h>
|
||||
#include <spdlog/spdlog.h>
|
||||
|
||||
#include <algorithm>
|
||||
#include <cctype>
|
||||
|
||||
namespace waybar::modules::niri {
|
||||
|
||||
Workspaces::Workspaces(const std::string& id, const Bar& bar, const Json::Value& config)
|
||||
: AModule(config, "workspaces", id, false, false), bar_(bar), box_(bar.orientation, 0) {
|
||||
const auto config_sort_by_number = config_["sort-by-number"];
|
||||
if (config_sort_by_number.isBool()) {
|
||||
spdlog::warn("[niri/workspaces]: Prefer sort-by-id instead of sort-by-number");
|
||||
sort_by_id_ = config_sort_by_number.asBool();
|
||||
}
|
||||
|
||||
const auto config_sort_by_id = config_["sort-by-id"];
|
||||
if (config_sort_by_id.isBool()) {
|
||||
sort_by_id_ = config_sort_by_id.asBool();
|
||||
}
|
||||
|
||||
const auto config_sort_by_name = config_["sort-by-name"];
|
||||
if (config_sort_by_name.isBool()) {
|
||||
sort_by_name_ = config_sort_by_name.asBool();
|
||||
}
|
||||
|
||||
const auto config_sort_by_coordinates = config_["sort-by-coordinates"];
|
||||
if (config_sort_by_coordinates.isBool()) {
|
||||
sort_by_coordinates_ = config_sort_by_coordinates.asBool();
|
||||
}
|
||||
|
||||
box_.set_name("workspaces");
|
||||
if (!id.empty()) {
|
||||
box_.get_style_context()->add_class(id);
|
||||
@@ -22,6 +46,12 @@ Workspaces::Workspaces(const std::string& id, const Bar& bar, const Json::Value&
|
||||
gIPC->registerForIPC("WorkspaceActiveWindowChanged", this);
|
||||
gIPC->registerForIPC("WorkspaceUrgencyChanged", this);
|
||||
|
||||
if (config["enable-bar-scroll"].asBool()) {
|
||||
auto& window = const_cast<Bar&>(bar_).window;
|
||||
window.add_events(Gdk::SCROLL_MASK | Gdk::SMOOTH_SCROLL_MASK);
|
||||
window.signal_scroll_event().connect(sigc::mem_fun(*this, &Workspaces::handleScroll));
|
||||
}
|
||||
|
||||
dp.emit();
|
||||
}
|
||||
|
||||
@@ -41,6 +71,8 @@ void Workspaces::doUpdate() {
|
||||
return ws["output"].asString() == bar_.output->name;
|
||||
});
|
||||
|
||||
sortWorkspaces(my_workspaces);
|
||||
|
||||
// Remove buttons for removed workspaces.
|
||||
for (auto it = buttons_.begin(); it != buttons_.end();) {
|
||||
auto ws = std::find_if(my_workspaces.begin(), my_workspaces.end(),
|
||||
@@ -97,13 +129,15 @@ void Workspaces::doUpdate() {
|
||||
|
||||
if (config_["format"].isString()) {
|
||||
auto format = config_["format"].asString();
|
||||
name = fmt::format(fmt::runtime(format), fmt::arg("icon", getIcon(name, ws)),
|
||||
fmt::arg("value", name), fmt::arg("name", ws["name"].asString()),
|
||||
fmt::arg("index", ws["idx"].asUInt()),
|
||||
fmt::arg("output", ws["output"].asString()));
|
||||
name = fmt::format(
|
||||
fmt::runtime(format), fmt::arg("icon", getIcon(name, ws)), fmt::arg("value", name),
|
||||
fmt::arg("name", ws["name"].asString()), fmt::arg("index", ws["idx"].asUInt()),
|
||||
fmt::arg("output", ws["output"].asString()), fmt::arg("total", my_workspaces.size()));
|
||||
}
|
||||
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);
|
||||
}
|
||||
@@ -114,6 +148,11 @@ void Workspaces::doUpdate() {
|
||||
button.show();
|
||||
else
|
||||
button.hide();
|
||||
} else if (config_["hide-empty"].asBool()) {
|
||||
if (ws["active_window_id"].isNull() && !ws["is_focused"].asBool())
|
||||
button.hide();
|
||||
else
|
||||
button.show();
|
||||
} else {
|
||||
button.show();
|
||||
}
|
||||
@@ -123,8 +162,7 @@ void Workspaces::doUpdate() {
|
||||
for (auto it = my_workspaces.cbegin(); it != my_workspaces.cend(); ++it) {
|
||||
const auto& ws = *it;
|
||||
|
||||
auto pos = ws["idx"].asUInt() - 1;
|
||||
if (alloutputs) pos = it - my_workspaces.cbegin();
|
||||
const auto pos = static_cast<int>(std::distance(my_workspaces.cbegin(), it));
|
||||
|
||||
auto& button = buttons_[ws["id"].asUInt64()];
|
||||
box_.reorder_child(button, pos);
|
||||
@@ -193,4 +231,98 @@ std::string Workspaces::getIcon(const std::string& value, const Json::Value& ws)
|
||||
return value;
|
||||
}
|
||||
|
||||
bool Workspaces::handleScroll(GdkEventScroll* e) {
|
||||
if (gdk_event_get_pointer_emulated((GdkEvent*)e) != 0) {
|
||||
/**
|
||||
* Ignore emulated scroll events on window
|
||||
*/
|
||||
return false;
|
||||
}
|
||||
|
||||
auto dir = AModule::getScrollDir(e);
|
||||
if (dir == SCROLL_DIR::NONE) {
|
||||
return true;
|
||||
}
|
||||
|
||||
try {
|
||||
Json::Value request(Json::objectValue);
|
||||
auto& action = (request["Action"] = Json::Value(Json::objectValue));
|
||||
|
||||
std::string action_name;
|
||||
|
||||
if (dir == SCROLL_DIR::DOWN || dir == SCROLL_DIR::RIGHT) {
|
||||
action_name = "FocusWorkspaceDown";
|
||||
} else if (dir == SCROLL_DIR::UP || dir == SCROLL_DIR::LEFT) {
|
||||
action_name = "FocusWorkspaceUp";
|
||||
} else {
|
||||
return true;
|
||||
}
|
||||
|
||||
action[action_name] = Json::Value(Json::objectValue);
|
||||
|
||||
IPC::send(request);
|
||||
|
||||
} catch (const std::exception& e) {
|
||||
spdlog::error("Workspaces: {}", e.what());
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
void Workspaces::sortWorkspaces(std::vector<Json::Value>& workspaces) const {
|
||||
auto get_name = [](const Json::Value& ws) -> std::string {
|
||||
if (ws["name"]) return ws["name"].asString();
|
||||
return std::to_string(ws["idx"].asUInt());
|
||||
};
|
||||
|
||||
auto is_numeric = [](const std::string& value) {
|
||||
return !value.empty() &&
|
||||
std::all_of(value.begin(), value.end(), [](unsigned char c) { return std::isdigit(c); });
|
||||
};
|
||||
|
||||
const bool names_are_numeric =
|
||||
std::all_of(workspaces.begin(), workspaces.end(),
|
||||
[&](const auto& ws) { return is_numeric(get_name(ws)); });
|
||||
|
||||
auto compare_numeric_strings = [](const std::string& a, const std::string& b) {
|
||||
if (a.size() != b.size()) return a.size() < b.size();
|
||||
return a < b;
|
||||
};
|
||||
|
||||
std::sort(workspaces.begin(), workspaces.end(), [&](const auto& a, const auto& b) {
|
||||
if (sort_by_id_) {
|
||||
return a["id"].asUInt64() < b["id"].asUInt64();
|
||||
}
|
||||
|
||||
if (sort_by_name_) {
|
||||
const auto a_name = get_name(a);
|
||||
const auto b_name = get_name(b);
|
||||
if (a_name == b_name) return a["id"].asUInt64() < b["id"].asUInt64();
|
||||
if (names_are_numeric) return compare_numeric_strings(a_name, b_name);
|
||||
return a_name < b_name;
|
||||
}
|
||||
|
||||
if (sort_by_coordinates_) {
|
||||
const auto& a_output = a["output"].asString();
|
||||
const auto& b_output = b["output"].asString();
|
||||
if (a_output == b_output) {
|
||||
const auto a_idx = a["idx"].asUInt();
|
||||
const auto b_idx = b["idx"].asUInt();
|
||||
if (a_idx == b_idx) return a["id"].asUInt64() < b["id"].asUInt64();
|
||||
return a_idx < b_idx;
|
||||
}
|
||||
return a_output < b_output;
|
||||
}
|
||||
|
||||
// Default to sorting by workspace index on each output.
|
||||
const auto& a_output = a["output"].asString();
|
||||
const auto& b_output = b["output"].asString();
|
||||
const auto a_idx = a["idx"].asUInt();
|
||||
const auto b_idx = b["idx"].asUInt();
|
||||
if (a_output == b_output) return a_idx < b_idx;
|
||||
return a_output < b_output;
|
||||
});
|
||||
}
|
||||
|
||||
} // namespace waybar::modules::niri
|
||||
|
||||
@@ -12,7 +12,8 @@ PowerProfilesDaemon::PowerProfilesDaemon(const std::string& id, const Json::Valu
|
||||
if (config_["tooltip-format"].isString()) {
|
||||
tooltipFormat_ = config_["tooltip-format"].asString();
|
||||
} else {
|
||||
tooltipFormat_ = "Power profile: {profile}\nDriver: {driver}";
|
||||
tooltipFormat_ =
|
||||
"Power profile: {profile}\nCPU driver: {cpu_driver}\nPlatform driver: {platform_driver}";
|
||||
}
|
||||
// Fasten your seatbelt, we're up for quite a ride. The rest of the
|
||||
// init is performed asynchronously. There's 2 callbacks involved.
|
||||
@@ -95,15 +96,51 @@ void PowerProfilesDaemon::populateInitState() {
|
||||
powerProfilesProxy_->get_cached_property(profilesVariant, "Profiles");
|
||||
for (auto& variantDict : profilesVariant.get()) {
|
||||
Glib::ustring name;
|
||||
// Legacy single `Driver` property, still exposed by older
|
||||
// power-profiles-daemon versions.
|
||||
Glib::ustring driver;
|
||||
Glib::ustring cpuDriver;
|
||||
Glib::ustring platformDriver;
|
||||
if (auto p = variantDict.find("Profile"); p != variantDict.end()) {
|
||||
name = p->second.get();
|
||||
}
|
||||
if (auto d = variantDict.find("Driver"); d != variantDict.end()) {
|
||||
driver = d->second.get();
|
||||
}
|
||||
if (auto cd = variantDict.find("CpuDriver"); cd != variantDict.end()) {
|
||||
cpuDriver = cd->second.get();
|
||||
}
|
||||
if (auto pd = variantDict.find("PlatformDriver"); pd != variantDict.end()) {
|
||||
platformDriver = pd->second.get();
|
||||
}
|
||||
|
||||
// Recent power-profiles-daemon versions split the single `Driver`
|
||||
// property into `CpuDriver` and `PlatformDriver`. When talking to an
|
||||
// older daemon that only exposes `Driver`, fall back to it so the new
|
||||
// {cpu_driver}/{platform_driver} placeholders still resolve.
|
||||
if (cpuDriver.empty()) {
|
||||
cpuDriver = driver;
|
||||
}
|
||||
if (platformDriver.empty()) {
|
||||
platformDriver = driver;
|
||||
}
|
||||
// Conversely, keep the legacy {driver} placeholder working against a
|
||||
// recent daemon that no longer exposes `Driver` by deriving it from
|
||||
// the CPU driver.
|
||||
if (driver.empty()) {
|
||||
driver = cpuDriver;
|
||||
}
|
||||
|
||||
if (driver.empty()) {
|
||||
driver = "Unavailable";
|
||||
cpuDriver = "Unavailable";
|
||||
platformDriver = "Unavailable";
|
||||
spdlog::warn("Cannot find power profiles daemon driver.");
|
||||
}
|
||||
|
||||
if (!name.empty()) {
|
||||
availableProfiles_.emplace_back(std::move(name), std::move(driver));
|
||||
availableProfiles_.emplace_back(std::move(name), std::move(driver), std::move(cpuDriver),
|
||||
std::move(platformDriver));
|
||||
} else {
|
||||
spdlog::error(
|
||||
"Power profiles daemon: power-profiles-daemon sent us an empty power profile name. "
|
||||
@@ -150,15 +187,14 @@ void PowerProfilesDaemon::switchToProfile(std::string const& str) {
|
||||
auto PowerProfilesDaemon::update() -> void {
|
||||
if (connected_ && activeProfile_ != availableProfiles_.end()) {
|
||||
auto profile = (*activeProfile_);
|
||||
// Set label
|
||||
fmt::dynamic_format_arg_store<fmt::format_context> store;
|
||||
store.push_back(fmt::arg("profile", profile.name));
|
||||
store.push_back(fmt::arg("driver", profile.driver));
|
||||
store.push_back(fmt::arg("icon", getIcon(0, profile.name)));
|
||||
label_.set_markup(fmt::vformat(format_, store));
|
||||
if (tooltipEnabled()) {
|
||||
label_.set_tooltip_markup(fmt::vformat(tooltipFormat_, store));
|
||||
}
|
||||
// Set label and tooltip
|
||||
updateLabelAndTooltip(format_, tooltipFormat_, fmt::arg("profile", profile.name),
|
||||
// Legacy placeholder, kept for backward compatibility with existing
|
||||
// configs.
|
||||
fmt::arg("driver", profile.driver),
|
||||
fmt::arg("cpu_driver", profile.cpuDriver),
|
||||
fmt::arg("platform_driver", profile.platformDriver),
|
||||
fmt::arg("icon", getIcon(0, profile.name)));
|
||||
|
||||
// Set CSS class
|
||||
if (!currentStyle_.empty()) {
|
||||
|
||||
@@ -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;
|
||||
|
||||
+20
-16
@@ -7,6 +7,11 @@ waybar::modules::Pulseaudio::Pulseaudio(const std::string& id, const Json::Value
|
||||
|
||||
backend = util::AudioBackend::getInstance([this] { this->dp.emit(); });
|
||||
backend->setIgnoredSinks(config_["ignored-sinks"]);
|
||||
backend->setSinkMapping(config_["sink-mapping"]);
|
||||
|
||||
if (config_["target"].isString() && config_["target"].asString() == "source") {
|
||||
target = util::PulseaudioTarget::Source;
|
||||
}
|
||||
}
|
||||
|
||||
bool waybar::modules::Pulseaudio::handleScroll(GdkEventScroll* e) {
|
||||
@@ -33,7 +38,7 @@ bool waybar::modules::Pulseaudio::handleScroll(GdkEventScroll* e) {
|
||||
? util::ChangeType::Increase
|
||||
: util::ChangeType::Decrease;
|
||||
|
||||
backend->changeVolume(change_type, step, max_volume);
|
||||
backend->changeVolume(change_type, step, max_volume, target);
|
||||
return true;
|
||||
}
|
||||
|
||||
@@ -68,7 +73,6 @@ const std::vector<std::string> waybar::modules::Pulseaudio::getPulseIcon() const
|
||||
|
||||
auto waybar::modules::Pulseaudio::update() -> void {
|
||||
auto format = format_;
|
||||
std::string tooltip_format;
|
||||
auto sink_volume = backend->getSinkVolume();
|
||||
if (!alt_) {
|
||||
std::string format_name = "format";
|
||||
@@ -116,29 +120,29 @@ auto waybar::modules::Pulseaudio::update() -> void {
|
||||
auto source_desc = backend->getSourceDesc();
|
||||
|
||||
format_source = fmt::format(fmt::runtime(format_source), fmt::arg("volume", source_volume));
|
||||
auto text = fmt::format(
|
||||
fmt::runtime(format), fmt::arg("desc", sink_desc), fmt::arg("volume", sink_volume),
|
||||
fmt::arg("format_source", format_source), fmt::arg("source_volume", source_volume),
|
||||
fmt::arg("source_desc", source_desc), fmt::arg("icon", getIcon(sink_volume, getPulseIcon())));
|
||||
|
||||
fmt::dynamic_format_arg_store<fmt::format_context> store;
|
||||
store.push_back(fmt::arg("desc", sink_desc));
|
||||
store.push_back(fmt::arg("volume", sink_volume));
|
||||
store.push_back(fmt::arg("format_source", format_source));
|
||||
store.push_back(fmt::arg("source_volume", source_volume));
|
||||
store.push_back(fmt::arg("source_desc", source_desc));
|
||||
store.push_back(fmt::arg("icon", getIcon(sink_volume, getPulseIcon())));
|
||||
|
||||
auto text = fmt::vformat(format, store);
|
||||
if (text.empty()) {
|
||||
label_.hide();
|
||||
} else {
|
||||
label_.set_markup(text);
|
||||
setLabelMarkup(text);
|
||||
label_.show();
|
||||
}
|
||||
|
||||
if (tooltipEnabled()) {
|
||||
if (tooltip_format.empty() && config_["tooltip-format"].isString()) {
|
||||
tooltip_format = config_["tooltip-format"].asString();
|
||||
}
|
||||
auto tooltip_format = resolveTooltipFormat("");
|
||||
if (!tooltip_format.empty()) {
|
||||
label_.set_tooltip_markup(fmt::format(
|
||||
fmt::runtime(tooltip_format), fmt::arg("desc", sink_desc),
|
||||
fmt::arg("volume", sink_volume), fmt::arg("format_source", format_source),
|
||||
fmt::arg("source_volume", source_volume), fmt::arg("source_desc", source_desc),
|
||||
fmt::arg("icon", getIcon(sink_volume, getPulseIcon()))));
|
||||
setTooltipMarkup(fmt::vformat(tooltip_format, store));
|
||||
} else {
|
||||
label_.set_tooltip_markup(sink_desc);
|
||||
setTooltipMarkup(sink_desc);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -6,77 +6,56 @@ PulseaudioSlider::PulseaudioSlider(const std::string& id, const Json::Value& con
|
||||
: ASlider(config, "pulseaudio-slider", id) {
|
||||
backend = util::AudioBackend::getInstance([this] { this->dp.emit(); });
|
||||
backend->setIgnoredSinks(config_["ignored-sinks"]);
|
||||
backend->setSinkMapping(config_["sink-mapping"]);
|
||||
|
||||
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_, target);
|
||||
}
|
||||
|
||||
backend->changeVolume(volume, min_, max_);
|
||||
}
|
||||
|
||||
} // namespace waybar::modules
|
||||
} // namespace waybar::modules
|
||||
|
||||
@@ -33,6 +33,33 @@ static const zriver_output_status_v1_listener output_status_listener_impl{
|
||||
.urgent_tags = listen_urgent_tags,
|
||||
};
|
||||
|
||||
static void listen_focused_output(void* data, struct zriver_seat_status_v1* zriver_seat_status_v1,
|
||||
struct wl_output* output) {
|
||||
static_cast<Tags*>(data)->handle_focused_output(output);
|
||||
}
|
||||
|
||||
static void listen_unfocused_output(void* data, struct zriver_seat_status_v1* zriver_seat_status_v1,
|
||||
struct wl_output* output) {
|
||||
static_cast<Tags*>(data)->handle_unfocused_output(output);
|
||||
}
|
||||
|
||||
static void listen_focused_view(void* data, struct zriver_seat_status_v1* zriver_seat_status_v1,
|
||||
const char* title) {
|
||||
// This module doesn't care
|
||||
}
|
||||
|
||||
static void listen_mode(void* data, struct zriver_seat_status_v1* zriver_seat_status_v1,
|
||||
const char* mode) {
|
||||
// This module doesn't care
|
||||
}
|
||||
|
||||
static const zriver_seat_status_v1_listener seat_status_listener_impl{
|
||||
.focused_output = listen_focused_output,
|
||||
.unfocused_output = listen_unfocused_output,
|
||||
.focused_view = listen_focused_view,
|
||||
.mode = listen_mode,
|
||||
};
|
||||
|
||||
static void listen_command_success(void* data,
|
||||
struct zriver_command_callback_v1* zriver_command_callback_v1,
|
||||
const char* output) {
|
||||
@@ -87,8 +114,10 @@ Tags::Tags(const std::string& id, const waybar::Bar& bar, const Json::Value& con
|
||||
control_{nullptr},
|
||||
seat_{nullptr},
|
||||
bar_(bar),
|
||||
output_{nullptr},
|
||||
box_{bar.orientation, 0},
|
||||
output_status_{nullptr} {
|
||||
output_status_{nullptr},
|
||||
seat_status_{nullptr} {
|
||||
struct wl_display* display = Client::inst()->wl_display;
|
||||
struct wl_registry* registry = wl_display_get_registry(display);
|
||||
wl_registry_add_listener(registry, ®istry_listener_impl, this);
|
||||
@@ -109,6 +138,11 @@ Tags::Tags(const std::string& id, const waybar::Bar& bar, const Json::Value& con
|
||||
return;
|
||||
}
|
||||
|
||||
// Store the output this module belongs to; the river_output_status and
|
||||
// river_seat_status objects (and their listeners) are created lazily in
|
||||
// handle_show() to avoid leaking objects without listeners here.
|
||||
output_ = gdk_wayland_monitor_get_wl_output(bar_.output->monitor->gobj());
|
||||
|
||||
box_.set_name("tags");
|
||||
if (!id.empty()) {
|
||||
box_.get_style_context()->add_class(id);
|
||||
@@ -149,6 +183,7 @@ Tags::Tags(const std::string& id, const waybar::Bar& bar, const Json::Value& con
|
||||
button.signal_button_press_event().connect(
|
||||
sigc::bind(sigc::mem_fun(*this, &Tags::handle_button_press), (1 << tag)));
|
||||
}
|
||||
button.get_style_context()->add_class("tag-" + std::to_string(tag + 1));
|
||||
button.show();
|
||||
}
|
||||
|
||||
@@ -160,6 +195,10 @@ Tags::~Tags() {
|
||||
zriver_output_status_v1_destroy(output_status_);
|
||||
}
|
||||
|
||||
if (seat_status_) {
|
||||
zriver_seat_status_v1_destroy(seat_status_);
|
||||
}
|
||||
|
||||
if (control_) {
|
||||
zriver_control_v1_destroy(control_);
|
||||
}
|
||||
@@ -171,10 +210,12 @@ Tags::~Tags() {
|
||||
|
||||
void Tags::handle_show() {
|
||||
if (!status_manager_) return;
|
||||
struct wl_output* output = gdk_wayland_monitor_get_wl_output(bar_.output->monitor->gobj());
|
||||
output_status_ = zriver_status_manager_v1_get_river_output_status(status_manager_, output);
|
||||
output_status_ = zriver_status_manager_v1_get_river_output_status(status_manager_, output_);
|
||||
zriver_output_status_v1_add_listener(output_status_, &output_status_listener_impl, this);
|
||||
|
||||
seat_status_ = zriver_status_manager_v1_get_river_seat_status(status_manager_, seat_);
|
||||
zriver_seat_status_v1_add_listener(seat_status_, &seat_status_listener_impl, this);
|
||||
|
||||
zriver_status_manager_v1_destroy(status_manager_);
|
||||
status_manager_ = nullptr;
|
||||
}
|
||||
@@ -266,4 +307,20 @@ void Tags::handle_urgent_tags(uint32_t tags) {
|
||||
}
|
||||
}
|
||||
|
||||
void Tags::handle_focused_output(struct wl_output* output) {
|
||||
if (output_ == output) {
|
||||
for (size_t i = 0; i < buttons_.size(); ++i) {
|
||||
buttons_[i].get_style_context()->add_class("output");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void Tags::handle_unfocused_output(struct wl_output* output) {
|
||||
if (output_ == output) {
|
||||
for (size_t i = 0; i < buttons_.size(); ++i) {
|
||||
buttons_[i].get_style_context()->remove_class("output");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
} /* namespace waybar::modules::river */
|
||||
|
||||
@@ -18,18 +18,7 @@ auto waybar::modules::Clock::update() -> void {
|
||||
tzset(); // Update timezone information
|
||||
auto now = std::chrono::system_clock::now();
|
||||
auto localtime = fmt::localtime(std::chrono::system_clock::to_time_t(now));
|
||||
auto text = fmt::format(fmt::runtime(format_), localtime);
|
||||
label_.set_markup(text);
|
||||
|
||||
if (tooltipEnabled()) {
|
||||
if (config_["tooltip-format"].isString()) {
|
||||
auto tooltip_format = config_["tooltip-format"].asString();
|
||||
auto tooltip_text = fmt::format(fmt::runtime(tooltip_format), localtime);
|
||||
label_.set_tooltip_markup(tooltip_text);
|
||||
} else {
|
||||
label_.set_tooltip_markup(text);
|
||||
}
|
||||
}
|
||||
updateLabelAndTooltip(format_, format_, localtime);
|
||||
// Call parent update
|
||||
ALabel::update();
|
||||
}
|
||||
|
||||
@@ -6,7 +6,11 @@
|
||||
|
||||
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::vector<std::string>& ignore_list,
|
||||
const std::function<void(std::unique_ptr<Item>&)>& on_add,
|
||||
const std::function<void(std::unique_ptr<Item>&)>& on_remove,
|
||||
const std::function<void()>& on_update)
|
||||
@@ -17,11 +21,13 @@ Host::Host(const std::size_t id, const Json::Value& config, const Bar& bar,
|
||||
sigc::mem_fun(*this, &Host::busAcquired))),
|
||||
config_(config),
|
||||
bar_(bar),
|
||||
ignore_list_(ignore_list),
|
||||
on_add_(on_add),
|
||||
on_remove_(on_remove),
|
||||
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;
|
||||
@@ -35,6 +41,42 @@ Host::~Host() {
|
||||
g_clear_object(&watcher_);
|
||||
}
|
||||
|
||||
void Host::checkIgnoreList(const std::vector<std::string>& ignore_list,
|
||||
const std::function<void(std::unique_ptr<Item>&)>& on_remove) {
|
||||
spdlog::debug("Host::checkIgnoreList - checking {} items against {} patterns", items_.size(),
|
||||
ignore_list.size());
|
||||
|
||||
for (auto it = items_.begin(); it != items_.end();) {
|
||||
auto& item = *it;
|
||||
spdlog::debug(" Checking item: bus_name='{}', category='{}', icon_name='{}', title='{}'",
|
||||
item->bus_name, item->category, item->icon_name, item->title);
|
||||
|
||||
bool should_remove = false;
|
||||
|
||||
for (const auto& ignored : ignore_list) {
|
||||
if (item->bus_name.find(ignored) != std::string::npos ||
|
||||
item->category.find(ignored) != std::string::npos ||
|
||||
item->icon_name.find(ignored) != std::string::npos ||
|
||||
item->id.find(ignored) != std::string::npos ||
|
||||
item->title.find(ignored) != std::string::npos) {
|
||||
spdlog::info(
|
||||
"Host: Ignoring item bus_name='{}', category='{}', icon_name='{}', title='{}' - "
|
||||
"matched pattern '{}'",
|
||||
item->bus_name, item->category, item->icon_name, item->title, ignored);
|
||||
on_remove(item);
|
||||
should_remove = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (should_remove) {
|
||||
it = items_.erase(it);
|
||||
} else {
|
||||
++it;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void Host::busAcquired(const Glib::RefPtr<Gio::DBus::Connection>& conn, Glib::ustring name) {
|
||||
watcher_id_ = Gio::DBus::watch_name(conn, "org.kde.StatusNotifierWatcher",
|
||||
sigc::mem_fun(*this, &Host::nameAppeared),
|
||||
@@ -53,6 +95,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 +116,31 @@ 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);
|
||||
}
|
||||
@@ -102,7 +166,9 @@ void Host::registerHost(GObject* src, GAsyncResult* res, gpointer data) {
|
||||
g_signal_connect(host->watcher_, "item-unregistered", G_CALLBACK(&Host::itemUnregistered), data);
|
||||
auto items = sn_watcher_dup_registered_items(host->watcher_);
|
||||
if (items != nullptr) {
|
||||
spdlog::info("Host: Found {} pre-registered SNI items", g_strv_length(items));
|
||||
for (uint32_t i = 0; items[i] != nullptr; i += 1) {
|
||||
spdlog::info("Host: Processing pre-registered item: {}", items[i]);
|
||||
host->addRegisteredItem(items[i]);
|
||||
}
|
||||
}
|
||||
@@ -111,7 +177,10 @@ void Host::registerHost(GObject* src, GAsyncResult* res, gpointer data) {
|
||||
|
||||
void Host::itemRegistered(SnWatcher* watcher, const gchar* service, gpointer data) {
|
||||
auto host = static_cast<SNI::Host*>(data);
|
||||
spdlog::info("Host::itemRegistered called with service: {}", service);
|
||||
host->addRegisteredItem(service);
|
||||
// host->checkIgnoreList(host->ignore_list_, std::bind(&Host::itemUnregistered, host,
|
||||
// std::placeholders::_1, std::placeholders::_2, data));
|
||||
}
|
||||
|
||||
void Host::itemUnregistered(SnWatcher* watcher, const gchar* service, gpointer data) {
|
||||
@@ -172,13 +241,23 @@ std::tuple<std::string, std::string> Host::getBusNameAndObjectPath(const std::st
|
||||
}
|
||||
|
||||
void Host::addRegisteredItem(const std::string& service) {
|
||||
// Check service string directly before parsing
|
||||
for (const auto& ignored : ignore_list_) {
|
||||
if (service.find(ignored) != std::string::npos) {
|
||||
spdlog::info("Host: Ignoring service '{}' - matched pattern '{}'", service, ignored);
|
||||
return;
|
||||
}
|
||||
}
|
||||
std::string bus_name, object_path;
|
||||
std::tie(bus_name, object_path) = getBusNameAndObjectPath(service);
|
||||
spdlog::debug("SNI item registered: bus_name={}, object_path={}, full_service={}", bus_name,
|
||||
object_path, service);
|
||||
auto it = std::find_if(items_.begin(), items_.end(), [&bus_name, &object_path](const auto& item) {
|
||||
return bus_name == item->bus_name && object_path == item->object_path;
|
||||
});
|
||||
if (it == items_.end()) {
|
||||
items_.emplace_back(new Item(
|
||||
spdlog::debug("Adding SNI item: {}", bus_name);
|
||||
items_.emplace_back(std::make_unique<Item>(
|
||||
bus_name, object_path, config_, bar_, [this](Item& item) { itemReady(item); },
|
||||
[this](Item& item) { itemInvalidated(item); }, on_update_));
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
+40
-13
@@ -8,11 +8,29 @@
|
||||
|
||||
namespace waybar::modules::SNI {
|
||||
|
||||
std::vector<std::string> Tray::parseIgnoreList(const Json::Value& config) {
|
||||
std::vector<std::string> ignore_list;
|
||||
if (config["ignore-list"].isArray()) {
|
||||
spdlog::info("Tray: Found ignore-list with {} items", config["ignore-list"].size());
|
||||
for (const auto& item : config["ignore-list"]) {
|
||||
if (item.isString()) {
|
||||
ignore_list.push_back(item.asString());
|
||||
spdlog::info("Tray: Adding to ignore list: {}", item.asString());
|
||||
}
|
||||
}
|
||||
} else {
|
||||
spdlog::info("Tray: No ignore-list configured");
|
||||
}
|
||||
return ignore_list;
|
||||
}
|
||||
|
||||
Tray::Tray(const std::string& id, const Bar& bar, const Json::Value& config)
|
||||
: AModule(config, "tray", id),
|
||||
box_(bar.orientation, 0),
|
||||
watcher_(SNI::Watcher::getInstance()),
|
||||
host_(nb_hosts_, config, bar, std::bind(&Tray::onAdd, this, std::placeholders::_1),
|
||||
ignore_list_(parseIgnoreList(config)),
|
||||
host_(nb_hosts_, config, bar, ignore_list_,
|
||||
std::bind(&Tray::onAdd, this, std::placeholders::_1),
|
||||
std::bind(&Tray::onRemove, this, std::placeholders::_1),
|
||||
std::bind(&Tray::queueUpdate, this)) {
|
||||
box_.set_name("tray");
|
||||
@@ -24,9 +42,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"]);
|
||||
@@ -34,14 +49,28 @@ Tray::Tray(const std::string& id, const Bar& bar, const Json::Value& config)
|
||||
dp.emit();
|
||||
}
|
||||
|
||||
void Tray::checkIgnoreList(std::unique_ptr<Item>* item_ptr) {
|
||||
// Delegate to Host's checkIgnoreList method
|
||||
host_.checkIgnoreList(ignore_list_, std::bind(&Tray::onRemove, this, std::placeholders::_1));
|
||||
}
|
||||
|
||||
void Tray::queueUpdate() { dp.emit(); }
|
||||
|
||||
void Tray::onAdd(std::unique_ptr<Item>& item) {
|
||||
spdlog::info("Tray::onAdd - item bus_name='{}', category='{}', icon_name='{}', title='{}'",
|
||||
item->bus_name, item->category, item->icon_name, item->title);
|
||||
|
||||
if (config_["reverse-direction"].isBool() && config_["reverse-direction"].asBool()) {
|
||||
box_.pack_end(item->event_box);
|
||||
} else {
|
||||
box_.pack_start(item->event_box);
|
||||
}
|
||||
|
||||
spdlog::debug("Tray::onAdd deferred check - checking ignore list");
|
||||
host_.checkIgnoreList(ignore_list_, std::bind(&Tray::onRemove, this, std::placeholders::_1));
|
||||
|
||||
item->event_box.signal_show().connect([this] { dp.emit(); });
|
||||
item->event_box.signal_hide().connect([this] { dp.emit(); });
|
||||
dp.emit();
|
||||
}
|
||||
|
||||
@@ -51,17 +80,15 @@ 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");
|
||||
}));
|
||||
// Check if any items should be ignored now that properties have loaded
|
||||
if (!ignore_list_.empty()) {
|
||||
spdlog::debug("Tray::update() - checking ignore list");
|
||||
host_.checkIgnoreList(ignore_list_, std::bind(&Tray::onRemove, this, std::placeholders::_1));
|
||||
}
|
||||
|
||||
// Call parent update
|
||||
std::vector<Gtk::Widget*> children = box_.get_children();
|
||||
event_box_.set_visible(std::any_of(children.begin(), children.end(),
|
||||
[](Gtk::Widget* child) { return child->get_visible(); }));
|
||||
AModule::update();
|
||||
}
|
||||
|
||||
|
||||
@@ -2,13 +2,46 @@
|
||||
|
||||
#include <fcntl.h>
|
||||
#include <spdlog/spdlog.h>
|
||||
#include <sys/socket.h>
|
||||
#include <sys/types.h>
|
||||
#include <sys/un.h>
|
||||
#include <unistd.h>
|
||||
|
||||
#include <cerrno>
|
||||
#include <cstdio>
|
||||
#include <cstdlib>
|
||||
#include <cstring>
|
||||
#include <limits>
|
||||
#include <stdexcept>
|
||||
#include <string_view>
|
||||
#include <utility>
|
||||
|
||||
#include "modules/sway/ipc/ipc.hpp"
|
||||
|
||||
namespace waybar::modules::sway {
|
||||
namespace {
|
||||
|
||||
void sendAll(int fd, const char* data, size_t size, const char* what) {
|
||||
size_t total = 0;
|
||||
while (total < size) {
|
||||
const auto res = ::send(fd, data + total, size - total, 0);
|
||||
if (res < 0) {
|
||||
if (errno == EINTR || errno == EAGAIN) {
|
||||
continue;
|
||||
}
|
||||
throw std::runtime_error(what);
|
||||
}
|
||||
if (res == 0) {
|
||||
throw std::runtime_error(what);
|
||||
}
|
||||
total += static_cast<size_t>(res);
|
||||
}
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
Ipc::Ipc() {
|
||||
const std::string& socketPath = getSocketPath();
|
||||
const std::string socketPath = getSocketPath();
|
||||
fd_ = util::ScopedFd(open(socketPath));
|
||||
fd_event_ = util::ScopedFd(open(socketPath));
|
||||
}
|
||||
@@ -29,49 +62,49 @@ Ipc::~Ipc() {
|
||||
}
|
||||
}
|
||||
|
||||
void Ipc::setWorker(std::function<void()>&& func) { thread_ = func; }
|
||||
void Ipc::setWorker(std::function<void()>&& func) { thread_ = std::move(func); }
|
||||
|
||||
const std::string Ipc::getSocketPath() const {
|
||||
std::string Ipc::getSocketPath() {
|
||||
const char* env = getenv("SWAYSOCK");
|
||||
if (env != nullptr) {
|
||||
return std::string(env);
|
||||
if (env != nullptr && env[0] != '\0') {
|
||||
return {env};
|
||||
}
|
||||
|
||||
FILE* in = popen("sway --get-socketpath 2>/dev/null", "r");
|
||||
if (in == nullptr) {
|
||||
throw std::runtime_error("Failed to get socket path");
|
||||
}
|
||||
|
||||
std::string str;
|
||||
{
|
||||
std::string str_buf;
|
||||
FILE* in;
|
||||
char buf[512] = {0};
|
||||
if ((in = popen("sway --get-socketpath 2>/dev/null", "r")) == nullptr) {
|
||||
throw std::runtime_error("Failed to get socket path");
|
||||
}
|
||||
while (fgets(buf, sizeof(buf), in) != nullptr) {
|
||||
str_buf.append(buf, sizeof(buf));
|
||||
}
|
||||
pclose(in);
|
||||
str = str_buf;
|
||||
if (str.empty()) {
|
||||
throw std::runtime_error("Socket path is empty");
|
||||
}
|
||||
char buf[512] = {0};
|
||||
while (fgets(buf, sizeof(buf), in) != nullptr) {
|
||||
str.append(buf);
|
||||
}
|
||||
if (str.back() == '\n') {
|
||||
|
||||
if (pclose(in) == -1) {
|
||||
throw std::runtime_error("Failed to get socket path");
|
||||
}
|
||||
|
||||
if (str.ends_with('\n')) {
|
||||
str.pop_back();
|
||||
}
|
||||
if (str.empty()) {
|
||||
throw std::runtime_error("Socket path is empty");
|
||||
}
|
||||
|
||||
return str;
|
||||
}
|
||||
|
||||
int Ipc::open(const std::string& socketPath) const {
|
||||
int Ipc::open(const std::string& socketPath) {
|
||||
util::ScopedFd fd(socket(AF_UNIX, SOCK_STREAM, 0));
|
||||
if (fd == -1) {
|
||||
throw std::runtime_error("Unable to open Unix socket");
|
||||
}
|
||||
(void)fcntl(fd, F_SETFD, FD_CLOEXEC);
|
||||
struct sockaddr_un addr;
|
||||
memset(&addr, 0, sizeof(struct sockaddr_un));
|
||||
addr.sun_family = AF_UNIX;
|
||||
struct sockaddr_un addr{.sun_family = AF_UNIX};
|
||||
strncpy(addr.sun_path, socketPath.c_str(), sizeof(addr.sun_path) - 1);
|
||||
addr.sun_path[sizeof(addr.sun_path) - 1] = 0;
|
||||
int l = sizeof(struct sockaddr_un);
|
||||
if (::connect(fd, reinterpret_cast<struct sockaddr*>(&addr), l) == -1) {
|
||||
if (::connect(fd, reinterpret_cast<struct sockaddr*>(&addr), sizeof addr) == -1) {
|
||||
throw std::runtime_error("Unable to connect to Sway");
|
||||
}
|
||||
return fd.release();
|
||||
@@ -80,55 +113,70 @@ int Ipc::open(const std::string& socketPath) const {
|
||||
struct Ipc::ipc_response Ipc::recv(int fd) {
|
||||
std::string header;
|
||||
header.resize(ipc_header_size_);
|
||||
auto data32 = reinterpret_cast<uint32_t*>(header.data() + ipc_magic_.size());
|
||||
size_t total = 0;
|
||||
|
||||
size_t total = 0;
|
||||
while (total < ipc_header_size_) {
|
||||
auto res = ::recv(fd, header.data() + total, ipc_header_size_ - total, 0);
|
||||
const ssize_t res = ::recv(fd, header.data() + total, ipc_header_size_ - total, 0);
|
||||
if (fd_event_ == -1 || fd_ == -1) {
|
||||
// IPC is closed so just return an empty response
|
||||
return {0, 0, ""};
|
||||
return {.size = 0, .type = 0, .payload = ""};
|
||||
}
|
||||
if (res <= 0) {
|
||||
if (res < 0) {
|
||||
if (errno == EINTR || errno == EAGAIN) {
|
||||
continue;
|
||||
}
|
||||
throw std::runtime_error("Unable to receive IPC header");
|
||||
}
|
||||
total += res;
|
||||
if (res == 0) {
|
||||
throw std::runtime_error("Unable to receive IPC header");
|
||||
}
|
||||
total += static_cast<size_t>(res);
|
||||
}
|
||||
auto magic = std::string(header.data(), header.data() + ipc_magic_.size());
|
||||
if (magic != ipc_magic_) {
|
||||
if (std::string_view(header.data(), ipc_magic_.size()) != ipc_magic_) {
|
||||
throw std::runtime_error("Invalid IPC magic");
|
||||
}
|
||||
|
||||
total = 0;
|
||||
uint32_t payload_size = 0;
|
||||
uint32_t payload_type = 0;
|
||||
memcpy(&payload_size, header.data() + ipc_magic_.size(), sizeof payload_size);
|
||||
memcpy(&payload_type, header.data() + ipc_magic_.size() + sizeof payload_size,
|
||||
sizeof payload_type);
|
||||
|
||||
std::string payload;
|
||||
payload.resize(data32[0]);
|
||||
while (total < data32[0]) {
|
||||
auto res = ::recv(fd, payload.data() + total, data32[0] - total, 0);
|
||||
payload.resize(payload_size);
|
||||
|
||||
total = 0;
|
||||
while (total < payload_size) {
|
||||
const ssize_t res = ::recv(fd, payload.data() + total, payload_size - total, 0);
|
||||
if (res < 0) {
|
||||
if (errno == EINTR || errno == EAGAIN) {
|
||||
continue;
|
||||
}
|
||||
throw std::runtime_error("Unable to receive IPC payload");
|
||||
}
|
||||
total += res;
|
||||
if (res == 0) {
|
||||
throw std::runtime_error("Unable to receive IPC payload");
|
||||
}
|
||||
total += static_cast<size_t>(res);
|
||||
}
|
||||
return {data32[0], data32[1], &payload.front()};
|
||||
|
||||
return {.size = payload_size, .type = payload_type, .payload = std::move(payload)};
|
||||
}
|
||||
|
||||
struct Ipc::ipc_response Ipc::send(int fd, uint32_t type, const std::string& payload) {
|
||||
std::string header;
|
||||
header.resize(ipc_header_size_);
|
||||
auto data32 = reinterpret_cast<uint32_t*>(header.data() + ipc_magic_.size());
|
||||
memcpy(header.data(), ipc_magic_.c_str(), ipc_magic_.size());
|
||||
data32[0] = payload.size();
|
||||
data32[1] = type;
|
||||
memcpy(header.data(), ipc_magic_.data(), ipc_magic_.size());
|
||||
if (payload.size() > std::numeric_limits<uint32_t>::max()) {
|
||||
throw std::runtime_error("IPC payload is too large");
|
||||
}
|
||||
const auto payload_size = static_cast<uint32_t>(payload.size());
|
||||
memcpy(header.data() + ipc_magic_.size(), &payload_size, sizeof payload_size);
|
||||
memcpy(header.data() + ipc_magic_.size() + sizeof payload_size, &type, sizeof type);
|
||||
|
||||
sendAll(fd, header.data(), ipc_header_size_, "Unable to send IPC header");
|
||||
sendAll(fd, payload.data(), payload.size(), "Unable to send IPC payload");
|
||||
|
||||
if (::send(fd, header.data(), ipc_header_size_, 0) == -1) {
|
||||
throw std::runtime_error("Unable to send IPC header");
|
||||
}
|
||||
if (::send(fd, payload.c_str(), payload.size(), 0) == -1) {
|
||||
throw std::runtime_error("Unable to send IPC payload");
|
||||
}
|
||||
return Ipc::recv(fd);
|
||||
}
|
||||
|
||||
|
||||
@@ -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());
|
||||
}
|
||||
@@ -104,7 +124,7 @@ auto Language::update() -> void {
|
||||
fmt::runtime(format_), fmt::arg("short", layout_.short_name),
|
||||
fmt::arg("shortDescription", layout_.short_description), fmt::arg("long", layout_.full_name),
|
||||
fmt::arg("variant", layout_.variant), fmt::arg("flag", layout_.country_flag())));
|
||||
label_.set_markup(display_layout);
|
||||
setLabelMarkup(display_layout);
|
||||
if (tooltipEnabled()) {
|
||||
if (tooltip_format_ != "") {
|
||||
auto tooltip_display_layout = trim(
|
||||
@@ -112,9 +132,9 @@ auto Language::update() -> void {
|
||||
fmt::arg("shortDescription", layout_.short_description),
|
||||
fmt::arg("long", layout_.full_name), fmt::arg("variant", layout_.variant),
|
||||
fmt::arg("flag", layout_.country_flag())));
|
||||
label_.set_tooltip_markup(tooltip_display_layout);
|
||||
setTooltipMarkup(tooltip_display_layout);
|
||||
} else {
|
||||
label_.set_tooltip_markup(display_layout);
|
||||
setTooltipMarkup(display_layout);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
|
||||
@@ -31,12 +31,12 @@ Scratchpad::Scratchpad(const std::string& id, const Json::Value& config)
|
||||
auto Scratchpad::update() -> void {
|
||||
if (count_ || show_empty_) {
|
||||
event_box_.show();
|
||||
label_.set_markup(
|
||||
setLabelMarkup(
|
||||
fmt::format(fmt::runtime(format_),
|
||||
fmt::arg("icon", getIcon(count_, "", config_["format-icons"].size())),
|
||||
fmt::arg("count", count_)));
|
||||
if (tooltip_enabled_) {
|
||||
label_.set_tooltip_markup(tooltip_text_);
|
||||
setTooltipMarkup(tooltip_text_);
|
||||
}
|
||||
} else {
|
||||
event_box_.hide();
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -69,6 +69,7 @@ Workspaces::Workspaces(const std::string& id, const Bar& bar, const Json::Value&
|
||||
m_windowRewriteRules = waybar::util::RegexCollection(
|
||||
windowRewrite, std::move(windowRewriteDefault), windowRewritePriorityFunction);
|
||||
}
|
||||
populateIgnoreWorkspacesConfig(config);
|
||||
ipc_.subscribe(R"(["workspace"])");
|
||||
ipc_.subscribe(R"(["window"])");
|
||||
ipc_.signal_event.connect(sigc::mem_fun(*this, &Workspaces::onEvent));
|
||||
@@ -97,6 +98,36 @@ void Workspaces::onEvent(const struct Ipc::ipc_response& res) {
|
||||
}
|
||||
}
|
||||
|
||||
auto Workspaces::populateIgnoreWorkspacesConfig(const Json::Value& config) -> void {
|
||||
auto ignoreWorkspaces = config["ignore-workspaces"];
|
||||
if (ignoreWorkspaces.isArray()) {
|
||||
for (const auto& workspaceRegex : ignoreWorkspaces) {
|
||||
if (workspaceRegex.isString()) {
|
||||
std::string ruleString = workspaceRegex.asString();
|
||||
try {
|
||||
const std::regex rule{ruleString, std::regex_constants::icase};
|
||||
m_ignoreWorkspaces.emplace_back(rule);
|
||||
} catch (const std::regex_error& e) {
|
||||
spdlog::error("Invalid rule {}: {}", ruleString, e.what());
|
||||
}
|
||||
} else {
|
||||
spdlog::error("Not a string: '{}'", workspaceRegex);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
bool Workspaces::isWorkspaceIgnored(std::string const& name) {
|
||||
for (auto& rule : m_ignoreWorkspaces) {
|
||||
if (std::regex_match(name, rule)) {
|
||||
return true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
void Workspaces::onCmd(const struct Ipc::ipc_response& res) {
|
||||
if (res.type == IPC_GET_TREE) {
|
||||
try {
|
||||
@@ -118,8 +149,9 @@ void Workspaces::onCmd(const struct Ipc::ipc_response& res) {
|
||||
});
|
||||
|
||||
for (auto& output : outputs) {
|
||||
std::copy(output["nodes"].begin(), output["nodes"].end(),
|
||||
std::back_inserter(workspaces_));
|
||||
std::copy_if(
|
||||
output["nodes"].begin(), output["nodes"].end(), std::back_inserter(workspaces_),
|
||||
[&](const auto& node) { return !(isWorkspaceIgnored(node["name"].asString())); });
|
||||
std::copy(output["floating_nodes"].begin(), output["floating_nodes"].end(),
|
||||
std::back_inserter(workspaces_));
|
||||
}
|
||||
@@ -143,7 +175,8 @@ void Workspaces::onCmd(const struct Ipc::ipc_response& res) {
|
||||
if (p_w.isArray() && !p_w.empty()) {
|
||||
// Adding to target outputs
|
||||
for (const Json::Value& output : p_w) {
|
||||
if (output.asString() == bar_.output->name) {
|
||||
auto output_name = output.asString();
|
||||
if (output_name == bar_.output->name || output_name == bar_.output->identifier) {
|
||||
Json::Value v;
|
||||
v["name"] = p_w_name;
|
||||
v["target_output"] = bar_.output->name;
|
||||
@@ -229,7 +262,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 {
|
||||
@@ -321,6 +357,18 @@ auto Workspaces::update() -> void {
|
||||
button.get_style_context()->remove_class("empty");
|
||||
}
|
||||
if ((*it)["output"].isString()) {
|
||||
// Simply attempt to remove all output classes every time to reset output classes. This works
|
||||
// even if a class has not been previously added to the style context.
|
||||
for (const auto &oclass : config_["output-classes"]) {
|
||||
button.get_style_context()->remove_class(oclass.asString());
|
||||
}
|
||||
// If output-classes contains a class for output associated with current workspace button, add
|
||||
// the class to its style context.
|
||||
std::string output_name = (*it)["output"].asString();
|
||||
if (config_["output-classes"].isMember(output_name) &&
|
||||
config_["output-classes"][output_name].isString()) {
|
||||
button.get_style_context()->add_class(config_["output-classes"][output_name].asString());
|
||||
}
|
||||
if (((*it)["output"].asString()) == bar_.output->name) {
|
||||
button.get_style_context()->add_class("current_output");
|
||||
} else {
|
||||
@@ -329,24 +377,44 @@ auto Workspaces::update() -> void {
|
||||
} else {
|
||||
button.get_style_context()->remove_class("current_output");
|
||||
}
|
||||
std::string output = (*it)["name"].asString();
|
||||
std::string full_name;
|
||||
if (!config_["disable-markup"].asBool()) {
|
||||
full_name = g_markup_escape_text((*it)["name"].asString().c_str(), -1);
|
||||
} else {
|
||||
full_name = (*it)["name"].asString();
|
||||
}
|
||||
|
||||
std::string windows = "";
|
||||
if (config_["window-rewrite"].isObject()) {
|
||||
updateWindows((*it), windows);
|
||||
}
|
||||
|
||||
auto index = (*it)["num"].asInt();
|
||||
|
||||
if (config_["format"].isString()) {
|
||||
auto format = config_["format"].asString();
|
||||
output = fmt::format(
|
||||
fmt::runtime(format), fmt::arg("icon", getIcon(output, *it)), fmt::arg("value", output),
|
||||
fmt::arg("name", trimWorkspaceName(output)), fmt::arg("index", (*it)["num"].asString()),
|
||||
fmt::arg("windows",
|
||||
windows.substr(0, windows.length() - m_formatWindowSeparator.length())),
|
||||
fmt::arg("output", (*it)["output"].asString()));
|
||||
std::string format;
|
||||
if (config_["format-for-negative-index"].isString() && index < 0) {
|
||||
format = config_["format-for-negative-index"].asString();
|
||||
} else {
|
||||
format = config_["format"].asString();
|
||||
}
|
||||
|
||||
auto name = trimWorkspaceName(full_name);
|
||||
auto output = (*it)["output"].asString();
|
||||
auto icon = getIcon(full_name, *it);
|
||||
auto separated_windows =
|
||||
windows.substr(0, windows.length() - m_formatWindowSeparator.length());
|
||||
|
||||
full_name =
|
||||
fmt::format(fmt::runtime(format), fmt::arg("index", index), fmt::arg("name", name),
|
||||
fmt::arg("value", full_name), fmt::arg("output", output),
|
||||
fmt::arg("icon", icon), fmt::arg("windows", separated_windows));
|
||||
}
|
||||
|
||||
if (!config_["disable-markup"].asBool()) {
|
||||
static_cast<Gtk::Label*>(button.get_children()[0])->set_markup(output);
|
||||
static_cast<Gtk::Label*>(button.get_children()[0])->set_markup(full_name);
|
||||
} else {
|
||||
button.set_label(output);
|
||||
button.set_label(full_name);
|
||||
}
|
||||
onButtonReady(*it, button);
|
||||
}
|
||||
@@ -369,11 +437,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());
|
||||
|
||||
@@ -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";
|
||||
@@ -276,7 +281,7 @@ auto SystemdFailedUnits::update() -> void {
|
||||
|
||||
last_status_ = overall_state_;
|
||||
|
||||
label_.set_markup(fmt::format(
|
||||
setLabelMarkup(fmt::format(
|
||||
fmt::runtime(nr_failed_ == 0 ? format_ok_ : format_), fmt::arg("nr_failed", nr_failed_),
|
||||
fmt::arg("nr_failed_system", nr_failed_system_), fmt::arg("nr_failed_user", nr_failed_user_),
|
||||
fmt::arg("system_state", system_state_), fmt::arg("user_state", user_state_),
|
||||
@@ -285,14 +290,14 @@ auto SystemdFailedUnits::update() -> void {
|
||||
std::string failed_list = BuildTooltipFailedList();
|
||||
auto tooltip_template = overall_state_ == "ok" ? tooltip_format_ok_ : tooltip_format_;
|
||||
if (!tooltip_template.empty()) {
|
||||
label_.set_tooltip_markup(fmt::format(
|
||||
setTooltipMarkup(fmt::format(
|
||||
fmt::runtime(tooltip_template), fmt::arg("nr_failed", nr_failed_),
|
||||
fmt::arg("nr_failed_system", nr_failed_system_),
|
||||
fmt::arg("nr_failed_user", nr_failed_user_), fmt::arg("system_state", system_state_),
|
||||
fmt::arg("user_state", user_state_), fmt::arg("overall_state", overall_state_),
|
||||
fmt::arg("failed_units_list", failed_list)));
|
||||
} else {
|
||||
label_.set_tooltip_text("");
|
||||
setTooltipMarkup("");
|
||||
}
|
||||
}
|
||||
ALabel::update();
|
||||
|
||||
+77
-19
@@ -1,6 +1,8 @@
|
||||
#include "modules/temperature.hpp"
|
||||
|
||||
#include <filesystem>
|
||||
#include <optional>
|
||||
#include <stdexcept>
|
||||
#include <string>
|
||||
|
||||
#if defined(__FreeBSD__)
|
||||
@@ -20,12 +22,59 @@ waybar::modules::Temperature::Temperature(const std::string& id, const Json::Val
|
||||
if (check_set_path(item.asString())) break;
|
||||
};
|
||||
|
||||
// if hwmon_path is an array, loop to find first valid item
|
||||
traverseAsArray(config_["hwmon-path"], [this](const std::string& path) {
|
||||
if (!std::filesystem::exists(path)) return false;
|
||||
file_path_ = path;
|
||||
return true;
|
||||
});
|
||||
if (config_["hwmon-by-name"].isString() && config_["input-filename"].isString()) {
|
||||
auto name = config_["hwmon-by-name"].asString();
|
||||
auto input_filename = config_["input-filename"].asString();
|
||||
for (const auto& entry : std::filesystem::directory_iterator("/sys/class/hwmon/")) {
|
||||
if (std::filesystem::is_directory(entry) && file_path_.empty()) {
|
||||
auto name_filepath = entry.path().string() + "/name";
|
||||
auto input_filepath = entry.path().string() + "/" + input_filename;
|
||||
|
||||
if (std::filesystem::exists(name_filepath) && std::filesystem::exists(input_filepath)) {
|
||||
std::ifstream name_file(name_filepath);
|
||||
if (!name_file.is_open()) {
|
||||
throw std::runtime_error("Error: Could not open file " + name_filepath);
|
||||
}
|
||||
|
||||
std::string line;
|
||||
while (std::getline(name_file, line)) {
|
||||
if (line.find(name) != std::string::npos) {
|
||||
file_path_ = input_filepath;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
if (file_path_.empty()) throw std::runtime_error("Could not find hwmon by name " + name);
|
||||
}
|
||||
|
||||
auto find_hwmon_by_name = [](const std::string& name) -> std::optional<std::filesystem::path> {
|
||||
for (const auto& entry : std::filesystem::directory_iterator("/sys/class/hwmon")) {
|
||||
std::ifstream f(entry.path() / "name");
|
||||
std::string hwname;
|
||||
if (f >> hwname && hwname == name) {
|
||||
return entry.path();
|
||||
}
|
||||
}
|
||||
return std::nullopt;
|
||||
};
|
||||
|
||||
// ensure either hwmon-name OR old paths are used, not both
|
||||
if (config_["hwmon-name"].isString() &&
|
||||
(!config_["hwmon-path"].isNull() || !config_["hwmon-path-abs"].isNull())) {
|
||||
throw std::runtime_error(
|
||||
"hwmon-name cannot be used together with hwmon-path or hwmon-path-abs");
|
||||
}
|
||||
|
||||
if (file_path_.empty()) {
|
||||
// if hwmon_path is an array, loop to find first valid item
|
||||
traverseAsArray(config_["hwmon-path"], [this](const std::string& path) {
|
||||
if (!std::filesystem::exists(path)) return false;
|
||||
file_path_ = path;
|
||||
return true;
|
||||
});
|
||||
}
|
||||
|
||||
if (file_path_.empty() && config_["input-filename"].isString()) {
|
||||
// fallback to hwmon_paths-abs
|
||||
@@ -40,6 +89,20 @@ waybar::modules::Temperature::Temperature(const std::string& id, const Json::Val
|
||||
});
|
||||
}
|
||||
|
||||
if (file_path_.empty() && config_["hwmon-name"].isString()) {
|
||||
if (!config_["input-filename"].isString()) {
|
||||
throw std::runtime_error("hwmon-name requires input-filename to be set");
|
||||
}
|
||||
|
||||
auto hwmon = find_hwmon_by_name(config_["hwmon-name"].asString());
|
||||
|
||||
if (!hwmon) {
|
||||
throw std::runtime_error("hwmon-name '" + config_["hwmon-name"].asString() + "' not found");
|
||||
}
|
||||
|
||||
file_path_ = hwmon->string() + "/" + config_["input-filename"].asString();
|
||||
}
|
||||
|
||||
if (file_path_.empty()) {
|
||||
auto zone = config_["thermal-zone"].isInt() ? config_["thermal-zone"].asInt() : 0;
|
||||
file_path_ = fmt::format("/sys/class/thermal/thermal_zone{}/temp", zone);
|
||||
@@ -92,19 +155,10 @@ auto waybar::modules::Temperature::update() -> void {
|
||||
event_box_.show();
|
||||
|
||||
auto max_temp = config_["critical-threshold"].isInt() ? config_["critical-threshold"].asInt() : 0;
|
||||
label_.set_markup(fmt::format(fmt::runtime(format), fmt::arg("temperatureC", temperature_c),
|
||||
fmt::arg("temperatureF", temperature_f),
|
||||
fmt::arg("temperatureK", temperature_k),
|
||||
fmt::arg("icon", getIcon(temperature_c, "", max_temp))));
|
||||
if (tooltipEnabled()) {
|
||||
std::string tooltip_format = "{temperatureC}°C";
|
||||
if (config_["tooltip-format"].isString()) {
|
||||
tooltip_format = config_["tooltip-format"].asString();
|
||||
}
|
||||
label_.set_tooltip_markup(fmt::format(
|
||||
fmt::runtime(tooltip_format), fmt::arg("temperatureC", temperature_c),
|
||||
fmt::arg("temperatureF", temperature_f), fmt::arg("temperatureK", temperature_k)));
|
||||
}
|
||||
updateLabelAndTooltip(format, "{temperatureC}°C", fmt::arg("temperatureC", temperature_c),
|
||||
fmt::arg("temperatureF", temperature_f),
|
||||
fmt::arg("temperatureK", temperature_k),
|
||||
fmt::arg("icon", getIcon(temperature_c, "", max_temp)));
|
||||
// Call parent update
|
||||
ALabel::update();
|
||||
}
|
||||
@@ -155,3 +209,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(); }
|
||||
|
||||
@@ -223,7 +223,7 @@ auto UPower::update() -> void {
|
||||
return;
|
||||
}
|
||||
|
||||
label_.set_markup(getText(upDevice_, format_));
|
||||
setLabelMarkup(getText(upDevice_, format_));
|
||||
// Set icon
|
||||
if (upDevice_.icon_name == NULL || !gtkTheme_->has_icon(upDevice_.icon_name))
|
||||
upDevice_.icon_name = (char*)NO_BATTERY.c_str();
|
||||
|
||||
@@ -7,7 +7,6 @@
|
||||
#include <algorithm>
|
||||
#include <chrono>
|
||||
|
||||
#include "gdkmm/cursor.h"
|
||||
#include "gdkmm/event.h"
|
||||
#include "gdkmm/types.h"
|
||||
#include "glibmm/fileutils.h"
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
|
||||
+138
-38
@@ -2,6 +2,9 @@
|
||||
|
||||
#include <spdlog/spdlog.h>
|
||||
|
||||
#include <cmath>
|
||||
#include <string>
|
||||
|
||||
bool isValidNodeId(uint32_t id) { return id > 0 && id < G_MAXUINT32; }
|
||||
|
||||
std::list<waybar::modules::Wireplumber*> waybar::modules::Wireplumber::modules;
|
||||
@@ -25,7 +28,8 @@ waybar::modules::Wireplumber::Wireplumber(const std::string& id, const Json::Val
|
||||
source_node_id_(0),
|
||||
source_muted_(false),
|
||||
source_volume_(0.0),
|
||||
default_source_name_(nullptr) {
|
||||
default_source_name_(nullptr),
|
||||
form_factor_("") {
|
||||
waybar::modules::Wireplumber::modules.push_back(this);
|
||||
|
||||
wp_init(WP_INIT_PIPEWIRE);
|
||||
@@ -108,6 +112,21 @@ void waybar::modules::Wireplumber::updateNodeName(waybar::modules::Wireplumber*
|
||||
: description != nullptr ? description
|
||||
: "Unknown node name";
|
||||
spdlog::debug("[{}]: Updating '{}' node name to: {}", self->name_, self->type_, self->node_name_);
|
||||
|
||||
// find form-factor
|
||||
const auto* devid = wp_properties_get(properties, "device.id");
|
||||
spdlog::debug("[{}]: '{}' device.id is {}", self->name_, self->type_, devid);
|
||||
|
||||
auto* dev = static_cast<WpDevice*>(wp_object_manager_lookup(
|
||||
self->om_, WP_TYPE_DEVICE, WP_CONSTRAINT_TYPE_G_PROPERTY, "bound-id", "=s", devid, nullptr));
|
||||
|
||||
if (const auto* ff =
|
||||
wp_pipewire_object_get_property(WP_PIPEWIRE_OBJECT(dev), "device.form-factor")) {
|
||||
self->form_factor_ = ff;
|
||||
spdlog::debug("[{}]: Updating node form factor to: {}", self->name_, self->form_factor_);
|
||||
} else {
|
||||
self->form_factor_ = "";
|
||||
}
|
||||
}
|
||||
|
||||
void waybar::modules::Wireplumber::updateSourceName(waybar::modules::Wireplumber* self,
|
||||
@@ -211,6 +230,7 @@ void waybar::modules::Wireplumber::onMixerChanged(waybar::modules::Wireplumber*
|
||||
return;
|
||||
}
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
spdlog::warn("[{}]: (onMixerChanged: {}) - Object with id {} not found", self->name_,
|
||||
@@ -369,6 +389,8 @@ void waybar::modules::Wireplumber::prepare(waybar::modules::Wireplumber* self) {
|
||||
"=s", self->type_, nullptr);
|
||||
wp_object_manager_add_interest(om_, WP_TYPE_NODE, WP_CONSTRAINT_TYPE_PW_PROPERTY, "media.class",
|
||||
"=s", "Audio/Source", nullptr);
|
||||
wp_object_manager_add_interest(om_, WP_TYPE_DEVICE, WP_CONSTRAINT_TYPE_PW_PROPERTY, "media.class",
|
||||
"=s", "Audio/Device", nullptr);
|
||||
}
|
||||
|
||||
void waybar::modules::Wireplumber::onDefaultNodesApiLoaded(WpObject* p, GAsyncResult* res,
|
||||
@@ -407,7 +429,7 @@ void waybar::modules::Wireplumber::onMixerApiLoaded(WpObject* p, GAsyncResult* r
|
||||
spdlog::debug("[{}]: loaded mixer API", self->name_);
|
||||
g_ptr_array_add(self->apis_, ({
|
||||
WpPlugin* p = wp_plugin_find(self->wp_core_, "mixer-api");
|
||||
g_object_set(G_OBJECT(p), "scale", 1 /* cubic */, nullptr);
|
||||
g_object_set(G_OBJECT(p), "scale", 0 /* linear */, nullptr);
|
||||
p;
|
||||
}));
|
||||
|
||||
@@ -426,13 +448,54 @@ void waybar::modules::Wireplumber::asyncLoadRequiredApiModules() {
|
||||
this);
|
||||
}
|
||||
|
||||
static const std::array<std::string, 7> ports = {
|
||||
"headphone", "speaker", "headset", "hands-free", "portable", "car", "hifi",
|
||||
};
|
||||
|
||||
std::vector<std::string> waybar::modules::Wireplumber::getWPIcon() {
|
||||
std::vector<std::string> res;
|
||||
if (muted_) {
|
||||
res.emplace_back(node_name_ + "-muted");
|
||||
}
|
||||
res.push_back(node_name_);
|
||||
res.push_back(source_name_);
|
||||
std::transform(form_factor_.begin(), form_factor_.end(), form_factor_.begin(), ::tolower);
|
||||
for (auto const& port : ports) {
|
||||
if (form_factor_.find(port) != std::string::npos) {
|
||||
if (muted_) {
|
||||
res.emplace_back(port + "-muted");
|
||||
}
|
||||
res.push_back(port);
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (muted_) {
|
||||
res.emplace_back("default-muted");
|
||||
}
|
||||
return res;
|
||||
}
|
||||
|
||||
auto waybar::modules::Wireplumber::update() -> void {
|
||||
auto format = format_;
|
||||
std::string tooltipFormat;
|
||||
std::string format_name = "format";
|
||||
|
||||
// Handle sink bluetooth state
|
||||
const std::string name = default_node_name_ != nullptr ? default_node_name_ : "";
|
||||
|
||||
auto bt = name.find("bluez") != std::string::npos || name.find("a2dp-sink") != std::string::npos;
|
||||
if (bt) {
|
||||
format_name += "-bluetooth";
|
||||
label_.get_style_context()->add_class("bluetooth");
|
||||
} else {
|
||||
label_.get_style_context()->remove_class("bluetooth");
|
||||
}
|
||||
|
||||
// Handle sink mute state
|
||||
if (muted_) {
|
||||
format = config_["format-muted"].isString() ? config_["format-muted"].asString() : format;
|
||||
// Check muted bluetooth format exists, otherwise fall back to default muted format.
|
||||
if (format_name != "format" && !config_[format_name + "-muted"].isString())
|
||||
format_name = "format";
|
||||
format_name += "-muted";
|
||||
label_.get_style_context()->add_class("muted");
|
||||
label_.get_style_context()->add_class("sink-muted");
|
||||
} else {
|
||||
@@ -447,18 +510,21 @@ auto waybar::modules::Wireplumber::update() -> void {
|
||||
label_.get_style_context()->remove_class("source-muted");
|
||||
}
|
||||
|
||||
int vol = round(volume_ * 100.0);
|
||||
int source_vol = round(source_volume_ * 100.0);
|
||||
double vol_cube = pow(volume_, 3);
|
||||
double source_vol_cube = pow(source_volume_, 3);
|
||||
|
||||
int vol = round(vol_cube * 100.0);
|
||||
int source_vol = round(source_vol_cube * 100.0);
|
||||
|
||||
double vol_db = 20.0 * log10(volume_);
|
||||
double source_vol_db = 20.0 * log10(source_volume_);
|
||||
|
||||
// Get the state and apply state-specific format if available
|
||||
auto state = getState(vol);
|
||||
if (!state.empty()) {
|
||||
std::string format_name = muted_ ? "format-muted" : "format";
|
||||
std::string state_format_name = format_name + "-" + state;
|
||||
if (config_[state_format_name].isString()) {
|
||||
format = config_[state_format_name].asString();
|
||||
}
|
||||
}
|
||||
if (!state.empty() && config_[format_name + "-" + state].isString())
|
||||
format = config_[format_name + "-" + state].asString();
|
||||
else if (config_[format_name].isString())
|
||||
format = config_[format_name].asString();
|
||||
|
||||
// Prepare source format string (similar to PulseAudio)
|
||||
std::string format_source = "{volume}%";
|
||||
@@ -476,24 +542,28 @@ auto waybar::modules::Wireplumber::update() -> void {
|
||||
std::string formatted_source =
|
||||
fmt::format(fmt::runtime(format_source), fmt::arg("volume", source_vol));
|
||||
|
||||
std::string markup =
|
||||
fmt::format(fmt::runtime(format), fmt::arg("node_name", node_name_), fmt::arg("volume", vol),
|
||||
fmt::arg("icon", getIcon(vol)), fmt::arg("format_source", formatted_source),
|
||||
fmt::arg("source_volume", source_vol), fmt::arg("source_desc", source_name_));
|
||||
label_.set_markup(markup);
|
||||
fmt::dynamic_format_arg_store<fmt::format_context> store;
|
||||
store.push_back(fmt::arg("node_name", node_name_));
|
||||
store.push_back(fmt::arg("volume", vol));
|
||||
store.push_back(fmt::arg("icon", getIcon(vol, getWPIcon())));
|
||||
store.push_back(fmt::arg("format_source", formatted_source));
|
||||
store.push_back(fmt::arg("source_volume", source_vol));
|
||||
store.push_back(fmt::arg("source_desc", source_name_));
|
||||
store.push_back(fmt::arg("volume_linear", volume_));
|
||||
store.push_back(fmt::arg("volume_cubic", vol_cube));
|
||||
store.push_back(fmt::arg("volume_db", vol_db));
|
||||
store.push_back(fmt::arg("source_volume_linear", source_volume_));
|
||||
store.push_back(fmt::arg("source_volume_cubic", source_vol_cube));
|
||||
store.push_back(fmt::arg("source_volume_db", source_vol_db));
|
||||
|
||||
setLabelMarkup(fmt::vformat(format, store));
|
||||
|
||||
if (tooltipEnabled()) {
|
||||
if (tooltipFormat.empty() && config_["tooltip-format"].isString()) {
|
||||
tooltipFormat = config_["tooltip-format"].asString();
|
||||
}
|
||||
|
||||
auto tooltipFormat = resolveTooltipFormat("");
|
||||
if (!tooltipFormat.empty()) {
|
||||
label_.set_tooltip_markup(fmt::format(
|
||||
fmt::runtime(tooltipFormat), fmt::arg("node_name", node_name_), fmt::arg("volume", vol),
|
||||
fmt::arg("icon", getIcon(vol)), fmt::arg("format_source", formatted_source),
|
||||
fmt::arg("source_volume", source_vol), fmt::arg("source_desc", source_name_)));
|
||||
setTooltipMarkup(fmt::vformat(tooltipFormat, store));
|
||||
} else {
|
||||
label_.set_tooltip_markup(node_name_);
|
||||
setTooltipMarkup(node_name_);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -510,28 +580,58 @@ bool waybar::modules::Wireplumber::handleScroll(GdkEventScroll* e) {
|
||||
return true;
|
||||
}
|
||||
double maxVolume = 1;
|
||||
double step = 1.0 / 100.0;
|
||||
double step = 1.0;
|
||||
if (config_["scroll-step"].isDouble()) {
|
||||
step = config_["scroll-step"].asDouble() / 100.0;
|
||||
step = config_["scroll-step"].asDouble();
|
||||
}
|
||||
if (config_["max-volume"].isDouble()) {
|
||||
maxVolume = config_["max-volume"].asDouble() / 100.0;
|
||||
maxVolume = config_["max-volume"].asDouble();
|
||||
}
|
||||
|
||||
if (step < min_step_) step = min_step_;
|
||||
double vol = volume_;
|
||||
std::string scale = "cubic_percent";
|
||||
if (config_["scroll-scale"].isString()) {
|
||||
scale = config_["scroll-scale"].asString();
|
||||
}
|
||||
|
||||
double newVol = volume_;
|
||||
if (scale == "cubic") {
|
||||
vol = pow(vol, 3);
|
||||
} else if (scale == "db") {
|
||||
vol = log10(vol) * 20.0;
|
||||
} else if (scale == "cubic_percent") {
|
||||
vol = pow(vol, 3) * 100.0;
|
||||
}
|
||||
|
||||
double newVol = vol;
|
||||
if (dir == SCROLL_DIR::UP) {
|
||||
if (volume_ < maxVolume) {
|
||||
newVol = volume_ + step;
|
||||
if (newVol > maxVolume) newVol = maxVolume;
|
||||
newVol = vol + step;
|
||||
} else if (dir == SCROLL_DIR::DOWN) {
|
||||
newVol = vol - step;
|
||||
}
|
||||
|
||||
if (scale == "cubic") {
|
||||
newVol = cbrt(newVol);
|
||||
} else if (scale == "db") {
|
||||
newVol = exp10(newVol / 20.0);
|
||||
} else if (scale == "cubic_percent") {
|
||||
newVol = cbrt(newVol / 100.0);
|
||||
}
|
||||
|
||||
if (dir == SCROLL_DIR::UP) {
|
||||
if (volume_ + min_step_ > newVol) {
|
||||
newVol = volume_ + min_step_;
|
||||
}
|
||||
} else if (dir == SCROLL_DIR::DOWN) {
|
||||
if (volume_ > 0) {
|
||||
newVol = volume_ - step;
|
||||
if (newVol < 0) newVol = 0;
|
||||
if (volume_ - min_step_ < newVol) {
|
||||
newVol = volume_ - min_step_;
|
||||
}
|
||||
}
|
||||
|
||||
if (newVol < 0)
|
||||
newVol = 0;
|
||||
else if (newVol > maxVolume)
|
||||
newVol = maxVolume;
|
||||
|
||||
if (newVol != volume_) {
|
||||
if (mixer_api_ == nullptr) return true;
|
||||
GVariant* variant = g_variant_new_double(newVol);
|
||||
|
||||
+306
-5
@@ -94,9 +94,46 @@ 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_);
|
||||
}
|
||||
|
||||
if (config_["justify"].isString()) {
|
||||
auto justify_str = config_["justify"].asString();
|
||||
if (justify_str == "left") {
|
||||
content_.set_halign(Gtk::ALIGN_START);
|
||||
} else if (justify_str == "right") {
|
||||
content_.set_halign(Gtk::ALIGN_END);
|
||||
} else if (justify_str == "center") {
|
||||
content_.set_halign(Gtk::ALIGN_CENTER);
|
||||
}
|
||||
}
|
||||
|
||||
content_.show();
|
||||
button.add(content_);
|
||||
@@ -293,9 +330,12 @@ void Task::handle_output_enter(struct wl_output* output) {
|
||||
button.signal_size_allocate().connect_notify(
|
||||
sigc::mem_fun(this, &Task::on_button_size_allocated));
|
||||
tbar_->add_button(button);
|
||||
button.show();
|
||||
if (!config_["active-only"].asBool() || active()) {
|
||||
button.show();
|
||||
}
|
||||
button_visible_ = true;
|
||||
spdlog::debug("{} now visible on {}", repr(), bar_.output->name);
|
||||
tbar_->update_bar_css_classes();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -308,6 +348,7 @@ void Task::handle_output_leave(struct wl_output* output) {
|
||||
button.hide();
|
||||
button_visible_ = false;
|
||||
spdlog::debug("{} now invisible on {}", repr(), bar_.output->name);
|
||||
tbar_->update_bar_css_classes();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -350,6 +391,20 @@ void Task::handle_done() {
|
||||
button.get_style_context()->remove_class("fullscreen");
|
||||
}
|
||||
|
||||
if (button_visible_ && config_["active-only"].asBool()) {
|
||||
if (active()) {
|
||||
button.show();
|
||||
} else {
|
||||
button.hide();
|
||||
}
|
||||
}
|
||||
|
||||
if (active()) {
|
||||
tbar_->assign_current_workspace(*this);
|
||||
}
|
||||
|
||||
tbar_->update_bar_css_classes();
|
||||
|
||||
if (config_["active-first"].isBool() && config_["active-first"].asBool() && active())
|
||||
tbar_->move_button(button, 0);
|
||||
|
||||
@@ -551,6 +606,8 @@ static void handle_global(void* data, struct wl_registry* registry, uint32_t nam
|
||||
const char* interface, uint32_t version) {
|
||||
if (std::strcmp(interface, zwlr_foreign_toplevel_manager_v1_interface.name) == 0) {
|
||||
static_cast<Taskbar*>(data)->register_manager(registry, name, version);
|
||||
} else if (std::strcmp(interface, ext_workspace_manager_v1_interface.name) == 0) {
|
||||
static_cast<Taskbar*>(data)->register_workspace_manager(registry, name, version);
|
||||
} else if (std::strcmp(interface, wl_seat_interface.name) == 0) {
|
||||
static_cast<Taskbar*>(data)->register_seat(registry, name, version);
|
||||
}
|
||||
@@ -568,6 +625,7 @@ Taskbar::Taskbar(const std::string& id, const waybar::Bar& bar, const Json::Valu
|
||||
bar_(bar),
|
||||
box_{bar.orientation, 0},
|
||||
manager_{nullptr},
|
||||
workspace_manager_{nullptr},
|
||||
seat_{nullptr} {
|
||||
box_.set_name("taskbar");
|
||||
if (!id.empty()) {
|
||||
@@ -577,6 +635,12 @@ Taskbar::Taskbar(const std::string& id, const waybar::Bar& bar, const Json::Valu
|
||||
box_.get_style_context()->add_class("empty");
|
||||
event_box_.add(box_);
|
||||
|
||||
// Make task buttons distribute evenly across the available width.
|
||||
if (config_["homogeneous"].isBool() && config_["homogeneous"].asBool()) {
|
||||
box_.set_homogeneous(true);
|
||||
box_.set_hexpand(true);
|
||||
}
|
||||
|
||||
struct wl_display* display = Client::inst()->wl_display;
|
||||
struct wl_registry* registry = wl_display_get_registry(display);
|
||||
|
||||
@@ -623,6 +687,27 @@ Taskbar::Taskbar(const std::string& id, const waybar::Bar& bar, const Json::Valu
|
||||
}
|
||||
|
||||
Taskbar::~Taskbar() {
|
||||
for (auto& workspace : workspaces_) {
|
||||
ext_workspace_handle_v1_destroy(workspace->handle);
|
||||
}
|
||||
workspaces_.clear();
|
||||
for (auto* group : workspace_groups_) {
|
||||
ext_workspace_group_handle_v1_destroy(group);
|
||||
}
|
||||
workspace_groups_.clear();
|
||||
|
||||
if (workspace_manager_) {
|
||||
struct wl_display* display = Client::inst()->wl_display;
|
||||
ext_workspace_manager_v1_stop(workspace_manager_);
|
||||
wl_display_roundtrip(display);
|
||||
|
||||
if (workspace_manager_) {
|
||||
spdlog::warn("Workspace manager destroyed before .finished event");
|
||||
ext_workspace_manager_v1_destroy(workspace_manager_);
|
||||
workspace_manager_ = nullptr;
|
||||
}
|
||||
}
|
||||
|
||||
if (manager_) {
|
||||
struct wl_display* display = Client::inst()->wl_display;
|
||||
/*
|
||||
@@ -639,6 +724,13 @@ Taskbar::~Taskbar() {
|
||||
manager_ = nullptr;
|
||||
}
|
||||
}
|
||||
|
||||
if (config_["bar-css-states"].asBool()) {
|
||||
set_bar_css_class("toplevel-active", false);
|
||||
set_bar_css_class("toplevel-maximized", false);
|
||||
set_bar_css_class("toplevel-minimized", false);
|
||||
set_bar_css_class("toplevel-fullscreen", false);
|
||||
}
|
||||
}
|
||||
|
||||
void Taskbar::update() {
|
||||
@@ -674,6 +766,80 @@ static const struct zwlr_foreign_toplevel_manager_v1_listener toplevel_manager_i
|
||||
.finished = tm_handle_finished,
|
||||
};
|
||||
|
||||
static void workspace_handle_id(void*, struct ext_workspace_handle_v1*, const char*) {}
|
||||
static void workspace_handle_name(void*, struct ext_workspace_handle_v1*, const char*) {}
|
||||
static void workspace_handle_coordinates(void*, struct ext_workspace_handle_v1*, struct wl_array*) {
|
||||
}
|
||||
|
||||
static void workspace_handle_state(void* data, struct ext_workspace_handle_v1*, uint32_t state) {
|
||||
static_cast<Taskbar::WorkspaceState*>(data)->state = state;
|
||||
}
|
||||
|
||||
static void workspace_handle_capabilities(void*, struct ext_workspace_handle_v1*, uint32_t) {}
|
||||
|
||||
static void workspace_handle_removed(void* data, struct ext_workspace_handle_v1* handle) {
|
||||
static_cast<Taskbar::WorkspaceState*>(data)->taskbar->handle_workspace_removed(handle);
|
||||
}
|
||||
|
||||
static const struct ext_workspace_handle_v1_listener workspace_handle_impl = {
|
||||
.id = workspace_handle_id,
|
||||
.name = workspace_handle_name,
|
||||
.coordinates = workspace_handle_coordinates,
|
||||
.state = workspace_handle_state,
|
||||
.capabilities = workspace_handle_capabilities,
|
||||
.removed = workspace_handle_removed,
|
||||
};
|
||||
|
||||
static void workspace_group_handle_capabilities(void*, struct ext_workspace_group_handle_v1*,
|
||||
uint32_t) {}
|
||||
static void workspace_group_handle_output_enter(void*, struct ext_workspace_group_handle_v1*,
|
||||
struct wl_output*) {}
|
||||
static void workspace_group_handle_output_leave(void*, struct ext_workspace_group_handle_v1*,
|
||||
struct wl_output*) {}
|
||||
static void workspace_group_handle_workspace_enter(void*, struct ext_workspace_group_handle_v1*,
|
||||
struct ext_workspace_handle_v1*) {}
|
||||
static void workspace_group_handle_workspace_leave(void*, struct ext_workspace_group_handle_v1*,
|
||||
struct ext_workspace_handle_v1*) {}
|
||||
|
||||
static void workspace_group_handle_removed(void* data,
|
||||
struct ext_workspace_group_handle_v1* group) {
|
||||
static_cast<Taskbar*>(data)->handle_workspace_group_removed(group);
|
||||
}
|
||||
|
||||
static const struct ext_workspace_group_handle_v1_listener workspace_group_impl = {
|
||||
.capabilities = workspace_group_handle_capabilities,
|
||||
.output_enter = workspace_group_handle_output_enter,
|
||||
.output_leave = workspace_group_handle_output_leave,
|
||||
.workspace_enter = workspace_group_handle_workspace_enter,
|
||||
.workspace_leave = workspace_group_handle_workspace_leave,
|
||||
.removed = workspace_group_handle_removed,
|
||||
};
|
||||
|
||||
static void workspace_manager_handle_group(void* data, struct ext_workspace_manager_v1*,
|
||||
struct ext_workspace_group_handle_v1* group) {
|
||||
static_cast<Taskbar*>(data)->handle_workspace_group_create(group);
|
||||
}
|
||||
|
||||
static void workspace_manager_handle_workspace(void* data, struct ext_workspace_manager_v1*,
|
||||
struct ext_workspace_handle_v1* workspace) {
|
||||
static_cast<Taskbar*>(data)->handle_workspace_create(workspace);
|
||||
}
|
||||
|
||||
static void workspace_manager_handle_done(void* data, struct ext_workspace_manager_v1*) {
|
||||
static_cast<Taskbar*>(data)->handle_workspace_done();
|
||||
}
|
||||
|
||||
static void workspace_manager_handle_finished(void* data, struct ext_workspace_manager_v1*) {
|
||||
static_cast<Taskbar*>(data)->handle_workspace_finished();
|
||||
}
|
||||
|
||||
static const struct ext_workspace_manager_v1_listener workspace_manager_impl = {
|
||||
.workspace_group = workspace_manager_handle_group,
|
||||
.workspace = workspace_manager_handle_workspace,
|
||||
.done = workspace_manager_handle_done,
|
||||
.finished = workspace_manager_handle_finished,
|
||||
};
|
||||
|
||||
void Taskbar::register_manager(struct wl_registry* registry, uint32_t name, uint32_t version) {
|
||||
if (manager_) {
|
||||
spdlog::warn("Register foreign toplevel manager again although already existing!");
|
||||
@@ -698,6 +864,18 @@ void Taskbar::register_manager(struct wl_registry* registry, uint32_t name, uint
|
||||
spdlog::debug("Failed to register manager");
|
||||
}
|
||||
|
||||
void Taskbar::register_workspace_manager(struct wl_registry* registry, uint32_t name,
|
||||
uint32_t version) {
|
||||
if (workspace_manager_) {
|
||||
return;
|
||||
}
|
||||
|
||||
version = std::min<uint32_t>(version, ext_workspace_manager_v1_interface.version);
|
||||
workspace_manager_ = static_cast<struct ext_workspace_manager_v1*>(
|
||||
wl_registry_bind(registry, name, &ext_workspace_manager_v1_interface, version));
|
||||
ext_workspace_manager_v1_add_listener(workspace_manager_, &workspace_manager_impl, this);
|
||||
}
|
||||
|
||||
void Taskbar::register_seat(struct wl_registry* registry, uint32_t name, uint32_t version) {
|
||||
if (seat_) {
|
||||
spdlog::warn("Register seat again although already existing!");
|
||||
@@ -717,8 +895,81 @@ void Taskbar::handle_finished() {
|
||||
manager_ = nullptr;
|
||||
}
|
||||
|
||||
void Taskbar::handle_workspace_group_create(struct ext_workspace_group_handle_v1* handle) {
|
||||
ext_workspace_group_handle_v1_add_listener(handle, &workspace_group_impl, this);
|
||||
workspace_groups_.push_back(handle);
|
||||
}
|
||||
|
||||
void Taskbar::handle_workspace_group_removed(struct ext_workspace_group_handle_v1* handle) {
|
||||
const auto group = std::find(workspace_groups_.begin(), workspace_groups_.end(), handle);
|
||||
if (group != workspace_groups_.end()) {
|
||||
ext_workspace_group_handle_v1_destroy(*group);
|
||||
workspace_groups_.erase(group);
|
||||
}
|
||||
}
|
||||
|
||||
void Taskbar::handle_workspace_create(struct ext_workspace_handle_v1* handle) {
|
||||
auto workspace = std::make_unique<WorkspaceState>(WorkspaceState{this, handle});
|
||||
ext_workspace_handle_v1_add_listener(handle, &workspace_handle_impl, workspace.get());
|
||||
workspaces_.push_back(std::move(workspace));
|
||||
}
|
||||
|
||||
void Taskbar::handle_workspace_done() {
|
||||
const auto active_workspace =
|
||||
std::find_if(workspaces_.begin(), workspaces_.end(), [](const auto& workspace) {
|
||||
return workspace->state & EXT_WORKSPACE_HANDLE_V1_STATE_ACTIVE;
|
||||
});
|
||||
current_workspace_ =
|
||||
active_workspace == workspaces_.end() ? nullptr : (*active_workspace)->handle;
|
||||
if (current_workspace_) {
|
||||
const auto active_task =
|
||||
std::find_if(tasks_.begin(), tasks_.end(), [](const auto& task) { return task->active(); });
|
||||
if (active_task != tasks_.end()) {
|
||||
(*active_task)->set_workspace(current_workspace_);
|
||||
}
|
||||
}
|
||||
update_bar_css_classes();
|
||||
}
|
||||
|
||||
void Taskbar::handle_workspace_finished() { workspace_manager_ = nullptr; }
|
||||
|
||||
void Taskbar::handle_workspace_removed(struct ext_workspace_handle_v1* handle) {
|
||||
if (current_workspace_ == handle) {
|
||||
current_workspace_ = nullptr;
|
||||
}
|
||||
for (auto& task : tasks_) {
|
||||
if (task->workspace() == handle) {
|
||||
task->set_workspace(nullptr);
|
||||
}
|
||||
}
|
||||
|
||||
const auto workspace =
|
||||
std::find_if(workspaces_.begin(), workspaces_.end(),
|
||||
[handle](const auto& workspace) { return workspace->handle == handle; });
|
||||
if (workspace != workspaces_.end()) {
|
||||
ext_workspace_handle_v1_destroy((*workspace)->handle);
|
||||
workspaces_.erase(workspace);
|
||||
}
|
||||
update_bar_css_classes();
|
||||
}
|
||||
|
||||
void Taskbar::add_button(Gtk::Button& bt) {
|
||||
box_.pack_start(bt, false, false);
|
||||
/* When "homogeneous" is enabled, let every child expand and fill so the buttons
|
||||
* divide the available width equally. Otherwise, only let the buttons expand to
|
||||
* fill the taskbar when "expand" is enabled and the bar is horizontal (see the
|
||||
* Task constructor for details). */
|
||||
const bool homogeneous = config_["homogeneous"].isBool() && config_["homogeneous"].asBool();
|
||||
bool expand = config_["expand"].isBool() && config_["expand"].asBool();
|
||||
bool horizontal = bar_.orientation == Gtk::ORIENTATION_HORIZONTAL;
|
||||
if (homogeneous) {
|
||||
box_.pack_start(bt, true, true);
|
||||
bt.set_hexpand(true);
|
||||
bt.set_halign(Gtk::ALIGN_FILL);
|
||||
} else if (expand && horizontal) {
|
||||
box_.pack_start(bt, true, true);
|
||||
} else {
|
||||
box_.pack_start(bt, false, false);
|
||||
}
|
||||
box_.get_style_context()->remove_class("empty");
|
||||
}
|
||||
|
||||
@@ -741,6 +992,56 @@ void Taskbar::remove_task(uint32_t id) {
|
||||
}
|
||||
|
||||
tasks_.erase(it);
|
||||
update_bar_css_classes();
|
||||
}
|
||||
|
||||
void Taskbar::assign_current_workspace(Task& task) {
|
||||
if (current_workspace_) {
|
||||
task.set_workspace(current_workspace_);
|
||||
}
|
||||
}
|
||||
|
||||
void Taskbar::update_bar_css_classes() {
|
||||
if (!config_["bar-css-states"].asBool()) {
|
||||
return;
|
||||
}
|
||||
|
||||
const auto active_task = std::find_if(tasks_.begin(), tasks_.end(), [](const TaskPtr& task) {
|
||||
return task->visible() && task->active();
|
||||
});
|
||||
|
||||
const bool has_active_task = active_task != tasks_.end();
|
||||
const auto on_current_workspace = [this](const TaskPtr& task) {
|
||||
if (!current_workspace_) {
|
||||
return task->active();
|
||||
}
|
||||
return task->workspace() == current_workspace_;
|
||||
};
|
||||
const bool has_maximized_task =
|
||||
std::any_of(tasks_.begin(), tasks_.end(), [&on_current_workspace](const TaskPtr& task) {
|
||||
return task->visible() && !task->minimized() && on_current_workspace(task) &&
|
||||
task->maximized();
|
||||
});
|
||||
const bool has_fullscreen_task =
|
||||
std::any_of(tasks_.begin(), tasks_.end(), [&on_current_workspace](const TaskPtr& task) {
|
||||
return task->visible() && !task->minimized() && on_current_workspace(task) &&
|
||||
task->fullscreen();
|
||||
});
|
||||
set_bar_css_class("toplevel-active", has_active_task);
|
||||
set_bar_css_class("toplevel-maximized", has_maximized_task);
|
||||
set_bar_css_class("toplevel-minimized", has_active_task && (*active_task)->minimized());
|
||||
set_bar_css_class("toplevel-fullscreen", has_fullscreen_task);
|
||||
}
|
||||
|
||||
void Taskbar::set_bar_css_class(const std::string& class_name, bool enabled) {
|
||||
const auto style = bar_.window.get_style_context();
|
||||
if (enabled && !style->has_class(class_name)) {
|
||||
spdlog::trace("Adding bar class: {}", class_name);
|
||||
style->add_class(class_name);
|
||||
} else if (!enabled && style->has_class(class_name)) {
|
||||
spdlog::trace("Removing bar class: {}", class_name);
|
||||
style->remove_class(class_name);
|
||||
}
|
||||
}
|
||||
|
||||
bool Taskbar::show_output(struct wl_output* output) const {
|
||||
|
||||
Reference in New Issue
Block a user