С момента написания статьи об xmonad кое-что поменялось и я перешёл на использование openbox. Поменялись и панели, теперь я использую lxpanel, который разрабатывается для lxde, легковесного рабочего стола, основанного на openbox. Отображаемые lxpanel элементы являются плагинами, динамически загружаемыми библиотеками. Сегодня мы напишем свой плагин для lxpanel.
Так как в стандартной поставке lxpanel виджет для регулировки громкости не очень хорошо (скорее очень нехорошо) отображает текущий уровень громкости, то я решил написать свой плагин, который исправит ситуацию.
Эта статья будет состоять из двух частей. Первая будет посвящена работе с pulseaudio (читается [pʌlsˈɔːdɪəu] пaлсодыо), а вторая — lxpanel.


На скриншоте выше результат можно разглядеть слева.
Писать мы будем на C. Да, именно на C, так как заголовочный файл lxpanel является ярким представителем того, как можно писать на C несовместимый с C++ код.

Внимание, весь код, приведённый в этой статье распространяется по лицензии GNU GPL версии 3 и выше!

Монитор Pulseaudio

Начиная работать с pulseaudio натыкаешься на, мягко говоря, скудную документацию, основу которой составляет сгенерированная doxygen`ом. Из документации мы узнаём, что у pulseaudio есть два типа API: simple и asynchronous. Беглый взгляд на simple API позволяет понять, что придётся использовать асинхронный API.
Основным элементом для работы с асинхронным API pulseaudio является контекст, представленный типом pa_context. Контекст представляет собой мост между приложением и демоном pulseaudio. Чтобы контекст работал, ему нужен mainloop. Pulseaudio представляет возможность использовать три варианта реализации основного цикла:

Из перечисленных вариантов нам больше всего подходит последний, так как lxpanel написана на GTK+ 2 и уже имеет запущенный GMainLoop. Остальные варианты приведут либо к зависанию lxpanel, либо к сложной и запутанной архитектуре.
Так как на машине может быть несколько звуковых устройств, то их надо как-то различать. Pulseaudio использует для этого понятие sink (не знаю, как правильно перевести). Sink`и имеют свой идентификатор или индекс, который является порядковым номером, начиная с нуля.
Итак, на основе полученной информации мы уже можем накидать интерфейс, который мы будем использовать для получения информации от pulseaudio.

#include <stdint.h>

#define PAMON_UNUSED(var) (void)var

typedef struct Pamon Pamon;

typedef enum
{
PAMON_ERROR_CONNECTION_FAILED,
PAMON_ERROR_INVALID_SINK
} PamonError;

typedef void (* PamonVolumeChangedCallback)(uint32_t current_volume_percents, void * userdata);
typedef void (* PamonErrorCallback)(PamonError error, void * userdata);

typedef struct
{
uint32_t sink_id;
PamonVolumeChangedCallback volume_callback;
PamonErrorCallback error_callback;
void * userdata;
} PamonInitData;

Pamon * pamonInit(PamonInitData * init_data);

void pamonSetSinkId(Pamon * pamon, uint32_t id);

int pamonStart(Pamon * pamon);

void pamonClose(Pamon * pamon);

const char * pamonTranslateError(PamonError error);

Немного поясню. Тип PamonError используется для сообщения об ошибке. Значение PAMON_ERROR_CONNECTION_FAILED говорит о том, что соединиться с демоном pulseaudio не удалось, а PAMON_ERROR_INVALID_SINK сообщает о неверном идентификаторе sink`а. Функция pamonTranslateError переводит идентификатор ошибки в понятный человеку вид, в строку.
Фунции pamonInit и pamonClose соответственно инициализируют и закрывают соединение с демоном pulseaudio. Для инициализации соединения в функцию pamonInit передаётся структура PamonInitData содержащая идентификатор sink`а и колбеки для уведомления о смене громкости и об ошибке. Кроме того структура  PamonInitData содержит поле userdata, которое будет передано в колбеки.
Назначение функции pamonSetSinkId должно быть ясно без пояснений.
Используя этот интерфейс можно написать консольное приложение, которое будет выводить текущий уровень громкости.

