Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Introduce a power-profiles-daemon module #2971

Merged
merged 9 commits into from
Mar 4, 2024
1 change: 1 addition & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@
- Local time
- Battery
- UPower
- Power profiles daemon
- Network
- Bluetooth
- Pulseaudio
Expand Down
49 changes: 49 additions & 0 deletions include/modules/power_profiles_daemon.hpp
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
#pragma once

#include <fmt/format.h>

#include "ALabel.hpp"
#include "giomm/dbusproxy.h"

namespace waybar::modules {

struct Profile {
std::string name;
std::string driver;
};

class PowerProfilesDaemon : public ALabel {
public:
PowerProfilesDaemon(const std::string &, const Json::Value &);
~PowerProfilesDaemon() override;
auto update() -> void override;
void profileChangedCb(const Gio::DBus::Proxy::MapChangedProperties &,
const std::vector<Glib::ustring> &);
void busConnectedCb(Glib::RefPtr<Gio::AsyncResult> &r);
void getAllPropsCb(Glib::RefPtr<Gio::AsyncResult> &r);
void setPropCb(Glib::RefPtr<Gio::AsyncResult> &r);
void populateInitState();
bool handleToggle(GdkEventButton *const &e) override;

private:
// True if we're connected to the dbug interface. False if we're
// not.
bool connected_;
// Look for a profile name in the list of available profiles and
// switch activeProfile_ to it.
void switchToProfile(std::string const &);
// Used to toggle/display the profiles
std::vector<Profile> availableProfiles_;
// Points to the active profile in the profiles list
std::vector<Profile>::iterator activeProfile_;
// Current CSS class applied to the label
std::string currentStyle_;
// Format strings
std::string labelFormat_;
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

labelFormat_ is unused

std::string tooltipFormat_;
// DBus Proxy used to track the current active profile
Glib::RefPtr<Gio::DBus::Proxy> powerProfilesProxy_;
sigc::connection powerProfileChangeSignal_;
};

} // namespace waybar::modules
1 change: 1 addition & 0 deletions meson.build
Original file line number Diff line number Diff line change
Expand Up @@ -212,6 +212,7 @@ if is_linux
'src/modules/cpu_usage/linux.cpp',
'src/modules/memory/common.cpp',
'src/modules/memory/linux.cpp',
'src/modules/power_profiles_daemon.cpp',
'src/modules/systemd_failed_units.cpp',
)
man_files += files(
Expand Down
13 changes: 12 additions & 1 deletion resources/config.jsonc
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@
"idle_inhibitor",
"pulseaudio",
"network",
"power_profiles_daemon",
"cpu",
"memory",
"temperature",
Expand Down Expand Up @@ -147,6 +148,17 @@
"battery#bat2": {
"bat": "BAT2"
},
"power_profiles_daemon": {
"format": "{icon}",
"tooltip-format": "Power profile: {profile}\nDriver: {driver}",
"tooltip": true,
"format-icons": {
"default": "",
"performance": "",
"balanced": "",
"power-saver": ""
}
},
"network": {
// "interface": "wlp2*", // (Optional) To force the use of this interface
"format-wifi": "{essid} ({signalStrength}%) ",
Expand Down Expand Up @@ -188,4 +200,3 @@
// "exec": "$HOME/.config/waybar/mediaplayer.py --player spotify 2> /dev/null" // Filter player based on name
}
}

16 changes: 16 additions & 0 deletions resources/style.css
Original file line number Diff line number Diff line change
Expand Up @@ -87,6 +87,7 @@ button:hover {
#mode,
#idle_inhibitor,
#scratchpad,
#power-profiles-daemon,
#mpd {
padding: 0 10px;
color: #ffffff;
Expand Down Expand Up @@ -139,6 +140,21 @@ button:hover {
animation-direction: alternate;
}

#power-profiles-daemon.performance {
background-color: #f53c3c;
color: #ffffff;
}

#power-profiles-daemon.balanced {
background-color: #2980b9;
color: #ffffff;
}

#power-profiles-daemon.power-saver {
background-color: #2ecc71;
color: #000000;
}

