Add microphone status indicator

Author Mitja Felicijan <mitja.felicijan@gmail.com> 2026-01-25 15:45:26 +0100
Committer Mitja Felicijan <mitja.felicijan@gmail.com> 2026-01-25 15:45:26 +0100
Commit f3dcaa18f2c97d39963df8414c80c4689c2882c4 (patch)
-rw-r--r-- Makefile 6
-rw-r--r-- audio.c 108
-rw-r--r-- config.def.h 5
-rw-r--r-- glitch.h 17
-rw-r--r-- main.c 13
-rw-r--r-- manager.c 35
-rw-r--r-- widgets.c 52
7 files changed, 215 insertions, 21 deletions
diff --git a/Makefile b/Makefile
1
CC           ?= clang
1
CC           ?= clang
2
CFLAGS       := -std=c99 -pedantic -Wall -Wextra -Wunused -Wswitch-enum
2
CFLAGS       := -std=c99 -pedantic -Wall -Wextra -Wunused -Wswitch-enum
3
INCLUDES     := $(shell pkg-config --cflags xft)
3
INCLUDES     := $(shell pkg-config --cflags xft libpulse)
4
LDFLAGS      := $(shell pkg-config --libs x11 xft) -lpthread
4
LDFLAGS      := $(shell pkg-config --libs x11 xft libpulse) -lpthread
5
DESTDIR      ?= /usr/local
5
DESTDIR      ?= /usr/local
6
DISPLAY_NUM  := 69
6
DISPLAY_NUM  := 69
7
  
7
  
...
15
  
15
  
16
all: glitch
16
all: glitch
17
  
17
  
18
glitch: main.c logging.c manager.c widgets.c switcher.c
18
glitch: main.c logging.c manager.c widgets.c switcher.c audio.c
19
	$(CC) $(CFLAGS) $(INCLUDES) -o $@ $^ $(LDFLAGS)
19
	$(CC) $(CFLAGS) $(INCLUDES) -o $@ $^ $(LDFLAGS)
20
  
20
  
21
config.h:
21
config.h:
...
diff --git a/audio.c b/audio.c
  
1
#include <pulse/pulseaudio.h>
  
2
#include <string.h>
  
3
#include "glitch.h"
  
4
  
  
5
extern WindowManager wm;
  
6
  
  
7
static void trigger_redraw(void) {
  
8
	if (!wm.dpy || wm.root == None) return;
  
9
  
  
10
	XLockDisplay(wm.dpy);
  
11
	XEvent ev = {0};
  
12
	ev.type = Expose;
  
13
	ev.xexpose.window = wm.root;
  
14
	ev.xexpose.x = 0;
  
15
	ev.xexpose.y = 0;
  
16
	ev.xexpose.width = 1;
  
17
	ev.xexpose.height = 1;
  
18
	ev.xexpose.count = 0;
  
19
  
  
20
	XSendEvent(wm.dpy, wm.root, False, ExposureMask, &ev);
  
21
	XFlush(wm.dpy);
  
22
	XUnlockDisplay(wm.dpy);
  
23
}
  
24
  
  
25
static void source_info_callback(pa_context *c, const pa_source_info *i, int eol, void *userdata) {
  
26
	(void)c;
  
27
	(void)userdata;
  
28
	if (eol > 0 || !i) return;
  
29
  
  
30
	// Check if this is the default source or matches our criteria
  
31
	// For simplicity, we can just track the mute state of the default source
  
32
	// Pulseaudio usually calls this for the specific source we requested
  
33
	int muted = i->mute;
  
34
	if (wm.mic_muted != muted) {
  
35
		wm.mic_muted = muted;
  
36
		trigger_redraw();
  
37
	}
  
38
}
  
39
  
  
40
static void update_mic_state(pa_context *c) {
  
41
	pa_operation *o = pa_context_get_source_info_by_name(c, "@DEFAULT_SOURCE@", source_info_callback, NULL);
  
42
	if (o) pa_operation_unref(o);
  
43
}
  
44
  
  
45
static void subscribe_callback(pa_context *c, pa_subscription_event_type_t t, uint32_t idx, void *userdata) {
  
46
	(void)idx;
  
47
	(void)userdata;
  
48
	if ((t & PA_SUBSCRIPTION_EVENT_FACILITY_MASK) == PA_SUBSCRIPTION_EVENT_SOURCE) {
  
49
		update_mic_state(c);
  
50
	}
  
51
}
  
