Nimbin[12]?Sway / SWOv / commits / 737e4b1

SWOv git · main

SWOv - window overview for sway

sway wayland c overview · first commit 2026-08-21 · last commit 2026-09-20 (19 days ago) · synced 3 days ago · upstream: github.com/nimbin2/SWOv

C 91.5% Markdown 7.6%
git clone https://git.christianimmanuel.de/sway/SWOv.gitwget https://git.christianimmanuel.de/sway/SWOv/archive/SWOv.tar.gz

Bettter handling of dragging into floating windows

Christian Immanuel · 2026-09-04 12:22 · 737e4b1e58f426c987dc346b90fa3ac25cb32f7e

 README.md      |   72 +-
 appwheel.c     | 1304 -------------------
 config.example |   13 +
 swbr.c         | 3887 --------------------------------------------------------
 swov.c         |  686 +++++++++-
 5 files changed, 728 insertions(+), 5234 deletions(-)

diff --git a/README.md b/README.md
index 5983dde..dfb4ad7 100644
--- a/README.md
+++ b/README.md
@@ -45,6 +45,7 @@ for_window [app_id="swov"] floating enable, border none
 | `space`, right click | mark or unmark a window (marks are what the next action applies to) |
 | `shift+space`, `a` | mark or unmark the whole workspace |
 | `c` | clear all marks |
+| `d` | open the launcher (`swas`) and step aside |
 | `x`, `del` | close marked or selected windows, `enter` confirms |
 | middle click | close that window straight away |
 | `f` | find windows by app id, title or workspace name, highlighting the hits |
@@ -69,6 +70,18 @@ Press, move, release. `Esc` cancels.
 A floating or fullscreen window is grabbed by its name plate — the rest of it is
 click-through, so the windows underneath stay selectable.
 
+The window on the pointer keeps its place in the grid while you drag it,
+sunk halfway into the background with an accent border — the only thing on
+screen wearing that colour, so there is no forgetting which one you picked up.
+
+A floating window is drawn see-through and answers the pointer only through
+its name plate, so the windows underneath stay reachable. Touch that plate and
+it comes forward — solid, framed in accent with a soft halo around it, and
+taking the whole of itself — so its edges can be
+aimed at and an app dropped inside it. It is given up when the pointer leaves
+its card, and for one that covers the whole workspace, in a band around the
+edge of the tile, since otherwise there would be no way back out.
+
 **A window** onto a tile moves it there. Onto the left or right edge of another
 window it lands beside it, splitting horizontally; top or bottom splits
 vertically. A bar shows the edge before you let go.
@@ -207,14 +220,64 @@ it is in the same spot whichever screen you are looking at, and it never
 collides with anything. `outputs_map_w` is how wide it is as a share of the
 tile area (0.18 by default), `outputs_map=0` turns it off.
 
-Click a monitor and the overview shows *its* workspaces instead. The one you
+Each monitor wears the same colour as its workspaces do while dragging, so
+the map and the grid agree about which screen is which. Click one and the
+overview shows *its* workspaces instead. The one you
 are looking at is filled in, the one sway is really on keeps a ring, and while
-those differ the header says `viewing DP-1` in a highlighted box — everything
+those differ the whole overview is framed in that monitor's colour — the same
+one its workspaces and its plate wear — with `viewing DP-1` in the header on a
+solid badge of it — everything
 you do from there, every key and every drop, lands on that screen. Clicking
 the one you are already looking at goes back to following sway.
 
+While anything is being dragged, the workspaces of your **other screens**
+appear alongside this one's, each wearing its monitor's colour — the tile is
+tinted with it, the border is it, and the output name sits in a filled badge
+of it where the workspace name usually goes. The colours are the accent turned
+around the wheel by the golden angle, one step per screen, so two monitors
+never look alike and none of them lands on `hl` — that one already means "this
+is where you are" — so a window or an app goes straight onto workspace 8 on the
+other monitor without changing what you are looking at. `drop_outputs=0`
+keeps them out.
+
+Holding a drag over a monitor in the map switches to that one after
+`map_dwell_ms`, which is off by default now that the workspaces come to you — an app coming from swas, a window, or a whole workspace. The press
+has to be still: any movement starts the wait again, so brushing past a plate
+on the way somewhere else never triggers it. A window carries on being
+dragged across the change, so it can be dropped on the screen you arrive at,
+and a launcher is told the target is void at the moment of the switch — the
+drag is still in the air, nothing was let go of. The plate presses in and
+fills as you hold, so the wait reads as a button going down; moving off before
+it completes cancels. That way a drag that started on one screen can finish on
+another.
+
 `swov output=DP-1` starts on a given screen.
 
+swov's own window is on the workspace like anything else, so it is drawn —
+leaving a hole where it sits would be worse — but it is never selectable,
+droppable or in the way.
+
+While something is being dragged, a ✕ bar runs along the bottom from the
+monitor map to the right edge. Dragging downwards is enough to reach it — no
+aiming, no corner to find — and letting go there does nothing at all. Letting
+go there does nothing at all — the same as dropping it back where it came
+from, but without having to find that spot again.
+
+swas is started with two pipes and told to talk to *this* overview rather
+than bringing up its own, so the wheel appears in front and this window stays
+where it is. Pick an app, drop it, and the wheel closes again — the overview
+is still there, on the same screen, with the same selection. `launcher=`
+changes the command; the whole thing is one argument, so it needs quoting on
+a shell: `swov 'launcher=swas --replace overview=1'`. It is the default, so
+pressing `d` works without setting anything.
+
+## Over a fullscreen window
+
+A fullscreen window sits above everything an ordinary window can reach, so an
+overlay that is not on the layer shell cannot be drawn over it. swov takes it
+off fullscreen while it is up and puts it back exactly as it was on the way
+out. `over_fullscreen=0` leaves it alone, and swov opens behind it.
+
 ## Workspace usage
 
 Every switch goes through swov, so it stamps the time as it goes: the workspace
@@ -293,6 +356,11 @@ lists them. The file above is read after it, so swov's own config always wins.
 | `drop_ghosts` | offer the free numbers while an app is dragged over the backdrop |
 | `cpu`, `cpu_min`, `cpu_full` | the load dot, and the range it covers |
 | `outputs_map`, `outputs_map_w`, `output` | the monitor map, its width, and which screen to start on |
+| `drop_outputs` | show the other screens' workspaces while dragging |
+| `over_fullscreen` | un-fullscreen whatever is in the way, and restore it on exit |
+| `map_dwell_ms` | hold a drag over a monitor this long to switch to it; `0` is off |
+| `launcher` | what `d` opens; `swas --replace overview=1` by default |
+| `cancel_drop` | the ✕ beside the monitor map: let a drag go there and nothing happens |
 | `track`, `usage_dots`, `dot_count`, `dot_px` | usage recording and its dot scale |
 | `start_selection` | `workspace`, `none` or `window` |
 | `cols`, `rows` | force the grid; default picks the largest tiles |
diff --git a/appwheel.c b/appwheel.c
deleted file mode 100644
index 01eff4e..0000000
--- a/appwheel.c
+++ /dev/null
@@ -1,1304 +0,0 @@
-/*
- * appwheel — a GTA-V-style radial (weapon-wheel) app launcher for Wayland & X11.
- *
- * Dependencies:
- *   - SDL3            (the only library you link)
- *   - stb_truetype.h  (vendored single-header — smooth fonts)
- *   - stb_image.h     (vendored single-header — PNG/JPG/BMP icons)
- *   - nanosvg.h + nanosvgrast.h  (vendored single-header — SVG icons)
- * The vendored *.h files just sit next to this source; nothing to install/link.
- *
- * Build:
- *   cc appwheel.c -o appwheel $(pkg-config --cflags --libs sdl3) -lm
- *
- * See --help for every option (all config keys also work on the command line).
- */
-
-/* ---------------------------------------------------------------------------
- * How this file is laid out (top to bottom):
- *   1. small helpers        colors, string trim, hex-color + tilde parsing
- *   2. Config               the struct, defaults, key=value setter, file loader
- *   3. text (Font)          stb_truetype atlas + drawing, with a debug fallback
- *   4. AppList              .desktop parsing, dir scanning, include/exclude/sort
- *   5. icons                Icon= resolution + stb_image / nanosvg (lazy, cached)
- *   6. launching + history  detached exec, most-recently-used log
- *   7. geometry             circle/sector/chevron primitives
- *   8. usage()/dump_config  --help text and the default config generator
- *   9. main()               setup, then the event + render loop
- * ------------------------------------------------------------------------- */
-
-#include <SDL3/SDL.h>
-
-#define STB_TRUETYPE_IMPLEMENTATION
-#include "stb_truetype.h"
-#define STB_IMAGE_IMPLEMENTATION
-#define STBI_ONLY_PNG
-#define STBI_ONLY_JPEG
-#define STBI_ONLY_BMP
-#include "stb_image.h"
-
-#include "sw_theme.h"
-
-/* SVG icons via nanosvg (vendored single-header). Third-party headers, so we
-   quiet their warnings without affecting our own -Wall build. */
-#pragma GCC diagnostic push
-#pragma GCC diagnostic ignored "-Wunused-function"
-#pragma GCC diagnostic ignored "-Wunused-but-set-variable"
-#pragma GCC diagnostic ignored "-Wsign-compare"
-#define NANOSVG_IMPLEMENTATION
-#define NANOSVG_ALL_COLOR_KEYWORDS
-#include "nanosvg.h"
-#define NANOSVGRAST_IMPLEMENTATION
-#include "nanosvgrast.h"
-#pragma GCC diagnostic pop
-
-#include <stdio.h>
-#include <stdlib.h>
-#include <string.h>
-#include <strings.h>
-#include <ctype.h>
-#include <math.h>
-#include <dirent.h>
-#include <glob.h>
-#include <unistd.h>
-#include <sys/stat.h>
-#include <sys/types.h>
-#include <sys/wait.h>
-#include <fcntl.h>
-#include <limits.h>
-
-#ifndef M_PI
-#define M_PI 3.14159265358979323846
-#endif
-#define DEG (M_PI / 180.0)
-#define ICON_AL (120.0f*(float)DEG)   /* bottom-left paging icon  */
-#define ICON_AR (60.0f*(float)DEG)    /* bottom-right paging icon */
-
-/* ------------------------------------------------------------------ */
-typedef struct { Uint8 r, g, b, a; } Col;
-static SDL_FColor tofc(Col c){ SDL_FColor f={c.r/255.0f,c.g/255.0f,c.b/255.0f,c.a/255.0f}; return f; }
-static char *xstrdup(const char *s){ char*p=malloc(strlen(s)+1); if(p)strcpy(p,s); return p; }
-static char *trim(char *s){
-    while(*s && isspace((unsigned char)*s)) s++;
-    if(!*s) return s;
-    char*e=s+strlen(s)-1; while(e>s && isspace((unsigned char)*e)) *e--='\0';
-    return s;
-}
-static int contains_ci(const char*hay,const char*needle){
-    if(!*needle) return 1;
-    size_t nl=strlen(needle);
-    for(const char*p=hay;*p;p++) if(strncasecmp(p,needle,nl)==0) return 1;
-    return 0;
-}
-static void parse_color(const char*s, Col*out){
-    if(*s=='#') s++;
-    unsigned r,g,b,a=255;
-    if(strlen(s)>=8 && sscanf(s,"%2x%2x%2x%2x",&r,&g,&b,&a)==4){}
-    else if(sscanf(s,"%2x%2x%2x",&r,&g,&b)==3){}
-    else return;
-    out->r=(Uint8)r; out->g=(Uint8)g; out->b=(Uint8)b; out->a=(Uint8)a;
-}
-static int file_exists(const char*p){ struct stat st; return stat(p,&st)==0 && S_ISREG(st.st_mode); }
-static float clampf(float v,float lo,float hi){ return v<lo?lo:(v>hi?hi:v); }
-/* expand a leading ~/ to $HOME (no shell involved when we launch) */
-static void expand_tilde(const char*in,char*out,size_t n){
-    if(in[0]=='~' && (in[1]=='/'||in[1]=='\0')){
-        const char*home=getenv("HOME");
-        if(home){ snprintf(out,n,"%s%s",home,in+1); return; }
-    }
-    snprintf(out,n,"%s",in);
-}
-
-/* ------------------------------------------------------------------ */
-/* config                                                              */
-/* ------------------------------------------------------------------ */
-typedef struct {
-    int   width,height,fullscreen,close_on_focus_loss;
-    int   slots; float arc_deg; int page_ms;
-    float radius, y_offset;           /* wheel size + vertical nudge (of min dim) */
-    int   icons, icon_px;
-    float ui_scale; int font_px;
-    float label_px, title_px, search_px, count_px;   /* per-element text sizes */
-    int   ssaa;                       /* full-scene supersample (1..4) */
-    int   animate, anim_ms;           /* app-data crossfade on results change */
-    int   recent_first;               /* bias the best match toward recently-used apps */
-    char  font[PATH_MAX];
-    char  sort[16], launcher[16], terminal[128];
-    char  history[PATH_MAX], dirs[4096], include[8192], exclude[8192];
-    Col bg,ring,ring2,hl,text,hltext,center,accent,dim;
-} Config;
-
-static void config_defaults(Config*c){
-    memset(c,0,sizeof *c);
-    c->width=900; c->height=900; c->fullscreen=1; c->close_on_focus_loss=0;
-    c->slots=11; c->arc_deg=240.0f; c->page_ms=110;
-    c->radius=0.44f; c->y_offset=0.10f;
-    c->icons=1; c->icon_px=46; c->ui_scale=1.0f; c->font_px=50;
-    c->label_px=24; c->title_px=25; c->search_px=20; c->count_px=20;
-    c->ssaa=2;
-    c->animate=1; c->anim_ms=90; c->recent_first=1;
-    strcpy(c->sort,"recent"); strcpy(c->launcher,"sh");
-    const char*term=getenv("TERMINAL");
-    snprintf(c->terminal,sizeof c->terminal,"%s",term?term:"xterm");
-    const char*home=getenv("HOME"),*xc=getenv("XDG_CACHE_HOME");
-    if(xc&&*xc) snprintf(c->history,sizeof c->history,"%s/appwheel/history",xc);
-    else        snprintf(c->history,sizeof c->history,"%s/.cache/appwheel/history",home?home:".");
-    c->bg    =(Col){0x0d,0x11,0x17,0x00};   /* transparent by default */
-    c->ring  =(Col){0x1e,0x27,0x33,0xf2};
-    c->ring2 =(Col){0x26,0x31,0x3f,0xf2};
-    c->hl    =(Col){0xcb,0x9b,0x00,0xff};
-    c->text  =(Col){0xe8,0xe8,0xe8,0xff};
-    c->hltext=(Col){0x14,0x14,0x14,0xff};
-    c->center=(Col){0x0d,0x11,0x17,0xe6};
-    c->accent=(Col){0x89,0xaf,0xc4,0xff};
-    c->dim   =(Col){0x5a,0x6b,0x7a,0xff};
-}
-static void config_set(Config*c,const char*k,const char*v){
-    if(!strcmp(k,"width"))c->width=atoi(v);
-    else if(!strcmp(k,"height"))c->height=atoi(v);
-    else if(!strcmp(k,"fullscreen"))c->fullscreen=atoi(v);
-    else if(!strcmp(k,"close_on_focus_loss"))c->close_on_focus_loss=atoi(v);
-    else if(!strcmp(k,"slots"))c->slots=atoi(v);
-    else if(!strcmp(k,"arc")||!strcmp(k,"arc_deg"))c->arc_deg=(float)atof(v);
-    else if(!strcmp(k,"page_ms"))c->page_ms=atoi(v);
-    else if(!strcmp(k,"radius")||!strcmp(k,"size"))c->radius=(float)atof(v);
-    else if(!strcmp(k,"y_offset")||!strcmp(k,"y"))c->y_offset=(float)atof(v);
-    else if(!strcmp(k,"icons"))c->icons=atoi(v);
-    else if(!strcmp(k,"icon_px"))c->icon_px=atoi(v);
-    else if(!strcmp(k,"ui_scale")||!strcmp(k,"font_scale")||!strcmp(k,"text_scale"))c->ui_scale=(float)atof(v);
-    else if(!strcmp(k,"font_px"))c->font_px=atoi(v);
-    else if(!strcmp(k,"label_px"))c->label_px=(float)atof(v);
-    else if(!strcmp(k,"title_px"))c->title_px=(float)atof(v);
-    else if(!strcmp(k,"search_px")||!strcmp(k,"query_px"))c->search_px=(float)atof(v);
-    else if(!strcmp(k,"count_px"))c->count_px=(float)atof(v);
-    else if(!strcmp(k,"ssaa")||!strcmp(k,"aa"))c->ssaa=atoi(v);
-    else if(!strcmp(k,"animate"))c->animate=atoi(v);
-    else if(!strcmp(k,"anim_ms"))c->anim_ms=atoi(v);
-    else if(!strcmp(k,"recent_first"))c->recent_first=atoi(v);
-    else if(!strcmp(k,"font"))snprintf(c->font,sizeof c->font,"%s",v);
-    else if(!strcmp(k,"sort"))snprintf(c->sort,sizeof c->sort,"%s",v);
-    else if(!strcmp(k,"launcher"))snprintf(c->launcher,sizeof c->launcher,"%s",v);
-    else if(!strcmp(k,"terminal"))snprintf(c->terminal,sizeof c->terminal,"%s",v);
-    else if(!strcmp(k,"history"))snprintf(c->history,sizeof c->history,"%s",v);
-    else if(!strcmp(k,"dirs"))snprintf(c->dirs,sizeof c->dirs,"%s",v);
-    else if(!strcmp(k,"include"))snprintf(c->include,sizeof c->include,"%s",v);
-    else if(!strcmp(k,"exclude"))snprintf(c->exclude,sizeof c->exclude,"%s",v);
-    else if(!strcmp(k,"bg"))parse_color(v,&c->bg);
-    else if(!strcmp(k,"ring"))parse_color(v,&c->ring);
-    else if(!strcmp(k,"ring2"))parse_color(v,&c->ring2);
-    else if(!strcmp(k,"hl"))parse_color(v,&c->hl);
-    else if(!strcmp(k,"text"))parse_color(v,&c->text);
-    else if(!strcmp(k,"hltext"))parse_color(v,&c->hltext);
-    else if(!strcmp(k,"center"))parse_color(v,&c->center);
-    else if(!strcmp(k,"accent"))parse_color(v,&c->accent);
-    else if(!strcmp(k,"dim"))parse_color(v,&c->dim);
-    else fprintf(stderr,"wheel: unknown key '%s'\n",k);
-}
-/* the shared ~/.config/sw/config, translated into appwheel's own keys */
-static void config_set_shared(void*ud,const char*k,const char*v){ config_set((Config*)ud,k,v); }
-
-static void config_load(Config*c,const char*path){
-    FILE*f=fopen(path,"r"); if(!f) return;
-    char line[8192];
-    while(fgets(line,sizeof line,f)){
-        char*s=trim(line); if(!*s||*s=='#'||*s==';') continue;
-        char*eq=strchr(s,'='); if(!eq) continue;
-        *eq='\0'; config_set(c,trim(s),trim(eq+1));
-    }
-    fclose(f);
-}
-
-/* ------------------------------------------------------------------ */
-/* text: TTF atlas via stb_truetype, with debug-font fallback          */
-/* ------------------------------------------------------------------ */
-#define GLYPH_LO 32
-#define GLYPH_HI 255
-#define NGLYPH   (GLYPH_HI-GLYPH_LO+1)
-
-typedef struct {
-    int ok; SDL_Texture *atlas; int px; float baseline;
-    struct { float u,v,w,h,xoff,yoff,adv; } g[NGLYPH];
-} Font;
-
-static int try_font_paths(char*out,size_t n){
-    const char*cand[]={
-        "/usr/share/fonts/truetype/dejavu/DejaVuSans.ttf",
-        "/usr/share/fonts/dejavu/DejaVuSans.ttf",
-        "/usr/share/fonts/TTF/DejaVuSans.ttf",
-        "/usr/share/fonts/truetype/liberation/LiberationSans-Regular.ttf",
-        "/usr/share/fonts/noto/NotoSans-Regular.ttf",
-        "/usr/share/fonts/truetype/freefont/FreeSans.ttf", NULL };
-    for(int i=0;cand[i];i++) if(file_exists(cand[i])){ snprintf(out,n,"%s",cand[i]); return 1; }
-    return 0;
-}
-
-/* Ask fontconfig (via its fc-match CLI) to resolve a family/pattern — e.g.
-   "sans-serif", "monospace", or "JetBrains Mono" — to an actual .ttf path.
-   This is how we pick up the DESKTOP's configured default font. No library is
-   linked; if fc-match isn't installed we just return 0 and fall back. */
-static int fc_match(const char*pattern,char*out,size_t n){
-    if(!pattern||!*pattern) pattern="sans-serif";
-    char safe[256]; size_t j=0;                    /* strip shell metacharacters */
-    for(size_t i=0;pattern[i]&&j<sizeof safe-1;i++){
-        char c=pattern[i];
-        if(c=='"'||c=='`'||c=='$'||c=='\\'||c==';'||c=='|'||c=='&'||c=='\n'||c=='\r') continue;
-        safe[j++]=c;
-    }
-    safe[j]='\0';
-    char cmd[512];
-    snprintf(cmd,sizeof cmd,"fc-match --format=%%{file} \"%s\" 2>/dev/null",safe);
-    FILE*p=popen(cmd,"r"); if(!p) return 0;
-    char buf[PATH_MAX]; size_t r=fread(buf,1,sizeof buf-1,p); buf[r]='\0';
-    pclose(p);
-    char*s=trim(buf);
-    if(*s && file_exists(s)){ snprintf(out,n,"%s",s); return 1; }
-    return 0;
-}
-
-static int font_load(Font*ft,SDL_Renderer*ren,const char*path,int px){
-    memset(ft,0,sizeof *ft); ft->px=px;
-    FILE*f=fopen(path,"rb"); if(!f) return 0;
-    fseek(f,0,SEEK_END); long sz=ftell(f); fseek(f,0,SEEK_SET);
-    unsigned char*buf=malloc(sz);
-    if(!buf||fread(buf,1,sz,f)!=(size_t)sz){ fclose(f); free(buf); return 0; }
-    fclose(f);
-    stbtt_fontinfo info;
-    if(!stbtt_InitFont(&info,buf,stbtt_GetFontOffsetForIndex(buf,0))){ free(buf); return 0; }
-    float sc=stbtt_ScaleForPixelHeight(&info,(float)px);
-    int asc,desc,gap; stbtt_GetFontVMetrics(&info,&asc,&desc,&gap); ft->baseline=asc*sc;
-
-    int AW=1024, penx=0,peny=0,rowh=0;
-    for(int cp=GLYPH_LO;cp<=GLYPH_HI;cp++){
-        int x0,y0,x1,y1; stbtt_GetCodepointBitmapBox(&info,cp,sc,sc,&x0,&y0,&x1,&y1);
-        int gw=x1-x0,gh=y1-y0; if(gw<0)gw=0; if(gh<0)gh=0;
-        if(penx+gw+1>AW){ penx=0; peny+=rowh+1; rowh=0; }
-        if(gh>rowh)rowh=gh;
-        penx+=gw+1;
-    }
-    int AH=peny+rowh+1; if(AH<1)AH=1;
-    Uint32*pix=calloc((size_t)AW*AH,4); if(!pix){ free(buf); return 0; }
-
-    penx=0;peny=0;rowh=0;
-    for(int cp=GLYPH_LO;cp<=GLYPH_HI;cp++){
-        int aw,lsb; stbtt_GetCodepointHMetrics(&info,cp,&aw,&lsb);
-        int x0,y0,x1,y1; stbtt_GetCodepointBitmapBox(&info,cp,sc,sc,&x0,&y0,&x1,&y1);
-        int gw=x1-x0,gh=y1-y0; if(gw<0)gw=0; if(gh<0)gh=0;
-        if(penx+gw+1>AW){ penx=0; peny+=rowh+1; rowh=0; }
-        if(gw&&gh){
-            unsigned char*bmp=malloc((size_t)gw*gh);
-            stbtt_MakeCodepointBitmap(&info,bmp,gw,gh,gw,sc,sc,cp);
-            for(int yy=0;yy<gh;yy++)for(int xx=0;xx<gw;xx++)
-                pix[(peny+yy)*AW+(penx+xx)]=((Uint32)bmp[yy*gw+xx]<<24)|0x00FFFFFF;
-            free(bmp);
-        }
-        int gi=cp-GLYPH_LO;
-        ft->g[gi].u=penx; ft->g[gi].v=peny; ft->g[gi].w=gw; ft->g[gi].h=gh;
-        ft->g[gi].xoff=x0; ft->g[gi].yoff=y0; ft->g[gi].adv=aw*sc;
-        if(gh>rowh)rowh=gh;
-        penx+=gw+1;
-    }
-    free(buf);
-    SDL_Surface*surf=SDL_CreateSurfaceFrom(AW,AH,SDL_PIXELFORMAT_ABGR8888,pix,AW*4);
-    if(!surf){ free(pix); return 0; }
-    ft->atlas=SDL_CreateTextureFromSurface(ren,surf);
-    SDL_DestroySurface(surf); free(pix);
-    if(!ft->atlas) return 0;
-    SDL_SetTextureScaleMode(ft->atlas,SDL_SCALEMODE_LINEAR);
-    SDL_SetTextureBlendMode(ft->atlas,SDL_BLENDMODE_BLEND);
-    ft->ok=1; return 1;
-}
-
-static unsigned utf8_next(const char**p){
-    const unsigned char*s=(const unsigned char*)*p; unsigned cp; int n;
-    if(s[0]<0x80){cp=s[0];n=1;}
-    else if((s[0]&0xE0)==0xC0){cp=s[0]&0x1F;n=2;}
-    else if((s[0]&0xF0)==0xE0){cp=s[0]&0x0F;n=3;}
-    else if((s[0]&0xF8)==0xF0){cp=s[0]&0x07;n=4;}
-    else{ *p=(const char*)(s+1); return 0xFFFD; }
-    for(int i=1;i<n;i++){ if((s[i]&0xC0)!=0x80){n=i;break;} cp=(cp<<6)|(s[i]&0x3F); }
-    *p=(const char*)(s+n); return cp;
-}
-static float text_width(Font*ft,float px_h,const char*s){
-    if(!ft->ok) return strlen(s)*8.0f*(px_h/8.0f);
-    float ds=px_h/ft->px,w=0; const char*p=s;
-    while(*p){ unsigned cp=utf8_next(&p); if(cp<GLYPH_LO||cp>GLYPH_HI)cp='?'; w+=ft->g[cp-GLYPH_LO].adv*ds; }
-    return w;
-}
-static void text_draw(SDL_Renderer*r,Font*ft,float x,float y,float px_h,Col c,const char*s){
-    if(!ft->ok){
-        /* built-in 8x8 font: compose with any active render scale (e.g. SSAA) */
-        float k=px_h/8.0f, csx,csy; SDL_GetRenderScale(r,&csx,&csy);
-        SDL_SetRenderDrawColor(r,c.r,c.g,c.b,c.a);
-        SDL_SetRenderScale(r,csx*k,csy*k);
-        SDL_RenderDebugText(r,x/k,y/k,s);
-        SDL_SetRenderScale(r,csx,csy);
-        return;
-    }
-    float ds=px_h/ft->px;
-    SDL_SetTextureColorMod(ft->atlas,c.r,c.g,c.b);
-    SDL_SetTextureAlphaMod(ft->atlas,c.a);
-    float pen=x; const char*p=s;
-    while(*p){ unsigned cp=utf8_next(&p); if(cp<GLYPH_LO||cp>GLYPH_HI)cp='?';
-        int gi=cp-GLYPH_LO;
-        if(ft->g[gi].w>0&&ft->g[gi].h>0){
-            SDL_FRect src={ft->g[gi].u,ft->g[gi].v,ft->g[gi].w,ft->g[gi].h};
-            SDL_FRect dst={pen+ft->g[gi].xoff*ds,y+(ft->baseline+ft->g[gi].yoff)*ds,ft->g[gi].w*ds,ft->g[gi].h*ds};
-            SDL_RenderTexture(r,ft->atlas,&src,&dst);
-        }
-        pen+=ft->g[gi].adv*ds;
-    }
-}
-static void text_centered(SDL_Renderer*r,Font*ft,float cx,float cy,float px_h,Col c,const char*s){
-    float w=text_width(ft,px_h,s); text_draw(r,ft,cx-w/2,cy-px_h/2,px_h,c,s);
-}
-static void fit_label(Font*ft,float px_h,const char*s,float max_w,char*out,size_t n){
-    if(text_width(ft,px_h,s)<=max_w){ snprintf(out,n,"%s",s); return; }
-    char buf[256]; snprintf(buf,sizeof buf,"%s",s); int len=(int)strlen(buf);
-    while(len>1){ buf[len]='\0';
-        char t[260]; snprintf(t,sizeof t,"%.*s..",len,buf);
-        if(text_width(ft,px_h,t)<=max_w){ snprintf(out,n,"%.*s",(int)n-1,t); return; }
-        len--;
-    }
-    snprintf(out,n,"..");
-}
-
-/* ------------------------------------------------------------------ */
-/* application list                                                    */
-/* ------------------------------------------------------------------ */
-typedef struct {
-    char *id,*name,*exec,*icon; int terminal;
-    SDL_Texture *tex; int icon_tried; int recent;   /* 1 = present in launch history */
-} App;
-typedef struct { App*v; int n,cap; } AppList;
-
-static void applist_push(AppList*l,App a){
-    if(l->n==l->cap){ l->cap=l->cap?l->cap*2:128; l->v=realloc(l->v,l->cap*sizeof*l->v); }
-    l->v[l->n++]=a;
-}
-static int applist_has_id(AppList*l,const char*id){
-    for(int i=0;i<l->n;i++) if(!strcmp(l->v[i].id,id)) return 1;
-    return 0;
-}
-static char *strip_field_codes(const char*exec){
-    char*out=malloc(strlen(exec)+1),*o=out;
-    for(const char*p=exec;*p;p++){ if(*p=='%'){ if(p[1]=='%'){*o++='%';p++;} else if(p[1])p++; } else *o++=*p; }
-    *o='\0';
-    char*r=out,*w=out; int sp=0;
-    for(;*r;r++){ if(*r==' '){ if(!sp)*w++=' '; sp=1; } else { *w++=*r; sp=0; } }
-    *w='\0'; return out;
-}
-static int parse_desktop(const char*path,const char*id,App*out){
-    FILE*f=fopen(path,"r"); if(!f) return 0;
-    char*name=NULL,*exec=NULL,*type=NULL,*icon=NULL;
-    int nodisplay=0,hidden=0,terminal=0,in=0; char line[8192];
-    while(fgets(line,sizeof line,f)){
-        char*s=trim(line);
-        if(*s=='['){ if(in)break; in=!strcmp(s,"[Desktop Entry]"); continue; }
-        if(!in||*s=='#') continue;
-        char*eq=strchr(s,'='); if(!eq) continue;
-        *eq='\0'; char*k=trim(s),*v=trim(eq+1);
-        if(!name&&!strcmp(k,"Name"))name=xstrdup(v);
-        else if(!exec&&!strcmp(k,"Exec"))exec=xstrdup(v);
-        else if(!type&&!strcmp(k,"Type"))type=xstrdup(v);
-        else if(!icon&&!strcmp(k,"Icon"))icon=xstrdup(v);
-        else if(!strcmp(k,"NoDisplay"))nodisplay=!strcasecmp(v,"true");
-        else if(!strcmp(k,"Hidden"))hidden=!strcasecmp(v,"true");
-        else if(!strcmp(k,"Terminal"))terminal=!strcasecmp(v,"true");
-    }
-    fclose(f);
-    int ok=1;
-    if(!name||!exec) ok=0;
-    if(type&&strcmp(type,"Application")) ok=0;
-    if(nodisplay||hidden) ok=0;
-    if(ok){
-        out->id=xstrdup(id); out->name=name; name=NULL;
-        out->exec=strip_field_codes(exec); out->icon=icon; icon=NULL;
-        out->terminal=terminal; out->tex=NULL; out->icon_tried=0;
-    }
-    free(name);free(exec);free(type);free(icon);
-    return ok;
-}
-static void scan_dir(AppList*l,const char*dir){
-    DIR*d=opendir(dir); if(!d) return;
-    struct dirent*e;
-    while((e=readdir(d))){
-        const char*nm=e->d_name; size_t len=strlen(nm);
-        if(len<9||strcmp(nm+len-8,".desktop")) continue;
-        char id[256]; snprintf(id,sizeof id,"%.*s",(int)(len-8),nm);
-        if(applist_has_id(l,id)) continue;
-        char path[PATH_MAX]; snprintf(path,sizeof path,"%s/%s",dir,nm);
-        App a; if(parse_desktop(path,id,&a)) applist_push(l,a);
-    }
-    closedir(d);
-}
-static void collect_default_dirs(char out[][PATH_MAX],int*n,int max){
-    const char*home=getenv("HOME"),*xdh=getenv("XDG_DATA_HOME"),*xdd=getenv("XDG_DATA_DIRS");
-    char buf[PATH_MAX];
-    if(xdh&&*xdh) snprintf(buf,sizeof buf,"%s/applications",xdh);
-    else snprintf(buf,sizeof buf,"%s/.local/share/applications",home?home:".");
-    if(*n<max) snprintf(out[(*n)++],PATH_MAX,"%s",buf);
-    const char*dirs=(xdd&&*xdd)?xdd:"/usr/local/share:/usr/share";
-    char*copy=xstrdup(dirs),*tok=strtok(copy,":");
-    while(tok&&*n<max){ snprintf(out[(*n)++],PATH_MAX,"%s/applications",tok); tok=strtok(NULL,":"); }
-    free(copy);
-    glob_t g;
-    if(glob("/opt/*/share/applications",0,NULL,&g)==0)
-        for(size_t i=0;i<g.gl_pathc&&*n<max;i++) snprintf(out[(*n)++],PATH_MAX,"%s",g.gl_pathv[i]);
-    globfree(&g);
-}
-static int cmp_name(const void*a,const void*b){ return strcasecmp(((const App*)a)->name,((const App*)b)->name); }
-
-static void apply_config_order(AppList*l,Config*c){
-    if(*c->exclude){
-        char*copy=xstrdup(c->exclude),*tok=strtok(copy,",");
-        while(tok){ char*t=trim(tok);
-            for(int i=0;i<l->n;i++)
-                if(!strcasecmp(l->v[i].id,t)||!strcasecmp(l->v[i].name,t)){
-                    free(l->v[i].id);free(l->v[i].name);free(l->v[i].exec);free(l->v[i].icon);
-                    l->v[i]=l->v[--l->n]; i--;
-                }
-            tok=strtok(NULL,",");
-        }
-        free(copy);
-    }
-    if(*c->include){
-        AppList kept={0};
-        char*copy=xstrdup(c->include),*tok=strtok(copy,",");
-        while(tok){ char*t=trim(tok);
-            for(int i=0;i<l->n;i++)
-                if(l->v[i].id&&(!strcasecmp(l->v[i].id,t)||!strcasecmp(l->v[i].name,t))){
-                    applist_push(&kept,l->v[i]); l->v[i].id=NULL; break;
-                }
-            tok=strtok(NULL,",");
-        }
-        free(copy);
-        for(int i=0;i<l->n;i++)
-            if(l->v[i].id){ free(l->v[i].id);free(l->v[i].name);free(l->v[i].exec);free(l->v[i].icon); }
-        free(l->v); *l=kept; return;
-    }
-    if(!strcmp(c->sort,"alpha")) qsort(l->v,l->n,sizeof*l->v,cmp_name);
-}
-static void apply_recency(AppList*l,Config*c){
-    if(*c->include) return;
-    if(strcmp(c->sort,"recent")) return;
-    FILE*f=fopen(c->history,"r");
-    qsort(l->v,l->n,sizeof*l->v,cmp_name);
-    if(!f) return;
-    App*ord=malloc(l->n*sizeof*ord); int on=0; char*used=calloc(l->n,1); char line[512];
-    while(fgets(line,sizeof line,f)){ char*id=trim(line); if(!*id)continue;
-        for(int i=0;i<l->n;i++) if(!used[i]&&!strcmp(l->v[i].id,id)){ l->v[i].recent=1; ord[on++]=l->v[i]; used[i]=1; break; } }
-    fclose(f);
-    for(int i=0;i<l->n;i++) if(!used[i]){ l->v[i].recent=0; ord[on++]=l->v[i]; }
-    memcpy(l->v,ord,l->n*sizeof*l->v); free(ord); free(used);
-}
-
-/* Search ordering: split matches into "direct" (query starts a word in the name)
-   and "close" (query only appears mid-word), then lay them out so the best match
-   sits at the TOP, direct matches fan to the RIGHT, close matches to the LEFT.
-   Fills filt[] and picks off/selslot so the best is centered (at the top). */
-static int is_sep(char c){ return c==' '||c=='-'||c=='_'||c=='.'||c=='/'||c==':'; }
-/* match quality: 0 exact, 1 whole-name prefix, 2 word-start, 3 mid-word, -1 none */
-static int match_tier(const char*name,const char*q,size_t ql){
-    if(!strcasecmp(name,q)) return 0;
-    if(!strncasecmp(name,q,ql)) return 1;
-    int ws=1;
-    for(const char*p=name;*p;p++){
-        if(ws && !strncasecmp(p,q,ql)) return 2;
-        ws=is_sep(*p);
-    }
-    return contains_ci(name,q)?3:-1;
-}
-typedef struct { int idx,tier,len,rb; } Match;
-static int match_cmp(const void*a,const void*b){
-    const Match*x=a,*y=b;
-    if(x->rb  !=y->rb  ) return x->rb  -y->rb;          /* recently-used first (if enabled) */
-    if(x->tier!=y->tier) return x->tier-y->tier;       /* better tier first */
-    if(x->len !=y->len ) return x->len -y->len;         /* shorter (closer) first */
-    return x->idx-y->idx;                               /* then recency order */
-}
-static int build_filter(AppList*apps,const char*query,int*filt,int slots,int recent_first,int*p_off,int*p_selslot){
-    int fn=0;
-    if(!query[0]){                                      /* no query: all apps, in order */
-        for(int i=0;i<apps->n;i++) filt[fn++]=i;
-        *p_off=0; *p_selslot=0; return fn;
-    }
-    size_t ql=strlen(query);
-    Match *m=malloc(apps->n*sizeof(Match)); int nm=0;
-    if(m) for(int i=0;i<apps->n;i++){
-        const char*nm_s=apps->v[i].name, *id_s=apps->v[i].id;
-        int tn=match_tier(nm_s,query,ql);                      /* match on the name ... */
-        int ti=id_s?match_tier(id_s,query,ql):-1;              /* ...or the id */
-        int t = tn<0?ti : (ti<0?tn : (tn<ti?tn:ti));           /* best of the two */
-        if(t<0) continue;
-        int mlen=1<<30;                                        /* shortest string that hit that tier */
-        if(tn==t){ int l=(int)strlen(nm_s); if(l<mlen)mlen=l; }
-        if(ti==t){ int l=(int)strlen(id_s); if(l<mlen)mlen=l; }
-        m[nm].idx=i; m[nm].tier=t; m[nm].len=mlen;
-        m[nm].rb=(recent_first && apps->v[i].recent)?0:1;      /* recently-used matches win */
-        nm++;
-    }
-    if(nm>1) qsort(m,nm,sizeof(Match),match_cmp);       /* best-ranked first */
-    int *dir=malloc(nm*sizeof(int)+1), *clo=malloc(nm*sizeof(int)+1);
-    int nd=0,nc=0;
-    if(dir&&clo) for(int j=0;j<nm;j++){                 /* keep ranked order within each group */
-        if(m[j].tier<3) dir[nd++]=m[j].idx; else clo[nc++]=m[j].idx;
-    }
-    free(m);
-    fn=nd+nc;
-    if(fn<=0){ free(dir);free(clo); *p_off=0;*p_selslot=0; return 0; }
-    int best,*right,nr,*left,nl;
-    if(nd>0){ best=dir[0]; right=dir+1; nr=nd-1; left=clo;   nl=nc;   }
-    else    { best=clo[0]; right=clo;   nr=0;    left=clo+1; nl=nc-1; }
-    int c=(fn-1)/2;                                     /* best sits at the array centre */
-    filt[c]=best;
-    int rp=0,lp=0;
-    for(int ri=c+1; ri<fn; ri++){                      /* right of centre: direct, then overflow */
-        if(rp<nr) filt[ri]=right[rp++]; else if(lp<nl) filt[ri]=left[lp++];
-    }
-    for(int li=c-1; li>=0; li--){                      /* left of centre: close, then overflow */
-        if(lp<nl) filt[li]=left[lp++]; else if(rp<nr) filt[li]=right[rp++];
-    }
-    free(dir); free(clo);
-    int vis = fn<slots?fn:slots; if(vis<1)vis=1;
-    if(vis>=4 && (vis&1)==0) vis--;                     /* odd -> best lands at the exact top */
-    int maxoff = fn>vis?fn-vis:0;
-    int off = c-(vis-1)/2; if(off<0)off=0; if(off>maxoff)off=maxoff;
-    int ss = c-off; if(ss<0)ss=0; if(ss>vis-1)ss=vis-1;
-    *p_off=off; *p_selslot=ss;
-    return fn;
-}
-
-/* ------------------------------------------------------------------ */
-/* icons                                                               */
-/* ------------------------------------------------------------------ */
-static void resolve_icon(const char*name,char*out,size_t n){
-    out[0]='\0'; if(!name||!*name) return;
-    if(strchr(name,'/')){ if(file_exists(name)) snprintf(out,n,"%s",name); return; }
-    const char*home=getenv("HOME"),*xdh=getenv("XDG_DATA_HOME");
-    char bases[8][PATH_MAX]; int nb=0;
-    if(xdh&&*xdh) snprintf(bases[nb++],PATH_MAX,"%s/icons",xdh);
-    else if(home) snprintf(bases[nb++],PATH_MAX,"%s/.local/share/icons",home);
-    if(home) snprintf(bases[nb++],PATH_MAX,"%s/.icons",home);
-    snprintf(bases[nb++],PATH_MAX,"/usr/local/share/icons");
-    snprintf(bases[nb++],PATH_MAX,"/usr/share/icons");
-    const char*themes[]={"hicolor","Adwaita","gnome","breeze","Papirus","Humanity",NULL};
-    const char*sizes[]={"48x48","64x64","32x32","128x128","256x256","96x96","24x24","scalable",NULL};
-    const char*cats[]={"apps","categories","devices","places","status","mimetypes","actions",NULL};
-    const char*exts[]={"png","svg","jpg","bmp",NULL};
-    char p[PATH_MAX];
-    /* icons dropped directly in an icons dir (not a theme subdir), e.g.
-       ~/.local/share/icons/duckduckgo.svg — check these first */
-    for(int b=0;b<nb;b++)for(int e=0;exts[e];e++){
-        snprintf(p,sizeof p,"%.3500s/%.400s.%s",bases[b],name,exts[e]);
-        if(file_exists(p)){ snprintf(out,n,"%s",p); return; }
-    }
-    for(int b=0;b<nb;b++)for(int t=0;themes[t];t++)for(int s=0;sizes[s];s++)
-        for(int ca=0;cats[ca];ca++)for(int e=0;exts[e];e++){
-            snprintf(p,sizeof p,"%.3500s/%s/%s/%s/%.400s.%s",bases[b],themes[t],sizes[s],cats[ca],name,exts[e]);
-            if(file_exists(p)){ snprintf(out,n,"%s",p); return; }
-        }
-    const char*pm[]={"/usr/share/pixmaps","/usr/local/share/pixmaps",NULL};
-    for(int i=0;pm[i];i++)for(int e=0;exts[e];e++){
-        snprintf(p,sizeof p,"%s/%.400s.%s",pm[i],name,exts[e]);
-        if(file_exists(p)){ snprintf(out,n,"%s",p); return; }
-    }
-}
-static SDL_Texture *load_icon_tex(SDL_Renderer*ren,const char*path){
-    int w,h,ch; unsigned char*data=stbi_load(path,&w,&h,&ch,4);
-    if(!data) return NULL;
-    SDL_Surface*s=SDL_CreateSurfaceFrom(w,h,SDL_PIXELFORMAT_ABGR8888,data,w*4);
-    if(!s){ stbi_image_free(data); return NULL; }
-    SDL_Texture*t=SDL_CreateTextureFromSurface(ren,s);
-    SDL_DestroySurface(s); stbi_image_free(data);
-    if(t){ SDL_SetTextureScaleMode(t,SDL_SCALEMODE_LINEAR); SDL_SetTextureBlendMode(t,SDL_BLENDMODE_BLEND); }
-    return t;
-}
-static int ends_with_ci(const char*s,const char*suf){
-    size_t ls=strlen(s),lf=strlen(suf);
-    return ls>=lf && strcasecmp(s+ls-lf,suf)==0;
-}
-/* rasterise an SVG icon to a texture at ~px pixels (aspect preserved) */
-static SDL_Texture *load_svg_tex(SDL_Renderer*ren,const char*path,int px){
-    if(px<16)px=16; if(px>512)px=512;
-    NSVGimage*img=nsvgParseFromFile(path,"px",96.0f);
-    if(!img) return NULL;
-    if(img->width<=0||img->height<=0){ nsvgDelete(img); return NULL; }
-    float big=img->width>img->height?img->width:img->height;
-    float sc=(float)px/big;
-    int ow=(int)(img->width*sc+0.5f), oh=(int)(img->height*sc+0.5f);
-    if(ow<1)ow=1; if(oh<1)oh=1;
-    unsigned char*pix=malloc((size_t)ow*oh*4);
-    NSVGrasterizer*r=nsvgCreateRasterizer();
-    if(!pix||!r){ free(pix); if(r)nsvgDeleteRasterizer(r); nsvgDelete(img); return NULL; }
-    nsvgRasterize(r,img,0,0,sc,pix,ow,oh,ow*4);
-    nsvgDeleteRasterizer(r); nsvgDelete(img);
-    SDL_Surface*s=SDL_CreateSurfaceFrom(ow,oh,SDL_PIXELFORMAT_ABGR8888,pix,ow*4);
-    if(!s){ free(pix); return NULL; }
-    SDL_Texture*t=SDL_CreateTextureFromSurface(ren,s);
-    SDL_DestroySurface(s); free(pix);
-    if(t){ SDL_SetTextureScaleMode(t,SDL_SCALEMODE_LINEAR); SDL_SetTextureBlendMode(t,SDL_BLENDMODE_BLEND); }
-    return t;
-}
-static SDL_Texture *app_icon(SDL_Renderer*ren,App*a,Config*c){
-    if(!c->icons) return NULL;
-    if(!a->icon_tried){ a->icon_tried=1;
-        char path[PATH_MAX]; resolve_icon(a->icon,path,sizeof path);
-        if(*path){
-            if(ends_with_ci(path,".svg")){
-                int px=(int)(c->icon_px*c->ui_scale*2.0f); if(px<48)px=48; if(px>256)px=256;
-                a->tex=load_svg_tex(ren,path,px);
-            } else {
-                a->tex=load_icon_tex(ren,path);
-            }
-        }
-    }
-    return a->tex;
-}
-
-/* ------------------------------------------------------------------ */
-/* launching + history                                                 */
-/* ------------------------------------------------------------------ */
-static void mkparents(const char*path){
-    char tmp[PATH_MAX]; snprintf(tmp,sizeof tmp,"%s",path);
-    for(char*p=tmp+1;*p;p++) if(*p=='/'){ *p='\0'; mkdir(tmp,0755); *p='/'; }
-}
-static void history_prepend(Config*c,const char*id){
-    mkparents(c->history);
-    char*lines[256]; int ln=0; FILE*f=fopen(c->history,"r");
-    if(f){ char b[512]; while(ln<255&&fgets(b,sizeof b,f)){ char*t=trim(b);
-        if(!*t||!strcmp(t,id))continue; lines[ln++]=xstrdup(t); }
-        fclose(f);
-    }
-    f=fopen(c->history,"w"); if(!f){ for(int i=0;i<ln;i++)free(lines[i]); return; }
-    fprintf(f,"%s\n",id);
-    for(int i=0;i<ln;i++){ fprintf(f,"%s\n",lines[i]); free(lines[i]); }
-    fclose(f);
-}
-static void launch(App*a,Config*c){
-    char cmd[8192];
-    if(!strcmp(c->launcher,"gtk-launch")) snprintf(cmd,sizeof cmd,"gtk-launch %s.desktop",a->id);
-    else if(a->terminal) snprintf(cmd,sizeof cmd,"%s -e %s",c->terminal,a->exec);
-    else snprintf(cmd,sizeof cmd,"%s",a->exec);
-    history_prepend(c,a->id);
-
-    /* Launch fully detached from whatever started us. Without this, the child
-       inherits our stdin/stdout/stderr and controlling terminal, so a terminal
-       app (or anything that touches the tty) corrupts the shell we were run
-       from. We: new session (setsid) so there's no controlling tty, a second
-       fork so it can never reacquire one and gets reparented to init, and
-       std fds pointed at /dev/null. */
-    pid_t pid=fork();
-    if(pid<0){ fprintf(stderr,"appwheel: fork failed\n"); return; }
-    if(pid==0){
-        setsid();
-        pid_t p2=fork();
-        if(p2>0) _exit(0);
-        int fd=open("/dev/null",O_RDWR);
-        if(fd>=0){ dup2(fd,0); dup2(fd,1); dup2(fd,2); if(fd>2) close(fd); }
-        const char*home=getenv("HOME"); if(home){ if(chdir(home)!=0){} }
-        execl("/bin/sh","sh","-c",cmd,(char*)NULL);
-        _exit(127);
-    }
-    waitpid(pid,NULL,0);   /* reap the intermediate child; grandchild -> init */
-}
-
-/* ------------------------------------------------------------------ */
-/* geometry primitives                                                 */
-/* ------------------------------------------------------------------ */
-static void fill_circle(SDL_Renderer*r,float cx,float cy,float rad,Col c){
-    const int SEG=96; SDL_Vertex v[SEG+2]; SDL_FColor fc=tofc(c);
-    v[0].position=(SDL_FPoint){cx,cy}; v[0].color=fc; v[0].tex_coord=(SDL_FPoint){0,0};
-    for(int i=0;i<=SEG;i++){ float a=(float)(2*M_PI*i/SEG);
-        v[i+1].position=(SDL_FPoint){cx+rad*cosf(a),cy+rad*sinf(a)};
-        v[i+1].color=fc; v[i+1].tex_coord=(SDL_FPoint){0,0}; }
-    int idx[SEG*3]; for(int i=0;i<SEG;i++){ idx[i*3]=0; idx[i*3+1]=i+1; idx[i*3+2]=i+2; }
-    SDL_RenderGeometry(r,NULL,v,SEG+2,idx,SEG*3);
-}
-static void fill_sector(SDL_Renderer*r,float cx,float cy,float r0,float r1,float a0,float a1,Col c){
-    int seg=(int)((a1-a0)/0.04f)+2; if(seg<2)seg=2; if(seg>160)seg=160;
-    int nv=(seg+1)*2; SDL_Vertex*v=malloc(nv*sizeof*v); SDL_FColor fc=tofc(c);
-    for(int i=0;i<=seg;i++){ float a=a0+(a1-a0)*i/seg,ca=cosf(a),sa=sinf(a);
-        v[2*i].position=(SDL_FPoint){cx+r0*ca,cy+r0*sa};
-        v[2*i+1].position=(SDL_FPoint){cx+r1*ca,cy+r1*sa};
-        v[2*i].color=v[2*i+1].color=fc; v[2*i].tex_coord=v[2*i+1].tex_coord=(SDL_FPoint){0,0}; }
-    int ni=seg*6,*idx=malloc(ni*sizeof*idx);
-    for(int i=0;i<seg;i++){ int b=2*i;
-        idx[i*6]=b;idx[i*6+1]=b+1;idx[i*6+2]=b+2; idx[i*6+3]=b+1;idx[i*6+4]=b+3;idx[i*6+5]=b+2; }
-    SDL_RenderGeometry(r,NULL,v,nv,idx,ni); free(v); free(idx);
-}
-static void fill_tri(SDL_Renderer*r,float x0,float y0,float x1,float y1,float x2,float y2,Col c){
-    SDL_FColor fc=tofc(c);
-    SDL_Vertex v[3]={ {{x0,y0},fc,{0,0}}, {{x1,y1},fc,{0,0}}, {{x2,y2},fc,{0,0}} };
-    int idx[3]={0,1,2};
-    SDL_RenderGeometry(r,NULL,v,3,idx,3);
-}
-/* stacked arrowheads pointing dir (+1 right / -1 left); count grows with speed */
-static void draw_chevrons(SDL_Renderer*r,float x,float y,int dir,float s,int count,Col c){
-    if(count<1)count=1;
-    float gap=s*0.72f, total=(count-1)*gap, start=x-dir*total/2;
-    for(int i=0;i<count;i++){
-        float ax=start+dir*i*gap;
-        fill_tri(r, ax+dir*s*0.5f, y,
-                    ax-dir*s*0.5f, y-s*0.62f,
-                    ax-dir*s*0.5f, y+s*0.62f, c);
-    }
-}
-static float norm_ang(float a){ while(a>M_PI)a-=2*M_PI; while(a<=-M_PI)a+=2*M_PI; return a; }
-
-/* is the cursor on a paging ICON? (used only for the startup arming) */
-static int on_icon(float mx,float my,float cx,float cy,float rl,float hitr){
-    float lx=cx+rl*cosf(ICON_AL), ly=cy+rl*sinf(ICON_AL);
-    float rx=cx+rl*cosf(ICON_AR), ry=cy+rl*sinf(ICON_AR);
-    float h2=hitr*hitr;
-    float a=mx-lx,b=my-ly, c=mx-rx,d=my-ry;
-    return (a*a+b*b<=h2) || (c*c+d*d<=h2);
-}
-
-/* ------------------------------------------------------------------ */
-static void dump_config(void){
-    fputs(
-"# appwheel config  —  ~/.config/appwheel/config\n"
-"# One key=value per line. '#' starts a comment. Blank lines are ignored.\n"
-"# Every key here also works on the command line (key=value or --key=value);\n"
-"# command-line values win over the file. See `appwheel --help` for the full list.\n"
-"# The values below are the built-in defaults, so this file changes nothing\n"
-"# until you edit it.\n"
-"\n"
-"# --- window ---\n"
-"width=900\n"
-"height=900\n"
-"fullscreen=1     # cover the screen as a borderless overlay (0 = 900x900 window)\n"
-"close_on_focus_loss=0   # 1 = quit when the window loses focus (dmenu-style)\n"
-"\n"
-"# --- ring layout & paging ---\n"
-"slots=11          # wide, easy-to-hit slots across the TOP arc\n"
-"arc=240           # degrees of the ring used for apps (rest = paging zone)\n"
-"radius=0.44       # wheel size, as a fraction of the shorter screen side\n"
-"y_offset=0.10     # nudge the wheel down (apps sit up top, so this centers it)\n"
-"page_ms=110       # base ms per paged step at the SLOW end. Bigger = calmer\n"
-"                  # start. Paging eases in: slow where you enter the bottom\n"
-"                  # zone, fast toward straight-down.\n"
-"\n"
-"# --- rendering ---\n"
-"ssaa=2            # full-scene supersampling 1..4 (smooths edges). alias: aa\n"
-"animate=1         # crossfade app names/icons when results change\n"
-"anim_ms=90        # its duration (0 or animate=0 to turn it off)\n"
-"recent_first=1    # bias the best match toward your recently-used apps\n"
-"icons=1           # show .desktop icons if found (PNG/SVG/JPG/BMP)\n"
-"icon_px=46        # icon size (before ui_scale)\n"
-"\n"
-"# --- text ---\n"
-"ui_scale=1.0      # master text size. 1.3 = bigger, 0.85 = smaller.\n"
-"                  # aliases: font_scale, text_scale\n"
-"label_px=24       # app labels on the wheel   (all four are multiplied by\n"
-"title_px=25       # selected app name (center)  ui_scale, so tweak one or all)\n"
-"search_px=20      # the text you type (search box)\n"
-"count_px=20       # the \"3 / 42\" counter\n"
-"# By default appwheel uses your desktop's configured font (via fontconfig).\n"
-"# Set a .ttf path or a family name to override, e.g. font=JetBrains Mono\n"
-"# font=/usr/share/fonts/truetype/dejavu/DejaVuSans.ttf\n"
-"font_px=50        # glyph atlas height; the sharpness ceiling for big text\n"
-"\n"
-"# --- launching / ordering ---\n"
-"# appwheel logs every app you launch to the history file below and shows the\n"
-"# most-recently-opened ones first. This is the default (sort=recent).\n"
-"sort=recent       # recent = most-recently-opened first (DEFAULT) | alpha = A-Z\n"
-"launcher=sh       # sh = run Exec= ; gtk-launch = launch by desktop id\n"
-"terminal=xterm    # used for Terminal=true entries when launcher=sh\n"
-"# history=~/.cache/appwheel/history   # the opened-apps log (auto-created)\n"
-"\n"
-"# which apps show / in what order (an id is the .desktop filename without\n"
-"# the extension; run `appwheel --list` to see them):\n"
-"# dirs=~/.local/share/applications:/usr/share/applications\n"
-"# include=firefox,code,gimp     # show ONLY these ids, in this exact order\n"
-"# exclude=htop,xterm            # hide these (matches id OR Name)\n"
-"\n"
-"# --- colors: #rrggbb or #rrggbbaa ---\n"
-"# bg alpha < ff => transparent window background (needs a compositor)\n"
-"bg=0d111700\n"
-"ring=1e2733f2\n"
-"ring2=26313ff2\n"
-"hl=cb9b00ff        # highlighted slot\n"
-"text=e8e8e8ff      # normal slot text\n"
-"hltext=141414ff    # text on the highlighted slot\n"
-"center=0d1117e6    # hub disc (keep some alpha so search text stays legible)\n"
-"accent=89afc4ff    # typed query text + active paging chevrons\n"
-"dim=5a6b7aff       # hints / counters / idle chevrons\n",
-    stdout);
-}
-
-static void usage(const char*a0){
-    printf(
-"appwheel — a GTA-V-style radial (weapon-wheel) app launcher (Wayland & X11)\n"
-"\n"
-"USAGE\n"
-"  %s [options] [key=value ...]\n"
-"\n"
-"  Every config key below is also a command-line argument. Both forms work:\n"
-"      appwheel slots=8 arc=220 icons=0\n"
-"      appwheel --slots=8 --arc=220 --icons=0\n"
-"  CLI values override the config file.\n"
-"\n"
-"OPTIONS\n"
-"  -c, --config PATH   config file (default: $XDG_CONFIG_HOME/wheel/config)\n"
-"      --dump-config   print a commented default config (redirect to save it):\n"
-"                        appwheel --dump-config > ~/.config/appwheel/config\n"
-"      --list          print discovered apps (with resolved icon) and exit\n"
-"      --no-recent     rank matches purely by relevance, ignoring recent-app bias\n"
-"  -d, --dmenu         read newline-separated items from stdin, print the chosen\n"
-"                      one to stdout (a dmenu/bemenu/wofi-style picker)\n"
-"  -h, --help          show this help and exit\n"
-"\n"
-"LAYOUT / INTERACTION\n"
-"  slots=11            wide, easy-to-hit app slots across the top arc\n"
-"  arc=240             degrees of the ring used for apps (rest = paging zone)\n"
-"  radius=0.44         wheel size (fraction of the shorter screen side)\n"
-"  y_offset=0.10       nudge the wheel down so it looks vertically centered\n"
-"  page_ms=110         base ms per paged step; the farther left/right the\n"
-"                      cursor sits in the bottom zone, the faster it pages\n"
-"\n"
-"ICONS  (PNG, SVG, JPG, BMP)\n"
-"  icons=1             1 = show .desktop icons, 0 = labels only\n"
-"  icon_px=46          icon draw size (before ui_scale)\n"
-"\n"
-"TEXT\n"
-"  ui_scale=1.0        master text size (aliases: font_scale, text_scale)\n"
-"  label_px=24         app labels on the wheel     ) each is multiplied\n"
-"  title_px=25         selected app name (center)  ) by ui_scale; set any\n"
-"  search_px=20        the text you type           ) one independently\n"
-"  count_px=20         the \"3 / 42\" counter        )\n"
-"  font=PATH|FAMILY    a .ttf path, or a fontconfig family like \"JetBrains Mono\".\n"
-"                      Default: your desktop's configured font (via fc-match).\n"
-"  font_px=50          atlas raster height; larger = crisper big text\n"
-"\n"
-"RENDERING\n"
-"  ssaa=2              full-scene supersampling 1..4 (smooths edges; alias: aa)\n"
-"  animate=1 anim_ms=90  crossfade names/icons when results change (0 = off)\n"
-"  width=900 height=900 fullscreen=1  close_on_focus_loss=0\n"
-"\n"
-"SOURCES / ORDER\n"
-"  An app's \"id\" is just its .desktop filename without the extension:\n"
-"      /usr/share/applications/firefox.desktop      -> id \"firefox\"\n"
-"      ~/.local/share/applications/spotify.desktop  -> id \"spotify\"\n"
-"  Run  appwheel --list  to print every id next to its visible Name.\n"
-"\n"
-"  sort=recent                    recent (most-recently-opened first, the\n"
-"                                 DEFAULT) | alpha (A-Z). Every launch is\n"
-"                                 logged to the history file automatically.\n"
-"  include=firefox,gimp,spotify   show ONLY these ids, in this exact order\n"
-"  exclude=htop,xterm             hide these (matches the id OR the Name)\n"
-"  dirs=DIR:DIR                   scan these instead of the default dirs, e.g.\n"
-"      dirs=~/.local/share/applications:/usr/share/applications\n"
-"  launcher=sh                    sh = run Exec= ; gtk-launch = launch by id\n"
-"  terminal=xterm                 terminal for Terminal=true apps (launcher=sh)\n"
-"  history=PATH                   recency file (default ~/.cache/wheel/history)\n"
-"\n"
-"EXAMPLES\n"
-"  appwheel --list                          # discover ids\n"
-"  appwheel include=firefox,code,gimp       # a curated wheel, in that order\n"
-"  appwheel exclude=htop sort=alpha ssaa=3  # hide htop, A-Z, extra-smooth\n"
-"  appwheel ui_scale=1.3 icon_px=56         # bigger text and icons\n"
-"\n"
-"COLORS  (#rrggbb or #rrggbbaa)\n"
-"  bg ring ring2 hl text hltext center accent dim\n"
-"  bg alpha < ff => transparent window background (needs a compositor)\n"
-"\n"
-"CONTROLS\n"
-"  mouse over top arc      highlight a slot\n"
-"  bottom-left / -right    page back / forward (farther out = faster)\n"
-"  scroll wheel            scroll the list under the fixed highlight\n"
-"  type                    filter by name        Backspace  edit filter\n"
-"  Enter / left-click      launch                Esc        clear filter / quit\n"
-"  Right-click             quit\n",
-    a0);
-}
-
-/* Load the app/menu list (stdin for dmenu, else scan .desktop dirs). Kept
-   separate so the SDL window can be created first — see main(). */
-static void load_apps(AppList*apps, Config*cfg, int dmenu){
-    if(dmenu){                                     /* dmenu mode: items come from stdin */
-        char line[8192];
-        while(fgets(line,sizeof line,stdin)){
-            size_t L=strlen(line);
-            while(L && (line[L-1]=='\n'||line[L-1]=='\r')) line[--L]=0;
-            App a; memset(&a,0,sizeof a);
-            a.id=xstrdup(line); a.name=xstrdup(line);
-            applist_push(apps,a);                  /* input order preserved */
-        }
-    } else if(*cfg->dirs){ char*copy=xstrdup(cfg->dirs),*tok=strtok(copy,":");
-        while(tok){ char d[PATH_MAX]; expand_tilde(tok,d,sizeof d); scan_dir(apps,d); tok=strtok(NULL,":"); }
-        free(copy);
-    } else {
-        char dirs[64][PATH_MAX]; int nd=0; collect_default_dirs(dirs,&nd,64);
-        for(int i=0;i<nd;i++) scan_dir(apps,dirs[i]);
-    }
-    if(!dmenu){ apply_config_order(apps,cfg); apply_recency(apps,cfg); }
-}
-
-int main(int argc,char**argv){
-    Config cfg; config_defaults(&cfg);
-    char cfgpath[PATH_MAX];
-    const char*xc=getenv("XDG_CONFIG_HOME"),*home=getenv("HOME");
-    if(xc&&*xc) snprintf(cfgpath,sizeof cfgpath,"%s/appwheel/config",xc);
-    else snprintf(cfgpath,sizeof cfgpath,"%s/.config/appwheel/config",home?home:".");
-
-    int want_list=0, dmenu=0;
-    for(int i=1;i<argc;i++){
-        if((!strcmp(argv[i],"-c")||!strcmp(argv[i],"--config"))&&i+1<argc)
-            snprintf(cfgpath,sizeof cfgpath,"%s",argv[++i]);
-        else if(!strcmp(argv[i],"-h")||!strcmp(argv[i],"--help")){ usage(argv[0]); return 0; }
-        else if(!strcmp(argv[i],"--dump-config")){ dump_config(); return 0; }
-        else if(!strcmp(argv[i],"--list")) want_list=1;
-        else if(!strcmp(argv[i],"--dmenu")||!strcmp(argv[i],"-d")) dmenu=1;
-    }
-    { char tmp[PATH_MAX]; expand_tilde(cfgpath,tmp,sizeof tmp); snprintf(cfgpath,sizeof cfgpath,"%s",tmp); }
-    sw_shared_apply("appwheel",config_set_shared,&cfg);   /* shared first */
-    config_load(&cfg,cfgpath);                            /* our own wins  */
-    for(int i=1;i<argc;i++){
-        if(!strcmp(argv[i],"-c")||!strcmp(argv[i],"--config")){ i++; continue; }
-        if(!strcmp(argv[i],"--list")||!strcmp(argv[i],"-h")||!strcmp(argv[i],"--help")||!strcmp(argv[i],"--dump-config")||!strcmp(argv[i],"--dmenu")||!strcmp(argv[i],"-d")) continue;
-        if(!strcmp(argv[i],"--no-recent")||!strcmp(argv[i],"--all-apps")){ cfg.recent_first=0; continue; }
-        char*a=argv[i]; while(*a=='-') a++;            /* accept --key=value too */
-        char*eq=strchr(a,'='); if(eq){ *eq='\0'; config_set(&cfg,a,eq+1); }
-    }
-    if(cfg.ssaa<1)cfg.ssaa=1; if(cfg.ssaa>4)cfg.ssaa=4;
-    if(cfg.slots<1)cfg.slots=1;
-    { char tmp[PATH_MAX];
-      expand_tilde(cfg.history,tmp,sizeof tmp); snprintf(cfg.history,sizeof cfg.history,"%s",tmp);
-      if(*cfg.font){ expand_tilde(cfg.font,tmp,sizeof tmp); snprintf(cfg.font,sizeof cfg.font,"%s",tmp); } }
-
-    AppList apps={0};
-
-    if(want_list){                                 /* --list: no window needed */
-        load_apps(&apps,&cfg,dmenu);
-        for(int j=0;j<apps.n;j++)
-            printf("%-26s | %-32s | icon=%s%s\n",apps.v[j].id,apps.v[j].name,
-                   apps.v[j].icon?apps.v[j].icon:"-", apps.v[j].terminal?"  [term]":"");
-        return 0;
-    }
-
-    /* Identify as AppWheel so the compositor shows a proper name, and set the
-       Wayland app_id / X11 WM_CLASS so float/center rules match. Before init. */
-    SDL_SetAppMetadata("AppWheel","1.0","org.appwheel.AppWheel");
-    SDL_SetHint(SDL_HINT_APP_ID,"appwheel");
-
-    if(!SDL_Init(SDL_INIT_VIDEO)){ fprintf(stderr,"SDL_Init: %s\n",SDL_GetError()); return 1; }
-
-    /* "Fullscreen" here means a borderless window the size of the display — a
-       floating overlay, NOT exclusive fullscreen. Exclusive fullscreen makes the
-       surface opaque (black instead of transparent) and causes a mode-switch
-       glitch on close, so we avoid it. */
-    int winw=cfg.width, winh=cfg.height;
-    SDL_Rect dbounds; int have_bounds=0;
-    if(cfg.fullscreen){
-        SDL_DisplayID d=SDL_GetPrimaryDisplay();
-        if(d && SDL_GetDisplayBounds(d,&dbounds)){ winw=dbounds.w; winh=dbounds.h; have_bounds=1; }
-    }
-    SDL_WindowFlags wf=SDL_WINDOW_BORDERLESS|SDL_WINDOW_ALWAYS_ON_TOP;
-    if(cfg.bg.a<255) wf|=SDL_WINDOW_TRANSPARENT;
-    SDL_Window*win=SDL_CreateWindow("AppWheel",winw,winh,wf);
-    if(!win){ fprintf(stderr,"CreateWindow: %s\n",SDL_GetError()); return 1; }
-    if(have_bounds) SDL_SetWindowPosition(win,dbounds.x,dbounds.y);
-    SDL_Renderer*ren=SDL_CreateRenderer(win,NULL);
-    if(!ren){ fprintf(stderr,"CreateRenderer: %s\n",SDL_GetError()); return 1; }
-    SDL_SetRenderDrawBlendMode(ren,SDL_BLENDMODE_BLEND);
-    SDL_RaiseWindow(win);
-    SDL_StartTextInput(win);
-
-    /* Get the window mapped & focused NOW, before the (possibly slow, on a busy
-       machine) .desktop scan and font load — so keystrokes typed during loading
-       are queued for us by the compositor instead of being lost. */
-    SDL_SetRenderDrawColor(ren,cfg.bg.r,cfg.bg.g,cfg.bg.b,cfg.bg.a);
-    SDL_RenderClear(ren); SDL_RenderPresent(ren);
-    for(int i=0;i<4;i++){ SDL_PumpEvents(); SDL_Delay(1); }
-
-    load_apps(&apps,&cfg,dmenu);                    /* slow part; keystrokes queue meanwhile */
-
-    if(apps.n==0){ fprintf(stderr,"appwheel: no %s\n", dmenu?"input on stdin":".desktop applications found");
-                   SDL_DestroyRenderer(ren); SDL_DestroyWindow(win); SDL_Quit(); return 1; }
-
-    Font font; memset(&font,0,sizeof font);
-    char fontpath[PATH_MAX]={0};
-    if(*cfg.font){                                   /* explicit: a path OR a family name */
-        if(file_exists(cfg.font)) snprintf(fontpath,sizeof fontpath,"%s",cfg.font);
-        else fc_match(cfg.font,fontpath,sizeof fontpath);
-    }
-    if(!*fontpath) fc_match("sans-serif",fontpath,sizeof fontpath);  /* the desktop default */
-    if(!*fontpath) try_font_paths(fontpath,sizeof fontpath);         /* last-resort known paths */
-    int atlas_px=(int)(cfg.font_px*cfg.ui_scale); if(atlas_px<10)atlas_px=10; if(atlas_px>200)atlas_px=200;
-    if(*fontpath) font_load(&font,ren,fontpath,atlas_px);
-    if(!font.ok) fprintf(stderr,"appwheel: no usable TTF font found; using built-in font\n");
-
-    int *filt=malloc(apps.n*sizeof*filt); int fn=0; char query[256]={0};
-    int off=0, selslot=0;                 /* fixed highlight = off+selslot     */
-    Uint64 anim0=0;                       /* time of last filter change (animation) */
-    /* Per-slot crossfade bookkeeping: last_* is the layout drawn last frame;
-       snap_* is frozen at the moment of a results change. A slot only fades if
-       its app+position differ from the snapshot, so anything that stays put
-       (e.g. the top icon while you refine a query) is not re-animated. */
-    #define SNAPMAX 256
-    int   snap_item[SNAPMAX], last_item[SNAPMAX]; float snap_ang[SNAPMAX], last_ang[SNAPMAX];
-    int   snap_n=0, last_n=0, snap_sel=-1, last_sel=-1;
-    #define REBUILD() do{ \
-        memcpy(snap_item,last_item,sizeof(int)*last_n); memcpy(snap_ang,last_ang,sizeof(float)*last_n); \
-        snap_n=last_n; snap_sel=last_sel; \
-        fn=build_filter(&apps,query,filt,cfg.slots,cfg.recent_first,&off,&selslot); anim0=SDL_GetTicks(); }while(0)
-    REBUILD();
-
-    int page_dir=0; float page_speed=0;   /* for the paging indicators         */
-    float mx=cfg.width/2.0f,my=cfg.height/2.0f;
-    Uint64 last_page=0, blink0=SDL_GetTicks();
-    int running=1, had_focus=0, page_armed=0, prev_region=0, chose=0;
-    #define CHOOSE() do{ if(dmenu){ printf("%s\n",apps.v[filt[sel]].name); fflush(stdout); chose=1; } \
-                         else launch(&apps.v[filt[sel]],&cfg); running=0; }while(0)
-    float arc=cfg.arc_deg*(float)DEG; if(arc<60*DEG)arc=60*DEG; if(arc>330*DEG)arc=330*DEG;
-
-    SDL_Texture*target=NULL; int tw=0,th=0;
-    float ui=cfg.ui_scale;
-
-    while(running){
-        int w,h; SDL_GetWindowSize(win,&w,&h);
-        int ss=cfg.ssaa;
-        float mind=(w<h?w:h);
-        float cx=w/2.0f, cy=h/2.0f + cfg.y_offset*mind;
-        float R=mind*cfg.radius, ri=R*0.46f, rc=ri*0.95f, rl=(R+ri)/2.0f;
-
-        int searching = query[0]!=0;
-        int vis = fn? (fn<cfg.slots?fn:cfg.slots) : 0;
-        if(searching && vis>=4 && (vis&1)==0) vis--;   /* odd count -> a real top slot, balanced sides */
-        int maxoff = fn>vis?fn-vis:0;
-        if(off<0)off=0;
-        if(off>maxoff)off=maxoff;
-        if(vis>0){ if(selslot<0)selslot=0; if(selslot>vis-1)selslot=vis-1; }
-        int sel = fn? off+selslot : 0;
-        if(sel>fn-1)sel=fn-1;
-
-        float step = arc/(float)(vis>0?vis:1);   /* equal sectors fill the arc */
-        float top=-(float)M_PI/2;
-        float astart=top-arc/2;
-
-        /* --- paging: hovering anywhere in the bottom zone pages; deeper toward
-           straight-down = faster (slow -> fast). Gated by page_armed so it never
-           fires from wherever the cursor happened to open on (startup safety). --- */
-        float half_arc = arc/2 + 2.0f*(float)DEG;     /* apps fill the arc; page below it */
-        page_dir=0; page_speed=0;
-        if(page_armed){
-          float dx=mx-cx,dy=my-cy,dist=sqrtf(dx*dx+dy*dy);
-          if(dist>rc){
-              float pa=atan2f(dy,dx), dtop=norm_ang(pa-top);
-              if(fabsf(dtop)>half_arc){               /* below the app arc = paging zone */
-                  page_dir = dtop>0?1:-1;             /* right=forward, left=back */
-                  float depth=fabsf(dtop)-half_arc, maxd=(float)M_PI-half_arc;
-                  page_speed = maxd>0?clampf(depth/maxd,0,1):0;
-                  if(fn>vis){
-                      Uint64 now=SDL_GetTicks();
-                      float t=page_speed*page_speed;   /* ease in: gentle start, fast finish */
-                      int iv=(int)(cfg.page_ms*(1.0f-0.82f*t)); if(iv<24)iv=24;
-                      if((Sint64)(now-last_page)>=iv){
-                          off += page_dir;
-                          if(off<0)off=0;
-                          if(off>maxoff)off=maxoff;
-                          last_page=now;
-                      }
-                  }
-              }
-          }
-        }
-
-        SDL_Event ev;
-        while(SDL_PollEvent(&ev)){
-            if(ev.type==SDL_EVENT_QUIT) running=0;
-            else if(ev.type==SDL_EVENT_WINDOW_FOCUS_GAINED) had_focus=1;
-            else if(ev.type==SDL_EVENT_WINDOW_FOCUS_LOST){ if(cfg.close_on_focus_loss && had_focus) running=0; }
-            else if(ev.type==SDL_EVENT_MOUSE_MOTION){
-                mx=ev.motion.x; my=ev.motion.y;
-                float dx=mx-cx,dy=my-cy,dist=sqrtf(dx*dx+dy*dy);
-                int region=1; float adtop=0;   /* 1=apps/center, 2=bottom off-icon, 3=on icon */
-                if(dist>rc){
-                    float pa=atan2f(dy,dx), dtop=norm_ang(pa-top);
-                    adtop=fabsf(dtop);
-                    if(adtop<=half_arc){                    /* within the app arc */
-                        if(vis>0){
-                            int slot = (int)floorf((dtop+arc/2)/(step>0?step:1));
-                            if(slot<0)slot=0; if(slot>vis-1)slot=vis-1;
-                            selslot=slot;
-                        }
-                    }
-                    else if(on_icon(mx,my,cx,cy,rl,R*0.10f)) region=3;   /* on the icon */
-                    else region=2;                                       /* bottom, off icon */
-                }
-                /* Startup safety (see the 3-state rules):
-                     - reaching the apps/center always arms (state 1)
-                     - approaching the icon from the bottom arms (state 2 -> icon)
-                     - leaving the icon DOWN/SIDEWAYS into the bottom arms; leaving
-                       it UP toward the apps does NOT, so you can pick an app
-                       without scrolling. Once armed, the whole bottom scrolls. */
-                float icon_dtop=fabsf(norm_ang(ICON_AL-top));   /* ~150 deg */
-                if(region==1) page_armed=1;
-                else if(region==3 && prev_region==2) page_armed=1;
-                else if(region==2 && prev_region==3 && adtop>=icon_dtop) page_armed=1;
-                prev_region=region;
-            }
-            else if(ev.type==SDL_EVENT_MOUSE_WHEEL){
-                int dir=ev.wheel.y>0?-1:(ev.wheel.y<0?1:0);
-                if(!dir&&ev.wheel.x)dir=ev.wheel.x>0?1:-1;
-                if(dir){
-                    if(maxoff>0){ off+=dir; if(off<0)off=0; if(off>maxoff)off=maxoff; }
-                    else if(vis>0){ selslot+=dir; if(selslot<0)selslot=0; if(selslot>vis-1)selslot=vis-1; }
-                }
-            }
-            else if(ev.type==SDL_EVENT_MOUSE_BUTTON_DOWN){
-                if(ev.button.button==SDL_BUTTON_LEFT&&fn){ CHOOSE(); }
-                else if(ev.button.button==SDL_BUTTON_RIGHT) running=0;
-            }
-            else if(ev.type==SDL_EVENT_TEXT_INPUT){
-                strncat(query,ev.text.text,sizeof query-strlen(query)-1);
-                REBUILD();
-            }
-            else if(ev.type==SDL_EVENT_KEY_DOWN){
-                SDL_Keycode k=ev.key.key;
-                if(k==SDLK_ESCAPE){ if(query[0]){query[0]='\0';REBUILD();} else running=0; }
-                else if(k==SDLK_RETURN||k==SDLK_KP_ENTER){ if(fn){ CHOOSE(); } }
-                else if(k==SDLK_BACKSPACE){ int L=strlen(query);
-                    if(L>0){ L--; while(L>0&&((unsigned char)query[L]&0xC0)==0x80)L--; query[L]='\0'; REBUILD(); } }
-                else if(k==SDLK_RIGHT || (k==SDLK_TAB && !(ev.key.mod&SDL_KMOD_SHIFT))){
-                    if(vis>0){ if(selslot<vis-1)selslot++; else if(off<maxoff)off++; } }
-                else if(k==SDLK_LEFT || (k==SDLK_TAB && (ev.key.mod&SDL_KMOD_SHIFT))){
-                    if(vis>0){ if(selslot>0)selslot--; else if(off>0)off--; } }
-                else if(k==SDLK_DOWN){ if(maxoff>0){ off+=vis; if(off>maxoff)off=maxoff; } }
-                else if(k==SDLK_UP){ if(maxoff>0){ off-=vis; if(off<0)off=0; } }
-            }
-        }
-        if(!running) break;
-
-        /* Input above may have changed fn/off/selslot (typing, backspace, paging),
-           so recompute the whole layout before rendering — otherwise this frame
-           would draw with the previous match count and the highlight flickers. */
-        searching = query[0]!=0;
-        vis = fn? (fn<cfg.slots?fn:cfg.slots) : 0;
-        if(searching && vis>=4 && (vis&1)==0) vis--;
-        maxoff = fn>vis?fn-vis:0;
-        if(off<0)off=0;
-        if(off>maxoff)off=maxoff;
-        if(vis>0){ if(selslot<0)selslot=0; if(selslot>vis-1)selslot=vis-1; }
-        sel = fn? off+selslot : 0;
-        if(sel>fn-1)sel=fn-1;
-        step = arc/(float)(vis>0?vis:1);
-
-        /* -------------------- render (optionally supersampled) ------------- */
-        if(ss>1){
-            if(!target||tw!=w*ss||th!=h*ss){
-                if(target)SDL_DestroyTexture(target);
-                target=SDL_CreateTexture(ren,SDL_PIXELFORMAT_RGBA8888,SDL_TEXTUREACCESS_TARGET,w*ss,h*ss);
-                if(target){ SDL_SetTextureScaleMode(target,SDL_SCALEMODE_LINEAR);
-                            SDL_SetTextureBlendMode(target,SDL_BLENDMODE_BLEND); tw=w*ss; th=h*ss; }
-            }
-            if(target){ SDL_SetRenderTarget(ren,target); SDL_SetRenderScale(ren,(float)ss,(float)ss); }
-            else ss=1;
-        }
-
-        SDL_SetRenderDrawColor(ren,cfg.bg.r,cfg.bg.g,cfg.bg.b,cfg.bg.a);
-        SDL_RenderClear(ren);
-
-        float slotw=step;
-        float label_px=cfg.label_px*ui;
-        /* app-data crossfade: on a results change, the names/icons/highlight text
-           fade back in while the ring sectors (background) stay put — smooths fast
-           typing without delaying anything. */
-        float af = (cfg.animate && cfg.anim_ms>0)
-                   ? clampf(0.30f + 0.70f*(float)(SDL_GetTicks()-anim0)/(float)cfg.anim_ms, 0.0f, 1.0f) : 1.0f;
-        for(int i=0;i<vis;i++){
-            int item=filt[off+i];
-            float a = astart+(i+0.5f)*step;
-            float a0=a-slotw/2+0.010f, a1=a+slotw/2-0.010f;
-            int hot=(off+i==sel);
-            Col scol=hot?cfg.hl:(i&1?cfg.ring2:cfg.ring);
-            fill_sector(ren,cx,cy,ri,R,a0,a1,scol);            /* sector stays solid */
-
-            /* stationary if this exact app sat at (nearly) this angle before the change */
-            int stationary=0;
-            for(int k=0;k<snap_n;k++) if(snap_item[k]==item && fabsf(snap_ang[k]-a)<0.02f){ stationary=1; break; }
-            float sfa = stationary?1.0f:af; Uint8 sfab=(Uint8)(255*sfa);
-            if(i<SNAPMAX){ last_item[i]=item; last_ang[i]=a; }
-
-            float lx=cx+rl*cosf(a), ly=cy+rl*sinf(a);
-            Col tc=hot?cfg.hltext:cfg.text; tc.a=(Uint8)(tc.a*sfa);   /* text fades (unless it held still) */
-            float chord=2*rl*sinf(slotw/2)*0.84f;
-            char lbl[160]; fit_label(&font,label_px,apps.v[item].name,chord,lbl,sizeof lbl);
-
-            SDL_Texture*ic=app_icon(ren,&apps.v[item],&cfg);
-            if(ic){
-                float isz=cfg.icon_px*ui; if(isz>chord)isz=chord;
-                float gap=7.0f*ui;                       /* breathing room */
-                float blockH=isz+gap+label_px;
-                float topy=ly-blockH/2;
-                SDL_FRect dst={lx-isz/2, topy, isz, isz};
-                SDL_SetTextureAlphaMod(ic,sfab);         /* icon fades (unless it held still) */
-                SDL_RenderTexture(ren,ic,NULL,&dst);
-                text_centered(ren,&font,lx, topy+isz+gap+label_px/2, label_px, tc, lbl);
-            } else {
-                text_centered(ren,&font,lx,ly,label_px,tc,lbl);
-            }
-        }
-        last_n = vis<SNAPMAX?vis:SNAPMAX;
-
-        /* paging indicators (chevrons) — brighten & multiply with speed */
-        if(fn>vis){
-            float aL=ICON_AL, aR=ICON_AR;   /* bottom-left / -right paging icons */
-            float lxp=cx+rl*cosf(aL), lyp=cy+rl*sinf(aL);
-            float rxp=cx+rl*cosf(aR), ryp=cy+rl*sinf(aR);
-            /* left */
-            { int on=(page_dir<0); float sp=on?page_speed:0;
-              Col c=on?cfg.accent:cfg.dim; c.a=on?255:210;
-              int cnt=1+(on?(int)lroundf(sp*2):0);
-              float s=(19.0f+ (on?13.0f*sp:0))*ui;
-              draw_chevrons(ren,lxp,lyp,-1,s,cnt,c); }
-            /* right */
-            { int on=(page_dir>0); float sp=on?page_speed:0;
-              Col c=on?cfg.accent:cfg.dim; c.a=on?255:210;
-              int cnt=1+(on?(int)lroundf(sp*2):0);
-              float s=(19.0f+ (on?13.0f*sp:0))*ui;
-              draw_chevrons(ren,rxp,ryp,+1,s,cnt,c); }
-        }
-
-        /* center hub */
-        fill_circle(ren,cx,cy,rc,cfg.center);
-        Uint64 now=SDL_GetTicks(); int caret=((now-blink0)/500)%2==0;
-        char shown[300];
-        if(query[0]) snprintf(shown,sizeof shown,"%s%s",query,caret?"|":" ");
-        else         snprintf(shown,sizeof shown,"type to search");
-        Col qc=query[0]?cfg.accent:cfg.dim;
-        text_centered(ren,&font,cx,cy-rc*0.42f,cfg.search_px*ui,qc,shown);
-
-        if(fn){
-            int sel_item=filt[sel]; last_sel=sel_item;
-            float caf = (sel_item==snap_sel)?1.0f:af;      /* same app in the hub -> no fade */
-            char nm[200]; fit_label(&font,cfg.title_px*ui,apps.v[sel_item].name,rc*1.7f,nm,sizeof nm);
-            Col nmc=cfg.text; nmc.a=(Uint8)(nmc.a*caf);
-            text_centered(ren,&font,cx,cy,cfg.title_px*ui,nmc,nm);
-            char cnt[64]; snprintf(cnt,sizeof cnt,"%d / %d",sel+1,fn);
-            Col cc=cfg.dim; cc.a=(Uint8)(cc.a*caf);
-            text_centered(ren,&font,cx,cy+rc*0.40f,cfg.count_px*ui,cc,cnt);
-        } else {
-            last_sel=-1;
-            text_centered(ren,&font,cx,cy,cfg.title_px*ui,cfg.dim,"no matches");
-        }
-
-        if(ss>1 && target){
-            SDL_SetRenderScale(ren,1,1);
-            SDL_SetRenderTarget(ren,NULL);
-            SDL_SetRenderDrawColor(ren,0,0,0,0);
-            SDL_RenderClear(ren);
-            SDL_RenderTexture(ren,target,NULL,NULL);     /* fade is per-element now */
-        }
-        SDL_RenderPresent(ren);
-        SDL_Delay(16);
-    }
-
-    if(target)SDL_DestroyTexture(target);
-    SDL_StopTextInput(win);
-    SDL_DestroyRenderer(ren);
-    SDL_DestroyWindow(win);
-    SDL_Quit();
-    return (dmenu && !chose) ? 1 : 0;   /* dmenu convention: non-zero on cancel */
-}
diff --git a/config.example b/config.example
index 37583ea..b5f0605 100644
--- a/config.example
+++ b/config.example
@@ -44,6 +44,19 @@ outputs_map=1      # with a second screen: a map of the monitors, bottom left,
                    # workspaces. It keeps that place to itself, so it is
                    # always in the same spot
 outputs_map_w=0.18 # how wide, as a share of the tile area
+launcher=swas --replace overview=1
+                   # what `d` opens. swas draws its own backdrop, so swov
+                   # steps aside rather than stacking two overlays
+over_fullscreen=1  # a fullscreen window is above anything a normal window
+                   # can reach, so swov takes it off fullscreen while it is
+                   # open and puts it back on the way out. 0 = open behind it
+drop_outputs=1     # while dragging, the other screens' workspaces show up
+                   # alongside this one's, marked with their output name, so
+                   # they can be dropped on directly
+map_dwell_ms=0     # dragging anything: hold it over another monitor
+                   # this long and the overview steps onto that screen. The
+                   # plate fills as you hold; moving at all starts the wait
+                   # again. 0 = off
 # output=DP-1      # start on that screen instead of the one sway is on
 blur=2             # --backdrop only: how soft the overview is drawn while
                    # something else has the foreground. 0 = off, 3 = softest.
diff --git a/swbr.c b/swbr.c
deleted file mode 100644
index d440894..0000000
--- a/swbr.c
+++ /dev/null
@@ -1,3887 +0,0 @@
-/* SwBr (swbr) — a floating layer-shell bar for Sway
- *
- * Sits on top of the tiled windows at the screen edge, does not steal space
- * from them. Hover it and press space to fold it into a thin signal strip.
- *
- * Dependencies:
- *   - libwayland-client   (the only library you link)
- *   - stb_truetype.h      (vendored single-header — smooth fonts)
- * The wlr-layer-shell protocol glue is vendored below, so there is no
- * wayland-scanner step and nothing else to install.
- *
- * Build:
- *   cc -std=c11 -O2 -Wall -Wextra -o swbr swbr.c -lwayland-client -lm
- *
- * Runtime deps: a running Sway session ($SWAYSOCK). No jq, no shell-outs:
- * the program talks to the Sway IPC socket directly. fontconfig (fc-match)
- * is used if present to pick a font, otherwise known paths are tried.
- *
- * See --help for every option (all config keys also work on the command line).
- */
-
-/* ---------------------------------------------------------------------------
- * How this file is laid out (top to bottom):
- *   1. small helpers        colors, string trim, hex-color + tilde parsing
- *   2. layer-shell glue     vendored wlr-layer-shell-unstable-v1 marshalling
- *   3. JSON                 the tiny parser used for sway's replies
- *   4. sway ipc             request/subscribe socket, workspaces, mode
- *   5. Config               the struct, defaults, key=value setter, file loader
- *   6. text (Font)          stb_truetype glyph cache, software blitting
- *   7. paint                ARGB32 buffer, rounded rectangles, clipping
- *   8. markup               the pango subset (<span foreground=..>, <b>, &amp;)
- *   9. status               the status_command child process and its lines
- *  10. wayland              globals, one Bar per output, shm, pointer, keys
- *  11. render               the expanded bar and the collapsed signal strip
- *  12. usage()/dump_config  --help text and the default config generator
- *  13. main()               setup, then the poll loop
- * ------------------------------------------------------------------------- */
-
-#define _POSIX_C_SOURCE 200809L
-#define _GNU_SOURCE
-
-#include <wayland-client.h>
-
-#define STB_TRUETYPE_IMPLEMENTATION
-#include "stb_truetype.h"
-
-#include <ctype.h>
-#include <errno.h>
-#include <fcntl.h>
-#include <limits.h>
-#include <math.h>
-#include <poll.h>
-#include <signal.h>
-#include <stdarg.h>
-#include <stdbool.h>
-#include <stdint.h>
-#include <stdio.h>
-#include <stdlib.h>
-#include <string.h>
-#include <strings.h>
-#include <sys/mman.h>
-#include <sys/socket.h>
-#include <sys/stat.h>
-#include <sys/types.h>
-#include <sys/un.h>
-#include <sys/wait.h>
-#include <time.h>
-#include <unistd.h>
-
-#include "sw_theme.h"
-
-#define APP_ID          "swbr"
-#define SWBR_VERSION  "0.9.0"
-#ifndef SWBR_BUILD                 /* set by the Makefile: md5 of this file */
-#define SWBR_BUILD "unknown"
-#endif
-#define MAX_WORKSPACES  64
-#define MAX_OUTPUTS     8
-#define MAX_RUNS        128
-#define MAX_BINDS       10
-#define MAX_CELLS       24
-
-enum { SLIM_AUTO, SLIM_TICK, SLIM_BAR, SLIM_CLOCK, SLIM_PRESENCE, SLIM_OFF };
-
-/* ------------------------------------------------------------------ util */
-
-static void die(const char *fmt, ...)
-{
-    va_list ap;
-    va_start(ap, fmt);
-    fputs("swbr: ", stderr);
-    vfprintf(stderr, fmt, ap);
-    va_end(ap);
-    fputc('\n', stderr);
-    exit(1);
-}
-
-static void *xmalloc(size_t n)
-{
-    void *p = malloc(n ? n : 1);
-    if (!p) die("out of memory");
-    return p;
-}
-
-static void *xrealloc(void *p, size_t n)
-{
-    void *q = realloc(p, n ? n : 1);
-    if (!q) die("out of memory");
-    return q;
-}
-
-static void str_set(char *dst, size_t cap, const char *src)
-{
-    if (!cap) return;
-    size_t n = strlen(src);
-    if (n >= cap) n = cap - 1;
-    memcpy(dst, src, n);
-    dst[n] = 0;
-}
-
-static char *trim(char *s)
-{
-    while (*s && isspace((unsigned char)*s)) s++;
-    if (!*s) return s;
-    char *e = s + strlen(s) - 1;
-    while (e > s && isspace((unsigned char)*e)) *e-- = 0;
-    return s;
-}
-
-static bool file_exists(const char *p)
-{
-    struct stat st;
-    return stat(p, &st) == 0 && S_ISREG(st.st_mode);
-}
-
-static float clampf(float v, float lo, float hi) { return v < lo ? lo : (v > hi ? hi : v); }
-static int   clampi(int v, int lo, int hi)       { return v < lo ? lo : (v > hi ? hi : v); }
-
-static uint32_t now_ms(void)
-{
-    struct timespec ts;
-    clock_gettime(CLOCK_MONOTONIC, &ts);
-    return (uint32_t)(ts.tv_sec * 1000u + ts.tv_nsec / 1000000u);
-}
-
-typedef struct { uint8_t r, g, b, a; } Col;
-
-static void parse_color(const char *s, Col *out)
-{
-    if (*s == '#') s++;
-    unsigned r, g, b, a = 255;
-    if (strlen(s) >= 8 && sscanf(s, "%2x%2x%2x%2x", &r, &g, &b, &a) == 4) { }
-    else if (sscanf(s, "%2x%2x%2x", &r, &g, &b) == 3) { }
-    else return;
-    out->r = (uint8_t)r; out->g = (uint8_t)g; out->b = (uint8_t)b; out->a = (uint8_t)a;
-}
-
-/* expand a leading ~/ to $HOME (no shell involved) */
-static void expand_tilde(const char *in, char *out, size_t n)
-{
-    if (in[0] == '~' && (in[1] == '/' || in[1] == 0)) {
-        const char *home = getenv("HOME");
-        if (home) { snprintf(out, n, "%s%s", home, in + 1); return; }
-    }
-    snprintf(out, n, "%s", in);
-}
-
-/* ------------------------------------------------- wlr-layer-shell glue */
-/* Vendored equivalent of what wayland-scanner emits for
- * wlr-layer-shell-unstable-v1.xml (Copyright (c) 2017 Drew DeVault, MIT-ish;
- * see the protocol file for the full notice). Only the requests and events
- * swbr actually uses are wrapped; get_popup is present in the table so the
- * opcodes line up, but is never called, hence its NULL type entry. */
-
-enum zwlr_layer_shell_v1_layer {
-    ZWLR_LAYER_SHELL_V1_LAYER_BACKGROUND = 0,
-    ZWLR_LAYER_SHELL_V1_LAYER_BOTTOM     = 1,
-    ZWLR_LAYER_SHELL_V1_LAYER_TOP        = 2,
-    ZWLR_LAYER_SHELL_V1_LAYER_OVERLAY    = 3
-};
-enum zwlr_layer_surface_v1_anchor {
-    ZWLR_LAYER_SURFACE_V1_ANCHOR_TOP    = 1,
-    ZWLR_LAYER_SURFACE_V1_ANCHOR_BOTTOM = 2,
-    ZWLR_LAYER_SURFACE_V1_ANCHOR_LEFT   = 4,
-    ZWLR_LAYER_SURFACE_V1_ANCHOR_RIGHT  = 8
-};
-enum zwlr_layer_surface_v1_keyboard_interactivity {
-    ZWLR_LAYER_SURFACE_V1_KEYBOARD_INTERACTIVITY_NONE      = 0,
-    ZWLR_LAYER_SURFACE_V1_KEYBOARD_INTERACTIVITY_EXCLUSIVE = 1,
-    ZWLR_LAYER_SURFACE_V1_KEYBOARD_INTERACTIVITY_ON_DEMAND = 2
-};
-
-struct zwlr_layer_shell_v1;
-struct zwlr_layer_surface_v1;
-
-extern const struct wl_interface zwlr_layer_surface_v1_interface;
-
-static const struct wl_interface *swbr_ls_types[] = {
-    NULL, NULL, NULL, NULL,
-    &zwlr_layer_surface_v1_interface,
-    &wl_surface_interface,
-    &wl_output_interface,
-    NULL, NULL, NULL,
-};
-
-static const struct wl_message zwlr_layer_shell_v1_requests[] = {
-    { "get_layer_surface", "no?ous", swbr_ls_types + 4 },
-    { "destroy", "3", swbr_ls_types + 0 },
-};
-static const struct wl_interface zwlr_layer_shell_v1_interface = {
-    "zwlr_layer_shell_v1", 4, 2, zwlr_layer_shell_v1_requests, 0, NULL,
-};
-
-static const struct wl_message zwlr_layer_surface_v1_requests[] = {
-    { "set_size", "uu", swbr_ls_types + 0 },
-    { "set_anchor", "u", swbr_ls_types + 0 },
-    { "set_exclusive_zone", "i", swbr_ls_types + 0 },
-    { "set_margin", "iiii", swbr_ls_types + 0 },
-    { "set_keyboard_interactivity", "u", swbr_ls_types + 0 },
-    { "get_popup", "o", swbr_ls_types + 9 },
-    { "ack_configure", "u", swbr_ls_types + 0 },
-    { "destroy", "", swbr_ls_types + 0 },
-    { "set_layer", "2u", swbr_ls_types + 0 },
-};
-static const struct wl_message zwlr_layer_surface_v1_events[] = {
-    { "configure", "uuu", swbr_ls_types + 0 },
-    { "closed", "", swbr_ls_types + 0 },
-};
-const struct wl_interface zwlr_layer_surface_v1_interface = {
-    "zwlr_layer_surface_v1", 4, 9, zwlr_layer_surface_v1_requests,
-    2, zwlr_layer_surface_v1_events,
-};
-
-struct zwlr_layer_surface_v1_listener {
-    void (*configure)(void *data, struct zwlr_layer_surface_v1 *s,
-                      uint32_t serial, uint32_t width, uint32_t height);
-    void (*closed)(void *data, struct zwlr_layer_surface_v1 *s);
-};
-
-#define LS_REQ_SET_SIZE        0
-#define LS_REQ_SET_ANCHOR      1
-#define LS_REQ_SET_EXCLUSIVE   2
-#define LS_REQ_SET_MARGIN      3
-#define LS_REQ_SET_KBD         4
-#define LS_REQ_ACK_CONFIGURE   6
-#define LS_REQ_DESTROY         7
-
-static struct zwlr_layer_surface_v1 *
-zwlr_layer_shell_v1_get_layer_surface(struct zwlr_layer_shell_v1 *shell,
-                                      struct wl_surface *surface,
-                                      struct wl_output *output,
-                                      uint32_t layer, const char *ns)
-{
-    struct wl_proxy *id = wl_proxy_marshal_flags(
-        (struct wl_proxy *)shell, 0, &zwlr_layer_surface_v1_interface,
-        wl_proxy_get_version((struct wl_proxy *)shell), 0,
-        NULL, surface, output, layer, ns);
-    return (struct zwlr_layer_surface_v1 *)id;
-}
-
-static int zwlr_layer_surface_v1_add_listener(struct zwlr_layer_surface_v1 *s,
-                                              const struct zwlr_layer_surface_v1_listener *l,
-                                              void *data)
-{
-    return wl_proxy_add_listener((struct wl_proxy *)s, (void (**)(void))l, data);
-}
-
-static void ls_set_size(struct zwlr_layer_surface_v1 *s, uint32_t w, uint32_t h)
-{ wl_proxy_marshal_flags((struct wl_proxy *)s, LS_REQ_SET_SIZE, NULL,
-                         wl_proxy_get_version((struct wl_proxy *)s), 0, w, h); }
-static void ls_set_anchor(struct zwlr_layer_surface_v1 *s, uint32_t a)
-{ wl_proxy_marshal_flags((struct wl_proxy *)s, LS_REQ_SET_ANCHOR, NULL,
-                         wl_proxy_get_version((struct wl_proxy *)s), 0, a); }
-static void ls_set_exclusive_zone(struct zwlr_layer_surface_v1 *s, int32_t z)
-{ wl_proxy_marshal_flags((struct wl_proxy *)s, LS_REQ_SET_EXCLUSIVE, NULL,
-                         wl_proxy_get_version((struct wl_proxy *)s), 0, z); }
-static void ls_set_margin(struct zwlr_layer_surface_v1 *s, int32_t t, int32_t r,
-                          int32_t b, int32_t l)
-{ wl_proxy_marshal_flags((struct wl_proxy *)s, LS_REQ_SET_MARGIN, NULL,
-                         wl_proxy_get_version((struct wl_proxy *)s), 0, t, r, b, l); }
-static void ls_set_keyboard(struct zwlr_layer_surface_v1 *s, uint32_t k)
-{ wl_proxy_marshal_flags((struct wl_proxy *)s, LS_REQ_SET_KBD, NULL,
-                         wl_proxy_get_version((struct wl_proxy *)s), 0, k); }
-static void ls_ack_configure(struct zwlr_layer_surface_v1 *s, uint32_t serial)
-{ wl_proxy_marshal_flags((struct wl_proxy *)s, LS_REQ_ACK_CONFIGURE, NULL,
-                         wl_proxy_get_version((struct wl_proxy *)s), 0, serial); }
-static void ls_destroy(struct zwlr_layer_surface_v1 *s)
-{ wl_proxy_marshal_flags((struct wl_proxy *)s, LS_REQ_DESTROY, NULL,
-                         wl_proxy_get_version((struct wl_proxy *)s),
-                         WL_MARSHAL_FLAG_DESTROY); }
-
-/* ------------------------------------------------------------------ json */
-
-typedef enum { J_NULL, J_BOOL, J_NUM, J_STR, J_ARR, J_OBJ } JType;
-
-typedef struct JV {
-    JType type;
-    bool b;
-    double num;
-    char *str;
-    struct JV **items;
-    char **keys;
-    int count, cap;
-} JV;
-
-static JV *jnew(JType t)
-{
-    JV *v = (JV *)xmalloc(sizeof(JV));
-    memset(v, 0, sizeof(*v));
-    v->type = t;
-    return v;
-}
-
-static void jfree(JV *v)
-{
-    if (!v) return;
-    for (int i = 0; i < v->count; ++i) {
-        if (v->keys) free(v->keys[i]);
-        jfree(v->items[i]);
-    }
-    free(v->items);
-    free(v->keys);
-    free(v->str);
-    free(v);
-}
-
-static void jpush(JV *v, char *key, JV *child)
-{
-    if (v->count == v->cap) {
-        v->cap = v->cap ? v->cap * 2 : 8;
-        v->items = (JV **)xrealloc(v->items, (size_t)v->cap * sizeof(JV *));
-        v->keys  = (char **)xrealloc(v->keys, (size_t)v->cap * sizeof(char *));
-    }
-    v->keys[v->count] = key;
-    v->items[v->count++] = child;
-}
-
-static const char *jskip_ws(const char *p)
-{
-    while (*p && (unsigned char)*p <= ' ') p++;
-    return p;
-}
-
-static void utf8_append(char **s, int *len, int *cap, unsigned cp)
-{
-    char tmp[4];
-    int n = 0;
-    if (cp < 0x80) { tmp[n++] = (char)cp; }
-    else if (cp < 0x800) { tmp[n++] = (char)(0xC0 | (cp >> 6)); tmp[n++] = (char)(0x80 | (cp & 0x3F)); }
-    else if (cp < 0x10000) {
-        tmp[n++] = (char)(0xE0 | (cp >> 12));
-        tmp[n++] = (char)(0x80 | ((cp >> 6) & 0x3F));
-        tmp[n++] = (char)(0x80 | (cp & 0x3F));
-    } else {
-        tmp[n++] = (char)(0xF0 | (cp >> 18));
-        tmp[n++] = (char)(0x80 | ((cp >> 12) & 0x3F));
-        tmp[n++] = (char)(0x80 | ((cp >> 6) & 0x3F));
-        tmp[n++] = (char)(0x80 | (cp & 0x3F));
-    }
-    while (*len + n + 1 > *cap) { *cap *= 2; *s = (char *)xrealloc(*s, (size_t)*cap); }
-    memcpy(*s + *len, tmp, (size_t)n);
-    *len += n;
-    (*s)[*len] = 0;
-}
-
-static const char *jparse_string(const char *p, char **out)
-{
-    p++;                                  /* opening quote */
-    int cap = 32, len = 0;
-    char *s = (char *)xmalloc((size_t)cap);
-    s[0] = 0;
-    while (*p && *p != '"') {
-        if (*p == '\\') {
-            p++;
-            unsigned cp;
-            switch (*p++) {
-            case 'n': cp = '\n'; break;
-            case 't': cp = '\t'; break;
-            case 'r': cp = '\r'; break;
-            case 'b': cp = '\b'; break;
-            case 'f': cp = '\f'; break;
-            case '/': cp = '/';  break;
-            case '\\': cp = '\\'; break;
-            case '"': cp = '"';  break;
-            case 'u': {
-                unsigned v = 0;
-                for (int i = 0; i < 4 && isxdigit((unsigned char)*p); ++i) {
-                    char c = *p++;
-                    v = v * 16 + (unsigned)(isdigit((unsigned char)c) ? c - '0'
-                                            : (tolower(c) - 'a' + 10));
-                }
-                cp = v;
-                if (cp >= 0xD800 && cp <= 0xDBFF && p[0] == '\\' && p[1] == 'u') {
-                    p += 2;
-                    unsigned lo = 0;
-                    for (int i = 0; i < 4 && isxdigit((unsigned char)*p); ++i) {
-                        char c = *p++;
-                        lo = lo * 16 + (unsigned)(isdigit((unsigned char)c) ? c - '0'
-                                                  : (tolower(c) - 'a' + 10));
-                    }
-                    cp = 0x10000 + ((cp - 0xD800) << 10) + (lo - 0xDC00);
-                }
-                break;
-            }
-            default:
-                if (!p[-1]) { free(s); *out = NULL; return NULL; }
-                cp = (unsigned char)p[-1];
-                break;
-            }
-            utf8_append(&s, &len, &cap, cp);
-        } else {
-            if (len + 2 > cap) { cap *= 2; s = (char *)xrealloc(s, (size_t)cap); }
-            s[len++] = *p++;
-            s[len] = 0;
-        }
-    }
-    if (*p != '"') { free(s); *out = NULL; return NULL; }
-    *out = s;
-    return p + 1;
-}
-
-static const char *jparse_value(const char *p, JV **out);
-
-static const char *jparse_container(const char *p, JV **out, bool is_obj)
-{
-    JV *v = jnew(is_obj ? J_OBJ : J_ARR);
-    p = jskip_ws(p + 1);
-    char close = is_obj ? '}' : ']';
-    if (*p == close) { *out = v; return p + 1; }
-
-    for (;;) {
-        char *key = NULL;
-        if (is_obj) {
-            p = jskip_ws(p);
-            if (*p != '"') { jfree(v); return NULL; }
-            p = jparse_string(p, &key);
-            if (!p) { jfree(v); return NULL; }
-            p = jskip_ws(p);
-            if (*p != ':') { free(key); jfree(v); return NULL; }
-            p++;
-        }
-        JV *child = NULL;
-        p = jparse_value(jskip_ws(p), &child);
-        if (!p) { free(key); jfree(v); return NULL; }
-        jpush(v, key, child);
-
-        p = jskip_ws(p);
-        if (*p == ',') { p++; continue; }
-        if (*p == close) { p++; break; }
-        jfree(v);
-        return NULL;
-    }
-    *out = v;
-    return p;
-}
-
-static const char *jparse_value(const char *p, JV **out)
-{
-    p = jskip_ws(p);
-    switch (*p) {
-    case '{': return jparse_container(p, out, true);
-    case '[': return jparse_container(p, out, false);
-    case '"': {
-        JV *v = jnew(J_STR);
-        p = jparse_string(p, &v->str);
-        if (!p) { jfree(v); return NULL; }
-        *out = v;
-        return p;
-    }
-    case 't':
-        if (strncmp(p, "true", 4)) return NULL;
-        *out = jnew(J_BOOL); (*out)->b = true; return p + 4;
-    case 'f':
-        if (strncmp(p, "false", 5)) return NULL;
-        *out = jnew(J_BOOL); (*out)->b = false; return p + 5;
-    case 'n':
-        if (strncmp(p, "null", 4)) return NULL;
-        *out = jnew(J_NULL); return p + 4;
-    default: {
-        char *end = NULL;
-        double d = strtod(p, &end);
-        if (end == p) return NULL;
-        JV *v = jnew(J_NUM);
-        v->num = d;
-        *out = v;
-        return end;
-    }
-    }
-}
-
-static JV *jparse(const char *text)
-{
-    JV *v = NULL;
-    const char *p = jparse_value(text, &v);
-    if (!p) { jfree(v); return NULL; }
-    return v;
-}
-
-static JV *jget(const JV *o, const char *key)
-{
-    if (!o || o->type != J_OBJ) return NULL;
-    for (int i = 0; i < o->count; ++i)
-        if (o->keys[i] && strcmp(o->keys[i], key) == 0) return o->items[i];
-    return NULL;
-}
-
-static const char *jstr(const JV *o, const char *key, const char *def)
-{
-    JV *v = jget(o, key);
-    return (v && v->type == J_STR) ? v->str : def;
-}
-
-static int jint(const JV *o, const char *key, int def)
-{
-    JV *v = jget(o, key);
-    return (v && v->type == J_NUM) ? (int)v->num : def;
-}
-
-static bool jbool(const JV *o, const char *key, bool def)
-{
-    JV *v = jget(o, key);
-    return (v && v->type == J_BOOL) ? v->b : def;
-}
-
-/* -------------------------------------------------------------- sway ipc */
-
-enum {
-    IPC_RUN_COMMAND    = 0,
-    IPC_GET_WORKSPACES = 1,
-    IPC_SUBSCRIBE      = 2,
-    IPC_GET_OUTPUTS    = 3
-};
-
-static int sway_fd = -1;      /* request socket */
-static int sway_evt_fd = -1;  /* event socket   */
-
-static int sway_connect(void)
-{
-    const char *path = getenv("SWAYSOCK");
-    if (!path || !*path) path = getenv("I3SOCK");
-    if (!path || !*path) return -1;
-
-    int fd = socket(AF_UNIX, SOCK_STREAM, 0);
-    if (fd < 0) return -1;
-
-    struct sockaddr_un addr;
-    memset(&addr, 0, sizeof(addr));
-    addr.sun_family = AF_UNIX;
-    str_set(addr.sun_path, sizeof(addr.sun_path), path);
-    if (connect(fd, (struct sockaddr *)&addr, sizeof(addr)) < 0) {
-        close(fd);
-        return -1;
-    }
-    return fd;
-}
-
-static bool write_all(int fd, const void *buf, size_t n)
-{
-    const char *p = (const char *)buf;
-    while (n) {
-        ssize_t w = write(fd, p, n);
-        if (w < 0) { if (errno == EINTR) continue; return false; }
-        p += w;
-        n -= (size_t)w;
-    }
-    return true;
-}
-
-static bool read_all(int fd, void *buf, size_t n)
-{
-    char *p = (char *)buf;
-    while (n) {
-        ssize_t r = read(fd, p, n);
-        if (r < 0) { if (errno == EINTR) continue; return false; }
-        if (r == 0) return false;
-        p += r;
-        n -= (size_t)r;
-    }
-    return true;
-}
-
-/* Returns a malloc'ed, NUL-terminated payload (caller frees), or NULL. */
-static char *sway_request(uint32_t type, const char *payload)
-{
-    if (sway_fd < 0) return NULL;
-
-    size_t plen = payload ? strlen(payload) : 0;
-    char hdr[14];
-    memcpy(hdr, "i3-ipc", 6);
-    uint32_t l = (uint32_t)plen, t = type;
-    memcpy(hdr + 6, &l, 4);
-    memcpy(hdr + 10, &t, 4);
-
-    if (!write_all(sway_fd, hdr, sizeof(hdr))) return NULL;
-    if (plen && !write_all(sway_fd, payload, plen)) return NULL;
-
-    char rhdr[14];
-    if (!read_all(sway_fd, rhdr, sizeof(rhdr))) return NULL;
-    if (memcmp(rhdr, "i3-ipc", 6) != 0) return NULL;
-
-    uint32_t rlen = 0;
-    memcpy(&rlen, rhdr + 6, 4);
-    if (rlen > (64u << 20)) return NULL;
-
-    char *body = (char *)xmalloc(rlen + 1);
-    if (rlen && !read_all(sway_fd, body, rlen)) { free(body); return NULL; }
-    body[rlen] = 0;
-    return body;
-}
-
-static JV *sway_query(uint32_t type)
-{
-    char *body = sway_request(type, NULL);
-    if (!body) return NULL;
-    JV *v = jparse(body);
-    free(body);
-    return v;
-}
-
-static bool sway_cmd(const char *fmt, ...)
-{
-    va_list ap;
-    va_start(ap, fmt);
-    int n = vsnprintf(NULL, 0, fmt, ap);
-    va_end(ap);
-    if (n < 0) return false;
-
-    char *cmd = (char *)xmalloc((size_t)n + 1);
-    va_start(ap, fmt);
-    vsnprintf(cmd, (size_t)n + 1, fmt, ap);
-    va_end(ap);
-
-    char *reply = sway_request(IPC_RUN_COMMAND, cmd);
-    free(cmd);
-    if (!reply) return false;
-
-    bool ok = true;
-    JV *v = jparse(reply);
-    if (v && v->type == J_ARR)
-        for (int i = 0; i < v->count; ++i)
-            if (!jbool(v->items[i], "success", true)) ok = false;
-    jfree(v);
-    free(reply);
-    return ok;
-}
-
-/* A second socket carries workspace/mode events, so the bar repaints when
- * sway says something changed instead of polling the tree. */
-static bool sway_subscribe_events(void)
-{
-    sway_evt_fd = sway_connect();
-    if (sway_evt_fd < 0) return false;
-
-    const char *payload = "[\"workspace\",\"mode\"]";
-    char hdr[14];
-    memcpy(hdr, "i3-ipc", 6);
-    uint32_t l = (uint32_t)strlen(payload), t = IPC_SUBSCRIBE;
-    memcpy(hdr + 6, &l, 4);
-    memcpy(hdr + 10, &t, 4);
-
-    if (!write_all(sway_evt_fd, hdr, sizeof(hdr)) ||
-        !write_all(sway_evt_fd, payload, l)) {
-        close(sway_evt_fd);
-        sway_evt_fd = -1;
-        return false;
-    }
-    char rhdr[14];
-    uint32_t rlen = 0;
-    if (!read_all(sway_evt_fd, rhdr, sizeof(rhdr))) {
-        close(sway_evt_fd); sway_evt_fd = -1; return false;
-    }
-    memcpy(&rlen, rhdr + 6, 4);
-    if (rlen && rlen < (1u << 20)) {
-        char *body = (char *)xmalloc(rlen + 1);
-        read_all(sway_evt_fd, body, rlen);
-        free(body);
-    }
-    int flags = fcntl(sway_evt_fd, F_GETFL, 0);
-    if (flags >= 0) fcntl(sway_evt_fd, F_SETFL, flags | O_NONBLOCK);
-    return true;
-}
-
-/* true when sway reported at least one change since the last call. The mode
- * name is picked out of "mode" events on the way past. */
-static char sway_mode[128] = "default";
-
-static bool sway_events_pending(void)
-{
-    if (sway_evt_fd < 0) return false;
-    bool any = false;
-
-    for (;;) {
-        char hdr[14];
-        ssize_t r = recv(sway_evt_fd, hdr, sizeof(hdr), MSG_DONTWAIT);
-        if (r <= 0) break;
-        if ((size_t)r < sizeof(hdr) &&
-            !read_all(sway_evt_fd, hdr + r, sizeof(hdr) - (size_t)r)) break;
-
-        uint32_t rlen = 0, rtype = 0;
-        memcpy(&rlen, hdr + 6, 4);
-        memcpy(&rtype, hdr + 10, 4);
-        if (rlen > (16u << 20)) break;
-
-        char *body = (char *)xmalloc(rlen + 1);
-        if (rlen && !read_all(sway_evt_fd, body, rlen)) { free(body); break; }
-        body[rlen] = 0;
-
-        /* 0x80000002 = mode, 0x80000000 = workspace */
-        if (rtype == 0x80000002u) {
-            JV *v = jparse(body);
-            if (v) str_set(sway_mode, sizeof(sway_mode), jstr(v, "change", "default"));
-            jfree(v);
-        }
-        free(body);
-        any = true;
-    }
-    return any;
-}
-
-/* ---------------------------------------------------------- workspaces */
-
-typedef struct {
-    int  num;
-    char name[128];      /* full name, e.g. "3:web" */
-    char label[128];     /* what we draw: number only, or the full name */
-    char output[64];
-    bool focused, visible, urgent;
-} Ws;
-
-static Ws  ws_list[MAX_WORKSPACES];
-static int ws_count = 0;
-
-/* "3:web" -> "3" ; a name without a leading number keeps the name */
-static void ws_make_label(Ws *w, bool full_names)
-{
-    if (full_names) { str_set(w->label, sizeof(w->label), w->name); return; }
-    const char *colon = strchr(w->name, ':');
-    if (colon && colon != w->name) {
-        size_t n = (size_t)(colon - w->name);
-        if (n >= sizeof(w->label)) n = sizeof(w->label) - 1;
-        memcpy(w->label, w->name, n);
-        w->label[n] = 0;
-        return;
-    }
-    if (w->num >= 0) snprintf(w->label, sizeof(w->label), "%d", w->num);
-    else             str_set(w->label, sizeof(w->label), w->name);
-}
-
-static void ws_reload(bool full_names)
-{
-    JV *r = sway_query(IPC_GET_WORKSPACES);
-    if (!r || r->type != J_ARR) { jfree(r); return; }
-
-    ws_count = 0;
-    for (int i = 0; i < r->count && ws_count < MAX_WORKSPACES; ++i) {
-        JV *o = r->items[i];
-        Ws *w = &ws_list[ws_count++];
-        memset(w, 0, sizeof(*w));
-        w->num = jint(o, "num", -1);
-        str_set(w->name, sizeof(w->name), jstr(o, "name", "?"));
-        str_set(w->output, sizeof(w->output), jstr(o, "output", ""));
-        w->focused = jbool(o, "focused", false);
-        w->visible = jbool(o, "visible", false);
-        w->urgent  = jbool(o, "urgent", false);
-        ws_make_label(w, full_names);
-    }
-    jfree(r);
-}
-
-/* ---------------------------------------------------------------- config */
-
-/* A cell is one command whose output is drawn on the right hand side.
- * Declared with `cell=NAME`, configured with `NAME.field=value`. */
-typedef struct {
-    char  name[32];
-    char  cmd[2048];
-    int   interval;              /* seconds; 0 = run once; -1 = keep streaming */
-    char  fmt[128];              /* %s is replaced by the output */
-    char  empty[128];            /* drawn when the command prints nothing */
-    int   gap;                   /* -1 = cell_gap */
-    int   pad;                   /* inner padding, left and right */
-    int   slim;                  /* how it shows in the folded strip */
-    float slim_min, slim_max;    /* value range a gauge maps, default 0..100 */
-    int   slim_w;                /* custom width in the folded strip */
-    Col   slim_color;            /* overrides the colour on the strip */
-    bool  has_slim_color;
-    Col   color, bg;
-    bool  has_color, has_bg;
-    Col   warn_col, crit_col;
-    float warn_th, crit_th;
-    int   warn_dir, crit_dir;    /* +1 = at or above, -1 = at or below */
-    bool  has_warn, has_crit;
-    int   min_w, max_w;
-    char  align[8];
-    int   sep, hide_empty, markup;
-    char  pos[8];                /* which group: left | center | right */
-    int   scroll;                /* too long -> ".." , and scroll while hovered */
-    float scroll_px;
-    uint32_t scroll_t;
-    char  bind[MAX_BINDS][256];
-
-    /* runtime */
-    pid_t pid;
-    int   fd;
-    char  out[1024];             /* the last complete output */
-    char  buf[2048];
-    size_t len;
-    uint32_t next_run;
-} Cell;
-
-typedef struct {
-    /* placement */
-    char layer[16], position[16];
-    int  height, exclusive, margin, side_margin, min_width;
-    char align_x[8];
-    float radius;
-    char outputs[512];
-
-    /* workspaces */
-    int  ws_names, ws_pad, ws_gap, ws_min_w, ws_click, scroll_workspace, ws_inset;
-    float ws_radius;
-    int  mode_show;
-
-    /* text */
-    char font[PATH_MAX], font_alt[PATH_MAX];
-    float ui_scale, text_px, ws_px, text_y;
-    int  pad_x, pad_right, markup;
-    int  scroll_speed, scroll_pause;
-    char layout[1024];
-
-    /* status */
-    char status_command[4096];
-    int  interval;
-    Cell cell[MAX_CELLS];
-    int  cell_count;
-    char msg_fifo[PATH_MAX], msg_target[32];
-    int  msg_timeout, msg_flash;
-    Col  msg_info, msg_warn, msg_error;
-    int  cell_gap, cell_inset, slim_ws_slots;
-    float cell_radius;
-    char separator[16];
-    Col  separator_color;
-
-    /* collapse */
-    char hide_key[32];
-    int  hide_keycode, hover_keys, collapsed_px, anim_ms, signals, start_collapsed;
-
-    /* mouse bindings, index = button number (1..9) */
-    char bind[MAX_BINDS][512];
-
-    /* colors */
-    Col bg, text, dim, accent, hl, urgent, outline, running;
-    Col ws_bg, ws_fg, ws_focused_bg, ws_focused_fg;
-    Col ws_visible_bg, ws_visible_fg, ws_urgent_bg, ws_urgent_fg;
-    Col mode_bg, mode_fg;
-} Config;
-
-static void config_defaults(Config *c)
-{
-    memset(c, 0, sizeof(*c));
-    str_set(c->layer, sizeof(c->layer), "top");
-    str_set(c->position, sizeof(c->position), "top");
-    c->height = 0;                 /* 0 = derive from the font size */
-    c->exclusive = 0;
-    c->margin = 0;
-    c->side_margin = 0;
-    c->min_width = 800;
-    str_set(c->align_x, sizeof(c->align_x), "center");
-    c->radius = 10.0f;
-
-    c->ws_names = 0;
-    c->ws_pad = 11;
-    c->ws_gap = 4;
-    c->ws_inset = 0;
-    c->ws_min_w = 0;
-    c->ws_click = 1;
-    c->scroll_workspace = 0;
-    c->ws_radius = 0.0f;           /* square: the button spans the full height */
-    c->mode_show = 1;
-
-    c->ui_scale = 1.0f;
-    c->text_px = 19.0f;
-    c->ws_px = 19.0f;
-    c->text_y = -2.0f;
-    c->pad_x = 12;
-    c->pad_right = 20;
-    c->scroll_speed = 45;
-    c->scroll_pause = 900;
-    c->markup = 1;
-
-    c->interval = 1;
-    c->cell_gap = 14;
-    c->cell_inset = 3;
-    c->slim_ws_slots = 10;
-    c->cell_radius = 5.0f;
-    c->msg_timeout = 8;
-    c->msg_flash = 1;
-    c->msg_info  = (Col){ 0x89, 0xaf, 0xc4, 0xff };
-    c->msg_warn  = (Col){ 0xcb, 0x9b, 0x00, 0xff };
-    c->msg_error = (Col){ 0xe0, 0x53, 0x3c, 0xff };
-    str_set(c->separator, sizeof(c->separator), "");
-    c->separator_color = (Col){ 0x5a, 0x6b, 0x7a, 0xff };
-
-    str_set(c->hide_key, sizeof(c->hide_key), "space");
-    c->hide_keycode = 0;
-    c->hover_keys = 1;
-    c->collapsed_px = 5;
-    c->anim_ms = 120;
-    c->signals = 1;
-    c->start_collapsed = 0;
-
-    c->bg            = (Col){ 0x26, 0x31, 0x3f, 0xe6 };
-    c->text          = (Col){ 0xe8, 0xe8, 0xe8, 0xff };
-    c->dim           = (Col){ 0x5a, 0x6b, 0x7a, 0xff };
-    c->accent        = (Col){ 0x89, 0xaf, 0xc4, 0xff };
-    c->hl            = (Col){ 0xcb, 0x9b, 0x00, 0xff };
-    c->urgent        = (Col){ 0xe0, 0x53, 0x3c, 0xff };
-    c->running       = (Col){ 0x2f, 0xbf, 0x71, 0xff };
-    c->outline       = (Col){ 0x0a, 0x0e, 0x14, 0x99 };
-    c->ws_bg         = (Col){ 0x1e, 0x27, 0x33, 0x00 };
-    c->ws_fg         = (Col){ 0xb3, 0xc0, 0xcd, 0xff };
-    c->ws_focused_bg = (Col){ 0xcb, 0x9b, 0x00, 0xff };
-    c->ws_focused_fg = (Col){ 0x14, 0x14, 0x14, 0xff };
-    c->ws_visible_bg = (Col){ 0x3c, 0x4a, 0x5c, 0xff };
-    c->ws_visible_fg = (Col){ 0xe1, 0xee, 0xff, 0xff };
-    c->ws_urgent_bg  = (Col){ 0xe0, 0x53, 0x3c, 0xff };
-    c->ws_urgent_fg  = (Col){ 0x14, 0x14, 0x14, 0xff };
-    c->mode_bg       = (Col){ 0xcb, 0x9b, 0x00, 0xff };
-    c->mode_fg       = (Col){ 0x14, 0x14, 0x14, 0xff };
-}
-
-/* ------------------------------------------------------------------ cells */
-
-static Cell *cell_find(Config *c, const char *name)
-{
-    for (int i = 0; i < c->cell_count; ++i)
-        if (!strcmp(c->cell[i].name, name)) return &c->cell[i];
-    return NULL;
-}
-
-static Cell *cell_add(Config *c, const char *name)
-{
-    Cell *e = cell_find(c, name);
-    if (e) return e;
-    if (c->cell_count >= MAX_CELLS) return NULL;
-    e = &c->cell[c->cell_count++];
-    memset(e, 0, sizeof(*e));
-    str_set(e->name, sizeof(e->name), name);
-    e->interval = 2;
-    e->hide_empty = 1;
-    e->gap = -1;
-    e->slim_min = 0.0f;
-    e->slim_max = 100.0f;
-    e->markup = 1;
-    e->sep = 1;
-    e->fd = -1;
-    e->pid = -1;
-    str_set(e->align, sizeof(e->align), "right");
-    str_set(e->pos, sizeof(e->pos), "right");
-    return e;
-}
-
-/* "70:cecb00" or "<30:ff2222" — direction, threshold, colour */
-static void cell_threshold(const char *v, float *th, int *dir, Col *col, bool *has)
-{
-    *dir = 1;
-    if (*v == '<') { *dir = -1; v++; }
-    else if (*v == '>') { *dir = 1; v++; }
-    *th = (float)atof(v);
-    const char *colon = strchr(v, ':');
-    if (colon) parse_color(colon + 1, col);
-    *has = true;
-}
-
-static void cell_set(Config *c, const char *name, const char *k, const char *v)
-{
-    Cell *e = cell_add(c, name);
-    if (!e) return;
-    if (!strcmp(k, "cmd") || !strcmp(k, "command")) str_set(e->cmd, sizeof(e->cmd), v);
-    else if (!strcmp(k, "interval")) e->interval = atoi(v);
-    else if (!strcmp(k, "fmt") || !strcmp(k, "format")) str_set(e->fmt, sizeof(e->fmt), v);
-    else if (!strcmp(k, "empty")) str_set(e->empty, sizeof(e->empty), v);
-    else if (!strcmp(k, "gap")) e->gap = atoi(v);
-    else if (!strcmp(k, "pad")) e->pad = atoi(v);
-    else if (!strcmp(k, "slim_min")) e->slim_min = (float)atof(v);
-    else if (!strcmp(k, "slim_max")) e->slim_max = (float)atof(v);
-    else if (!strcmp(k, "slim_w")) e->slim_w = atoi(v);
-    else if (!strcmp(k, "slim_color")) {
-        e->slim_color.a = 255;
-        parse_color(v, &e->slim_color);
-        e->has_slim_color = true;
-    }
-    else if (!strcmp(k, "slim")) {
-        if (!strcasecmp(v, "bar")) e->slim = SLIM_BAR;
-        else if (!strcasecmp(v, "clock")) e->slim = SLIM_CLOCK;
-        else if (!strcasecmp(v, "tick")) e->slim = SLIM_TICK;
-        else if (!strcasecmp(v, "presence") || !strcasecmp(v, "running") ||
-                 !strcasecmp(v, "app")) e->slim = SLIM_PRESENCE;
-        else if (!strcasecmp(v, "off") || !strcmp(v, "0")) e->slim = SLIM_OFF;
-        else e->slim = SLIM_AUTO;
-    }
-    else if (!strcmp(k, "color") || !strcmp(k, "fg")) { parse_color(v, &e->color); e->has_color = true; }
-    else if (!strcmp(k, "bg")) { e->bg.a = 255; parse_color(v, &e->bg); e->has_bg = true; }
-    else if (!strcmp(k, "warn")) cell_threshold(v, &e->warn_th, &e->warn_dir, &e->warn_col, &e->has_warn);
-    else if (!strcmp(k, "crit")) cell_threshold(v, &e->crit_th, &e->crit_dir, &e->crit_col, &e->has_crit);
-    else if (!strcmp(k, "min_w") || !strcmp(k, "width")) e->min_w = atoi(v);
-    else if (!strcmp(k, "max_w")) e->max_w = atoi(v);
-    else if (!strcmp(k, "align")) str_set(e->align, sizeof(e->align), v);
-    else if (!strcmp(k, "pos") || !strcmp(k, "place")) str_set(e->pos, sizeof(e->pos), v);
-    else if (!strcmp(k, "scroll") || !strcmp(k, "text")) e->scroll = atoi(v);
-    else if (!strcmp(k, "sep") || !strcmp(k, "separator")) e->sep = atoi(v);
-    else if (!strcmp(k, "hide_empty")) e->hide_empty = atoi(v);
-    else if (!strcmp(k, "markup")) e->markup = atoi(v);
-    else if (!strncmp(k, "button", 6) && isdigit((unsigned char)k[6]) && !k[7]) {
-        int b = k[6] - '0';
-        if (b >= 1 && b < MAX_BINDS) str_set(e->bind[b], sizeof(e->bind[b]), v);
-    }
-    else fprintf(stderr, "swbr: unknown cell key '%s.%s'\n", name, k);
-}
-
-static void config_set(Config *c, const char *k, const char *v)
-{
-    const char *dot = strchr(k, '.');
-    if (dot && dot != k) {                       /* NAME.field=value */
-        char name[32];
-        size_t n = (size_t)(dot - k);
-        if (n >= sizeof(name)) n = sizeof(name) - 1;
-        memcpy(name, k, n);
-        name[n] = 0;
-        cell_set(c, name, dot + 1, v);
-        return;
-    }
-    if (!strcmp(k, "cell")) { cell_add(c, v); return; }
-    if (!strcmp(k, "cell_gap")) { c->cell_gap = atoi(v); return; }
-    if (!strcmp(k, "cell_inset")) { c->cell_inset = atoi(v); return; }
-    if (!strcmp(k, "slim_ws_slots")) { c->slim_ws_slots = atoi(v); return; }
-    if (!strcmp(k, "cell_radius")) { c->cell_radius = (float)atof(v); return; }
-    if (!strcmp(k, "separator")) { str_set(c->separator, sizeof(c->separator), v); return; }
-    if (!strcmp(k, "separator_color")) { parse_color(v, &c->separator_color); return; }
-
-    if (!strcmp(k, "layer")) str_set(c->layer, sizeof(c->layer), v);
-    else if (!strcmp(k, "position")) str_set(c->position, sizeof(c->position), v);
-    else if (!strcmp(k, "height")) c->height = atoi(v);
-    else if (!strcmp(k, "exclusive") || !strcmp(k, "reserve_space")) c->exclusive = atoi(v);
-    else if (!strcmp(k, "margin")) c->margin = atoi(v);
-    else if (!strcmp(k, "side_margin")) c->side_margin = atoi(v);
-    else if (!strcmp(k, "min_width") || !strcmp(k, "width") ||
-             !strcmp(k, "max_width")) c->min_width = atoi(v);
-    else if (!strcmp(k, "align_x")) str_set(c->align_x, sizeof(c->align_x), v);
-    else if (!strcmp(k, "radius")) c->radius = (float)atof(v);
-    else if (!strcmp(k, "outputs")) str_set(c->outputs, sizeof(c->outputs), v);
-    else if (!strcmp(k, "ws_names") || !strcmp(k, "workspace_names")) c->ws_names = atoi(v);
-    else if (!strcmp(k, "ws_pad")) c->ws_pad = atoi(v);
-    else if (!strcmp(k, "ws_gap")) c->ws_gap = atoi(v);
-    else if (!strcmp(k, "ws_min_w")) c->ws_min_w = atoi(v);
-    else if (!strcmp(k, "ws_inset")) c->ws_inset = atoi(v);
-    else if (!strcmp(k, "ws_click")) c->ws_click = atoi(v);
-    else if (!strcmp(k, "ws_radius")) c->ws_radius = (float)atof(v);
-    else if (!strcmp(k, "scroll_workspace")) c->scroll_workspace = atoi(v);
-    else if (!strcmp(k, "mode_show")) c->mode_show = atoi(v);
-    else if (!strcmp(k, "font")) str_set(c->font, sizeof(c->font), v);
-    else if (!strcmp(k, "font_alt")) str_set(c->font_alt, sizeof(c->font_alt), v);
-    else if (!strcmp(k, "ui_scale") || !strcmp(k, "font_scale") || !strcmp(k, "text_scale"))
-        c->ui_scale = (float)atof(v);
-    else if (!strcmp(k, "text_px")) c->text_px = (float)atof(v);
-    else if (!strcmp(k, "ws_px")) c->ws_px = (float)atof(v);
-    else if (!strcmp(k, "pad_x")) c->pad_x = atoi(v);
-    else if (!strcmp(k, "pad_right")) c->pad_right = atoi(v);
-    else if (!strcmp(k, "scroll_speed")) c->scroll_speed = atoi(v);
-    else if (!strcmp(k, "scroll_pause")) c->scroll_pause = atoi(v);
-    else if (!strcmp(k, "text_y")) c->text_y = (float)atof(v);
-    else if (!strcmp(k, "markup")) c->markup = atoi(v);
-    else if (!strcmp(k, "status_command")) str_set(c->status_command, sizeof(c->status_command), v);
-    else if (!strcmp(k, "interval")) c->interval = atoi(v);
-    else if (!strcmp(k, "bar") || !strcmp(k, "layout"))
-        str_set(c->layout, sizeof(c->layout), v);
-    else if (!strcmp(k, "msg_fifo")) str_set(c->msg_fifo, sizeof(c->msg_fifo), v);
-    else if (!strcmp(k, "msg_target")) str_set(c->msg_target, sizeof(c->msg_target), v);
-    else if (!strcmp(k, "msg_timeout")) c->msg_timeout = atoi(v);
-    else if (!strcmp(k, "msg_flash")) c->msg_flash = atoi(v);
-    else if (!strcmp(k, "msg_info")) parse_color(v, &c->msg_info);
-    else if (!strcmp(k, "msg_warn")) parse_color(v, &c->msg_warn);
-    else if (!strcmp(k, "msg_error")) parse_color(v, &c->msg_error);
-    else if (!strcmp(k, "hide_key")) str_set(c->hide_key, sizeof(c->hide_key), v);
-    else if (!strcmp(k, "hide_keycode")) c->hide_keycode = atoi(v);
-    else if (!strcmp(k, "hover_keys")) c->hover_keys = atoi(v);
-    else if (!strcmp(k, "collapsed_px")) c->collapsed_px = atoi(v);
-    else if (!strcmp(k, "anim_ms")) c->anim_ms = atoi(v);
-    else if (!strcmp(k, "signals")) c->signals = atoi(v);
-    else if (!strcmp(k, "start_collapsed")) c->start_collapsed = atoi(v);
-    else if (!strncmp(k, "button", 6) && isdigit((unsigned char)k[6]) && !k[7]) {
-        int b = k[6] - '0';
-        if (b >= 1 && b < MAX_BINDS) str_set(c->bind[b], sizeof(c->bind[b]), v);
-    }
-    else if (!strcmp(k, "bg") || !strcmp(k, "background")) parse_color(v, &c->bg);
-    else if (!strcmp(k, "text") || !strcmp(k, "statusline")) parse_color(v, &c->text);
-    else if (!strcmp(k, "dim")) parse_color(v, &c->dim);
-    else if (!strcmp(k, "accent")) parse_color(v, &c->accent);
-    else if (!strcmp(k, "hl")) parse_color(v, &c->hl);
-    else if (!strcmp(k, "urgent")) parse_color(v, &c->urgent);
-    else if (!strcmp(k, "running")) parse_color(v, &c->running);
-    else if (!strcmp(k, "outline")) parse_color(v, &c->outline);
-    else if (!strcmp(k, "ws_bg")) parse_color(v, &c->ws_bg);
-    else if (!strcmp(k, "ws_fg")) parse_color(v, &c->ws_fg);
-    else if (!strcmp(k, "ws_focused_bg")) parse_color(v, &c->ws_focused_bg);
-    else if (!strcmp(k, "ws_focused_fg")) parse_color(v, &c->ws_focused_fg);
-    else if (!strcmp(k, "ws_visible_bg")) parse_color(v, &c->ws_visible_bg);
-    else if (!strcmp(k, "ws_visible_fg")) parse_color(v, &c->ws_visible_fg);
-    else if (!strcmp(k, "ws_urgent_bg")) parse_color(v, &c->ws_urgent_bg);
-    else if (!strcmp(k, "ws_urgent_fg")) parse_color(v, &c->ws_urgent_fg);
-    else if (!strcmp(k, "mode_bg")) parse_color(v, &c->mode_bg);
-    else if (!strcmp(k, "mode_fg")) parse_color(v, &c->mode_fg);
-    else fprintf(stderr, "swbr: unknown key '%s'\n", k);
-}
-
-/* Cut a trailing "  # comment" off a value. Quotes are respected, so the
- * '#' inside  foreground="#19cb00"  or inside a quoted sed script survives.
- * Only a '#'
- * that follows whitespace starts a comment, so  fmt=%s#  is safe too. */
-static void strip_comment(char *v)
-{
-    char q = 0;
-    for (char *p = v; *p; ++p) {
-        if (q) { if (*p == q) q = 0; continue; }
-        if (*p == '\'' || *p == '"') { q = *p; continue; }
-        if (*p == '#' && p > v && isspace((unsigned char)p[-1])) { *p = 0; return; }
-    }
-}
-
-/* the shared ~/.config/sw/config, translated into swbr's own keys */
-static void config_set_shared(void *ud, const char *k, const char *v)
-{
-    config_set((Config *)ud, k, v);
-}
-
-static void config_load(Config *c, const char *path)
-{
-    FILE *f = fopen(path, "r");
-    if (!f) { fprintf(stderr, "swbr: no config at %s\n", path); return; }
-    char line[8192];
-    while (fgets(line, sizeof(line), f)) {
-        char *s = trim(line);
-        if (!*s || *s == '#' || *s == ';') continue;
-        char *eq = strchr(s, '=');
-        if (!eq) continue;
-        *eq = 0;
-        char *v = eq + 1;
-        strip_comment(v);
-        config_set(c, trim(s), trim(v));
-    }
-    fclose(f);
-}
-
-static Config cfg;
-
-/* ------------------------------------------------------------------ text */
-/* stb_truetype, rasterised straight into our own ARGB buffer. Glyphs are
- * cached per size on first use, so any codepoint works — the status line is
- * full of box drawing characters and symbols. */
-
-typedef struct {
-    unsigned char *data;
-    stbtt_fontinfo info;
-    bool ok;
-} FontFile;
-
-static FontFile font_pri, font_alt;
-
-typedef struct {
-    unsigned cp;                 /* 0 = free slot */
-    int w, h, x0, y0;
-    float adv;
-    unsigned char *bmp;
-} Glyph;
-
-typedef struct {
-    int px;
-    float sc_pri, sc_alt, ascent, lineh;
-    Glyph *tab;
-    int cap, n;
-    bool used;
-} Font;
-
-#define FONT_CACHE 12
-static Font font_cache[FONT_CACHE];
-
-static bool font_file_load(FontFile *ff, const char *path)
-{
-    memset(ff, 0, sizeof(*ff));
-    FILE *f = fopen(path, "rb");
-    if (!f) return false;
-    fseek(f, 0, SEEK_END);
-    long sz = ftell(f);
-    fseek(f, 0, SEEK_SET);
-    if (sz <= 0) { fclose(f); return false; }
-    ff->data = (unsigned char *)xmalloc((size_t)sz);
-    if (fread(ff->data, 1, (size_t)sz, f) != (size_t)sz) {
-        fclose(f); free(ff->data); ff->data = NULL; return false;
-    }
-    fclose(f);
-    if (!stbtt_InitFont(&ff->info, ff->data, stbtt_GetFontOffsetForIndex(ff->data, 0))) {
-        free(ff->data); ff->data = NULL; return false;
-    }
-    ff->ok = true;
-    return true;
-}
-
-static bool try_font_paths(char *out, size_t n)
-{
-    const char *cand[] = {
-        "/usr/share/fonts/truetype/dejavu/DejaVuSansMono.ttf",
-        "/usr/share/fonts/dejavu/DejaVuSansMono.ttf",
-        "/usr/share/fonts/TTF/DejaVuSansMono.ttf",
-        "/usr/share/fonts/truetype/dejavu/DejaVuSans.ttf",
-        "/usr/share/fonts/truetype/liberation/LiberationMono-Regular.ttf",
-        "/usr/share/fonts/noto/NotoSansMono-Regular.ttf",
-        "/usr/share/fonts/truetype/freefont/FreeMono.ttf", NULL
-    };
-    for (int i = 0; cand[i]; ++i)
-        if (file_exists(cand[i])) { snprintf(out, n, "%s", cand[i]); return true; }
-    return false;
-}
-
-/* Ask fontconfig (via its fc-match CLI) to resolve a family or pattern to an
- * actual .ttf path. No library is linked; if fc-match is missing we fall back
- * to the known paths above. */
-static bool fc_match(const char *pattern, char *out, size_t n)
-{
-    if (!pattern || !*pattern) pattern = "monospace";
-    char safe[256];
-    size_t j = 0;
-    for (size_t i = 0; pattern[i] && j < sizeof(safe) - 1; ++i) {
-        char ch = pattern[i];
-        if (ch == '"' || ch == '`' || ch == '$' || ch == '\\' || ch == ';' ||
-            ch == '|' || ch == '&' || ch == '\n' || ch == '\r') continue;
-        safe[j++] = ch;
-    }
-    safe[j] = 0;
-    char cmd[512];
-    snprintf(cmd, sizeof(cmd), "fc-match --format=%%{file} \"%s\" 2>/dev/null", safe);
-    FILE *p = popen(cmd, "r");
-    if (!p) return false;
-    char buf[PATH_MAX];
-    size_t r = fread(buf, 1, sizeof(buf) - 1, p);
-    buf[r] = 0;
-    pclose(p);
-    char *s = trim(buf);
-    if (*s && file_exists(s)) { snprintf(out, n, "%s", s); return true; }
-    return false;
-}
-
-static char font_path_used[PATH_MAX];
-static char font_alt_used[PATH_MAX];
-static void fonts_init(void)
-{
-    char path[PATH_MAX] = "";
-    if (*cfg.font) {
-        expand_tilde(cfg.font, path, sizeof(path));
-        if (!file_exists(path)) fc_match(cfg.font, path, sizeof(path));
-    }
-    if (!*path || !file_exists(path)) fc_match("monospace", path, sizeof(path));
-    if (!*path || !file_exists(path)) try_font_paths(path, sizeof(path));
-    if (*path) { font_file_load(&font_pri, path); str_set(font_path_used, PATH_MAX, path); }
-    if (!font_pri.ok) fprintf(stderr, "swbr: no usable font found\n");
-
-    char alt[PATH_MAX] = "";
-    if (*cfg.font_alt) {
-        expand_tilde(cfg.font_alt, alt, sizeof(alt));
-        if (!file_exists(alt)) fc_match(cfg.font_alt, alt, sizeof(alt));
-    } else {
-        /* a symbol font, if the desktop has one — covers glyphs a mono font
-         * usually lacks. Silently skipped when absent. */
-        if (!fc_match("Noto Sans Symbols 2", alt, sizeof(alt)))
-            fc_match("Noto Sans Symbols2", alt, sizeof(alt));
-    }
-    if (*alt && file_exists(alt) && (!*path || strcmp(alt, path))) {
-        font_file_load(&font_alt, alt);
-        str_set(font_alt_used, PATH_MAX, alt);
-    }
-}
-
-static void glyph_tab_init(Font *f, int cap)
-{
-    f->cap = cap;
-    f->n = 0;
-    f->tab = (Glyph *)xmalloc((size_t)cap * sizeof(Glyph));
-    memset(f->tab, 0, (size_t)cap * sizeof(Glyph));
-}
-
-static Glyph *glyph_slot(Font *f, unsigned cp)
-{
-    unsigned h = cp * 2654435761u;
-    for (;;) {
-        unsigned i = h & (unsigned)(f->cap - 1);
-        for (int probe = 0; probe < f->cap; ++probe) {
-            Glyph *g = &f->tab[(i + (unsigned)probe) & (unsigned)(f->cap - 1)];
-            if (g->cp == cp) return g;
-            if (g->cp == 0) return g;
-        }
-        /* full: grow and rehash */
-        Glyph *old = f->tab;
-        int oldcap = f->cap;
-        glyph_tab_init(f, oldcap * 2);
-        for (int k = 0; k < oldcap; ++k) {
-            if (!old[k].cp) continue;
-            Glyph *g = glyph_slot(f, old[k].cp);
-            *g = old[k];
-            f->n++;
-        }
-        free(old);
-    }
-}
-
-static Glyph *glyph_get(Font *f, unsigned cp)
-{
-    Glyph *g = glyph_slot(f, cp);
-    if (g->cp == cp) return g;
-
-    FontFile *ff = &font_pri;
-    float sc = f->sc_pri;
-    int gi = font_pri.ok ? stbtt_FindGlyphIndex(&font_pri.info, (int)cp) : 0;
-    if (!gi && font_alt.ok) {
-        int gi2 = stbtt_FindGlyphIndex(&font_alt.info, (int)cp);
-        if (gi2) { ff = &font_alt; sc = f->sc_alt; gi = gi2; }
-    }
-    memset(g, 0, sizeof(*g));
-    g->cp = cp;
-    f->n++;
-    if (!ff->ok || !gi) {
-        g->adv = (float)f->px * 0.5f;      /* missing: leave a blank */
-        return g;
-    }
-    int aw, lsb;
-    stbtt_GetGlyphHMetrics(&ff->info, gi, &aw, &lsb);
-    g->adv = (float)aw * sc;
-    int x0, y0, x1, y1;
-    stbtt_GetGlyphBitmapBox(&ff->info, gi, sc, sc, &x0, &y0, &x1, &y1);
-    int w = x1 - x0, h = y1 - y0;
-    if (w > 0 && h > 0 && w < 512 && h < 512) {
-        g->bmp = (unsigned char *)xmalloc((size_t)w * (size_t)h);
-        stbtt_MakeGlyphBitmap(&ff->info, g->bmp, w, h, w, sc, sc, gi);
-        g->w = w; g->h = h; g->x0 = x0; g->y0 = y0;
-    }
-    return g;
-}
-
-/* One Font per pixel size (and therefore per output scale). */
-static Font *font_get(int px)
-{
-    if (px < 4) px = 4;
-    for (int i = 0; i < FONT_CACHE; ++i)
-        if (font_cache[i].used && font_cache[i].px == px) return &font_cache[i];
-
-    int slot = -1;
-    for (int i = 0; i < FONT_CACHE; ++i) if (!font_cache[i].used) { slot = i; break; }
-    if (slot < 0) slot = 0;                       /* recycle the first one */
-    Font *f = &font_cache[slot];
-    if (f->used) {
-        for (int k = 0; k < f->cap; ++k) free(f->tab[k].bmp);
-        free(f->tab);
-        memset(f, 0, sizeof(*f));
-    }
-    f->used = true;
-    f->px = px;
-    f->sc_pri = font_pri.ok ? stbtt_ScaleForPixelHeight(&font_pri.info, (float)px) : 1.0f;
-    f->sc_alt = font_alt.ok ? stbtt_ScaleForPixelHeight(&font_alt.info, (float)px) : 1.0f;
-    int asc = 0, desc = 0, gap = 0;
-    if (font_pri.ok) stbtt_GetFontVMetrics(&font_pri.info, &asc, &desc, &gap);
-    f->ascent = (float)asc * f->sc_pri;
-    f->lineh = (float)(asc - desc) * f->sc_pri;
-    if (f->ascent <= 0) { f->ascent = (float)px * 0.8f; f->lineh = (float)px; }
-    glyph_tab_init(f, 256);
-    return f;
-}
-
-static unsigned utf8_next(const char **p)
-{
-    const unsigned char *s = (const unsigned char *)*p;
-    unsigned cp;
-    int n;
-    if (s[0] < 0x80) { cp = s[0]; n = 1; }
-    else if ((s[0] & 0xE0) == 0xC0) { cp = s[0] & 0x1Fu; n = 2; }
-    else if ((s[0] & 0xF0) == 0xE0) { cp = s[0] & 0x0Fu; n = 3; }
-    else if ((s[0] & 0xF8) == 0xF0) { cp = s[0] & 0x07u; n = 4; }
-    else { *p = (const char *)(s + 1); return 0xFFFD; }
-    for (int i = 1; i < n; ++i) {
-        if ((s[i] & 0xC0) != 0x80) { n = i; break; }
-        cp = (cp << 6) | (s[i] & 0x3Fu);
-    }
-    *p = (const char *)(s + n);
-    return cp;
-}
-
-static float text_width(Font *f, const char *s)
-{
-    float w = 0;
-    const char *p = s;
-    while (*p) {
-        unsigned cp = utf8_next(&p);
-        if (cp == 0xFE0F || cp == 0x200D) continue;   /* variation selectors */
-        w += glyph_get(f, cp)->adv;
-    }
-    return w;
-}
-
-/* ----------------------------------------------------------------- paint */
-/* ARGB8888, premultiplied — that is what wl_shm wants. */
-
-typedef struct {
-    uint32_t *px;
-    int w, h;
-    int cx0, cy0, cx1, cy1;      /* clip rectangle */
-} Canvas;
-
-static void canvas_clip(Canvas *cv, int x0, int y0, int x1, int y1)
-{
-    cv->cx0 = clampi(x0, 0, cv->w);
-    cv->cy0 = clampi(y0, 0, cv->h);
-    cv->cx1 = clampi(x1, 0, cv->w);
-    cv->cy1 = clampi(y1, 0, cv->h);
-}
-
-static void canvas_clip_all(Canvas *cv) { canvas_clip(cv, 0, 0, cv->w, cv->h); }
-
-static inline void blend(Canvas *cv, int x, int y, Col c, float cov)
-{
-    if (x < cv->cx0 || x >= cv->cx1 || y < cv->cy0 || y >= cv->cy1) return;
-    float a = (float)c.a / 255.0f * cov;
-    if (a <= 0.0f) return;
-    uint32_t d = cv->px[y * cv->w + x];
-    float da = (float)((d >> 24) & 0xff) / 255.0f;
-    float dr = (float)((d >> 16) & 0xff) / 255.0f;
-    float dg = (float)((d >> 8) & 0xff) / 255.0f;
-    float db = (float)(d & 0xff) / 255.0f;
-    float sr = (float)c.r / 255.0f * a;
-    float sg = (float)c.g / 255.0f * a;
-    float sb = (float)c.b / 255.0f * a;
-    float ia = 1.0f - a;
-    float orr = sr + dr * ia, og = sg + dg * ia, ob = sb + db * ia, oa = a + da * ia;
-    cv->px[y * cv->w + x] =
-        ((uint32_t)(orr * 255.0f + 0.5f) << 16) |
-        ((uint32_t)(og  * 255.0f + 0.5f) << 8)  |
-        ((uint32_t)(ob  * 255.0f + 0.5f))       |
-        ((uint32_t)(oa  * 255.0f + 0.5f) << 24);
-}
-
-static void fill_rect(Canvas *cv, float x, float y, float w, float h, Col c)
-{
-    if (w <= 0 || h <= 0 || !c.a) return;
-    int x0 = (int)floorf(x), y0 = (int)floorf(y);
-    int x1 = (int)ceilf(x + w), y1 = (int)ceilf(y + h);
-    for (int yy = y0; yy < y1; ++yy)
-        for (int xx = x0; xx < x1; ++xx) {
-            float cxs = clampf(x + w, (float)xx, (float)xx + 1) - clampf(x, (float)xx, (float)xx + 1);
-            float cys = clampf(y + h, (float)yy, (float)yy + 1) - clampf(y, (float)yy, (float)yy + 1);
-            blend(cv, xx, yy, c, cxs * cys);
-        }
-}
-
-/* Rounded box with a radius per corner (tl, tr, br, bl), anti-aliased through
- * the usual signed distance trick. A corner radius of 0 is a square corner —
- * that is how the bar stays flush with the screen edge. */
-static void fill_round(Canvas *cv, float x, float y, float w, float h,
-                       float rtl, float rtr, float rbr, float rbl, Col c)
-{
-    if (w <= 0 || h <= 0 || !c.a) return;
-    float hw = w * 0.5f, hh = h * 0.5f;
-    float ccx = x + hw, ccy = y + hh;
-    float cap = fminf(hw, hh);
-    rtl = clampf(rtl, 0, cap); rtr = clampf(rtr, 0, cap);
-    rbr = clampf(rbr, 0, cap); rbl = clampf(rbl, 0, cap);
-
-    int x0 = (int)floorf(x) - 1, y0 = (int)floorf(y) - 1;
-    int x1 = (int)ceilf(x + w) + 1, y1 = (int)ceilf(y + h) + 1;
-    for (int yy = y0; yy < y1; ++yy) {
-        if (yy < cv->cy0 || yy >= cv->cy1) continue;
-        for (int xx = x0; xx < x1; ++xx) {
-            if (xx < cv->cx0 || xx >= cv->cx1) continue;
-            float px = (float)xx + 0.5f - ccx, py = (float)yy + 0.5f - ccy;
-            float r = px < 0 ? (py < 0 ? rtl : rbl) : (py < 0 ? rtr : rbr);
-            float qx = fabsf(px) - (hw - r), qy = fabsf(py) - (hh - r);
-            float mx = fmaxf(qx, 0.0f), my = fmaxf(qy, 0.0f);
-            float d = sqrtf(mx * mx + my * my) + fminf(fmaxf(qx, qy), 0.0f) - r;
-            float cov = clampf(0.5f - d, 0.0f, 1.0f);
-            if (cov > 0) blend(cv, xx, yy, c, cov);
-        }
-    }
-}
-
-static void text_draw(Canvas *cv, Font *f, float x, float baseline, Col c, const char *s)
-{
-    const char *p = s;
-    float pen = x;
-    while (*p) {
-        unsigned cp = utf8_next(&p);
-        if (cp == 0xFE0F || cp == 0x200D) continue;
-        Glyph *g = glyph_get(f, cp);
-        if (g->bmp) {
-            int gx = (int)lroundf(pen) + g->x0;
-            int gy = (int)lroundf(baseline) + g->y0;
-            for (int yy = 0; yy < g->h; ++yy) {
-                int ty = gy + yy;
-                if (ty < cv->cy0 || ty >= cv->cy1) continue;
-                const unsigned char *row = g->bmp + (size_t)yy * (size_t)g->w;
-                for (int xx = 0; xx < g->w; ++xx) {
-                    unsigned char a = row[xx];
-                    if (a) blend(cv, gx + xx, ty, c, (float)a / 255.0f);
-                }
-            }
-        }
-        pen += g->adv;
-    }
-}
-
-/* ---------------------------------------------------------------- markup */
-/* The subset of pango markup swaybar users actually write:
- *   <span foreground="#rrggbb" background="#..">..</span>, <b>, <i>, <u>, <tt>
- *   and the five XML entities. Anything else is passed through as text. */
-
-typedef struct {
-    char text[512];
-    Col  fg, bg;
-    bool has_fg, has_bg;
-} Run;
-
-typedef struct {
-    Run v[MAX_RUNS];
-    int n;
-} Runs;
-
-typedef struct { Col fg, bg; bool has_fg, has_bg; } Style;
-
-static void runs_push(Runs *rs, const char *buf, int len, Style *st)
-{
-    if (len <= 0 || rs->n >= MAX_RUNS) return;
-    Run *r = &rs->v[rs->n++];
-    memset(r, 0, sizeof(*r));
-    if (len > (int)sizeof(r->text) - 1) len = (int)sizeof(r->text) - 1;
-    memcpy(r->text, buf, (size_t)len);
-    r->text[len] = 0;
-    r->fg = st->fg; r->bg = st->bg;
-    r->has_fg = st->has_fg; r->has_bg = st->has_bg;
-}
-
-static void entity_append(char *buf, int *len, int cap, const char *ent, int elen)
-{
-    char tmp[8];
-    const char *out = NULL;
-    if (elen == 3 && !strncmp(ent, "amp", 3)) out = "&";
-    else if (elen == 2 && !strncmp(ent, "lt", 2)) out = "<";
-    else if (elen == 2 && !strncmp(ent, "gt", 2)) out = ">";
-    else if (elen == 4 && !strncmp(ent, "quot", 4)) out = "\"";
-    else if (elen == 4 && !strncmp(ent, "apos", 4)) out = "'";
-    else if (elen > 1 && ent[0] == '#') {
-        bool hex = (ent[1] == 'x' || ent[1] == 'X');
-        unsigned cp = (unsigned)strtoul(ent + (hex ? 2 : 1), NULL, hex ? 16 : 10);
-        int n = 0;
-        if (cp && cp < 0x110000) {
-            if (cp < 0x80) tmp[n++] = (char)cp;
-            else if (cp < 0x800) {
-                tmp[n++] = (char)(0xC0 | (cp >> 6));
-                tmp[n++] = (char)(0x80 | (cp & 0x3F));
-            } else if (cp < 0x10000) {
-                tmp[n++] = (char)(0xE0 | (cp >> 12));
-                tmp[n++] = (char)(0x80 | ((cp >> 6) & 0x3F));
-                tmp[n++] = (char)(0x80 | (cp & 0x3F));
-            } else {
-                tmp[n++] = (char)(0xF0 | (cp >> 18));
-                tmp[n++] = (char)(0x80 | ((cp >> 12) & 0x3F));
-                tmp[n++] = (char)(0x80 | ((cp >> 6) & 0x3F));
-                tmp[n++] = (char)(0x80 | (cp & 0x3F));
-            }
-        }
-        tmp[n] = 0;
-        out = tmp;
-    }
-    if (!out) return;
-    int n = (int)strlen(out);
-    if (*len + n < cap) { memcpy(buf + *len, out, (size_t)n); *len += n; }
-}
-
-static void tag_attr(const char *tag, const char *name, char *out, size_t cap)
-{
-    *out = 0;
-    size_t nl = strlen(name);
-    for (const char *p = tag; *p; ++p) {
-        if (strncasecmp(p, name, nl)) continue;
-        const char *q = p + nl;
-        while (*q == ' ') q++;
-        if (*q != '=') continue;
-        q++;
-        while (*q == ' ') q++;
-        char quote = 0;
-        if (*q == '"' || *q == '\'') quote = *q++;
-        size_t j = 0;
-        while (*q && j + 1 < cap && (quote ? *q != quote : (*q != ' ' && *q != '>')))
-            out[j++] = *q++;
-        out[j] = 0;
-        return;
-    }
-}
-
-static void markup_parse(const char *s, Runs *rs, Col def_fg)
-{
-    rs->n = 0;
-    Style stack[16];
-    int sp = 0;
-    stack[0] = (Style){ def_fg, def_fg, false, false };
-
-    char buf[512];
-    int len = 0;
-
-    if (!cfg.markup) {
-        Style st = stack[0];
-        runs_push(rs, s, (int)strlen(s), &st);
-        return;
-    }
-
-    for (const char *p = s; *p; ) {
-        if (*p == '<') {
-            const char *end = strchr(p, '>');
-            if (!end) { if (len < (int)sizeof(buf) - 1) buf[len++] = *p++; continue; }
-            char tag[512];
-            size_t tl = (size_t)(end - p - 1);
-            if (tl >= sizeof(tag)) tl = sizeof(tag) - 1;
-            memcpy(tag, p + 1, tl);
-            tag[tl] = 0;
-            p = end + 1;
-
-            runs_push(rs, buf, len, &stack[sp]);
-            len = 0;
-
-            if (tag[0] == '/') {
-                if (sp > 0) sp--;
-            } else if (!strncasecmp(tag, "span", 4)) {
-                Style st = stack[sp];
-                char val[64];
-                tag_attr(tag, "foreground", val, sizeof(val));
-                if (!*val) tag_attr(tag, "fgcolor", val, sizeof(val));
-                if (!*val) tag_attr(tag, "color", val, sizeof(val));
-                if (*val) { st.fg = def_fg; st.fg.a = 255; parse_color(val, &st.fg); st.has_fg = true; }
-                tag_attr(tag, "background", val, sizeof(val));
-                if (!*val) tag_attr(tag, "bgcolor", val, sizeof(val));
-                if (*val) { st.bg.a = 255; parse_color(val, &st.bg); st.has_bg = true; }
-                if (sp < 15) stack[++sp] = st;
-            } else {
-                if (sp < 15) stack[sp + 1] = stack[sp], sp++;   /* <b>, <i>, .. */
-            }
-            continue;
-        }
-        if (*p == '&') {
-            const char *semi = strchr(p, ';');
-            if (semi && semi - p < 12) {
-                entity_append(buf, &len, (int)sizeof(buf), p + 1, (int)(semi - p - 1));
-                p = semi + 1;
-                continue;
-            }
-        }
-        if (len < (int)sizeof(buf) - 1) buf[len++] = *p;
-        p++;
-    }
-    runs_push(rs, buf, len, &stack[sp]);
-}
-
-/* ---------------------------------------------------------------- status */
-/* status_command runs under sh. Every line it prints replaces the status
- * text. If the command exits (a one-shot script), it is started again after
- * `interval` seconds — so both styles work: a script that loops forever, and
- * a script that prints once and quits. */
-
-static pid_t   status_pid = -1;
-static int     status_fd = -1;
-static char    status_line[4096] = "";
-static char    status_buf[8192];
-static size_t  status_len = 0;
-static uint32_t status_next_spawn = 0;
-
-static void status_spawn(void)
-{
-    if (!*cfg.status_command) return;
-    int fds[2];
-    if (pipe(fds) < 0) return;
-
-    pid_t pid = fork();
-    if (pid < 0) { close(fds[0]); close(fds[1]); return; }
-    if (pid == 0) {
-        close(fds[0]);
-        dup2(fds[1], STDOUT_FILENO);
-        if (fds[1] != STDOUT_FILENO) close(fds[1]);
-        setsid();
-        signal(SIGPIPE, SIG_DFL);
-        execl("/bin/sh", "sh", "-c", cfg.status_command, (char *)NULL);
-        _exit(127);
-    }
-    close(fds[1]);
-    int fl = fcntl(fds[0], F_GETFL, 0);
-    if (fl >= 0) fcntl(fds[0], F_SETFL, fl | O_NONBLOCK);
-    status_fd = fds[0];
-    status_pid = pid;
-    status_len = 0;
-}
-
-static void status_stop(void)
-{
-    if (status_pid > 0) { kill(-status_pid, SIGTERM); waitpid(status_pid, NULL, WNOHANG); }
-    if (status_fd >= 0) close(status_fd);
-    status_pid = -1;
-    status_fd = -1;
-}
-
-/* true when a new complete line arrived */
-static bool status_read(void)
-{
-    if (status_fd < 0) return false;
-    bool got = false;
-    for (;;) {
-        ssize_t r = read(status_fd, status_buf + status_len,
-                         sizeof(status_buf) - status_len - 1);
-        if (r < 0) {
-            if (errno == EINTR) continue;
-            break;                                   /* EAGAIN */
-        }
-        if (r == 0) {                                /* child closed stdout */
-            close(status_fd);
-            status_fd = -1;
-            if (status_pid > 0) { waitpid(status_pid, NULL, 0); status_pid = -1; }
-            status_next_spawn = now_ms() + (uint32_t)(cfg.interval > 0 ? cfg.interval * 1000 : 1000);
-            break;
-        }
-        status_len += (size_t)r;
-        status_buf[status_len] = 0;
-
-        char *nl;
-        while ((nl = memchr(status_buf, '\n', status_len)) != NULL) {
-            *nl = 0;
-            str_set(status_line, sizeof(status_line), status_buf);
-            got = true;
-            size_t used = (size_t)(nl - status_buf) + 1;
-            memmove(status_buf, status_buf + used, status_len - used);
-            status_len -= used;
-            status_buf[status_len] = 0;
-        }
-        if (status_len >= sizeof(status_buf) - 1) status_len = 0;   /* runaway line */
-    }
-    return got;
-}
-
-/* run a mouse binding, detached */
-static void run_command(const char *cmd)
-{
-    if (!cmd || !*cmd) return;
-    pid_t pid = fork();
-    if (pid < 0) return;
-    if (pid == 0) {
-        setsid();
-        if (fork() != 0) _exit(0);
-        execl("/bin/sh", "sh", "-c", cmd, (char *)NULL);
-        _exit(127);
-    }
-    waitpid(pid, NULL, 0);
-}
-
-
-
-
-/* ---------------------------------------------------------------- layout */
-/* One line decides what sits where:
- *
- *     bar={workspaces||cmus,volume,(clock,date)}
- *
- * "||" splits groups — one part is left only, two are left and right, three
- * are left, centre and right. "," separates items. Parentheses glue items
- * together with no separator between them. Names are cell names plus the two
- * built-ins, "workspaces" and "mode". Because it assigns position, order and
- * separators, put this line last in the config. */
-
-enum { IT_CELL, IT_WS, IT_MODE };
-enum { G_LEFT, G_CENTER, G_RIGHT, G_COUNT };
-
-typedef struct { int kind, cell, sep; } Item;
-
-static Item layout_item[G_COUNT][MAX_CELLS + 4];
-static int  layout_n[G_COUNT];
-
-static void layout_push(int g, int kind, int cell, int sep)
-{
-    if (layout_n[g] >= MAX_CELLS + 4) return;
-    Item *it = &layout_item[g][layout_n[g]++];
-    it->kind = kind;
-    it->cell = cell;
-    it->sep = sep;
-}
-
-static void layout_default(void)
-{
-    for (int g = 0; g < G_COUNT; ++g) layout_n[g] = 0;
-    layout_push(G_LEFT, IT_WS, -1, 0);
-    layout_push(G_LEFT, IT_MODE, -1, 0);
-    for (int i = 0; i < cfg.cell_count; ++i) {
-        int g = !strcmp(cfg.cell[i].pos, "left") ? G_LEFT
-              : !strcmp(cfg.cell[i].pos, "center") ? G_CENTER : G_RIGHT;
-        layout_push(g, IT_CELL, i, cfg.cell[i].sep);
-    }
-}
-
-static void layout_name(int g, char *name, bool joined)
-{
-    char *n = trim(name);
-    if (!*n) return;
-    if (!strcasecmp(n, "workspaces") || !strcasecmp(n, "ws")) {
-        layout_push(g, IT_WS, -1, joined ? 0 : 1);
-        return;
-    }
-    if (!strcasecmp(n, "mode")) { layout_push(g, IT_MODE, -1, joined ? 0 : 1); return; }
-    for (int i = 0; i < cfg.cell_count; ++i)
-        if (!strcmp(cfg.cell[i].name, n)) {
-            cfg.cell[i].sep = joined ? 0 : 1;
-            layout_push(g, IT_CELL, i, cfg.cell[i].sep);
-            return;
-        }
-    fprintf(stderr, "swbr: bar= names unknown item '%s'\n", n);
-}
-
-static void layout_parse(const char *spec)
-{
-    if (!*spec) { layout_default(); return; }
-    for (int g = 0; g < G_COUNT; ++g) layout_n[g] = 0;
-
-    char buf[1024];
-    str_set(buf, sizeof(buf), spec);
-    char *p = trim(buf);
-    if (*p == '{') p++;
-    char *end = p + strlen(p);
-    while (end > p && (end[-1] == '}' || isspace((unsigned char)end[-1]))) *--end = 0;
-
-    /* split the groups on "||" */
-    char *parts[G_COUNT] = { NULL, NULL, NULL };
-    int np = 0;
-    parts[np++] = p;
-    for (char *q = p; *q && np < G_COUNT; ++q)
-        if (q[0] == '|' && q[1] == '|') { *q = 0; parts[np++] = q + 2; }
-
-    int gmap1[1] = { G_LEFT };
-    int gmap2[2] = { G_LEFT, G_RIGHT };
-    int gmap3[3] = { G_LEFT, G_CENTER, G_RIGHT };
-    int *gmap = np == 1 ? gmap1 : (np == 2 ? gmap2 : gmap3);
-
-    for (int i = 0; i < np; ++i) {
-        int g = gmap[i];
-        char name[64];
-        int nl = 0;
-        bool joined = false;
-        for (char *q = parts[i]; ; ++q) {
-            if (*q == '(') { joined = true; nl = 0; continue; }
-            if (*q == ',' || *q == ')' || *q == 0) {
-                name[nl < (int)sizeof(name) ? nl : (int)sizeof(name) - 1] = 0;
-                bool last_of_group = (*q == ')');
-                if (nl) layout_name(g, name, joined && !last_of_group);
-                nl = 0;
-                if (*q == ')') joined = false;
-                if (*q == 0) break;
-                continue;
-            }
-            if (nl < (int)sizeof(name) - 1) name[nl++] = *q;
-        }
-    }
-}
-
-/* ---------------------------------------------------------------- message */
-/* A named pipe the rest of the system can shout into:
- *     echo "warn: backup is running" > $XDG_RUNTIME_DIR/swbr.fifo
- *     swbr --msg "error: disk full"
- *     swbr --msg clear
- * The pipe is opened read-write so it never reports end of file when the
- * writer goes away. A message can take over a cell (msg_target), which is how
- * the music title turns into the warning while one is active. */
-
-static char msg_text[512] = "";
-static int  msg_level = 0;                    /* 0 info, 1 warn, 2 error */
-static uint32_t msg_until = 0;                /* 0 = stays until cleared */
-static char msg_path[PATH_MAX] = "";
-static int  msg_fd = -1;
-static char msg_buf[1024];
-static size_t msg_len = 0;
-
-static void msg_resolve_path(void)
-{
-    if (*cfg.msg_fifo) { expand_tilde(cfg.msg_fifo, msg_path, sizeof(msg_path)); return; }
-    const char *rd = getenv("XDG_RUNTIME_DIR");
-    if (rd && *rd) snprintf(msg_path, sizeof(msg_path), "%s/swbr.fifo", rd);
-    else snprintf(msg_path, sizeof(msg_path), "/tmp/swbr-%d.fifo", (int)getuid());
-}
-
-static bool msg_active(void)
-{
-    if (!*msg_text) return false;
-    if (msg_until && (int32_t)(now_ms() - msg_until) >= 0) return false;
-    return true;
-}
-
-static Col msg_color(void)
-{
-    return msg_level >= 2 ? cfg.msg_error : (msg_level == 1 ? cfg.msg_warn : cfg.msg_info);
-}
-
-static void msg_set(const char *line)
-{
-    const char *t = line;
-    int lvl = 0;
-    if (!strncasecmp(t, "error:", 6))        { lvl = 2; t += 6; }
-    else if (!strncasecmp(t, "err:", 4))     { lvl = 2; t += 4; }
-    else if (!strncasecmp(t, "warning:", 8)) { lvl = 1; t += 8; }
-    else if (!strncasecmp(t, "warn:", 5))    { lvl = 1; t += 5; }
-    else if (!strncasecmp(t, "info:", 5))    { lvl = 0; t += 5; }
-    while (*t == ' ' || *t == '\t') t++;
-
-    if (!*t || !strcasecmp(t, "clear") || !strcasecmp(t, "-")) {
-        msg_text[0] = 0;
-        msg_until = 0;
-        return;
-    }
-    str_set(msg_text, sizeof(msg_text), t);
-    msg_level = lvl;
-    msg_until = cfg.msg_timeout > 0 ? now_ms() + (uint32_t)cfg.msg_timeout * 1000u : 0;
-}
-
-static void msg_open(void)
-{
-    msg_resolve_path();
-    struct stat st;
-    if (stat(msg_path, &st) == 0 && !S_ISFIFO(st.st_mode)) {
-        fprintf(stderr, "swbr: %s exists and is not a fifo\n", msg_path);
-        return;
-    }
-    if (mkfifo(msg_path, 0600) < 0 && errno != EEXIST) {
-        fprintf(stderr, "swbr: cannot create %s: %s\n", msg_path, strerror(errno));
-        return;
-    }
-    msg_fd = open(msg_path, O_RDWR | O_NONBLOCK | O_CLOEXEC);
-}
-
-static bool msg_read(void)
-{
-    if (msg_fd < 0) return false;
-    bool got = false;
-    for (;;) {
-        ssize_t r = read(msg_fd, msg_buf + msg_len, sizeof(msg_buf) - msg_len - 1);
-        if (r < 0) { if (errno == EINTR) continue; break; }
-        if (r == 0) break;
-        msg_len += (size_t)r;
-        msg_buf[msg_len] = 0;
-        char *nl;
-        while ((nl = memchr(msg_buf, '\n', msg_len)) != NULL) {
-            size_t used = (size_t)(nl - msg_buf) + 1;
-            *nl = 0;
-            msg_set(trim(msg_buf));
-            memmove(msg_buf, msg_buf + used, msg_len - used);
-            msg_len -= used;
-            msg_buf[msg_len] = 0;
-            got = true;
-        }
-        if (msg_len >= sizeof(msg_buf) - 1) msg_len = 0;
-    }
-    return got;
-}
-
-/* swbr --msg TEXT : hand the line to the running bar and exit */
-static int msg_send(const char *text)
-{
-    msg_resolve_path();
-    int fd = open(msg_path, O_WRONLY | O_NONBLOCK);
-    if (fd < 0) {
-        fprintf(stderr, "swbr: no bar listening on %s (%s)\n", msg_path, strerror(errno));
-        return 1;
-    }
-    char line[600];
-    snprintf(line, sizeof(line), "%s\n", text);
-    ssize_t w = write(fd, line, strlen(line));
-    close(fd);
-    return w > 0 ? 0 : 1;
-}
-
-/* ------------------------------------------------------------ cell runtime */
-/* Every cell is its own little process on its own schedule. They are read
- * without blocking, so a slow command never stalls the bar — its old value
- * just stays on screen until the new one arrives. */
-
-static void cell_spawn(Cell *e)
-{
-    if (!*e->cmd || e->pid > 0) return;
-    int fds[2];
-    if (pipe(fds) < 0) return;
-    pid_t pid = fork();
-    if (pid < 0) { close(fds[0]); close(fds[1]); return; }
-    if (pid == 0) {
-        close(fds[0]);
-        dup2(fds[1], STDOUT_FILENO);
-        if (fds[1] != STDOUT_FILENO) close(fds[1]);
-        setsid();
-        signal(SIGPIPE, SIG_DFL);
-        execl("/bin/sh", "sh", "-c", e->cmd, (char *)NULL);
-        _exit(127);
-    }
-    close(fds[1]);
-    int fl = fcntl(fds[0], F_GETFL, 0);
-    if (fl >= 0) fcntl(fds[0], F_SETFL, fl | O_NONBLOCK);
-    e->fd = fds[0];
-    e->pid = pid;
-    e->len = 0;
-}
-
-static void cell_finish(Cell *e)
-{
-    e->buf[e->len] = 0;
-    char *t = trim(e->buf);
-    for (char *q = t; *q; ++q) if (*q == '\n' || *q == '\r' || *q == '\t') *q = ' ';
-    str_set(e->out, sizeof(e->out), t);
-    e->len = 0;
-}
-
-/* true when a cell got a new value */
-static bool cells_read(void)
-{
-    bool got = false;
-    for (int i = 0; i < cfg.cell_count; ++i) {
-        Cell *e = &cfg.cell[i];
-        if (e->fd < 0) continue;
-        for (;;) {
-            ssize_t r = read(e->fd, e->buf + e->len, sizeof(e->buf) - e->len - 1);
-            if (r < 0) { if (errno == EINTR) continue; break; }
-            if (r == 0) {                                   /* command finished */
-                cell_finish(e);
-                close(e->fd);
-                e->fd = -1;
-                if (e->pid > 0) { waitpid(e->pid, NULL, 0); e->pid = -1; }
-                e->next_run = now_ms() +
-                    (uint32_t)(e->interval > 0 ? e->interval * 1000 : 1000);
-                got = true;
-                break;
-            }
-            e->len += (size_t)r;
-            e->buf[e->len] = 0;
-
-            if (e->interval < 0) {                          /* streaming */
-                char *nl;
-                while ((nl = memchr(e->buf, '\n', e->len)) != NULL) {
-                    size_t used = (size_t)(nl - e->buf) + 1;
-                    *nl = 0;
-                    str_set(e->out, sizeof(e->out), trim(e->buf));
-                    memmove(e->buf, e->buf + used, e->len - used);
-                    e->len -= used;
-                    e->buf[e->len] = 0;
-                    got = true;
-                }
-            }
-            if (e->len >= sizeof(e->buf) - 1) { cell_finish(e); got = true; }
-        }
-    }
-    return got;
-}
-
-static void cells_tick(void)
-{
-    uint32_t t = now_ms();
-    for (int i = 0; i < cfg.cell_count; ++i) {
-        Cell *e = &cfg.cell[i];
-        if (e->pid > 0 || !*e->cmd) continue;
-        if (e->interval == 0 && e->next_run) continue;       /* run once */
-        if (e->next_run && (int32_t)(t - e->next_run) < 0) continue;
-        e->next_run = t + 1;
-        cell_spawn(e);
-    }
-}
-
-static void cells_stop(void)
-{
-    for (int i = 0; i < cfg.cell_count; ++i) {
-        Cell *e = &cfg.cell[i];
-        if (e->pid > 0) { kill(-e->pid, SIGTERM); waitpid(e->pid, NULL, WNOHANG); }
-        if (e->fd >= 0) close(e->fd);
-        e->pid = -1;
-        e->fd = -1;
-    }
-}
-
-/* the text a cell draws: its output run through fmt */
-static void cell_text(Cell *e, char *out, size_t cap)
-{
-    if (!*e->fmt) { str_set(out, cap, e->out); return; }
-    const char *pc = strstr(e->fmt, "%s");
-    if (!pc) { str_set(out, cap, e->fmt); return; }
-    snprintf(out, cap, "%.*s%s%s", (int)(pc - e->fmt), e->fmt, e->out, pc + 2);
-}
-
-/* the first number in the output, ignoring markup tags */
-static bool cell_number(const char *s, float *out)
-{
-    bool in_tag = false;
-    for (const char *p = s; *p; ++p) {
-        if (*p == '<') { in_tag = true; continue; }
-        if (*p == '>') { in_tag = false; continue; }
-        if (in_tag) continue;
-        if (isdigit((unsigned char)*p) ||
-            ((*p == '-' || *p == '.') && isdigit((unsigned char)p[1]))) {
-            *out = (float)atof(p);
-            return true;
-        }
-    }
-    return false;
-}
-
-static Col cell_color(Cell *e)
-{
-    Col base = e->has_color ? e->color : cfg.text;
-    float n;
-    if ((e->has_warn || e->has_crit) && cell_number(e->out, &n)) {
-        if (e->has_warn && (e->warn_dir > 0 ? n >= e->warn_th : n <= e->warn_th))
-            base = e->warn_col;
-        if (e->has_crit && (e->crit_dir > 0 ? n >= e->crit_th : n <= e->crit_th))
-            base = e->crit_col;
-    }
-    return base;
-}
-
-static bool cell_is_msg(Cell *e)
-{
-    return *cfg.msg_target && !strcmp(cfg.msg_target, e->name) && msg_active();
-}
-
-static bool cell_visible(Cell *e)
-{
-    if (cell_is_msg(e)) return true;
-    if (!*e->cmd && !*e->fmt && !*e->empty) return false;
-    if (*e->out) return true;
-    return *e->empty || !e->hide_empty;      /* a placeholder stays clickable */
-}
-
-/* what a cell draws right now, and in which colour */
-static void cell_display(Cell *e, char *out, size_t cap, Col *col)
-{
-    if (cell_is_msg(e)) { str_set(out, cap, msg_text); *col = msg_color(); return; }
-    if (!*e->out && *e->empty) { str_set(out, cap, e->empty); *col = cell_color(e); return; }
-    cell_text(e, out, cap);
-    *col = cell_color(e);
-}
-
-/* --------------------------------------------------------------- wayland */
-
-struct zwlr_layer_shell_v1;
-
-typedef struct Bar Bar;
-
-typedef struct {
-    uint32_t id;
-    struct wl_output *wl;
-    char name[64];
-    int scale, mode_w, mode_h;
-    Bar *bar;
-    bool alive;
-} Output;
-
-struct Bar {
-    Output *out;
-    struct wl_surface *surf;
-    struct zwlr_layer_surface_v1 *ls;
-    struct wl_callback *frame;
-
-    int w, h, want_w;            /* logical surface size */
-    int scale;
-    bool configured, dirty, closed;
-
-    /* shm: one pool, two buffers */
-    struct wl_shm_pool *pool;
-    void *pool_data;
-    size_t pool_size;
-    struct wl_buffer *buf[2];
-    void *bufmem[2];
-    bool busy[2];
-    int stride, bw, bh;
-
-    /* collapse state */
-    bool collapsed;
-    float vis, anim_from, anim_to;
-    uint32_t anim_start;
-
-    /* pointer */
-    bool hovered, kbd_grab;
-    double px, py;
-
-    /* workspace hit boxes, rebuilt on every render */
-    struct { float x0, x1; char name[128]; } hit[MAX_WORKSPACES];
-    int hits;
-    struct { float x0, x1; int cell; } chit[MAX_CELLS];
-    int chits;
-};
-
-static struct wl_display    *dpy;
-static struct wl_registry   *registry;
-static struct wl_compositor *compositor;
-static struct wl_shm        *shm;
-static struct wl_seat       *seat;
-static struct wl_pointer    *pointer;
-static struct wl_keyboard   *keyboard;
-static struct zwlr_layer_shell_v1 *layer_shell;
-
-static Output outputs[MAX_OUTPUTS];
-static int    output_count = 0;
-static Bar   *pointer_bar = NULL;
-static bool   running = true;
-
-static void bar_render(Bar *b);
-static void bar_damage(Bar *b) { if (b) { b->dirty = true; } }
-
-static void damage_all(void)
-{
-    for (int i = 0; i < output_count; ++i)
-        if (outputs[i].bar) bar_damage(outputs[i].bar);
-}
-
-static bool output_wanted(const char *name)
-{
-    if (!*cfg.outputs) return true;
-    char tmp[512];
-    str_set(tmp, sizeof(tmp), cfg.outputs);
-    for (char *tok = strtok(tmp, ","); tok; tok = strtok(NULL, ",")) {
-        char *t = trim(tok);
-        if (!strcasecmp(t, name) || !strcmp(t, "*")) return true;
-    }
-    return false;
-}
-
-/* -------- sizes (logical pixels, multiplied by the output scale) -------- */
-
-static int bar_height_logical(void)
-{
-    if (cfg.height > 0) return cfg.height;
-    float base = fmaxf(cfg.text_px, cfg.ws_px) * cfg.ui_scale;
-    int h = (int)lroundf(base * 1.35f) + 4;
-    return h < 10 ? 10 : h;
-}
-
-static float bar_visible_logical(Bar *b)
-{
-    float full = (float)bar_height_logical();
-    float col = (float)clampi(cfg.collapsed_px, 1, (int)full);
-    return col + (full - col) * clampf(b->vis, 0.0f, 1.0f);
-}
-
-/* -------------------------------------------------------------- buffers */
-
-static void buffer_release(void *data, struct wl_buffer *wl_buffer)
-{
-    Bar *b = (Bar *)data;
-    for (int i = 0; i < 2; ++i)
-        if (b->buf[i] == wl_buffer) b->busy[i] = false;
-}
-static const struct wl_buffer_listener buffer_listener = { buffer_release };
-
-static void bar_free_buffers(Bar *b)
-{
-    for (int i = 0; i < 2; ++i) {
-        if (b->buf[i]) wl_buffer_destroy(b->buf[i]);
-        b->buf[i] = NULL;
-        b->busy[i] = false;
-        b->bufmem[i] = NULL;
-    }
-    if (b->pool) wl_shm_pool_destroy(b->pool);
-    b->pool = NULL;
-    if (b->pool_data) munmap(b->pool_data, b->pool_size);
-    b->pool_data = NULL;
-    b->pool_size = 0;
-}
-
-static int anon_shm(size_t size)
-{
-    int fd = memfd_create("swbr", MFD_CLOEXEC);
-    if (fd < 0) return -1;
-    if (ftruncate(fd, (off_t)size) < 0) { close(fd); return -1; }
-    return fd;
-}
-
-static bool bar_alloc(Bar *b)
-{
-    int bw = b->w * b->scale, bh = b->h * b->scale;
-    if (bw <= 0 || bh <= 0) return false;
-    if (b->pool && b->bw == bw && b->bh == bh) return true;
-
-    bar_free_buffers(b);
-    b->bw = bw;
-    b->bh = bh;
-    b->stride = bw * 4;
-    size_t one = (size_t)b->stride * (size_t)bh;
-    size_t total = one * 2;
-
-    int fd = anon_shm(total);
-    if (fd < 0) return false;
-    void *mem = mmap(NULL, total, PROT_READ | PROT_WRITE, MAP_SHARED, fd, 0);
-    if (mem == MAP_FAILED) { close(fd); return false; }
-
-    b->pool_data = mem;
-    b->pool_size = total;
-    b->pool = wl_shm_create_pool(shm, fd, (int32_t)total);
-    close(fd);
-    for (int i = 0; i < 2; ++i) {
-        b->buf[i] = wl_shm_pool_create_buffer(b->pool, (int32_t)(one * (size_t)i),
-                                              bw, bh, b->stride, WL_SHM_FORMAT_ARGB8888);
-        b->bufmem[i] = (char *)mem + one * (size_t)i;
-        b->busy[i] = false;
-        wl_buffer_add_listener(b->buf[i], &buffer_listener, b);
-    }
-    return true;
-}
-
-/* ---------------------------------------------------------------- render */
-
-/* Baseline that puts the ink optically centred in a band: the em box is
- * centred, then nudged by text_y (logical px, negative = up). */
-static float baseline_for(Font *f, float y0, float vis, float s)
-{
-    return y0 + vis * 0.5f + (f->ascent - f->lineh * 0.5f) + cfg.text_y * s;
-}
-
-static Col col_scale_alpha(Col c, float f)
-{
-    c.a = (uint8_t)clampi((int)lroundf((float)c.a * clampf(f, 0.0f, 1.0f)), 0, 255);
-    return c;
-}
-
-static bool ws_on_bar(Bar *b, Ws *w)
-{
-    if (!b->out || !*b->out->name || !*w->output) return true;
-    return strcmp(w->output, b->out->name) == 0;
-}
-
-/* --------------------------------------------------------------- slim mode
- * Folded, the bar is a few pixels tall and still has to say something. Each
- * workspace keeps a fixed slot so its position never moves, the clock turns
- * into twelve dots, and anything with a percentage becomes a small gauge. */
-
-static Ws *ws_by_num(Bar *b, int num)
-{
-    for (int i = 0; i < ws_count; ++i)
-        if (ws_list[i].num == num && ws_on_bar(b, &ws_list[i])) return &ws_list[i];
-    return NULL;
-}
-
-static int slim_mode(Cell *e)
-{
-    if (e->slim != SLIM_AUTO) return e->slim;
-    if (!*e->out) return SLIM_TICK;              /* a placeholder is a button */
-    if (strchr(e->out, '%')) return SLIM_BAR;
-    float v;
-    if ((e->has_warn || e->has_crit) && cell_number(e->out, &v)) return SLIM_BAR;
-    return SLIM_TICK;
-}
-
-static float slim_width_of(Cell *e, int mode, float s)
-{
-    if (e && e->slim_w > 0) return (float)e->slim_w * s;
-    switch (mode) {
-    case SLIM_CLOCK: return 12.0f * 3.0f * s + 11.0f * 2.0f * s;
-    case SLIM_BAR:   return 40.0f * s;
-    case SLIM_PRESENCE: return 10.0f * s;
-    case SLIM_OFF:   return 0.0f;
-    default:         return 12.0f * s;
-    }
-}
-
-
-static void slim_draw_one(Canvas *cv, Cell *e, int mode, float x, float y,
-                          float w, float h, float s)
-{
-    Col c = e->has_slim_color ? e->slim_color : cell_color(e);
-    if (mode == SLIM_PRESENCE) {
-        /* the cell only exists while its program does, so a block here means
-         * "that one is up" — its own colour, not shared with any gauge */
-        if (!e->has_slim_color) c = cfg.running;
-        fill_rect(cv, x, y, w, h, c);
-        return;
-    }
-    if (mode == SLIM_CLOCK) {
-        time_t t = time(NULL);
-        struct tm tmv;
-        localtime_r(&t, &tmv);
-        int hour = tmv.tm_hour % 12;
-        float dot = 3.0f * s, gap = 2.0f * s;
-        for (int i = 0; i < 12; ++i) {
-            Col dc = cfg.dim;
-            float dw = dot;
-            if (i < hour) dc = cfg.accent;
-            else if (i == hour) { dc = cfg.hl; dw = dot * (0.35f + 0.65f * (float)tmv.tm_min / 59.0f); }
-            fill_rect(cv, x + (float)i * (dot + gap), y, dw, h, dc);
-        }
-        return;
-    }
-    if (mode == SLIM_BAR) {
-        float v = 0;
-        bool have = cell_number(e->out, &v);
-        float lo = e->slim_min, hi = e->slim_max;
-        float f = (hi > lo) ? (v - lo) / (hi - lo) : 0.0f;
-        f = clampf(f, 0.0f, 1.0f);
-        Col track = cfg.dim;                       /* visible, or a part filled
-                                                    * gauge just looks short */
-        track.a = 110;
-        fill_rect(cv, x, y, w, h, track);
-        if (have) fill_rect(cv, x, y, w * f, h, c);
-        return;
-    }
-    fill_rect(cv, x, y, w, h, c);
-}
-
-static void draw_signals(Canvas *cv, Bar *b, float y, float h, float s, Runs *rs)
-{
-    if (!cfg.signals) return;
-    float gap = 3.0f * s;
-    float x = 4.0f * s;
-
-    if (msg_active() && cfg.msg_flash) {          /* an error is worth the whole strip */
-        fill_rect(cv, x, y, (float)cv->w - 8.0f * s, h, msg_color());
-        return;
-    }
-
-    /* one fixed slot per workspace number, so slot 3 is always workspace 3 */
-    int slots = 0;
-    for (int i = 0; i < ws_count; ++i)
-        if (ws_on_bar(b, &ws_list[i]) && ws_list[i].num > slots) slots = ws_list[i].num;
-    if (slots <= 0)
-        for (int i = 0; i < ws_count; ++i) if (ws_on_bar(b, &ws_list[i])) slots++;
-    if (cfg.slim_ws_slots > 0 && slots > cfg.slim_ws_slots) slots = cfg.slim_ws_slots;
-
-    float tick = 14.0f * s;
-    b->hits = 0;
-    b->chits = 0;
-    for (int i = 1; i <= slots; ++i) {
-        Ws *w = ws_by_num(b, i);
-        Col c = cfg.dim;
-        c.a = 60;                                  /* empty slot: a faint place holder */
-        if (w) {
-            if (w->urgent) c = cfg.urgent;
-            else if (w->focused) c = cfg.hl;
-            else if (w->visible) c = cfg.accent;
-            else { c = cfg.dim; c.a = 170; }
-        }
-        fill_rect(cv, x, y, tick, h, c);
-        if (w && b->hits < MAX_WORKSPACES) {      /* still switchable when folded */
-            b->hit[b->hits].x0 = x / s;
-            b->hit[b->hits].x1 = (x + tick) / s;
-            str_set(b->hit[b->hits].name, sizeof(b->hit[b->hits].name), w->name);
-            b->hits++;
-        }
-        x += tick + gap;
-    }
-
-    if (!cfg.cell_count) {                         /* status_command fallback */
-        float rx = (float)cv->w - 4.0f * s;
-        for (int i = rs->n - 1; i >= 0 && rx > x; --i) {
-            if (!rs->v[i].has_fg) continue;
-            float w = clampf((float)strlen(rs->v[i].text) * 2.0f * s, 6.0f * s, 40.0f * s);
-            rx -= w;
-            fill_rect(cv, rx, y, w, h, rs->v[i].fg);
-            rx -= gap;
-        }
-        return;
-    }
-
-    /* the right hand group, in layout order */
-    int idx[MAX_CELLS], mode[MAX_CELLS], n = 0;
-    bool join[MAX_CELLS];
-    float total = 0;
-    for (int i = 0; i < layout_n[G_RIGHT] && n < MAX_CELLS; ++i) {
-        Item *it = &layout_item[G_RIGHT][i];
-        if (it->kind != IT_CELL) continue;
-        Cell *e = &cfg.cell[it->cell];
-        if (!cell_visible(e)) continue;
-        int m = slim_mode(e);
-        if (m == SLIM_OFF) continue;
-        idx[n] = it->cell;
-        mode[n] = m;
-        join[n] = (n > 0 && !layout_item[G_RIGHT][i - 1].sep);
-        total += slim_width_of(e, m, s) + (n ? (join[n] ? 1.0f * s : gap) : 0);
-        n++;
-    }
-    float rx = (float)cv->w - 4.0f * s - total;
-    if (rx < x + gap) rx = x + gap;
-    for (int k = 0; k < n; ++k) {
-        float w = slim_width_of(&cfg.cell[idx[k]], mode[k], s);
-        if (k) rx += join[k] ? 1.0f * s : gap;
-        if (rx + w > (float)cv->w - 3.0f * s) break;
-        slim_draw_one(cv, &cfg.cell[idx[k]], mode[k], rx, y, w, h, s);
-        if (b->chits < MAX_CELLS) {               /* buttons keep working */
-            b->chit[b->chits].x0 = rx / s;
-            b->chit[b->chits].x1 = (rx + w) / s;
-            b->chit[b->chits].cell = idx[k];
-            b->chits++;
-        }
-        rx += w;
-    }
-}
-/* --------------------------------------------------------------- workspaces
- * The buttons are a placeable item like any cell, so they are measured and
- * drawn through the same two-pass code. With ws_inset > 0 they float inside
- * the bar as their own pills instead of being as tall as the bar itself. */
-
-static float ws_button_w(Font *f, Ws *w, float s)
-{
-    return fmaxf(text_width(f, w->label) + 2.0f * (float)cfg.ws_pad * s,
-                 (float)cfg.ws_min_w * s);
-}
-
-static float ws_block_w(Font *f, Bar *b, float s)
-{
-    float w = 0;
-    int n = 0;
-    for (int i = 0; i < ws_count; ++i) {
-        if (!ws_on_bar(b, &ws_list[i])) continue;
-        w += ws_button_w(f, &ws_list[i], s);
-        n++;
-    }
-    if (n > 1) w += (float)(n - 1) * (float)cfg.ws_gap * s;
-    return w;
-}
-
-/* one pill, square on the screen edge only when it is flush with it */
-static void pill(Canvas *cv, float x, float y0, float vis, float w, float s,
-                 float rad, Col c)
-{
-    float in = (float)cfg.ws_inset * s;
-    bool top = strcmp(cfg.position, "bottom") != 0;
-    float y = y0 + in, h = vis - 2.0f * in;
-    if (in <= 0) {
-        if (top) fill_round(cv, x, y0, w, vis, 0, 0, rad, rad, c);
-        else     fill_round(cv, x, y0, w, vis, rad, rad, 0, 0, c);
-        return;
-    }
-    fill_round(cv, x, y, w, h, rad, rad, rad, rad, c);
-}
-
-static float ws_radius_px(float vis, float s)
-{
-    if (cfg.ws_radius >= 0) return cfg.ws_radius * s;
-    if (cfg.ws_inset > 0) return vis;            /* capped to a full pill */
-    return cfg.radius * s;
-}
-
-static void ws_block_draw(Canvas *cv, Bar *b, Font *f, float x, float y0,
-                          float vis, float s, float fade)
-{
-    float rad = ws_radius_px(vis, s);
-    float base = baseline_for(f, y0, vis, s);
-    for (int i = 0; i < ws_count; ++i) {
-        Ws *w = &ws_list[i];
-        if (!ws_on_bar(b, w)) continue;
-        float bwid = ws_button_w(f, w, s);
-        float tw = text_width(f, w->label);
-
-        Col bg = cfg.ws_bg, fg = cfg.ws_fg;
-        if (w->urgent)       { bg = cfg.ws_urgent_bg;  fg = cfg.ws_urgent_fg;  }
-        else if (w->focused) { bg = cfg.ws_focused_bg; fg = cfg.ws_focused_fg; }
-        else if (w->visible) { bg = cfg.ws_visible_bg; fg = cfg.ws_visible_fg; }
-
-        pill(cv, x, y0, vis, bwid, s, rad, col_scale_alpha(bg, fade));
-        text_draw(cv, f, x + (bwid - tw) * 0.5f, base, col_scale_alpha(fg, fade), w->label);
-
-        if (b->hits < MAX_WORKSPACES) {
-            b->hit[b->hits].x0 = x / s;
-            b->hit[b->hits].x1 = (x + bwid) / s;
-            str_set(b->hit[b->hits].name, sizeof(b->hit[b->hits].name), w->name);
-            b->hits++;
-        }
-        x += bwid + (float)cfg.ws_gap * s;
-    }
-}
-
-static bool mode_shown(void) { return cfg.mode_show && strcmp(sway_mode, "default"); }
-
-static float mode_w(Font *f, float s)
-{
-    if (!mode_shown()) return 0;
-    return text_width(f, sway_mode) + 2.0f * (float)cfg.ws_pad * s;
-}
-
-static void mode_draw(Canvas *cv, Font *f, float x, float y0, float vis, float s, float fade)
-{
-    if (!mode_shown()) return;
-    pill(cv, x, y0, vis, mode_w(f, s), s, ws_radius_px(vis, s),
-         col_scale_alpha(cfg.mode_bg, fade));
-    text_draw(cv, f, x + (float)cfg.ws_pad * s, baseline_for(f, y0, vis, s),
-              col_scale_alpha(cfg.mode_fg, fade), sway_mode);
-}
-
-/* ------------------------------------------------------------- text cells */
-/* A cell with scroll=1 never pushes the layout around: too long and it ends
- * in "..", and it scrolls through the full text while the pointer is on it. */
-
-static bool scroll_running = false;
-
-static void runs_truncate(Font *f, Runs *rs, float maxw, float ellw)
-{
-    float w = 0;
-    for (int i = 0; i < rs->n; ++i) {
-        char *txt = rs->v[i].text;
-        const char *p = txt;
-        while (*p) {
-            const char *prev = p;
-            unsigned cp = utf8_next(&p);
-            float aw = glyph_get(f, cp)->adv;
-            if (w + aw > maxw - ellw) {
-                size_t keep = (size_t)(prev - txt);
-                txt[keep] = 0;
-                if (keep + 3 < sizeof(rs->v[i].text)) strcat(txt, "..");
-                rs->n = i + 1;
-                return;
-            }
-            w += aw;
-        }
-    }
-}
-
-/* how far a hovered cell has scrolled: pause, glide, pause, jump back */
-static float scroll_offset(Cell *e, float over, float s)
-{
-    uint32_t t = now_ms();
-    if (!e->scroll_t) e->scroll_t = t;
-    float speed = fmaxf((float)cfg.scroll_speed, 1.0f) * s;
-    float pause = fmaxf((float)cfg.scroll_pause, 0.0f) / 1000.0f;
-    float dur = over / speed;
-    float cycle = pause + dur + pause;
-    float el = (float)(t - e->scroll_t) / 1000.0f;
-    float ph = cycle > 0 ? fmodf(el, cycle) : 0;
-    scroll_running = true;
-    if (ph < pause) return 0;
-    if (ph < pause + dur) return (ph - pause) / dur * over;
-    return over;
-}
-
-/* ------------------------------------------------------------------ groups */
-
-/* space after an item: the cell may override the global cell_gap, which is
- * how two cells are pushed together into one visible box */
-static float item_gap(Item *it, float s)
-{
-    int g = cfg.cell_gap;
-    if (it->kind == IT_CELL && cfg.cell[it->cell].gap >= 0) g = cfg.cell[it->cell].gap;
-    return (float)g * s;
-}
-
-static float item_width(Canvas *cv, Bar *b, Font *f, Item *it, float s,
-                        char *txt, size_t cap, Col *col, float *tw)
-{
-    (void)cv;
-    if (it->kind == IT_WS)   { *tw = 0; return ws_block_w(f, b, s); }
-    if (it->kind == IT_MODE) { *tw = 0; return mode_w(f, s); }
-
-    Cell *e = &cfg.cell[it->cell];
-    cell_display(e, txt, cap, col);
-    Runs rs;
-    int saved = cfg.markup;
-    cfg.markup = e->markup;
-    markup_parse(txt, &rs, *col);
-    cfg.markup = saved;
-    float w = 0;
-    for (int k = 0; k < rs.n; ++k) w += text_width(f, rs.v[k].text);
-    *tw = w;
-    float pad = 2.0f * (float)e->pad * s;
-    float box = fmaxf(w + pad, (float)e->min_w * s);
-    if (e->max_w > 0) box = fminf(box, (float)e->max_w * s);
-    if (e->scroll && e->min_w > 0) box = (float)e->min_w * s;   /* fixed slot */
-    return box;
-}
-
-/* Draws one group. Returns the left edge it occupies, so the next group in
- * from the right knows where it must stop. With measure = true nothing is
- * painted and the group's total width comes back instead — that is how the
- * left group reserves its space before the right one is placed. */
-static float draw_group_(Canvas *cv, Bar *b, Font *f, int g, float y0, float vis,
-                         float s, float fade, float left_limit, float right,
-                         bool measure)
-{
-    static char txt[MAX_CELLS + 4][1024];
-    float bw[MAX_CELLS + 4], tw[MAX_CELLS + 4];
-    Col col[MAX_CELLS + 4];
-    int idx[MAX_CELLS + 4], n = 0;
-
-    float sep_w = *cfg.separator ? text_width(f, cfg.separator) : 0.0f;
-    float total = 0;
-
-    for (int i = 0; i < layout_n[g]; ++i) {
-        Item *it = &layout_item[g][i];
-        if (it->kind == IT_CELL && !cell_visible(&cfg.cell[it->cell])) continue;
-        float w = item_width(cv, b, f, it, s, txt[n], sizeof(txt[n]), &col[n], &tw[n]);
-        if (w <= 0) continue;
-        bw[n] = w;
-        idx[n] = i;
-        total += w;
-        n++;
-    }
-    if (!n) return measure ? 0.0f : right;
-    for (int k = 0; k + 1 < n; ++k) {
-        Item *it = &layout_item[g][idx[k]];
-        float ig = item_gap(it, s);
-        total += ig + (it->sep && sep_w > 0 ? ig + sep_w : 0.0f);
-    }
-
-    /* too wide: text cells give up their width first, that is what they are
-     * for. Everything else keeps its size and the group clips at the edge. */
-    float avail = right - left_limit;
-    if (total > avail) {
-        float excess = total - avail;
-        float floor_w = 60.0f * s;
-        for (int k = 0; k < n && excess > 0.5f; ++k) {
-            Item *it = &layout_item[g][idx[k]];
-            if (it->kind != IT_CELL || !cfg.cell[it->cell].scroll) continue;
-            float can = bw[k] - floor_w;
-            if (can <= 0) continue;
-            float take = fminf(can, excess);
-            bw[k] -= take;
-            total -= take;
-            excess -= take;
-        }
-    }
-    if (measure) return total;
-
-    float x;
-    if (g == G_LEFT)        x = left_limit;
-    else if (g == G_CENTER) x = ((float)cv->w - total) * 0.5f;
-    else                    x = right - total;
-    if (x + total > right) x = right - total;
-    if (x < left_limit) x = left_limit;
-    float origin = x;
-    float base = baseline_for(f, y0, vis, s);
-
-    /* where every item lands, so joined backgrounds can be merged into one
-     * box instead of two rounded ones meeting in a notch */
-    float xs[MAX_CELLS + 4];
-    {
-        float cx = x;
-        for (int k = 0; k < n; ++k) {
-            Item *it = &layout_item[g][idx[k]];
-            xs[k] = cx;
-            cx += bw[k];
-            if (k + 1 < n) {
-                float ig = item_gap(it, s);
-                cx += ig + (it->sep && sep_w > 0 ? sep_w + ig : 0.0f);
-            }
-        }
-    }
-    for (int k = 0; k < n; ) {
-        Item *it = &layout_item[g][idx[k]];
-        if (it->kind != IT_CELL || !cfg.cell[it->cell].has_bg ||
-            cell_is_msg(&cfg.cell[it->cell])) { k++; continue; }
-        Col bgc = cfg.cell[it->cell].bg;
-        int e2 = k;
-        while (e2 + 1 < n) {                       /* extend over glued cells */
-            Item *cur = &layout_item[g][idx[e2]];
-            Item *nx = &layout_item[g][idx[e2 + 1]];
-            if (cur->sep || item_gap(cur, s) > 0.5f) break;
-            if (nx->kind != IT_CELL || !cfg.cell[nx->cell].has_bg) break;
-            Col nc = cfg.cell[nx->cell].bg;
-            if (nc.r != bgc.r || nc.g != bgc.g || nc.b != bgc.b || nc.a != bgc.a) break;
-            e2++;
-        }
-        float in = (float)cfg.cell_inset * s;
-        float r = cfg.cell_radius * s;
-        fill_round(cv, xs[k], y0 + in, xs[e2] + bw[e2] - xs[k], vis - 2.0f * in,
-                   r, r, r, r, col_scale_alpha(bgc, fade));
-        k = e2 + 1;
-    }
-
-    for (int k = 0; k < n; ++k) {
-        Item *it = &layout_item[g][idx[k]];
-        x = xs[k];
-
-        if (it->kind == IT_WS) {
-            ws_block_draw(cv, b, f, x, y0, vis, s, fade);
-        } else if (it->kind == IT_MODE) {
-            mode_draw(cv, f, x, y0, vis, s, fade);
-        } else {
-            Cell *e = &cfg.cell[it->cell];
-            bool hover = b->hovered && b->px * s >= x && b->px * s < x + bw[k];
-
-            Runs rs;
-            int saved = cfg.markup;
-            cfg.markup = e->markup;
-            markup_parse(txt[k], &rs, col[k]);
-            cfg.markup = saved;
-
-            float pad = (float)e->pad * s;
-            float inner = bw[k] - 2.0f * pad;
-            float tx = x + pad, over = tw[k] - inner;
-            if (over > 0 && e->scroll) {
-                if (hover) tx -= scroll_offset(e, over, s);
-                else { e->scroll_t = 0; runs_truncate(f, &rs, inner, text_width(f, "..")); }
-            } else if (over <= 0) {
-                if (!strcmp(e->align, "right")) tx = x + bw[k] - pad - tw[k];
-                else if (!strcmp(e->align, "center")) tx = x + (bw[k] - tw[k]) * 0.5f;
-            }
-
-            int ox0 = cv->cx0, ox1 = cv->cx1;
-            canvas_clip(cv, (int)(x + pad), cv->cy0, (int)ceilf(x + bw[k] - pad), cv->cy1);
-            for (int r = 0; r < rs.n; ++r) {
-                float rw = text_width(f, rs.v[r].text);
-                if (rs.v[r].has_bg)
-                    fill_rect(cv, tx, y0 + 2.0f * s, rw, vis - 4.0f * s,
-                              col_scale_alpha(rs.v[r].bg, fade));
-                text_draw(cv, f, tx, base, col_scale_alpha(rs.v[r].fg, fade), rs.v[r].text);
-                tx += rw;
-            }
-            canvas_clip(cv, ox0, cv->cy0, ox1, cv->cy1);
-
-            if (b->chits < MAX_CELLS) {
-                b->chit[b->chits].x0 = x / s;
-                b->chit[b->chits].x1 = (x + bw[k]) / s;
-                b->chit[b->chits].cell = it->cell;
-                b->chits++;
-            }
-        }
-
-        x += bw[k];
-        if (k + 1 < n) {
-            float ig = item_gap(it, s);
-            x += ig;
-            if (it->sep && sep_w > 0) {
-                text_draw(cv, f, x, base, col_scale_alpha(cfg.separator_color, fade),
-                          cfg.separator);
-                x += sep_w + ig;
-            }
-        }
-    }
-    return origin;
-}
-
-/* A message with no cell to live in is drawn centred on its own. */
-static float draw_message(Canvas *cv, Bar *b, Font *f, float y0, float vis,
-                          float s, float fade, float left_limit, float right)
-{
-    if (!msg_active()) return right;
-    for (int i = 0; i < cfg.cell_count; ++i)      /* a cell already shows it */
-        if (cell_is_msg(&cfg.cell[i])) return right;
-
-    Runs rs;
-    markup_parse(msg_text, &rs, msg_color());
-    float w = 0;
-    for (int i = 0; i < rs.n; ++i) w += text_width(f, rs.v[i].text);
-    float x = ((float)cv->w - w) * 0.5f;
-    if (x + w > right) x = right - w;
-    if (x < left_limit) x = left_limit;
-    float origin = x;
-    float base = baseline_for(f, y0, vis, s);
-    int ox0 = cv->cx0, ox1 = cv->cx1;
-    canvas_clip(cv, (int)x, cv->cy0, (int)ceilf(x + w), cv->cy1);
-    for (int i = 0; i < rs.n; ++i) {
-        float rw = text_width(f, rs.v[i].text);
-        text_draw(cv, f, x, base, col_scale_alpha(rs.v[i].fg, fade), rs.v[i].text);
-        x += rw;
-    }
-    canvas_clip(cv, ox0, cv->cy0, ox1, cv->cy1);
-    (void)b;
-    return origin;
-}
-
-static float draw_group(Canvas *cv, Bar *b, Font *f, int g, float y0, float vis,
-                        float s, float fade, float left_limit, float right)
-{
-    return draw_group_(cv, b, f, g, y0, vis, s, fade, left_limit, right, false);
-}
-
-static float group_width(Canvas *cv, Bar *b, Font *f, int g, float vis, float s)
-{
-    return draw_group_(cv, b, f, g, 0, vis, s, 1.0f, 0, (float)cv->w, true);
-}
-
-/* what the group wants, with nothing squeezed */
-static float group_natural(Canvas *cv, Bar *b, Font *f, int g, float vis, float s)
-{
-    return draw_group_(cv, b, f, g, 0, vis, s, 1.0f, 0, 1.0e6f, true);
-}
-
-/* The bar sizes itself: as wide as its content needs, never under min_width,
- * never wider than the output. Cells come and go (charging, docker, a longer
- * clock), so this is checked on every paint and the layer surface is only
- * resized when it actually changed. */
-static int bar_natural_width(Bar *b, Canvas *cv, Font *f, float vis, float s)
-{
-    float lw = group_natural(cv, b, f, G_LEFT, vis, s);
-    float cw = group_natural(cv, b, f, G_CENTER, vis, s);
-    float rw = group_natural(cv, b, f, G_RIGHT, vis, s);
-    float gap = (float)cfg.cell_gap * s;
-    int groups = (lw > 0) + (cw > 0) + (rw > 0);
-    float total = lw + cw + rw;
-    if (groups > 1) total += (float)(groups - 1) * gap * 2.0f;
-    total += (float)cfg.pad_x * s + (float)(cfg.pad_right >= 0 ? cfg.pad_right : cfg.pad_x) * s;
-
-    int want = (int)ceilf(total / s);
-    if (want < cfg.min_width) want = cfg.min_width;
-
-    int limit = 0;
-    if (b->out && b->out->mode_w > 0)
-        limit = b->out->mode_w / (b->out->scale > 0 ? b->out->scale : 1) - 2 * cfg.side_margin;
-    if (limit > 0 && want > limit) want = limit;
-    return want;
-}
-
-static void bar_fit_width(Bar *b, Canvas *cv, Font *f, float vis, float s)
-{
-    if (cfg.min_width <= 0 || !b->ls) return;
-    int want = bar_natural_width(b, cv, f, vis, s);
-    if (want != b->want_w && abs(want - b->w) >= 2) {
-        b->want_w = want;
-        ls_set_size(b->ls, (uint32_t)want, (uint32_t)b->h);
-        wl_surface_commit(b->surf);
-    }
-}
-
-/* Everything the bar paints, into a plain canvas. Split out of bar_render so
- * it can be exercised without a compositor. */
-static void bar_paint(Bar *b, Canvas *cv, float visL, float s)
-{
-    bool top = strcmp(cfg.position, "bottom") != 0;
-    float vis = visL * s;
-    float y0 = top ? 0.0f : (float)cv->h - vis;
-    float rad = cfg.radius * s;
-    float bw = (float)cv->w;
-
-    memset(cv->px, 0, (size_t)cv->w * (size_t)cv->h * 4);
-    canvas_clip_all(cv);
-
-    /* flush with the screen edge: the two corners that touch it stay square */
-    if (top) fill_round(cv, 0, y0, bw, vis, 0, 0, rad, rad, cfg.bg);
-    else     fill_round(cv, 0, y0, bw, vis, rad, rad, 0, 0, cfg.bg);
-
-    Runs rs;
-    markup_parse(status_line, &rs, cfg.text);
-
-    float fade = clampf(b->vis * 1.6f - 0.25f, 0.0f, 1.0f);
-    b->hits = 0;
-    b->chits = 0;
-    if (fade <= 0.01f) {
-        draw_signals(cv, b, y0, vis, s, &rs);
-        return;
-    }
-
-    canvas_clip(cv, 0, (int)floorf(y0), (int)bw, (int)ceilf(y0 + vis));
-
-    Font *f = font_get((int)lroundf(cfg.text_px * cfg.ui_scale * s));
-    bar_fit_width(b, cv, f, vis, s);
-    float gap = (float)cfg.cell_gap * s;
-    float left_limit = (float)cfg.pad_x * s;
-    float right = bw - (float)(cfg.pad_right >= 0 ? cfg.pad_right : cfg.pad_x) * s;
-
-    if (cfg.cell_count || layout_n[G_LEFT]) {
-        float lw = group_width(cv, b, f, G_LEFT, vis, s);
-        float after_left = left_limit + (lw > 0 ? lw + gap : 0.0f);
-        float rx = draw_group(cv, b, f, G_RIGHT, y0, vis, s, fade, after_left, right);
-        float mx = draw_message(cv, b, f, y0, vis, s, fade, after_left, rx - gap);
-        float cx = draw_group(cv, b, f, G_CENTER, y0, vis, s, fade, after_left,
-                              fminf(rx, mx) - gap);
-        draw_group(cv, b, f, G_LEFT, y0, vis, s, fade, left_limit, cx - gap);
-    }
-
-    if (!cfg.cell_count && *status_line) {         /* status_command fallback */
-        float total = 0;
-        for (int i = 0; i < rs.n; ++i) total += text_width(f, rs.v[i].text);
-        float sx = right - total;
-        float base = baseline_for(f, y0, vis, s);
-        for (int i = 0; i < rs.n; ++i) {
-            float rw = text_width(f, rs.v[i].text);
-            text_draw(cv, f, sx, base, col_scale_alpha(rs.v[i].fg, fade), rs.v[i].text);
-            sx += rw;
-        }
-    }
-    canvas_clip_all(cv);
-}
-
-static void bar_render(Bar *b)
-{
-    if (!b->configured || b->closed) return;
-    if (!bar_alloc(b)) return;
-
-    int slot = -1;
-    for (int i = 0; i < 2; ++i) if (!b->busy[i]) { slot = i; break; }
-    if (slot < 0) return;                     /* both in flight: next frame */
-
-    float s = (float)b->scale;
-    float visL = bar_visible_logical(b);
-    bool top = strcmp(cfg.position, "bottom") != 0;
-    Canvas cv = { (uint32_t *)b->bufmem[slot], b->bw, b->bh, 0, 0, b->bw, b->bh };
-    bar_paint(b, &cv, visL, s);
-
-    /* hand the buffer over */
-    b->busy[slot] = true;
-    wl_surface_set_buffer_scale(b->surf, b->scale);
-    wl_surface_attach(b->surf, b->buf[slot], 0, 0);
-    wl_surface_damage_buffer(b->surf, 0, 0, b->bw, b->bh);
-
-    /* only the visible strip takes clicks — the rest of the surface is
-     * transparent and must not swallow them */
-    struct wl_region *reg = wl_compositor_create_region(compositor);
-    int rv = (int)ceilf(visL);
-    wl_region_add(reg, 0, top ? 0 : b->h - rv, b->w, rv);
-    wl_surface_set_input_region(b->surf, reg);
-    wl_region_destroy(reg);
-
-    if (cfg.exclusive)
-        ls_set_exclusive_zone(b->ls, rv + cfg.margin);
-
-    wl_surface_commit(b->surf);
-    b->dirty = false;
-}
-/* ------------------------------------------------------------- animation */
-
-static void bar_set_collapsed(Bar *b, bool c)
-{
-    b->collapsed = c;
-    b->anim_from = b->vis;
-    b->anim_to = c ? 0.0f : 1.0f;
-    b->anim_start = now_ms();
-    if (cfg.anim_ms <= 0) b->vis = b->anim_to;
-    b->dirty = true;
-}
-
-static bool bar_animating(Bar *b) { return b->vis != b->anim_to; }
-
-static void bar_anim_step(Bar *b, uint32_t t)
-{
-    if (!bar_animating(b)) return;
-    if (cfg.anim_ms <= 0) { b->vis = b->anim_to; b->dirty = true; return; }
-    float k = (float)(t - b->anim_start) / (float)cfg.anim_ms;
-    if (k >= 1.0f) b->vis = b->anim_to;
-    else b->vis = b->anim_from + (b->anim_to - b->anim_from) * (k * k * (3.0f - 2.0f * k));
-    b->dirty = true;
-}
-
-static void bar_set_grab(Bar *b, bool on)
-{
-    if (!cfg.hover_keys || b->kbd_grab == on || !b->ls) return;
-    b->kbd_grab = on;
-    ls_set_keyboard(b->ls, on ? ZWLR_LAYER_SURFACE_V1_KEYBOARD_INTERACTIVITY_EXCLUSIVE
-                              : ZWLR_LAYER_SURFACE_V1_KEYBOARD_INTERACTIVITY_NONE);
-    wl_surface_commit(b->surf);
-}
-
-/* ---------------------------------------------------------- layer surface */
-
-static void ls_configure(void *data, struct zwlr_layer_surface_v1 *s,
-                         uint32_t serial, uint32_t w, uint32_t h)
-{
-    Bar *b = (Bar *)data;
-    ls_ack_configure(s, serial);
-    if (w) b->w = (int)w;
-    if (h) b->h = (int)h;
-    b->configured = true;
-    b->dirty = true;
-    bar_render(b);
-}
-
-static void ls_closed(void *data, struct zwlr_layer_surface_v1 *s)
-{
-    (void)s;
-    Bar *b = (Bar *)data;
-    b->closed = true;
-    b->configured = false;
-}
-
-static const struct zwlr_layer_surface_v1_listener ls_listener = { ls_configure, ls_closed };
-
-static void bar_create(Output *o)
-{
-    if (o->bar || !layer_shell || !compositor || !shm) return;
-    if (!output_wanted(o->name)) return;
-
-    Bar *b = (Bar *)xmalloc(sizeof(Bar));
-    memset(b, 0, sizeof(*b));
-    b->out = o;
-    b->scale = o->scale > 0 ? o->scale : 1;
-    b->w = 0;
-    b->h = bar_height_logical();
-    b->vis = cfg.start_collapsed ? 0.0f : 1.0f;
-    b->anim_to = b->vis;
-    b->collapsed = cfg.start_collapsed != 0;
-
-    bool top = strcmp(cfg.position, "bottom") != 0;
-    uint32_t layer = ZWLR_LAYER_SHELL_V1_LAYER_TOP;
-    if (!strcmp(cfg.layer, "overlay")) layer = ZWLR_LAYER_SHELL_V1_LAYER_OVERLAY;
-    else if (!strcmp(cfg.layer, "bottom")) layer = ZWLR_LAYER_SHELL_V1_LAYER_BOTTOM;
-    else if (!strcmp(cfg.layer, "background")) layer = ZWLR_LAYER_SHELL_V1_LAYER_BACKGROUND;
-
-    b->surf = wl_compositor_create_surface(compositor);
-    b->ls = zwlr_layer_shell_v1_get_layer_surface(layer_shell, b->surf, o->wl, layer, APP_ID);
-    zwlr_layer_surface_v1_add_listener(b->ls, &ls_listener, b);
-
-    uint32_t anchor = top ? ZWLR_LAYER_SURFACE_V1_ANCHOR_TOP
-                          : ZWLR_LAYER_SURFACE_V1_ANCHOR_BOTTOM;
-    if (cfg.min_width > 0) {
-        /* a floating pill: anchored to one edge only, so the compositor
-         * centres it — unless align_x pins it to a side. The width is not
-         * fixed, bar_render grows it to whatever the content needs. */
-        if (!strcmp(cfg.align_x, "left"))  anchor |= ZWLR_LAYER_SURFACE_V1_ANCHOR_LEFT;
-        if (!strcmp(cfg.align_x, "right")) anchor |= ZWLR_LAYER_SURFACE_V1_ANCHOR_RIGHT;
-        b->w = cfg.min_width;
-        ls_set_anchor(b->ls, anchor);
-        ls_set_size(b->ls, (uint32_t)cfg.min_width, (uint32_t)b->h);
-    } else {
-        anchor |= ZWLR_LAYER_SURFACE_V1_ANCHOR_LEFT | ZWLR_LAYER_SURFACE_V1_ANCHOR_RIGHT;
-        ls_set_anchor(b->ls, anchor);
-        ls_set_size(b->ls, 0, (uint32_t)b->h);
-    }
-    ls_set_margin(b->ls, top ? cfg.margin : 0, cfg.side_margin,
-                  top ? 0 : cfg.margin, cfg.side_margin);
-    ls_set_exclusive_zone(b->ls, cfg.exclusive ? b->h + cfg.margin : 0);
-    ls_set_keyboard(b->ls, ZWLR_LAYER_SURFACE_V1_KEYBOARD_INTERACTIVITY_NONE);
-    wl_surface_commit(b->surf);
-    o->bar = b;
-}
-
-static void bar_destroy(Output *o)
-{
-    Bar *b = o->bar;
-    if (!b) return;
-    if (pointer_bar == b) pointer_bar = NULL;
-    bar_free_buffers(b);
-    if (b->ls) ls_destroy(b->ls);
-    if (b->surf) wl_surface_destroy(b->surf);
-    free(b);
-    o->bar = NULL;
-}
-
-/* --------------------------------------------------------------- outputs */
-
-static void out_geometry(void *d, struct wl_output *o, int32_t x, int32_t y,
-                         int32_t pw, int32_t ph, int32_t sub, const char *make,
-                         const char *model, int32_t tr)
-{ (void)d;(void)o;(void)x;(void)y;(void)pw;(void)ph;(void)sub;(void)make;(void)model;(void)tr; }
-
-static void out_mode(void *d, struct wl_output *wo, uint32_t f, int32_t w, int32_t h, int32_t r)
-{
-    (void)wo; (void)r;
-    Output *o = (Output *)d;
-    if (f & WL_OUTPUT_MODE_CURRENT) { o->mode_w = w; o->mode_h = h; }
-}
-
-static void out_scale(void *d, struct wl_output *wo, int32_t factor)
-{
-    (void)wo;
-    Output *o = (Output *)d;
-    o->scale = factor > 0 ? factor : 1;
-    if (o->bar && o->bar->scale != o->scale) {
-        o->bar->scale = o->scale;
-        bar_free_buffers(o->bar);
-        o->bar->dirty = true;
-    }
-}
-
-static void out_name(void *d, struct wl_output *wo, const char *name)
-{
-    (void)wo;
-    Output *o = (Output *)d;
-    str_set(o->name, sizeof(o->name), name ? name : "");
-}
-
-static void out_description(void *d, struct wl_output *o, const char *desc)
-{ (void)d;(void)o;(void)desc; }
-
-static void out_done(void *d, struct wl_output *wo)
-{
-    (void)wo;
-    Output *o = (Output *)d;
-    if (!o->bar) bar_create(o);
-    else o->bar->dirty = true;
-}
-
-static const struct wl_output_listener output_listener = {
-    out_geometry, out_mode, out_done, out_scale, out_name, out_description
-};
-
-/* --------------------------------------------------------------- pointer */
-
-static Bar *bar_of_surface(struct wl_surface *s)
-{
-    for (int i = 0; i < output_count; ++i)
-        if (outputs[i].bar && outputs[i].bar->surf == s) return outputs[i].bar;
-    return NULL;
-}
-
-static void ptr_enter(void *d, struct wl_pointer *p, uint32_t serial,
-                      struct wl_surface *surf, wl_fixed_t sx, wl_fixed_t sy)
-{
-    (void)d; (void)p; (void)serial;
-    Bar *b = bar_of_surface(surf);
-    pointer_bar = b;
-    if (!b) return;
-    b->hovered = true;
-    b->px = wl_fixed_to_double(sx);
-    b->py = wl_fixed_to_double(sy);
-    bar_set_grab(b, true);
-}
-
-static void ptr_leave(void *d, struct wl_pointer *p, uint32_t serial, struct wl_surface *surf)
-{
-    (void)d; (void)p; (void)serial;
-    Bar *b = bar_of_surface(surf);
-    if (!b) return;
-    b->hovered = false;
-    b->dirty = true;
-    bar_set_grab(b, false);
-    if (pointer_bar == b) pointer_bar = NULL;
-}
-
-static void ptr_motion(void *d, struct wl_pointer *p, uint32_t t, wl_fixed_t sx, wl_fixed_t sy)
-{
-    (void)d; (void)p; (void)t;
-    if (!pointer_bar) return;
-    pointer_bar->px = wl_fixed_to_double(sx);
-    pointer_bar->py = wl_fixed_to_double(sy);
-    pointer_bar->dirty = true;
-}
-
-static void ws_switch(const char *name)
-{
-    char esc[256];
-    size_t j = 0;
-    for (const char *q = name; *q && j < sizeof(esc) - 2; ++q) {
-        if (*q == '"' || *q == '\\') esc[j++] = '\\';
-        esc[j++] = *q;
-    }
-    esc[j] = 0;
-    sway_cmd("workspace --no-auto-back-and-forth \"%s\"", esc);
-}
-
-static void bar_click(Bar *b, int button)
-{
-    for (int i = 0; i < b->chits; ++i)
-        if (b->px >= b->chit[i].x0 && b->px < b->chit[i].x1) {
-            Cell *e = &cfg.cell[b->chit[i].cell];
-            if (cell_is_msg(e)) { msg_text[0] = 0; msg_until = 0; damage_all(); return; }
-            if (button >= 1 && button < MAX_BINDS && *e->bind[button]) {
-                run_command(e->bind[button]);
-                return;
-            }
-            break;
-        }
-    if (button == 1 && cfg.ws_click) {
-        for (int i = 0; i < b->hits; ++i)
-            if (b->px >= b->hit[i].x0 && b->px < b->hit[i].x1) {
-                ws_switch(b->hit[i].name);
-                return;
-            }
-    }
-    if ((button == 4 || button == 5) && cfg.scroll_workspace) {
-        sway_cmd("workspace %s_on_output", button == 4 ? "prev" : "next");
-        return;
-    }
-    if (button >= 1 && button < MAX_BINDS && *cfg.bind[button])
-        run_command(cfg.bind[button]);
-}
-
-static void ptr_button(void *d, struct wl_pointer *p, uint32_t serial, uint32_t t,
-                       uint32_t button, uint32_t state)
-{
-    (void)d; (void)p; (void)serial; (void)t;
-    if (state != WL_POINTER_BUTTON_STATE_PRESSED || !pointer_bar) return;
-    int n = 0;
-    switch (button) {
-    case 272: n = 1; break;   /* BTN_LEFT   */
-    case 274: n = 2; break;   /* BTN_MIDDLE */
-    case 273: n = 3; break;   /* BTN_RIGHT  */
-    case 275: n = 8; break;   /* BTN_SIDE   */
-    case 276: n = 9; break;   /* BTN_EXTRA  */
-    default: return;
-    }
-    bar_click(pointer_bar, n);
-}
-
-static void ptr_axis(void *d, struct wl_pointer *p, uint32_t t, uint32_t axis, wl_fixed_t value)
-{
-    (void)d; (void)p; (void)t;
-    if (!pointer_bar) return;
-    double v = wl_fixed_to_double(value);
-    if (v == 0) return;
-    int n;
-    if (axis == WL_POINTER_AXIS_VERTICAL_SCROLL) n = v < 0 ? 4 : 5;
-    else                                         n = v < 0 ? 6 : 7;
-    bar_click(pointer_bar, n);
-}
-
-static void ptr_frame(void *d, struct wl_pointer *p) { (void)d; (void)p; }
-static void ptr_axis_source(void *d, struct wl_pointer *p, uint32_t s) { (void)d;(void)p;(void)s; }
-static void ptr_axis_stop(void *d, struct wl_pointer *p, uint32_t t, uint32_t a)
-{ (void)d;(void)p;(void)t;(void)a; }
-static void ptr_axis_discrete(void *d, struct wl_pointer *p, uint32_t a, int32_t v)
-{ (void)d;(void)p;(void)a;(void)v; }
-
-/* designated, so newer libwayland versions with extra axis events still
-   compile against this without a warning — we bind wl_seat at version 5,
-   where those events are never sent */
-static const struct wl_pointer_listener pointer_listener = {
-    .enter = ptr_enter, .leave = ptr_leave, .motion = ptr_motion,
-    .button = ptr_button, .axis = ptr_axis, .frame = ptr_frame,
-    .axis_source = ptr_axis_source, .axis_stop = ptr_axis_stop,
-    .axis_discrete = ptr_axis_discrete
-};
-
-/* -------------------------------------------------------------- keyboard */
-/* No libxkbcommon: the bar only ever looks for one key, so it compares the
- * raw evdev keycode. That makes the binding layout independent — the key in
- * the space bar's place, whatever your layout calls it. */
-
-static struct { const char *name; int code; } keycodes[] = {
-    { "space", 57 }, { "escape", 1 }, { "esc", 1 }, { "tab", 15 }, { "enter", 28 },
-    { "backspace", 14 }, { "minus", 12 }, { "grave", 41 }, { "backslash", 43 },
-    { "q", 16 }, { "w", 17 }, { "e", 18 }, { "r", 19 }, { "t", 20 }, { "y", 21 },
-    { "u", 22 }, { "i", 23 }, { "o", 24 }, { "p", 25 }, { "a", 30 }, { "s", 31 },
-    { "d", 32 }, { "f", 33 }, { "g", 34 }, { "h", 35 }, { "j", 36 }, { "k", 37 },
-    { "l", 38 }, { "z", 44 }, { "x", 45 }, { "c", 46 }, { "v", 47 }, { "b", 48 },
-    { "n", 49 }, { "m", 50 },
-    { "1", 2 }, { "2", 3 }, { "3", 4 }, { "4", 5 }, { "5", 6 }, { "6", 7 },
-    { "7", 8 }, { "8", 9 }, { "9", 10 }, { "0", 11 },
-    { "f1", 59 }, { "f2", 60 }, { "f3", 61 }, { "f4", 62 }, { "f5", 63 },
-    { "f6", 64 }, { "f7", 65 }, { "f8", 66 }, { "f9", 67 }, { "f10", 68 },
-    { "f11", 87 }, { "f12", 88 }, { NULL, 0 }
-};
-
-static int hide_keycode(void)
-{
-    if (cfg.hide_keycode > 0) return cfg.hide_keycode;
-    for (int i = 0; keycodes[i].name; ++i)
-        if (!strcasecmp(keycodes[i].name, cfg.hide_key)) return keycodes[i].code;
-    return 57;
-}
-
-static void kb_keymap(void *d, struct wl_keyboard *k, uint32_t fmt, int32_t fd, uint32_t sz)
-{ (void)d; (void)k; (void)fmt; (void)sz; if (fd >= 0) close(fd); }
-static void kb_enter(void *d, struct wl_keyboard *k, uint32_t s, struct wl_surface *su,
-                     struct wl_array *keys)
-{ (void)d;(void)k;(void)s;(void)su;(void)keys; }
-static void kb_leave(void *d, struct wl_keyboard *k, uint32_t s, struct wl_surface *su)
-{ (void)d;(void)k;(void)s;(void)su; }
-static void kb_modifiers(void *d, struct wl_keyboard *k, uint32_t s, uint32_t dep,
-                         uint32_t lat, uint32_t lock, uint32_t grp)
-{ (void)d;(void)k;(void)s;(void)dep;(void)lat;(void)lock;(void)grp; }
-static void kb_repeat(void *d, struct wl_keyboard *k, int32_t rate, int32_t delay)
-{ (void)d;(void)k;(void)rate;(void)delay; }
-
-static void kb_key(void *d, struct wl_keyboard *k, uint32_t serial, uint32_t t,
-                   uint32_t key, uint32_t state)
-{
-    (void)d; (void)k; (void)serial; (void)t;
-    if (state != WL_KEYBOARD_KEY_STATE_PRESSED) return;
-    if ((int)key != hide_keycode()) return;
-    Bar *b = pointer_bar;
-    if (b) bar_set_collapsed(b, !b->collapsed);
-}
-
-static const struct wl_keyboard_listener keyboard_listener = {
-    kb_keymap, kb_enter, kb_leave, kb_key, kb_modifiers, kb_repeat
-};
-
-static void seat_caps(void *d, struct wl_seat *s, uint32_t caps)
-{
-    (void)d;
-    if ((caps & WL_SEAT_CAPABILITY_POINTER) && !pointer) {
-        pointer = wl_seat_get_pointer(s);
-        wl_pointer_add_listener(pointer, &pointer_listener, NULL);
-    }
-    if ((caps & WL_SEAT_CAPABILITY_KEYBOARD) && !keyboard) {
-        keyboard = wl_seat_get_keyboard(s);
-        wl_keyboard_add_listener(keyboard, &keyboard_listener, NULL);
-    }
-}
-static void seat_name(void *d, struct wl_seat *s, const char *n) { (void)d;(void)s;(void)n; }
-static const struct wl_seat_listener seat_listener = { seat_caps, seat_name };
-
-/* -------------------------------------------------------------- registry */
-
-static uint32_t vmin(uint32_t a, uint32_t b) { return a < b ? a : b; }
-
-static void reg_global(void *d, struct wl_registry *r, uint32_t id,
-                       const char *iface, uint32_t ver)
-{
-    (void)d;
-    if (!strcmp(iface, wl_compositor_interface.name)) {
-        compositor = (struct wl_compositor *)wl_registry_bind(r, id, &wl_compositor_interface, vmin(ver, 4));
-    } else if (!strcmp(iface, wl_shm_interface.name)) {
-        shm = (struct wl_shm *)wl_registry_bind(r, id, &wl_shm_interface, 1);
-    } else if (!strcmp(iface, wl_seat_interface.name)) {
-        if (!seat) {
-            seat = (struct wl_seat *)wl_registry_bind(r, id, &wl_seat_interface, vmin(ver, 5));
-            wl_seat_add_listener(seat, &seat_listener, NULL);
-        }
-    } else if (!strcmp(iface, zwlr_layer_shell_v1_interface.name)) {
-        layer_shell = (struct zwlr_layer_shell_v1 *)
-            wl_registry_bind(r, id, &zwlr_layer_shell_v1_interface, vmin(ver, 4));
-    } else if (!strcmp(iface, wl_output_interface.name)) {
-        Output *o = NULL;
-        for (int i = 0; i < output_count; ++i)
-            if (!outputs[i].alive) { o = &outputs[i]; break; }
-        if (!o) {
-            if (output_count >= MAX_OUTPUTS) return;
-            o = &outputs[output_count++];
-        }
-        memset(o, 0, sizeof(*o));
-        o->id = id;
-        o->scale = 1;
-        o->alive = true;
-        o->wl = (struct wl_output *)wl_registry_bind(r, id, &wl_output_interface, vmin(ver, 4));
-        wl_output_add_listener(o->wl, &output_listener, o);
-    }
-}
-
-static void reg_remove(void *d, struct wl_registry *r, uint32_t id)
-{
-    (void)d; (void)r;
-    for (int i = 0; i < output_count; ++i) {
-        if (!outputs[i].alive || outputs[i].id != id) continue;
-        bar_destroy(&outputs[i]);
-        if (outputs[i].wl) wl_output_destroy(outputs[i].wl);
-        outputs[i].alive = false;
-        outputs[i].wl = NULL;
-        outputs[i].name[0] = 0;
-    }
-}
-
-static const struct wl_registry_listener registry_listener = { reg_global, reg_remove };
-
-/* ------------------------------------------------------------------ help */
-
-static void usage(void)
-{
-    fputs(
-"SwBr " SWBR_VERSION " (build " SWBR_BUILD ") — a floating layer-shell bar for Sway\n"
-"\n"
-"usage: swbr [key=value ...]\n"
-"       every config key also works as key=value, --key=value or -s key=value\n"
-"\n"
-"  --config PATH       read this file instead of the default\n"
-"  --dump-config       print a config file with all defaults, then exit\n"
-"  --probe             print outputs, sizes, font metrics and cell values\n"
-"  --slim              start folded (same as start_collapsed=1)\n"
-"  --msg TEXT          send a message to the running bar, then exit\n"
-"                      'warn: ..', 'error: ..', 'info: ..', or 'clear'\n"
-"  --help, --version\n"
-"\n"
-"config file: ${XDG_CONFIG_HOME:-~/.config}/swbr/config\n"
-"\n"
-"placement\n"
-"  layer=top           background | bottom | top | overlay\n"
-"  position=top        top | bottom\n"
-"  height=0            bar height in logical px, 0 = derive from the font\n"
-"  exclusive=0         1 = reserve the space (windows tile below the bar)\n"
-"                      0 = float above them\n"
-"  margin=0            gap to the screen edge\n"
-"  side_margin=0       gap left and right\n"
-"  min_width=800       0 = span the whole output. Otherwise the bar floats\n"
-"                      and sizes itself to its content: never narrower than\n"
-"                      this, never wider than the screen. align_x places it\n"
-"  radius=14           corner radius; the two corners at the screen edge\n"
-"                      stay square\n"
-"  outputs=            comma separated output names, empty = every output\n"
-"\n"
-"workspaces\n"
-"  ws_names=0          0 = number only, 1 = the full workspace name\n"
-"  ws_pad=11 ws_gap=4 ws_min_w=0\n"
-"  ws_inset=0          gap above and below the buttons; >0 makes them pills\n"
-"  ws_radius=0         0 = square, the button spans the whole bar height\n"
-"                      -1 = a full pill when inset, else follows radius\n"
-"  ws_click=1          left click switches workspace\n"
-"  scroll_workspace=0  1 = the wheel switches workspaces\n"
-"  mode_show=1         show the binding mode (resize, ..)\n"
-"\n"
-"text\n"
-"  font=               .ttf path or a fontconfig family. Default: monospace\n"
-"  font_alt=           fallback font for glyphs the first one lacks\n"
-"  ui_scale=1.0 text_px=19 ws_px=19 pad_x=12 pad_right=20\n"
-"  text_y=-2           nudge every baseline, logical px, negative = up\n"
-"  markup=1            parse the pango subset (<span foreground=..>, &amp;)\n"
-"\n"
-"cells — the right hand side, one command per cell\n"
-"  cell=NAME           declare a cell; the order of these lines is the order\n"
-"                      the cells are drawn in, left to right\n"
-"  NAME.cmd=CMD        the shell command; its output is the cell text\n"
-"  NAME.interval=2     seconds between runs. 0 = run once, -1 = keep the\n"
-"                      command running and take every line it prints\n"
-"  NAME.fmt=%s C       %s is replaced by the output\n"
-"  NAME.color=b3c0cd   text colour\n"
-"  NAME.bg=            cell background\n"
-"  NAME.warn=>70:cecb00   recolour when the first number in the output is at\n"
-"  NAME.crit=<12:ff2222   or above (>) / at or below (<) the threshold\n"
-"  NAME.min_w=280      keep this width even when the text is shorter\n"
-"  NAME.max_w=0        clip beyond this width\n"
-"  NAME.align=right    left | center | right, inside min_w\n"
-"  NAME.pos=right      which group the cell joins: left | center | right\n"
-"  NAME.scroll=0       1 = a text cell: too long is cut with '..' and scrolls\n"
-"                      through while the pointer is on it. Pair with min_w\n"
-"  scroll_speed=45 scroll_pause=900\n"
-"\n"
-"layout — one line that places everything\n"
-"  bar={workspaces||cmus,volume,(clock,date)}\n"
-"    ||   splits groups: 1 = left, 2 = left+right, 3 = left+center+right\n"
-"    ,    separates items    (..)  glues items with no separator between\n"
-"    names are cell names plus the built-ins 'workspaces' and 'mode'\n"
-"  It sets position, order and separators, so keep it last in the config.\n"
-"  NAME.sep=1          draw the separator after this cell\n"
-"  NAME.hide_empty=1   no output = the cell and its separator disappear\n"
-"  NAME.empty=TEXT     drawn instead of hiding, so the area stays clickable\n"
-"  NAME.gap=           space after this cell; 0 glues it to the next one and\n"
-"                      equal background colours merge into a single box\n"
-"  NAME.pad=0          padding inside the cell, left and right\n"
-"  NAME.slim=auto      folded strip: auto | tick | bar | clock | presence\n"
-"                      | off. presence = a block in the running colour that\n"
-"                      is there only while the program is\n"
-"  NAME.slim_color=    override this cell's colour on the strip\n"
-"  NAME.slim_min=0 NAME.slim_max=100   the range a gauge maps\n"
-"  NAME.slim_w=0       width in the folded strip, 0 = the mode's default\n"
-"  NAME.markup=1       parse pango markup in this cell's output\n"
-"  NAME.button1=CMD    click this cell\n"
-"  cell_gap=14 cell_inset=3 cell_radius=5\n"
-"  separator=  separator_color=5a6b7aff\n"
-"\n"
-"status (used only when no cells are configured)\n"
-"  status_command=     shell command; every line it prints becomes the status\n"
-"  interval=1          seconds before restarting a command that exited\n"
-"\n"
-"messages\n"
-"  a fifo other programs can shout into, or `swbr --msg TEXT`\n"
-"  msg_fifo=           default $XDG_RUNTIME_DIR/swbr.fifo\n"
-"  msg_target=         name of the cell a message takes over while it lasts\n"
-"  msg_timeout=8       seconds, 0 = until cleared. Click it to dismiss\n"
-"  msg_flash=1         paint the whole folded strip in the message colour\n"
-"  msg_info= msg_warn= msg_error=\n"
-"\n"
-"folding\n"
-"  hide_key=space      hover the bar and press this to fold it into a strip\n"
-"  hide_keycode=0      raw evdev code, overrides hide_key\n"
-"  hover_keys=1        grab the keyboard while the pointer is over the bar\n"
-"  collapsed_px=6      height of the folded strip, logical px\n"
-"  anim_ms=120         fold animation, 0 = instant\n"
-"  signals=1           the folded strip keeps saying something: a fixed slot\n"
-"                      per workspace, twelve dots for a clock cell, a gauge\n"
-"                      for anything with a percentage\n"
-"  slim_ws_slots=10    at most this many workspace slots\n"
-"  start_collapsed=0\n"
-"  SIGUSR1 folds or unfolds every bar:  pkill -USR1 swbr\n"
-"\n"
-"mouse\n"
-"  button1..button9=CMD   shell command per button (1 left, 2 middle,\n"
-"                         3 right, 4/5 wheel, 6/7 tilt, 8/9 side)\n"
-"\n"
-"colors (#rrggbb or #rrggbbaa)\n"
-"  bg text dim accent hl urgent outline\n"
-"  ws_bg ws_fg ws_focused_bg ws_focused_fg ws_visible_bg ws_visible_fg\n"
-"  ws_urgent_bg ws_urgent_fg mode_bg mode_fg\n"
-"  running             the folded strip's 'this program is up' colour\n", stdout);
-}
-
-static void print_color(const char *k, Col c)
-{
-    printf("%s=%02x%02x%02x%02x\n", k, c.r, c.g, c.b, c.a);
-}
-
-static void dump_config(void)
-{
-    Config c;
-    config_defaults(&c);
-    printf("# SwBr config  —  ~/.config/swbr/config\n"
-           "# One key=value per line. '#' starts a comment.\n"
-           "# Every key also works on the command line; command line wins.\n\n");
-    printf("# --- placement ---\n");
-    printf("layer=%s\nposition=%s\nheight=%d\nexclusive=%d\nmargin=%d\nside_margin=%d\n"
-           "min_width=%d\nalign_x=%s\nradius=%g\noutputs=%s\n",
-           c.layer, c.position, c.height, c.exclusive, c.margin, c.side_margin,
-           c.min_width, c.align_x, (double)c.radius, c.outputs);
-    printf("\n# --- workspaces ---\n");
-    printf("ws_names=%d\nws_pad=%d\nws_gap=%d\nws_min_w=%d\nws_inset=%d\nws_radius=%g\n"
-           "ws_click=%d\nscroll_workspace=%d\nmode_show=%d\n",
-           c.ws_names, c.ws_pad, c.ws_gap, c.ws_min_w, c.ws_inset, (double)c.ws_radius,
-           c.ws_click, c.scroll_workspace, c.mode_show);
-    printf("\n# --- text ---\n");
-    printf("# font=\n# font_alt=\nui_scale=%g\ntext_px=%g\nws_px=%g\ntext_y=%g\n"
-           "pad_x=%d\npad_right=%d\nmarkup=%d\nscroll_speed=%d\nscroll_pause=%d\n",
-           (double)c.ui_scale, (double)c.text_px, (double)c.ws_px, (double)c.text_y,
-           c.pad_x, c.pad_right, c.markup, c.scroll_speed, c.scroll_pause);
-    printf("\n# --- cells (the right hand side) ---\n");
-    printf("cell_gap=%d\ncell_inset=%d\ncell_radius=%g\nseparator=%s\n",
-           c.cell_gap, c.cell_inset, (double)c.cell_radius, c.separator);
-    print_color("separator_color", c.separator_color);
-    printf("# cell=clock\n# clock.cmd=date '+%%H:%%M'\n# clock.interval=10\n");
-    printf("\n# --- messages ---\n");
-    printf("# msg_fifo=\n# msg_target=cmus\nmsg_timeout=%d\nmsg_flash=%d\n",
-           c.msg_timeout, c.msg_flash);
-    print_color("msg_info", c.msg_info);
-    print_color("msg_warn", c.msg_warn);
-    print_color("msg_error", c.msg_error);
-    printf("\n# --- layout: keep this last, it places everything ---\n");
-    printf("# bar={workspaces||cmus,volume,(clock,date)}\n");
-    printf("\n# --- status (only when no cells are configured) ---\n");
-    printf("# status_command=~/bin/sway_bar_status.sh\ninterval=%d\n", c.interval);
-    printf("\n# --- folding ---\n");
-    printf("hide_key=%s\nhover_keys=%d\ncollapsed_px=%d\nanim_ms=%d\nsignals=%d\n"
-           "slim_ws_slots=%d\n"
-           "start_collapsed=%d\n",
-           c.hide_key, c.hover_keys, c.collapsed_px, c.anim_ms, c.signals,
-           c.slim_ws_slots, c.start_collapsed);
-    printf("\n# --- mouse ---\n# button2=cmus_control toggle\n");
-    printf("\n# --- colors ---\n");
-    print_color("bg", c.bg);
-    print_color("text", c.text);
-    print_color("dim", c.dim);
-    print_color("accent", c.accent);
-    print_color("hl", c.hl);
-    print_color("urgent", c.urgent);
-    print_color("running", c.running);
-    print_color("outline", c.outline);
-    print_color("ws_bg", c.ws_bg);
-    print_color("ws_fg", c.ws_fg);
-    print_color("ws_focused_bg", c.ws_focused_bg);
-    print_color("ws_focused_fg", c.ws_focused_fg);
-    print_color("ws_visible_bg", c.ws_visible_bg);
-    print_color("ws_visible_fg", c.ws_visible_fg);
-    print_color("ws_urgent_bg", c.ws_urgent_bg);
-    print_color("ws_urgent_fg", c.ws_urgent_fg);
-    print_color("mode_bg", c.mode_bg);
-    print_color("mode_fg", c.mode_fg);
-}
-
-/* ------------------------------------------------------------------ main */
-
-static int  probe = 0;
-static int  cfg_start_collapsed_arg = 0;
-
-static void probe_report(void)
-{
-    printf("SwBr %s (build %s)\n", SWBR_VERSION, SWBR_BUILD);
-    printf("font          %s\n", *font_path_used ? font_path_used : "(none found)");
-    printf("font_alt      %s\n", *font_alt_used ? font_alt_used : "(none)");
-    printf("bar height    %d logical px  (height=%d text_px=%g ui_scale=%g)\n",
-           bar_height_logical(), cfg.height, (double)cfg.text_px, (double)cfg.ui_scale);
-    printf("min_width     %d  (0 = full output width)\n", cfg.min_width);
-    printf("outputs=      '%s'%s\n", cfg.outputs,
-           *cfg.outputs ? "  (only these get a bar)" : "  (every output)");
-    printf("position=%s layer=%s exclusive=%d cells=%d\n",
-           cfg.position, cfg.layer, cfg.exclusive, cfg.cell_count);
-    for (int i = 0; i < output_count; ++i) {
-        Output *o = &outputs[i];
-        if (!o->alive) continue;
-        printf("output %-10s scale %d  wanted %d  bar %s\n",
-               *o->name ? o->name : "(unnamed)", o->scale,
-               (int)output_wanted(o->name), o->bar ? "yes" : "NO");
-    }
-    for (int i = 0; i < output_count; ++i) {
-        Output *o = &outputs[i];
-        if (!o->alive || !o->bar) continue;
-        Bar *b = o->bar;
-        Font *f = font_get((int)lroundf(cfg.text_px * cfg.ui_scale * b->scale));
-        printf("bar %-10s surface %dx%d logical, %dx%d px, scale %d\n",
-               o->name, b->w, b->h, b->bw, b->bh, b->scale);
-        if (cfg.min_width > 0 && b->bw > 0) {
-            Canvas probe_cv = { NULL, b->bw, b->bh, 0, 0, b->bw, b->bh };
-            printf("            content wants %d logical px (min_width %d)\n",
-                   bar_natural_width(b, &probe_cv, f,
-                                     (float)bar_height_logical() * b->scale,
-                                     (float)b->scale), cfg.min_width);
-        }
-        printf("            font px %d ascent %.1f lineh %.1f baseline %.1f of %d\n",
-               f->px, (double)f->ascent, (double)f->lineh,
-               (double)baseline_for(f, 0, (float)b->bh, (float)b->scale), b->bh);
-    }
-    for (int i = 0; i < cfg.cell_count; ++i) {
-        Cell *e = &cfg.cell[i];
-        printf("cell %-10s iv=%-3d min_w=%-4d sep=%d  '%s'\n",
-               e->name, e->interval, e->min_w, e->sep, e->out);
-    }
-    printf("folded strip (%d px), right side left to right:\n", cfg.collapsed_px);
-    for (int i = 0; i < layout_n[G_RIGHT]; ++i) {
-        Item *it = &layout_item[G_RIGHT][i];
-        if (it->kind != IT_CELL) continue;
-        Cell *e = &cfg.cell[it->cell];
-        if (!cell_visible(e) || !*e->out) { printf("  %-10s hidden\n", e->name); continue; }
-        int m = slim_mode(e);
-        Col c = cell_color(e);
-        float v = 0;
-        bool have = cell_number(e->out, &v);
-        const char *mn = m == SLIM_BAR ? "gauge" : m == SLIM_CLOCK ? "clock"
-                       : m == SLIM_OFF ? "off"
-                       : m == SLIM_PRESENCE ? "up" : "tick";
-        printf("  %-10s %-5s %3.0fpx %02x%02x%02x", e->name, mn,
-               (double)slim_width_of(e, m, 1.0f), c.r, c.g, c.b);
-        if (m == SLIM_BAR && have)
-            printf("  %.1f of %g..%g = %.0f%% full",
-                   (double)v, (double)e->slim_min, (double)e->slim_max,
-                   (double)(clampf((v - e->slim_min) / (e->slim_max - e->slim_min),
-                                   0.0f, 1.0f) * 100.0f));
-        printf("  '%s'\n", e->out);
-    }
-}
-
-/* one redraw when a timed message runs out */
-static bool msg_expiry_due(void)
-{
-    static bool was = false;
-    bool now_on = msg_active();
-    bool changed = was != now_on;
-    was = now_on;
-    return changed;
-}
-
-static volatile sig_atomic_t sig_toggle = 0, sig_quit = 0;
-
-static void on_signal(int s)
-{
-    if (s == SIGUSR1) sig_toggle = 1;
-    else sig_quit = 1;
-}
-
-int main(int argc, char **argv)
-{
-    char cfgpath[PATH_MAX];
-    const char *xdg = getenv("XDG_CONFIG_HOME"), *home = getenv("HOME");
-    if (xdg && *xdg) snprintf(cfgpath, sizeof(cfgpath), "%s/swbr/config", xdg);
-    else snprintf(cfgpath, sizeof(cfgpath), "%s/.config/swbr/config", home ? home : ".");
-
-    for (int i = 1; i < argc; ++i) {
-        if (!strcmp(argv[i], "--config") && i + 1 < argc)
-            expand_tilde(argv[++i], cfgpath, sizeof(cfgpath));
-        else if (!strcmp(argv[i], "--help") || !strcmp(argv[i], "-h")) { usage(); return 0; }
-        else if (!strcmp(argv[i], "--version") || !strcmp(argv[i], "-v")) {
-            printf("SwBr %s (build %s)\n", SWBR_VERSION, SWBR_BUILD); return 0;
-        } else if (!strcmp(argv[i], "--dump-config")) { dump_config(); return 0; }
-        else if (!strcmp(argv[i], "--probe")) probe = 1;
-        else if (!strcmp(argv[i], "--slim") || !strcmp(argv[i], "--collapsed"))
-            cfg_start_collapsed_arg = 1;
-    }
-
-    config_defaults(&cfg);
-    sw_shared_apply("swbr", config_set_shared, &cfg);   /* shared first */
-    config_load(&cfg, cfgpath);                         /* our own wins  */
-
-    for (int i = 1; i < argc; ++i)                  /* talk to a running bar */
-        if (!strcmp(argv[i], "--msg") || !strcmp(argv[i], "-m")) {
-            if (i + 1 >= argc) die("--msg needs a text ('warn: ..', 'clear')");
-            return msg_send(argv[i + 1]);
-        }
-
-    for (int i = 1; i < argc; ++i) {                 /* command line wins */
-        const char *a = argv[i];
-        if (!strcmp(a, "--config")) { i++; continue; }
-        if (!strcmp(a, "--probe") || !strcmp(a, "--slim") ||
-            !strcmp(a, "--collapsed")) continue;
-        if (!strcmp(a, "-s") && i + 1 < argc) a = argv[++i];
-        else if (!strncmp(a, "--", 2)) a += 2;
-        char buf[4096];
-        str_set(buf, sizeof(buf), a);
-        char *eq = strchr(buf, '=');
-        if (!eq) continue;
-        *eq = 0;
-        config_set(&cfg, trim(buf), trim(eq + 1));
-    }
-
-    signal(SIGUSR1, on_signal);
-    signal(SIGINT, on_signal);
-    signal(SIGTERM, on_signal);
-    signal(SIGPIPE, SIG_IGN);
-    signal(SIGCHLD, SIG_IGN);
-
-    sway_fd = sway_connect();
-    if (sway_fd < 0) die("cannot reach sway (is SWAYSOCK set?)");
-    sway_subscribe_events();
-    ws_reload(cfg.ws_names != 0);
-
-    if (cfg_start_collapsed_arg) cfg.start_collapsed = 1;
-    layout_parse(cfg.layout);
-    fonts_init();
-
-    dpy = wl_display_connect(NULL);
-    if (!dpy) die("cannot connect to the wayland display");
-    registry = wl_display_get_registry(dpy);
-    wl_registry_add_listener(registry, &registry_listener, NULL);
-    wl_display_roundtrip(dpy);                       /* globals */
-    wl_display_roundtrip(dpy);                       /* output names, seats */
-
-    if (!compositor || !shm) die("compositor is missing wl_compositor/wl_shm");
-    if (!layer_shell) die("compositor does not support wlr-layer-shell");
-
-    for (int i = 0; i < output_count; ++i)
-        if (outputs[i].alive && !outputs[i].bar) bar_create(&outputs[i]);
-
-    if (*cfg.status_command && !cfg.cell_count) status_spawn();
-    cells_tick();
-    msg_open();
-
-    if (probe) {
-        for (int i = 0; i < 60; ++i) {          /* give the cells a moment */
-            cells_read();
-            usleep(20000);
-        }
-        wl_display_roundtrip(dpy);
-        probe_report();
-        cells_stop();
-        return 0;
-    }
-
-    int wlfd = wl_display_get_fd(dpy);
-    while (running && !sig_quit) {
-        bool redraw = false;
-
-        if (sig_toggle) {
-            sig_toggle = 0;
-            bool any_open = false;
-            for (int i = 0; i < output_count; ++i)
-                if (outputs[i].bar && !outputs[i].bar->collapsed) any_open = true;
-            for (int i = 0; i < output_count; ++i)
-                if (outputs[i].bar) bar_set_collapsed(outputs[i].bar, any_open);
-            redraw = true;
-        }
-
-        if (sway_events_pending()) { ws_reload(cfg.ws_names != 0); redraw = true; }
-        if (status_read()) redraw = true;
-        cells_tick();
-        if (cells_read()) redraw = true;
-        if (msg_read()) redraw = true;
-        if (msg_expiry_due()) redraw = true;
-
-        if (status_fd < 0 && *cfg.status_command && status_next_spawn &&
-            (int32_t)(now_ms() - status_next_spawn) >= 0) {
-            status_next_spawn = 0;
-            status_spawn();
-        }
-
-        uint32_t t = now_ms();
-        bool animating = false;
-        for (int i = 0; i < output_count; ++i) {
-            Bar *b = outputs[i].bar;
-            if (!b) continue;
-            bar_anim_step(b, t);
-            if (bar_animating(b)) animating = true;
-        }
-        if (redraw) damage_all();
-
-        scroll_running = false;
-        for (int i = 0; i < output_count; ++i) {
-            Bar *b = outputs[i].bar;
-            if (b && b->dirty) bar_render(b);
-        }
-        if (scroll_running) damage_all();
-
-        while (wl_display_prepare_read(dpy) != 0) wl_display_dispatch_pending(dpy);
-        wl_display_flush(dpy);
-
-        struct pollfd pfd[4 + MAX_CELLS];
-        int n = 0;
-        pfd[n].fd = wlfd;        pfd[n].events = POLLIN; n++;
-        pfd[n].fd = sway_evt_fd; pfd[n].events = POLLIN; n++;
-        if (status_fd >= 0) { pfd[n].fd = status_fd; pfd[n].events = POLLIN; n++; }
-        if (msg_fd >= 0) { pfd[n].fd = msg_fd; pfd[n].events = POLLIN; n++; }
-        for (int i = 0; i < cfg.cell_count; ++i)
-            if (cfg.cell[i].fd >= 0) { pfd[n].fd = cfg.cell[i].fd; pfd[n].events = POLLIN; n++; }
-
-        int timeout = -1;
-        if (animating || scroll_running) timeout = 16;
-        else if (cfg.cell_count || msg_until) timeout = 100;
-        else if (status_fd < 0 && *cfg.status_command) timeout = 100;
-        else {
-            bool any_dirty = false;
-            for (int i = 0; i < output_count; ++i)
-                if (outputs[i].bar && outputs[i].bar->dirty) any_dirty = true;
-            if (any_dirty) timeout = 16;
-        }
-
-        int pr = poll(pfd, (nfds_t)n, timeout);
-        if (pr < 0 && errno != EINTR) { wl_display_cancel_read(dpy); break; }
-
-        if (pfd[0].revents & POLLIN) {
-            if (wl_display_read_events(dpy) < 0) break;
-        } else {
-            wl_display_cancel_read(dpy);
-        }
-        if (wl_display_dispatch_pending(dpy) < 0) break;
-        if ((pfd[0].revents & (POLLERR | POLLHUP)) != 0) break;
-    }
-
-    status_stop();
-    cells_stop();
-    if (msg_fd >= 0) { close(msg_fd); unlink(msg_path); }
-    for (int i = 0; i < output_count; ++i) bar_destroy(&outputs[i]);
-    if (dpy) { wl_display_flush(dpy); wl_display_disconnect(dpy); }
-    if (sway_fd >= 0) close(sway_fd);
-    if (sway_evt_fd >= 0) close(sway_evt_fd);
-    return 0;
-}
diff --git a/swov.c b/swov.c
index ff71131..32a3198 100644
--- a/swov.c
+++ b/swov.c
@@ -39,6 +39,7 @@
 #include <sys/stat.h>
 #include <sys/un.h>
 #include <limits.h>
+#include <sys/wait.h>
 #include <unistd.h>
 
 #include "sw_theme.h"
@@ -229,6 +230,9 @@ typedef struct {
     int   back_scope;        /* -b: 0 global, 1 this monitor, 2 sway's own */
     int   blur;              /* --backdrop only: 0 off, 1..3 how soft       */
     int   drop_ghosts;       /* --backdrop: offer the free numbers as tiles */
+    int   drop_outputs;      /* mid-drag, show every screen's workspaces    */
+    int   over_fullscreen;   /* un-fullscreen what is in the way, and put
+                                it back on the way out                      */
     int   cpu;               /* the load dots on a tile, measured by swbr   */
     float cpu_idle;          /* under this, nothing is happening at all     */
     float cpu_min, cpu_full; /* the scale, in cores                         */
@@ -241,9 +245,11 @@ typedef struct {
     int   show_empty;
     int   all_outputs;
     int   outputs_map;       /* the little map of the monitors, bottom left  */
+    int   map_dwell_ms;      /* hold an app over one this long to step onto it */
     float outputs_map_w;     /* how wide, as a share of the tile area        */
     char  output[64];        /* start looking at this screen, not the one
                                 sway is on                                   */
+    char  launcher[192];     /* what `d` opens, and swov steps aside for     */
     int   show_header;
     int   show_hints;
     int   quit_on_focus_loss;
@@ -287,6 +293,8 @@ static Cfg cfg_defaults(void)
     c.blur       = 2;
     c.cpu        = 1;
     c.drop_ghosts = 1;
+    c.drop_outputs = 1;
+    c.over_fullscreen = 1;
     c.cpu_idle   = 0.01f;    /* cores; under this nothing is happening */
     c.cpu_min    = 0.25f;    /* cores, not a share of the machine */
     c.cpu_full   = 4.0f;
@@ -305,6 +313,8 @@ static Cfg cfg_defaults(void)
     c.all_outputs        = 0;
     c.outputs_map        = 1;
     c.outputs_map_w      = 0.18f;
+    c.map_dwell_ms       = 0;
+    str_set(c.launcher, sizeof(c.launcher), "swas --replace overview=1");
     c.show_header        = 1;
     c.show_hints         = 1;
     c.quit_on_focus_loss = 1;
@@ -378,6 +388,8 @@ static void cfg_set(Cfg *c, const char *k, const char *v)
     else if (key_is(k,"track"))         c->track = atoi(v) != 0;
     else if (key_is(k,"cpu"))       c->cpu = atoi(v) != 0;
     else if (key_is(k,"drop_ghosts")) c->drop_ghosts = atoi(v) != 0;
+    else if (key_is(k,"drop_outputs")) c->drop_outputs = atoi(v) != 0;
+    else if (key_is(k,"over_fullscreen")) c->over_fullscreen = atoi(v) != 0;
     else if (key_is(k,"cpu_idle"))  c->cpu_idle = (float)atof(v);
     else if (key_is(k,"cpu_min"))   c->cpu_min = (float)atof(v);
     else if (key_is(k,"cpu_full"))  c->cpu_full = (float)atof(v);
@@ -396,7 +408,9 @@ static void cfg_set(Cfg *c, const char *k, const char *v)
     else if (key_is(k,"all_outputs"))   c->all_outputs = atoi(v) != 0;
     else if (key_is(k,"outputs_map"))   c->outputs_map = atoi(v) != 0;
     else if (key_is(k,"outputs_map_w")) c->outputs_map_w = (float)atof(v);
+    else if (key_is(k,"map_dwell_ms"))  c->map_dwell_ms = atoi(v);
     else if (key_is(k,"output"))        str_set(c->output, sizeof(c->output), v);
+    else if (key_is(k,"launcher"))      str_set(c->launcher, sizeof(c->launcher), v);
     else if (key_is(k,"show_header"))   c->show_header = atoi(v) != 0;
     else if (key_is(k,"show_hints"))    c->show_hints = atoi(v) != 0;
     else if (key_is(k,"quit_on_focus_loss")) c->quit_on_focus_loss = atoi(v) != 0;
@@ -1327,7 +1341,10 @@ static int adopt_window(int pid, const char *ws, double timeout, bool focus,
     double deadline = now_secs() + timeout;
     int    con_id   = -1;
 
-    int assigned[24], nassigned = 0;
+    /* Each of these is a rule sway keeps for the rest of the session — there
+     * is no unassign — so they are worth counting. Two dozen per launch was
+     * generous; six covers a wrapper script and its children. */
+    int assigned[6], nassigned = 0;
     if (!no_assign) {
         assign_pid_to_ws(pid, ws);                /* before anything maps */
         assigned[nassigned++] = pid;
@@ -1615,6 +1632,42 @@ static void fill_round_rect(SDL_FRect r, float rad, SDL_FColor c)
     }
 }
 
+/* The same, with the top or the bottom left square.
+ *
+ * A tab meets the panel below it and a panel meets the tab above it: rounding
+ * both sides of that join leaves a pinch of background showing through, which
+ * is what made a tabbed workspace look wrong. */
+static void fill_round_side(SDL_FRect r, float rad, bool round_top,
+                            bool round_bot, SDL_FColor c)
+{
+    if (r.w <= 0.0f || r.h <= 0.0f) return;
+    if (round_top && round_bot) { fill_round_rect(r, rad, c); return; }
+
+    rad = SDL_min(rad, SDL_min(r.w, r.h) * 0.5f);
+    if (rad <= 0.5f) { set_col(c); SDL_RenderFillRect(REN, &r); return; }
+
+    set_col(c);
+    SDL_FRect mid = { r.x, r.y + (round_top ? rad : 0.0f), r.w,
+                      r.h - (round_top ? rad : 0.0f) - (round_bot ? rad : 0.0f) };
+    SDL_RenderFillRect(REN, &mid);
+
+    int steps = (int)SDL_ceilf(rad);
+    for (int i = 0; i < steps; ++i) {
+        float dy = rad - (float)i - 0.5f;
+        float dx = SDL_sqrtf(SDL_max(0.0f, rad * rad - dy * dy));
+        float x0 = r.x + rad - dx;
+        float w  = r.w - 2.0f * rad + 2.0f * dx;
+        if (round_top) {
+            SDL_FRect t = { x0, r.y + (float)i, w, 1.0f };
+            SDL_RenderFillRect(REN, &t);
+        }
+        if (round_bot) {
+            SDL_FRect b = { x0, r.y + r.h - (float)i - 1.0f, w, 1.0f };
+            SDL_RenderFillRect(REN, &b);
+        }
+    }
+}
+
 /* rounded rectangle outline of thickness t, drawn inside r */
 static void stroke_round_rect(SDL_FRect r, float rad, float t, SDL_FColor c)
 {
@@ -2183,6 +2236,7 @@ typedef struct {
 
     int   x, y, w, h;          /* absolute geometry, as sway reports it */
     bool  floating, focused, urgent, fullscreen, sticky;
+    bool  is_self;             /* swov's own window, or its backdrop         */
     bool  marked;              /* multi-selection */
     bool  match;               /* passes the current filter */
     bool  visible;             /* the one on top of its tabbed container */
@@ -2232,6 +2286,7 @@ static int NWIN = 0;
 static Ws  WSS[MAX_WORKSPACES];
 static int NWS = 0;
 
+static bool DRAG_ALL_OUTPUTS = false;   /* mid-drag: every screen shows */
 static char FOCUSED_OUTPUT[64] = {0};   /* the output whose workspaces we show */
 static char HOME_OUTPUT[64]    = {0};   /* the one sway is really on           */
 static bool PINNED_OUTPUT;              /* ...and we are looking elsewhere     */
@@ -2246,7 +2301,17 @@ typedef struct {
 } Out;
 static Out  OUTS[8];
 static int  NOUTS;
-static SDL_FRect MAP_RECT;              /* the free corner it is drawn in */
+static bool dirty   = true;      /* a frame is only drawn when this is set */
+static SDL_FRect MAP_RECT;              /* the corner it is drawn in */
+static SDL_FRect CANCEL_RECT;           /* and the ✕ beside it, while dragging */
+
+/* Holding an app over another monitor for a moment steps the overview onto
+ * it, so a drag that started on one screen can finish on another. It is a
+ * press you hold, so it shows how far along it is. */
+static int    MAP_HOVER = -1;
+static int    DRAG_CON;          /* the window being dragged, by id */
+static double MAP_HOVER_SINCE;
+static float  MAP_HOVER_X, MAP_HOVER_Y;
 
 static void outputs_reload(void)
 {
@@ -2309,6 +2374,12 @@ static void collect_views(const JV *node, Ws *ws, bool floating)
 
         w->con_id     = jint(node, "id", 0);
         w->pid        = jint(node, "pid", 0);
+        /* This very window, and the backdrop behind it. It is on the
+         * workspace like anything else and hiding it would leave a hole, but
+         * it must not be selectable, droppable or in the way: it is about to
+         * be gone. */
+        w->is_self    = w->pid == getpid() ||
+                        !strncmp(w->app_id, APP_ID, sizeof(APP_ID) - 1);
         w->focused    = jbool(node, "focused", false);
         w->visible    = jbool(node, "visible", true);
         w->urgent     = jbool(node, "urgent", false);
@@ -2405,7 +2476,8 @@ static void walk_outputs(const JV *node)
     if (strcmp(type, "output") == 0) {
         const char *name = jstr(node, "name", "");
         if (strcmp(name, "__i3") == 0) return;
-        if (!C.all_outputs && FOCUSED_OUTPUT[0] && strcmp(name, FOCUSED_OUTPUT) != 0) return;
+        if (!C.all_outputs && !DRAG_ALL_OUTPUTS &&
+        FOCUSED_OUTPUT[0] && strcmp(name, FOCUSED_OUTPUT) != 0) return;
         str_set(CUR_OUTPUT, sizeof(CUR_OUTPUT), name);
     }
 
@@ -2597,6 +2669,41 @@ static float pos_x(const char *p, float w, float margin)
 static int  sel_ws = 0;
 static bool sel_active = true;   /* false: no tile and no window highlighted */
 static int  hov_ws = -1, hov_win = -1;
+
+/* A floating window is see-through and answers the pointer only through its
+ * name plate, so the windows underneath stay reachable. Once you are on that
+ * plate it comes forward and takes the whole of itself — otherwise you lose
+ * it the moment you move off the plate, which is exactly when you were about
+ * to aim inside it. It is given up again at the edges of its own card, and
+ * for one that covers the whole workspace, in a band along the tile: without
+ * that there would be no way back out. */
+static int  LIFT_WIN = -1;
+
+static bool lift_holds(const Win *w, const Ws *ws, float x, float y)
+{
+    SDL_FRect c = w->card;
+    if (x < c.x || x >= c.x + c.w || y < c.y || y >= c.y + c.h) return false;
+
+    float band = SDL_clamp(SDL_min(ws->tile.w, ws->tile.h) * 0.06f,
+                           8.0f * SC, 40.0f * SC);
+    bool fills = c.w >= ws->screen.w - band && c.h >= ws->screen.h - band;
+    if (!fills) return true;
+
+    return x >= ws->tile.x + band && x < ws->tile.x + ws->tile.w - band &&
+           y >= ws->tile.y + band && y < ws->tile.y + ws->tile.h - band;
+}
+
+static void lift_update(float x, float y, int ws_idx, int win_idx)
+{
+    if (LIFT_WIN >= 0 && LIFT_WIN < NWIN) {
+        const Win *w = &WINS[LIFT_WIN];
+        if (w->ws >= 0 && w->ws < NWS && lift_holds(w, &WSS[w->ws], x, y)) return;
+        LIFT_WIN = -1;
+    }
+    if (win_idx >= 0 && win_idx < NWIN && ws_idx >= 0 && !WINS[win_idx].is_self &&
+        (WINS[win_idx].floating || WINS[win_idx].fullscreen))
+        LIFT_WIN = win_idx;
+}
 static char query[128];
 static int  qlen = 0;
 static bool filtering = false;   /* "/" — hides everything that does not match */
@@ -2706,17 +2813,22 @@ static void layout_cards(Ws *ws)
         bool  tabs_fit = strip >= 16.0f * SC && base.w / (float)n >= 26.0f * SC;
 
         if (tabs_fit) {
-            /* a tab per window across the top, the active one showing its
-             * contents underneath — the same shape sway draws */
-            float tw = base.w / (float)n;
+            /* A tab per window across the top, the active one showing its
+             * contents underneath — the same shape sway draws. The strip is
+             * inset from the group's own edge, so a tab never sits against
+             * the border and the first and last are not clipped by its
+             * rounded corners. */
+            float pad = SDL_clamp(base.w * 0.02f, 2.0f * SC, 8.0f * SC);
+            float inner = base.w - 2.0f * pad;
+            float tw = inner / (float)n;
             for (int k = 0; k < n; ++k) {
                 Win *cw = &WINS[ws->first + group[k]];
-                cw->tab = (SDL_FRect){ base.x + (float)k * tw + inset * 0.5f, base.y,
-                                       tw - inset, strip };
+                cw->tab = (SDL_FRect){ base.x + pad + (float)k * tw + inset * 0.5f,
+                                       base.y + pad, tw - inset, strip };
                 cw->has_tab = true;
                 cw->card = (k == active)
-                    ? (SDL_FRect){ base.x, base.y + strip + inset * 0.5f,
-                                   base.w, base.h - strip - inset * 0.5f }
+                    ? (SDL_FRect){ base.x + pad, base.y + pad + strip + inset * 0.5f,
+                                   inner, base.h - 2.0f * pad - strip - inset * 0.5f }
                     : cw->tab;
             }
         } else {
@@ -2968,6 +3080,7 @@ static void layout(void)
      * grid happened to leave over meant it moved about, and vanished
      * entirely when the grid came out full. The tiles get what is left. */
     MAP_RECT = (SDL_FRect){ 0, 0, 0, 0 };
+    CANCEL_RECT = (SDL_FRect){ 0, 0, 0, 0 };
     if (C.outputs_map && NOUTS > 1) {
         int minx = OUTS[0].x, miny = OUTS[0].y;
         int maxx = OUTS[0].x + OUTS[0].w, maxy = OUTS[0].y + OUTS[0].h;
@@ -2984,6 +3097,14 @@ static void layout(void)
             float cap = ah * 0.30f;
             if (mh > cap) { mh = cap; mw = mh * dw / dh; }
             MAP_RECT = (SDL_FRect){ ax, ay + ah - mh, mw, mh };
+
+            /* Somewhere to put a drag down and have nothing happen. It runs
+               from the monitors to the right edge and fills the band, so
+               dragging downwards is enough — no aiming, no corner to find. */
+            float cx0 = ax + mw + gap * 2.0f;
+            CANCEL_RECT = (SDL_FRect){ cx0, ay + ah - mh,
+                                       SDL_max(mh, ax + aw - cx0), mh };
+
             ah -= mh + gap;
             if (ah < 60.0f) { MAP_RECT = (SDL_FRect){ 0, 0, 0, 0 }; ah = (float)RH - HEADER_H - FOOTER_H - m; }
         }
@@ -3347,7 +3468,10 @@ static int hit_test(float mx, float my, int *out_ws)
         *out_ws = i;
         for (int j = ws->count - 1; j >= 0; --j) {      /* floats are last = on top */
             Win *w = &WINS[ws->first + j];
-            SDL_FRect r = (w->hit.w > 0.0f) ? w->hit : w->card;
+            if (w->is_self) continue;              /* scenery, not a target */
+            SDL_FRect r = (ws->first + j == LIFT_WIN)
+                        ? w->card                          /* lifted: all of it */
+                        : ((w->hit.w > 0.0f) ? w->hit : w->card);
             if (mx >= r.x && mx < r.x + r.w && my >= r.y && my < r.y + r.h)
                 return ws->first + j;
         }
@@ -3401,7 +3525,8 @@ typedef enum {
     DROP_WS_SWAP,    /* workspace onto another workspace                   */
     DROP_WS_NUM,     /* workspace onto a free number (a ghost slot)        */
     DROP_WIN_NEWWS,  /* window onto a free number: sway creates it          */
-    DROP_WS_EDGE     /* window along one edge of a tile: beside the lot      */
+    DROP_WS_EDGE,    /* window along one edge of a tile: beside the lot      */
+    DROP_CANCEL      /* the ✕ beside the monitors: let go and nothing happens */
 } DropKind;
 
 enum { EDGE_LEFT, EDGE_RIGHT, EDGE_TOP, EDGE_BOTTOM };
@@ -3455,6 +3580,23 @@ static bool point_in(SDL_FRect r, float x, float y)
     return x >= r.x && x < r.x + r.w && y >= r.y && y < r.y + r.h;
 }
 
+/* Every screen's workspaces, or only this one's. Rebuilding the model in the
+ * middle of a drag would lose the window being dragged, so its id is kept and
+ * looked up again on the other side. */
+static void show_all_outputs(bool on)
+{
+    if (DRAG_ALL_OUTPUTS == on) return;
+    DRAG_ALL_OUTPUTS = on;
+
+    int keep = press_win >= 0 ? WINS[press_win].con_id : DRAG_CON;
+    reload_model();
+    press_win = -1;
+    if (keep > 0)
+        for (int i = 0; i < NWIN; ++i)
+            if (WINS[i].con_id == keep) { press_win = i; break; }
+    dirty = true;
+}
+
 static void set_ghosts(int lo, int hi)
 {
     if (lo == ghost_lo && hi == ghost_hi) return;
@@ -3479,11 +3621,59 @@ static void update_ghosts(float x, float y)
     } else if (press_win >= 0) {
         want = !point_in(drag_src_tile, x, y);
     }
-    if (want) set_ghosts(0, 10);
+    if (want) {
+        if (C.drop_outputs) show_all_outputs(true);
+        set_ghosts(0, 10);
+    }
+}
+
+/* Holding a drag over another monitor steps the overview onto it. The press
+ * has to be still: any movement starts the wait again, so brushing past a
+ * plate on the way somewhere else never triggers it. */
+static bool point_in_cancel(float x, float y)
+{
+    return CANCEL_RECT.w > 0.0f &&
+           x >= CANCEL_RECT.x && x < CANCEL_RECT.x + CANCEL_RECT.w &&
+           y >= CANCEL_RECT.y && y < CANCEL_RECT.y + CANCEL_RECT.h;
+}
+
+static bool map_dwell_hover(float x, float y)
+{
+    if (MAP_RECT.w <= 0.0f) return false;
+
+    int on = -1;
+    for (int i = 0; i < NOUTS; ++i) {
+        SDL_FRect r = OUTS[i].box;
+        if (r.w > 0.0f && x >= r.x && x < r.x + r.w &&
+            y >= r.y && y < r.y + r.h) { on = i; break; }
+    }
+    if (on >= 0 && strcmp(OUTS[on].name, FOCUSED_OUTPUT) == 0) on = -1;
+
+    float moved = SDL_fabsf(x - MAP_HOVER_X) + SDL_fabsf(y - MAP_HOVER_Y);
+    if (on != MAP_HOVER || moved > 4.0f * SC) {
+        MAP_HOVER = on;
+        MAP_HOVER_SINCE = now_secs();
+        MAP_HOVER_X = x;
+        MAP_HOVER_Y = y;
+        dirty = true;
+    }
+    return on >= 0;
 }
 
 static void drag_update_target(float x, float y)
 {
+    if (point_in_cancel(x, y)) {
+        drop_kind = DROP_CANCEL;
+        drop_ws = drop_win = drop_num = -1;
+        MAP_HOVER = -1;
+        return;
+    }
+    if (map_dwell_hover(x, y)) {          /* over a monitor: nothing else */
+        drop_kind = DROP_NONE;
+        drop_ws = drop_win = drop_num = -1;
+        return;
+    }
+
     drop_kind = DROP_NONE;
     drop_ws = drop_win = drop_num = -1;
     drop_insert = false;
@@ -3844,11 +4034,14 @@ static void drag_finish(void)
                      WINS[press_win].con_id, drop_num);
         break;
 
+    case DROP_CANCEL:                        /* put down where it started */
     case DROP_NONE:
     default:
         break;
     }
 
+    show_all_outputs(false);
+    DRAG_CON = 0;
     drag_active = drag_ws_mode = false;
     drop_kind = DROP_NONE;
     press_win = press_ws = -1;
@@ -3979,14 +4172,28 @@ static void draw_card(Win *w, bool tile_selected)
     if (selected)   fill = mix(fill, C.hl, is_hovered ? 0.16f : 0.10f);
     if (dimmed)     fill = with_alpha(mix(fill, C.tile, 0.6f), fill.a * 0.45f);
 
+    /* The one being dragged, still drawn where it came from. Halfway to the
+     * background, so it reads as a space the window has left rather than a
+     * window sitting there — and it is the only thing on screen wearing the
+     * accent, so there is no forgetting which one is on the pointer. */
+    bool being_dragged = drag_active && press_win >= 0 && &WINS[press_win] == w;
+    if (being_dragged)
+        fill = with_alpha(mix(fill, C.bg, 0.55f), fill.a * 0.8f);
+
     /* A floating dialog, and especially a fullscreen window, sits on top of
      * the tiled ones. Drawing it solid would hide the whole workspace, so it
      * gets see-through — the more of the screen it covers, the more so. Its
      * border, icon and label stay at full strength, so it is still obvious
      * which window it is. */
+    /* Aiming inside a floating window is guesswork when it is see-through:
+     * you are trying to hit an edge you can barely make out. Point at one —
+     * or drag over one — and it comes forward, solid, until the pointer
+     * leaves again. */
+    bool lifted = LIFT_WIN >= 0 && LIFT_WIN < NWIN && &WINS[LIFT_WIN] == w;
+
     float over = 1.0f, cover = 0.0f;
     bool on_top = w->floating || w->fullscreen;
-    if (on_top) {
+    if (on_top && !lifted) {
         const Ws *ws = &WSS[w->ws];
         float screen_area = ws->screen.w * ws->screen.h;
         cover = (screen_area > 1.0f) ? (w->card.w * w->card.h) / screen_area : 1.0f;
@@ -3997,14 +4204,34 @@ static void draw_card(Win *w, bool tile_selected)
     /* Anything drawn on top of other windows gets a frame and a name plate
      * rather than a filled card: its label would otherwise sit in the middle,
      * exactly where the label of the window showing through already is. */
-    bool as_overlay = on_top;
+    bool as_overlay = on_top && !lifted;
+
+    /* A tab is rounded on top and flat where it meets its panel; the panel
+     * is flat on top and rounded below. A window that is neither is rounded
+     * all round, as before. */
+    bool is_tab   = w->has_tab && w->card.h <= w->tab.h + 1.0f;
+    bool is_panel = w->has_tab && !is_tab;
 
     if (w->floating) drop_shadow(r, rad, 9.0f * SC, with_alpha(C.shadow_col, C.shadow_col.a * over));
-    fill_round_rect(r, rad, fill);
+    if (is_tab)        fill_round_side(r, rad, true, false, fill);
+    else if (is_panel) fill_round_side(r, rad, false, true, fill);
+    else               fill_round_rect(r, rad, fill);
 
-    /* border: selection beats hover beats mark beats plain */
+    /* border: what is being dragged beats everything, then selection, hover,
+       mark, plain */
     float bw = SDL_max(1.0f, C.border * SC * 0.75f);
-    if (confirm_kill && (w->marked || selected))
+    if (lifted && !being_dragged) {
+        /* it has come forward: a heavier accent frame, and a soft one just
+           outside it so it reads as raised rather than merely outlined */
+        SDL_FRect glow = { r.x - 3.0f * SC, r.y - 3.0f * SC,
+                           r.w + 6.0f * SC, r.h + 6.0f * SC };
+        stroke_round_rect(glow, rad + 3.0f * SC, SDL_max(1.0f, 3.0f * SC),
+                          with_alpha(C.accent, 0.25f));
+    }
+
+    if (being_dragged)     stroke_round_rect(r, rad, bw * 1.6f, C.accent);
+    else if (lifted)       stroke_round_rect(r, rad, bw * 1.8f, C.accent);
+    else if (confirm_kill && (w->marked || selected))
                            stroke_round_rect(r, rad, bw * 1.35f, C.urgent);
     else if (selected)     stroke_round_rect(r, rad, bw * 1.35f, C.hl);
     else if (is_hit)       stroke_round_rect(r, rad, bw * 1.2f, C.match);
@@ -4111,6 +4338,64 @@ static void draw_card(Win *w, bool tile_selected)
     if (has_icon) SDL_SetTextureAlphaModFloat(w->icon, 1.0f);
 }
 
+static void rgb_to_hsv(SDL_FColor c, float *h, float *s, float *v)
+{
+    float mx = SDL_max(c.r, SDL_max(c.g, c.b));
+    float mn = SDL_min(c.r, SDL_min(c.g, c.b));
+    float d = mx - mn;
+    *v = mx;
+    *s = mx > 0.0001f ? d / mx : 0.0f;
+    if (d < 0.0001f) { *h = 0.0f; return; }
+    if      (mx == c.r) *h = 60.0f * SDL_fmodf((c.g - c.b) / d, 6.0f);
+    else if (mx == c.g) *h = 60.0f * ((c.b - c.r) / d + 2.0f);
+    else                *h = 60.0f * ((c.r - c.g) / d + 4.0f);
+    if (*h < 0.0f) *h += 360.0f;
+}
+
+static SDL_FColor hsv_to_rgb(float h, float s, float v, float a)
+{
+    h = SDL_fmodf(h, 360.0f);
+    if (h < 0.0f) h += 360.0f;
+    float c = v * s, x = c * (1.0f - SDL_fabsf(SDL_fmodf(h / 60.0f, 2.0f) - 1.0f));
+    float m = v - c, r = 0, g = 0, b = 0;
+    if      (h <  60) { r = c; g = x; }
+    else if (h < 120) { r = x; g = c; }
+    else if (h < 180) { g = c; b = x; }
+    else if (h < 240) { g = x; b = c; }
+    else if (h < 300) { r = x; b = c; }
+    else              { r = c; b = x; }
+    return (SDL_FColor){ r + m, g + m, b + m, a };
+}
+
+/* One colour per monitor: the accent, turned around the wheel by the golden
+ * angle for each screen. Two monitors never look alike, they keep the
+ * palette's weight, and none of them lands on `hl` — that one already means
+ * "this is the one you are on", and having a whole screen's worth of
+ * workspaces wear it said the opposite of what it meant. */
+static SDL_FColor output_colour(const char *name)
+{
+    int at = 0;
+    for (int i = 0; i < NOUTS; ++i)
+        if (!strcmp(OUTS[i].name, name)) { at = i; break; }
+
+    float h, sat, val, hl_h, hl_s, hl_v;
+    rgb_to_hsv(C.accent, &h, &sat, &val);
+    rgb_to_hsv(C.hl, &hl_h, &hl_s, &hl_v);
+
+    h += 137.5f * (float)at;                    /* golden angle: no repeats */
+    if (sat < 0.35f) sat = 0.55f;               /* a grey accent gives no hue */
+    if (val < 0.55f) val = 0.75f;
+
+    for (int guard = 0; guard < 4; ++guard) {   /* keep clear of hl */
+        float d = SDL_fabsf(SDL_fmodf(h - hl_h + 540.0f, 360.0f) - 180.0f);
+        if (d > 35.0f) break;
+        h += 50.0f;
+    }
+    return hsv_to_rgb(h, sat, val, 1.0f);
+}
+
+static SDL_FColor output_colour(const char *name);
+
 static void draw_workspace(int idx)
 {
     Ws *ws = &WSS[idx];
@@ -4126,8 +4411,18 @@ static void draw_workspace(int idx)
 
     bool has_hit = qlen == 0 || ws->count == 0 || ws_first_visible(ws) >= 0;
 
+    /* A workspace on another screen, shown because something is being
+     * dragged: a real place to drop on, but not here. Sinking it into the
+     * background was too polite to see at a glance, so it wears its
+     * monitor's colour instead — the tile is tinted with it, the border is
+     * it, and the name sits in a badge of it. */
+    bool elsewhere = ws->output[0] && FOCUSED_OUTPUT[0] &&
+                     strcmp(ws->output, FOCUSED_OUTPUT) != 0;
+    SDL_FColor ocol = output_colour(ws->output);
+
     SDL_FColor fill = selected ? C.tile_sel : (hovered ? C.tile_hover : C.tile);
     if (!has_hit) fill = with_alpha(mix(fill, C.bg, 0.35f), fill.a * 0.75f);
+    if (elsewhere) fill = mix(fill, ocol, 0.16f);
 
     drop_shadow(tile, rad, 7.0f * SC, C.shadow_col);
     fill_round_rect(tile, rad, fill);
@@ -4141,6 +4436,10 @@ static void draw_workspace(int idx)
                            bc = with_alpha(C.match, 0.8f);
     else if (ws->visible)  bc = with_alpha(C.current, 0.75f);
     else                   bw = SDL_max(1.0f, C.border * SC * 0.45f);
+    if (elsewhere && !selected) {
+        bc = with_alpha(ocol, 0.95f);
+        bw = SDL_max(1.5f, C.border * SC * 1.3f);
+    }
     stroke_round_rect(tile, rad, bw, bc);
 
     /* ---- tile header: count left, name centred, number right ---- */
@@ -4182,6 +4481,22 @@ static void draw_workspace(int idx)
         Tex t = text_make_fit(F_LABEL, buf[0] ? buf : "\xe2\x96\x8f", (int)(box.w - p));
         tex_draw(t, box.x + p * 0.5f, box.y + (box.h - (float)t.h) * 0.5f, C.text);
         tex_free(&t);
+    } else if (elsewhere && tb_w > 20.0f * SC) {
+        /* the monitor first, in its own colour, then the workspace */
+        Tex on = text_make_fit(F_LABEL, ws->output, (int)(tb_w * 0.55f));
+        float bh = (float)on.h + p * 0.5f;
+        SDL_FRect pill = { tb_x, top - p * 0.2f, (float)on.w + p, bh };
+        fill_round_rect(pill, bh * 0.35f, ocol);
+        tex_draw(on, pill.x + p * 0.5f, pill.y + (bh - (float)on.h) * 0.5f, C.bg);
+        tex_free(&on);
+
+        if (ws->title.t) {
+            float left = pill.x + pill.w + p * 0.6f;
+            Tex t = text_make_fit(F_LABEL, ws->name, (int)(tb_x + tb_w - left));
+            tex_draw(t, left, top + (bh - (float)t.h) * 0.5f - p * 0.2f,
+                     with_alpha(C.text, 0.9f));
+            tex_free(&t);
+        }
     } else if (ws->title.t && tb_w > 20.0f * SC) {
         Tex t = ws->title;
         float x = tb_x + (tb_w - (float)t.w) * 0.5f;
@@ -4411,8 +4726,37 @@ static void draw_outputs_map(void)
         if (r.w < 4.0f || r.h < 4.0f) continue;
 
         bool showing = strcmp(d->name, FOCUSED_OUTPUT) == 0;
-        fill_round_rect(r, rad, showing ? mix(C.tile_sel, C.hl, 0.25f)
-                                        : with_alpha(C.tile, 0.75f));
+
+        /* Held over: the plate presses in a little and fills from the left,
+         * so the wait reads as a button going down rather than a pause. */
+        float held = 0.0f;
+        if (i == MAP_HOVER && C.map_dwell_ms > 0)
+            held = SDL_clamp((float)((now_secs() - MAP_HOVER_SINCE) /
+                                     (C.map_dwell_ms / 1000.0)), 0.0f, 1.0f);
+        if (held > 0.0f) {
+            float in = held * SDL_min(r.w, r.h) * 0.06f;   /* pressed in */
+            r.x += in; r.y += in; r.w -= 2.0f * in; r.h -= 2.0f * in;
+        }
+
+        /* the same colour its workspaces wear while dragging, so the map and
+           the grid agree about which screen is which */
+        SDL_FColor oc = output_colour(d->name);
+        fill_round_rect(r, rad, showing ? mix(C.tile_sel, oc, 0.30f)
+                                        : mix(with_alpha(C.tile, 0.8f), oc, 0.35f));
+        stroke_round_rect(r, rad, SDL_max(1.0f, 1.2f * SC),
+                          with_alpha(oc, showing ? 0.95f : 0.7f));
+
+        if (held > 0.0f) {
+            SDL_FRect fillr = { r.x, r.y, r.w * held, r.h };
+            SDL_Rect clip = { (int)r.x, (int)r.y, (int)SDL_ceilf(fillr.w), (int)SDL_ceilf(r.h) };
+            SDL_Rect prev;
+            bool had = SDL_GetRenderClipRect(REN, &prev) && SDL_RenderClipEnabled(REN);
+            SDL_SetRenderClipRect(REN, &clip);
+            fill_round_rect(r, rad, with_alpha(C.hl, 0.55f + 0.35f * held));
+            SDL_SetRenderClipRect(REN, had ? &prev : NULL);
+            stroke_round_rect(r, rad, SDL_max(1.0f, 2.0f * SC), with_alpha(C.hl, 0.95f));
+        }
+
         if (d->focused)
             stroke_round_rect(r, rad, SDL_max(1.0f, 2.0f * SC),
                               with_alpha(C.current, 0.9f));
@@ -4421,12 +4765,59 @@ static void draw_outputs_map(void)
         if (t.t) {
             tex_draw(t, r.x + (r.w - (float)t.w) * 0.5f,
                      r.y + (r.h - (float)t.h) * 0.5f,
-                     showing ? C.hltext : C.subtext);
+                     showing ? C.hltext : with_alpha(C.text, 0.85f));
             tex_free(&t);
         }
     }
 }
 
+/* Looking at a screen you are not sitting in front of is easy to forget, and
+ * everything you press lands over there. The whole overview gets a frame in
+ * that monitor's colour, the same one its workspaces and its plate on the map
+ * wear — hard to mistake for the ordinary view, impossible to miss. */
+static void draw_pinned_frame(void)
+{
+    if (!PINNED_OUTPUT || !FOCUSED_OUTPUT[0]) return;
+
+    SDL_FColor oc = output_colour(FOCUSED_OUTPUT);
+    float w = SDL_max(3.0f, 5.0f * SC);
+    SDL_FRect r = { w * 0.5f, w * 0.5f, (float)RW - w, (float)RH - w };
+    stroke_round_rect(r, C.radius * SC, w, with_alpha(oc, 0.9f));
+
+    /* and a second, softer one just inside it, so it reads as a frame rather
+       than a window border the compositor drew */
+    SDL_FRect inner = { r.x + w, r.y + w, r.w - 2.0f * w, r.h - 2.0f * w };
+    stroke_round_rect(inner, C.radius * SC, SDL_max(1.0f, 1.5f * SC),
+                      with_alpha(oc, 0.28f));
+}
+
+static void draw_cancel_target(void)
+{
+    if (CANCEL_RECT.w < 8.0f || !drag_active) return;
+
+    SDL_FRect r = CANCEL_RECT;
+    bool hot = drop_kind == DROP_CANCEL;
+    float rad = C.radius * SC;                 /* the same corner as a tile */
+
+    fill_round_rect(r, rad, hot ? with_alpha(C.urgent, 0.18f)
+                                : with_alpha(C.tile, 0.28f));
+    if (hot)
+        stroke_round_rect(r, rad, SDL_max(1.0f, 1.5f * SC),
+                          with_alpha(C.urgent, 0.85f));
+
+    /* A small ✕ in the middle, two strokes rather than a line of dots — the
+     * dots were what made it look chewed. */
+    float box = r.h * 0.20f, th = SDL_max(1.0f, 1.6f * SC);
+    SDL_FColor c = with_alpha(hot ? C.urgent : C.subtext, hot ? 0.95f : 0.45f);
+    float cx = r.x + r.w * 0.5f, cy = r.y + r.h * 0.5f;
+
+    set_col(c);
+    for (float o = -th * 0.5f; o <= th * 0.5f; o += 0.7f) {
+        SDL_RenderLine(REN, cx - box + o, cy - box, cx + box + o, cy + box);
+        SDL_RenderLine(REN, cx - box + o, cy + box, cx + box + o, cy - box);
+    }
+}
+
 static void render(void)
 {
     SDL_SetRenderDrawBlendMode(REN, SDL_BLENDMODE_NONE);
@@ -4488,9 +4879,11 @@ static void render(void)
         if (i != sel_ws) draw_workspace(i);
     draw_workspace(sel_ws);                       /* selection is drawn last */
 
-    if (drag_active) draw_drop_indicator();
-
     draw_outputs_map();
+    draw_cancel_target();
+    draw_pinned_frame();
+
+    if (drag_active) draw_drop_indicator();
 
     float m = C.margin * SC;
     float head_x0 = (float)RW, head_x1 = 0.0f;
@@ -4501,15 +4894,14 @@ static void render(void)
                            : (float)RH - FOOTER_H * 0.5f - (float)T_HEADER.h * 0.5f;
         head_x0 = pos_x(C.header_pos, (float)T_HEADER.w, m);
         head_x1 = head_x0 + (float)T_HEADER.w;
+        SDL_FColor pin = output_colour(FOCUSED_OUTPUT);
         if (PINNED_OUTPUT) {                       /* not the screen you are on */
-            float p = 6.0f * SC;
+            float p = 8.0f * SC;
             SDL_FRect box = { head_x0 - p, y - p * 0.5f,
                               (float)T_HEADER.w + 2.0f * p, (float)T_HEADER.h + p };
-            fill_round_rect(box, box.h * 0.35f, with_alpha(C.hl, 0.22f));
-            stroke_round_rect(box, box.h * 0.35f, SDL_max(1.0f, 1.5f * SC),
-                              with_alpha(C.hl, 0.85f));
+            fill_round_rect(box, box.h * 0.35f, pin);
         }
-        tex_draw(T_HEADER, head_x0, y, PINNED_OUTPUT ? C.hl : C.hint);
+        tex_draw(T_HEADER, head_x0, y, PINNED_OUTPUT ? C.bg : C.hint);
     }
 
     if (T_QUERY.t) {
@@ -4543,7 +4935,6 @@ static void render(void)
 /* --------------------------------------------------------------- actions */
 
 static bool running = true;
-static bool dirty   = true;      /* a frame is only drawn when this is set */
 static bool BACKDROP;            /* drawn behind another program's window   */
 
 /* swbr writes a new set of numbers every few seconds; the overview sits open
@@ -4572,6 +4963,33 @@ static void cpu_poll(void)
     dirty = true;
 }
 
+/* A fullscreen window is above everything a normal window can reach, so an
+ * overlay that is not on the layer shell simply cannot be seen over it. The
+ * only honest way is to take it off fullscreen for as long as we are up, and
+ * put it back exactly as it was on the way out. */
+static int FS_RESTORE;
+
+static void fullscreen_step_aside(void)
+{
+    if (!C.over_fullscreen || BACKDROP) return;
+
+    for (int i = 0; i < NWIN; ++i) {
+        if (!WINS[i].fullscreen) continue;
+        if (FOCUSED_OUTPUT[0] && WINS[i].ws >= 0 && WINS[i].ws < NWS &&
+            strcmp(WSS[WINS[i].ws].output, FOCUSED_OUTPUT) != 0) continue;
+        FS_RESTORE = WINS[i].con_id;
+        sway_cmd("[con_id=%d] fullscreen disable", FS_RESTORE);
+        break;
+    }
+}
+
+static void fullscreen_put_back(void)
+{
+    if (FS_RESTORE <= 0) return;
+    sway_cmd("[con_id=%d] fullscreen enable", FS_RESTORE);
+    FS_RESTORE = 0;
+}
+
 static void reload_model(void)
 {
     /* remember what was selected so a reload does not lose the cursor */
@@ -4610,6 +5028,7 @@ static void reload_model(void)
 
     cpu_load();                  /* whatever swbr last measured */
     mark("cpu numbers");
+    if (!FS_RESTORE) fullscreen_step_aside();
     layout();
     apply_filter();
     rebuild_chrome();
@@ -4844,7 +5263,30 @@ static float        MOUSE_SCALE = 1.0f;
  * and gets back the workspace under them. It fades in so that picking an app
  * before the tree is even read shows nothing at all, rather than a frame that
  * flashes into view. */
+/* Anything else starting with a dash is a typo, and saying so beats doing
+ * nothing quietly — but only for names that are not options at all. */
+static bool arg_is_known(const char *a)
+{
+    static const char *KNOWN[] = {
+        "-h", "-n", "-v", "--help", "--version", "--info", "--usage",
+        "--workspaces", "--adopt", "--adopt-debug", "--adopt-focus",
+        "--adopt-timeout", "--adopt-wait", "--beside", "--edge", "--no-assign",
+        "--backdrop", "--backdrop-debug", "--timing", "--shot", "--mouse",
+        "--go", "--set", "--back", "--config", "--no-config", "--",
+    };
+    for (size_t i = 0; i < SDL_arraysize(KNOWN); ++i)
+        if (!strcmp(a, KNOWN[i])) return true;
+    return false;
+}
+
 static bool  BACKDROP;
+static int   OV_IN = 0, OV_OUT = 1;   /* the launcher's pipes, or our own
+                                         stdin and stdout when spawned */
+static bool  SERVING;                 /* a launcher of ours is in front */
+static void  serve_end(void);
+static void  backdrop_reply(const char *fmt, ...);
+static void  backdrop_raise_tick(void);
+static pid_t SERVE_PID;
 static float BACKDROP_A;              /* what is drawn now, 0..1 */
 static float BACKDROP_WANT = 1.0f;    /* where it is heading      */
 static bool  BACKDROP_LEAVING;
@@ -4977,6 +5419,68 @@ static int digit_from_scancode(SDL_Scancode sc)
 static void begin_edit(int idx);
 static void end_edit(bool commit);
 
+/* Hand the overview to a launcher without giving up being the overview.
+ *
+ * swas normally starts its own backdrop, so pressing `d` here would replace
+ * this window with an identical one. Instead it is started with two pipes and
+ * told to talk to us: the wheel appears in front, this stays behind it, and
+ * when the wheel is done we are still here. */
+static void serve_end(void)
+{
+    if (!SERVING) return;
+    SERVING = false;
+    if (OV_IN  > 2) close(OV_IN);
+    if (OV_OUT > 2) close(OV_OUT);
+    OV_IN = 0; OV_OUT = 1;
+    if (SERVE_PID > 0) { waitpid(SERVE_PID, NULL, WNOHANG); SERVE_PID = 0; }
+
+    drag_active = false;
+    drop_kind = DROP_NONE;
+    set_ghosts(-1, -1);
+    dirty = true;
+}
+
+static void serve_launcher(const char *cmd)
+{
+    if (!cmd || !*cmd || SERVING) return;
+
+    int to_swov[2], to_swas[2];
+    if (pipe(to_swov) != 0) return;                 /* swas -> us */
+    if (pipe(to_swas) != 0) { close(to_swov[0]); close(to_swov[1]); return; }
+
+    pid_t p = fork();
+    if (p < 0) {
+        close(to_swov[0]); close(to_swov[1]);
+        close(to_swas[0]); close(to_swas[1]);
+        return;
+    }
+    if (p == 0) {
+        char in[16], out[16];
+        snprintf(in,  sizeof(in),  "%d", to_swas[0]);   /* swas reads this */
+        snprintf(out, sizeof(out), "%d", to_swov[1]);   /* and writes this */
+        close(to_swov[0]);
+        close(to_swas[1]);
+        setenv("SWAS_OV_IN",  in,  1);
+        setenv("SWAS_OV_OUT", out, 1);
+        setsid();
+        execl("/bin/sh", "sh", "-c", cmd, (char *)NULL);
+        _exit(127);
+    }
+
+    close(to_swov[1]);
+    close(to_swas[0]);
+    OV_IN  = to_swov[0];
+    OV_OUT = to_swas[1];
+    SERVE_PID = p;
+    SERVING = true;
+    fprintf(stderr, "swov: serving '%s' as its overview (pid %d)\n", cmd, (int)p);
+
+    int fl = fcntl(OV_IN, F_GETFL, 0);
+    if (fl != -1) fcntl(OV_IN, F_SETFL, fl | O_NONBLOCK);
+
+    backdrop_reply("ready");
+}
+
 static void handle_key(const SDL_KeyboardEvent *k)
 {
     bool shift = (k->mod & SDL_KMOD_SHIFT) != 0;
@@ -5098,6 +5602,10 @@ static void handle_key(const SDL_KeyboardEvent *k)
         else                        step_ws(shift ? -1 : 1);
         break;
 
+    case SDLK_D:                                   /* the launcher, in front */
+        if (C.launcher[0] && !SERVING) serve_launcher(C.launcher);
+        break;
+
     case SDLK_W:                                   /* window level <-> workspace */
         if (NWS > 0) {
             Ws *ws = &WSS[sel_ws];
@@ -5318,6 +5826,8 @@ static void handle_mouse_motion(const SDL_MouseMotionEvent *mo)
     }
 
     if (drag_active) {
+        int dws = -1;
+        lift_update(mx, my, dws, hit_test(mx, my, &dws));
         drag_update_target(mx, my);
         dirty = true;
         return;
@@ -5325,6 +5835,7 @@ static void handle_mouse_motion(const SDL_MouseMotionEvent *mo)
 
     int ws_idx = -1;
     int win_idx = hit_test(mx, my, &ws_idx);
+    lift_update(mx, my, ws_idx, win_idx);
     if (ws_idx == hov_ws && win_idx == hov_win) return;
 
     hov_ws = ws_idx;
@@ -5430,7 +5941,11 @@ static void handle_event(const SDL_Event *e)
         break;
 
     case SDL_EVENT_WINDOW_FOCUS_LOST:
-        if (C.quit_on_focus_loss) running = false;
+        /* Losing the keyboard normally means the overview's moment has
+         * passed. Not while we are the backdrop for a launcher we started:
+         * that launcher taking the focus is the whole point, and quitting
+         * here is what made the overview vanish the instant swas appeared. */
+        if (C.quit_on_focus_loss && !SERVING && !BACKDROP) running = false;
         break;
 
     default:
@@ -5451,12 +5966,16 @@ static void handle_event(const SDL_Event *e)
  */
 static void backdrop_reply(const char *fmt, ...)
 {
+    char line[512];
     va_list ap;
     va_start(ap, fmt);
-    vprintf(fmt, ap);
+    int n = vsnprintf(line, sizeof(line) - 2, fmt, ap);
     va_end(ap);
-    fputc('\n', stdout);
-    fflush(stdout);
+    if (n < 0) return;
+    line[n] = '\n';
+    line[n + 1] = 0;
+    ssize_t w = write(OV_OUT, line, (size_t)n + 1);
+    (void)w;
 }
 
 static void backdrop_hover(float fx, float fy)
@@ -5465,7 +5984,7 @@ static void backdrop_hover(float fx, float fy)
 
     int si = slot_at(x, y);
     if (si >= 0 && SLOTS[si].ws < 0) {            /* a free number */
-        sel_active = false;
+
         drop_kind  = DROP_WIN_NEWWS;              /* the ghost lights up */
         drop_num   = SLOTS[si].num;
         drag_active = true;
@@ -5474,13 +5993,34 @@ static void backdrop_hover(float fx, float fy)
         return;
     }
 
+    if (point_in_cancel(x, y)) {              /* let go here and nothing happens */
+        drag_active = true;
+        drop_kind = DROP_CANCEL;
+        MAP_HOVER = -1;
+        dirty = true;
+        backdrop_reply("target none");
+        return;
+    }
+
+    /* over the map of the monitors: hold still and we step onto that one */
+    if (MAP_RECT.w > 0.0f) {
+        bool on_map = map_dwell_hover(x, y);
+        if (on_map) {
+
+            drag_active = false;
+            drop_kind = DROP_NONE;
+            backdrop_reply("target none");
+            return;
+        }
+    }
+
     int ws_idx = -1;
     int win_idx = hit_test(x, y, &ws_idx);
     drag_active = false;
     drop_kind = DROP_NONE;
 
     if (ws_idx < 0) {
-        sel_active = false;
+
         dirty = true;
         backdrop_reply("target none");
         return;
@@ -5555,6 +6095,7 @@ static bool backdrop_command(char *line)
         return true;
     }
     if (!strcmp(line, "drag on")) {
+        if (C.drop_outputs) show_all_outputs(true);
         /* The free numbers become ghost tiles to drop on, exactly as they do
          * when a window is dragged inside swov — dropping an app on a
          * workspace that does not exist yet has to work the same either way.
@@ -5565,6 +6106,8 @@ static bool backdrop_command(char *line)
         return true;
     }
     if (!strcmp(line, "drag off")) {
+        MAP_HOVER = -1;
+        show_all_outputs(false);
         set_ghosts(-1, -1);
         BLUR_WANT = 1.0f;
         drag_active = false;
@@ -5600,10 +6143,16 @@ static void backdrop_poll(void)
     static char buf[512];
     static int  len;
 
-    struct pollfd p = { 0, POLLIN, 0 };
+    struct pollfd p = { OV_IN, POLLIN, 0 };
     while (poll(&p, 1, 0) > 0 && (p.revents & (POLLIN | POLLHUP))) {
-        ssize_t n = read(0, buf + len, sizeof(buf) - 1 - (size_t)len);
-        if (n <= 0) { BACKDROP_LEAVING = true; return; }   /* parent is gone */
+        ssize_t n = read(OV_IN, buf + len, sizeof(buf) - 1 - (size_t)len);
+        if (n <= 0) {
+            /* Serving a launcher we started ourselves: it has finished, so
+             * go back to being an overview rather than leaving with it. */
+            if (SERVING) { serve_end(); len = 0; return; }
+            BACKDROP_LEAVING = true;
+            return;
+        }
         len += (int)n;
         buf[len] = 0;
 
@@ -5642,9 +6191,59 @@ static void backdrop_raise_tick(void)
 }
 
 /* ease towards the wanted alpha; leaving means fade out, then exit */
+/* the hold, checked every frame: the pointer may have stopped moving, so no
+ * further hover ever arrives */
+static void map_dwell_tick(void)
+{
+    if (MAP_HOVER < 0) return;
+    dirty = true;                                  /* the fill is growing */
+
+    double want = C.map_dwell_ms / 1000.0;
+    if (want <= 0.0) want = 0.001;
+    if (now_secs() - MAP_HOVER_SINCE < want) return;
+
+    str_set(FOCUSED_OUTPUT, sizeof(FOCUSED_OUTPUT), OUTS[MAP_HOVER].name);
+    PINNED_OUTPUT = strcmp(FOCUSED_OUTPUT, HOME_OUTPUT) != 0;
+    MAP_HOVER = -1;
+
+    /* A window being dragged from here is about to vanish from the model —
+     * it lives on the screen we are leaving. Keep hold of its id so it can
+     * still be dropped on the screen we are arriving at. */
+    if (press_win >= 0) DRAG_CON = WINS[press_win].con_id;
+
+    drop_kind = DROP_NONE;
+    drop_ws = drop_win = -1;
+    press_win = press_ws = -1;
+    sel_active = false;
+    reload_model();
+
+    if (DRAG_CON > 0) {                            /* find it again, or keep
+                                                      dragging by id alone */
+        for (int i = 0; i < NWIN; ++i)
+            if (WINS[i].con_id == DRAG_CON) { press_win = i; break; }
+    }
+
+    /* The drag is still in the air — a screen changed under it, nothing was
+     * let go of. Say so, or a launcher waiting on the other end of the pipe
+     * acts on the target it had a moment ago. */
+    if (BACKDROP || SERVING) {
+        drag_active = true;
+        set_ghosts(C.drop_ghosts ? 0 : -1, C.drop_ghosts ? 10 : -1);
+        backdrop_reply("target none");
+
+        /* The app on the pointer is drawn by the launcher, on its own
+         * surface. Redrawing everything here is enough for sway to put this
+         * window in front of it, and then the icon is behind the overview
+         * for the rest of the drag. Ask for it back. */
+        if (RAISE_APP[0]) { RAISE_LEFT = 3; RAISE_AT = 0.0; }
+    }
+    dirty = true;
+}
+
 static void backdrop_step(float ms)
 {
     backdrop_raise_tick();
+    map_dwell_tick();
 
     if (BACKDROP_LEAVING) BACKDROP_WANT = 0.0f;
 
@@ -5846,6 +6445,8 @@ int main(int argc, char **argv)
         else if (!strcmp(a, "--adopt-focus")) adopt_focus = true;
         else if (!strcmp(a, "--adopt-debug")) ADOPT_LOG = true;
         else if (!strcmp(a, "--timing")) TIMING = true;
+        else if (a[0] == '-' && !strchr(a, '=') && !arg_is_known(a))
+            fprintf(stderr, "swov: unknown option '%s' (see --help)\n", a);
         else if (!strcmp(a, "--no-assign")) adopt_no_assign = true;
         else if (!strcmp(a, "--beside") && i + 1 < argc) adopt_beside = atoi(argv[++i]);
         else if (!strcmp(a, "--edge") && i + 1 < argc)   adopt_edge = argv[++i];
@@ -6157,16 +6758,18 @@ int main(int argc, char **argv)
         SDL_Event e;
         if (anim_running()) dirty = true;
         cpu_poll();
+        if (drag_active) map_dwell_tick();
 
-        if (BACKDROP) {
+        if (BACKDROP || SERVING) {
             backdrop_poll();
+            if (SERVING) backdrop_raise_tick();
             Uint64 now = SDL_GetTicks();
-            backdrop_step((float)(now - last_tick));
+            if (BACKDROP) backdrop_step((float)(now - last_tick));
             last_tick = now;
             if (!running) break;
         }
 
-        if (SDL_WaitEventTimeout(&e, BACKDROP ? 8
+        if (SDL_WaitEventTimeout(&e, (BACKDROP || SERVING) ? 8
                                     : (anim_running() || pending_reload) ? 8 : 60)) {
             handle_event(&e);
             while (running && SDL_PollEvent(&e)) handle_event(&e);   /* coalesce */
@@ -6210,6 +6813,7 @@ int main(int argc, char **argv)
     TTF_Quit();
     SDL_DestroyRenderer(REN);
     SDL_DestroyWindow(WIN_HANDLE);
+    fullscreen_put_back();
     SDL_Quit();
     if (sway_fd >= 0) close(sway_fd);
     if (sway_evt_fd >= 0) close(sway_evt_fd);