#include <stdlib.h>
#include <stdio.h>
#include <string.h>
#include <signal.h>
#include <unistd.h>
#include <glib.h>
#include "pamon.h"

#define APP_NAME "pamon"
#define DEFAULT_FORMAT "%i%%"

typedef struct
{
char * format;
uint32_t sink;
int print_help;
} Options;

static void pamonVolumeChanged(uint32_t current_volume, void * userdata);
static void pamonError(PamonError error, void * userdata);
static void onSignal(int signum);
static void parseOpts(int argc, char ** argv, Options * result);
static void printUsage();

static Pamon * pamon = NULL;
static GMainLoop * main_loop = NULL;
static char * format = NULL;


int main(int argc, char ** argv)
{
struct sigaction action;
memset(&action, 0, sizeof(struct sigaction));
action.sa_handler = onSignal;
sigaction(SIGINT, &action, NULL);
PamonInitData pamon_init_data;
memset(&pamon_init_data, 0, sizeof(PamonInitData));
{
Options opts;
memset(&opts, 0, sizeof(Options));
parseOpts(argc, argv, &opts);
if(opts.print_help)
{
printUsage();
free(opts.format);
return EXIT_SUCCESS;
}
pamon_init_data.sink_id = opts.sink;
format = opts.format;
}
pamon_init_data.volume_callback = pamonVolumeChanged;
pamon_init_data.error_callback = pamonError;
main_loop = g_main_loop_new(NULL, FALSE);
pamon = pamonInit(&pamon_init_data);
g_main_loop_run(main_loop);
return EXIT_SUCCESS;
}

void onSignal(int signum)
{
pamonClose(pamon);
g_main_loop_quit(main_loop);
g_main_loop_unref(main_loop);
free(format);
printf("\nquit\n");
exit(EXIT_SUCCESS);
}

void pamonVolumeChanged(uint32_t current_volume, void * userdata)
{
PAMON_UNUSED(userdata);
printf(format ? format : DEFAULT_FORMAT, current_volume);
printf("\n");
fflush(stdout);
}

void pamonError(PamonError error, void * userdata)
{
PAMON_UNUSED(userdata);
fprintf(stderr, "Error: %s\n", pamonTranslateError(error));
}

void parseOpts(int argc, char ** argv, Options * result)
{
for(;;)
{
switch(getopt(argc, argv, "f:s:"))
{
case -1:
return;
case '?':
result->print_help = 1;
break;
case 'f':
result->format = (char *)malloc(strlen(optarg) + 1);
strcpy(result->format, optarg);
break;
case 's':
sscanf(optarg, "%i", &result->sink);
break;
default:
break;
}
}
}

void printUsage()
{
printf(
"Usage: %s [-s sink] [-f format]\n"
" sink - pulseaudio sink id\n"
" format - print format. Use %%i to print current volume value. Use %%%% to print the %% mark\n"
"\n"
"Use Ctrl+C to exit program\n",
APP_NAME);
fflush(stdout);
}

