summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorMitja Felicijan <mitja.felicijan@gmail.com>2026-01-25 15:45:26 +0100
committerMitja Felicijan <mitja.felicijan@gmail.com>2026-01-25 15:45:26 +0100
commitf3dcaa18f2c97d39963df8414c80c4689c2882c4 (patch)
tree51d60b4692c2b712ac848e16a97fc67e3a177733
parent12aa9ddc7337161c3e6ed18414787789a9c8dc22 (diff)
downloadglitch-f3dcaa18f2c97d39963df8414c80c4689c2882c4.tar.gz
Add microphone status indicator
-rw-r--r--Makefile6
-rw-r--r--audio.c108
-rw-r--r--config.def.h5
-rw-r--r--glitch.h17
-rw-r--r--main.c13
-rw-r--r--manager.c35
-rw-r--r--widgets.c52
7 files changed, 215 insertions, 21 deletions
diff --git a/Makefile b/Makefile
index 6bd69ad..b716353 100644
--- a/Makefile
+++ b/Makefile
@@ -1,7 +1,7 @@
CC ?= clang
CFLAGS := -std=c99 -pedantic -Wall -Wextra -Wunused -Wswitch-enum
-INCLUDES := $(shell pkg-config --cflags xft)
-LDFLAGS := $(shell pkg-config --libs x11 xft) -lpthread
+INCLUDES := $(shell pkg-config --cflags xft libpulse)
+LDFLAGS := $(shell pkg-config --libs x11 xft libpulse) -lpthread
DESTDIR ?= /usr/local
DISPLAY_NUM := 69
@@ -15,7 +15,7 @@ endif
all: glitch
-glitch: main.c logging.c manager.c widgets.c switcher.c
+glitch: main.c logging.c manager.c widgets.c switcher.c audio.c
$(CC) $(CFLAGS) $(INCLUDES) -o $@ $^ $(LDFLAGS)
config.h:
diff --git a/audio.c b/audio.c
new file mode 100644
index 0000000..4aa8d50
--- /dev/null
+++ b/audio.c
@@ -0,0 +1,108 @@
+#include <pulse/pulseaudio.h>
+#include <string.h>
+#include "glitch.h"
+
+extern WindowManager wm;
+
+static void trigger_redraw(void) {
+ if (!wm.dpy || wm.root == None) return;
+
+ XLockDisplay(wm.dpy);
+ XEvent ev = {0};
+ ev.type = Expose;
+ ev.xexpose.window = wm.root;
+ ev.xexpose.x = 0;
+ ev.xexpose.y = 0;
+ ev.xexpose.width = 1;
+ ev.xexpose.height = 1;
+ ev.xexpose.count = 0;
+
+ XSendEvent(wm.dpy, wm.root, False, ExposureMask, &ev);
+ XFlush(wm.dpy);
+ XUnlockDisplay(wm.dpy);
+}
+
+static void source_info_callback(pa_context *c, const pa_source_info *i, int eol, void *userdata) {
+ (void)c;
+ (void)userdata;
+ if (eol > 0 || !i) return;
+
+ // Check if this is the default source or matches our criteria
+ // For simplicity, we can just track the mute state of the default source
+ // Pulseaudio usually calls this for the specific source we requested
+ int muted = i->mute;
+ if (wm.mic_muted != muted) {
+ wm.mic_muted = muted;
+ trigger_redraw();
+ }
+}
+
+static void update_mic_state(pa_context *c) {
+ pa_operation *o = pa_context_get_source_info_by_name(c, "@DEFAULT_SOURCE@", source_info_callback, NULL);
+ if (o) pa_operation_unref(o);
+}
+
+static void subscribe_callback(pa_context *c, pa_subscription_event_type_t t, uint32_t idx, void *userdata) {
+ (void)idx;
+ (void)userdata;
+ if ((t & PA_SUBSCRIPTION_EVENT_FACILITY_MASK) == PA_SUBSCRIPTION_EVENT_SOURCE) {
+ update_mic_state(c);
+ }
+}
+
+static void context_state_callback(pa_context *c, void *userdata) {
+ (void)userdata;
+ switch (pa_context_get_state(c)) {
+ case PA_CONTEXT_READY:
+ pa_context_set_subscribe_callback(c, subscribe_callback, NULL);
+ pa_context_subscribe(c, PA_SUBSCRIPTION_MASK_SOURCE, NULL, NULL);
+ update_mic_state(c);
+ break;
+ case PA_CONTEXT_FAILED:
+ case PA_CONTEXT_TERMINATED:
+ break;
+ case PA_CONTEXT_UNCONNECTED:
+ case PA_CONTEXT_CONNECTING:
+ case PA_CONTEXT_AUTHORIZING:
+ case PA_CONTEXT_SETTING_NAME:
+ break;
+ }
+}
+
+void init_audio(void) {
+ wm.pa_mainloop = pa_threaded_mainloop_new();
+ if (!wm.pa_mainloop) return;
+
+ wm.pa_ctx = pa_context_new(pa_threaded_mainloop_get_api(wm.pa_mainloop), "glitch-wm");
+ if (!wm.pa_ctx) return;
+
+ pa_context_set_state_callback(wm.pa_ctx, context_state_callback, NULL);
+
+ if (pa_context_connect(wm.pa_ctx, NULL, PA_CONTEXT_NOFLAGS, NULL) < 0) {
+ return;
+ }
+
+ if (pa_threaded_mainloop_start(wm.pa_mainloop) < 0) {
+ return;
+ }
+}
+
+void deinit_audio(void) {
+ if (wm.pa_mainloop) pa_threaded_mainloop_stop(wm.pa_mainloop);
+ if (wm.pa_ctx) pa_context_unref(wm.pa_ctx);
+ if (wm.pa_mainloop) pa_threaded_mainloop_free(wm.pa_mainloop);
+}
+
+void toggle_mic_mute(const Arg *arg) {
+ (void)arg;
+ if (!wm.pa_ctx || pa_context_get_state(wm.pa_ctx) != PA_CONTEXT_READY) return;
+
+ // Instant UI feedback
+ wm.mic_muted = !wm.mic_muted;
+ trigger_redraw();
+
+ pa_threaded_mainloop_lock(wm.pa_mainloop);
+ pa_operation *o = pa_context_set_source_mute_by_name(wm.pa_ctx, "@DEFAULT_SOURCE@", wm.mic_muted, NULL, NULL);
+ if (o) pa_operation_unref(o);
+ pa_threaded_mainloop_unlock(wm.pa_mainloop);
+}
diff --git a/config.def.h b/config.def.h
index dc7965d..4fbf5ed 100644
--- a/config.def.h
+++ b/config.def.h
@@ -23,6 +23,10 @@ static const char *on_top_inactive_border_color = "darkorange";
static const char *widget_font = "Berkeley Mono:size=7:bold";
static const char *indicator_fg_color = "white";
static const char *indicator_bg_color = "blue";
+static const char *mic_active_bg_color = "darkred";
+static const char *mic_muted_bg_color = "#333333";
+static const char *mic_active_fg_color = "white";
+static const char *mic_muted_fg_color = "white";
static const char *widget_fg_color = "#999999";
static const char *time_format = "%A %d.%m.%Y %H:%M:%S";
@@ -83,6 +87,7 @@ static Keybinds keybinds[] = {
{ MODKEY, XK_f, toggle_fullscreen, { 0 } },
{ MODKEY | ShiftMask, XK_r, reload, { 0 } },
{ MODKEY, XK_c, center_window, { 0 } },
+ { MODKEY, XK_m, toggle_mic_mute, { 0 } },
{ MODKEY | ShiftMask, XK_q, quit, { 0 } },
{ MODKEY, XK_q, close_window, { 0 } },
};
diff --git a/glitch.h b/glitch.h
index d8b9166..507b007 100644
--- a/glitch.h
+++ b/glitch.h
@@ -8,8 +8,11 @@
#include <X11/keysym.h>
#include <X11/XF86keysym.h>
#include <X11/Xft/Xft.h>
+#include <pulse/pulseaudio.h>
+#ifndef MAX
#define MAX(a, b) ((a) > (b) ? (a) : (b))
+#endif
#define LENGTH(x) (sizeof(x) / sizeof((x)[0]))
#define NUM_DESKTOPS 9
@@ -70,6 +73,10 @@ typedef struct {
XftColor xft_bg_color;
XftColor xft_root_bg_color;
XftColor xft_widget_color;
+ XftColor xft_mic_active_bg;
+ XftColor xft_mic_muted_bg;
+ XftColor xft_mic_active_fg;
+ XftColor xft_mic_muted_fg;
unsigned long last_widget_update;
Client *clients;
@@ -79,6 +86,11 @@ typedef struct {
Window *cycle_clients;
int cycle_count;
int active_cycle_index;
+
+ // PulseAudio
+ pa_threaded_mainloop *pa_mainloop;
+ pa_context *pa_ctx;
+ int mic_muted;
} WindowManager;
typedef struct {
@@ -159,6 +171,11 @@ void center_window(const Arg *arg);
void widget_desktop_indicator(void);
void widget_datetime(void);
+void widget_mic_indicator(void);
void redraw_widgets(void);
+void init_audio(void);
+void deinit_audio(void);
+void toggle_mic_mute(const Arg *arg);
+
#endif // GLITCH_H
diff --git a/main.c b/main.c
index fff831b..c2585d9 100644
--- a/main.c
+++ b/main.c
@@ -20,6 +20,7 @@ static void* expose_timer_thread(void* arg) {
sleep(1);
if (wm.dpy != NULL) {
+ XLockDisplay(wm.dpy);
XEvent event = {0};
event.type = Expose;
event.xexpose.window = wm.root;
@@ -32,6 +33,7 @@ static void* expose_timer_thread(void* arg) {
// This is thread-safe - XSendEvent is designed for this.
XSendEvent(wm.dpy, wm.root, False, ExposureMask, &event);
XFlush(wm.dpy);
+ XUnlockDisplay(wm.dpy);
}
}
return NULL;
@@ -84,16 +86,7 @@ int main(int argc, char *argv[]) {
handle_motion_notify();
break;
case ClientMessage:
- {
- static Atom redraw_atom = None;
- if (redraw_atom == None) redraw_atom = XInternAtom(wm.dpy, "GLITCH_WIDGET_REDRAW", False);
-
- if (wm.ev.xclient.message_type == redraw_atom) {
- redraw_widgets();
- } else {
- handle_client_message();
- }
- }
+ handle_client_message();
break;
case ButtonPress:
handle_button_press();
diff --git a/manager.c b/manager.c
index 1d628ba..9ee8181 100644
--- a/manager.c
+++ b/manager.c
@@ -284,6 +284,30 @@ void init_window_manager(void) {
XRenderColor black_render = {0x0000, 0x0000, 0x0000, 0xFFFF};
XftColorAllocValue(wm.dpy, visual, wm.cmap, &black_render, &wm.xft_root_bg_color);
+ if (!XftColorAllocName(wm.dpy, visual, wm.cmap, mic_active_bg_color, &wm.xft_mic_active_bg)) {
+ log_message(stdout, LOG_WARNING, "Failed to allocate color %s, falling back to orange", mic_active_bg_color);
+ XRenderColor render_color = {0xFFFF, 0x8000, 0x0000, 0xFFFF};
+ XftColorAllocValue(wm.dpy, visual, wm.cmap, &render_color, &wm.xft_mic_active_bg);
+ }
+
+ if (!XftColorAllocName(wm.dpy, visual, wm.cmap, mic_muted_bg_color, &wm.xft_mic_muted_bg)) {
+ log_message(stdout, LOG_WARNING, "Failed to allocate color %s, falling back to dark gray", mic_muted_bg_color);
+ XRenderColor render_color = {0x4444, 0x4444, 0x4444, 0xFFFF};
+ XftColorAllocValue(wm.dpy, visual, wm.cmap, &render_color, &wm.xft_mic_muted_bg);
+ }
+
+ if (!XftColorAllocName(wm.dpy, visual, wm.cmap, mic_active_fg_color, &wm.xft_mic_active_fg)) {
+ log_message(stdout, LOG_WARNING, "Failed to allocate color %s, falling back to white", mic_active_fg_color);
+ XRenderColor render_color = {0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF};
+ XftColorAllocValue(wm.dpy, visual, wm.cmap, &render_color, &wm.xft_mic_active_fg);
+ }
+
+ if (!XftColorAllocName(wm.dpy, visual, wm.cmap, mic_muted_fg_color, &wm.xft_mic_muted_fg)) {
+ log_message(stdout, LOG_WARNING, "Failed to allocate color %s, falling back to white", mic_muted_fg_color);
+ XRenderColor render_color = {0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF};
+ XftColorAllocValue(wm.dpy, visual, wm.cmap, &render_color, &wm.xft_mic_muted_fg);
+ }
+
if (!XftColorAllocName(wm.dpy, visual, wm.cmap, widget_fg_color, &wm.xft_widget_color)) {
log_message(stdout, LOG_WARNING, "Failed to allocate color %s, falling back to gray", widget_fg_color);
XRenderColor render_color = {0x8888, 0x8888, 0x8888, 0xFFFF};
@@ -372,6 +396,7 @@ void init_window_manager(void) {
}
redraw_widgets();
update_client_list();
+ init_audio();
XSync(wm.dpy, False);
}
@@ -399,8 +424,13 @@ void execute_shortcut(const char *command) {
}
void deinit_window_manager(void) {
+ deinit_audio();
XftColorFree(wm.dpy, DefaultVisual(wm.dpy, wm.screen), wm.cmap, &wm.xft_color);
XftColorFree(wm.dpy, DefaultVisual(wm.dpy, wm.screen), wm.cmap, &wm.xft_bg_color);
+ XftColorFree(wm.dpy, DefaultVisual(wm.dpy, wm.screen), wm.cmap, &wm.xft_mic_active_bg);
+ XftColorFree(wm.dpy, DefaultVisual(wm.dpy, wm.screen), wm.cmap, &wm.xft_mic_muted_bg);
+ XftColorFree(wm.dpy, DefaultVisual(wm.dpy, wm.screen), wm.cmap, &wm.xft_mic_active_fg);
+ XftColorFree(wm.dpy, DefaultVisual(wm.dpy, wm.screen), wm.cmap, &wm.xft_mic_muted_fg);
XftDrawDestroy(wm.xft_draw);
XftFontClose(wm.dpy, wm.font);
@@ -1042,11 +1072,6 @@ void handle_expose(void) {
}
}
-void redraw_widgets(void) {
- widget_desktop_indicator();
- widget_datetime();
-}
-
void set_active_border(Window window) {
if (window == None) return;
if (!window_exists(window)) return;
diff --git a/widgets.c b/widgets.c
index 3c043c3..4024bba 100644
--- a/widgets.c
+++ b/widgets.c
@@ -33,17 +33,57 @@ void widget_desktop_indicator(void) {
XftDrawStringUtf8(wm.xft_draw, &wm.xft_color, wm.font, text_x, text_y, (FcChar8 *)buf, strlen(buf));
}
+void widget_mic_indicator(void) {
+ int screen_width = DisplayWidth(wm.dpy, wm.screen);
+ int padding = 3;
+
+ // Desktop indicator size
+ char desktop_buf[8];
+ snprintf(desktop_buf, sizeof(desktop_buf), "%u", wm.current_desktop);
+ XGlyphInfo desktop_extents;
+ XftTextExtentsUtf8(wm.dpy, wm.font, (FcChar8 *)desktop_buf, strlen(desktop_buf), &desktop_extents);
+ int desktop_size = (wm.font->height > desktop_extents.width ? wm.font->height : desktop_extents.width) + padding * 2;
+
+ const char *buf = "MIC";
+ XGlyphInfo extents;
+ XftTextExtentsUtf8(wm.dpy, wm.font, (FcChar8 *)buf, strlen(buf), &extents);
+
+ int size_w = extents.width + padding * 4;
+ int size_h = desktop_size;
+ int x = screen_width - desktop_size - size_w - 20;
+ int y = 10;
+
+ XftColor *bg = wm.mic_muted ? &wm.xft_mic_muted_bg : &wm.xft_mic_active_bg;
+ XftColor *fg = wm.mic_muted ? &wm.xft_mic_muted_fg : &wm.xft_mic_active_fg;
+
+ // Draw the background.
+ XftDrawRect(wm.xft_draw, bg, x, y, size_w, size_h);
+
+ // Center the text.
+ int text_x = x + (size_w - extents.width) / 2 + extents.x;
+ int text_y = y + (size_h - wm.font->ascent - wm.font->descent) / 2 + wm.font->ascent;
+
+ XftDrawStringUtf8(wm.xft_draw, fg, wm.font, text_x, text_y, (FcChar8 *)buf, strlen(buf));
+}
+
void widget_datetime(void) {
int screen_width = DisplayWidth(wm.dpy, wm.screen);
int padding = 3;
- // We need to know the desktop indicator size to position the time correctly.
+ // Desktop indicator size
char desktop_buf[8];
snprintf(desktop_buf, sizeof(desktop_buf), "%u", wm.current_desktop);
XGlyphInfo desktop_extents;
XftTextExtentsUtf8(wm.dpy, wm.font, (FcChar8 *)desktop_buf, strlen(desktop_buf), &desktop_extents);
int desktop_size = (wm.font->height > desktop_extents.width ? wm.font->height : desktop_extents.width) + padding * 2;
- int desktop_x = screen_width - desktop_size - 10;
+
+ // Mic indicator size
+ const char *mic_buf = "MIC";
+ XGlyphInfo mic_extents;
+ XftTextExtentsUtf8(wm.dpy, wm.font, (FcChar8 *)mic_buf, strlen(mic_buf), &mic_extents);
+ int mic_size_w = mic_extents.width + padding * 4;
+
+ int offset_x = desktop_size + mic_size_w + 40;
char time_buf[64];
time_t now = time(NULL);
@@ -53,7 +93,7 @@ void widget_datetime(void) {
XGlyphInfo time_extents;
XftTextExtentsUtf8(wm.dpy, wm.font, (FcChar8 *)time_buf, strlen(time_buf), &time_extents);
- int time_x = desktop_x - time_extents.xOff - 20;
+ int time_x = screen_width - offset_x - time_extents.xOff;
int y = 10;
int win_height = desktop_size;
@@ -64,3 +104,9 @@ void widget_datetime(void) {
int time_text_y = y + (win_height - wm.font->ascent - wm.font->descent) / 2 + wm.font->ascent;
XftDrawStringUtf8(wm.xft_draw, &wm.xft_color, wm.font, time_x, time_text_y, (FcChar8 *)time_buf, strlen(time_buf));
}
+
+void redraw_widgets(void) {
+ widget_desktop_indicator();
+ widget_mic_indicator();
+ widget_datetime();
+}