Commit Diff


commit - 2ae45b1c436f5e25cf4aad9f95f7640eb4affcac
commit + 092a63203be736454ac80eab935c05c8d8386a4d
blob - 3e6d7786253c41479580428aa687557a1c0c7e68
blob + 62b9f2b960db4a229fc83813d5be8b043832d0e5
--- Makefile
+++ Makefile
@@ -2,9 +2,9 @@ PROG=		siomixer
 WARNINGS=	yes
 BINDIR?=	/usr/local/bin
 MANDIR?=	/usr/local/man/man
-CFLAGS+=	$$(pkg-config --cflags gtk4) -g -O0 -Wall
+CFLAGS+=	$$(pkg-config --cflags gtk4) -g -O0 -Wall -fno-omit-frame-pointer -Wimplicit-fallthrough
 LDADD+=		-lutil -lsndio $$(pkg-config --libs gtk4)
-SRCS=		siomixer.c audiowidget.c
+SRCS=		siomixer.c ctlitem.c
 
 VERSION=	0.1
 
blob - fb314cce0616924d8b46bda7c46d3cb66cef6bdf (mode 644)
blob + /dev/null
--- audiowidget.c
+++ /dev/null
@@ -1,165 +0,0 @@
-/*
- * Copyright (c) 2026 Tobias Heider <tobhe@openbsd.org>
- * Copyright (c) 2014-2020 Alexandre Ratchov <alex@caoua.org>
- *
- * Permission to use, copy, modify, and distribute this software for any
- * purpose with or without fee is hereby granted, provided that the above
- * copyright notice and this permission notice appear in all copies.
- *
- * THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
- * WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
- * MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
- * ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
- * WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
- * ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
- * OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
- */
-
-#include <sndio.h>
-#include <gtk/gtk.h>
-
-#include "siomixer.h"
-
-extern struct app_state s;
-
-struct info *_nextpar(struct info *);
-static gboolean _on_debounce_timeout(gpointer);
-static void _on_scale_value_changed(GtkRange *, gpointer);
-static char *_format_percent(GtkScale *, double, gpointer);
-
-/*
- * find the next parameter with the same group, name, func
- */
-struct info *
-_nextpar(struct info *i)
-{
-	char *str, *group, *func;
-	int unit;
-
-	group = i->desc.group;
-	func = i->desc.func;
-	str = i->desc.node0.name;
-	unit = i->desc.node0.unit;
-	for (i = i->next; i != NULL; i = i->next) {
-		if (strcmp(i->desc.group, group) != 0 ||
-		    strcmp(i->desc.node0.name, str) != 0 ||
-		    strcmp(i->desc.func, func) != 0)
-			break;
-		/* XXX: need to check for -1 ? */
-		if (i->desc.node0.unit != unit)
-			return i;
-	}
-	return NULL;
-}
-
-static gboolean
-_on_debounce_timeout(gpointer user_data)
-{
-	struct info *i = user_data;
-	double value = gtk_range_get_value(audiowidget_get_gtkrange(i->widget));
-	unsigned int uvalue = (unsigned int)(value + 0.5);
-
-	for (; i != NULL; i = _nextpar(i)) {
-		sioctl_setval(s.hdl, i->ctladdr, uvalue);
-		i->timeout = 0;
-	}
-	return G_SOURCE_REMOVE; // one-shot
-}
-
-static void
-_on_scale_value_changed(GtkRange *range, gpointer user_data)
-{
-	struct info *i = user_data;
-
-	/* Rate limit */
-	if (i->timeout != 0)
-		return;
-	i->timeout = g_timeout_add(150, _on_debounce_timeout, i);
-}
-
-/* Handle conversion from 0-255 to % */
-static char *
-_format_percent(GtkScale *scale, double value, gpointer user_data)
-{
-	GtkAdjustment *adj = gtk_range_get_adjustment(GTK_RANGE(scale));
-	double min = gtk_adjustment_get_lower(adj);
-	double max = gtk_adjustment_get_upper(adj);
-	double percent = (value - min) / (max - min) * 100.0;
-
-	// printf("(%f - %f) / (%f - %f) * 100.0 = %f\n", value, min, max, min, percent);
-
-	return g_strdup_printf("%.0f%%", percent);
-}
-
-GtkRange *
-audiowidget_get_gtkrange(AudioWidget *a)
-{
-	return GTK_RANGE(a->scale);
-}
-
-GtkWidget *
-audiowidget_get_gtkwidget(AudioWidget *a)
-{
-	return a->box;
-}
-
-AudioWidget *
-audiowidget_new(struct info *i)
-{
-	char *label = NULL;
-	char *group = NULL;
-	AudioWidget *a;
-
-	switch (i->desc.type) {
-		case SIOCTL_NUM:
-			/* Only numeric switches make sense as scales */
-			break;
-		default:
-			/* do nothing */
-			return NULL;
-	}
-	// printf("%s %s %d\n", i->desc.group, i->desc.func, i->curval);
-
-	a = calloc(1, sizeof(AudioWidget));
-	if (a == NULL)
-		return NULL;
-
-	if (i->desc.group[0] != '\0')
-		asprintf(&group, "%s/", i->desc.group);
-	asprintf(&label, "%s%s.%s", group ? group : "", i->desc.node0.name, i->desc.func);
-
-	GtkWidget *box = gtk_box_new(GTK_ORIENTATION_VERTICAL, 5);
-	g_object_ref_sink(box);
-
-	GtkWidget *l = gtk_label_new(label);
-	gtk_widget_set_vexpand(l, FALSE);
-	gtk_box_append(GTK_BOX(box), l);
-
-	GtkWidget *gscale = gtk_scale_new_with_range(GTK_ORIENTATION_VERTICAL, 0, i->desc.maxval, 1);
-	gtk_range_set_inverted(GTK_RANGE(gscale), TRUE);
-	gtk_widget_set_vexpand(gscale, TRUE);
-	gtk_scale_set_draw_value(GTK_SCALE(gscale), TRUE);
-	gtk_scale_set_format_value_func(GTK_SCALE(gscale), _format_percent, NULL, NULL);
-	g_signal_connect(gscale, "value-changed", G_CALLBACK(_on_scale_value_changed), i);
-	gtk_range_set_value(GTK_RANGE(gscale), i->curval);
-	gtk_box_append(GTK_BOX(box), gscale);
-
-	free(label);
-	free(group);
-
-	a->box = box;
-	a->scale = gscale;
-
-	return a;
-}
-
-void
-audiowidget_free(AudioWidget *a)
-{
-	/*
-         * sink and label are owned by box and 
-         * should get freed with it
-         */
-	g_object_unref(a->box);
-	free(a);
-}
blob - /dev/null
blob + 562f126da682607f64c39783beb9bb8087db7e62 (mode 644)
--- /dev/null
+++ ctlitem.c
@@ -0,0 +1,271 @@
+/*
+ * Copyright (c) 2026 Tobias Heider <tobhe@openbsd.org>
+ *
+ * Permission to use, copy, modify, and distribute this software for any
+ * purpose with or without fee is hereby granted, provided that the above
+ * copyright notice and this permission notice appear in all copies.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
+ * WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
+ * MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
+ * ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
+ * WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
+ * ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
+ * OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
+ */
+
+#include <gtk/gtk.h>
+#include <sndio.h>
+
+#include "ctlitem.h"
+#include "siomixer.h"
+
+extern struct app_state s;
+
+typedef enum
+{
+	PROP_LEVEL = 1,
+	PROP_MUTE,
+	N_PROPS
+} SiomixerCtlItemProperty;
+
+static GParamSpec *properties[N_PROPS] = { NULL, };
+
+struct _SiomixerCtlItem {
+	GObject parent_instance;
+	char *group;
+	char *name;
+	unsigned ctladdr;
+	unsigned timeout;
+	int maxlevel;
+	int level;
+	int mute;
+};
+G_DEFINE_TYPE (SiomixerCtlItem, siomixer_ctl_item, G_TYPE_OBJECT)
+
+static void siomixer_ctl_item_init(SiomixerCtlItem *);
+static void siomixer_ctl_item_class_init(SiomixerCtlItemClass *);
+
+static void siomixer_ctl_item_set_property(GObject *, guint,
+    const GValue *, GParamSpec *);
+static void siomixer_ctl_item_get_property(GObject *, guint,
+    GValue *, GParamSpec *);
+
+static char *_format_percent(GtkScale *, double, gpointer);
+static void siomixer_ctl_item_init(SiomixerCtlItem *);
+static void siomixer_ctl_item_class_init(SiomixerCtlItemClass *);
+static void siomixer_ctl_item_finalize(GObject *);
+
+static gboolean _on_debounce_timeout(gpointer);
+static void _on_scale_value_changed(GObject *, gpointer);
+
+/* Handle conversion from 0-255 to % */
+static char *
+_format_percent(GtkScale *scale, double value, gpointer user_data)
+{
+	GtkAdjustment *adj = gtk_range_get_adjustment(GTK_RANGE(scale));
+	double min = gtk_adjustment_get_lower(adj);
+	double max = gtk_adjustment_get_upper(adj);
+	double percent = (value - min) / (max - min) * 100.0;
+
+	return g_strdup_printf("%.0f%%", percent);
+}
+
+static void
+siomixer_ctl_item_init(SiomixerCtlItem *self)
+{
+	self->name = NULL;
+	self->group = NULL;
+	self->ctladdr = 0;
+	self->timeout = 0;
+	self->maxlevel = -1;
+	self->level = -1;
+	self->mute = -1;
+}
+
+static void
+siomixer_ctl_item_class_init(SiomixerCtlItemClass *klass)
+{
+	GObjectClass *object_class = G_OBJECT_CLASS(klass);
+
+	object_class->set_property = siomixer_ctl_item_set_property;
+	object_class->get_property = siomixer_ctl_item_get_property;
+	object_class->finalize = siomixer_ctl_item_finalize;
+
+	properties[PROP_LEVEL] =
+	    g_param_spec_int("level", "Level", "Volume level", 0, 255, 0, G_PARAM_READWRITE);
+	properties[PROP_MUTE] =
+	    g_param_spec_int("mute", "Mute", "Volume mute", -1, 1, 0, G_PARAM_READWRITE);
+
+	g_object_class_install_properties(object_class, N_PROPS, properties);
+
+}
+
+static void
+siomixer_ctl_item_set_property(GObject *o, guint property_id,
+    const GValue *value, GParamSpec *pspec)
+{
+	SiomixerCtlItem *self = SIOMIXER_CTL_ITEM(o);
+	switch ((SiomixerCtlItemProperty) property_id) {
+	case PROP_LEVEL:
+		self->level = g_value_get_int (value);
+		break;
+	case PROP_MUTE:
+		self->mute = g_value_get_int (value);
+		break;
+	default:
+		G_OBJECT_WARN_INVALID_PROPERTY_ID (o, property_id, pspec);
+		break;
+	}
+}
+
+static void
+siomixer_ctl_item_get_property(GObject *o, guint property_id,
+    GValue *value, GParamSpec *pspec)
+{
+	SiomixerCtlItem *self = SIOMIXER_CTL_ITEM(o);
+	switch ((SiomixerCtlItemProperty) property_id) {
+	case PROP_LEVEL:
+		g_value_set_int(value, self->level);
+		break;
+	case PROP_MUTE:
+		g_value_set_int(value, self->mute);
+		break;
+	default:
+		G_OBJECT_WARN_INVALID_PROPERTY_ID (o, property_id, pspec);
+		break;
+	}
+}
+
+static void
+siomixer_ctl_item_finalize(GObject *o)
+{
+	SiomixerCtlItem *self = SIOMIXER_CTL_ITEM(o);
+	free(self->name);
+	free(self->group);
+	G_OBJECT_CLASS(siomixer_ctl_item_parent_class)->finalize(o);
+}
+
+static gboolean
+_on_debounce_timeout(gpointer user_data)
+{
+	SiomixerCtlItem *item = SIOMIXER_CTL_ITEM(user_data);
+
+	sioctl_setval(s.hdl, item->ctladdr, item->level);
+	item->timeout = 0;
+	return G_SOURCE_REMOVE;
+}
+
+static void
+_on_scale_value_changed(GObject *o, gpointer user_data)
+{
+	SiomixerCtlItem *item = SIOMIXER_CTL_ITEM(o);
+
+	/* Rate limit */
+	if (item->timeout != 0)
+		return;
+	item->timeout = g_timeout_add(150, _on_debounce_timeout, o);
+}
+
+GtkWidget *
+siomixer_ctl_item_to_widget(gpointer o, gpointer user_data)
+{
+	SiomixerCtlItem *item = SIOMIXER_CTL_ITEM(o);
+	char *label = NULL;
+	char *group = NULL;
+
+	/* We only care about scales */
+	if (item->level == -1)
+		return NULL;
+
+	GtkWidget *box = gtk_box_new(GTK_ORIENTATION_VERTICAL, 5);
+	if (strlen(item->group))
+		asprintf(&group, "%s/", item->group);
+	asprintf(&label, "%s%s", group ? group : "", item->name);
+
+	GtkWidget *l = gtk_label_new(label);
+	gtk_widget_set_vexpand(l, FALSE);
+	gtk_box_append(GTK_BOX(box), l);
+
+	GtkWidget *gscale = gtk_scale_new_with_range(GTK_ORIENTATION_VERTICAL, 0, item->maxlevel, 1);
+	gtk_range_set_inverted(GTK_RANGE(gscale), TRUE);
+	gtk_widget_set_vexpand(gscale, TRUE);
+	gtk_scale_set_draw_value(GTK_SCALE(gscale), TRUE);
+	gtk_scale_set_format_value_func(GTK_SCALE(gscale), _format_percent, NULL, NULL);
+
+	g_signal_connect(item, "notify::level", G_CALLBACK(_on_scale_value_changed), item);
+	GtkAdjustment *a = gtk_range_get_adjustment(GTK_RANGE(gscale)); 
+	g_object_bind_property(item, "level", a, "value",
+	    G_BINDING_SYNC_CREATE | G_BINDING_BIDIRECTIONAL);
+
+	gtk_box_append(GTK_BOX(box), gscale);
+
+	free(label);
+	free(group);
+
+	return box;
+}
+
+SiomixerCtlItem *
+siomixer_ctl_item_new(char *group, char *name, unsigned addr)
+{
+	SiomixerCtlItem *item = g_object_new(SIOMIXER_TYPE_CTL_ITEM, NULL);
+	item->group = strdup(group);
+	item->name = strdup(name);
+	item->ctladdr = addr;
+	return item;
+}
+
+int
+siomixer_ctl_item_cmpdesc(struct sioctl_desc *d, SiomixerCtlItem *self)
+{
+	int res;
+
+	res = strcmp(d->group, self->group);
+	if (res != 0)
+		return res;
+	res = strcmp(d->node0.name, self->name);
+	if (res != 0)
+		return res;
+	return res;
+}
+
+void
+siomixer_ctl_item_set_level(SiomixerCtlItem *self, int level)
+{
+	self->level = level;
+	g_object_notify_by_pspec(G_OBJECT(self),
+	    properties[PROP_LEVEL]);
+}
+
+void
+siomixer_ctl_item_set_maxlevel(SiomixerCtlItem *self, int level)
+{
+	self->maxlevel = level;
+}
+
+void
+siomixer_ctl_item_set_mute(SiomixerCtlItem *self, int mute)
+{
+	self->mute = mute;
+	g_object_notify_by_pspec(G_OBJECT(self),
+            properties[PROP_MUTE]);
+}
+
+int
+siomixer_ctl_item_get_level(SiomixerCtlItem *self)
+{
+	return self->level;
+}
+
+int
+siomixer_ctl_item_get_maxlevel(SiomixerCtlItem *self)
+{
+	return self->maxlevel;
+}
+
+int
+siomixer_ctl_item_get_mute(SiomixerCtlItem *self)
+{
+	return self->mute;
+}
blob - 7cbbe889753607cde67e5baef9e8732ecfe034c5
blob + 81716996c756b1f80953d68971779eedfdcc952c
--- siomixer.c
+++ siomixer.c
@@ -16,11 +16,12 @@
  */
 
 #include <poll.h>