52
  
  
53
static void context_state_callback(pa_context *c, void *userdata) {
  
54
	(void)userdata;
  
55
	switch (pa_context_get_state(c)) {
  
56
		case PA_CONTEXT_READY:
  
57
			pa_context_set_subscribe_callback(c, subscribe_callback, NULL);
  
58
			pa_context_subscribe(c, PA_SUBSCRIPTION_MASK_SOURCE, NULL, NULL);
  
59
			update_mic_state(c);
  
60
			break;
  
61
		case PA_CONTEXT_FAILED:
  
62
		case PA_CONTEXT_TERMINATED:
  
63
			break;
  
64
		case PA_CONTEXT_UNCONNECTED:
  
65
		case PA_CONTEXT_CONNECTING:
  
66
		case PA_CONTEXT_AUTHORIZING:
  
67
		case PA_CONTEXT_SETTING_NAME:
  
68
			break;
  
69
	}
  
70
}
  
71
  
  
72
void init_audio(void) {
  
73
	wm.pa_mainloop = pa_threaded_mainloop_new();
  
74
	if (!wm.pa_mainloop) return;
  
75
  
  
76
	wm.pa_ctx = pa_context_new(pa_threaded_mainloop_get_api(wm.pa_mainloop), "glitch-wm");
  
77
	if (!wm.pa_ctx) return;
  
78
  
  
79
	pa_context_set_state_callback(wm.pa_ctx, context_state_callback, NULL);
  
80
  
  
81
	if (pa_context_connect(wm.pa_ctx, NULL, PA_CONTEXT_NOFLAGS, NULL) < 0) {
  
82
		return;
  
83
	}
  
84
  
  
85
	if (pa_threaded_mainloop_start(wm.pa_mainloop) < 0) {
  
86
		return;
  
87
	}
  
88
}
  
89
  
  
90
void deinit_audio(void) {
  
91
	if (wm.pa_mainloop) pa_threaded_mainloop_stop(wm.pa_mainloop);
  
92
	if (wm.pa_ctx) pa_context_unref(wm.pa_ctx);
  
93
	if (wm.pa_mainloop) pa_threaded_mainloop_free(wm.pa_mainloop);
  
94
}
  
95
  
  
96
void toggle_mic_mute(const Arg *arg) {
  
97
	(void)arg;
  
98
	if (!wm.pa_ctx || pa_context_get_state(wm.pa_ctx) != PA_CONTEXT_READY) return;
  
99
  
  
100
	// Instant UI feedback
  
101
	wm.mic_muted = !wm.mic_muted;
  
102
	trigger_redraw();
  
103
  
  
104
	pa_threaded_mainloop_lock(wm.pa_mainloop);
  
105
	pa_operation *o = pa_context_set_source_mute_by_name(wm.pa_ctx, "@DEFAULT_SOURCE@", wm.mic_muted, NULL, NULL);
  
106
	if (o) pa_operation_unref(o);
  
107
	pa_threaded_mainloop_unlock(wm.pa_mainloop);
  
108
}
diff --git a/config.def.h b/config.def.h
...
23
static const char *widget_font = "Berkeley Mono:size=7:bold";
23
static const char *widget_font = "Berkeley Mono:size=7:bold";
24
static const char *indicator_fg_color = "white";
24
static const char *indicator_fg_color = "white";
25
static const char *indicator_bg_color = "blue";
25
static const char *indicator_bg_color = "blue";
  
26
static const char *mic_active_bg_color = "darkred";
  
27
static const char *mic_muted_bg_color = "#333333";
  
28
static const char *mic_active_fg_color = "white";
  
29
static const char *mic_muted_fg_color = "white";
26
static const char *widget_fg_color = "#999999";
30
static const char *widget_fg_color = "#999999";
27
static const char *time_format = "%A %d.%m.%Y %H:%M:%S";
31
static const char *time_format = "%A %d.%m.%Y %H:%M:%S";
28
  
32
  
...
83
	{ MODKEY,               XK_f,       toggle_fullscreen,   { 0 }        },
