Merge remote-tracking branch 'origin/master' into pr/pulseaudio_mapping
# Conflicts: # src/util/audio_backend.cpp
This commit is contained in:
+220
-65
@@ -1,9 +1,12 @@
|
||||
#include "util/audio_backend.hpp"
|
||||
|
||||
#include <fmt/core.h>
|
||||
#include <pulse/def.h>
|
||||
#include <pulse/error.h>
|
||||
#include <pulse/introspect.h>
|
||||
#include <pulse/subscribe.h>
|
||||
#include <pulse/volume.h>
|
||||
#include <spdlog/spdlog.h>
|
||||
|
||||
#include <algorithm>
|
||||
#include <cmath>
|
||||
@@ -21,6 +24,8 @@ AudioBackend::AudioBackend(std::function<void()> on_updated_cb, private_construc
|
||||
source_volume_(0),
|
||||
source_muted_(false),
|
||||
on_updated_cb_(std::move(on_updated_cb)) {
|
||||
// Initialize pa_volume_ with safe defaults
|
||||
pa_cvolume_init(&pa_volume_);
|
||||
mainloop_ = pa_threaded_mainloop_new();
|
||||
if (mainloop_ == nullptr) {
|
||||
throw std::runtime_error("pa_mainloop_new() failed.");
|
||||
@@ -35,12 +40,16 @@ AudioBackend::AudioBackend(std::function<void()> on_updated_cb, private_construc
|
||||
}
|
||||
|
||||
AudioBackend::~AudioBackend() {
|
||||
if (context_ != nullptr) {
|
||||
pa_context_disconnect(context_);
|
||||
}
|
||||
|
||||
if (mainloop_ != nullptr) {
|
||||
mainloop_api_->quit(mainloop_api_, 0);
|
||||
// Lock the mainloop so we can safely disconnect the context.
|
||||
// This must be done before stopping the thread.
|
||||
pa_threaded_mainloop_lock(mainloop_);
|
||||
if (context_ != nullptr) {
|
||||
pa_context_disconnect(context_);
|
||||
pa_context_unref(context_);
|
||||
context_ = nullptr;
|
||||
}
|
||||
pa_threaded_mainloop_unlock(mainloop_);
|
||||
pa_threaded_mainloop_stop(mainloop_);
|
||||
pa_threaded_mainloop_free(mainloop_);
|
||||
}
|
||||
@@ -64,11 +73,18 @@ void AudioBackend::connectContext() {
|
||||
}
|
||||
}
|
||||
|
||||
void AudioBackend::contextStateCb(pa_context *c, void *data) {
|
||||
auto *backend = static_cast<AudioBackend *>(data);
|
||||
void AudioBackend::contextStateCb(pa_context* c, void* data) {
|
||||
auto* backend = static_cast<AudioBackend*>(data);
|
||||
switch (pa_context_get_state(c)) {
|
||||
case PA_CONTEXT_TERMINATED:
|
||||
backend->mainloop_api_->quit(backend->mainloop_api_, 0);
|
||||
// Only quit the mainloop if this is still the active context.
|
||||
// During reconnection, the old context fires TERMINATED after the new one
|
||||
// has already been created; quitting in that case would kill the new context.
|
||||
// Note: context_ is only written from PA callbacks (while the mainloop lock is
|
||||
// held), so this comparison is safe within any PA callback.
|
||||
if (backend->context_ == nullptr || backend->context_ == c) {
|
||||
backend->mainloop_api_->quit(backend->mainloop_api_, 0);
|
||||
}
|
||||
break;
|
||||
case PA_CONTEXT_READY:
|
||||
pa_context_get_server_info(c, serverInfoCb, data);
|
||||
@@ -83,13 +99,17 @@ void AudioBackend::contextStateCb(pa_context *c, void *data) {
|
||||
nullptr, nullptr);
|
||||
break;
|
||||
case PA_CONTEXT_FAILED:
|
||||
// When pulseaudio server restarts, the connection is "failed". Try to reconnect.
|
||||
// pa_threaded_mainloop_lock is already acquired in callback threads.
|
||||
// So there is no need to lock it again.
|
||||
if (backend->context_ != nullptr) {
|
||||
pa_context_disconnect(backend->context_);
|
||||
if (pa_context_errno(c) != PA_ERR_CONNECTIONREFUSED) {
|
||||
// When pulseaudio server restarts, the connection is "failed". Try to reconnect.
|
||||
// pa_threaded_mainloop_lock is already acquired in callback threads.
|
||||
// So there is no need to lock it again.
|
||||
if (backend->context_ != nullptr) {
|
||||
pa_context_disconnect(backend->context_);
|
||||
pa_context_unref(backend->context_);
|
||||
backend->context_ = nullptr;
|
||||
}
|
||||
backend->connectContext();
|
||||
}
|
||||
backend->connectContext();
|
||||
break;
|
||||
case PA_CONTEXT_CONNECTING:
|
||||
case PA_CONTEXT_AUTHORIZING:
|
||||
@@ -102,8 +122,8 @@ void AudioBackend::contextStateCb(pa_context *c, void *data) {
|
||||
/*
|
||||
* Called when an event we subscribed to occurs.
|
||||
*/
|
||||
void AudioBackend::subscribeCb(pa_context *context, pa_subscription_event_type_t type, uint32_t idx,
|
||||
void *data) {
|
||||
void AudioBackend::subscribeCb(pa_context* context, pa_subscription_event_type_t type, uint32_t idx,
|
||||
void* data) {
|
||||
unsigned facility = type & PA_SUBSCRIPTION_EVENT_FACILITY_MASK;
|
||||
unsigned operation = type & PA_SUBSCRIPTION_EVENT_TYPE_MASK;
|
||||
if (operation != PA_SUBSCRIPTION_EVENT_CHANGE) {
|
||||
@@ -125,24 +145,33 @@ void AudioBackend::subscribeCb(pa_context *context, pa_subscription_event_type_t
|
||||
/*
|
||||
* Called in response to a volume change request
|
||||
*/
|
||||
void AudioBackend::volumeModifyCb(pa_context *c, int success, void *data) {
|
||||
auto *backend = static_cast<AudioBackend *>(data);
|
||||
void AudioBackend::volumeModifyCb(pa_context* c, int success, void* data) {
|
||||
auto* backend = static_cast<AudioBackend*>(data);
|
||||
if (success != 0) {
|
||||
pa_context_get_sink_info_by_index(backend->context_, backend->sink_idx_, sinkInfoCb, data);
|
||||
if ((backend->context_ != nullptr) &&
|
||||
pa_context_get_state(backend->context_) == PA_CONTEXT_READY) {
|
||||
pa_context_get_sink_info_by_index(backend->context_, backend->sink_idx_, sinkInfoCb, data);
|
||||
}
|
||||
} else {
|
||||
spdlog::debug("Volume modification failed");
|
||||
}
|
||||
}
|
||||
|
||||
/*
|
||||
* Called when the requested sink information is ready.
|
||||
*/
|
||||
void AudioBackend::sinkInfoCb(pa_context * /*context*/, const pa_sink_info *i, int /*eol*/,
|
||||
void *data) {
|
||||
void AudioBackend::sinkInfoCb(pa_context* /*context*/, const pa_sink_info* i, int /*eol*/,
|
||||
void* data) {
|
||||
if (i == nullptr) return;
|
||||
|
||||
auto *backend = static_cast<AudioBackend *>(data);
|
||||
auto running = i->state == PA_SINK_RUNNING;
|
||||
auto idle = i->state == PA_SINK_IDLE;
|
||||
spdlog::trace("Sink name {} Running:[{}] Idle:[{}]", i->name, running, idle);
|
||||
|
||||
auto* backend = static_cast<AudioBackend*>(data);
|
||||
|
||||
if (!backend->ignored_sinks_.empty()) {
|
||||
for (const auto &ignored_sink : backend->ignored_sinks_) {
|
||||
for (const auto& ignored_sink : backend->ignored_sinks_) {
|
||||
if (ignored_sink == i->description) {
|
||||
if (i->name == backend->current_sink_name_) {
|
||||
// If the current sink happens to be ignored it is never considered running
|
||||
@@ -162,26 +191,44 @@ void AudioBackend::sinkInfoCb(pa_context * /*context*/, const pa_sink_info *i, i
|
||||
}
|
||||
}
|
||||
|
||||
if (backend->current_sink_name_ == i->name) {
|
||||
backend->current_sink_running_ = i->state == PA_SINK_RUNNING;
|
||||
backend->default_sink_running_ = backend->default_sink_name == i->name &&
|
||||
(i->state == PA_SINK_RUNNING || i->state == PA_SINK_IDLE);
|
||||
|
||||
if (i->name != backend->default_sink_name && i->name != backend->current_sink_name_ &&
|
||||
!backend->default_sink_running_) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (!backend->current_sink_running_ && i->state == PA_SINK_RUNNING) {
|
||||
if (backend->current_sink_name_ == i->name) {
|
||||
backend->current_sink_running_ = (i->state == PA_SINK_RUNNING || i->state == PA_SINK_IDLE);
|
||||
}
|
||||
|
||||
if (!backend->current_sink_running_ &&
|
||||
(i->state == PA_SINK_RUNNING || i->state == PA_SINK_IDLE)) {
|
||||
backend->current_sink_name_ = i->name;
|
||||
backend->current_sink_running_ = true;
|
||||
}
|
||||
|
||||
if (backend->current_sink_name_ == i->name) {
|
||||
backend->pa_volume_ = i->volume;
|
||||
float volume =
|
||||
static_cast<float>(pa_cvolume_avg(&(backend->pa_volume_))) / float{PA_VOLUME_NORM};
|
||||
backend->sink_idx_ = i->index;
|
||||
backend->volume_ = std::round(volume * 100.0F);
|
||||
// Safely copy the volume structure
|
||||
if (pa_cvolume_valid(&i->volume) != 0) {
|
||||
backend->pa_volume_ = i->volume;
|
||||
float volume =
|
||||
static_cast<float>(pa_cvolume_avg(&(backend->pa_volume_))) / float{PA_VOLUME_NORM};
|
||||
backend->sink_idx_ = i->index;
|
||||
backend->volume_ = std::round(volume * 100.0F);
|
||||
} else {
|
||||
spdlog::error("Invalid volume structure received from PulseAudio");
|
||||
// Initialize with safe defaults
|
||||
pa_cvolume_init(&backend->pa_volume_);
|
||||
backend->volume_ = 0;
|
||||
}
|
||||
|
||||
backend->muted_ = i->mute != 0;
|
||||
backend->desc_ = i->description;
|
||||
backend->monitor_ = i->monitor_source_name;
|
||||
backend->port_name_ = i->active_port != nullptr ? i->active_port->name : "Unknown";
|
||||
if (const auto *ff = pa_proplist_gets(i->proplist, PA_PROP_DEVICE_FORM_FACTOR)) {
|
||||
if (const auto* ff = pa_proplist_gets(i->proplist, PA_PROP_DEVICE_FORM_FACTOR)) {
|
||||
backend->form_factor_ = ff;
|
||||
} else {
|
||||
backend->form_factor_ = "";
|
||||
@@ -193,9 +240,9 @@ void AudioBackend::sinkInfoCb(pa_context * /*context*/, const pa_sink_info *i, i
|
||||
/*
|
||||
* Called when the requested source information is ready.
|
||||
*/
|
||||
void AudioBackend::sourceInfoCb(pa_context * /*context*/, const pa_source_info *i, int /*eol*/,
|
||||
void *data) {
|
||||
auto *backend = static_cast<AudioBackend *>(data);
|
||||
void AudioBackend::sourceInfoCb(pa_context* /*context*/, const pa_source_info* i, int /*eol*/,
|
||||
void* data) {
|
||||
auto* backend = static_cast<AudioBackend*>(data);
|
||||
if (i != nullptr && backend->default_source_name_ == i->name) {
|
||||
auto source_volume = static_cast<float>(pa_cvolume_avg(&(i->volume))) / float{PA_VOLUME_NORM};
|
||||
backend->source_volume_ = std::round(source_volume * 100.0F);
|
||||
@@ -211,76 +258,184 @@ void AudioBackend::sourceInfoCb(pa_context * /*context*/, const pa_source_info *
|
||||
* Called when the requested information on the server is ready. This is
|
||||
* used to find the default PulseAudio sink.
|
||||
*/
|
||||
void AudioBackend::serverInfoCb(pa_context *context, const pa_server_info *i, void *data) {
|
||||
auto *backend = static_cast<AudioBackend *>(data);
|
||||
backend->current_sink_name_ = i->default_sink_name;
|
||||
backend->default_source_name_ = i->default_source_name;
|
||||
void AudioBackend::serverInfoCb(pa_context* context, const pa_server_info* i, void* data) {
|
||||
auto* backend = static_cast<AudioBackend*>(data);
|
||||
if (i == nullptr) return;
|
||||
backend->current_sink_name_ = i->default_sink_name ? i->default_sink_name : "";
|
||||
backend->default_sink_name = i->default_sink_name ? i->default_sink_name : "";
|
||||
backend->default_source_name_ = i->default_source_name ? i->default_source_name : "";
|
||||
|
||||
pa_context_get_sink_info_list(context, sinkInfoCb, data);
|
||||
pa_context_get_source_info_list(context, sourceInfoCb, data);
|
||||
}
|
||||
|
||||
uint16_t AudioBackend::getVolume(PulseaudioTarget target) const {
|
||||
if (target == PulseaudioTarget::Source) {
|
||||
return source_volume_;
|
||||
} else {
|
||||
return volume_;
|
||||
}
|
||||
}
|
||||
|
||||
void AudioBackend::changeVolume(uint16_t volume, uint16_t min_volume, uint16_t max_volume) {
|
||||
double volume_tick = static_cast<double>(PA_VOLUME_NORM) / 100;
|
||||
pa_cvolume pa_volume = pa_volume_;
|
||||
// Early return if context is not ready
|
||||
if ((context_ == nullptr) || pa_context_get_state(context_) != PA_CONTEXT_READY) {
|
||||
spdlog::error("PulseAudio context not ready");
|
||||
return;
|
||||
}
|
||||
|
||||
// Prepare volume structure
|
||||
pa_cvolume pa_volume;
|
||||
|
||||
pa_cvolume_init(&pa_volume);
|
||||
|
||||
// Use existing volume structure if valid, otherwise create a safe default
|
||||
if ((pa_cvolume_valid(&pa_volume_) != 0) && (pa_channels_valid(pa_volume_.channels) != 0)) {
|
||||
pa_volume = pa_volume_;
|
||||
} else {
|
||||
// Set stereo as a safe default
|
||||
pa_volume.channels = 2;
|
||||
spdlog::debug("Using default stereo volume structure");
|
||||
}
|
||||
|
||||
// Set the volume safely
|
||||
volume = std::clamp(volume, min_volume, max_volume);
|
||||
pa_cvolume_set(&pa_volume, pa_volume_.channels, volume * volume_tick);
|
||||
pa_volume_t vol = volume * (static_cast<double>(PA_VOLUME_NORM) / 100);
|
||||
|
||||
// Set all channels to the same volume manually to avoid pa_cvolume_set
|
||||
for (uint8_t i = 0; i < pa_volume.channels; i++) {
|
||||
pa_volume.values[i] = vol;
|
||||
}
|
||||
|
||||
// Apply the volume change
|
||||
pa_threaded_mainloop_lock(mainloop_);
|
||||
pa_context_set_sink_volume_by_index(context_, sink_idx_, &pa_volume, volumeModifyCb, this);
|
||||
pa_threaded_mainloop_unlock(mainloop_);
|
||||
}
|
||||
|
||||
void AudioBackend::changeVolume(ChangeType change_type, double step, uint16_t max_volume) {
|
||||
double volume_tick = static_cast<double>(PA_VOLUME_NORM) / 100;
|
||||
pa_volume_t change = volume_tick;
|
||||
pa_cvolume pa_volume = pa_volume_;
|
||||
// Early return if context is not ready
|
||||
if ((context_ == nullptr) || pa_context_get_state(context_) != PA_CONTEXT_READY) {
|
||||
spdlog::error("PulseAudio context not ready");
|
||||
return;
|
||||
}
|
||||
|
||||
// Prepare volume structure
|
||||
pa_cvolume pa_volume;
|
||||
pa_cvolume_init(&pa_volume);
|
||||
|
||||
// Use existing volume structure if valid, otherwise create a safe default
|
||||
if ((pa_cvolume_valid(&pa_volume_) != 0) && (pa_channels_valid(pa_volume_.channels) != 0)) {
|
||||
pa_volume = pa_volume_;
|
||||
} else {
|
||||
// Set stereo as a safe default
|
||||
pa_volume.channels = 2;
|
||||
spdlog::debug("Using default stereo volume structure");
|
||||
|
||||
// Initialize all channels to current volume level
|
||||
double volume_tick = static_cast<double>(PA_VOLUME_NORM) / 100;
|
||||
pa_volume_t vol = volume_ * volume_tick;
|
||||
for (uint8_t i = 0; i < pa_volume.channels; i++) {
|
||||
pa_volume.values[i] = vol;
|
||||
}
|
||||
|
||||
// No need to continue with volume change if we had to create a new structure
|
||||
pa_threaded_mainloop_lock(mainloop_);
|
||||
pa_context_set_sink_volume_by_index(context_, sink_idx_, &pa_volume, volumeModifyCb, this);
|
||||
pa_threaded_mainloop_unlock(mainloop_);
|
||||
return;
|
||||
}
|
||||
|
||||
// Calculate volume change
|
||||
double volume_tick = static_cast<double>(PA_VOLUME_NORM) / 100;
|
||||
pa_volume_t change;
|
||||
max_volume = std::min(max_volume, static_cast<uint16_t>(PA_VOLUME_UI_MAX));
|
||||
|
||||
if (change_type == ChangeType::Increase) {
|
||||
if (volume_ < max_volume) {
|
||||
if (volume_ + step > max_volume) {
|
||||
change = round((max_volume - volume_) * volume_tick);
|
||||
} else {
|
||||
change = round(step * volume_tick);
|
||||
}
|
||||
pa_cvolume_inc(&pa_volume, change);
|
||||
if (change_type == ChangeType::Increase && volume_ < max_volume) {
|
||||
// Calculate how much to increase
|
||||
if (volume_ + step > max_volume) {
|
||||
change = round((max_volume - volume_) * volume_tick);
|
||||
} else {
|
||||
change = round(step * volume_tick);
|
||||
}
|
||||
} else if (change_type == ChangeType::Decrease) {
|
||||
if (volume_ > 0) {
|
||||
if (volume_ - step < 0) {
|
||||
change = round(volume_ * volume_tick);
|
||||
} else {
|
||||
change = round(step * volume_tick);
|
||||
}
|
||||
pa_cvolume_dec(&pa_volume, change);
|
||||
|
||||
// Manually increase each channel's volume
|
||||
for (uint8_t i = 0; i < pa_volume.channels; i++) {
|
||||
pa_volume.values[i] = std::min(pa_volume.values[i] + change, PA_VOLUME_MAX);
|
||||
}
|
||||
} else if (change_type == ChangeType::Decrease && volume_ > 0) {
|
||||
// Calculate how much to decrease
|
||||
if (volume_ - step < 0) {
|
||||
change = round(volume_ * volume_tick);
|
||||
} else {
|
||||
change = round(step * volume_tick);
|
||||
}
|
||||
|
||||
// Manually decrease each channel's volume
|
||||
for (uint8_t i = 0; i < pa_volume.channels; i++) {
|
||||
pa_volume.values[i] = (pa_volume.values[i] > change) ? (pa_volume.values[i] - change) : 0;
|
||||
}
|
||||
} else {
|
||||
// No change needed
|
||||
return;
|
||||
}
|
||||
|
||||
// Apply the volume change
|
||||
pa_threaded_mainloop_lock(mainloop_);
|
||||
pa_context_set_sink_volume_by_index(context_, sink_idx_, &pa_volume, volumeModifyCb, this);
|
||||
pa_threaded_mainloop_unlock(mainloop_);
|
||||
}
|
||||
|
||||
bool AudioBackend::getMuted(PulseaudioTarget target) const {
|
||||
if (target == PulseaudioTarget::Source) {
|
||||
return source_muted_;
|
||||
} else {
|
||||
return muted_;
|
||||
}
|
||||
}
|
||||
|
||||
void AudioBackend::toggleSinkMute() {
|
||||
if (context_ == nullptr || pa_context_get_state(context_) != PA_CONTEXT_READY) return;
|
||||
muted_ = !muted_;
|
||||
pa_threaded_mainloop_lock(mainloop_);
|
||||
pa_context_set_sink_mute_by_index(context_, sink_idx_, static_cast<int>(muted_), nullptr,
|
||||
nullptr);
|
||||
pa_threaded_mainloop_unlock(mainloop_);
|
||||
}
|
||||
|
||||
void AudioBackend::toggleSinkMute(bool mute) {
|
||||
if (context_ == nullptr || pa_context_get_state(context_) != PA_CONTEXT_READY) return;
|
||||
muted_ = mute;
|
||||
pa_threaded_mainloop_lock(mainloop_);
|
||||
pa_context_set_sink_mute_by_index(context_, sink_idx_, static_cast<int>(muted_), nullptr,
|
||||
nullptr);
|
||||
pa_threaded_mainloop_unlock(mainloop_);
|
||||
}
|
||||
|
||||
void AudioBackend::toggleSourceMute() {
|
||||
source_muted_ = !muted_;
|
||||
if (context_ == nullptr || pa_context_get_state(context_) != PA_CONTEXT_READY) return;
|
||||
source_muted_ = !source_muted_;
|
||||
pa_threaded_mainloop_lock(mainloop_);
|
||||
pa_context_set_source_mute_by_index(context_, source_idx_, static_cast<int>(source_muted_),
|
||||
nullptr, nullptr);
|
||||
pa_threaded_mainloop_unlock(mainloop_);
|
||||
}
|
||||
|
||||
void AudioBackend::toggleSourceMute(bool mute) {
|
||||
if (context_ == nullptr || pa_context_get_state(context_) != PA_CONTEXT_READY) return;
|
||||
source_muted_ = mute;
|
||||
pa_threaded_mainloop_lock(mainloop_);
|
||||
pa_context_set_source_mute_by_index(context_, source_idx_, static_cast<int>(source_muted_),
|
||||
nullptr, nullptr);
|
||||
pa_threaded_mainloop_unlock(mainloop_);
|
||||
}
|
||||
|
||||
void AudioBackend::unmute(PulseaudioTarget target) {
|
||||
if (target == PulseaudioTarget::Source) {
|
||||
toggleSourceMute(false);
|
||||
} else {
|
||||
toggleSinkMute(false);
|
||||
}
|
||||
}
|
||||
|
||||
bool AudioBackend::isBluetooth() {
|
||||
@@ -289,9 +444,9 @@ bool AudioBackend::isBluetooth() {
|
||||
monitor_.find("bluez") != std::string::npos;
|
||||
}
|
||||
|
||||
void AudioBackend::setIgnoredSinks(const Json::Value &config) {
|
||||
void AudioBackend::setIgnoredSinks(const Json::Value& config) {
|
||||
if (config.isArray()) {
|
||||
for (const auto &ignored_sink : config) {
|
||||
for (const auto& ignored_sink : config) {
|
||||
if (ignored_sink.isString()) {
|
||||
ignored_sinks_.push_back(ignored_sink.asString());
|
||||
}
|
||||
@@ -299,7 +454,7 @@ void AudioBackend::setIgnoredSinks(const Json::Value &config) {
|
||||
}
|
||||
}
|
||||
|
||||
void AudioBackend::setSinkMapping(const Json::Value &config) {
|
||||
void AudioBackend::setSinkMapping(const Json::Value& config) {
|
||||
if (config.isObject()) {
|
||||
for (auto it = config.begin(); it != config.end(); ++it) {
|
||||
if (it.key().isString() && it->isString()) {
|
||||
|
||||
@@ -8,14 +8,16 @@
|
||||
#include <optional>
|
||||
#include <utility>
|
||||
|
||||
#include "util/udev_deleter.hpp"
|
||||
|
||||
namespace {
|
||||
class FileDescriptor {
|
||||
public:
|
||||
explicit FileDescriptor(int fd) : fd_(fd) {}
|
||||
FileDescriptor(const FileDescriptor &other) = delete;
|
||||
FileDescriptor(FileDescriptor &&other) noexcept = delete;
|
||||
FileDescriptor &operator=(const FileDescriptor &other) = delete;
|
||||
FileDescriptor &operator=(FileDescriptor &&other) noexcept = delete;
|
||||
FileDescriptor(const FileDescriptor& other) = delete;
|
||||
FileDescriptor(FileDescriptor&& other) noexcept = delete;
|
||||
FileDescriptor& operator=(const FileDescriptor& other) = delete;
|
||||
FileDescriptor& operator=(FileDescriptor&& other) noexcept = delete;
|
||||
~FileDescriptor() {
|
||||
if (fd_ != -1) {
|
||||
if (close(fd_) != 0) {
|
||||
@@ -29,43 +31,27 @@ class FileDescriptor {
|
||||
int fd_;
|
||||
};
|
||||
|
||||
struct UdevDeleter {
|
||||
void operator()(udev *ptr) { udev_unref(ptr); }
|
||||
};
|
||||
|
||||
struct UdevDeviceDeleter {
|
||||
void operator()(udev_device *ptr) { udev_device_unref(ptr); }
|
||||
};
|
||||
|
||||
struct UdevEnumerateDeleter {
|
||||
void operator()(udev_enumerate *ptr) { udev_enumerate_unref(ptr); }
|
||||
};
|
||||
|
||||
struct UdevMonitorDeleter {
|
||||
void operator()(udev_monitor *ptr) { udev_monitor_unref(ptr); }
|
||||
};
|
||||
|
||||
void check_eq(int rc, int expected, const char *message = "eq, rc was: ") {
|
||||
void check_eq(int rc, int expected, const char* message = "eq, rc was: ") {
|
||||
if (rc != expected) {
|
||||
throw std::runtime_error(fmt::format(fmt::runtime(message), rc));
|
||||
}
|
||||
}
|
||||
|
||||
void check_neq(int rc, int bad_rc, const char *message = "neq, rc was: ") {
|
||||
void check_neq(int rc, int bad_rc, const char* message = "neq, rc was: ") {
|
||||
if (rc == bad_rc) {
|
||||
throw std::runtime_error(fmt::format(fmt::runtime(message), rc));
|
||||
}
|
||||
}
|
||||
|
||||
void check0(int rc, const char *message = "rc wasn't 0") { check_eq(rc, 0, message); }
|
||||
void check0(int rc, const char* message = "rc wasn't 0") { check_eq(rc, 0, message); }
|
||||
|
||||
void check_gte(int rc, int gte, const char *message = "rc was: ") {
|
||||
void check_gte(int rc, int gte, const char* message = "rc was: ") {
|
||||
if (rc < gte) {
|
||||
throw std::runtime_error(fmt::format(fmt::runtime(message), rc));
|
||||
}
|
||||
}
|
||||
|
||||
void check_nn(const void *ptr, const char *message = "ptr was null") {
|
||||
void check_nn(const void* ptr, const char* message = "ptr was null") {
|
||||
if (ptr == nullptr) {
|
||||
throw std::runtime_error(message);
|
||||
}
|
||||
@@ -75,48 +61,68 @@ void check_nn(const void *ptr, const char *message = "ptr was null") {
|
||||
|
||||
namespace waybar::util {
|
||||
|
||||
static void upsert_device(std::vector<BacklightDevice> &devices, udev_device *dev) {
|
||||
const char *name = udev_device_get_sysname(dev);
|
||||
static void upsert_device(std::vector<BacklightDevice>& devices, udev_device* dev) {
|
||||
const char* name = udev_device_get_sysname(dev);
|
||||
check_nn(name);
|
||||
|
||||
const char *actual_brightness_attr =
|
||||
const char* actual_brightness_attr =
|
||||
strncmp(name, "amdgpu_bl", 9) == 0 || strcmp(name, "apple-panel-bl") == 0
|
||||
? "brightness"
|
||||
: "actual_brightness";
|
||||
|
||||
const char *actual = udev_device_get_sysattr_value(dev, actual_brightness_attr);
|
||||
const char *max = udev_device_get_sysattr_value(dev, "max_brightness");
|
||||
const char *power = udev_device_get_sysattr_value(dev, "bl_power");
|
||||
const char* actual = udev_device_get_sysattr_value(dev, actual_brightness_attr);
|
||||
const char* max = udev_device_get_sysattr_value(dev, "max_brightness");
|
||||
const char* power = udev_device_get_sysattr_value(dev, "bl_power");
|
||||
|
||||
auto found = std::find_if(devices.begin(), devices.end(), [name](const BacklightDevice &device) {
|
||||
auto found = std::find_if(devices.begin(), devices.end(), [name](const BacklightDevice& device) {
|
||||
return device.name() == name;
|
||||
});
|
||||
if (found != devices.end()) {
|
||||
if (actual != nullptr) {
|
||||
found->set_actual(std::stoi(actual));
|
||||
try {
|
||||
found->set_actual(std::stoi(actual));
|
||||
} catch (const std::exception&) {
|
||||
}
|
||||
}
|
||||
if (max != nullptr) {
|
||||
found->set_max(std::stoi(max));
|
||||
try {
|
||||
found->set_max(std::stoi(max));
|
||||
} catch (const std::exception&) {
|
||||
}
|
||||
}
|
||||
if (power != nullptr) {
|
||||
found->set_powered(std::stoi(power) == 0);
|
||||
try {
|
||||
found->set_powered(std::stoi(power) == 0);
|
||||
} catch (const std::exception&) {
|
||||
}
|
||||
}
|
||||
} else {
|
||||
const int actual_int = actual == nullptr ? 0 : std::stoi(actual);
|
||||
const int max_int = max == nullptr ? 0 : std::stoi(max);
|
||||
const bool power_bool = power == nullptr ? true : std::stoi(power) == 0;
|
||||
int actual_int = 0, max_int = 0;
|
||||
bool power_bool = true;
|
||||
try {
|
||||
if (actual != nullptr) actual_int = std::stoi(actual);
|
||||
} catch (const std::exception&) {
|
||||
}
|
||||
try {
|
||||
if (max != nullptr) max_int = std::stoi(max);
|
||||
} catch (const std::exception&) {
|
||||
}
|
||||
try {
|
||||
if (power != nullptr) power_bool = std::stoi(power) == 0;
|
||||
} catch (const std::exception&) {
|
||||
}
|
||||
devices.emplace_back(name, actual_int, max_int, power_bool);
|
||||
}
|
||||
}
|
||||
|
||||
static void enumerate_devices(std::vector<BacklightDevice> &devices, udev *udev) {
|
||||
static void enumerate_devices(std::vector<BacklightDevice>& devices, udev* udev) {
|
||||
std::unique_ptr<udev_enumerate, UdevEnumerateDeleter> enumerate{udev_enumerate_new(udev)};
|
||||
udev_enumerate_add_match_subsystem(enumerate.get(), "backlight");
|
||||
udev_enumerate_scan_devices(enumerate.get());
|
||||
udev_list_entry *enum_devices = udev_enumerate_get_list_entry(enumerate.get());
|
||||
udev_list_entry *dev_list_entry;
|
||||
udev_list_entry* enum_devices = udev_enumerate_get_list_entry(enumerate.get());
|
||||
udev_list_entry* dev_list_entry;
|
||||
udev_list_entry_foreach(dev_list_entry, enum_devices) {
|
||||
const char *path = udev_list_entry_get_name(dev_list_entry);
|
||||
const char* path = udev_list_entry_get_name(dev_list_entry);
|
||||
std::unique_ptr<udev_device, UdevDeviceDeleter> dev{udev_device_new_from_syspath(udev, path)};
|
||||
check_nn(dev.get(), "dev new failed");
|
||||
upsert_device(devices, dev.get());
|
||||
@@ -150,10 +156,18 @@ BacklightBackend::BacklightBackend(std::chrono::milliseconds interval,
|
||||
throw std::runtime_error("No backlight found");
|
||||
}
|
||||
|
||||
#ifdef HAVE_LOGIN_PROXY
|
||||
// Connect to the login interface
|
||||
login_proxy_ = Gio::DBus::Proxy::create_for_bus_sync(
|
||||
Gio::DBus::BusType::BUS_TYPE_SYSTEM, "org.freedesktop.login1",
|
||||
"/org/freedesktop/login1/session/self", "org.freedesktop.login1.Session");
|
||||
"/org/freedesktop/login1/session/auto", "org.freedesktop.login1.Session");
|
||||
|
||||
if (!login_proxy_) {
|
||||
login_proxy_ = Gio::DBus::Proxy::create_for_bus_sync(
|
||||
Gio::DBus::BusType::BUS_TYPE_SYSTEM, "org.freedesktop.login1",
|
||||
"/org/freedesktop/login1/session/self", "org.freedesktop.login1.Session");
|
||||
}
|
||||
#endif
|
||||
|
||||
udev_thread_ = [this] {
|
||||
std::unique_ptr<udev, UdevDeleter> udev{udev_new()};
|
||||
@@ -190,10 +204,12 @@ BacklightBackend::BacklightBackend(std::chrono::milliseconds interval,
|
||||
devices = devices_;
|
||||
}
|
||||
for (int i = 0; i < event_count; ++i) {
|
||||
const auto &event = events[i];
|
||||
const auto& event = events[i];
|
||||
check_eq(event.data.fd, udev_fd, "unexpected udev fd");
|
||||
std::unique_ptr<udev_device, UdevDeviceDeleter> dev{udev_monitor_receive_device(mon.get())};
|
||||
check_nn(dev.get(), "epoll dev was null");
|
||||
if (!dev) {
|
||||
continue;
|
||||
}
|
||||
upsert_device(devices, dev.get());
|
||||
}
|
||||
|
||||
@@ -210,27 +226,27 @@ BacklightBackend::BacklightBackend(std::chrono::milliseconds interval,
|
||||
};
|
||||
}
|
||||
|
||||
const BacklightDevice *BacklightBackend::best_device(const std::vector<BacklightDevice> &devices,
|
||||
const BacklightDevice* BacklightBackend::best_device(const std::vector<BacklightDevice>& devices,
|
||||
std::string_view preferred_device) {
|
||||
const auto found = std::find_if(
|
||||
devices.begin(), devices.end(),
|
||||
[preferred_device](const BacklightDevice &dev) { return dev.name() == preferred_device; });
|
||||
[preferred_device](const BacklightDevice& dev) { return dev.name() == preferred_device; });
|
||||
if (found != devices.end()) {
|
||||
return &(*found);
|
||||
}
|
||||
|
||||
const auto max = std::max_element(
|
||||
devices.begin(), devices.end(),
|
||||
[](const BacklightDevice &l, const BacklightDevice &r) { return l.get_max() < r.get_max(); });
|
||||
[](const BacklightDevice& l, const BacklightDevice& r) { return l.get_max() < r.get_max(); });
|
||||
|
||||
return max == devices.end() ? nullptr : &(*max);
|
||||
}
|
||||
|
||||
const BacklightDevice *BacklightBackend::get_previous_best_device() {
|
||||
const BacklightDevice* BacklightBackend::get_previous_best_device() {
|
||||
return previous_best_.has_value() ? &(*previous_best_) : nullptr;
|
||||
}
|
||||
|
||||
void BacklightBackend::set_previous_best_device(const BacklightDevice *device) {
|
||||
void BacklightBackend::set_previous_best_device(const BacklightDevice* device) {
|
||||
if (device == nullptr) {
|
||||
previous_best_ = std::nullopt;
|
||||
} else {
|
||||
@@ -238,7 +254,7 @@ void BacklightBackend::set_previous_best_device(const BacklightDevice *device) {
|
||||
}
|
||||
}
|
||||
|
||||
void BacklightBackend::set_scaled_brightness(const std::string &preferred_device, int brightness) {
|
||||
void BacklightBackend::set_scaled_brightness(const std::string& preferred_device, int brightness) {
|
||||
GET_BEST_DEVICE(best, (*this), preferred_device);
|
||||
|
||||
if (best != nullptr) {
|
||||
@@ -248,7 +264,7 @@ void BacklightBackend::set_scaled_brightness(const std::string &preferred_device
|
||||
}
|
||||
}
|
||||
|
||||
void BacklightBackend::set_brightness(const std::string &preferred_device, ChangeType change_type,
|
||||
void BacklightBackend::set_brightness(const std::string& preferred_device, ChangeType change_type,
|
||||
double step) {
|
||||
GET_BEST_DEVICE(best, (*this), preferred_device);
|
||||
|
||||
@@ -263,8 +279,13 @@ void BacklightBackend::set_brightness(const std::string &preferred_device, Chang
|
||||
}
|
||||
}
|
||||
|
||||
void BacklightBackend::set_brightness_internal(const std::string &device_name, int brightness,
|
||||
void BacklightBackend::set_brightness_internal(const std::string& device_name, int brightness,
|
||||
int max_brightness) {
|
||||
if (!login_proxy_) {
|
||||
spdlog::error("Login proxy not available, cannot set brightness");
|
||||
return;
|
||||
}
|
||||
|
||||
brightness = std::clamp(brightness, 0, max_brightness);
|
||||
|
||||
auto call_args = Glib::VariantContainerBase(
|
||||
@@ -273,11 +294,12 @@ void BacklightBackend::set_brightness_internal(const std::string &device_name, i
|
||||
login_proxy_->call_sync("SetBrightness", call_args);
|
||||
}
|
||||
|
||||
int BacklightBackend::get_scaled_brightness(const std::string &preferred_device) {
|
||||
int BacklightBackend::get_scaled_brightness(const std::string& preferred_device) {
|
||||
GET_BEST_DEVICE(best, (*this), preferred_device);
|
||||
|
||||
if (best != nullptr) {
|
||||
return best->get_actual() * 100 / best->get_max();
|
||||
if (best->get_max() == 0) return 0;
|
||||
return static_cast<int>(std::round(best->get_actual() * 100.0F / best->get_max()));
|
||||
}
|
||||
|
||||
return 0;
|
||||
|
||||
@@ -2,7 +2,13 @@
|
||||
|
||||
#include <poll.h>
|
||||
#include <spdlog/spdlog.h>
|
||||
#ifndef __OpenBSD__
|
||||
#include <sys/inotify.h>
|
||||
#else
|
||||
#include <sys/event.h>
|
||||
#include <sys/time.h>
|
||||
#include <sys/types.h>
|
||||
#endif
|
||||
|
||||
#include <filesystem>
|
||||
#include <fstream>
|
||||
@@ -17,7 +23,8 @@ namespace {
|
||||
const std::regex IMPORT_REGEX(R"(@import\s+(?:url\()?(?:"|')([^"')]+)(?:"|')\)?;)");
|
||||
}
|
||||
|
||||
waybar::CssReloadHelper::CssReloadHelper(std::string cssFile, std::function<void()> callback)
|
||||
waybar::CssReloadHelper::CssReloadHelper(std::string cssFile,
|
||||
std::function<void(const std::string&)> callback)
|
||||
: m_cssFile(std::move(cssFile)), m_callback(std::move(callback)) {}
|
||||
|
||||
std::string waybar::CssReloadHelper::getFileContents(const std::string& filename) {
|
||||
@@ -82,6 +89,12 @@ void waybar::CssReloadHelper::monitorChanges() {
|
||||
}
|
||||
}
|
||||
|
||||
void waybar::CssReloadHelper::changeCssFile(const std::string& newCssFile) {
|
||||
m_fileMonitors.clear();
|
||||
m_cssFile = newCssFile;
|
||||
monitorChanges();
|
||||
}
|
||||
|
||||
void waybar::CssReloadHelper::handleFileChange(Glib::RefPtr<Gio::File> const& file,
|
||||
Glib::RefPtr<Gio::File> const& other_type,
|
||||
Gio::FileMonitorEvent event_type) {
|
||||
@@ -89,7 +102,7 @@ void waybar::CssReloadHelper::handleFileChange(Glib::RefPtr<Gio::File> const& fi
|
||||
// fire for one
|
||||
if (event_type == Gio::FileMonitorEvent::FILE_MONITOR_EVENT_CHANGES_DONE_HINT) {
|
||||
spdlog::debug("Reloading style, file changed: {}", file->get_path());
|
||||
m_callback();
|
||||
m_callback(m_cssFile);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -109,11 +122,15 @@ std::vector<std::string> waybar::CssReloadHelper::parseImports(const std::string
|
||||
auto maxIterations = 100U;
|
||||
do {
|
||||
previousSize = imports.size();
|
||||
std::vector<std::string> to_parse;
|
||||
for (const auto& [file, parsed] : imports) {
|
||||
if (!parsed) {
|
||||
parseImports(file, imports);
|
||||
to_parse.push_back(file);
|
||||
}
|
||||
}
|
||||
for (const auto& file : to_parse) {
|
||||
parseImports(file, imports);
|
||||
}
|
||||
|
||||
} while (imports.size() > previousSize && maxIterations-- > 0);
|
||||
|
||||
|
||||
@@ -41,5 +41,7 @@ EnumType EnumParser<EnumType>::parseStringToEnum(const std::string& str,
|
||||
// Explicit instantiations for specific EnumType types you intend to use
|
||||
// Add explicit instantiations for all relevant EnumType types
|
||||
template struct EnumParser<modules::hyprland::Workspaces::SortMethod>;
|
||||
template struct EnumParser<modules::hyprland::Workspaces::ActiveWindowPosition>;
|
||||
template struct EnumParser<util::KillSignalAction>;
|
||||
|
||||
} // namespace waybar::util
|
||||
|
||||
+16
-4
@@ -15,11 +15,23 @@ bool DefaultGtkIconThemeWrapper::has_icon(const std::string& value) {
|
||||
return Gtk::IconTheme::get_default()->has_icon(value);
|
||||
}
|
||||
|
||||
Glib::RefPtr<Gdk::Pixbuf> DefaultGtkIconThemeWrapper::load_icon(const char* name, int tmp_size,
|
||||
Gtk::IconLookupFlags flags) {
|
||||
Glib::RefPtr<Gdk::Pixbuf> DefaultGtkIconThemeWrapper::load_icon(
|
||||
const char* name, int tmp_size, Gtk::IconLookupFlags flags,
|
||||
Glib::RefPtr<Gtk::StyleContext> style) {
|
||||
const std::lock_guard<std::mutex> lock(default_theme_mutex);
|
||||
|
||||
auto default_theme = Gtk::IconTheme::get_default();
|
||||
default_theme->rescan_if_needed();
|
||||
return default_theme->load_icon(name, tmp_size, flags);
|
||||
|
||||
auto icon_info = default_theme->lookup_icon(name, tmp_size, flags);
|
||||
|
||||
if (icon_info == nullptr) {
|
||||
return default_theme->load_icon(name, tmp_size, flags);
|
||||
}
|
||||
|
||||
if (style.get() == nullptr) {
|
||||
return icon_info.load_icon();
|
||||
}
|
||||
|
||||
bool is_sym = false;
|
||||
return icon_info.load_symbolic(style, is_sym);
|
||||
}
|
||||
|
||||
@@ -0,0 +1,210 @@
|
||||
#include "util/icon_loader.hpp"
|
||||
|
||||
#include "util/string.hpp"
|
||||
|
||||
std::vector<std::string> IconLoader::search_prefix() {
|
||||
std::vector<std::string> prefixes = {""};
|
||||
|
||||
const char* home_env = std::getenv("HOME");
|
||||
std::string home_dir = home_env ? home_env : "";
|
||||
if (!home_dir.empty()) {
|
||||
prefixes.push_back(home_dir + "/.local/share/");
|
||||
}
|
||||
|
||||
auto xdg_data_dirs = std::getenv("XDG_DATA_DIRS");
|
||||
if (!xdg_data_dirs) {
|
||||
prefixes.emplace_back("/usr/share/");
|
||||
prefixes.emplace_back("/usr/local/share/");
|
||||
} else {
|
||||
std::string xdg_data_dirs_str(xdg_data_dirs);
|
||||
size_t start = 0;
|
||||
size_t end = 0;
|
||||
|
||||
do {
|
||||
end = xdg_data_dirs_str.find(':', start);
|
||||
auto p = xdg_data_dirs_str.substr(start, end - start);
|
||||
prefixes.push_back(trim(p) + "/");
|
||||
|
||||
start = end == std::string::npos ? end : end + 1;
|
||||
} while (end != std::string::npos);
|
||||
}
|
||||
|
||||
for (auto& p : prefixes) spdlog::debug("Using 'desktop' search path prefix: {}", p);
|
||||
|
||||
return prefixes;
|
||||
}
|
||||
|
||||
Glib::RefPtr<Gio::DesktopAppInfo> IconLoader::get_app_info_by_name(const std::string& app_id) {
|
||||
static std::vector<std::string> prefixes = search_prefix();
|
||||
|
||||
std::vector<std::string> app_folders = {"", "applications/", "applications/kde/",
|
||||
"applications/org.kde."};
|
||||
|
||||
std::vector<std::string> suffixes = {"", ".desktop"};
|
||||
|
||||
for (auto const& prefix : prefixes) {
|
||||
for (auto const& folder : app_folders) {
|
||||
for (auto const& suffix : suffixes) {
|
||||
auto app_info_ =
|
||||
Gio::DesktopAppInfo::create_from_filename(prefix + folder + app_id + suffix);
|
||||
if (!app_info_) {
|
||||
continue;
|
||||
}
|
||||
|
||||
return app_info_;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return {};
|
||||
}
|
||||
|
||||
Glib::RefPtr<Gio::DesktopAppInfo> IconLoader::get_desktop_app_info(const std::string& app_id) {
|
||||
auto app_info = get_app_info_by_name(app_id);
|
||||
if (app_info) {
|
||||
return app_info;
|
||||
}
|
||||
|
||||
std::string desktop_file = "";
|
||||
|
||||
gchar*** desktop_list = g_desktop_app_info_search(app_id.c_str());
|
||||
if (desktop_list != nullptr && desktop_list[0] != nullptr) {
|
||||
for (size_t i = 0; desktop_list[0][i]; i++) {
|
||||
if (desktop_file == "") {
|
||||
desktop_file = desktop_list[0][i];
|
||||
} else {
|
||||
auto tmp_info = Gio::DesktopAppInfo::create(desktop_list[0][i]);
|
||||
if (!tmp_info)
|
||||
// see https://github.com/Alexays/Waybar/issues/1446
|
||||
continue;
|
||||
|
||||
auto startup_class = tmp_info->get_startup_wm_class();
|
||||
if (startup_class == app_id) {
|
||||
desktop_file = desktop_list[0][i];
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
g_strfreev(desktop_list[0]);
|
||||
}
|
||||
g_free(desktop_list);
|
||||
|
||||
return get_app_info_by_name(desktop_file);
|
||||
}
|
||||
|
||||
Glib::RefPtr<Gdk::Pixbuf> IconLoader::load_icon_from_file(std::string const& icon_path, int size) {
|
||||
try {
|
||||
auto pb = Gdk::Pixbuf::create_from_file(icon_path, size, size);
|
||||
return pb;
|
||||
} catch (...) {
|
||||
return {};
|
||||
}
|
||||
}
|
||||
|
||||
std::string IconLoader::get_icon_name_from_icon_theme(
|
||||
const Glib::RefPtr<Gtk::IconTheme>& icon_theme, const std::string& app_id) {
|
||||
if (icon_theme->lookup_icon(app_id, 24)) return app_id;
|
||||
|
||||
return "";
|
||||
}
|
||||
|
||||
bool IconLoader::image_load_icon(Gtk::Image& image, const Glib::RefPtr<Gtk::IconTheme>& icon_theme,
|
||||
Glib::RefPtr<Gio::DesktopAppInfo> app_info, int size) {
|
||||
std::string ret_icon_name = "unknown";
|
||||
if (app_info) {
|
||||
std::string icon_name =
|
||||
get_icon_name_from_icon_theme(icon_theme, app_info->get_startup_wm_class());
|
||||
if (!icon_name.empty()) {
|
||||
ret_icon_name = icon_name;
|
||||
} else {
|
||||
if (app_info->get_icon()) {
|
||||
ret_icon_name = app_info->get_icon()->to_string();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Glib::RefPtr<Gdk::Pixbuf> pixbuf;
|
||||
auto scaled_icon_size = size * image.get_scale_factor();
|
||||
|
||||
try {
|
||||
pixbuf = icon_theme->load_icon(ret_icon_name, scaled_icon_size, Gtk::ICON_LOOKUP_FORCE_SIZE);
|
||||
} catch (...) {
|
||||
if (Glib::file_test(ret_icon_name, Glib::FILE_TEST_EXISTS)) {
|
||||
pixbuf = load_icon_from_file(ret_icon_name, scaled_icon_size);
|
||||
} else {
|
||||
try {
|
||||
pixbuf = DefaultGtkIconThemeWrapper::load_icon(
|
||||
"image-missing", scaled_icon_size, Gtk::IconLookupFlags::ICON_LOOKUP_FORCE_SIZE);
|
||||
} catch (...) {
|
||||
pixbuf = {};
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (pixbuf) {
|
||||
if (pixbuf->get_width() != scaled_icon_size && pixbuf->get_height() > 0) {
|
||||
int width = scaled_icon_size * pixbuf->get_width() / pixbuf->get_height();
|
||||
pixbuf = pixbuf->scale_simple(width, scaled_icon_size, Gdk::InterpType::INTERP_BILINEAR);
|
||||
}
|
||||
auto surface = Gdk::Cairo::create_surface_from_pixbuf(pixbuf, image.get_scale_factor(),
|
||||
image.get_window());
|
||||
image.set(surface);
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
void IconLoader::add_custom_icon_theme(const std::string& theme_name) {
|
||||
auto icon_theme = Gtk::IconTheme::create();
|
||||
icon_theme->set_custom_theme(theme_name);
|
||||
custom_icon_themes_.push_back(icon_theme);
|
||||
spdlog::debug("Use custom icon theme: {}", theme_name);
|
||||
}
|
||||
|
||||
bool IconLoader::image_load_icon(Gtk::Image& image, Glib::RefPtr<Gio::DesktopAppInfo> app_info,
|
||||
int size) const {
|
||||
for (auto& icon_theme : custom_icon_themes_) {
|
||||
if (image_load_icon(image, icon_theme, app_info, size)) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return image_load_icon(image, default_icon_theme_, app_info, size);
|
||||
}
|
||||
|
||||
Glib::RefPtr<Gio::DesktopAppInfo> IconLoader::get_app_info_from_app_id_list(
|
||||
const std::string& app_id_list) {
|
||||
std::string app_id;
|
||||
std::istringstream stream(app_id_list);
|
||||
Glib::RefPtr<Gio::DesktopAppInfo> app_info_;
|
||||
|
||||
/* Wayfire sends a list of app-id's in space separated format, other compositors
|
||||
* send a single app-id, but in any case this works fine */
|
||||
while (stream >> app_id) {
|
||||
app_info_ = get_desktop_app_info(app_id);
|
||||
if (app_info_) {
|
||||
return app_info_;
|
||||
}
|
||||
|
||||
auto lower_app_id = app_id;
|
||||
std::ranges::transform(lower_app_id, lower_app_id.begin(),
|
||||
[](char c) { return std::tolower(c); });
|
||||
app_info_ = get_desktop_app_info(lower_app_id);
|
||||
if (app_info_) {
|
||||
return app_info_;
|
||||
}
|
||||
|
||||
size_t start = 0, end = app_id.size();
|
||||
start = app_id.rfind(".", end);
|
||||
std::string app_name = app_id.substr(start + 1, app_id.size());
|
||||
app_info_ = get_desktop_app_info(app_name);
|
||||
if (app_info_) {
|
||||
return app_info_;
|
||||
}
|
||||
|
||||
start = app_id.find("-");
|
||||
app_name = app_id.substr(0, start);
|
||||
app_info_ = get_desktop_app_info(app_name);
|
||||
}
|
||||
return app_info_;
|
||||
}
|
||||
@@ -4,11 +4,11 @@
|
||||
|
||||
namespace waybar::util::PipewireBackend {
|
||||
|
||||
static void getNodeInfo(void *data_, const struct pw_node_info *info) {
|
||||
auto *pNodeInfo = static_cast<PrivacyNodeInfo *>(data_);
|
||||
static void getNodeInfo(void* data_, const struct pw_node_info* info) {
|
||||
auto* pNodeInfo = static_cast<PrivacyNodeInfo*>(data_);
|
||||
pNodeInfo->handleNodeEventInfo(info);
|
||||
|
||||
static_cast<PipewireBackend *>(pNodeInfo->data)->privacy_nodes_changed_signal_event.emit();
|
||||
static_cast<PipewireBackend*>(pNodeInfo->data)->privacy_nodes_changed_signal_event.emit();
|
||||
}
|
||||
|
||||
static const struct pw_node_events NODE_EVENTS = {
|
||||
@@ -16,8 +16,8 @@ static const struct pw_node_events NODE_EVENTS = {
|
||||
.info = getNodeInfo,
|
||||
};
|
||||
|
||||
static void proxyDestroy(void *data) {
|
||||
static_cast<PrivacyNodeInfo *>(data)->handleProxyEventDestroy();
|
||||
static void proxyDestroy(void* data) {
|
||||
static_cast<PrivacyNodeInfo*>(data)->handleProxyEventDestroy();
|
||||
}
|
||||
|
||||
static const struct pw_proxy_events PROXY_EVENTS = {
|
||||
@@ -25,14 +25,14 @@ static const struct pw_proxy_events PROXY_EVENTS = {
|
||||
.destroy = proxyDestroy,
|
||||
};
|
||||
|
||||
static void registryEventGlobal(void *_data, uint32_t id, uint32_t permissions, const char *type,
|
||||
uint32_t version, const struct spa_dict *props) {
|
||||
static_cast<PipewireBackend *>(_data)->handleRegistryEventGlobal(id, permissions, type, version,
|
||||
props);
|
||||
static void registryEventGlobal(void* _data, uint32_t id, uint32_t permissions, const char* type,
|
||||
uint32_t version, const struct spa_dict* props) {
|
||||
static_cast<PipewireBackend*>(_data)->handleRegistryEventGlobal(id, permissions, type, version,
|
||||
props);
|
||||
}
|
||||
|
||||
static void registryEventGlobalRemove(void *_data, uint32_t id) {
|
||||
static_cast<PipewireBackend *>(_data)->handleRegistryEventGlobalRemove(id);
|
||||
static void registryEventGlobalRemove(void* _data, uint32_t id) {
|
||||
static_cast<PipewireBackend*>(_data)->handleRegistryEventGlobalRemove(id);
|
||||
}
|
||||
|
||||
static const struct pw_registry_events REGISTRY_EVENTS = {
|
||||
@@ -54,11 +54,17 @@ PipewireBackend::PipewireBackend(PrivateConstructorTag tag)
|
||||
context_ = pw_context_new(pw_thread_loop_get_loop(mainloop_), nullptr, 0);
|
||||
if (context_ == nullptr) {
|
||||
pw_thread_loop_unlock(mainloop_);
|
||||
pw_thread_loop_destroy(mainloop_);
|
||||
mainloop_ = nullptr;
|
||||
throw std::runtime_error("pa_context_new() failed.");
|
||||
}
|
||||
core_ = pw_context_connect(context_, nullptr, 0);
|
||||
if (core_ == nullptr) {
|
||||
pw_thread_loop_unlock(mainloop_);
|
||||
pw_context_destroy(context_);
|
||||
context_ = nullptr;
|
||||
pw_thread_loop_destroy(mainloop_);
|
||||
mainloop_ = nullptr;
|
||||
throw std::runtime_error("pw_context_connect() failed");
|
||||
}
|
||||
registry_ = pw_core_get_registry(core_, PW_VERSION_REGISTRY, 0);
|
||||
@@ -78,7 +84,7 @@ PipewireBackend::~PipewireBackend() {
|
||||
}
|
||||
|
||||
if (registry_ != nullptr) {
|
||||
pw_proxy_destroy((struct pw_proxy *)registry_);
|
||||
pw_proxy_destroy((struct pw_proxy*)registry_);
|
||||
}
|
||||
|
||||
spa_zero(registryListener_);
|
||||
@@ -103,11 +109,11 @@ std::shared_ptr<PipewireBackend> PipewireBackend::getInstance() {
|
||||
return std::make_shared<PipewireBackend>(tag);
|
||||
}
|
||||
|
||||
void PipewireBackend::handleRegistryEventGlobal(uint32_t id, uint32_t permissions, const char *type,
|
||||
uint32_t version, const struct spa_dict *props) {
|
||||
void PipewireBackend::handleRegistryEventGlobal(uint32_t id, uint32_t permissions, const char* type,
|
||||
uint32_t version, const struct spa_dict* props) {
|
||||
if (props == nullptr || strcmp(type, PW_TYPE_INTERFACE_Node) != 0) return;
|
||||
|
||||
const char *lookupStr = spa_dict_lookup(props, PW_KEY_MEDIA_CLASS);
|
||||
const char* lookupStr = spa_dict_lookup(props, PW_KEY_MEDIA_CLASS);
|
||||
if (lookupStr == nullptr) return;
|
||||
std::string mediaClass = lookupStr;
|
||||
enum PrivacyNodeType mediaType = PRIVACY_NODE_TYPE_NONE;
|
||||
@@ -121,11 +127,12 @@ void PipewireBackend::handleRegistryEventGlobal(uint32_t id, uint32_t permission
|
||||
return;
|
||||
}
|
||||
|
||||
auto *proxy = (pw_proxy *)pw_registry_bind(registry_, id, type, version, sizeof(PrivacyNodeInfo));
|
||||
auto* proxy = (pw_proxy*)pw_registry_bind(registry_, id, type, version, sizeof(PrivacyNodeInfo));
|
||||
|
||||
if (proxy == nullptr) return;
|
||||
|
||||
auto *pNodeInfo = (PrivacyNodeInfo *)pw_proxy_get_user_data(proxy);
|
||||
auto* pNodeInfo = (PrivacyNodeInfo*)pw_proxy_get_user_data(proxy);
|
||||
new (pNodeInfo) PrivacyNodeInfo{};
|
||||
pNodeInfo->id = id;
|
||||
pNodeInfo->data = this;
|
||||
pNodeInfo->type = mediaType;
|
||||
@@ -135,13 +142,17 @@ void PipewireBackend::handleRegistryEventGlobal(uint32_t id, uint32_t permission
|
||||
|
||||
pw_proxy_add_object_listener(proxy, &pNodeInfo->object_listener, &NODE_EVENTS, pNodeInfo);
|
||||
|
||||
privacy_nodes.insert_or_assign(id, pNodeInfo);
|
||||
{
|
||||
std::lock_guard<std::mutex> lock(mutex_);
|
||||
privacy_nodes.insert_or_assign(id, pNodeInfo);
|
||||
}
|
||||
}
|
||||
|
||||
void PipewireBackend::handleRegistryEventGlobalRemove(uint32_t id) {
|
||||
mutex_.lock();
|
||||
auto iter = privacy_nodes.find(id);
|
||||
if (iter != privacy_nodes.end()) {
|
||||
privacy_nodes[id]->~PrivacyNodeInfo();
|
||||
privacy_nodes.erase(id);
|
||||
}
|
||||
mutex_.unlock();
|
||||
|
||||
@@ -3,9 +3,9 @@
|
||||
namespace waybar::util::PipewireBackend {
|
||||
|
||||
std::string PrivacyNodeInfo::getName() {
|
||||
const std::vector<std::string *> names{&application_name, &node_name};
|
||||
const std::vector<std::string*> names{&application_name, &node_name};
|
||||
std::string name = "Unknown Application";
|
||||
for (const auto &item : names) {
|
||||
for (const auto& item : names) {
|
||||
if (item != nullptr && !item->empty()) {
|
||||
name = *item;
|
||||
name[0] = toupper(name[0]);
|
||||
@@ -16,10 +16,10 @@ std::string PrivacyNodeInfo::getName() {
|
||||
}
|
||||
|
||||
std::string PrivacyNodeInfo::getIconName() {
|
||||
const std::vector<std::string *> names{&application_icon_name, &pipewire_access_portal_app_id,
|
||||
&application_name, &node_name};
|
||||
const std::vector<std::string*> names{&application_icon_name, &pipewire_access_portal_app_id,
|
||||
&application_name, &node_name};
|
||||
std::string name = "application-x-executable-symbolic";
|
||||
for (const auto &item : names) {
|
||||
for (const auto& item : names) {
|
||||
if (item != nullptr && !item->empty() && DefaultGtkIconThemeWrapper::has_icon(*item)) {
|
||||
return *item;
|
||||
}
|
||||
@@ -32,10 +32,10 @@ void PrivacyNodeInfo::handleProxyEventDestroy() {
|
||||
spa_hook_remove(&object_listener);
|
||||
}
|
||||
|
||||
void PrivacyNodeInfo::handleNodeEventInfo(const struct pw_node_info *info) {
|
||||
void PrivacyNodeInfo::handleNodeEventInfo(const struct pw_node_info* info) {
|
||||
state = info->state;
|
||||
|
||||
const struct spa_dict_item *item;
|
||||
const struct spa_dict_item* item;
|
||||
spa_dict_for_each(item, info->props) {
|
||||
if (strcmp(item->key, PW_KEY_CLIENT_ID) == 0) {
|
||||
client_id = strtoul(item->value, nullptr, 10);
|
||||
@@ -49,6 +49,8 @@ void PrivacyNodeInfo::handleNodeEventInfo(const struct pw_node_info *info) {
|
||||
pipewire_access_portal_app_id = item->value;
|
||||
} else if (strcmp(item->key, PW_KEY_APP_ICON_NAME) == 0) {
|
||||
application_icon_name = item->value;
|
||||
} else if (strcmp(item->key, "stream.monitor") == 0) {
|
||||
is_monitor = strcmp(item->value, "true") == 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
+21
-17
@@ -17,8 +17,6 @@ static constexpr const char* PORTAL_NAMESPACE = "org.freedesktop.appearance";
|
||||
static constexpr const char* PORTAL_KEY = "color-scheme";
|
||||
} // namespace waybar
|
||||
|
||||
using namespace Gio;
|
||||
|
||||
auto fmt::formatter<waybar::Appearance>::format(waybar::Appearance c, format_context& ctx) const {
|
||||
string_view name;
|
||||
switch (c) {
|
||||
@@ -36,8 +34,8 @@ auto fmt::formatter<waybar::Appearance>::format(waybar::Appearance c, format_con
|
||||
}
|
||||
|
||||
waybar::Portal::Portal()
|
||||
: DBus::Proxy(DBus::Connection::get_sync(DBus::BusType::BUS_TYPE_SESSION), PORTAL_BUS_NAME,
|
||||
PORTAL_OBJ_PATH, PORTAL_INTERFACE),
|
||||
: Gio::DBus::Proxy(Gio::DBus::Connection::get_sync(Gio::DBus::BusType::BUS_TYPE_SESSION),
|
||||
PORTAL_BUS_NAME, PORTAL_OBJ_PATH, PORTAL_INTERFACE),
|
||||
currentMode(Appearance::UNKNOWN) {
|
||||
refreshAppearance();
|
||||
};
|
||||
@@ -60,21 +58,27 @@ void waybar::Portal::refreshAppearance() {
|
||||
// xdg-desktop-portal 1.17 will fix this issue with a new `ReadOne` method,
|
||||
// but this version is not yet released.
|
||||
// TODO(xdg-desktop-portal v1.17): switch to ReadOne
|
||||
auto container = Glib::VariantBase::cast_dynamic<Glib::VariantContainerBase>(response);
|
||||
Glib::VariantBase modev;
|
||||
container.get_child(modev, 0);
|
||||
auto mode =
|
||||
Glib::VariantBase::cast_dynamic<Glib::Variant<Glib::Variant<Glib::Variant<uint32_t>>>>(modev)
|
||||
.get()
|
||||
.get()
|
||||
.get();
|
||||
auto newMode = Appearance(mode);
|
||||
if (newMode == currentMode) {
|
||||
try {
|
||||
auto container = Glib::VariantBase::cast_dynamic<Glib::VariantContainerBase>(response);
|
||||
Glib::VariantBase modev;
|
||||
container.get_child(modev, 0);
|
||||
auto mode =
|
||||
Glib::VariantBase::cast_dynamic<Glib::Variant<Glib::Variant<Glib::Variant<uint32_t>>>>(
|
||||
modev)
|
||||
.get()
|
||||
.get()
|
||||
.get();
|
||||
auto newMode = Appearance(mode);
|
||||
if (newMode == currentMode) {
|
||||
return;
|
||||
}
|
||||
spdlog::info("Discovered appearance '{}'", newMode);
|
||||
currentMode = newMode;
|
||||
m_signal_appearance_changed.emit(currentMode);
|
||||
} catch (const std::bad_cast& e) {
|
||||
spdlog::error("Unexpected appearance variant format: {}", e.what());
|
||||
return;
|
||||
}
|
||||
spdlog::info("Discovered appearance '{}'", newMode);
|
||||
currentMode = newMode;
|
||||
m_signal_appearance_changed.emit(currentMode);
|
||||
}
|
||||
|
||||
waybar::Appearance waybar::Portal::getAppearance() { return currentMode; };
|
||||
|
||||
@@ -18,21 +18,21 @@ class PrepareForSleep {
|
||||
}
|
||||
}
|
||||
|
||||
static void prepareForSleep_cb(GDBusConnection *system_bus, const gchar *sender_name,
|
||||
const gchar *object_path, const gchar *interface_name,
|
||||
const gchar *signal_name, GVariant *parameters,
|
||||
static void prepareForSleep_cb(GDBusConnection* system_bus, const gchar* sender_name,
|
||||
const gchar* object_path, const gchar* interface_name,
|
||||
const gchar* signal_name, GVariant* parameters,
|
||||
gpointer user_data) {
|
||||
if (g_variant_is_of_type(parameters, G_VARIANT_TYPE("(b)")) != 0) {
|
||||
gboolean sleeping;
|
||||
g_variant_get(parameters, "(b)", &sleeping);
|
||||
|
||||
auto *self = static_cast<PrepareForSleep *>(user_data);
|
||||
auto* self = static_cast<PrepareForSleep*>(user_data);
|
||||
self->signal.emit(sleeping);
|
||||
}
|
||||
}
|
||||
|
||||
public:
|
||||
static PrepareForSleep &GetInstance() {
|
||||
static PrepareForSleep& GetInstance() {
|
||||
static PrepareForSleep instance;
|
||||
return instance;
|
||||
}
|
||||
@@ -40,10 +40,10 @@ class PrepareForSleep {
|
||||
|
||||
private:
|
||||
guint login1_id;
|
||||
GDBusConnection *login1_connection;
|
||||
GDBusConnection* login1_connection;
|
||||
};
|
||||
} // namespace
|
||||
|
||||
waybar::SafeSignal<bool> &waybar::util::prepare_for_sleep() {
|
||||
waybar::SafeSignal<bool>& waybar::util::prepare_for_sleep() {
|
||||
return PrepareForSleep::GetInstance().signal;
|
||||
}
|
||||
|
||||
@@ -3,6 +3,7 @@
|
||||
#include <json/value.h>
|
||||
#include <spdlog/spdlog.h>
|
||||
|
||||
#include <algorithm>
|
||||
#include <utility>
|
||||
|
||||
namespace waybar::util {
|
||||
|
||||
@@ -18,7 +18,7 @@ std::string rewriteString(const std::string& value, const Json::Value& rules) {
|
||||
// malformated regexes will cause an exception.
|
||||
// in this case, log error and try the next rule.
|
||||
const std::regex rule{it.key().asString(), std::regex_constants::icase};
|
||||
if (std::regex_match(value, rule)) {
|
||||
if (std::regex_match(res, rule)) {
|
||||
res = std::regex_replace(res, rule, it->asString());
|
||||
}
|
||||
} catch (const std::regex_error& e) {
|
||||
|
||||
@@ -0,0 +1,60 @@
|
||||
#include <filesystem>
|
||||
#include <fstream>
|
||||
#include <iomanip>
|
||||
#include <regex>
|
||||
#include <sstream>
|
||||
namespace fs = std::filesystem;
|
||||
|
||||
struct TransformResult {
|
||||
std::string css;
|
||||
bool was_transformed;
|
||||
};
|
||||
|
||||
TransformResult transform_8bit_to_hex(const std::string& file_path) {
|
||||
std::ifstream f(file_path, std::ios::in | std::ios::binary);
|
||||
const auto size = fs::file_size(file_path);
|
||||
std::string result(size, '\0');
|
||||
if (!f.is_open() || !f.good()) {
|
||||
throw std::runtime_error("Cannot open file: " + file_path);
|
||||
}
|
||||
|
||||
if (size == 0) {
|
||||
return {.css = result, .was_transformed = false};
|
||||
}
|
||||
|
||||
f.read(result.data(), size);
|
||||
|
||||
static std::regex pattern(
|
||||
R"(\#([a-fA-F0-9]{2})([a-fA-F0-9]{2})([a-fA-F0-9]{2})([a-fA-F0-9]{2}))");
|
||||
std::string final_output;
|
||||
|
||||
auto it = std::sregex_iterator(result.begin(), result.end(), pattern);
|
||||
auto eof = std::sregex_iterator();
|
||||
|
||||
if (it == eof) {
|
||||
return {.css = result, .was_transformed = false};
|
||||
}
|
||||
|
||||
std::smatch match;
|
||||
while (it != eof) {
|
||||
match = *it;
|
||||
|
||||
final_output += match.prefix().str();
|
||||
|
||||
int r = stoi(match[1].str(), nullptr, 16);
|
||||
int g = stoi(match[2].str(), nullptr, 16);
|
||||
int b = stoi(match[3].str(), nullptr, 16);
|
||||
double a = (stoi(match[4].str(), nullptr, 16) / 255.0);
|
||||
|
||||
std::stringstream ss;
|
||||
ss << "rgba(" << r << "," << g << "," << b << "," << std::fixed << std::setprecision(2) << a
|
||||
<< ")";
|
||||
final_output += ss.str();
|
||||
|
||||
++it;
|
||||
}
|
||||
|
||||
final_output += match.suffix().str();
|
||||
|
||||
return {.css = final_output, .was_transformed = true};
|
||||
}
|
||||
@@ -1,6 +1,6 @@
|
||||
#include "util/ustring_clen.hpp"
|
||||
|
||||
int ustring_clen(const Glib::ustring &str) {
|
||||
int ustring_clen(const Glib::ustring& str) {
|
||||
int total = 0;
|
||||
for (unsigned int i : str) {
|
||||
total += g_unichar_iswide(i) + 1;
|
||||
|
||||
@@ -0,0 +1,62 @@
|
||||
#include <glib.h>
|
||||
|
||||
#include <string>
|
||||
#include <util/utf8_string.hpp>
|
||||
|
||||
namespace waybar::util {
|
||||
|
||||
namespace {
|
||||
// Wide characters count as two, zero-width characters count as zero
|
||||
// Modifies str in-place (unless width = std::string::npos)
|
||||
// Returns the total width of the string pre-truncating
|
||||
size_t measure_and_truncate(std::string& str, size_t width = std::string::npos) {
|
||||
if (str.length() == 0) return 0;
|
||||
|
||||
const gchar* trunc_end = nullptr;
|
||||
|
||||
size_t total_width = 0;
|
||||
|
||||
for (gchar *data = str.data(), *end = data + str.size(); data != nullptr;) {
|
||||
gunichar c = g_utf8_get_char_validated(data, end - data);
|
||||
if (c == -1U || c == -2U) {
|
||||
// invalid unicode, treat string as ascii
|
||||
if (width != std::string::npos && str.length() > width) str.resize(width);
|
||||
return str.length();
|
||||
} else if (g_unichar_iswide(c)) {
|
||||
total_width += 2;
|
||||
} else if (!g_unichar_iszerowidth(c) && c != 0xAD) { // neither zero-width nor soft hyphen
|
||||
total_width += 1;
|
||||
}
|
||||
|
||||
data = g_utf8_find_next_char(data, end);
|
||||
if (width != std::string::npos && total_width <= width && !g_unichar_isspace(c))
|
||||
trunc_end = data;
|
||||
}
|
||||
|
||||
if (trunc_end) str.resize(trunc_end - str.data());
|
||||
|
||||
return total_width;
|
||||
}
|
||||
} // namespace
|
||||
|
||||
size_t utf8_width(const std::string& str) {
|
||||
return measure_and_truncate(const_cast<std::string&>(str));
|
||||
}
|
||||
|
||||
void utf8_truncate(std::string& s, const std::string& ellipsis, size_t max_len) {
|
||||
if (max_len == 0) {
|
||||
s.resize(0);
|
||||
return;
|
||||
}
|
||||
size_t len = measure_and_truncate(s, max_len);
|
||||
if (len > max_len) {
|
||||
size_t ellipsis_len = utf8_width(ellipsis);
|
||||
if (max_len >= ellipsis_len) {
|
||||
if (ellipsis_len) measure_and_truncate(s, max_len - ellipsis_len);
|
||||
s += ellipsis;
|
||||
} else {
|
||||
s.resize(0);
|
||||
}
|
||||
}
|
||||
}
|
||||
} // namespace waybar::util
|
||||
Reference in New Issue
Block a user