mirror of
https://codeberg.org/dnkl/yambar.git
synced 2025-04-19 19:25:41 +02:00
modules: add pulse
The pulse module shows information about PulseAudio sinks and sources.
This commit is contained in:
parent
54c70bb6ad
commit
dcf21f0b06
15 changed files with 653 additions and 7 deletions
|
@ -18,6 +18,7 @@ packages:
|
|||
- wlroots-dev
|
||||
- json-c-dev
|
||||
- libmpdclient-dev
|
||||
- libpulse
|
||||
- alsa-lib-dev
|
||||
- ttf-dejavu
|
||||
- gcovr
|
||||
|
|
|
@ -13,7 +13,7 @@ before_script:
|
|||
- apk add pixman-dev freetype-dev fontconfig-dev
|
||||
- apk add libxcb-dev xcb-util-wm-dev xcb-util-cursor-dev yaml-dev
|
||||
- apk add wayland-dev wayland-protocols wlroots-dev
|
||||
- apk add json-c-dev libmpdclient-dev alsa-lib-dev
|
||||
- apk add json-c-dev libmpdclient-dev alsa-lib-dev pulseaudio-dev
|
||||
- apk add ttf-dejavu
|
||||
- apk add git
|
||||
- apk add flex bison
|
||||
|
|
|
@ -37,7 +37,7 @@ pipeline:
|
|||
- apk add pixman-dev freetype-dev fontconfig-dev
|
||||
- apk add libxcb-dev xcb-util-wm-dev xcb-util-cursor-dev yaml-dev
|
||||
- apk add wayland-dev wayland-protocols wlroots-dev
|
||||
- apk add json-c-dev libmpdclient-dev alsa-lib-dev
|
||||
- apk add json-c-dev libmpdclient-dev alsa-lib-dev pulseaudio-dev
|
||||
- apk add ttf-dejavu
|
||||
- apk add git
|
||||
- apk add flex bison
|
||||
|
@ -95,7 +95,7 @@ pipeline:
|
|||
- apk add pixman-dev freetype-dev fontconfig-dev
|
||||
- apk add libxcb-dev xcb-util-wm-dev xcb-util-cursor-dev yaml-dev
|
||||
- apk add wayland-dev wayland-protocols wlroots-dev
|
||||
- apk add json-c-dev libmpdclient-dev alsa-lib-dev
|
||||
- apk add json-c-dev libmpdclient-dev alsa-lib-dev pulseaudio-dev
|
||||
- apk add ttf-dejavu
|
||||
- apk add git
|
||||
- apk add flex bison
|
||||
|
|
|
@ -27,7 +27,8 @@
|
|||
* network: request link stats and expose under tags `dl-speed` and
|
||||
`ul-speed` when `poll-interval` is set.
|
||||
* new module: disk-io.
|
||||
* alsa: `dB` tag ([#202][202])
|
||||
* new module: pulse ([#223][223]).
|
||||
* alsa: `dB` tag ([#202][202]).
|
||||
* mpd: `file` tag ([#219][219]).
|
||||
* on-click: support `next`/`previous` mouse buttons ([#228][228]).
|
||||
|
||||
|
@ -36,6 +37,7 @@
|
|||
[200]: https://codeberg.org/dnkl/yambar/issues/200
|
||||
[202]: https://codeberg.org/dnkl/yambar/issues/202
|
||||
[219]: https://codeberg.org/dnkl/yambar/pulls/219
|
||||
[223]: https://codeberg.org/dnkl/yambar/pulls/223
|
||||
[228]: https://codeberg.org/dnkl/yambar/pulls/228
|
||||
|
||||
|
||||
|
|
3
PKGBUILD
3
PKGBUILD
|
@ -1,5 +1,5 @@
|
|||
pkgname=yambar
|
||||
pkgver=1.8.0
|
||||
pkgver=1.8.0.r77.ge9a6994
|
||||
pkgrel=1
|
||||
pkgdesc="Simplistic and highly configurable status panel for X and Wayland"
|
||||
arch=('x86_64' 'aarch64')
|
||||
|
@ -15,6 +15,7 @@ depends=(
|
|||
'libudev.so'
|
||||
'json-c'
|
||||
'libmpdclient'
|
||||
'libpulse'
|
||||
'fcft>=3.0.0' 'fcft<4.0.0')
|
||||
optdepends=('xcb-util-errors: better X error messages')
|
||||
source=()
|
||||
|
|
|
@ -16,6 +16,7 @@ depends=(
|
|||
'libudev.so'
|
||||
'json-c'
|
||||
'libmpdclient'
|
||||
'libpulse'
|
||||
'fcft>=3.0.0' 'fcft<4.0.0')
|
||||
source=()
|
||||
|
||||
|
|
|
@ -86,6 +86,7 @@ Available modules:
|
|||
* mem
|
||||
* mpd
|
||||
* network
|
||||
* pulse
|
||||
* removables
|
||||
* river
|
||||
* script (see script [examples](examples/scripts))
|
||||
|
|
|
@ -10,6 +10,7 @@ foreach man_src : ['yambar.1.scd', 'yambar.5.scd', 'yambar-decorations.5.scd',
|
|||
'yambar-modules-foreign-toplevel.5.scd',
|
||||
'yambar-modules-i3.5.scd', 'yambar-modules-label.5.scd',
|
||||
'yambar-modules-mpd.5.scd', 'yambar-modules-network.5.scd',
|
||||
'yambar-modules-pulse.5.scd',
|
||||
'yambar-modules-removables.5.scd', 'yambar-modules-river.5.scd',
|
||||
'yambar-modules-script.5.scd', 'yambar-modules-sway-xkb.5.scd',
|
||||
'yambar-modules-sway.5.scd', 'yambar-modules-xkb.5.scd',
|
||||
|
|
67
doc/yambar-modules-pulse.5.scd
Normal file
67
doc/yambar-modules-pulse.5.scd
Normal file
|
@ -0,0 +1,67 @@
|
|||
yambar-modules-pulse(5)
|
||||
|
||||
# NAME
|
||||
pulse - Monitors a PulseAudio source and/or sink
|
||||
|
||||
# TAGS
|
||||
|
||||
[[ *Name*
|
||||
:[ *Type*
|
||||
:[ *Description*
|
||||
| online
|
||||
: bool
|
||||
: True when connected to the PulseAudio server
|
||||
| sink_online
|
||||
: bool
|
||||
: True when the sink is present
|
||||
| source_online
|
||||
: bool
|
||||
: True when the source is present
|
||||
| sink_percent
|
||||
: range
|
||||
: Sink volume level, as a percentage
|
||||
| source_percent
|
||||
: range
|
||||
: Source volume level, as a percentage
|
||||
| sink_muted
|
||||
: bool
|
||||
: True if the sink is muted, otherwise false
|
||||
| source_muted
|
||||
: bool
|
||||
: True if the source is muted, otherwise false
|
||||
| sink_port
|
||||
: string
|
||||
: Description of the active sink port
|
||||
| source_port
|
||||
: string
|
||||
: Description of the active source port
|
||||
|
||||
# CONFIGURATION
|
||||
|
||||
[[ *Name*
|
||||
:[ *Type*
|
||||
:[ *Req*
|
||||
:[ *Description*
|
||||
| sink
|
||||
: string
|
||||
: no
|
||||
: Name of sink to monitor (default: _@DEFAULT\_SINK@_).
|
||||
| source
|
||||
: string
|
||||
: no
|
||||
: Name of source to monitor (default: _@DEFAULT\_SOURCE@_).
|
||||
|
||||
# EXAMPLES
|
||||
|
||||
```
|
||||
bar:
|
||||
left:
|
||||
- pulse:
|
||||
content:
|
||||
string: {text: "{sink_percent}% ({sink_port})"}
|
||||
```
|
||||
|
||||
# SEE ALSO
|
||||
|
||||
*yambar-modules*(5), *yambar-particles*(5), *yambar-tags*(5), *yambar-decorations*(5)
|
||||
|
|
@ -150,6 +150,8 @@ Available modules have their own pages:
|
|||
|
||||
*yambar-modules-network*(5)
|
||||
|
||||
*yambar-modules-pulse*(5)
|
||||
|
||||
*yambar-modules-removables*(5)
|
||||
|
||||
*yambar-modules-river*(5)
|
||||
|
|
|
@ -131,7 +131,8 @@ yambar = executable(
|
|||
version,
|
||||
dependencies: [bar, libepoll, libinotify, pixman, yaml, threads, dl, tllist, fcft] +
|
||||
decorations + particles + modules,
|
||||
c_args: [plugin_mpd_enabled? '-DPLUGIN_ENABLED_MPD':[]],
|
||||
c_args: [plugin_mpd_enabled? '-DPLUGIN_ENABLED_MPD':[],
|
||||
plugin_pulse_enabled? '-DPLUGIN_ENABLED_PULSE':[]],
|
||||
build_rpath: '$ORIGIN/modules:$ORIGIN/decorations:$ORIGIN/particles',
|
||||
export_dynamic: true,
|
||||
install: true,
|
||||
|
@ -168,7 +169,10 @@ summary(
|
|||
)
|
||||
|
||||
summary(
|
||||
{'Music Player Daemon (MPD)': plugin_mpd_enabled},
|
||||
{
|
||||
'Music Player Daemon (MPD)': plugin_mpd_enabled,
|
||||
'PulseAudio': plugin_pulse_enabled,
|
||||
},
|
||||
section: 'Optional modules',
|
||||
bool_yn: true
|
||||
)
|
||||
|
|
|
@ -8,3 +8,6 @@ option(
|
|||
option(
|
||||
'plugin-mpd', type: 'feature', value: 'auto',
|
||||
description: 'Music Player Daemon (MPD) support')
|
||||
option(
|
||||
'plugin-pulse', type: 'feature', value: 'auto',
|
||||
description: 'PulseAudio support')
|
||||
|
|
|
@ -11,6 +11,9 @@ xcb_xkb = dependency('xcb-xkb', required: get_option('backend-x11'))
|
|||
mpd = dependency('libmpdclient', required: get_option('plugin-mpd'))
|
||||
plugin_mpd_enabled = mpd.found()
|
||||
|
||||
pulse = dependency('libpulse', required: get_option('plugin-pulse'))
|
||||
plugin_pulse_enabled = pulse.found()
|
||||
|
||||
# Module name -> (source-list, dep-list)
|
||||
mod_data = {
|
||||
'alsa': [[], [m, alsa]],
|
||||
|
@ -32,6 +35,10 @@ if plugin_mpd_enabled
|
|||
mod_data += {'mpd': [[], [mpd]]}
|
||||
endif
|
||||
|
||||
if plugin_pulse_enabled
|
||||
mod_data += {'pulse': [[], [pulse]]}
|
||||
endif
|
||||
|
||||
if backend_x11
|
||||
mod_data += {
|
||||
'xkb': [[], [xcb_stuff, xcb_xkb]],
|
||||
|
|
550
modules/pulse.c
Normal file
550
modules/pulse.c
Normal file
|
@ -0,0 +1,550 @@
|
|||
#include <math.h>
|
||||
#include <stdbool.h>
|
||||
#include <stdio.h>
|
||||
#include <stdlib.h>
|
||||
#include <string.h>
|
||||
|
||||
#include <unistd.h>
|
||||
#include <sys/timerfd.h>
|
||||
|
||||
#include <pulse/pulseaudio.h>
|
||||
|
||||
#define LOG_MODULE "pulse"
|
||||
#define LOG_ENABLE_DBG 0
|
||||
#include "../bar/bar.h"
|
||||
#include "../config-verify.h"
|
||||
#include "../config.h"
|
||||
#include "../log.h"
|
||||
#include "../plugin.h"
|
||||
|
||||
struct private {
|
||||
char *sink_name;
|
||||
char *source_name;
|
||||
struct particle *label;
|
||||
|
||||
bool online;
|
||||
|
||||
bool sink_online;
|
||||
pa_cvolume sink_volume;
|
||||
bool sink_muted;
|
||||
char *sink_port;
|
||||
uint32_t sink_index;
|
||||
|
||||
bool source_online;
|
||||
pa_cvolume source_volume;
|
||||
bool source_muted;
|
||||
char *source_port;
|
||||
uint32_t source_index;
|
||||
|
||||
int refresh_timer_fd;
|
||||
bool refresh_scheduled;
|
||||
|
||||
pa_mainloop *mainloop;
|
||||
pa_context *context;
|
||||
};
|
||||
|
||||
static void
|
||||
destroy(struct module *mod)
|
||||
{
|
||||
struct private *priv = mod->private;
|
||||
priv->label->destroy(priv->label);
|
||||
free(priv->sink_name);
|
||||
free(priv->source_name);
|
||||
free(priv->sink_port);
|
||||
free(priv->source_port);
|
||||
free(priv);
|
||||
module_default_destroy(mod);
|
||||
}
|
||||
|
||||
static const char *
|
||||
description(struct module *mod)
|
||||
{
|
||||
return "pulse";
|
||||
}
|
||||
|
||||
static struct exposable *
|
||||
content(struct module *mod)
|
||||
{
|
||||
struct private *priv = mod->private;
|
||||
|
||||
mtx_lock(&mod->lock);
|
||||
|
||||
pa_volume_t sink_volume_max = pa_cvolume_max(&priv->sink_volume);
|
||||
pa_volume_t source_volume_max = pa_cvolume_max(&priv->source_volume);
|
||||
int sink_percent = round(100.0 * sink_volume_max / PA_VOLUME_NORM);
|
||||
int source_percent = round(100.0 * source_volume_max / PA_VOLUME_NORM);
|
||||
|
||||
struct tag_set tags = {
|
||||
.tags = (struct tag *[]){
|
||||
tag_new_bool(mod, "online", priv->online),
|
||||
|
||||
tag_new_bool(mod, "sink_online", priv->sink_online),
|
||||
tag_new_int_range(mod, "sink_percent", sink_percent, 0, 100),
|
||||
tag_new_bool(mod, "sink_muted", priv->sink_muted),
|
||||
tag_new_string(mod, "sink_port", priv->sink_port),
|
||||
|
||||
tag_new_bool(mod, "source_online", priv->source_online),
|
||||
tag_new_int_range(mod, "source_percent", source_percent, 0, 100),
|
||||
tag_new_bool(mod, "source_muted", priv->source_muted),
|
||||
tag_new_string(mod, "source_port", priv->source_port),
|
||||
},
|
||||
.count = 9,
|
||||
};
|
||||
|
||||
mtx_unlock(&mod->lock);
|
||||
|
||||
struct exposable *exposable = priv->label->instantiate(priv->label, &tags);
|
||||
|
||||
tag_set_destroy(&tags);
|
||||
return exposable;
|
||||
}
|
||||
|
||||
static const char *
|
||||
context_error(pa_context *c)
|
||||
{
|
||||
return pa_strerror(pa_context_errno(c));
|
||||
}
|
||||
|
||||
static void
|
||||
abort_event_cb(pa_mainloop_api *api,
|
||||
pa_io_event *event,
|
||||
int fd,
|
||||
pa_io_event_flags_t flags,
|
||||
void *userdata)
|
||||
{
|
||||
struct module *mod = userdata;
|
||||
struct private *priv = mod->private;
|
||||
|
||||
pa_context_disconnect(priv->context);
|
||||
}
|
||||
|
||||
static void
|
||||
refresh_timer_cb(pa_mainloop_api *api,
|
||||
pa_io_event *event,
|
||||
int fd,
|
||||
pa_io_event_flags_t flags,
|
||||
void *userdata)
|
||||
{
|
||||
struct module *mod = userdata;
|
||||
struct private *priv = mod->private;
|
||||
|
||||
// Drain the refresh timer.
|
||||
uint64_t n;
|
||||
if (read(priv->refresh_timer_fd, &n, sizeof n) < 0)
|
||||
LOG_ERRNO("failed to read from timerfd");
|
||||
|
||||
// Clear the refresh flag.
|
||||
priv->refresh_scheduled = false;
|
||||
|
||||
// Refresh the bar.
|
||||
mod->bar->refresh(mod->bar);
|
||||
}
|
||||
|
||||
// Refresh the bar after a small delay. Without the delay, the bar
|
||||
// would be refreshed multiple times per event (e.g., a volume change),
|
||||
// and sometimes the active port would be reported incorrectly for a
|
||||
// brief moment. (This behavior was observed with PipeWire 0.3.61.)
|
||||
static void
|
||||
schedule_refresh(struct module *mod)
|
||||
{
|
||||
struct private *priv = mod->private;
|
||||
|
||||
// Do nothing if a refresh has already been scheduled.
|
||||
if (priv->refresh_scheduled)
|
||||
return;
|
||||
|
||||
// Start the refresh timer.
|
||||
struct itimerspec t = {
|
||||
.it_interval = { .tv_sec = 0, .tv_nsec = 0 },
|
||||
.it_value = { .tv_sec = 0, .tv_nsec = 50000000 },
|
||||
};
|
||||
timerfd_settime(priv->refresh_timer_fd, 0, &t, NULL);
|
||||
|
||||
// Set the refresh flag.
|
||||
priv->refresh_scheduled = true;
|
||||
}
|
||||
|
||||
static void
|
||||
set_server_online(struct module *mod)
|
||||
{
|
||||
struct private *priv = mod->private;
|
||||
|
||||
mtx_lock(&mod->lock);
|
||||
priv->online = true;
|
||||
mtx_unlock(&mod->lock);
|
||||
|
||||
schedule_refresh(mod);
|
||||
}
|
||||
|
||||
static void
|
||||
set_server_offline(struct module *mod)
|
||||
{
|
||||
struct private *priv = mod->private;
|
||||
|
||||
mtx_lock(&mod->lock);
|
||||
priv->online = false;
|
||||
priv->sink_online = false;
|
||||
priv->source_online = false;
|
||||
mtx_unlock(&mod->lock);
|
||||
|
||||
schedule_refresh(mod);
|
||||
}
|
||||
|
||||
static void
|
||||
set_sink_info(struct module *mod, const pa_sink_info *sink_info)
|
||||
{
|
||||
struct private *priv = mod->private;
|
||||
|
||||
mtx_lock(&mod->lock);
|
||||
|
||||
free(priv->sink_port);
|
||||
|
||||
priv->sink_online = true;
|
||||
priv->sink_index = sink_info->index;
|
||||
priv->sink_volume = sink_info->volume;
|
||||
priv->sink_muted = sink_info->mute;
|
||||
priv->sink_port = sink_info->active_port != NULL
|
||||
? strdup(sink_info->active_port->description)
|
||||
: NULL;
|
||||
|
||||
mtx_unlock(&mod->lock);
|
||||
|
||||
schedule_refresh(mod);
|
||||
}
|
||||
|
||||
static void
|
||||
set_sink_offline(struct module *mod)
|
||||
{
|
||||
struct private *priv = mod->private;
|
||||
|
||||
mtx_lock(&mod->lock);
|
||||
priv->sink_online = false;
|
||||
mtx_unlock(&mod->lock);
|
||||
|
||||
schedule_refresh(mod);
|
||||
}
|
||||
|
||||
static void
|
||||
set_source_info(struct module *mod, const pa_source_info *source_info)
|
||||
{
|
||||
struct private *priv = mod->private;
|
||||
|
||||
mtx_lock(&mod->lock);
|
||||
|
||||
free(priv->source_port);
|
||||
|
||||
priv->source_online = true;
|
||||
priv->source_index = source_info->index;
|
||||
priv->source_volume = source_info->volume;
|
||||
priv->source_muted = source_info->mute;
|
||||
priv->source_port = source_info->active_port != NULL
|
||||
? strdup(source_info->active_port->description)
|
||||
: NULL;
|
||||
|
||||
mtx_unlock(&mod->lock);
|
||||
|
||||
schedule_refresh(mod);
|
||||
}
|
||||
|
||||
static void
|
||||
set_source_offline(struct module *mod)
|
||||
{
|
||||
struct private *priv = mod->private;
|
||||
|
||||
mtx_lock(&mod->lock);
|
||||
priv->source_online = false;
|
||||
mtx_unlock(&mod->lock);
|
||||
|
||||
schedule_refresh(mod);
|
||||
}
|
||||
|
||||
static void
|
||||
sink_info_cb(pa_context *c, const pa_sink_info *i, int eol, void *userdata)
|
||||
{
|
||||
struct module *mod = userdata;
|
||||
|
||||
if (eol < 0) {
|
||||
LOG_ERR("failed to get sink info: %s", context_error(c));
|
||||
set_sink_offline(mod);
|
||||
} else if (eol == 0) {
|
||||
set_sink_info(mod, i);
|
||||
}
|
||||
}
|
||||
|
||||
static void
|
||||
source_info_cb(pa_context *c, const pa_source_info *i, int eol, void *userdata)
|
||||
{
|
||||
struct module *mod = userdata;
|
||||
|
||||
if (eol < 0) {
|
||||
LOG_ERR("failed to get source info: %s", context_error(c));
|
||||
set_source_offline(mod);
|
||||
} else if (eol == 0) {
|
||||
set_source_info(mod, i);
|
||||
}
|
||||
}
|
||||
|
||||
static void
|
||||
server_info_cb(pa_context *c, const pa_server_info *i, void *userdata)
|
||||
{
|
||||
LOG_INFO("%s, version %s", i->server_name, i->server_version);
|
||||
}
|
||||
|
||||
static void
|
||||
get_sink_info_by_name(pa_context *c, const char *name, void *userdata)
|
||||
{
|
||||
pa_operation *o =
|
||||
pa_context_get_sink_info_by_name(c, name, sink_info_cb, userdata);
|
||||
pa_operation_unref(o);
|
||||
}
|
||||
|
||||
static void
|
||||
get_source_info_by_name(pa_context *c, const char *name, void *userdata)
|
||||
{
|
||||
pa_operation *o =
|
||||
pa_context_get_source_info_by_name(c, name, source_info_cb, userdata);
|
||||
pa_operation_unref(o);
|
||||
}
|
||||
|
||||
static void
|
||||
get_sink_info_by_index(pa_context *c, uint32_t index, void *userdata)
|
||||
{
|
||||
pa_operation *o =
|
||||
pa_context_get_sink_info_by_index(c, index, sink_info_cb, userdata);
|
||||
pa_operation_unref(o);
|
||||
}
|
||||
|
||||
static void
|
||||
get_source_info_by_index(pa_context *c, uint32_t index, void *userdata)
|
||||
{
|
||||
pa_operation *o =
|
||||
pa_context_get_source_info_by_index(c, index, source_info_cb, userdata);
|
||||
pa_operation_unref(o);
|
||||
}
|
||||
|
||||
static void
|
||||
get_server_info(pa_context *c, void *userdata)
|
||||
{
|
||||
pa_operation *o = pa_context_get_server_info(c, server_info_cb, userdata);
|
||||
pa_operation_unref(o);
|
||||
}
|
||||
|
||||
static void
|
||||
subscribe(pa_context *c, void *userdata)
|
||||
{
|
||||
pa_subscription_mask_t mask = PA_SUBSCRIPTION_MASK_SERVER
|
||||
| PA_SUBSCRIPTION_MASK_SINK
|
||||
| PA_SUBSCRIPTION_MASK_SOURCE;
|
||||
pa_operation *o = pa_context_subscribe(c, mask, NULL, userdata);
|
||||
pa_operation_unref(o);
|
||||
}
|
||||
|
||||
static pa_context *
|
||||
connect_to_server(struct module *mod);
|
||||
|
||||
static void
|
||||
context_state_change_cb(pa_context *c, void *userdata)
|
||||
{
|
||||
struct module *mod = userdata;
|
||||
struct private *priv = mod->private;
|
||||
|
||||
pa_context_state_t state = pa_context_get_state(c);
|
||||
switch (state) {
|
||||
case PA_CONTEXT_UNCONNECTED:
|
||||
case PA_CONTEXT_CONNECTING:
|
||||
case PA_CONTEXT_AUTHORIZING:
|
||||
case PA_CONTEXT_SETTING_NAME:
|
||||
break;
|
||||
|
||||
case PA_CONTEXT_READY:
|
||||
set_server_online(mod);
|
||||
subscribe(c, mod);
|
||||
get_server_info(c, mod);
|
||||
get_sink_info_by_name(c, priv->sink_name, mod);
|
||||
get_source_info_by_name(c, priv->source_name, mod);
|
||||
break;
|
||||
|
||||
case PA_CONTEXT_FAILED:
|
||||
LOG_WARN("connection lost");
|
||||
set_server_offline(mod);
|
||||
pa_context_unref(priv->context);
|
||||
priv->context = connect_to_server(mod);
|
||||
break;
|
||||
|
||||
case PA_CONTEXT_TERMINATED:
|
||||
LOG_DBG("connection terminated");
|
||||
set_server_offline(mod);
|
||||
pa_mainloop_quit(priv->mainloop, 0);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
static void
|
||||
subscription_event_cb(pa_context *c,
|
||||
pa_subscription_event_type_t event_type,
|
||||
uint32_t index,
|
||||
void *userdata)
|
||||
{
|
||||
struct module *mod = userdata;
|
||||
struct private *priv = mod->private;
|
||||
|
||||
int facility = event_type & PA_SUBSCRIPTION_EVENT_FACILITY_MASK;
|
||||
int type = event_type & PA_SUBSCRIPTION_EVENT_TYPE_MASK;
|
||||
|
||||
switch (facility) {
|
||||
case PA_SUBSCRIPTION_EVENT_SERVER:
|
||||
get_sink_info_by_name(c, priv->sink_name, mod);
|
||||
get_source_info_by_name(c, priv->source_name, mod);
|
||||
break;
|
||||
|
||||
case PA_SUBSCRIPTION_EVENT_SINK:
|
||||
if (index == priv->sink_index) {
|
||||
if (type == PA_SUBSCRIPTION_EVENT_CHANGE)
|
||||
get_sink_info_by_index(c, index, mod);
|
||||
else if (type == PA_SUBSCRIPTION_EVENT_REMOVE)
|
||||
set_sink_offline(mod);
|
||||
}
|
||||
break;
|
||||
|
||||
case PA_SUBSCRIPTION_EVENT_SOURCE:
|
||||
if (index == priv->source_index) {
|
||||
if (type == PA_SUBSCRIPTION_EVENT_CHANGE)
|
||||
get_source_info_by_index(c, index, mod);
|
||||
else if (type == PA_SUBSCRIPTION_EVENT_REMOVE)
|
||||
set_source_offline(mod);
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
static pa_context *
|
||||
connect_to_server(struct module *mod)
|
||||
{
|
||||
struct private *priv = mod->private;
|
||||
|
||||
// Create connection context.
|
||||
pa_mainloop_api *api = pa_mainloop_get_api(priv->mainloop);
|
||||
pa_context *c = pa_context_new(api, "yambar");
|
||||
if (c == NULL) {
|
||||
LOG_ERR("failed to create PulseAudio connection context");
|
||||
return NULL;
|
||||
}
|
||||
|
||||
// Register callback functions.
|
||||
pa_context_set_state_callback(c, context_state_change_cb, mod);
|
||||
pa_context_set_subscribe_callback(c, subscription_event_cb, mod);
|
||||
|
||||
// Connect to server.
|
||||
pa_context_flags_t flags = PA_CONTEXT_NOFAIL
|
||||
| PA_CONTEXT_NOAUTOSPAWN;
|
||||
if (pa_context_connect(c, NULL, flags, NULL) < 0) {
|
||||
LOG_ERR("failed to connect to PulseAudio server: %s", context_error(c));
|
||||
pa_context_unref(c);
|
||||
return NULL;
|
||||
}
|
||||
|
||||
return c;
|
||||
}
|
||||
|
||||
static int
|
||||
run(struct module *mod)
|
||||
{
|
||||
struct private *priv = mod->private;
|
||||
int ret = -1;
|
||||
|
||||
// Create main loop.
|
||||
priv->mainloop = pa_mainloop_new();
|
||||
if (priv->mainloop == NULL) {
|
||||
LOG_ERR("failed to create PulseAudio main loop");
|
||||
return -1;
|
||||
}
|
||||
|
||||
// Create refresh timer.
|
||||
priv->refresh_timer_fd = timerfd_create(CLOCK_MONOTONIC, TFD_NONBLOCK);
|
||||
if (priv->refresh_timer_fd < 0) {
|
||||
LOG_ERRNO("failed to create timerfd");
|
||||
pa_mainloop_free(priv->mainloop);
|
||||
return -1;
|
||||
}
|
||||
|
||||
// Connect to server.
|
||||
priv->context = connect_to_server(mod);
|
||||
if (priv->context == NULL) {
|
||||
pa_mainloop_free(priv->mainloop);
|
||||
close(priv->refresh_timer_fd);
|
||||
return -1;
|
||||
}
|
||||
|
||||
// Poll refresh timer and abort event.
|
||||
pa_mainloop_api *api = pa_mainloop_get_api(priv->mainloop);
|
||||
api->io_new(api, priv->refresh_timer_fd, PA_IO_EVENT_INPUT,
|
||||
refresh_timer_cb, mod);
|
||||
api->io_new(api, mod->abort_fd, PA_IO_EVENT_INPUT | PA_IO_EVENT_HANGUP,
|
||||
abort_event_cb, mod);
|
||||
|
||||
// Run main loop.
|
||||
if (pa_mainloop_run(priv->mainloop, &ret) < 0) {
|
||||
LOG_ERR("PulseAudio main loop error");
|
||||
ret = -1;
|
||||
}
|
||||
|
||||
// Clean up.
|
||||
pa_context_unref(priv->context);
|
||||
pa_mainloop_free(priv->mainloop);
|
||||
close(priv->refresh_timer_fd);
|
||||
|
||||
return ret;
|
||||
}
|
||||
|
||||
static struct module *
|
||||
pulse_new(const char *sink_name,
|
||||
const char *source_name,
|
||||
struct particle *label)
|
||||
{
|
||||
struct private *priv = calloc(1, sizeof *priv);
|
||||
priv->label = label;
|
||||
priv->sink_name = strdup(sink_name);
|
||||
priv->source_name = strdup(source_name);
|
||||
|
||||
struct module *mod = module_common_new();
|
||||
mod->private = priv;
|
||||
mod->run = &run;
|
||||
mod->destroy = &destroy;
|
||||
mod->content = &content;
|
||||
mod->description = &description;
|
||||
return mod;
|
||||
}
|
||||
|
||||
static struct module *
|
||||
from_conf(const struct yml_node *node, struct conf_inherit inherited)
|
||||
{
|
||||
const struct yml_node *sink = yml_get_value(node, "sink");
|
||||
const struct yml_node *source = yml_get_value(node, "source");
|
||||
const struct yml_node *content = yml_get_value(node, "content");
|
||||
|
||||
return pulse_new(
|
||||
sink != NULL ? yml_value_as_string(sink) : "@DEFAULT_SINK@",
|
||||
source != NULL ? yml_value_as_string(source) : "@DEFAULT_SOURCE@",
|
||||
conf_to_particle(content, inherited));
|
||||
}
|
||||
|
||||
static bool
|
||||
verify_conf(keychain_t *chain, const struct yml_node *node)
|
||||
{
|
||||
static const struct attr_info attrs[] = {
|
||||
{"sink", false, &conf_verify_string},
|
||||
{"source", false, &conf_verify_string},
|
||||
MODULE_COMMON_ATTRS,
|
||||
};
|
||||
|
||||
return conf_verify_dict(chain, node, attrs);
|
||||
}
|
||||
|
||||
const struct module_iface module_pulse_iface = {
|
||||
.verify_conf = &verify_conf,
|
||||
.from_conf = &from_conf,
|
||||
};
|
||||
|
||||
#if defined(CORE_PLUGINS_AS_SHARED_LIBRARIES)
|
||||
extern const struct module_iface iface __attribute__((weak, alias("module_pulse_iface")));
|
||||
#endif
|
6
plugin.c
6
plugin.c
|
@ -44,6 +44,9 @@ EXTERN_MODULE(label);
|
|||
EXTERN_MODULE(mpd);
|
||||
#endif
|
||||
EXTERN_MODULE(network);
|
||||
#if defined(PLUGIN_ENABLED_PULSE)
|
||||
EXTERN_MODULE(pulse);
|
||||
#endif
|
||||
EXTERN_MODULE(removables);
|
||||
EXTERN_MODULE(river);
|
||||
EXTERN_MODULE(sway_xkb);
|
||||
|
@ -129,6 +132,9 @@ init(void)
|
|||
REGISTER_CORE_MODULE(mpd, mpd);
|
||||
#endif
|
||||
REGISTER_CORE_MODULE(network, network);
|
||||
#if defined(PLUGIN_ENABLED_PULSE)
|
||||
REGISTER_CORE_MODULE(pulse, pulse);
|
||||
#endif
|
||||
REGISTER_CORE_MODULE(removables, removables);
|
||||
#if defined(HAVE_PLUGIN_river)
|
||||
REGISTER_CORE_MODULE(river, river);
|
||||
|
|
Loading…
Add table
Reference in a new issue