87
	{ MODKEY,               XK_f,       toggle_fullscreen,   { 0 }        },
84
	{ MODKEY | ShiftMask,   XK_r,       reload,              { 0 }        },
88
	{ MODKEY | ShiftMask,   XK_r,       reload,              { 0 }        },
85
	{ MODKEY,               XK_c,       center_window,       { 0 }        },
89
	{ MODKEY,               XK_c,       center_window,       { 0 }        },
  
90
	{ MODKEY,               XK_m,       toggle_mic_mute,     { 0 }        },
86
	{ MODKEY | ShiftMask,   XK_q,       quit,                { 0 }        },
91
	{ MODKEY | ShiftMask,   XK_q,       quit,                { 0 }        },
87
	{ MODKEY,               XK_q,       close_window,        { 0 }        },
92
	{ MODKEY,               XK_q,       close_window,        { 0 }        },
88
};
93
};
...
diff --git a/glitch.h b/glitch.h
...
8
#include <X11/keysym.h>
8
#include <X11/keysym.h>
9
#include <X11/XF86keysym.h>
9
#include <X11/XF86keysym.h>
10
#include <X11/Xft/Xft.h>
10
#include <X11/Xft/Xft.h>
  
11
#include <pulse/pulseaudio.h>
11
  
12
  
  
13
#ifndef MAX
12
#define MAX(a, b) ((a) > (b) ? (a) : (b))
14
#define MAX(a, b) ((a) > (b) ? (a) : (b))
  
15
#endif
13
#define LENGTH(x) (sizeof(x) / sizeof((x)[0]))
16
#define LENGTH(x) (sizeof(x) / sizeof((x)[0]))
14
  
17
  
15
#define NUM_DESKTOPS 9
18
#define NUM_DESKTOPS 9
...
70
	XftColor xft_bg_color;
73
	XftColor xft_bg_color;
71
	XftColor xft_root_bg_color;
74
	XftColor xft_root_bg_color;
72
	XftColor xft_widget_color;
75
	XftColor xft_widget_color;
  
76
	XftColor xft_mic_active_bg;
  
77
	XftColor xft_mic_muted_bg;
  
78
	XftColor xft_mic_active_fg;
  
79
	XftColor xft_mic_muted_fg;
73
	
80
	
74
	unsigned long last_widget_update;
81
	unsigned long last_widget_update;
75
	Client *clients;
82
	Client *clients;
...
79
	Window *cycle_clients;
86
	Window *cycle_clients;
80
	int cycle_count;
87
	int cycle_count;
81
	int active_cycle_index;
88
	int active_cycle_index;
  
89
  
  
90
	// PulseAudio
  
91
	pa_threaded_mainloop *pa_mainloop;
  
92
	pa_context *pa_ctx;
  
93
	int mic_muted;
82
} WindowManager;
94
} WindowManager;
83
  
95
  
84
typedef struct {
96
typedef struct {
...
159
  
171
  
160
void widget_desktop_indicator(void);
172
void widget_desktop_indicator(void);
161
void widget_datetime(void);
173
void widget_datetime(void);
  
174
void widget_mic_indicator(void);
162
void redraw_widgets(void);
175
void redraw_widgets(void);
  
176
  
  
177
void init_audio(void);
  
178
void deinit_audio(void);
  
179
void toggle_mic_mute(const Arg *arg);
163
  
180
  
164
#endif // GLITCH_H
181
#endif // GLITCH_H
diff --git a/main.c b/main.c
...
20
		sleep(1);
20
		sleep(1);
21
  
21
  
22
		if (wm.dpy != NULL) {
22
		if (wm.dpy != NULL) {
  
23
			XLockDisplay(wm.dpy);
23
			XEvent event = {0};
24
			XEvent event = {0};
24
			event.type = Expose;
25
			event.type = Expose;
25
			event.xexpose.window = wm.root;
26
			event.xexpose.window = wm.root;
...
32
			// This is thread-safe - XSendEvent is designed for this.
33
			// This is thread-safe - XSendEvent is designed for this.
33
			XSendEvent(wm.dpy, wm.root, False, ExposureMask, &event);
34
			XSendEvent(wm.dpy, wm.root, False, ExposureMask, &event);
34
			XFlush(wm.dpy);
35
			XFlush(wm.dpy);
  
36
			XUnlockDisplay(wm.dpy);
35
		}
37
		}
36
	}
38
	}