Здесь мы получаем из опций приложения идентификатор sink`а и формат сообщения. Создаём основной цикл GLib и запускам pamon, передав функции pamonVolumeChanged и pamonError в качестве колбеков. Программа завершается по нажатию Ctrl+C. Обработчик сигнала SIGINT позволит освободить занятые ресурсы перед выходом, хотя это и не обязательно.

Перейдём к самому интересному, к реализации интерфейса взаимодействия с pulseaudio.

#include <stdlib.h>
#include <string.h>
#include <math.h>
#include <pulse/pulseaudio.h>
#include <pulse/glib-mainloop.h>
#include "pamon.h"

struct Pamon
{
pa_context * pulse_context;
pa_glib_mainloop * pulse_glib_mainloop;
PamonVolumeChangedCallback volume_callback;
PamonErrorCallback error_callback;
int sink_id;
void * userdata;
};

Pamon * pamonInit(PamonInitData * init_data)
{
Pamon * pamon = (Pamon *)malloc(sizeof(Pamon));
memset(pamon, 0, sizeof(Pamon));
if(init_data)
{
pamon->userdata = init_data->userdata;
pamon->volume_callback = init_data->volume_callback;
pamon->error_callback = init_data->error_callback;
pamon->sink_id = init_data->sink_id;
}
pamon->pulse_glib_mainloop = pa_glib_mainloop_new(NULL);
pa_mainloop_api * api = pa_glib_mainloop_get_api(pamon->pulse_glib_mainloop);
pamon->pulse_context = pa_context_new(api, NULL);
pa_context_set_state_callback(pamon->pulse_context,
(pa_context_notify_cb_t)pulseContextStateCallback, pamon);
pa_context_connect(pamon->pulse_context, NULL, 0, NULL);
return pamon;
}

Функция инициализации создаёт и заполняет экземпляр структкры Pamon. Затем создаётся основной цикл из дефолтного контекста GLib и, после получения API основного цикла, инициализируется контекст. pa_mainloop_api служит абстрактным интерфейсом для работы со всеми тремя типами основных циклов. Так как мы используем асинхронные API и соединение с демоном pulseaudio происходит в отдельном потоке, функция pa_context_connect может сообщить о своём результате только в функцию обратного вызова, которую мы и устанавливаем с помощью pa_context_set_state_callback. При установке колбеков мы можем передавать пользовательские данные, которые будут отданы колбеку. Здесь и далее мы будем использовать указатель на структуру Pamon, хранящую всю служебную информацию.

void pulseContextStateCallback(pa_context * context, Pamon * pamon)
{
switch(pa_context_get_state(context))
{
case PA_CONTEXT_READY:
pa_context_set_subscribe_callback(context, (pa_context_subscribe_cb_t)pulseContextCallback, pamon);
pa_context_subscribe(context, PA_SUBSCRIPTION_MASK_SINK, NULL, NULL);
performSinkHandler(context, pamon);
break;
case PA_CONTEXT_FAILED:
performError(pamon, PAMON_ERROR_CONNECTION_FAILED);
break;
default:
break;
}
}

Здесь мы подписываемся на события контекста с помощью функции pa_context_subscribe, устнавив колбек вызовом pa_context_set_subscribe_callback. Так как нам интересны только события от sink`ов, то передаём маску PA_SUBSCRIPTION_MASK_SINK для фильтрации того, что нам будет приходить в колбек.
Если произошла ошибка, о ней сообщаем вызовом performError, который, по сути, зовёт колбек, переданный при инициализации.

void performError(Pamon * pamon, PamonError error)
{
if(pamon->error_callback)
pamon->error_callback(error, pamon->userdata);
}

Кроме подписки на события, мы форсируем получение значения громкости для изначальной инициализации подписчика.

void performSinkHandler(pa_context * context, Pamon * pamon)
{
pa_operation * operation = pa_context_get_sink_info_by_index(context, pamon->sink_id,
(pa_sink_info_cb_t)pulseSinkInfoCallback, pamon);
pa_operation_unref(operation);
}

Как видно, функция performSinkHandler выполняет только одно действие — просит информацию о sink`е по его идентификатору вызовом функции pa_context_get_sink_info_by_index. Так как информация о ходе выполнения асинхронной операции нам не интересна, то сразу же отпускаем ссылку на экземпляр pa_operation вызвав pa_operation_unref. Итак, информация о sink`е приходит в функцию pulseSinkInfoCallback.

void pulseSinkInfoCallback(pa_context * context, const pa_sink_info * info, int eol, Pamon * pamon)
{
if(eol == -1)
{
performError(pamon, PAMON_ERROR_INVALID_SINK);
}
if(info && pamon && pamon->volume_callback)
{
uint32_t volume = info->mute || !info->volume.channels ? 0 : info->volume.values[0];
uint32_t percents = (uint32_t)round((double)(volume * 100) / PA_VOLUME_NORM);
pamon->volume_callback(percents, pamon->userdata);
}
}