-#include <sndio.h>
 #include <fcntl.h>
 
 #include <gtk/gtk.h>
+#include <sndio.h>
 
+#include "ctlitem.h"
 #include "siomixer.h"
 
 struct app_state s = { 0 };
@@ -43,63 +44,13 @@ static GSourceFuncs sioctl_source_funcs = {
 	NULL, NULL,
 };
 
-int cmpdesc(struct sioctl_desc *, struct sioctl_desc *);
-int isdiag(struct info *);
-struct info *vecent(struct info *, char *, int);
 struct info *nextfunc(struct info *);
 struct info *firstent(struct info *, char *);
 struct info *nextent(struct info *, int);
 void ondesc(void *, struct sioctl_desc *, int);
 void onctl(void *, unsigned, unsigned);
-static void _info_free(struct info *);
 
-static void
-_info_free(struct info *i)
-{
-	if (i == NULL)
-		return;
-	if (i->widget)
-		audiowidget_free(i->widget);
-	if (i->timeout)
-		g_source_remove(i->timeout);
-	free(i);
-}
-
 /*
- * compare two sioctl_desc structures, used to sort infolist
- */
-int
-cmpdesc(struct sioctl_desc *d1, struct sioctl_desc *d2)
-{
-	int res;
-
-	res = strcmp(d1->group, d2->group);
-	if (res != 0)
-		return res;
-	res = strcmp(d1->node0.name, d2->node0.name);
-	if (res != 0)
-		return res;
-	res = d1->type - d2->type;
-	if (res != 0)
-		return res;
-	res = strcmp(d1->func, d2->func);
-	if (res != 0)
-		return res;
-	res = d1->node0.unit - d2->node0.unit;
-	if (d1->type == SIOCTL_SEL ||
-	    d1->type == SIOCTL_VEC ||
-	    d1->type == SIOCTL_LIST) {
-		if (res != 0)
-			return res;
-		res = strcmp(d1->node1.name, d2->node1.name);
-		if (res != 0)
-			return res;
-		res = d1->node1.unit - d2->node1.unit;
-	}
-	return res;
-}
-
-/*
  * register a new knob/button, called from the poll() loop.  this may be
  * called when label string changes, in which case we update the
  * existing label widget rather than inserting a new one.
@@ -107,98 +58,86 @@ cmpdesc(struct sioctl_desc *d1, struct sioctl_desc *d2
 void
 ondesc(void *arg, struct sioctl_desc *d, int curval)
 {
-	struct info *i, **pi;
-	int cmp;
+	gint i, cmp = -1;
+	guint n_items;
+	SiomixerCtlItem *item = NULL;
 
 	if (d == NULL)
 		return;
 
-	if (s.flowbox)
-		gtk_flow_box_remove_all(GTK_FLOW_BOX(s.flowbox));
+	/* We don't handle units for now */
+	if (d->node0.unit > 0)
+		return;
 
