From f3dcaa18f2c97d39963df8414c80c4689c2882c4 Mon Sep 17 00:00:00 2001 From: Mitja Felicijan Date: Sun, 25 Jan 2026 15:45:26 +0100 Subject: Add microphone status indicator --- Makefile | 6 ++-- audio.c | 108 +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ config.def.h | 5 +++ glitch.h | 17 ++++++++++ main.c | 13 ++----- manager.c | 35 ++++++++++++++++--- widgets.c | 52 ++++++++++++++++++++++++++-- 7 files changed, 215 insertions(+), 21 deletions(-) create mode 100644 audio.c 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 +#include +#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 #include #include +#include +#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(); +} -- cgit v1.2.3