Я не нашёл документации о параметре eol, но методом научного тыка выяснил, что при ошибке этот флаг принимает значение -1.
Структура pa_sink_info содержит всю необходимую нам информацию. Уровень громкости тут содержится не в процентах, а в довольно больших числах. Кроме того, уровень громкости можно получить для каждого канала отдельно. Поле channels содержит количество доступных каналов, а массив values, состоящий из 32-ух целочисленных значений имеет инициализированными ровно channels первых элементов. Здесь я схалтурил и взял первое попавшееся значение. Карту соответствия индексов массива с реальными каналами можно посмотреть в поле channel_map структуры pa_sink_info.
Высчитать текущий уровень громкости в процентах помогает константа PA_VOLUME_NORM, которая содержит значение громкости при 100%, 65536.

На этом наша работа с pulseaudio заканчивается. Осталось только корректно завершить работу.

void pamonClose(Pamon * pamon)
{
if(!pamon) return;
pa_context_disconnect(pamon->pulse_context);
pa_context_unref(pamon->pulse_context);
pa_glib_mainloop_free(pamon->pulse_glib_mainloop);
free(pamon);
}

Для этого отсоединяемся от демона pulseaudio c помощью вызова pa_context_disconnect, отпускаем ссылку на контекст с помощью pa_context_unref и высвобождаем память, занятую основным циклом используя pa_glib_mainloop_free. Естественно не забываем и о нашей структуре, хранящей служебную информацию. Следует обратить внимание, что для завершения работы с другими типами mainloop`ов нужно выполнять действия, соответствующие выбранному типу.

За бортом остались две функции: pamonSetSinkId и pamonTranslateError. Их назначение понятно, а код прост, поэтому приведу его без комментариев.

#define ERSTR_UNKNOWN "Unknown error"
#define ERSTR_INVALID_SINK "Invalid sink"
#define ERSTR_CONNECTION_FAILED "Unable to connect to the pulseaudio daemon"

void pamonSetSinkId(Pamon * pamon, uint32_t id)
{
if(!pamon || pamon->sink_id == id)
return;
pamon->sink_id = id;
performSinkHandler(pamon->pulse_context, pamon);
}

const char * pamonTranslateError(PamonError error)
{
switch(error)
{
case PAMON_ERROR_CONNECTION_FAILED:
return ERSTR_CONNECTION_FAILED;
case PAMON_ERROR_INVALID_SINK:
return ERSTR_INVALID_SINK;
default:
return ERSTR_UNKNOWN;
}
}

lxpanel

Полученное приложение уже можно использовать, например, в conky или dzen2. Так

выглядит dzen2, запущенный командой

$ ./pamon -f " Current volume: %i%%" | dzen2 -bg "#505050" -fg white -ta left

Но наша цель — встроить монитор в lxpanel, поэтому создаём ещё один файл, lxpamon.c, который будем компилировать в динамическую библиотеку.
Первое, что нам надо сделать — это подключить необходимые заголовочные файлы.

#include <stdlib.h>
#include <glib/gi18n.h>
#include <gtk/gtk.h>
#include <lxpanel/plugin.h>
#include "pamon.h"

Файл lxpanel/plugin.h и есть тот самый файл, который не способен переварить компилятор C++.
Документации по написанию плагинов для lxpanel ещё меньше, чем документации по pulseaudio, а точнее — ровно одна страничка. Даже зачитав её до дыр, Вы не сможете написать полноценный плагин. Для того, чтобы написать свой, мне пришлось посмотреть в исходинки плагинов, поставляемых с lxpanel. Я скачал исходники из репозитория debian по этой ссылке. Файл src/plugins/dclock.c, содержащий исходники плагина с часами, оказался наиболее компактным и полезным.
Итак, для того, чтобы встроить плагин в панель, мы должны собрать библиотеку и положить её в  /usr/lib/lxpanel/plugins/ (или в  /usr/lib/x86_64-linux-gnu/lxpanel/plugins/ для мультиархитектурного debian`а). Есть одна особенность, имя файла не должно содержать префикс lib и иметь имя <имя_плагина>.so. В нашем случае файл будет называться lxpamon.so.
Библиотека с плагином должна экспозить экземпляр структуры PluginClass (к сожалению, ссылок мне давать не на что). Но не просто экспозить, а экспозить с определённым именем. Имя должно иметь вид <имя_плагина>_plugin_class. То есть, наш экземпляр будет иметь имя lxpamon_plugin_class.