-	/*
-	 * delete control
-	 */
-	for (pi = &s.infolist; (i = *pi) != NULL; pi = &i->next) {
-		if (d->addr == i->desc.addr) {
-			*pi = i->next;
-			_info_free(i);
-			break;
-		}
-	}
-
 	switch (d->type) {
-	case SIOCTL_NUM:
 	case SIOCTL_SW:
-	case SIOCTL_VEC:
-	case SIOCTL_LIST:
-	case SIOCTL_SEL:
+	case SIOCTL_NUM:
+	case SIOCTL_NONE:
+		printf("%d: %s %s %s %d\n", d->type, d->group, d->node0.name, d->func, d->node0.unit);
 		/*
-		 * find the right position to insert the new widget
+		 * find the right position to insert or merge ctl
 		 */
-		for (pi = &s.infolist; (i = *pi) != NULL; pi = &i->next) {
-			cmp = cmpdesc(d, &i->desc);
-			if (cmp <= 0)
+		n_items = g_list_model_get_n_items(G_LIST_MODEL(s.store));
+		printf("nitems %d\n", n_items);
+		for (i = 0; i < n_items; i++) {
+			item = g_list_model_get_item(G_LIST_MODEL(s.store), i);
+			cmp = siomixer_ctl_item_cmpdesc(d, item);
+			printf("cmp=%d\n", cmp);
+			if (cmp == 0) {
+				/* Update */
+				printf("found at %d\n", i);
 				break;
-		}
-		i = malloc(sizeof(struct info));
-		if (i == NULL) {
-			perror("malloc");
-			exit(1);
-		}
-		i->desc = *d;
-		printf("new node: %s %s %s %d\n", i->desc.group, i->desc.node0.name, i->desc.func, i->desc.node0.unit);
-		i->ctladdr = d->addr;
-		i->curval = i->newval = curval;
-		i->mode = MODE_IGNORE;
-		i->next = *pi;
-		i->timeout = 0;
-		i->widget = NULL;
-		*pi = i;
-		break;
-	default:
-		break;
-	}
-
-	/* Reconstruct flow_box */
-	if (s.flowbox) {
-		for (i = s.infolist; i != NULL; i = nextfunc(i)) {
-			if (i->widget == NULL)
-				i->widget = audiowidget_new(i);
-			if (i->widget) {
-				gtk_flow_box_append(GTK_FLOW_BOX(s.flowbox),
-				    audiowidget_get_gtkwidget(i->widget));
+			} else if (cmp < 0) {
+				item = NULL;
+				break;
 			}
+			item = NULL;
 		}
+		if (item == NULL) {
+			printf("alloc new...\n");
+			item = siomixer_ctl_item_new(d->group, d->node0.name, d->addr);
+			g_list_store_insert(G_LIST_STORE(s.store), i, item);
+		}
+		break;
+	default:
+		printf("default: %s/%s.%s %d\n", d->group, d->node0.name, d->func, d->type);
+		return;
 	}
-}
 
-/*
- * return true of the vector entry is diagonal
- */
-int
-isdiag(struct info *e)
-{
-	if (e->desc.node0.unit < 0 || e->desc.node1.unit < 0)
-		return 1;
-	return e->desc.node1.unit == e->desc.node0.unit;
-}
-
-/*
- * find the selector or vector entry with the given name and channels
- */
-struct info *
-vecent(struct info *i, char *vstr, int vunit)
-{
-	while (i != NULL) {
-		if ((strcmp(i->desc.node1.name, vstr) == 0) &&
-		    (vunit < 0 || i->desc.node1.unit == vunit))
-			break;
-		i = i->next;
+	switch (d->type) {
+	case SIOCTL_SW:
+	case SIOCTL_NUM:
+		if (strcmp(d->func, "level") == 0) {
+			siomixer_ctl_item_set_level(item, curval);
+			siomixer_ctl_item_set_maxlevel(item, d->maxval);
+		} if (strcmp(d->func, "mute") == 0) {
+			siomixer_ctl_item_set_mute(item, curval);
+		}
+		break;
+	case SIOCTL_NONE:
+		if (strcmp(d->func, "level") == 0) {
+			siomixer_ctl_item_set_level(item, -1);
+		} else if (strcmp(d->func, "mute") == 0) {
+			siomixer_ctl_item_set_mute(item, -1);
+		}
+		/* All controls unused -> free */
+		if ((siomixer_ctl_item_get_level(item) == -1) &&
+		    (siomixer_ctl_item_get_mute(item) == -1)) {
+			printf(">>> delete at %d\n", i);
+			g_list_store_remove(G_LIST_STORE(s.store), i);
+			printf(">>> after delete at %d\n", i);
+		}
+		break;
+	case SIOCTL_VEC:
+	case SIOCTL_LIST:
+	case SIOCTL_SEL:
+	default:
+		printf("unhandled: %s %s %s %d\n", d->group, d->node0.name, d->func, d->node0.unit);
+		break;
 	}
-	return i;
 }
 
+#if 0
 /*
  * skip all parameters with the same group, name, and func
  */
@@ -271,36 +210,29 @@ nextent(struct info *i, int mono)
 	}
 	return NULL;
 }