37
	return NULL;
39
	return NULL;
...
84
				handle_motion_notify();
86
				handle_motion_notify();
85
				break;
87
				break;
86
			case ClientMessage: 
88
			case ClientMessage: 
87
				{
89
				handle_client_message();
88
					static Atom redraw_atom = None;
  
89
					if (redraw_atom == None) redraw_atom = XInternAtom(wm.dpy, "GLITCH_WIDGET_REDRAW", False);
  
90
  
  
91
					if (wm.ev.xclient.message_type == redraw_atom) {
  
92
						redraw_widgets();
  
93
					} else {
  
94
						handle_client_message();
  
95
					}
  
96
				}
  
97
				break;
90
				break;
98
			case ButtonPress:
91
			case ButtonPress:
99
				handle_button_press();
92
				handle_button_press();
...
diff --git a/manager.c b/manager.c
...
284
	XRenderColor black_render = {0x0000, 0x0000, 0x0000, 0xFFFF};
284
	XRenderColor black_render = {0x0000, 0x0000, 0x0000, 0xFFFF};
285
	XftColorAllocValue(wm.dpy, visual, wm.cmap, &black_render, &wm.xft_root_bg_color);
285
	XftColorAllocValue(wm.dpy, visual, wm.cmap, &black_render, &wm.xft_root_bg_color);
286
  
286
  
  
287
	if (!XftColorAllocName(wm.dpy, visual, wm.cmap, mic_active_bg_color, &wm.xft_mic_active_bg)) {
  
288
		log_message(stdout, LOG_WARNING, "Failed to allocate color %s, falling back to orange", mic_active_bg_color);
  
289
		XRenderColor render_color = {0xFFFF, 0x8000, 0x0000, 0xFFFF};
  
290
		XftColorAllocValue(wm.dpy, visual, wm.cmap, &render_color, &wm.xft_mic_active_bg);
  
291
	}
  
292
  
  
293
	if (!XftColorAllocName(wm.dpy, visual, wm.cmap, mic_muted_bg_color, &wm.xft_mic_muted_bg)) {
  
294
		log_message(stdout, LOG_WARNING, "Failed to allocate color %s, falling back to dark gray", mic_muted_bg_color);
  
295
		XRenderColor render_color = {0x4444, 0x4444, 0x4444, 0xFFFF};
  
296
		XftColorAllocValue(wm.dpy, visual, wm.cmap, &render_color, &wm.xft_mic_muted_bg);
  
297
	}
  
298
  
  
299
	if (!XftColorAllocName(wm.dpy, visual, wm.cmap, mic_active_fg_color, &wm.xft_mic_active_fg)) {
  
300
		log_message(stdout, LOG_WARNING, "Failed to allocate color %s, falling back to white", mic_active_fg_color);
  
301
		XRenderColor render_color = {0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF};
  
302
		XftColorAllocValue(wm.dpy, visual, wm.cmap, &render_color, &wm.xft_mic_active_fg);
  
303
	}
  
304
  
  
305
	if (!XftColorAllocName(wm.dpy, visual, wm.cmap, mic_muted_fg_color, &wm.xft_mic_muted_fg)) {
  
306
		log_message(stdout, LOG_WARNING, "Failed to allocate color %s, falling back to white", mic_muted_fg_color);
  
307
		XRenderColor render_color = {0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF};
  
308
		XftColorAllocValue(wm.dpy, visual, wm.cmap, &render_color, &wm.xft_mic_muted_fg);
  
309
	}
  
310
  
287
	if (!XftColorAllocName(wm.dpy, visual, wm.cmap, widget_fg_color, &wm.xft_widget_color)) {
311
	if (!XftColorAllocName(wm.dpy, visual, wm.cmap, widget_fg_color, &wm.xft_widget_color)) {
288
		log_message(stdout, LOG_WARNING, "Failed to allocate color %s, falling back to gray", widget_fg_color);
312
		log_message(stdout, LOG_WARNING, "Failed to allocate color %s, falling back to gray", widget_fg_color);
289
		XRenderColor render_color = {0x8888, 0x8888, 0x8888, 0xFFFF};
313
		XRenderColor render_color = {0x8888, 0x8888, 0x8888, 0xFFFF};
...
372
	}
396
	}
