A cmds/pavolmon/README.md => cmds/pavolmon/README.md +12 -0
@@ 0,0 1,12 @@
+This simple program connects to the PulseAudio server and constantly monitors
+the volume level of a default output device. Whenever it changes it prints
+on the standard output a single line containg the level.
+
+Installation:
+
+```
+g++ -o pavolmon $(pkg-config -libs libpulse) -lm ./cmds/pavolmon/volmon.cpp
+sudo cp pavolmon /usr/local/bin
+```
+
+This program is needed by the Volume widget.
A cmds/pavolmon/volmon.cpp => cmds/pavolmon/volmon.cpp +255 -0
@@ 0,0 1,255 @@
+/**
+ * Author: Jason White
+ * License: Public Domain
+ *
+ * Description:
+ * This is a simple test program to hook into PulseAudio volume change
+ * notifications. It was created for the possibility of having an automatically
+ * updating volume widget in a tiling window manager status bar.
+ *
+ * Compiling:
+ *
+ * g++ $(pkg-config libpulse --cflags --libs) pulsetest.c -o pulsetest
+ *
+ * Modified by Radosław Kintzi to print voulme of a default sink
+ */
+#include <assert.h>
+#include <signal.h>
+#include <pulse/pulseaudio.h>
+#include <cstring>
+#include <iostream>
+#include <cmath>
+
+class PulseAudio
+{
+private:
+ pa_mainloop* _mainloop;
+ pa_mainloop_api* _mainloop_api;
+ pa_context* _context;
+ pa_signal_event* _signal;
+ const char *_default_sink_name;
+public:
+ PulseAudio()
+ : _mainloop(NULL), _mainloop_api(NULL), _context(NULL), _signal(NULL)
+ {
+ }
+
+ /**
+ * Initializes state and connects to the PulseAudio server.
+ */
+ bool initialize()
+ {
+ _mainloop = pa_mainloop_new();
+ if (!_mainloop)
+ {
+ std::cerr<< "pa_mainloop_new() failed.\n";
+ return false;
+ }
+
+ _mainloop_api = pa_mainloop_get_api(_mainloop);
+
+ if (pa_signal_init(_mainloop_api) != 0)
+ {
+ std::cerr << "pa_signal_init() failed\n";
+ return false;
+ }
+
+ _signal = pa_signal_new(SIGINT, exit_signal_callback, this);
+ if (!_signal)
+ {
+ std::cerr << "pa_signal_new() failed\n";
+ return false;
+ }
+ signal(SIGPIPE, SIG_IGN);
+
+ _context = pa_context_new(_mainloop_api, "PulseAudio Test");
+ if (!_context)
+ {
+ std::cerr << "pa_context_new() failed\n";
+ return false;
+ }
+
+ if (pa_context_connect(_context, NULL, PA_CONTEXT_NOAUTOSPAWN, NULL) < 0)
+ {
+ std::cerr << "pa_context_connect() failed: " << pa_strerror(pa_context_errno(_context)) << "\n";
+ return false;
+ }
+
+ pa_context_set_state_callback(_context, context_state_callback, this);
+
+ return true;
+ }
+
+ /**
+ * Runs the main PulseAudio event loop. Calling quit will cause the event
+ * loop to exit.
+ */
+ int run()
+ {
+ int ret = 1;
+ if (pa_mainloop_run(_mainloop, &ret) < 0)
+ {
+ std::cerr << "pa_mainloop_run() failed.\n";
+ return ret;
+ }
+
+ return ret;
+ }
+
+ /**
+ * Exits the main loop with the specified return code.
+ */
+ void quit(int ret = 0)
+ {
+ _mainloop_api->quit(_mainloop_api, ret);
+ }
+
+ /**
+ * Called when the PulseAudio system is to be destroyed.
+ */
+ void destroy()
+ {
+ if (_context)
+ {
+ pa_context_unref(_context);
+ _context = NULL;
+ }
+
+ if (_signal)
+ {
+ pa_signal_free(_signal);
+ pa_signal_done();
+ _signal = NULL;
+ }
+
+ if (_mainloop)
+ {
+ pa_mainloop_free(_mainloop);
+ _mainloop = NULL;
+ _mainloop_api = NULL;
+ }
+ }
+
+ ~PulseAudio()
+ {
+ destroy();
+ }
+
+private:
+
+ /*
+ * Called on SIGINT.
+ */
+ static void exit_signal_callback(pa_mainloop_api *m, pa_signal_event *e, int sig, void *userdata)
+ {
+ PulseAudio* pa = (PulseAudio*)userdata;
+ if (pa) pa->quit();
+ }
+
+ /*
+ * Called whenever the context status changes.
+ */
+ static void context_state_callback(pa_context *c, void *userdata)
+ {
+ assert(c && userdata);
+
+ PulseAudio* pa = (PulseAudio*)userdata;
+
+ switch (pa_context_get_state(c))
+ {
+ case PA_CONTEXT_CONNECTING:
+ case PA_CONTEXT_AUTHORIZING:
+ case PA_CONTEXT_SETTING_NAME:
+ break;
+
+ case PA_CONTEXT_READY:
+ std::cerr <<"PulseAudio connection established.\n";
+ pa_context_get_server_info(c, server_info_callback, userdata);
+
+ // Subscribe to sink events from the server. This is how we get
+ // volume change notifications from the server.
+ pa_context_set_subscribe_callback(c, subscribe_callback, userdata);
+ pa_context_subscribe(c, (pa_subscription_mask_t)(PA_SUBSCRIPTION_MASK_SINK|PA_SUBSCRIPTION_MASK_SERVER), NULL, NULL);
+ break;
+
+ case PA_CONTEXT_TERMINATED:
+ pa->quit(0);
+ std::cerr << "PulseAudio connection terminated.\n";
+ break;
+
+ case PA_CONTEXT_FAILED:
+ default:
+ std::cerr<< "Connection failure: " << pa_strerror(pa_context_errno(c)) << "\n";
+ pa->quit(1);
+ break;
+ }
+ }
+
+ /*
+ * Called when an event we subscribed to occurs.
+ */
+ static void subscribe_callback(pa_context *c,
+ pa_subscription_event_type_t type, uint32_t idx, void *userdata)
+ {
+ unsigned facility = type & PA_SUBSCRIPTION_EVENT_FACILITY_MASK;
+ //type &= PA_SUBSCRIPTION_EVENT_TYPE_MASK;
+
+ pa_operation *op = NULL;
+
+ switch (facility)
+ {
+ case PA_SUBSCRIPTION_EVENT_SINK:
+ op = pa_context_get_sink_info_by_index(c, idx, sink_info_callback, userdata);
+ break;
+ case PA_SUBSCRIPTION_EVENT_SERVER:
+ op= pa_context_get_server_info(c, server_info_callback, userdata);
+ break;
+
+ default:
+ assert(0); // Got event we aren't expecting.
+ break;
+ }
+
+ if (op)
+ pa_operation_unref(op);
+ }
+
+ /*
+ * Called when the requested sink information is ready.
+ */
+ static void sink_info_callback(pa_context *c, const pa_sink_info *i,
+ int eol, void *userdata)
+ {
+ PulseAudio* pa = (PulseAudio*)userdata;
+ if (i && strcmp(pa->_default_sink_name, i->name)==0)
+ {
+ float volume = (float)pa_cvolume_avg(&(i->volume)) / (float)PA_VOLUME_NORM;
+ std::cout << std::round(volume * 100.0f) << (i->mute ? "m" :"") << "\n";
+ std::cout.flush();
+ }
+ }
+
+ /*
+ * Called when the requested information on the server is ready. This is
+ * used to find the default PulseAudio sink.
+ */
+ static void server_info_callback(pa_context *c, const pa_server_info *i,
+ void *userdata)
+ {
+ PulseAudio* pa = (PulseAudio*)userdata;
+ pa->_default_sink_name = i->default_sink_name;
+ pa_context_get_sink_info_by_name(c, i->default_sink_name, sink_info_callback, userdata);
+ }
+};
+
+
+int main(int argc, char *argv[])
+{
+ PulseAudio pa = PulseAudio();
+ if (!pa.initialize())
+ return 0;
+
+ int ret = pa.run();
+
+ return ret;
+}
M go.mod => go.mod +1 -0
@@ 6,6 6,7 @@ require (
github.com/adrg/xdg v0.4.0
github.com/godbus/dbus/v5 v5.1.0
github.com/google/go-cmp v0.6.0
+ github.com/google/shlex v0.0.0-20191202100458-e7afc7fbc510
github.com/shirou/gopsutil/v3 v3.23.10
gopkg.in/yaml.v3 v3.0.1
)
M go.sum => go.sum +2 -0
@@ 11,6 11,8 @@ github.com/google/go-cmp v0.5.6/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/
github.com/google/go-cmp v0.5.9/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI=
github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
+github.com/google/shlex v0.0.0-20191202100458-e7afc7fbc510 h1:El6M4kTTCOh6aBiKaUGG7oYTSPP8MxqL4YI3kZKwcP4=
+github.com/google/shlex v0.0.0-20191202100458-e7afc7fbc510/go.mod h1:pupxD2MaaD3pAXIBCelhxNneeOaAeabZDe5s4K6zSpQ=
github.com/lufia/plan9stats v0.0.0-20211012122336-39d0f177ccd0 h1:6E+4a0GO5zZEnZ81pIr0yLvtUWk2if982qA3F3QD6H4=
github.com/lufia/plan9stats v0.0.0-20211012122336-39d0f177ccd0/go.mod h1:zJYVVT2jmtg6P3p1VtQj7WsuWi/y4VnjVBn7F8KPB3I=
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
A widgets/volume.go => widgets/volume.go +188 -0
@@ 0,0 1,188 @@
+package widgets
+
+import (
+ "bufio"
+ "context"
+ "io"
+ "os/exec"
+ "strconv"
+ "strings"
+ "sync"
+
+ "github.com/google/shlex"
+ "gobytes.dev/statusbar"
+ "gobytes.dev/statusbar/internal/registry"
+)
+
+type Volume struct {
+ Format string `yaml:"format"`
+ Icon string `yaml:"icon"`
+ MuteIcon string `yaml:"muteIcon"`
+ VolMonCmd string `yaml:"volMonCommand"`
+ VolUpCmd string `yaml:"volUpCommand"`
+ VolDownCmd string `yaml:"volDownCommand"`
+ MuteToggleCmd string `yaml:"muteToggleCommand"`
+ OnClickCmd string `yaml:"onClickCommand"`
+
+ volMonCmd *exec.Cmd
+ volUpCmd []string
+ volDownCmd []string
+ muteToggleCmd []string
+ onClickCmd []string
+
+ fmt Format
+ rc io.ReadCloser
+ procErr error
+
+ baseWidget
+}
+
+func (w *Volume) WidgetName() string { return "Volume" }
+func (w *Volume) New() statusbar.Widget { return new(Volume) }
+
+func (w *Volume) Run(ctx context.Context, update statusbar.UpdateFunc, done func()) {
+ defer done()
+ w.init()
+ if w.volMonCmd == nil {
+ w.errorf("No volMonCommand provided")
+ return
+ }
+ rc, err := w.volMonCmd.StdoutPipe()
+ if err != nil {
+ w.errorf(err.Error())
+ return
+ }
+ w.rc = rc
+ var wg sync.WaitGroup
+ ctx, cancel := context.WithCancel(ctx)
+ end := func() {
+ cancel()
+ wg.Done()
+ }
+ wg.Add(2)
+ go w.readVol(ctx, end, update)
+ go w.run(ctx, end, w.volMonCmd)
+ wg.Wait()
+ w.errorf(w.procErr.Error())
+}
+
+func (w *Volume) Event(ev statusbar.Event) {
+ var cmd []string
+ switch ev.Button {
+ case 4:
+ cmd = w.volUpCmd
+ case 5:
+ cmd = w.volDownCmd
+ case 1:
+ cmd = w.onClickCmd
+ case 3:
+ cmd = w.muteToggleCmd
+ }
+ if len(cmd) == 0 {
+ return
+ }
+ go func() {
+ c := exec.Command(cmd[0], cmd[1:]...)
+ err := c.Run()
+ if err != nil {
+ w.errorf(err.Error())
+ }
+ }()
+}
+
+func (w *Volume) run(ctx context.Context, done func(), cmd *exec.Cmd) {
+ defer done()
+ w.procErr = cmd.Run()
+}
+
+func (w *Volume) readVol(ctx context.Context, done func(), update statusbar.UpdateFunc) {
+ defer done()
+ buf := bufio.NewReader(w.rc)
+ line, err := buf.ReadString('\n')
+ for ; err == nil; line, err = buf.ReadString('\n') {
+ var mute bool
+ line = strings.TrimSpace(line)
+ mute = strings.HasSuffix(line, "m")
+ line = strings.TrimSuffix(line, "m")
+ vol, err := strconv.Atoi(line)
+ if err != nil {
+ w.errorf(err.Error())
+ continue
+ }
+ w.emitVol(vol, mute, update)
+ }
+ if err != nil {
+ w.errorf(err.Error())
+ }
+}
+
+func (w *Volume) emitVol(vol int, mute bool, update statusbar.UpdateFunc) {
+ icon := w.Icon
+ if mute {
+ icon = w.MuteIcon
+ }
+ status := w.fmt.Format(icon, vol)
+ update([]statusbar.Block{
+ {
+ FullText: status,
+ Align: "left",
+ Name: "volmon",
+ Instance: "0",
+ Markup: "pango",
+ },
+ })
+}
+
+func (w *Volume) init() {
+ w.prefix = "Volume"
+ if w.Format == "" {
+ w.Format = "{icon} {level}"
+ }
+ if w.Icon == "" {
+ w.Icon = "\ue892"
+ }
+ if w.MuteIcon == "" {
+ w.MuteIcon = "\ue890"
+ }
+ w.fmt = ParseFormat(w.Format, "icon:%s", "level:%d")
+ if w.VolMonCmd == "" {
+ w.VolMonCmd = "pavolmon"
+ }
+ if cmd, err := shlex.Split(w.VolMonCmd); err != nil {
+ w.errorf(err.Error())
+ } else {
+ w.volMonCmd = exec.Command(cmd[0], cmd[1:]...)
+ }
+ if w.VolUpCmd != "" {
+ if cmd, err := shlex.Split(w.VolUpCmd); err != nil {
+ w.errorf(err.Error())
+ } else {
+ w.volUpCmd = cmd
+ }
+ }
+ if w.VolDownCmd != "" {
+ if cmd, err := shlex.Split(w.VolDownCmd); err != nil {
+ w.errorf(err.Error())
+ } else {
+ w.volDownCmd = cmd
+ }
+ }
+ if w.MuteToggleCmd != "" {
+ if cmd, err := shlex.Split(w.MuteToggleCmd); err != nil {
+ w.errorf(err.Error())
+ } else {
+ w.muteToggleCmd = cmd
+ }
+ }
+ if w.OnClickCmd != "" {
+ if cmd, err := shlex.Split(w.OnClickCmd); err != nil {
+ w.errorf(err.Error())
+ } else {
+ w.onClickCmd = cmd
+ }
+ }
+}
+
+func init() {
+ registry.RegisterWidget(new(Volume))
+}