+#endif
 
 static void
 activate (GtkApplication *app, gpointer user_data)
 {
-	struct info *i;
+	printf(">>> %s: __entry__ \n", __func__);
 	GtkWidget *window;
 	GtkWidget *scroll;
+	GtkWidget *flowbox;
 
 	window = gtk_application_window_new (app);
 	gtk_window_set_title(GTK_WINDOW (window), "OpenMixer");
 	gtk_window_set_default_size(GTK_WINDOW (window), -1, 400);
 
+	flowbox = gtk_flow_box_new();
+	gtk_flow_box_bind_model(GTK_FLOW_BOX(flowbox), G_LIST_MODEL(s.store),
+	    siomixer_ctl_item_to_widget, NULL, NULL);
+	gtk_flow_box_set_selection_mode(GTK_FLOW_BOX(flowbox), GTK_SELECTION_NONE);
+
 	scroll = gtk_scrolled_window_new();
+	gtk_scrolled_window_set_child(GTK_SCROLLED_WINDOW(scroll), flowbox);
 
-	s.flowbox = gtk_flow_box_new();
-	gtk_flow_box_set_selection_mode(GTK_FLOW_BOX(s.flowbox), GTK_SELECTION_NONE);
-
-	/* Build initial list of widgets and append to flow box */
-	for (i = s.infolist; i != NULL; i = nextfunc(i)) {
-		if (i->widget == NULL)
-			i->widget = audiowidget_new(i);
-		if (i->widget)
-			gtk_flow_box_append(GTK_FLOW_BOX(s.flowbox),
-			    audiowidget_get_gtkwidget(i->widget));
-	}
-
-	gtk_scrolled_window_set_child(GTK_SCROLLED_WINDOW(scroll), s.flowbox);
-
 	gtk_window_set_child(GTK_WINDOW(window), scroll);
-
 	gtk_window_present(GTK_WINDOW (window));
 }
 
