diff --git a/Makefile b/Makefile index bdc4333..f2c71fd 100644 --- a/Makefile +++ b/Makefile @@ -5,9 +5,9 @@ # make clean — remove build artifacts # make iso — build the bootable ISO (requires Arch Linux + archiso) -.PHONY: all wm deskbar launcher installer nebula orionpkg skykit rigel quill lens clean install iso +.PHONY: all wm deskbar launcher installer nebula orionpkg skykit rigel quill lens orionnotify orionlock clean install iso -all: wm deskbar launcher installer nebula orionpkg skykit rigel quill lens +all: wm deskbar launcher installer nebula orionpkg skykit rigel quill lens orionnotify orionlock wm: $(MAKE) -C wm @@ -39,6 +39,12 @@ quill: lens: $(MAKE) -C lens +orionnotify: + $(MAKE) -C orionnotify + +orionlock: + $(MAKE) -C orionlock + clean: $(MAKE) -C wm clean $(MAKE) -C deskbar clean @@ -49,7 +55,9 @@ clean: $(MAKE) -C skykit clean $(MAKE) -C rigel clean $(MAKE) -C quill clean - $(MAKE) -C lens clean + $(MAKE) -C lens clean + $(MAKE) -C orionnotify clean + $(MAKE) -C orionlock clean install: all # binaries @@ -62,6 +70,8 @@ install: all install -Dm755 rigel/rigel /usr/local/bin/rigel install -Dm755 quill/quill /usr/local/bin/quill install -Dm755 lens/lens /usr/local/bin/lens + install -Dm755 orionnotify/orionnotify /usr/local/bin/orionnotify + install -Dm755 orionlock/orionlock /usr/local/bin/orionlock # GTK theme mkdir -p /usr/share/themes cp -r themes/OrionOS /usr/share/themes/ @@ -77,6 +87,7 @@ install: all @echo " orionwallpaper" @echo " picom --config /etc/orionos/picom.conf &" @echo " oriondeskbar &" + @echo " orionnotify &" @echo " exec orionwm" iso: diff --git a/deskbar/Makefile b/deskbar/Makefile index 74f6726..18587dc 100644 --- a/deskbar/Makefile +++ b/deskbar/Makefile @@ -7,6 +7,14 @@ ifeq ($(PKG_XFT_C),) PKG_XFT_L := -L/opt/homebrew/lib -lXft -lfontconfig endif +# optional imlib2 for app icon rendering +PKG_IL_C := $(shell pkg-config --cflags imlib2 2>/dev/null) +PKG_IL_L := $(shell pkg-config --libs imlib2 2>/dev/null) +ifneq ($(PKG_IL_C),) + PKG_XFT_C += $(PKG_IL_C) -DHAVE_IMLIB2 + PKG_XFT_L += $(PKG_IL_L) +endif + CFLAGS = -Wall -Wextra -O2 -Iinclude $(PKG_XFT_C) LDFLAGS = $(PKG_XFT_L) diff --git a/deskbar/include/deskbar.h b/deskbar/include/deskbar.h index 2ebf161..8c54a3f 100644 --- a/deskbar/include/deskbar.h +++ b/deskbar/include/deskbar.h @@ -45,10 +45,12 @@ #define DB_MAX_APPS 32 /* ── per-app context menu ────────────────────────────────────────────── */ -#define DB_CTX_W 140 /* context menu width */ +#define DB_CTX_W 160 /* context menu width */ #define DB_CTX_ITEM_H 26 /* item row height */ #define DB_CTX_PAD 4 /* top/bottom padding */ -#define DB_CTX_N 3 /* number of items: Raise, Minimize, Close */ +#define DB_CTX_N 3 /* base items: Raise, Minimize, Close */ +#define DB_CTX_NDESK 4 /* number of "→ Desktop N" items */ +#define DB_CTX_SEP_H 6 /* separator line height */ /* ── fly-out menu ────────────────────────────────────────────────────── */ #define DB_FLYOUT_W 196 /* fly-out menu panel width */ @@ -102,8 +104,10 @@ static const unsigned long DB_ICON_COLORS[] = { typedef struct AppEntry { Window win; char title[64]; + char cls[64]; /* WM_CLASS instance for icon lookup */ int focused; int minimized; + int desktop; /* which desktop this window is on */ } AppEntry; typedef struct Deskbar { diff --git a/deskbar/src/deskbar.c b/deskbar/src/deskbar.c index 04e1411..06fdf60 100644 --- a/deskbar/src/deskbar.c +++ b/deskbar/src/deskbar.c @@ -1,5 +1,9 @@ #include "deskbar.h" +#ifdef HAVE_IMLIB2 +#include +#endif + #define TRAY_REQUEST_DOCK 0 #define XEMBED_EMBEDDED_NOTIFY 0 #define XEMBED_VERSION 0 @@ -141,6 +145,24 @@ void db_update_apps(Deskbar *db) { ewmh_get_title(db, wins[i], db->apps[db->napps].title, sizeof(db->apps[db->napps].title)); + + /* WM_CLASS instance for icon lookup */ + db->apps[db->napps].cls[0] = '\0'; + { + XClassHint ch = {NULL, NULL}; + if (XGetClassHint(db->dpy, wins[i], &ch)) { + if (ch.res_name) { + strncpy(db->apps[db->napps].cls, ch.res_name, + sizeof(db->apps[db->napps].cls) - 1); + XFree(ch.res_name); + } + if (ch.res_class) XFree(ch.res_class); + } + } + + /* store which desktop this window is on */ + db->apps[db->napps].desktop = (int)win_desk; + if (db->apps[db->napps].title[0] != '\0') db->napps++; } @@ -426,12 +448,52 @@ void db_draw(Deskbar *db) { XSetForeground(db->dpy, db->gc, row_bg); XFillRectangle(db->dpy, db->win, db->gc, 0, iy, DB_W, DB_ITEM_H); - /* letter icon */ - unsigned long icon_bg = db_app_icon_color(db->apps[i].title); + /* icon: imlib2 if available, else letter-square */ int icon_x = 5; int icon_y = iy + (DB_ITEM_H - DB_ICON_SZ) / 2; - draw_letter_icon(db, xd, icon_x, icon_y, - db->apps[i].title, icon_bg, mini); + int icon_drawn = 0; +#ifdef HAVE_IMLIB2 + { + static const char *dirs[] = { + "/usr/share/icons/Papirus-Dark/22x22/apps", + "/usr/share/icons/Papirus/22x22/apps", + "/usr/share/icons/hicolor/22x22/apps", + "/usr/share/icons/hicolor/32x32/apps", + "/usr/share/pixmaps", + NULL + }; + const char *names[2] = { db->apps[i].cls, db->apps[i].title }; + for (int ni = 0; ni < 2 && !icon_drawn; ni++) { + if (!names[ni][0]) continue; + for (int di = 0; dirs[di] && !icon_drawn; di++) { + char path[512]; + const char *exts[] = { "png", "svg", "xpm", NULL }; + for (int ei = 0; exts[ei] && !icon_drawn; ei++) { + snprintf(path, sizeof(path), "%s/%s.%s", + dirs[di], names[ni], exts[ei]); + imlib_context_set_display(db->dpy); + imlib_context_set_visual(db->visual); + imlib_context_set_colormap(db->cmap); + imlib_context_set_drawable(db->win); + Imlib_Image img = imlib_load_image_immediately(path); + if (img) { + imlib_context_set_image(img); + imlib_context_set_blend(1); + imlib_render_image_on_drawable_at_size( + icon_x, icon_y, DB_ICON_SZ, DB_ICON_SZ); + imlib_free_image(); + icon_drawn = 1; + } + } + } + } + } +#endif + if (!icon_drawn) { + unsigned long icon_bg = db_app_icon_color(db->apps[i].title); + draw_letter_icon(db, xd, icon_x, icon_y, + db->apps[i].title, icon_bg, mini); + } /* app name */ int name_x = icon_x + DB_ICON_SZ + 6; @@ -584,7 +646,7 @@ static const MI MENU[] = { { "Settings", NULL, 0, 1, 1 }, { NULL, NULL, 1, 0,-1 }, { "About OrionOS", "alacritty -e bash -c 'fastfetch; read' &", 0, 0,-1 }, - { "Lock Screen", "xscreensaver-command -lock &", 0, 0,-1 }, + { "Lock Screen", "orionlock &", 0, 0,-1 }, { NULL, NULL, 1, 0,-1 }, { "Restart", "systemctl reboot", 0, 0,-1 }, { "Shut Down", "systemctl poweroff", 0, 0,-1 }, @@ -818,7 +880,28 @@ static void handle_menu_press(Deskbar *db, int rx, int ry) { /* ── per-app context menu ─────────────────────────────────────────────── */ static int ctx_total_h(void) { - return DB_CTX_PAD + DB_CTX_N * DB_CTX_ITEM_H + DB_CTX_PAD; + return DB_CTX_PAD + DB_CTX_N * DB_CTX_ITEM_H + + DB_CTX_SEP_H + DB_CTX_NDESK * DB_CTX_ITEM_H + + DB_CTX_PAD; +} + +/* y offset of a context-menu item (base items, then sep, then desk items) */ +static int ctx_item_y(int idx) { + if (idx < DB_CTX_N) + return DB_CTX_PAD + idx * DB_CTX_ITEM_H; + /* desktop items come after separator */ + return DB_CTX_PAD + DB_CTX_N * DB_CTX_ITEM_H + + DB_CTX_SEP_H + (idx - DB_CTX_N) * DB_CTX_ITEM_H; +} + +static int ctx_item_at(int lx) { + /* lx = ry - ctx_win_y */ + int total = DB_CTX_N + DB_CTX_NDESK; + for (int i = 0; i < total; i++) { + int iy = ctx_item_y(i); + if (lx >= iy && lx < iy + DB_CTX_ITEM_H) return i; + } + return -1; } static void db_ctx_draw(Deskbar *db) { @@ -827,6 +910,8 @@ static void db_ctx_draw(Deskbar *db) { int wh = ctx_total_h(); int mini = (db->ctx_app_idx >= 0 && db->ctx_app_idx < db->napps) ? db->apps[db->ctx_app_idx].minimized : 0; + int cur_desk = (db->ctx_app_idx >= 0 && db->ctx_app_idx < db->napps) + ? db->apps[db->ctx_app_idx].desktop : -1; static const char *labels_normal[] = { "Raise", "Minimize", "Close" }; static const char *labels_mini[] = { "Restore", "Restore", "Close" }; @@ -842,8 +927,9 @@ static void db_ctx_draw(Deskbar *db) { XSetForeground(db->dpy, db->gc, db_color(db, 0x505050)); XDrawRectangle(db->dpy, db->ctx_win, db->gc, 0, 0, ww - 1, wh - 1); + /* base items */ for (int i = 0; i < DB_CTX_N; i++) { - int iy = DB_CTX_PAD + i * DB_CTX_ITEM_H; + int iy = ctx_item_y(i); if (i == db->ctx_hov) { XSetForeground(db->dpy, db->gc, db_color(db, 0x4A90D9)); @@ -851,15 +937,42 @@ static void db_ctx_draw(Deskbar *db) { 2, iy + 1, ww - 4, DB_CTX_ITEM_H - 2); } - /* "Close" item tinted red */ - unsigned long tc = (i == db->ctx_hov) ? 0xFFFFFF - : (i == DB_CTX_N - 1) ? 0xFF6B6B /* Close = red tint */ - : 0xE8E8E8; + unsigned long tc = (i == db->ctx_hov) ? 0xFFFFFF + : (i == DB_CTX_N - 1) ? 0xFF6B6B + : 0xE8E8E8; draw_str(db, xd, db->font_ui, tc, 14, str_baseline(db->font_ui, iy, DB_CTX_ITEM_H), labels[i], 0, 0); } + /* separator */ + { + int sep_y = DB_CTX_PAD + DB_CTX_N * DB_CTX_ITEM_H + DB_CTX_SEP_H / 2; + XSetForeground(db->dpy, db->gc, db_color(db, 0x505050)); + XDrawLine(db->dpy, db->ctx_win, db->gc, 8, sep_y, ww - 9, sep_y); + } + + /* "→ Desktop N" items */ + for (int d = 0; d < DB_CTX_NDESK; d++) { + int item_idx = DB_CTX_N + d; + int iy = ctx_item_y(item_idx); + + if (item_idx == db->ctx_hov) { + XSetForeground(db->dpy, db->gc, db_color(db, 0x4A90D9)); + XFillRectangle(db->dpy, db->ctx_win, db->gc, + 2, iy + 1, ww - 4, DB_CTX_ITEM_H - 2); + } + + char label[32]; + snprintf(label, sizeof(label), "\xe2\x86\x92 Desktop %d", d + 1); + unsigned long tc = (item_idx == db->ctx_hov) ? 0xFFFFFF + : (d == cur_desk) ? 0x88AACC /* current = dimmed */ + : 0xE8E8E8; + draw_str(db, xd, db->font_ui, tc, + 14, str_baseline(db->font_ui, iy, DB_CTX_ITEM_H), + label, 0, 0); + } + XftDrawDestroy(xd); XFlush(db->dpy); } @@ -912,9 +1025,7 @@ static void db_ctx_handle_motion(Deskbar *db, int rx, int ry) { if (db->ctx_hov != -1) { db->ctx_hov = -1; db_ctx_draw(db); } return; } - int lx = ry - cy - DB_CTX_PAD; - int idx = (lx >= 0) ? lx / DB_CTX_ITEM_H : -1; - if (idx >= DB_CTX_N) idx = -1; + int idx = ctx_item_at(ry - cy); if (idx != db->ctx_hov) { db->ctx_hov = idx; db_ctx_draw(db); } } @@ -931,13 +1042,12 @@ static void db_ctx_handle_press(Deskbar *db, int rx, int ry) { return; } - int lx = ry - cy - DB_CTX_PAD; - int idx = (lx >= 0) ? lx / DB_CTX_ITEM_H : -1; + int idx = ctx_item_at(ry - cy); Window target = db->apps[app_idx].win; - int mini = db->apps[app_idx].minimized; + int mini = db->apps[app_idx].minimized; db_ctx_close(db); - if (idx < 0 || idx >= DB_CTX_N) return; + if (idx < 0) return; XEvent ev = {0}; ev.xclient.type = ClientMessage; @@ -953,13 +1063,11 @@ static void db_ctx_handle_press(Deskbar *db, int rx, int ry) { SubstructureRedirectMask | SubstructureNotifyMask, &ev); } else if (idx == 1) { if (mini) { - /* Restore */ ev.xclient.window = target; ev.xclient.message_type = XInternAtom(db->dpy, "_NET_ACTIVE_WINDOW", False); ev.xclient.data.l[0] = 2; ev.xclient.data.l[1] = CurrentTime; } else { - /* Minimize via WM_CHANGE_STATE IconicState */ ev.xclient.window = target; ev.xclient.message_type = XInternAtom(db->dpy, "WM_CHANGE_STATE", False); ev.xclient.data.l[0] = 3; /* IconicState */ @@ -967,13 +1075,21 @@ static void db_ctx_handle_press(Deskbar *db, int rx, int ry) { XSendEvent(db->dpy, db->root, False, SubstructureRedirectMask | SubstructureNotifyMask, &ev); } else if (idx == 2) { - /* Close via _NET_CLOSE_WINDOW */ ev.xclient.window = target; ev.xclient.message_type = XInternAtom(db->dpy, "_NET_CLOSE_WINDOW", False); ev.xclient.data.l[0] = CurrentTime; ev.xclient.data.l[1] = 2; XSendEvent(db->dpy, db->root, False, SubstructureRedirectMask | SubstructureNotifyMask, &ev); + } else if (idx >= DB_CTX_N && idx < DB_CTX_N + DB_CTX_NDESK) { + /* Move to Desktop N via _NET_WM_DESKTOP */ + int new_desk = idx - DB_CTX_N; + ev.xclient.window = target; + ev.xclient.message_type = XInternAtom(db->dpy, "_NET_WM_DESKTOP", False); + ev.xclient.data.l[0] = new_desk; + ev.xclient.data.l[1] = 2; + XSendEvent(db->dpy, db->root, False, + SubstructureRedirectMask | SubstructureNotifyMask, &ev); } } diff --git a/orionlock/Makefile b/orionlock/Makefile new file mode 100644 index 0000000..040b239 --- /dev/null +++ b/orionlock/Makefile @@ -0,0 +1,24 @@ +CC = gcc + +PKG_C := $(shell pkg-config --cflags xft x11 2>/dev/null) +PKG_L := $(shell pkg-config --libs xft x11 2>/dev/null) + +ifeq ($(PKG_C),) + PKG_C := -I/opt/homebrew/include + PKG_L := -L/opt/homebrew/lib -lXft -lfontconfig -lX11 +endif + +CFLAGS = -Wall -Wextra -O2 $(PKG_C) +LDFLAGS = $(PKG_L) -lpam + +BIN = orionlock + +.PHONY: all clean install +all: $(BIN) +$(BIN): orionlock.c + $(CC) $(CFLAGS) -o $@ $^ $(LDFLAGS) +install: $(BIN) + install -Dm755 $(BIN) /usr/local/bin/$(BIN) + install -Dm644 orionlock.pam /etc/pam.d/login 2>/dev/null || true +clean: + rm -f $(BIN) diff --git a/orionlock/orionlock.c b/orionlock/orionlock.c new file mode 100644 index 0000000..f806d31 --- /dev/null +++ b/orionlock/orionlock.c @@ -0,0 +1,271 @@ +/* + * orionlock — PAM-authenticated fullscreen screen lock. + * Grabs keyboard + pointer, draws clock and password entry. + * On correct password, ungrabs and exits. + */ +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#define COL_BG 0x000000 +#define COL_CLOCK 0xFFFFFF +#define COL_DATE 0x8899AA +#define COL_DOT 0x4A90D9 +#define COL_ERR 0xFF5F57 +#define COL_HINT 0x555566 +#define PW_MAX 256 +#define DOT_R 5 +#define DOT_PAD 8 + +static Display *dpy; +static int screen; +static Window root, lock_win; +static Visual *visual; +static Colormap cmap; +static GC gc; +static XftFont *font_clock; +static XftFont *font_date; +static XftFont *font_ui; + +static char pw_buf[PW_MAX]; +static int pw_len = 0; +static int auth_fail = 0; +static int auth_ok = 0; + +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 void draw_lock(void) { + int sw = DisplayWidth(dpy, screen); + int sh = DisplayHeight(dpy, screen); + + XSetForeground(dpy, gc, alloc_color(COL_BG)); + XFillRectangle(dpy, lock_win, gc, 0, 0, sw, sh); + + XftDraw *xd = XftDrawCreate(dpy, lock_win, visual, cmap); + XftColor col; + XGlyphInfo ext; + + /* clock */ + { + time_t t = time(NULL); + struct tm *tm = localtime(&t); + char clock_str[16], date_str[32]; + strftime(clock_str, sizeof(clock_str), "%H:%M", tm); + strftime(date_str, sizeof(date_str), "%A, %B %d", tm); + + xft_color(COL_CLOCK, &col); + XftTextExtentsUtf8(dpy, font_clock, (const FcChar8 *)clock_str, + strlen(clock_str), &ext); + int cx = (sw - (int)ext.width) / 2; + int cy = sh / 2 - 100; + XftDrawStringUtf8(xd, &col, font_clock, cx, cy, + (const FcChar8 *)clock_str, strlen(clock_str)); + XftColorFree(dpy, visual, cmap, &col); + + xft_color(COL_DATE, &col); + XftTextExtentsUtf8(dpy, font_date, (const FcChar8 *)date_str, + strlen(date_str), &ext); + cx = (sw - (int)ext.width) / 2; + XftDrawStringUtf8(xd, &col, font_date, cx, cy + 40, + (const FcChar8 *)date_str, strlen(date_str)); + XftColorFree(dpy, visual, cmap, &col); + } + + /* password dots */ + { + int dots = pw_len; + if (dots > 20) dots = 20; + int total_w = dots * (DOT_R * 2 + DOT_PAD) - (dots > 0 ? DOT_PAD : 0); + int start_x = (sw - total_w) / 2; + int dot_y = sh / 2 + 10; + unsigned long dot_col = alloc_color(auth_fail ? COL_ERR : COL_DOT); + XSetForeground(dpy, gc, dot_col); + for (int i = 0; i < dots; i++) { + int dx = start_x + i * (DOT_R * 2 + DOT_PAD); + XFillArc(dpy, lock_win, gc, + dx, dot_y - DOT_R, DOT_R * 2, DOT_R * 2, 0, 360 * 64); + } + if (dots == 0 && pw_len == 0) { + /* hint: show lock icon placeholder */ + xft_color(COL_HINT, &col); + const char *hint = "Type password to unlock"; + XftTextExtentsUtf8(dpy, font_ui, (const FcChar8 *)hint, + strlen(hint), &ext); + int hx = (sw - (int)ext.width) / 2; + XftDrawStringUtf8(xd, &col, font_ui, hx, dot_y + DOT_R + 18, + (const FcChar8 *)hint, strlen(hint)); + XftColorFree(dpy, visual, cmap, &col); + } + } + + /* error message */ + if (auth_fail) { + xft_color(COL_ERR, &col); + const char *msg = "Incorrect password"; + XftTextExtentsUtf8(dpy, font_ui, (const FcChar8 *)msg, + strlen(msg), &ext); + int ex = (sw - (int)ext.width) / 2; + XftDrawStringUtf8(xd, &col, font_ui, ex, sh / 2 + 60, + (const FcChar8 *)msg, strlen(msg)); + XftColorFree(dpy, visual, cmap, &col); + } + + XftDrawDestroy(xd); + XFlush(dpy); +} + +/* ── PAM ─────────────────────────────────────────────────────────────── */ + +static const char *pam_pw = NULL; + +static int pam_conv_fn(int num_msg, const struct pam_message **msg, + struct pam_response **resp, void *appdata_ptr) { + (void)appdata_ptr; + struct pam_response *r = calloc(num_msg, sizeof(struct pam_response)); + if (!r) return PAM_CONV_ERR; + for (int i = 0; i < num_msg; i++) { + if (msg[i]->msg_style == PAM_PROMPT_ECHO_OFF || + msg[i]->msg_style == PAM_PROMPT_ECHO_ON) { + r[i].resp = strdup(pam_pw ? pam_pw : ""); + } + } + *resp = r; + return PAM_SUCCESS; +} + +static int try_auth(const char *password) { + const char *user = getenv("USER"); + if (!user) user = "root"; + pam_pw = password; + + struct pam_conv conv = { pam_conv_fn, NULL }; + pam_handle_t *ph = NULL; + int ret = pam_start("login", user, &conv, &ph); + if (ret != PAM_SUCCESS) { pam_end(ph, ret); return 0; } + ret = pam_authenticate(ph, PAM_SILENT); + pam_end(ph, ret); + return (ret == PAM_SUCCESS); +} + +int main(void) { + dpy = XOpenDisplay(NULL); + if (!dpy) { fprintf(stderr, "orionlock: cannot open display\n"); return 1; } + screen = DefaultScreen(dpy); + root = RootWindow(dpy, screen); + visual = DefaultVisual(dpy, screen); + cmap = DefaultColormap(dpy, screen); + + int sw = DisplayWidth(dpy, screen); + int sh = DisplayHeight(dpy, screen); + + XSetWindowAttributes swa; + swa.override_redirect = True; + swa.background_pixel = alloc_color(COL_BG); + swa.event_mask = ExposureMask | KeyPressMask; + lock_win = XCreateWindow(dpy, root, 0, 0, sw, sh, 0, + DefaultDepth(dpy, screen), InputOutput, + visual, CWOverrideRedirect | CWBackPixel | CWEventMask, &swa); + gc = XCreateGC(dpy, lock_win, 0, NULL); + + font_clock = XftFontOpenName(dpy, screen, "Sans:size=64:weight=bold"); + font_date = XftFontOpenName(dpy, screen, "Sans:size=20"); + font_ui = XftFontOpenName(dpy, screen, "Sans:size=11"); + if (!font_clock) font_clock = XftFontOpenName(dpy, screen, "fixed:size=20"); + if (!font_date) font_date = font_clock; + if (!font_ui) font_ui = font_clock; + + XMapRaised(dpy, lock_win); + XSync(dpy, False); + + /* grab keyboard and pointer */ + for (int t = 0; t < 10; t++) { + int r = XGrabKeyboard(dpy, lock_win, True, GrabModeAsync, GrabModeAsync, CurrentTime); + if (r == GrabSuccess) break; + usleep(100000); + } + XGrabPointer(dpy, lock_win, True, ButtonPressMask | ButtonReleaseMask | + PointerMotionMask, GrabModeAsync, GrabModeAsync, + lock_win, None, CurrentTime); + + draw_lock(); + + time_t last_tick = 0; + + while (!auth_ok) { + /* update clock every second even with no events */ + fd_set fds; + FD_ZERO(&fds); + FD_SET(ConnectionNumber(dpy), &fds); + struct timeval tv = { 1, 0 }; + select(ConnectionNumber(dpy) + 1, &fds, NULL, NULL, &tv); + + time_t now = time(NULL); + if (now != last_tick) { last_tick = now; draw_lock(); } + + while (XPending(dpy)) { + XEvent e; + XNextEvent(dpy, &e); + if (e.type == Expose && e.xexpose.count == 0) { + draw_lock(); + } else if (e.type == KeyPress) { + char buf[32] = {0}; + KeySym sym; + XLookupString(&e.xkey, buf, sizeof(buf) - 1, &sym, NULL); + + if (sym == XK_Return || sym == XK_KP_Enter) { + pw_buf[pw_len] = '\0'; + if (try_auth(pw_buf)) { + auth_ok = 1; + } else { + auth_fail = 1; + pw_len = 0; + draw_lock(); + usleep(400000); + auth_fail = 0; + } + } else if (sym == XK_BackSpace || sym == XK_Delete) { + if (pw_len > 0) pw_len--; + auth_fail = 0; + } else if (sym == XK_Escape) { + pw_len = 0; + auth_fail = 0; + } else if (buf[0] >= 0x20 && pw_len < PW_MAX - 1) { + pw_buf[pw_len++] = buf[0]; + auth_fail = 0; + } + draw_lock(); + } + } + } + + XUngrabKeyboard(dpy, CurrentTime); + XUngrabPointer(dpy, CurrentTime); + XDestroyWindow(dpy, lock_win); + XSync(dpy, False); + XCloseDisplay(dpy); + return 0; +} diff --git a/orionnotify/Makefile b/orionnotify/Makefile new file mode 100644 index 0000000..4513b7a --- /dev/null +++ b/orionnotify/Makefile @@ -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) diff --git a/orionnotify/orionnotify.c b/orionnotify/orionnotify.c new file mode 100644 index 0000000..4178a43 --- /dev/null +++ b/orionnotify/orionnotify.c @@ -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 +#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 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; +} diff --git a/wm/Makefile b/wm/Makefile index f388297..9b08d73 100644 --- a/wm/Makefile +++ b/wm/Makefile @@ -12,9 +12,9 @@ ifeq ($(PKG_XFT_C),) endif CFLAGS = -Wall -Wextra -O2 -Iinclude $(PKG_XFT_C) -LDFLAGS = $(PKG_XFT_L) +LDFLAGS = $(PKG_XFT_L) -lm -SRC = src/wm.c src/client.c src/events.c src/draw.c src/color.c src/menu.c src/switcher.c +SRC = src/wm.c src/client.c src/events.c src/draw.c src/color.c src/menu.c src/switcher.c src/expose.c OBJ = $(SRC:.c=.o) BIN = orionwm diff --git a/wm/include/wm.h b/wm/include/wm.h index e9727be..f8ddbf5 100644 --- a/wm/include/wm.h +++ b/wm/include/wm.h @@ -82,6 +82,9 @@ typedef struct Client { int min_w, min_h; /* PMinSize */ int inc_w, inc_h; /* PResizeInc — 0 means no constraint */ int base_w, base_h; /* PBaseSize — baseline for increment calc */ + /* Exposé saved geometry */ + int exp_x, exp_y; + unsigned exp_w, exp_h; struct Client *next; } Client; @@ -141,6 +144,11 @@ typedef struct WM { Window swl_wins[64]; char swl_titles[64][128]; int swl_mini[64]; /* 1 if corresponding entry is minimized */ + + /* Exposé window overview */ + int expose_active; + int expose_count; + Window expose_wins[64]; /* frames shown in overview */ } WM; /* core */ @@ -163,6 +171,7 @@ void client_toggle_maximize(WM *wm, Client *c); void client_snap_left(WM *wm, Client *c); void client_snap_right(WM *wm, Client *c); void client_set_wm_state(WM *wm, Client *c, long state); +void client_move_to_desktop(WM *wm, Client *c, int new_desk); /* drawing */ void frame_draw(WM *wm, Client *c); @@ -190,6 +199,10 @@ void switcher_prev(WM *wm); void switcher_hide(WM *wm, int commit); void switcher_draw(WM *wm); +/* exposé */ +void expose_show(WM *wm); +void expose_hide(WM *wm, Window selected_frame); + /* events */ void on_map_request(WM *wm, XMapRequestEvent *e); void on_unmap_notify(WM *wm, XUnmapEvent *e); diff --git a/wm/src/client.c b/wm/src/client.c index d9d9644..6e815a9 100644 --- a/wm/src/client.c +++ b/wm/src/client.c @@ -328,6 +328,34 @@ void client_snap_left(WM *wm, Client *c) { frame_draw(wm, c); } +void client_move_to_desktop(WM *wm, Client *c, int new_desk) { + if (new_desk < 0 || new_desk >= 4 || c->desktop == new_desk) return; + + /* hide frame if it's on the current (now-active) desktop */ + if (c->desktop == wm->cur_desktop && !c->minimized) + XUnmapWindow(wm->dpy, c->frame); + + c->desktop = new_desk; + + /* update _NET_WM_DESKTOP */ + long desk = new_desk; + XChangeProperty(wm->dpy, c->win, wm->net_wm_desktop, XA_CARDINAL, 32, + PropModeReplace, (unsigned char *)&desk, 1); + + /* if we moved the focused window away, re-focus another */ + if (wm->focused == c) { + wm->focused->focused = 0; + frame_draw(wm, wm->focused); + wm->focused = NULL; + for (Client *n = wm->clients; n; n = n->next) { + if (!n->minimized && n->desktop == wm->cur_desktop) { + client_focus(wm, n); + break; + } + } + } +} + void client_snap_right(WM *wm, Client *c) { if (c->maximized) client_unmaximize(wm, c); int sw = DisplayWidth(wm->dpy, wm->screen); diff --git a/wm/src/events.c b/wm/src/events.c index d244864..f6c77e8 100644 --- a/wm/src/events.c +++ b/wm/src/events.c @@ -74,6 +74,7 @@ void on_expose(WM *wm, XExposeEvent *e) { if (c && !c->minimized) frame_draw(wm, c); } + void on_configure_request(WM *wm, XConfigureRequestEvent *e) { Client *c = client_find_by_win(wm, e->window); XWindowChanges wc; @@ -136,6 +137,7 @@ void on_client_message(WM *wm, XClientMessageEvent *e) { Atom net_active = XInternAtom(wm->dpy, "_NET_ACTIVE_WINDOW", False); Atom net_close = XInternAtom(wm->dpy, "_NET_CLOSE_WINDOW", False); Atom wm_change_state = XInternAtom(wm->dpy, "WM_CHANGE_STATE", False); + Atom net_wm_desktop = XInternAtom(wm->dpy, "_NET_WM_DESKTOP", False); /* _NET_CURRENT_DESKTOP → switch virtual desktop */ if (e->message_type == wm->net_current_desktop && e->window == wm->root) { @@ -143,6 +145,13 @@ void on_client_message(WM *wm, XClientMessageEvent *e) { return; } + /* _NET_WM_DESKTOP → move window to desktop */ + if (e->message_type == net_wm_desktop) { + Client *c = client_find_by_win(wm, e->window); + if (c) client_move_to_desktop(wm, c, (int)e->data.l[0]); + return; + } + if (e->message_type == net_active) { Client *c = client_find_by_win(wm, e->window); if (c) client_restore(wm, c); @@ -208,6 +217,14 @@ void on_property_notify(WM *wm, XPropertyEvent *e) { } void on_button_press(WM *wm, XButtonEvent *e) { + /* Exposé — click on any frame to select and exit */ + if (wm->expose_active) { + Client *c = client_find_by_frame(wm, e->window); + if (c) expose_hide(wm, c->frame); + else expose_hide(wm, None); + return; + } + /* menu window */ if (wm->menu_visible && e->window == wm->menu_win) { if (e->button == Button1) menu_click(wm, e->x, e->y); @@ -401,9 +418,45 @@ void on_key_press(WM *wm, XKeyEvent *e) { if ((e->state & Mod4Mask) && sym == XK_Down && wm->focused) client_unmaximize(wm, wm->focused); - /* Escape — hide menu */ - if (sym == XK_Escape && wm->menu_visible) - menu_hide(wm); + /* Super+Space — Exposé window overview */ + if ((e->state & Mod4Mask) && sym == XK_space) { + if (wm->expose_active) expose_hide(wm, None); + else expose_show(wm); + return; + } + + /* Super+1-4 — switch to desktop N */ + if ((e->state & Mod4Mask) && !(e->state & ShiftMask)) { + int desk = -1; + if (sym == XK_1) desk = 0; + else if (sym == XK_2) desk = 1; + else if (sym == XK_3) desk = 2; + else if (sym == XK_4) desk = 3; + if (desk >= 0) { + if (wm->expose_active) expose_hide(wm, None); + wm_switch_desktop(wm, desk); + return; + } + } + + /* Super+Shift+1-4 — move focused window to desktop N */ + if ((e->state & Mod4Mask) && (e->state & ShiftMask) && wm->focused) { + int desk = -1; + if (sym == XK_1) desk = 0; + else if (sym == XK_2) desk = 1; + else if (sym == XK_3) desk = 2; + else if (sym == XK_4) desk = 3; + if (desk >= 0) { + client_move_to_desktop(wm, wm->focused, desk); + return; + } + } + + /* Escape — hide menu or exit Exposé */ + if (sym == XK_Escape) { + if (wm->expose_active) expose_hide(wm, None); + else if (wm->menu_visible) menu_hide(wm); + } } void on_key_release(WM *wm, XKeyEvent *e) { diff --git a/wm/src/expose.c b/wm/src/expose.c new file mode 100644 index 0000000..aeae543 --- /dev/null +++ b/wm/src/expose.c @@ -0,0 +1,93 @@ +#include "wm.h" +#include + +void expose_show(WM *wm) { + if (wm->expose_active) return; + + /* collect non-minimized windows on the current desktop */ + int n = 0; + for (Client *c = wm->clients; c; c = c->next) { + if (!c->minimized && c->desktop == wm->cur_desktop && n < 64) { + /* save original geometry */ + c->exp_x = c->x; + c->exp_y = c->y; + c->exp_w = c->w; + c->exp_h = c->h; + wm->expose_wins[n++] = c->frame; + } + } + if (n == 0) return; + + wm->expose_active = 1; + wm->expose_count = n; + + int sw = DisplayWidth(wm->dpy, wm->screen) - DESKBAR_W; + int sh = DisplayHeight(wm->dpy, wm->screen); + + int cols = (int)ceil(sqrt((double)n)); + int rows = (n + cols - 1) / cols; + int pad = 16; + int cell_w = (sw - pad * (cols + 1)) / cols; + int cell_h = (sh - pad * (rows + 1)) / rows; + + int idx = 0; + for (int row = 0; row < rows; row++) { + for (int col = 0; col < cols && idx < n; col++, idx++) { + Client *c = client_find_by_frame(wm, wm->expose_wins[idx]); + if (!c) continue; + + /* un-maximize so we can freely resize */ + if (c->maximized) { + c->maximized = 0; + XDeleteProperty(wm->dpy, c->win, wm->net_wm_state); + } + + int tx = pad + col * (cell_w + pad); + int ty = pad + row * (cell_h + pad); + int tw = cell_w; + int th = cell_h; + + c->x = tx; c->y = ty; + c->w = (unsigned)(tw - BORDER_W * 2); + c->h = (unsigned)(th - TITLE_HEIGHT - BORDER_W); + + int fw = (int)c->w + BORDER_W * 2; + int fh = (int)c->h + TITLE_HEIGHT + BORDER_W; + XMoveResizeWindow(wm->dpy, c->frame, c->x, c->y, fw, fh); + XResizeWindow(wm->dpy, c->win, c->w, c->h); + frame_draw(wm, c); + } + } + XSync(wm->dpy, False); +} + +void expose_hide(WM *wm, Window selected_frame) { + if (!wm->expose_active) return; + wm->expose_active = 0; + + Client *to_focus = NULL; + + for (int i = 0; i < wm->expose_count; i++) { + Client *c = client_find_by_frame(wm, wm->expose_wins[i]); + if (!c) continue; + + c->x = c->exp_x; + c->y = c->exp_y; + c->w = c->exp_w; + c->h = c->exp_h; + + int fw = (int)c->w + BORDER_W * 2; + int fh = (int)c->h + TITLE_HEIGHT + BORDER_W; + XMoveResizeWindow(wm->dpy, c->frame, c->x, c->y, fw, fh); + XResizeWindow(wm->dpy, c->win, c->w, c->h); + frame_draw(wm, c); + + if (c->frame == selected_frame) to_focus = c; + } + + wm->expose_count = 0; + XSync(wm->dpy, False); + + if (to_focus) client_focus(wm, to_focus); + else if (wm->focused) client_focus(wm, wm->focused); +} diff --git a/wm/src/wm.c b/wm/src/wm.c index 388237c..1868e36 100644 --- a/wm/src/wm.c +++ b/wm/src/wm.c @@ -103,6 +103,17 @@ void wm_init(WM *wm) { { XK_Right, Mod4Mask }, /* snap right */ { XK_Up, Mod4Mask }, /* maximize */ { XK_Down, Mod4Mask }, /* restore */ + { XK_space, Mod4Mask }, /* Exposé overview */ + /* Super+1-4: switch desktop */ + { XK_1, Mod4Mask }, + { XK_2, Mod4Mask }, + { XK_3, Mod4Mask }, + { XK_4, Mod4Mask }, + /* Super+Shift+1-4: move window to desktop */ + { XK_1, Mod4Mask | ShiftMask }, + { XK_2, Mod4Mask | ShiftMask }, + { XK_3, Mod4Mask | ShiftMask }, + { XK_4, Mod4Mask | ShiftMask }, { XK_Escape, 0 }, }; for (int i = 0; i < (int)(sizeof(grabs)/sizeof(grabs[0])); i++) {