Files
OrionOS/orionnotify/orionnotify.c
T
Claude 2a42b826fb Add 6 desktop features: workspace shortcuts, Exposé, deskbar icons, notifications, screen lock, move-to-desktop
- Super+1-4: switch virtual desktop; Super+Shift+1-4: move focused window to desktop N
- Super+Space: Exposé window overview (tiled grid), click to select, Escape to cancel
- wm/src/expose.c: expose_show/expose_hide using ceil(sqrt) grid layout with saved/restored geometry
- client_move_to_desktop(): moves window between desktops, fires _NET_WM_DESKTOP update
- on_client_message: handles _NET_WM_DESKTOP to move windows from external requests
- deskbar: optional imlib2 app icon rendering (Papirus-Dark → Papirus → hicolor → pixmaps fallback)
- deskbar: XGetClassHint populates cls field for better icon lookup
- deskbar: context menu extended with separator + "→ Desktop 1-4" items via _NET_WM_DESKTOP
- orionnotify/: freedesktop.org notification daemon (libdbus-1), stacks up to 5 popups top-right
- orionlock/: PAM-authenticated fullscreen screen lock with clock, date, password dots
- Lock Screen menu item wired to orionlock
- Root Makefile includes orionnotify and orionlock targets

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01GHiBQKPfUoaJgCKuA9op1H
2026-08-30 20:14:45 +00:00

293 lines
9.0 KiB
C