label:focus {
background-color: #000000;
}
Expand Down
4 changes: 4 additions & 0 deletions src/factory.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -86,6 +86,7 @@
#endif
#if defined(__linux__)
#include "modules/bluetooth.hpp"
#include "modules/power_profiles_daemon.hpp"
#endif
#ifdef HAVE_LOGIND_INHIBITOR
#include "modules/inhibitor.hpp"
Expand Down Expand Up @@ -282,6 +283,9 @@ waybar::AModule* waybar::Factory::makeModule(const std::string& name,
if (ref == "bluetooth") {
return new waybar::modules::Bluetooth(id, config_[name]);
}
if (ref == "power_profiles_daemon") {
return new waybar::modules::PowerProfilesDaemon(id, config_[name]);
}
#endif
#ifdef HAVE_LOGIND_INHIBITOR
if (ref == "inhibitor") {
Expand Down
214 changes: 214 additions & 0 deletions src/modules/power_profiles_daemon.cpp
Original file line number Diff line number Diff line change
@@ -0,0 +1,214 @@
#include "modules/power_profiles_daemon.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)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Waybar requires fmt >= 8.1.1, this check is not necessary in a new code.

#include <fmt/args.h>
#else
#include <fmt/core.h>
#endif

#include <glibmm.h>
#include <glibmm/variant.h>
#include <spdlog/spdlog.h>

namespace waybar::modules {

PowerProfilesDaemon::PowerProfilesDaemon(const std::string& id, const Json::Value& config)
: ALabel(config, "power-profiles-daemon", id, "{profile}", 0, false, true), connected_(false) {
if (config_["format"].isString()) {
format_ = config_["format"].asString();
} else {
format_ = "{icon}";
}
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

ALabel::ALabel will do that for you if you pass the correct default format string to the constructor.


if (config_["tooltip-format"].isString()) {
tooltipFormat_ = config_["tooltip-format"].asString();
} else {
tooltipFormat_ = "Power profile: {profile}\nDriver: {driver}";
}
// Fasten your seatbelt, we're up for quite a ride. The rest of the
// init is performed asynchronously. There's 2 callbacks involved.
// Here's the overall idea:
// 1. Async connect to the system bus.
// 2. In the system bus connect callback, try to call
// org.freedesktop.DBus.Properties.GetAll to see if
// power-profiles-daemon is able to respond.
// 3. In the GetAll callback, connect the activeProfile monitoring
// callback, consider the init to be successful. Meaning start
// drawing the module.
//
// There's sadly no other way around that, we have to try to call a
// method on the proxy to see whether or not something's responding
// on the other side.

// NOTE: the DBus adresses are under migration. They should be
// changed to org.freedesktop.UPower.PowerProfiles at some point.
//
// See
// https://gitlab.freedesktop.org/upower/power-profiles-daemon/-/releases/0.20
//
// The old name is still announced for now. Let's rather use the old
// adresses for compatibility sake.
//
// Revisit this in 2026, systems should be updated by then.
Gio::DBus::Proxy::create_for_bus(Gio::DBus::BusType::BUS_TYPE_SYSTEM, "net.hadess.PowerProfiles",
"/net/hadess/PowerProfiles", "net.hadess.PowerProfiles",
sigc::mem_fun(*this, &PowerProfilesDaemon::busConnectedCb));
}

PowerProfilesDaemon::~PowerProfilesDaemon() {
if (powerProfileChangeSignal_.connected()) {
powerProfileChangeSignal_.disconnect();
}
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

For any connections made with sigc::mem_fun where the object inherits sigc::trackable (base class for the modules does), this will happen automatically. You don't need to track or disconnect the connection.

if (powerProfilesProxy_) {
powerProfilesProxy_.reset();
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

RefPtr already does that during destruction.

}
}

void PowerProfilesDaemon::busConnectedCb(Glib::RefPtr<Gio::AsyncResult>& r) {
try {
powerProfilesProxy_ = Gio::DBus::Proxy::create_for_bus_finish(r);
using GetAllProfilesVar = Glib::Variant<std::tuple<Glib::ustring>>;
auto callArgs = GetAllProfilesVar::create(std::make_tuple("net.hadess.PowerProfiles"));

auto container = Glib::VariantBase::cast_dynamic<Glib::VariantContainerBase>(callArgs);
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is the manual conversion here necessary? You should be able to pass callArgs to call directly.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ah, yes, indeed, my bad. I was sure I tried and it error-ed out. It definitely does not 😕

powerProfilesProxy_->call("org.freedesktop.DBus.Properties.GetAll",
sigc::mem_fun(*this, &PowerProfilesDaemon::getAllPropsCb), container);
// Connect active profile callback
} catch (const std::exception& e) {
spdlog::error("Failed to create the power profiles daemon DBus proxy");
}
}

// Callback for the GetAll call.
//
// We're abusing this call to make sure power-profiles-daemon is
// available on the host. We're not really using
void PowerProfilesDaemon::getAllPropsCb(Glib::RefPtr<Gio::AsyncResult>& r) {
try {
auto _ = powerProfilesProxy_->call_finish(r);
// Power-profiles-daemon responded something, we can assume it's
// available, we can safely attach the activeProfile monitoring
// now.
connected_ = true;
powerProfileChangeSignal_ = powerProfilesProxy_->signal_properties_changed().connect(
sigc::mem_fun(*this, &PowerProfilesDaemon::profileChangedCb));
populateInitState();
dp.emit();
} catch (const std::exception& err) {
spdlog::error("Failed to query power-profiles-daemon via dbus: {}", err.what());
}
}

void PowerProfilesDaemon::populateInitState() {
// Retrieve current active profile
Glib::Variant<std::string> profileStr;
powerProfilesProxy_->get_cached_property(profileStr, "ActiveProfile");

// Retrieve profiles list, it's aa{sv}.
using ProfilesType = std::vector<std::map<Glib::ustring, Glib::Variant<std::string>>>;
Glib::Variant<ProfilesType> profilesVariant;
powerProfilesProxy_->get_cached_property(profilesVariant, "Profiles");
Glib::ustring name;
Glib::ustring driver;
Profile profile;
for (auto& variantDict : profilesVariant.get()) {
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();
}
profile = {name, driver};
availableProfiles_.push_back(profile);
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

availableProfiles_.emplace_back(name, driver); would avoid one temporary object.

And if you move name/driver declaration into the loop (which you might want to do anyways to avoid accidentally reusing values from previous iterations), it'll be safe to write if (!name.empty()) { availableProfiles_.emplace_back(std::move(name), std::move(driver)); }

}

// Find the index of the current activated mode (to toggle)
std::string str = profileStr.get();
switchToProfile(str);

update();
}

void PowerProfilesDaemon::profileChangedCb(
const Gio::DBus::Proxy::MapChangedProperties& changedProperties,
const std::vector<Glib::ustring>& invalidatedProperties) {
// We're likely connected if this callback gets triggered.
// But better be safe than sorry.
if (connected_) {
if (auto activeProfileVariant = changedProperties.find("ActiveProfile");
activeProfileVariant != changedProperties.end()) {
std::string activeProfile =
Glib::VariantBase::cast_dynamic<Glib::Variant<std::string>>(activeProfileVariant->second)
.get();
switchToProfile(activeProfile);
update();
}
}
}

// Look for the profile str in our internal profiles list. Using a
// vector to store the profiles ain't the smartest move
// complexity-wise, but it makes toggling between the mode easy. This
// vector is 3 elements max, we'll be fine :P
void PowerProfilesDaemon::switchToProfile(std::string const& str) {
auto pred = [str](Profile const& p) { return p.name == str; };
this->activeProfile_ = std::find_if(availableProfiles_.begin(), availableProfiles_.end(), pred);
if (activeProfile_ == availableProfiles_.end()) {
throw std::runtime_error("FATAL, can't find the active profile in the available profiles list");
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm not sure throwing exception is necessary here. It will be caught by Glibmm exception handler anyways, with no consequences.
You can just log the error and hide the module on the next update.

}
}

auto PowerProfilesDaemon::update() -> void {
if (connected_) {
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_text(fmt::vformat(tooltipFormat_, store));
}

// Set CSS class
if (!currentStyle_.empty()) {
label_.get_style_context()->remove_class(currentStyle_);
}
label_.get_style_context()->add_class(profile.name);
currentStyle_ = profile.name;

ALabel::update();
}
}

bool PowerProfilesDaemon::handleToggle(GdkEventButton* const& e) {
if (connected_) {
if (e->type == GdkEventType::GDK_BUTTON_PRESS && powerProfilesProxy_) {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The check for powerProfilesProxy_ could be redundant if you already check connected_

activeProfile_++;
if (activeProfile_ == availableProfiles_.end()) {
activeProfile_ = availableProfiles_.begin();
}

using VarStr = Glib::Variant<Glib::ustring>;
using SetPowerProfileVar = Glib::Variant<std::tuple<Glib::ustring, Glib::ustring, VarStr>>;
VarStr activeProfileVariant = VarStr::create(activeProfile_->name);
auto callArgs = SetPowerProfileVar::create(
std::make_tuple("net.hadess.PowerProfiles", "ActiveProfile", activeProfileVariant));
auto container = Glib::VariantBase::cast_dynamic<Glib::VariantContainerBase>(callArgs);
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The manual cast might be unnecessary

powerProfilesProxy_->call("org.freedesktop.DBus.Properties.Set",
sigc::mem_fun(*this, &PowerProfilesDaemon::setPropCb), container);
}
}
return true;
}

void PowerProfilesDaemon::setPropCb(Glib::RefPtr<Gio::AsyncResult>& r) {
auto _ = powerProfilesProxy_->call_finish(r);
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Please, check for exceptions here.

update();
}

} // namespace waybar::modules