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
This commit is contained in:
14 files changed
+977
-31
No files matched your search
@@ -0,0 +1,22 @@
|
||||
CC = gcc
|
||||
|
||||
PKG_C := $(shell pkg-config --cflags dbus-1 xft x11 2>/dev/null)
|
||||
PKG_L := $(shell pkg-config --libs dbus-1 xft x11 2>/dev/null)
|
||||
|
||||
ifeq ($(PKG_C),)
|
||||
$(error "Required: libdbus-1-dev libxft-dev libx11-dev")
|
||||
endif
|
||||
|
||||
CFLAGS = -Wall -Wextra -O2 $(PKG_C)
|
||||
LDFLAGS = $(PKG_L)
|
||||
|
||||
BIN = orionnotify
|
||||
|
||||
.PHONY: all clean install
|
||||
all: $(BIN)
|
||||
$(BIN): orionnotify.c
|
||||
$(CC) $(CFLAGS) -o $@ $^ $(LDFLAGS)
|
||||
install: $(BIN)
|
||||
install -Dm755 $(BIN) /usr/local/bin/$(BIN)
|
||||
clean:
|
||||
rm -f $(BIN)
|
||||
@@ -0,0 +1,292 @@
|
||||
/*
|
||||
* 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 = ¬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 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++;
|
||||
|
||||
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;
|
||||
}
|
||||
Reference in new issue
Block a user