diff --git a/README.md b/README.md index 74f46600..b104adec 100644 --- a/README.md +++ b/README.md @@ -67,6 +67,7 @@ libnl [Network module] libappindicator-gtk3 [Tray module] libdbusmenu-gtk3 [Tray module] libmpdclient [MPD module] +libsndio [sndio module] ``` **Build dependencies** diff --git a/include/factory.hpp b/include/factory.hpp index ebc2359f..3efe6cbd 100644 --- a/include/factory.hpp +++ b/include/factory.hpp @@ -39,6 +39,9 @@ #ifdef HAVE_LIBMPDCLIENT #include "modules/mpd.hpp" #endif +#ifdef HAVE_LIBSNDIO +#include "modules/sndio.hpp" +#endif #include "bar.hpp" #include "modules/custom.hpp" #include "modules/temperature.hpp" diff --git a/include/modules/sndio.hpp b/include/modules/sndio.hpp new file mode 100644 index 00000000..f14d0622 --- /dev/null +++ b/include/modules/sndio.hpp @@ -0,0 +1,29 @@ +#pragma once + +#include +#include +#include "ALabel.hpp" +#include "util/sleeper_thread.hpp" + +namespace waybar::modules { + +class Sndio : public ALabel { + public: + Sndio(const std::string&, const Json::Value&); + ~Sndio(); + auto update() -> void; + auto set_desc(struct sioctl_desc *, unsigned int) -> void; + auto put_val(unsigned int, unsigned int) -> void; + bool handleScroll(GdkEventScroll *); + bool handleToggle(GdkEventButton* const&); + + private: + util::SleeperThread thread_; + struct sioctl_hdl *hdl_; + std::vector pfds_; + unsigned int addr_; + unsigned int volume_, old_volume_, maxval_; + bool muted_; +}; + +} // namespace waybar::modules diff --git a/man/waybar-sndio.5.scd b/man/waybar-sndio.5.scd new file mode 100644 index 00000000..a61c3325 --- /dev/null +++ b/man/waybar-sndio.5.scd @@ -0,0 +1,83 @@ +waybar-sndio(5) + +# NAME + +waybar - sndio module + +# DESCRIPTION + +The *sndio* module displays the current volume reported by sndio(7). + +Additionally, you can control the volume by scrolling *up* or *down* while the +cursor is over the module, and clicking on the module toggles mute. + +# CONFIGURATION + +*format*: ++ + typeof: string ++ + default: {volume}% ++ + The format for how information should be displayed. + +*rotate*: ++ + typeof: integer ++ + Positive value to rotate the text label. + +*max-length*: ++ + typeof: integer ++ + The maximum length in character the module should display. + +*scroll-step*: ++ + typeof: int ++ + default: 5 ++ + The speed in which to change the volume when scrolling. + +*on-click*: ++ + typeof: string ++ + Command to execute when clicked on the module. + This replaces the default behaviour of toggling mute. + +*on-click-middle*: ++ + typeof: string ++ + Command to execute when middle-clicked on the module using mousewheel. + +*on-click-right*: ++ + typeof: string ++ + Command to execute when you right clicked on the module. + +*on-update*: ++ + typeof: string ++ + Command to execute when the module is updated. + +*on-scroll-up*: ++ + typeof: string ++ + Command to execute when scrolling up on the module. + This replaces the default behaviour of volume control. + +*on-scroll-down*: ++ + typeof: string ++ + Command to execute when scrolling down on the module. + This replaces the default behaviour of volume control. + +*smooth-scrolling-threshold*: ++ + typeof: double ++ + Threshold to be used when scrolling. + +# FORMAT REPLACEMENTS + +*{volume}*: Volume in percentage. + +*{raw_value}*: Volume as value reported by sndio. + +# EXAMPLES + +``` +"sndio": { + "format": "{raw_value} 🎜", + "scroll-step": 3 +} +``` + +# STYLE + +- *#sndio* +- *#sndio.muted* diff --git a/meson.build b/meson.build index 5ac09869..023894cd 100644 --- a/meson.build +++ b/meson.build @@ -96,6 +96,19 @@ libnlgen = dependency('libnl-genl-3.0', required: get_option('libnl')) libpulse = dependency('libpulse', required: get_option('pulseaudio')) libudev = dependency('libudev', required: get_option('libudev')) libmpdclient = dependency('libmpdclient', required: get_option('mpd')) + +libsndio = compiler.find_library('sndio', required: get_option('sndio')) +if libsndio.found() + if not compiler.has_function('sioctl_open', prefix: '#include ', dependencies: libsndio) + if get_option('sndio').enabled() + error('libsndio is too old, required >=1.7.0') + else + warning('libsndio is too old, required >=1.7.0') + libsndio = dependency('', required: false) + endif + endif +endif + gtk_layer_shell = dependency('gtk-layer-shell-0', required: get_option('gtk-layer-shell'), fallback : ['gtk-layer-shell', 'gtk_layer_shell_dep']) @@ -207,6 +220,11 @@ if gtk_layer_shell.found() add_project_arguments('-DHAVE_GTK_LAYER_SHELL', language: 'cpp') endif +if libsndio.found() + add_project_arguments('-DHAVE_LIBSNDIO', language: 'cpp') + src_files += 'src/modules/sndio.cpp' +endif + if get_option('rfkill').enabled() if is_linux add_project_arguments('-DWANT_RFKILL', language: 'cpp') @@ -241,6 +259,7 @@ executable( libepoll, libmpdclient, gtk_layer_shell, + libsndio, tz_dep ], include_directories: [include_directories('include')], @@ -292,6 +311,7 @@ if scdoc.found() 'waybar-states.5.scd', 'waybar-wlr-taskbar.5.scd', 'waybar-bluetooth.5.scd', + 'waybar-sndio.5.scd', ] foreach file : man_files diff --git a/meson_options.txt b/meson_options.txt index de47da74..cb5581b3 100644 --- a/meson_options.txt +++ b/meson_options.txt @@ -8,3 +8,4 @@ option('man-pages', type: 'feature', value: 'auto', description: 'Generate and i option('mpd', type: 'feature', value: 'auto', description: 'Enable support for the Music Player Daemon') option('gtk-layer-shell', type: 'feature', value: 'auto', description: 'Use gtk-layer-shell library for popups support') option('rfkill', type: 'feature', value: 'auto', description: 'Enable support for RFKILL') +option('sndio', type: 'feature', value: 'auto', description: 'Enable support for sndio') diff --git a/src/factory.cpp b/src/factory.cpp index af93b20f..8a5e8250 100644 --- a/src/factory.cpp +++ b/src/factory.cpp @@ -76,6 +76,11 @@ waybar::AModule* waybar::Factory::makeModule(const std::string& name) const { if (ref == "mpd") { return new waybar::modules::MPD(id, config_[name]); } +#endif +#ifdef HAVE_LIBSNDIO + if (ref == "sndio") { + return new waybar::modules::Sndio(id, config_[name]); + } #endif if (ref == "temperature") { return new waybar::modules::Temperature(id, config_[name]); diff --git a/src/modules/sndio.cpp b/src/modules/sndio.cpp new file mode 100644 index 00000000..e72c56f9 --- /dev/null +++ b/src/modules/sndio.cpp @@ -0,0 +1,167 @@ +#include "modules/sndio.hpp" +#include +#include +#include +#include + +namespace waybar::modules { + +void ondesc(void *arg, struct sioctl_desc *d, int curval) { + auto self = static_cast(arg); + if (d == NULL) { + // d is NULL when the list is done + return; + } + self->set_desc(d, curval); +} + +void onval(void *arg, unsigned int addr, unsigned int val) { + auto self = static_cast(arg); + self->put_val(addr, val); +} + +Sndio::Sndio(const std::string &id, const Json::Value &config) + : ALabel(config, "sndio", id, "{volume}%"), + hdl_(nullptr), + pfds_(0), + addr_(0), + volume_(0), + old_volume_(0), + maxval_(0), + muted_(false) { + hdl_ = sioctl_open(SIO_DEVANY, SIOCTL_READ | SIOCTL_WRITE, 0); + if (hdl_ == nullptr) { + throw std::runtime_error("sioctl_open() failed."); + } + + if(sioctl_ondesc(hdl_, ondesc, this) == 0) { + throw std::runtime_error("sioctl_ondesc() failed."); + } + + sioctl_onval(hdl_, onval, this); + + pfds_.reserve(sioctl_nfds(hdl_)); + + event_box_.show(); + + event_box_.add_events(Gdk::SCROLL_MASK | Gdk::SMOOTH_SCROLL_MASK | Gdk::BUTTON_PRESS_MASK); + event_box_.signal_scroll_event().connect( + sigc::mem_fun(*this, &Sndio::handleScroll)); + event_box_.signal_button_press_event().connect( + sigc::mem_fun(*this, &Sndio::handleToggle)); + + thread_ = [this] { + dp.emit(); + + int nfds = sioctl_pollfd(hdl_, pfds_.data(), POLLIN); + if (nfds == 0) { + throw std::runtime_error("sioctl_pollfd() failed."); + } + while (poll(pfds_.data(), nfds, -1) < 0) { + if (errno != EINTR) { + throw std::runtime_error("poll() failed."); + } + } + + int revents = sioctl_revents(hdl_, pfds_.data()); + if (revents & POLLHUP) { + throw std::runtime_error("disconnected!"); + } + }; +} + +Sndio::~Sndio() { + sioctl_close(hdl_); +} + +auto Sndio::update() -> void { + auto format = format_; + unsigned int vol = 100. * static_cast(volume_) / static_cast(maxval_); + + if (volume_ == 0) { + label_.get_style_context()->add_class("muted"); + } else { + label_.get_style_context()->remove_class("muted"); + } + + label_.set_markup(fmt::format(format, + fmt::arg("volume", vol), + fmt::arg("raw_value", volume_))); + + ALabel::update(); +} + +auto Sndio::set_desc(struct sioctl_desc *d, unsigned int val) -> void { + std::string name{d->func}; + std::string node_name{d->node0.name}; + + if (name == "level" && node_name == "output" && d->type == SIOCTL_NUM) { + // store addr for output.level value, used in put_val + addr_ = d->addr; + maxval_ = d->maxval; + volume_ = val; + } +} + +auto Sndio::put_val(unsigned int addr, unsigned int val) -> void { + if (addr == addr_) { + volume_ = val; + } +} + +bool Sndio::handleScroll(GdkEventScroll *e) { + // change the volume only when no user provided + // events are configured + 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; + } + + int step = 5; + if (config_["scroll-step"].isInt()) { + step = config_["scroll-step"].asInt(); + } + + int new_volume = volume_; + if (muted_) { + new_volume = old_volume_; + } + + if (dir == SCROLL_DIR::UP) { + new_volume += step; + } else if (dir == SCROLL_DIR::DOWN) { + new_volume -= step; + } + new_volume = std::clamp(new_volume, 0, static_cast(maxval_)); + + // quits muted mode if volume changes + muted_ = false; + + sioctl_setval(hdl_, addr_, new_volume); + + return true; +} + +bool Sndio::handleToggle(GdkEventButton* const& e) { + // toggle mute only when no user provided events are configured + if (config_["on-click"].isString()) { + return AModule::handleToggle(e); + } + + muted_ = !muted_; + if (muted_) { + // store old volume to be able to restore it later + old_volume_ = volume_; + sioctl_setval(hdl_, addr_, 0); + } else { + sioctl_setval(hdl_, addr_, old_volume_); + } + + return true; +} + +} /* namespace waybar::modules */