/*
* orionnotify — freedesktop.org notification daemon
* Implements org.freedesktop.Notifications via libdbus-1.
* Draws popups in the top-right corner (left of the deskbar).
*/
#include <X11/Xlib.h>
#include <X11/Xutil.h>
#include <X11/Xft/Xft.h>
#include <dbus/dbus.h>
#include <stdlib.h>
#include <stdio.h>
#include <string.h>
#include <time.h>
#include <unistd.h>
#define DESKBAR_W 200
#define NOTIF_W 320
#define NOTIF_H 72
#define NOTIF_PAD 10
#define NOTIF_MAX 5
#define NOTIF_GAP 6
#define NOTIF_DEFAULT_MS 4000
#define COL_BG 0x2A2D36
#define COL_BORDER 0x4A6080
#define COL_TITLE 0xE8F0FF
#define COL_BODY 0xA0B8D0
#define COL_ACCENT 0x1E58A8
typedef struct {
Window win;
char summary[128];
char body[256];
int expire_ms;
struct timespec show_time;
int active;
} Notif;
static Notif notifs[NOTIF_MAX];
static int n_notifs = 0;
static unsigned int next_id = 1;
static Display *dpy;
static int screen;
static Visual *visual;
static Colormap cmap;
static GC gc;
static XftFont *font_title;
static XftFont *font_body;
static unsigned long alloc_color(unsigned long rgb) {
XColor c;
c.red = ((rgb >> 16) & 0xFF) * 257;
c.green = ((rgb >> 8) & 0xFF) * 257;
c.blue = ((rgb) & 0xFF) * 257;
c.flags = DoRed | DoGreen | DoBlue;
XAllocColor(dpy, cmap, &c);
return c.pixel;
}
static void xft_color(unsigned long rgb, XftColor *out) {
XRenderColor rc;
rc.red = ((rgb >> 16) & 0xFF) * 257;
rc.green = ((rgb >> 8) & 0xFF) * 257;
rc.blue = ((rgb) & 0xFF) * 257;
rc.alpha = 0xFFFF;
XftColorAllocValue(dpy, visual, cmap, &rc, out);
}
static int notif_x(void) {
return DisplayWidth(dpy, screen) - DESKBAR_W - NOTIF_W - 12;
}
static int notif_y(int slot) {
return 12 + slot * (NOTIF_H + NOTIF_GAP);
}
static void notif_draw(int idx) {
Notif *n = &notifs[idx];
if (!n->active || !n->win) return;
Window w = n->win;
XSetForeground(dpy, gc, alloc_color(COL_BG));
XFillRectangle(dpy, w, gc, 0, 0, NOTIF_W, NOTIF_H);
XSetForeground(dpy, gc, alloc_color(COL_ACCENT));
XFillRectangle(dpy, w, gc, 0, 0, 3, NOTIF_H);
XSetForeground(dpy, gc, alloc_color(COL_BORDER));
XDrawRectangle(dpy, w, gc, 0, 0, NOTIF_W - 1, NOTIF_H - 1);
XftDraw *xd = XftDrawCreate(dpy, w, visual, cmap);
XftColor col;
xft_color(COL_TITLE, &col);
XftDrawStringUtf8(xd, &col, font_title, NOTIF_PAD + 4, 24,
(const FcChar8 *)n->summary, strlen(n->summary));
XftColorFree(dpy, visual, cmap, &col);
xft_color(COL_BODY, &col);
XftDrawStringUtf8(xd, &col, font_body, NOTIF_PAD + 4, 50,
(const FcChar8 *)n->body, strlen(n->body));
XftColorFree(dpy, visual, cmap, &col);
XftDrawDestroy(xd);
XFlush(dpy);
}
static void notif_restack(void) {
int slot = 0;
for (int i = 0; i < NOTIF_MAX; i++) {
if (!notifs[i].active) continue;
XMoveWindow(dpy, notifs[i].win, notif_x(), notif_y(slot));
slot++;
}
}
static void notif_close(int idx) {
if (!notifs[idx].active) return;
XDestroyWindow(dpy, notifs[idx].win);
notifs[idx].win = None;
notifs[idx].active = 0;
n_notifs--;
notif_restack();
}
static unsigned int notif_add(const char *summary, const char *body, int expire_ms) {
if (n_notifs >= NOTIF_MAX) {
/* evict oldest */
for (int i = 0; i < NOTIF_MAX; i++) {
if (notifs[i].active) { notif_close(i); break; }
}
}
int slot = -1;
for (int i = 0; i < NOTIF_MAX; i++) {
if (!notifs[i].active) { slot = i; break; }
}
if (slot < 0) return 0;
Notif *n = &notifs[slot];
snprintf(n->summary, sizeof(n->summary), "%s", summary ? summary : "");
snprintf(n->body, sizeof(n->body), "%s", body ? body : "");
n->expire_ms = (expire_ms <= 0) ? NOTIF_DEFAULT_MS : expire_ms;
clock_gettime(CLOCK_MONOTONIC, &n->show_time);
n->active = 1;
n_notifs++;
int nx = notif_x();
int ny = notif_y(n_notifs - 1);
XSetWindowAttributes swa;
swa.override_redirect = True;
swa.background_pixel = alloc_color(COL_BG);
swa.event_mask = ExposureMask | ButtonPressMask;
n->win = XCreateWindow(dpy, RootWindow(dpy, screen),
nx, ny, NOTIF_W, NOTIF_H, 0,
DefaultDepth(dpy, screen), InputOutput,
visual, CWOverrideRedirect | CWBackPixel | CWEventMask, &swa);
XMapRaised(dpy, n->win);
notif_draw(slot);
return next_id++;
}
static void expire_check(void) {
struct timespec now;
clock_gettime(CLOCK_MONOTONIC, &now);
for (int i = 0; i < NOTIF_MAX; i++) {
if (!notifs[i].active) continue;
long elapsed_ms = (now.tv_sec - notifs[i].show_time.tv_sec) * 1000
+ (now.tv_nsec - notifs[i].show_time.tv_nsec) / 1000000;
if (elapsed_ms >= notifs[i].expire_ms)
notif_close(i);
}
}
/* ── DBus message handling ───────────────────────────────────────────── */
static DBusHandlerResult handle_message(DBusConnection *conn,
DBusMessage *msg, void *data) {
(void)data;
if (!dbus_message_is_method_call(msg, "org.freedesktop.Notifications", "Notify"))
return DBUS_HANDLER_RESULT_NOT_YET_HANDLED;
DBusMessageIter iter;
dbus_message_iter_init(msg, &iter);
/* app_name */ dbus_message_iter_next(&iter);
/* replaces_id */ dbus_message_iter_next(&iter);
/* app_icon */ dbus_message_iter_next(&iter);
const char *summary = "";
if (dbus_message_iter_get_arg_type(&iter) == DBUS_TYPE_STRING)
dbus_message_iter_get_basic(&iter, &summary);
dbus_message_iter_next(&iter);
const char *body = "";
if (dbus_message_iter_get_arg_type(&iter) == DBUS_TYPE_STRING)
dbus_message_iter_get_basic(&iter, &body);
dbus_message_iter_next(&iter); /* actions */
dbus_message_iter_next(&iter); /* hints */
dbus_int32_t expire_timeout = NOTIF_DEFAULT_MS;
if (dbus_message_iter_get_arg_type(&iter) == DBUS_TYPE_INT32)
dbus_message_iter_get_basic(&iter, &expire_timeout);
unsigned int id = notif_add(summary, body, (int)expire_timeout);
DBusMessage *reply = dbus_message_new_method_return(msg);
dbus_uint32_t uid = id;
dbus_message_append_args(reply, DBUS_TYPE_UINT32, &uid, DBUS_TYPE_INVALID);
dbus_connection_send(conn, reply, NULL);
dbus_message_unref(reply);
return DBUS_HANDLER_RESULT_HANDLED;
}
int main(void) {
dpy = XOpenDisplay(NULL);
if (!dpy) { fprintf(stderr, "orionnotify: cannot open display\n"); return 1; }
screen = DefaultScreen(dpy);
visual = DefaultVisual(dpy, screen);
cmap = DefaultColormap(dpy, screen);
gc = XCreateGC(dpy, RootWindow(dpy, screen), 0, NULL);
font_title = XftFontOpenName(dpy, screen, "Sans:size=10:weight=bold");
font_body = XftFontOpenName(dpy, screen, "Sans:size=9");
if (!font_title) font_title = XftFontOpenName(dpy, screen, "fixed:size=10");
if (!font_body) font_body = font_title;
DBusError err;
dbus_error_init(&err);
DBusConnection *conn = dbus_bus_get(DBUS_BUS_SESSION, &err);
if (!conn) {
fprintf(stderr, "orionnotify: dbus: %s\n", err.message);
return 1;
}
int ret = dbus_bus_request_name(conn, "org.freedesktop.Notifications",
DBUS_NAME_FLAG_REPLACE_EXISTING, &err);
if (ret != DBUS_REQUEST_NAME_REPLY_PRIMARY_OWNER) {
fprintf(stderr, "orionnotify: could not claim notification bus name\n");
return 1;
}
dbus_connection_add_filter(conn, handle_message, NULL, NULL);
int dbus_fd = -1;
dbus_connection_get_unix_fd(conn, &dbus_fd);
int xfd = ConnectionNumber(dpy);
for (;;) {
/* pump X events */
while (XPending(dpy)) {
XEvent e;
XNextEvent(dpy, &e);
if (e.type == Expose && e.xexpose.count == 0) {
for (int i = 0; i < NOTIF_MAX; i++)
if (notifs[i].active && notifs[i].win == e.xany.window)
notif_draw(i);
}
if (e.type == ButtonPress) {
for (int i = 0; i < NOTIF_MAX; i++)
if (notifs[i].active && notifs[i].win == e.xany.window)
notif_close(i);
}
}
/* pump dbus */
dbus_connection_read_write(conn, 0);
DBusMessage *msg;
while ((msg = dbus_connection_pop_message(conn)) != NULL) {
handle_message(conn, msg, NULL);
dbus_message_unref(msg);
}
expire_check();
/* sleep up to 200ms, woken by X or dbus */
fd_set fds;
FD_ZERO(&fds);
FD_SET(xfd, &fds);
if (dbus_fd >= 0) FD_SET(dbus_fd, &fds);
int maxfd = xfd > dbus_fd ? xfd : dbus_fd;
struct timeval tv = { 0, 200000 };
select(maxfd + 1, &fds, NULL, NULL, &tv);
}
return 0;
}