SWOv git · main
SWOv - window overview for sway
C 91.5% Markdown 7.6%git clone https://git.christianimmanuel.de/sway/SWOv.gitwget https://git.christianimmanuel.de/sway/SWOv/archive/SWOv.tar.gzBettter handling of dragging into floating windows
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>, &) - * 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=..>, &)\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, ®istry_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);