PluginClass lxpamon_plugin_class = {
PLUGINCLASS_VERSIONING,
type: "lxpamon",
name: N_("Pulseaudio volume monitor"),
version: "1.0",
description: N_("Pulseaudio volume monitor plugin for LXPanel"),
one_per_system: FALSE,
expand_available: FALSE,
constructor: lxpamonConstructor,
destructor: lxpamonDestructor,
config: lxpamonConfigure,
save: lxpamonSaveConfig,
panel_configuration_changed: lxpamonPanelConfigurationChanged
};

Структура PluginClass описывает наш плагин. Макрос PLUGINCLASS_VERSIONING заполняет поля для определения версии структуры. Поле type должно содержать имя нашего плагина. Поля version и description вполне понятны. Флаг one_per_system позволяет ограничит количество активных экземпляров плагина одной штукой, а флаг expand_available разрешает растягивать виджет на всю свободную область панели (как tasklist или spacer). Макрос N_ объявлен в GLib и предназначен для интернационализации.
Оставшиеся поля контролируют жизненный цикл плагина, это следующие процедуры обратного вызова:

  • constructor — инициализирует плагин;
  • destructor — вызывается перед выгрузкой плагина;
  • config — колбек, который будет зваться для редактирования настроек (например, при нажатии на кнопку Edit в настройках элементов панели);
  • save — зовётся для сохранения настроек в конфиг;
  • panel_configuration_changed — колбек, сообщающий, что конфигурация самой панели изменилась.

Для того, чтобы хранить текущее состояние плагина, будем использовать структуру PluginData.

typedef struct
{
Pamon * pamon;
GtkWidget * label;
uint32_t current_volume;
int sink;
char * format;
int bold_font;
} PluginData;

И сразу же приведу код конструктора.

int lxpamonConstructor(Plugin * plugin, char ** config)
{
GtkWidget * label = gtk_label_new(NULL);
plugin->pwid = gtk_event_box_new();
gtk_container_add(GTK_CONTAINER(plugin->pwid), GTK_WIDGET(label));
g_signal_connect(G_OBJECT(plugin->pwid), "button_press_event",
G_CALLBACK(plugin_button_press_event), plugin);
gtk_widget_show_all(plugin->pwid);
PluginData * plug_data = g_new0(PluginData, 1);
plug_data->label = label;
lxpamonInitConfig(config, plug_data);
plugin->priv = plug_data;
PamonInitData pamon_init_data;
memset(&pamon_init_data, 0, sizeof(PamonInitData));
pamon_init_data.volume_callback = (PamonVolumeChangedCallback)pamonVolumeChanged;
pamon_init_data.error_callback = (PamonErrorCallback)pamonError;
pamon_init_data.userdata = plugin;
pamon_init_data.sink_id = plug_data->sink;
Pamon * pamon = pamonInit(&pamon_init_data);
plug_data->pamon = pamon;
return TRUE;
}

Здесь создаются GTK+ контейнер и label, который кладётся в контейнер. Вообще говоря, Вы может создавать любые виджеты. Осонвное, что Вы должны сделать — положить указатель на основной виджет в поле pwid структуры Plugin, которая передаётся на вход конструктору. Вообще, указатель на структуру Plugin будет передаваться во все колбеки. Эта структура содержит кое-какие полезные поля, с которыми мы познакомимся по ходу дела. Поле priv предназначено для хранения пользовательских данных, поэтому положим туда указатель на объект структуры PluginData.
Вторым параметром в конструктор передаётся конфиг, который мы читаем в функции lxpamonInitConfig. Сохраняется конфиг, как уже было сказано, функцией lxpamonSaveConfig.