373
	redraw_widgets();
397
	redraw_widgets();
374
	update_client_list();
398
	update_client_list();
  
399
	init_audio();
375
	XSync(wm.dpy, False);
400
	XSync(wm.dpy, False);
376
}
401
}
377
  
402
  
...
399
}
424
}
400
  
425
  
401
void deinit_window_manager(void) {
426
void deinit_window_manager(void) {
  
427
	deinit_audio();
402
	XftColorFree(wm.dpy, DefaultVisual(wm.dpy, wm.screen), wm.cmap, &wm.xft_color);
428
	XftColorFree(wm.dpy, DefaultVisual(wm.dpy, wm.screen), wm.cmap, &wm.xft_color);
403
	XftColorFree(wm.dpy, DefaultVisual(wm.dpy, wm.screen), wm.cmap, &wm.xft_bg_color);
429
	XftColorFree(wm.dpy, DefaultVisual(wm.dpy, wm.screen), wm.cmap, &wm.xft_bg_color);
  
430
	XftColorFree(wm.dpy, DefaultVisual(wm.dpy, wm.screen), wm.cmap, &wm.xft_mic_active_bg);
  
431
	XftColorFree(wm.dpy, DefaultVisual(wm.dpy, wm.screen), wm.cmap, &wm.xft_mic_muted_bg);
  
432
	XftColorFree(wm.dpy, DefaultVisual(wm.dpy, wm.screen), wm.cmap, &wm.xft_mic_active_fg);
  
433
	XftColorFree(wm.dpy, DefaultVisual(wm.dpy, wm.screen), wm.cmap, &wm.xft_mic_muted_fg);
404
	XftDrawDestroy(wm.xft_draw);
434
	XftDrawDestroy(wm.xft_draw);
405
  
435
  
406
	XftFontClose(wm.dpy, wm.font);
436
	XftFontClose(wm.dpy, wm.font);
...
1040
			wm.last_widget_update = now_ms;
1070
			wm.last_widget_update = now_ms;
1041
		}
1071
		}
1042
	}
1072
	}
1043
}
  
1044
  
  
1045
void redraw_widgets(void) {
  
1046
	widget_desktop_indicator();
  
1047
	widget_datetime();
  
1048
}
1073
}
1049
  
1074
  
1050
void set_active_border(Window window) {
1075
void set_active_border(Window window) {
...
diff --git a/widgets.c b/widgets.c
...
33
	XftDrawStringUtf8(wm.xft_draw, &wm.xft_color, wm.font, text_x, text_y, (FcChar8 *)buf, strlen(buf));
33
	XftDrawStringUtf8(wm.xft_draw, &wm.xft_color, wm.font, text_x, text_y, (FcChar8 *)buf, strlen(buf));
34
}
34
}
35
  
35
  
  
36
void widget_mic_indicator(void) {
  
37
	int screen_width = DisplayWidth(wm.dpy, wm.screen);
  
38
	int padding = 3;
  
39
  
  
40
	// Desktop indicator size
  
41
	char desktop_buf[8];
  
42
	snprintf(desktop_buf, sizeof(desktop_buf), "%u", wm.current_desktop);
  
43
	XGlyphInfo desktop_extents;
  
44
	XftTextExtentsUtf8(wm.dpy, wm.font, (FcChar8 *)desktop_buf, strlen(desktop_buf), &desktop_extents);
  
45
	int desktop_size = (wm.font->height > desktop_extents.width ? wm.font->height : desktop_extents.width) + padding * 2;
  
46
  
  
47
	const char *buf = "MIC";
  
48
	XGlyphInfo extents;
  
49
	XftTextExtentsUtf8(wm.dpy, wm.font, (FcChar8 *)buf, strlen(buf), &extents);
  
50
  
  
51
	int size_w = extents.width + padding * 4;
  
52
	int size_h = desktop_size;
  
53
	int x = screen_width - desktop_size - size_w - 20;
  
54
	int y = 10;
  
55
  
  
56
	XftColor *bg = wm.mic_muted ? &wm.xft_mic_muted_bg : &wm.xft_mic_active_bg;
  
57
	XftColor *fg = wm.mic_muted ? &wm.xft_mic_muted_fg : &wm.xft_mic_active_fg;
  
58
  
  
59
	// Draw the background.
  
60
	XftDrawRect(wm.xft_draw, bg, x, y, size_w, size_h);
  
61
  
  
62
	// Center the text.
  
63
	int text_x = x + (size_w - extents.width) / 2 + extents.x;
  
64
	int text_y = y + (size_h - wm.font->ascent - wm.font->descent) / 2 + wm.font->ascent;
  
65
  
  
66
	XftDrawStringUtf8(wm.xft_draw, fg, wm.font, text_x, text_y, (FcChar8 *)buf, strlen(buf));
  
67
}
  