@@ -310,6 +242,7 @@ activate (GtkApplication *app, gpointer user_data)
 void
 onctl(void *arg, unsigned addr, unsigned val)
 {
+#if 0
 	struct info *i;
 
 	/* Update infolist */
@@ -332,6 +265,7 @@ onctl(void *arg, unsigned addr, unsigned val)
 			gtk_range_set_value(audiowidget_get_gtkrange(i->widget),
 			    i->curval);
 	}
+#endif
 }
 
 static gboolean
@@ -371,6 +305,7 @@ main (int argc, char **argv)
 		fprintf(stderr, "%s: can't open control device\n", devname);
 		exit(1);
 	}
+	s.store = g_list_store_new(SIOMIXER_TYPE_CTL_ITEM);
 
 	/* Initial state */
 	if (!sioctl_ondesc(s.hdl, ondesc, NULL)) {
blob - /dev/null
blob + 4650de767eed318e09849ee2ce78c2b4778ffd41 (mode 644)
--- /dev/null
+++ ctlitem.h
@@ -0,0 +1,38 @@
+/*
+ * Copyright (c) 2026 Tobias Heider <tobhe@openbsd.org>
+ *
+ * Permission to use, copy, modify, and distribute this software for any
+ * purpose with or without fee is hereby granted, provided that the above
+ * copyright notice and this permission notice appear in all copies.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
+ * WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
+ * MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
+ * ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
+ * WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
+ * ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
+ * OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
+ */
+
+#ifndef CTLITEM_H
+#define CTLITEM_H
+
+G_BEGIN_DECLS
+
+#define SIOMIXER_TYPE_CTL_ITEM siomixer_ctl_item_get_type()
+G_DECLARE_FINAL_TYPE(SiomixerCtlItem, siomixer_ctl_item, SIOMIXER, CTL_ITEM, GObject)
+
+GtkWidget *siomixer_ctl_item_to_widget(gpointer, gpointer);
+
+SiomixerCtlItem * siomixer_ctl_item_new(char *, char *, unsigned);
+int siomixer_ctl_item_cmpdesc(struct sioctl_desc *, SiomixerCtlItem *);
+
+void siomixer_ctl_item_set_level(SiomixerCtlItem *, int);
+void siomixer_ctl_item_set_maxlevel(SiomixerCtlItem *, int);
+void siomixer_ctl_item_set_mute(SiomixerCtlItem *, int);
+
+int siomixer_ctl_item_get_level(SiomixerCtlItem *);
+int siomixer_ctl_item_get_maxlevel(SiomixerCtlItem *);
+int siomixer_ctl_item_get_mute(SiomixerCtlItem *);
+
+#endif /* CTLITEM_H */
blob - 7e42d7f50d932615ce037e3c74d143164df47487
blob + e6205d8df618198cb51d55ac3008362d9565a8ee
--- siomixer.h
+++ siomixer.h
@@ -14,30 +14,16 @@
  * OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
  */
 
+#include <glib-object.h>
+
 typedef struct _audiowidget AudioWidget;
 
-struct info {
-	struct info *next;
-	struct sioctl_desc desc;
-	unsigned ctladdr;
-#define MODE_IGNORE	0	/* ignore this value */
-#define MODE_PRINT	1	/* print-only, don't change value */
-#define MODE_SET	2	/* set to newval value */
-#define MODE_ADD	3	/* increase current value by newval */
-#define MODE_SUB	4	/* decrease current value by newval */
-#define MODE_TOGGLE	5	/* toggle current value */
-	unsigned mode;
-	int curval, newval;
-	unsigned timeout;
-	AudioWidget *widget;
-};
-
 struct app_state {
 	struct sioctl_hdl *hdl;
 	struct pollfd *pfds;
 	int nfds;
 	struct info *infolist;
-	GtkWidget *flowbox;	
+	GListStore *store;
 };
 
 struct _audiowidget {
@@ -45,7 +31,9 @@ struct _audiowidget {
 	GtkWidget *scale;
 };
 
-AudioWidget *audiowidget_new(struct info *);
 GtkWidget *audiowidget_get_gtkwidget(AudioWidget *);
 GtkRange *audiowidget_get_gtkrange(AudioWidget *);
+#if 0
+AudioWidget *audiowidget_new(struct info *);
 void audiowidget_free(AudioWidget *);
+#endif