diff --git a/Makefile b/Makefile index 6bd69ad244fb78621dd50222b7541d1260991059..b7163530e9eeb59efc3f7595020cba00262e3c32 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 0000000000000000000000000000000000000000..4aa8d5058d4f9c07e3c959efa634e760700f054a --- /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 dc7965de517d0e758b8e5d3a405e334618ba7f64..4fbf5ed4b31cc2f22961b430e3c393952741ddff 100644 --- a/config.def.h +++ b/config.def.h @@ -23,6 +23,10 @@ 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 @@ { MODKEY, XK_z, window_vmaximize, { 0 } }, { 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 d8b9166b99b11abfd2e6765e5003397bcd3b897c..507b0076fda511c16b796df82fc5c35050e59ee8 100644 --- a/glitch.h +++ b/glitch.h @@ -8,8 +8,11 @@ #include #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 @@ XftColor xft_color; 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 @@ Window cycle_win; 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 fff831bd126e9932c53051e9d2ea27f29e05446f..c2585d917e15044e4f6814cc6d797adf22ebf2c7 100644 --- a/main.c +++ b/main.c @@ -20,6 +20,7 @@ for(;;) { sleep(1); if (wm.dpy != NULL) { + XLockDisplay(wm.dpy); XEvent event = {0}; event.type = Expose; event.xexpose.window = wm.root; @@ -32,6 +33,7 @@ // 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 @@ while (XCheckTypedEvent(wm.dpy, MotionNotify, &wm.ev)); 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 1d628ba1d71bbeed0c4e1934edd84c5afeef7342..9ee8181f8a919e7fd4e1e96cc940d85f2eda7ea9 100644 --- a/manager.c +++ b/manager.c @@ -284,6 +284,30 @@ // Root background color (black) for widgets to blend in. 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 @@ if (children) XFree(children); } redraw_widgets(); update_client_list(); + init_audio(); XSync(wm.dpy, False); } @@ -399,8 +424,13 @@ } } 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); @@ -1040,11 +1070,6 @@ redraw_widgets(); wm.last_widget_update = now_ms; } } -} - -void redraw_widgets(void) { - widget_desktop_indicator(); - widget_datetime(); } void set_active_border(Window window) { diff --git a/widgets.c b/widgets.c index 3c043c3d9198733950fc0b2740f141428fd5c193..4024bba9b931d715f993f41fd12a4e013b85202e 100644 --- a/widgets.c +++ b/widgets.c @@ -33,17 +33,57 @@ 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 @@ 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 @@ // Draw the time. 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(); +}