68
  
36
void widget_datetime(void) {
69
void widget_datetime(void) {
37
	int screen_width = DisplayWidth(wm.dpy, wm.screen);
70
	int screen_width = DisplayWidth(wm.dpy, wm.screen);
38
	int padding = 3;
71
	int padding = 3;
39
  
72
  
40
	// We need to know the desktop indicator size to position the time correctly.
73
	// Desktop indicator size
41
	char desktop_buf[8];
74
	char desktop_buf[8];
42
	snprintf(desktop_buf, sizeof(desktop_buf), "%u", wm.current_desktop);
75
	snprintf(desktop_buf, sizeof(desktop_buf), "%u", wm.current_desktop);
43
	XGlyphInfo desktop_extents;
76
	XGlyphInfo desktop_extents;
44
	XftTextExtentsUtf8(wm.dpy, wm.font, (FcChar8 *)desktop_buf, strlen(desktop_buf), &desktop_extents);
77
	XftTextExtentsUtf8(wm.dpy, wm.font, (FcChar8 *)desktop_buf, strlen(desktop_buf), &desktop_extents);
45
	int desktop_size = (wm.font->height > desktop_extents.width ? wm.font->height : desktop_extents.width) + padding * 2;
78
	int desktop_size = (wm.font->height > desktop_extents.width ? wm.font->height : desktop_extents.width) + padding * 2;
46
	int desktop_x = screen_width - desktop_size - 10;
79
  
  
80
	// Mic indicator size
  
81
	const char *mic_buf = "MIC";
  
82
	XGlyphInfo mic_extents;
  
83
	XftTextExtentsUtf8(wm.dpy, wm.font, (FcChar8 *)mic_buf, strlen(mic_buf), &mic_extents);
  
84
	int mic_size_w = mic_extents.width + padding * 4;
  
85
  
  
86
	int offset_x = desktop_size + mic_size_w + 40;
47
  
87
  
48
	char time_buf[64];
88
	char time_buf[64];
49
	time_t now = time(NULL);
89
	time_t now = time(NULL);
...
53
	XGlyphInfo time_extents;
93
	XGlyphInfo time_extents;
54
	XftTextExtentsUtf8(wm.dpy, wm.font, (FcChar8 *)time_buf, strlen(time_buf), &time_extents);
94
	XftTextExtentsUtf8(wm.dpy, wm.font, (FcChar8 *)time_buf, strlen(time_buf), &time_extents);
55
  
95
  
56
	int time_x = desktop_x - time_extents.xOff - 20;
96
	int time_x = screen_width - offset_x - time_extents.xOff;
57
	int y = 10;
97
	int y = 10;
58
	int win_height = desktop_size;
98
	int win_height = desktop_size;
59
  
99
  
...
64
	int time_text_y = y + (win_height - wm.font->ascent - wm.font->descent) / 2 + wm.font->ascent;
104
	int time_text_y = y + (win_height - wm.font->ascent - wm.font->descent) / 2 + wm.font->ascent;
65
	XftDrawStringUtf8(wm.xft_draw, &wm.xft_color, wm.font, time_x, time_text_y, (FcChar8 *)time_buf, strlen(time_buf));
105
	XftDrawStringUtf8(wm.xft_draw, &wm.xft_color, wm.font, time_x, time_text_y, (FcChar8 *)time_buf, strlen(time_buf));
66
}
106
}
  
107
  
  
108
void redraw_widgets(void) {
  
109
	widget_desktop_indicator();
  
110
	widget_mic_indicator();
  
111
	widget_datetime();
  
112
}