/* * orionnotify — freedesktop.org notification daemon * Implements org.freedesktop.Notifications via libdbus-1. * Draws popups in the top-right corner (left of the deskbar). */ #include #include #include #include #include #include #include #include #include #include #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 = ¬ifs[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 void notif_log(const char *summary, const char *body) { const char *home = getenv("HOME"); if (!home) return; char dir[512], path[560]; snprintf(dir, sizeof(dir), "%s/.local/share/orionos", home); snprintf(path, sizeof(path), "%s/notif-history.log", dir); mkdir(dir, 0755); FILE *f = fopen(path, "a"); if (!f) return; time_t t = time(NULL); /* sanitise: replace tabs and newlines with spaces */ char sum[128], bod[256]; strncpy(sum, summary ? summary : "", 127); sum[127] = '\0'; strncpy(bod, body ? body : "", 255); bod[255] = '\0'; for (char *p = sum; *p; p++) if (*p == '\t' || *p == '\n') *p = ' '; for (char *p = bod; *p; p++) if (*p == '\t' || *p == '\n') *p = ' '; fprintf(f, "%ld\t%s\t%s\n", (long)t, sum, bod); fclose(f); } 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 = ¬ifs[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++; notif_log(summary, body); 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; }