#define DEFAULT_FORMAT "%i%%"
#define CONFIG_FORMAT "Format"
#define CONFIG_SINK "Sink"
#define CONFIG_BOLD "BoldFont"

void lxpamonInitConfig(char ** src, PluginData * plug_data)
{
line str;
str.len = 256;
while(lxpanel_get_line(src, &str))
{
if(strcmp(CONFIG_FORMAT, str.t[0]) == 0)
plug_data->format = g_strdup(str.t[1]);
else if(strcmp(CONFIG_SINK, str.t[0]) == 0)
sscanf(str.t[1], "%i", &plug_data->sink);
else if(strcmp(CONFIG_BOLD, str.t[0]) == 0)
plug_data->bold_font = str2num(bool_pair, str.t[1], 0);
}
if(!plug_data->format)
plug_data->format = g_strdup(DEFAULT_FORMAT);
}

void lxpamonSaveConfig(Plugin * plugin, FILE * file)
{
PluginData * plug_data = (PluginData *)plugin->priv;
lxpanel_put_str(file, CONFIG_FORMAT, plug_data->format);
lxpanel_put_int(file, CONFIG_SINK, plug_data->sink);
lxpanel_put_bool(file, CONFIG_BOLD, plug_data->bold_font);
}

Для записи данных в конфиг, используются макросы lxpanel_put_str, lxpanel_put_bool и lxpanel_put_int, кторые принемают файл, имя параметра и значение. Наш конфиг будет выглядеть примерно так

Plugin {
type = lxpamon
Config {
Format=%i%%
Sink=0
BoldFont=1
}
}

Конфиги лежат в ~/.config/lxpanel/default/panels/<имя_панели>. Для последовательного чтения записей из конфига следует использовать функцию lxpanel_get_line. Она записывает в свой второй параметр пару имя-значение из конфига. Как только пары кончатся, функция вернёт ноль. Функция str2num хоть и кажется, что переводит строку в число, но на деле она работает только с предопределёнными соответствиями, которые передаются в первом параметре. Например, bool_pair представлен примерно так { {«0», 0}, {«1», 1 } }.

Для того, чтобы пользователь смог задать настройки, мы должны показать диалог при обработке соответствующего события.

void lxpamonConfigure(Plugin * plugin, GtkWindow * parent)
{
PluginData * data = (PluginData *)plugin->priv;
GtkWidget * dlg = create_generic_config_dlg(_(plugin->class->name), GTK_WIDGET(parent),
(GSourceFunc)lxpamonApplyConfiguration, plugin,
_("Pulseaudio sink"), &data->sink, CONF_TYPE_INT,
_("Volume display format"), &data->format, CONF_TYPE_STR,
_("Use %i to print current volume. Use %% to print the % mark."), NULL, CONF_TYPE_TRIM,
_("Bold font"), &data->bold_font, CONF_TYPE_BOOL,
NULL);
gtk_window_present(GTK_WINDOW(dlg));
}

Здесь используется функция create_generic_config_dlg, показывающая стандартный диалог с настройками.

Вся проблема в том, что этой функции нет в файле lxpanel/plugin.h. Она объявлена в самой панели, но стандартные плагины используют именно её. Поэтому мы добавим себе её объявление.

extern GtkWidget * create_generic_config_dlg(const char * title, GtkWidget * parent,
GSourceFunc apply_func, Plugin * plugin, const char * name, ...);

