wpctl: add list subcommand to show objects in a more script-friendly format

This commit is contained in:
Sergey Veselkov 2026-03-09 21:40:02 +03:00
parent bda1c6db43
commit 88495d5da1

View file

@ -35,12 +35,31 @@ struct _WpCtl
gint exit_code;
};
typedef enum {
LIST_MEDIA_ALL = 0,
LIST_MEDIA_AUDIO,
LIST_MEDIA_VIDEO,
} ListMediaType;
typedef enum {
LIST_OBJECT_ALL = 0,
LIST_OBJECT_DEVICES,
LIST_OBJECT_SINKS,
LIST_OBJECT_SOURCES,
} ListObjectType;
static struct {
union {
struct {
gboolean display_nicknames;
gboolean display_names;
} status;
struct {
ListMediaType media_type;
ListObjectType object_type;
} list;
struct {
guint64 id;
gboolean show_referenced;
@ -543,6 +562,146 @@ status_run (WpCtl * self)
g_main_loop_quit (self->loop);
}
/* list */
static gboolean
list_parse_positional (gint argc, gchar ** argv, GError **error)
{
cmdline.list.media_type = LIST_MEDIA_ALL;
cmdline.list.object_type = LIST_OBJECT_ALL;
if (argc < 3)
return TRUE;
if (g_strcmp0 (argv[2], "audio") == 0) {
cmdline.list.media_type = LIST_MEDIA_AUDIO;
} else if (g_strcmp0 (argv[2], "video") == 0) {
cmdline.list.media_type = LIST_MEDIA_VIDEO;
} else {
g_set_error (error, wpctl_error_domain_quark(), 0,
"'%s' is not a valid list option", argv[2]);
return FALSE;
}
if (argc < 4)
return TRUE;
if (g_strcmp0 (argv[3], "devices") == 0) {
cmdline.list.object_type = LIST_OBJECT_DEVICES;
} else if (g_strcmp0 (argv[3], "sinks") == 0) {
cmdline.list.object_type = LIST_OBJECT_SINKS;
} else if (g_strcmp0 (argv[3], "sources") == 0) {
cmdline.list.object_type = LIST_OBJECT_SOURCES;
} else {
g_set_error (error, wpctl_error_domain_quark(), 0,
"'%s' is not a valid list option", argv[3]);
return FALSE;
}
return TRUE;
}
static gboolean
list_prepare (WpCtl * self, GError ** error)
{
wp_object_manager_add_interest (self->om, WP_TYPE_DEVICE, NULL);
wp_object_manager_add_interest (self->om, WP_TYPE_NODE, NULL);
wp_object_manager_add_interest (self->om, WP_TYPE_METADATA, NULL);
return TRUE;
}
static void
list_print_device (const GValue *item, gpointer data)
{
WpPipewireObject *obj = g_value_get_object (item);
guint32 id = wp_proxy_get_bound_id (WP_PROXY (obj));
const gchar *name = wp_pipewire_object_get_property (obj, PW_KEY_DEVICE_NAME);
printf ("%u\t%s\t \n", id, name);
}
static void
list_print_dev_node (const GValue *item, gpointer data)
{
WpPipewireObject *obj = g_value_get_object (item);
guint32 *default_node = data;
guint32 id = wp_proxy_get_bound_id (WP_PROXY (obj));
gboolean is_default = (*default_node == id);
const gchar *name = wp_pipewire_object_get_property (obj, PW_KEY_NODE_NAME);
printf ("%u\t%s\t%c\n", id, name, is_default ? '*' : ' ');
}
static const struct {
const gchar *name;
ListMediaType value;
} list_media_types[] = {
{ "Audio", LIST_MEDIA_AUDIO },
{ "Video", LIST_MEDIA_VIDEO },
};
static void
list_run (WpCtl * self)
{
guint32 default_node;
g_autoptr (WpPlugin) def_nodes_api = wp_plugin_find (self->core, "default-nodes-api");
const ListMediaType media_filter = cmdline.list.media_type;
const ListObjectType object_filter = cmdline.list.object_type;
for (guint i = 0; i < G_N_ELEMENTS (list_media_types); i++) {
if (media_filter != LIST_MEDIA_ALL &&
media_filter != list_media_types[i].value)
continue;
const gchar *media_type = list_media_types[i].name;
gchar media_type_glob[16];
gchar media_class[24];
g_snprintf (media_type_glob, sizeof(media_type_glob), "*%s*", media_type);
/* Devices */
if (object_filter == LIST_OBJECT_ALL || object_filter == LIST_OBJECT_DEVICES) {
g_autoptr (WpIterator) it = wp_object_manager_new_filtered_iterator (self->om,
WP_TYPE_DEVICE,
WP_CONSTRAINT_TYPE_PW_PROPERTY, PW_KEY_MEDIA_CLASS, "#s", media_type_glob,
NULL);
wp_iterator_foreach (it, list_print_device, self);
}
/* Sinks */
if (object_filter == LIST_OBJECT_ALL || object_filter == LIST_OBJECT_SINKS) {
g_snprintf (media_class, sizeof(media_class), "%s/Sink", media_type);
default_node = -1;
if (def_nodes_api)
g_signal_emit_by_name (def_nodes_api, "get-default-node", media_class,
&default_node);
g_autoptr (WpIterator) it = wp_object_manager_new_filtered_iterator (self->om,
WP_TYPE_NODE,
WP_CONSTRAINT_TYPE_PW_PROPERTY, PW_KEY_MEDIA_CLASS, "#s", "*/Sink*",
WP_CONSTRAINT_TYPE_PW_PROPERTY, PW_KEY_MEDIA_CLASS, "#s", media_type_glob,
WP_CONSTRAINT_TYPE_PW_PROPERTY, PW_KEY_NODE_LINK_GROUP, "-",
NULL);
wp_iterator_foreach (it, list_print_dev_node, &default_node);
}
/* Sources */
if (object_filter == LIST_OBJECT_ALL || object_filter == LIST_OBJECT_SOURCES) {
g_snprintf (media_class, sizeof(media_class), "%s/Source", media_type);
default_node = -1;
if (def_nodes_api)
g_signal_emit_by_name (def_nodes_api, "get-default-node", media_class,
&default_node);
g_autoptr (WpIterator) it = wp_object_manager_new_filtered_iterator (self->om,
WP_TYPE_NODE,
WP_CONSTRAINT_TYPE_PW_PROPERTY, PW_KEY_MEDIA_CLASS, "#s", "*/Source*",
WP_CONSTRAINT_TYPE_PW_PROPERTY, PW_KEY_MEDIA_CLASS, "#s", media_type_glob,
WP_CONSTRAINT_TYPE_PW_PROPERTY, PW_KEY_NODE_LINK_GROUP, "-",
NULL);
wp_iterator_foreach (it, list_print_dev_node, &default_node);
}
}
g_main_loop_quit (self->loop);
}
/* get-volume */
static gboolean
@ -1714,6 +1873,16 @@ static const struct subcommand {
.prepare = status_prepare,
.run = status_run,
},
{
.name = "list",
.positional_args = "[audio|video] [devices|sinks|sources]",
.summary = "Displays PipeWire objects, optionally filtered by media and object type",
.description = NULL,
.entries = { { NULL } },
.parse_positional = list_parse_positional,
.prepare = list_prepare,
.run = list_run,
},
{
.name = "get-volume",
.positional_args = "ID",