diff --git a/deskbar/include/deskbar.h b/deskbar/include/deskbar.h index ffc7a89..2ebf161 100644 --- a/deskbar/include/deskbar.h +++ b/deskbar/include/deskbar.h @@ -44,6 +44,12 @@ #define DB_TRAY_MAX 16 /* maximum simultaneous tray icons */ #define DB_MAX_APPS 32 +/* ── per-app context menu ────────────────────────────────────────────── */ +#define DB_CTX_W 140 /* 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 */ + /* ── fly-out menu ────────────────────────────────────────────────────── */ #define DB_FLYOUT_W 196 /* fly-out menu panel width */ #define DB_FLYOUT_IH 28 /* item row height */ @@ -139,6 +145,11 @@ typedef struct Deskbar { int sub_hov; /* hovered item in sub-menu */ int sw_cache; /* cached screen width for menu positioning */ + /* per-app right-click context menu */ + Window ctx_win; /* None when closed */ + int ctx_app_idx; /* which app row the ctx menu is for */ + int ctx_hov; /* hovered item index, -1 = none */ + /* system tray (freedesktop.org XEmbed tray protocol) */ Window tray_icons[DB_TRAY_MAX]; int n_tray_icons; diff --git a/deskbar/src/deskbar.c b/deskbar/src/deskbar.c index 61df494..b785a0e 100644 --- a/deskbar/src/deskbar.c +++ b/deskbar/src/deskbar.c @@ -809,6 +809,168 @@ static void handle_menu_press(Deskbar *db, int rx, int ry) { db_menu_close(db); } +/* ── per-app context menu ─────────────────────────────────────────────── */ + +static int ctx_total_h(void) { + return DB_CTX_PAD + DB_CTX_N * DB_CTX_ITEM_H + DB_CTX_PAD; +} + +static void db_ctx_draw(Deskbar *db) { + if (!db->ctx_win) return; + int ww = DB_CTX_W; + 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; + + static const char *labels_normal[] = { "Raise", "Minimize", "Close" }; + static const char *labels_mini[] = { "Restore", "Restore", "Close" }; + const char **labels = mini ? labels_mini : labels_normal; + + XftDraw *xd = XftDrawCreate(db->dpy, db->ctx_win, db->visual, db->cmap); + + /* background */ + XSetForeground(db->dpy, db->gc, db_color(db, 0x2C2C2C)); + XFillRectangle(db->dpy, db->ctx_win, db->gc, 0, 0, ww, wh); + + /* 1px border */ + XSetForeground(db->dpy, db->gc, db_color(db, 0x505050)); + XDrawRectangle(db->dpy, db->ctx_win, db->gc, 0, 0, ww - 1, wh - 1); + + for (int i = 0; i < DB_CTX_N; i++) { + int iy = DB_CTX_PAD + i * DB_CTX_ITEM_H; + + if (i == 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); + } + + /* "Close" item tinted red */ + unsigned long tc = (i == db->ctx_hov) ? 0xFFFFFF + : (i == DB_CTX_N - 1) ? 0xFF6B6B /* Close = red tint */ + : 0xE8E8E8; + draw_str(db, xd, db->font_ui, tc, + 14, str_baseline(db->font_ui, iy, DB_CTX_ITEM_H), + labels[i], 0, 0); + } + + XftDrawDestroy(xd); + XFlush(db->dpy); +} + +static void db_ctx_close(Deskbar *db) { + if (!db->ctx_win) return; + XUngrabPointer(db->dpy, CurrentTime); + XDestroyWindow(db->dpy, db->ctx_win); + db->ctx_win = None; + db->ctx_app_idx = -1; + db->ctx_hov = -1; +} + +static void db_ctx_open(Deskbar *db, int app_idx, int rx, int ry) { + db_ctx_close(db); + + int sw = DisplayWidth(db->dpy, db->screen); + int sh = DisplayHeight(db->dpy, db->screen); + int wh = ctx_total_h(); + int wx = sw - DB_W - DB_CTX_W - 2; + int wy = ry; + if (wy + wh > sh) wy = sh - wh - 2; + if (wy < 0) wy = 0; + + XSetWindowAttributes swa; + swa.override_redirect = True; + swa.background_pixel = db_color(db, 0x2C2C2C); + swa.event_mask = ExposureMask; + db->ctx_win = XCreateWindow( + db->dpy, db->root, wx, wy, DB_CTX_W, wh, 1, + db->depth, InputOutput, db->visual, + CWOverrideRedirect | CWBackPixel | CWEventMask, &swa); + + XSetWindowBorder(db->dpy, db->ctx_win, db_color(db, 0x505050)); + db->ctx_app_idx = app_idx; + db->ctx_hov = -1; + XMapRaised(db->dpy, db->ctx_win); + db_ctx_draw(db); + + XGrabPointer(db->dpy, db->ctx_win, True, + ButtonPressMask | PointerMotionMask | ButtonReleaseMask, + GrabModeAsync, GrabModeAsync, None, None, CurrentTime); +} + +static void db_ctx_handle_motion(Deskbar *db, int rx, int ry) { + if (!db->ctx_win) return; + int cx, cy; unsigned cw, ch, bw, dep; Window cr; + XGetGeometry(db->dpy, db->ctx_win, &cr, &cx, &cy, &cw, &ch, &bw, &dep); + if (rx < cx || ry < cy || rx >= cx + (int)cw || ry >= cy + (int)ch) { + 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; + if (idx != db->ctx_hov) { db->ctx_hov = idx; db_ctx_draw(db); } +} + +static void db_ctx_handle_press(Deskbar *db, int rx, int ry) { + if (!db->ctx_win) return; + int cx, cy; unsigned cw, ch, bw, dep; Window cr; + XGetGeometry(db->dpy, db->ctx_win, &cr, &cx, &cy, &cw, &ch, &bw, &dep); + + int app_idx = db->ctx_app_idx; + int in_menu = (rx >= cx && ry >= cy && rx < cx + (int)cw && ry < cy + (int)ch); + + if (!in_menu || app_idx < 0 || app_idx >= db->napps) { + db_ctx_close(db); + return; + } + + int lx = ry - cy - DB_CTX_PAD; + int idx = (lx >= 0) ? lx / DB_CTX_ITEM_H : -1; + Window target = db->apps[app_idx].win; + int mini = db->apps[app_idx].minimized; + db_ctx_close(db); + + if (idx < 0 || idx >= DB_CTX_N) return; + + XEvent ev = {0}; + ev.xclient.type = ClientMessage; + ev.xclient.format = 32; + + if (idx == 0) { + /* Raise / 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; + XSendEvent(db->dpy, db->root, False, + 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 */ + } + 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); + } +} + /* ── input ────────────────────────────────────────────────────────────── */ void db_hover(Deskbar *db, int x, int y) { @@ -981,6 +1143,26 @@ void db_run(Deskbar *db) { XEvent e; XNextEvent(db->dpy, &e); + /* While the per-app context menu is open, the pointer is grabbed. + Route motion and button events through the ctx handlers. */ + if (db->ctx_win) { + if (e.type == MotionNotify) { + db_ctx_handle_motion(db, e.xmotion.x_root, e.xmotion.y_root); + continue; + } + if (e.type == ButtonPress) { + db_ctx_handle_press(db, e.xbutton.x_root, e.xbutton.y_root); + continue; + } + if (e.type == ButtonRelease) + continue; + if (e.type == Expose && e.xexpose.count == 0) { + if (e.xany.window == db->ctx_win) db_ctx_draw(db); + else if (e.xany.window == db->win) db_draw(db); + continue; + } + } + /* While the fly-out menu is open, the pointer is grabbed. Route all motion and button events through the menu handlers (they use root-relative coords to determine which panel was hit). */ @@ -1026,6 +1208,18 @@ void db_run(Deskbar *db) { system("pactl set-sink-volume @DEFAULT_SINK@ -5% 2>/dev/null &"); break; } + if (e.xbutton.button == Button3) { + /* right-click on an app row → open context menu */ + int ay = db_apps_y(); + int by = e.xbutton.y; + if (by >= ay && by < ay + db->napps * DB_ITEM_H) { + int idx = (by - ay) / DB_ITEM_H; + if (idx >= 0 && idx < db->napps) + db_ctx_open(db, idx, + e.xbutton.x_root, e.xbutton.y_root); + } + break; + } if (e.xbutton.button == Button1 && e.xbutton.y >= sy) system("pactl set-sink-mute @DEFAULT_SINK@ toggle 2>/dev/null &"); else diff --git a/themes/picom.conf b/themes/picom.conf index 231446b..4f347f5 100644 --- a/themes/picom.conf +++ b/themes/picom.conf @@ -1,22 +1,29 @@ -# OrionOS picom compositor config -# Goal: subtle window shadows, no transparency, fast rendering. -# BeOS/OrionOS didn't have transparency — we keep it clean. +# OrionOS picom compositor config — macOS-inspired +# Rounded corners, soft macOS-style shadows, fast fading. -backend = "xrender"; # xrender works on all hardware; switch to glx for performance +backend = "xrender"; vsync = false; +# ── rounded corners ─────────────────────────────────────────────────────── +corner-radius = 10; +rounded-corners-exclude = [ + "_NET_WM_WINDOW_TYPE@:a *= 'DOCK'", + "_NET_WM_WINDOW_TYPE@:a *= 'DESKTOP'", + "_NET_WM_WINDOW_TYPE@:a *= 'TOOLTIP'", + "_NET_WM_WINDOW_TYPE@:a *= 'NOTIFICATION'", + "override_redirect = 1 && !WM_CLASS@:s" +]; + # ── shadows ─────────────────────────────────────────────────────────────── +# macOS-style: larger, softer, offset down-right shadow = true; -shadow-radius = 8; -shadow-offset-x = 4; -shadow-offset-y = 4; -shadow-opacity = 0.30; +shadow-radius = 18; +shadow-offset-x = 0; +shadow-offset-y = 8; +shadow-opacity = 0.35; shadow-color = "#000000"; -# Don't shadow the deskbar, menus, or tooltips shadow-exclude = [ - "class_g = 'skydeskbar'", - "class_g = 'skylauncher'", "_NET_WM_WINDOW_TYPE@:a *= 'DOCK'", "_NET_WM_WINDOW_TYPE@:a *= 'TOOLTIP'", "_NET_WM_WINDOW_TYPE@:a *= 'NOTIFICATION'", @@ -27,20 +34,18 @@ shadow-exclude = [ # ── fading ──────────────────────────────────────────────────────────────── fading = true; -fade-in-step = 0.06; -fade-out-step = 0.06; -fade-delta = 8; # ms between steps — snappy, not sluggish +fade-in-step = 0.08; +fade-out-step = 0.08; +fade-delta = 6; no-fading-openclose = false; # ── opacity ─────────────────────────────────────────────────────────────── -# No transparency — BeOS aesthetic is solid, crisp windows. inactive-opacity = 1.0; active-opacity = 1.0; frame-opacity = 1.0; inactive-opacity-override = false; # ── blur ───────────────────────────────────────────────────────────────── -# Disabled — keep it clean. blur-method = "none"; # ── focus & refresh ─────────────────────────────────────────────────────── @@ -55,9 +60,9 @@ mark-ovredir-focused = false; wintypes: { tooltip = { fade = true; shadow = false; opacity = 1.0; focus = true; }; - dock = { shadow = false; clip-shadow-above = true; }; + dock = { shadow = false; clip-shadow-above = true; corner-radius = 0; }; dnd = { shadow = false; }; - popup_menu = { opacity = 1.0; shadow = false; }; - dropdown_menu = { opacity = 1.0; shadow = false; }; - notification = { shadow = false; }; + popup_menu = { opacity = 1.0; shadow = false; corner-radius = 8; }; + dropdown_menu = { opacity = 1.0; shadow = false; corner-radius = 8; }; + notification = { shadow = false; corner-radius = 8; }; }; diff --git a/wm/include/wm.h b/wm/include/wm.h index 5a1ea43..3c0c287 100644 --- a/wm/include/wm.h +++ b/wm/include/wm.h @@ -29,6 +29,7 @@ #define COLOR_SNAP_OVERLAY 0x4A90D9 /* macOS blue snap highlight */ /* ── geometry ────────────────────────────────────────────────────────── */ +#define DESKBAR_W 200 /* must match DB_W in deskbar/include/deskbar.h */ #define TITLE_HEIGHT 28 /* macOS-style height */ #define TITLE_TAB_W 200 #define BORDER_W 1 /* thin 1px macOS-style border */ @@ -130,6 +131,7 @@ typedef struct WM { int swl_count; Window swl_wins[64]; char swl_titles[64][128]; + int swl_mini[64]; /* 1 if corresponding entry is minimized */ } WM; /* core */ @@ -175,6 +177,7 @@ int menu_item_at(int py); /* switcher */ void switcher_show(WM *wm); void switcher_next(WM *wm); +void switcher_prev(WM *wm); void switcher_hide(WM *wm, int commit); void switcher_draw(WM *wm); diff --git a/wm/src/client.c b/wm/src/client.c index 5f50b34..5fc5aa4 100644 --- a/wm/src/client.c +++ b/wm/src/client.c @@ -65,9 +65,9 @@ Client *client_add(WM *wm, Window win) { c->x = wa.x; c->y = wa.y; } - /* never land behind deskbar (top-right panel ~180px wide) */ + /* never land behind deskbar */ int sw = DisplayWidth(wm->dpy, wm->screen); - if (c->x + (int)c->w > sw - 185 && c->y < 200) + if (c->x + (int)c->w > sw - DESKBAR_W && c->y < 200) c->y = 40; /* fetch title */ @@ -228,7 +228,7 @@ void client_maximize(WM *wm, Client *c) { int sh = DisplayHeight(wm->dpy, wm->screen); /* leave room for deskbar on the right */ - int deskbar_w = 185; + int deskbar_w = DESKBAR_W; int new_w = sw - deskbar_w - BORDER_W * 2; int new_h = sh - TITLE_HEIGHT - BORDER_W; @@ -280,7 +280,7 @@ void client_snap_left(WM *wm, Client *c) { if (c->maximized) client_unmaximize(wm, c); int sw = DisplayWidth(wm->dpy, wm->screen); int sh = DisplayHeight(wm->dpy, wm->screen); - int deskbar_w = 185; + int deskbar_w = DESKBAR_W; int half_w = (sw - deskbar_w) / 2; c->x = 0; @@ -299,7 +299,7 @@ void client_snap_right(WM *wm, Client *c) { if (c->maximized) client_unmaximize(wm, c); int sw = DisplayWidth(wm->dpy, wm->screen); int sh = DisplayHeight(wm->dpy, wm->screen); - int deskbar_w = 185; + int deskbar_w = DESKBAR_W; int half_w = (sw - deskbar_w) / 2; c->x = half_w; diff --git a/wm/src/events.c b/wm/src/events.c index 031b4fe..7f430fa 100644 --- a/wm/src/events.c +++ b/wm/src/events.c @@ -33,7 +33,7 @@ static void check_snap(WM *wm, Client *c) { } else { client_snap_left(wm, c); } - } else if (c->x + (int)(c->w + BORDER_W * 2) >= sw - 185 - SNAP_DIST) { + } else if (c->x + (int)(c->w + BORDER_W * 2) >= sw - DESKBAR_W - SNAP_DIST) { client_snap_right(wm, c); } else if (c->y <= SNAP_DIST) { client_maximize(wm, c); @@ -95,10 +95,34 @@ void on_enter_notify(WM *wm, XCrossingEvent *e) { } void on_client_message(WM *wm, XClientMessageEvent *e) { - Atom net_active = XInternAtom(wm->dpy, "_NET_ACTIVE_WINDOW", False); + 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); + if (e->message_type == net_active) { Client *c = client_find_by_win(wm, e->window); if (c) client_restore(wm, c); + return; + } + if (e->message_type == net_close) { + Client *c = client_find_by_win(wm, e->window); + if (c) { + XEvent ev = {0}; + ev.xclient.type = ClientMessage; + ev.xclient.window = c->win; + ev.xclient.message_type = wm->wm_protocols; + ev.xclient.format = 32; + ev.xclient.data.l[0] = wm->wm_delete_window; + ev.xclient.data.l[1] = CurrentTime; + XSendEvent(wm->dpy, c->win, False, NoEventMask, &ev); + } + return; + } + /* WM_CHANGE_STATE IconicState → minimize */ + if (e->message_type == wm_change_state && e->data.l[0] == 3) { + Client *c = client_find_by_win(wm, e->window); + if (c && !c->minimized) client_minimize(wm, c); + return; } } @@ -263,10 +287,19 @@ void on_key_press(WM *wm, XKeyEvent *e) { if ((e->state & Mod1Mask) && sym == XK_F4 && wm->focused) send_delete(wm, wm->focused->win); - /* Alt+Tab — visual switcher */ + /* Alt+Tab / Shift+Alt+Tab — visual switcher */ if ((e->state & Mod1Mask) && sym == XK_Tab) { - if (wm->swl_visible) switcher_next(wm); - else if (wm->clients) switcher_show(wm); + int backward = (e->state & ShiftMask) != 0; + if (wm->swl_visible) { + if (backward) switcher_prev(wm); + else switcher_next(wm); + } else if (wm->clients) { + switcher_show(wm); + if (backward && wm->swl_visible) { + wm->swl_idx = wm->swl_count - 1; + switcher_draw(wm); + } + } return; } diff --git a/wm/src/switcher.c b/wm/src/switcher.c index 45cb2d3..3096d85 100644 --- a/wm/src/switcher.c +++ b/wm/src/switcher.c @@ -8,17 +8,19 @@ static void swl_populate(WM *wm) { wm->swl_count = 0; - /* current focused window goes first */ + /* current focused (non-minimized) window goes first */ if (wm->focused && !wm->focused->minimized) { - wm->swl_wins[0] = wm->focused->win; + wm->swl_wins[0] = wm->focused->win; + wm->swl_mini[0] = 0; strncpy(wm->swl_titles[0], wm->focused->title, 127); wm->swl_titles[0][127] = '\0'; wm->swl_count = 1; } + /* all other windows, including minimized ones */ for (Client *c = wm->clients; c && wm->swl_count < 64; c = c->next) { - if (c->minimized) continue; if (wm->focused && c == wm->focused) continue; - wm->swl_wins[wm->swl_count] = c->win; + wm->swl_wins[wm->swl_count] = c->win; + wm->swl_mini[wm->swl_count] = c->minimized; strncpy(wm->swl_titles[wm->swl_count], c->title, 127); wm->swl_titles[wm->swl_count][127] = '\0'; wm->swl_count++; @@ -61,29 +63,34 @@ void switcher_draw(WM *wm) { for (int vi = 0; vi < vis; vi++) { int i = vstart + vi; int selected = (i == wm->swl_idx); + int mini = wm->swl_mini[i]; int ix = SWL_PAD + vi * (SWL_ITEM_W + SWL_PAD); int iy = SWL_PAD; - unsigned long card_bg = color_alloc(wm, selected ? COLOR_TITLE_ACTIVE : 0x1C2338); + unsigned long card_bg = color_alloc(wm, + selected ? COLOR_TITLE_ACTIVE : (mini ? 0x141820 : 0x1C2338)); XSetForeground(wm->dpy, wm->gc, card_bg); XFillRectangle(wm->dpy, wm->swl_win, wm->gc, ix, iy, SWL_ITEM_W, SWL_ITEM_H); XSetForeground(wm->dpy, wm->gc, - color_alloc(wm, selected ? COLOR_BORDER_LIGHT : 0x242C3E)); + color_alloc(wm, selected ? COLOR_BORDER_LIGHT : (mini ? 0x202028 : 0x242C3E))); XDrawRectangle(wm->dpy, wm->swl_win, wm->gc, ix, iy, SWL_ITEM_W - 1, SWL_ITEM_H - 1); - /* letter icon */ + /* letter icon — dimmed for minimized */ int icon_x = ix + (SWL_ITEM_W - SWL_ICON_SZ) / 2; int icon_y = iy + 8; XSetForeground(wm->dpy, wm->gc, - color_alloc(wm, selected ? 0x285898 : 0x182538)); + color_alloc(wm, selected ? 0x285898 : (mini ? 0x1A1E28 : 0x182538))); XFillRectangle(wm->dpy, wm->swl_win, wm->gc, icon_x, icon_y, SWL_ICON_SZ, SWL_ICON_SZ); char letter[2] = { wm->swl_titles[i][0] ? wm->swl_titles[i][0] : '?', '\0' }; if (letter[0] >= 'a' && letter[0] <= 'z') letter[0] -= 32; + unsigned long icon_rgb = selected ? COLOR_TITLE_TEXT_ACT + : (mini ? 0x3A4860 + : COLOR_TITLE_TEXT_INACT); XftColor icon_col; - xft_color(wm, selected ? COLOR_TITLE_TEXT_ACT : COLOR_TITLE_TEXT_INACT, &icon_col); + xft_color(wm, icon_rgb, &icon_col); XGlyphInfo ext; XftTextExtentsUtf8(wm->dpy, wm->font_title, (const FcChar8 *)letter, 1, &ext); int lx = icon_x + (SWL_ICON_SZ - (int)ext.width) / 2; @@ -91,10 +98,19 @@ void switcher_draw(WM *wm) { XftDrawStringUtf8(xd, &icon_col, wm->font_title, lx, ly, (const FcChar8 *)letter, 1); XftColorFree(wm->dpy, wm->visual, wm->cmap, &icon_col); - /* title below icon */ - const char *title = wm->swl_titles[i]; + /* title below icon — prefix [..] for minimized */ + char title_buf[132]; + const char *base_title = wm->swl_titles[i]; + if (mini && !selected) + snprintf(title_buf, sizeof(title_buf), "[%s]", base_title); + else + snprintf(title_buf, sizeof(title_buf), "%s", base_title); + const char *title = title_buf; XftColor title_col; - xft_color(wm, selected ? COLOR_TITLE_TEXT_ACT : COLOR_TITLE_TEXT_INACT, &title_col); + unsigned long title_rgb = selected ? COLOR_TITLE_TEXT_ACT + : (mini ? 0x3A4860 + : COLOR_TITLE_TEXT_INACT); + xft_color(wm, title_rgb, &title_col); XGlyphInfo text_ext; XftTextExtentsUtf8(wm->dpy, wm->font_title, (const FcChar8 *)title, strlen(title), &text_ext); @@ -163,6 +179,12 @@ void switcher_next(WM *wm) { switcher_draw(wm); } +void switcher_prev(WM *wm) { + if (!wm->swl_visible) return; + wm->swl_idx = (wm->swl_idx - 1 + wm->swl_count) % wm->swl_count; + switcher_draw(wm); +} + void switcher_hide(WM *wm, int commit) { if (!wm->swl_visible) return; XUngrabKeyboard(wm->dpy, CurrentTime); diff --git a/wm/src/wm.c b/wm/src/wm.c index 1634123..f70b384 100644 --- a/wm/src/wm.c +++ b/wm/src/wm.c @@ -90,6 +90,7 @@ void wm_init(WM *wm) { /* global key grabs */ struct { KeySym sym; unsigned mod; } grabs[] = { { XK_Tab, Mod1Mask }, + { XK_Tab, Mod1Mask | ShiftMask }, /* Shift+Alt+Tab — backwards */ { XK_F4, Mod1Mask }, { XK_F9, Mod1Mask }, /* minimize */ { XK_F10, Mod1Mask }, /* maximize */