Эта функция принимает родителя, заголовок и колбек, который будет вызван для применения настроек. В списке неопределённых параметров функция принимает тройки «имя параметра», «указатель на значение» и «тип значения». Завершается список параметров NULL`ом. Макрос _ объявлен в GLib и нужен для интернационализации. При принятии параметров мы устанавливаем новый sink для монитора и перерисовываем виджет

void lxpamonApplyConfiguration(Plugin * plugin)
{
PluginData * plug_data = (PluginData *)plugin->priv;
pamonSetSinkId(plug_data->pamon, plug_data->sink);
lxpamonDrawVolume(plugin);
}

static void lxpamonDrawVolume(Plugin * plugin)
{
PluginData * plug_data = (PluginData *)plugin->priv;
char * format = plug_data->format ? plug_data->format : "%%i";
char * buf = (char *)malloc(strlen(format) + 4);
if(INVALID_VOLUME == plug_data->current_volume)
strcpy(buf, "ERROR");
else
sprintf(buf, plug_data->format, plug_data->current_volume);
panel_draw_label_text(plugin->panel, plug_data->label, buf, plug_data->bold_font, TRUE);
free(buf);
}

При отрисовке мы снова используем функцию, которая не объявлена в lxpanel/plugin.h.

extern void panel_draw_label_text(Panel * p, GtkWidget * label, char * text,
gboolean bold, gboolean custom_color);

Функция panel_draw_label_text рисует текст на лейбле цветом, заданным в конфигурации панели. Структура Plugin содержит поле panel, у которого в свою очередь есть поля, хранящие цвета панели, в том числе цвет шрифта. Но есть и флаг usefontcolor, который установлен в 0, а при попытке использовать значение цвета получается что-то красное. Функция panel_draw_label_text также позволяет рисовать полужирным шрифтом, чем мы тоже пользуемся.
Значение INVALID_VOLUME устанавливается при обработке ошибки от pamon. В этом случае наш плагин напишет слово «ERROR».

Функция panel_draw_label_text не последняя из тех, которые забыли положить в файл lxpanel/plugin.h, ещё нам нужен стандартный обработчик клика мыши.

extern gboolean plugin_button_press_event(GtkWidget * widget, GdkEventButton * event, Plugin * plugin);

Эту функцию мы повесели на «button_press_event» в конструкторе. Эта функция добавляет в контекстное меню пункты для редактирования настроек и удаления виджета с панели.

После того, как настройки панели изменены нам нужно перерисовать свой виджет. Именно для этого мы и подписывались на это событие.

void lxpamonPanelConfigurationChanged(Plugin * plugin)
{
lxpamonDrawVolume(plugin);
}

Обработчики событий от pulseaudio тривиальны и не нуждаются в комментариях за одним исключением, ошибка может произойти до того, как поле plugin->priv будет присвоено, поэтому его нужно проверить.

void lxpamonPanelConfigurationChanged(Plugin * plugin)
{
lxpamonDrawVolume(plugin);
}

void pamonVolumeChanged(uint32_t current_volume, Plugin * plugin)
{
PluginData * plug_data = (PluginData *)plugin->priv;
plug_data->current_volume = current_volume;
lxpamonDrawVolume(plugin);
}

void pamonError(PamonError error, Plugin * plugin)
{
fprintf(stderr, "Pulseaudio monitor error: %s\n", pamonTranslateError(error));
PluginData * plug_data = (PluginData *)plugin->priv;
if(plug_data)
{
plug_data->current_volume = INVALID_VOLUME;
lxpamonDrawVolume(plugin);
}
}

Последнее, что осталось сделать — освободить ресурсы при выгрузке плагина. Здесь это гораздо важнее, чем в консольном приложении, так как панель может продолжать работать ещё долгое время и утекшая память не вернётся системе.

void lxpamonDestructor(Plugin * plugin)
{
PluginData * plug_data = (PluginData *)plugin->priv;
pamonClose(plug_data->pamon);
g_free(plug_data->format);
g_free(plug_data);
}

Поле plugin->pwid осовобождать не нужно, lxpanel это сделает за Вас.

P.S.

Исходники проекта лежат на github.

git clone https://github.com/brainstream/lxpamon.git

Возможно, проект я буду дорабатывать, если кому интересна изначальная версия, она лежит здесь.

Получившийся результат можно посмотреть на видео ниже