SwAS git · main
SwAS - app switcher for sway (SVG/PNG icons, themeable)
C 97.5% Markdown 1.6%git clone https://git.christianimmanuel.de/sway/SwAS.gitwget https://git.christianimmanuel.de/sway/SwAS/archive/SwAS.tar.gzFix dmenu connection and maybe stuff
Makefile | 23 +++++- README.md | 53 +++++++++++-- config.example | 13 +++- swas.c | 229 +++++++++++++++++++++++++++++++++++++++++++++++++++++---- 4 files changed, 294 insertions(+), 24 deletions(-) diff --git a/Makefile b/Makefile index d3d3b3a..d43c2e0 100644 --- a/Makefile +++ b/Makefile @@ -4,8 +4,11 @@ # make static build with SDL3 baked in (no libSDL3.so needed at runtime) # make strict build with -Wall -Wextra (for hacking on it) # make run build and run -# make install install to $(PREFIX)/bin (default: ~/.local) +# make install install to $(PREFIX)/bin (default: /usr) +# the example goes to $(PREFIX)/share/swas # make config write a default config to ~/.config/swas/config +# make link symlink ~/.config/swas/config at the installed example +# make uninstall # make clean # # Requires: a C compiler, pkg-config, and SDL3 dev files. @@ -20,8 +23,9 @@ CC ?= cc PKG_CONFIG ?= pkg-config CFLAGS ?= -O2 -PREFIX ?= $(HOME)/.local +PREFIX ?= /usr bindir ?= $(PREFIX)/bin +datadir ?= $(PREFIX)/share/swas BUILD := $(shell md5sum swas.c 2>/dev/null | cut -c1-8) SRC := swas.c @@ -33,7 +37,7 @@ SDL_LDPATH := $(shell $(PKG_CONFIG) --libs-only-L sdl3) # transitive libs SDL needs when it is linked statically, minus SDL itself: SDL_STATIC_EXTRA := $(filter-out -lSDL3,$(shell $(PKG_CONFIG) --static --libs sdl3)) -.PHONY: all static strict run install config clean +.PHONY: all static strict run install config link uninstall clean all: $(BIN) @@ -58,7 +62,9 @@ run: all install: all install -Dm755 $(BIN) $(DESTDIR)$(bindir)/$(BIN) + install -Dm644 config.example $(DESTDIR)$(datadir)/config.example @echo "installed to $(DESTDIR)$(bindir)/$(BIN)" + @echo "example $(DESTDIR)$(datadir)/config.example" @./$(BIN) --version config: all @@ -66,5 +72,16 @@ config: all ./$(BIN) --dump-config > $(HOME)/.config/swas/config @echo "wrote $(HOME)/.config/swas/config" +# The shipped example, live: every install updates what you are running. +link: + @mkdir -p $(HOME)/.config/swas + ln -sfn $(datadir)/config.example $(HOME)/.config/swas/config + @echo "$(HOME)/.config/swas/config -> $(datadir)/config.example" + +uninstall: + rm -f $(DESTDIR)$(bindir)/$(BIN) + rm -f $(DESTDIR)$(datadir)/config.example + -rmdir $(DESTDIR)$(datadir) 2>/dev/null || true + clean: rm -f $(BIN) diff --git a/README.md b/README.md index a4f4dc5..1ad10e3 100644 --- a/README.md +++ b/README.md @@ -14,8 +14,9 @@ Debian trixie and newer: ```sh sudo apt install build-essential pkg-config libsdl3-dev make -make install # ~/.local/bin, or PREFIX=/usr/local +sudo make install # /usr/bin, examples in /usr/share/swas make config # optional: write a default config to ~/.config/swas/config +make link # or: config -> the installed example, kept in sync ``` `make static` bakes SDL3 into the binary, so it runs without `libSDL3.so`. It @@ -44,9 +45,20 @@ for_window [app_id="swas"] floating enable, border none | `esc` | clear the filter, else quit | | right click | quit | -Apps are read from the `.desktop` files in the usual directories. The wheel -shows the most recently opened ones first; every launch is written to -`~/.cache/swas/history`. +Apps are read from the `.desktop` files in the usual directories. The one you +used last sits at the top of the arc, under the pointer, and distance from the +top is how long ago you used it — second to the right, third to the left, +fourth to the right, and so on. Past the end of the history the rest carry on +outwards in alphabetical order: + +``` +history: firefox foot code gimp mpv, then the alphabet + +darktable blender mpv code [firefox] foot gimp ardour calibre +``` + +`center_first=0` fills the arc from the left instead. Every launch is written +to `~/.cache/swas/history`. ```sh swas --list # every id, name and resolved icon @@ -54,6 +66,11 @@ swas include=firefox,code,gimp # a curated wheel, in that order swas --dmenu < items # a dmenu-style picker on stdin/stdout ``` +In scripts written for dmenu or bemenu, `MENU=swas` works as it is: dmenu's own +flags (`-l 10`, `-p ""`, `-i`, ...) switch on dmenu mode and are otherwise +ignored. Blank lines in the input are skipped. Cancelling exits non-zero, like +dmenu. + ## Drop onto a workspace Drag an app out of the wheel and let go over a workspace: it starts *there*, @@ -69,6 +86,19 @@ while you hover there. `esc` and right click cancel too. While it is small it sits on a disc of its own (`drop_bg`), so it keeps an edge against a busy overview instead of reading as a handful of loose marks. +When an app does not appear, swas says so under the wheel for a few seconds: +`could not start Firefox` if the launch itself failed, `Firefox did not start` +if the process was gone a second and a half later — a typo in a `.desktop` +file, a missing binary, something that exits on startup. It stays quiet for +anything that is merely slow. + +`drop_assign=0` opens the app on the workspace you are on and moves it after, +instead of assigning it first. Assigning is tidier — sway puts the window +straight where it belongs — but it means the app maps on a workspace that is +not on screen, and a toolkit that reads its scale from the output it lands on +has no output to read. GTK and WebKit do that, and come out at scale 1 on a +scaled screen, which looks like everything is too big. + `drop_size` is that width in pixels, which is what you actually want to say rather than a fraction of a screen you have to work out; `drop_size=0` falls back to `drop_shrink` as a fraction. `drop_corner` moves it out of the middle @@ -83,7 +113,7 @@ sway makes it. underneath, showing every workspace with the windows already in it. ``` -bindsym $mod+space exec swas overview=1 +bindsym $mod+space exec swas --replace overview=1 no_focus [app_id="swov-backdrop"] for_window [app_id="swov-backdrop"] floating enable, border none @@ -99,6 +129,14 @@ new ones are absent, so nothing is lost by not moving them. argument — it is *not* something you put after `for_window`. Written the wrong way round it fails silently at load and the backdrop takes your keyboard. +Started by swov (its `d` key), swas talks to that overview instead of +bringing up its own: the wheel appears in front of it, and closes again after +one drop, leaving the overview where it was. That is what `SWAS_OV_IN` and +`SWAS_OV_OUT` in the environment mean. + +`--replace` closes a wheel that is already open before starting, so pressing +the binding twice gives you one wheel rather than two stacked on each other. + The `no_focus` line matters. Without it sway hands the keyboard to the backdrop the moment it appears and the first letters you type are lost. @@ -147,6 +185,11 @@ workspace list and hands it the launched process, which swov follows until its window appears and then moves. Without swov the feature is simply off. Set `drop_focus=1` to follow the app to its workspace instead of staying put. +`make install` also drops the shipped examples in `/usr/share/swas/`. +`make link` points your config at one of them, so the next install is the +config you are running — handy if you want to follow the examples rather than +keep your own copy. Edits you make there are overwritten on install. + ## Config `${XDG_CONFIG_HOME:-~/.config}/swas/config`, `key=value`, `#` comments. diff --git a/config.example b/config.example index 83fc570..3bb176b 100644 --- a/config.example +++ b/config.example @@ -55,6 +55,11 @@ drop_corner=center # where it sits while dragging: center, top-left, # wheel cancels wherever it is drop_ms=130 # how long that takes drop_px=10 # movement before a press turns into a drag +drop_assign=1 # the app is assigned to its workspace before it opens, so + # sway puts it there as it maps. Set 0 if a toolkit comes + # out the wrong size: it then opens on the workspace you + # are on — where it can see the output and its scale — and + # is moved afterwards drop_focus=0 # 1 = also switch to that workspace drop_close_here=0 # the wheel stays open after every drop, so you can keep # filling workspaces. 1 = close when you drop on the @@ -69,7 +74,13 @@ overview_debug=0 # 1 = print the conversation with the backdrop to stderr # --- launching / ordering --- # swas logs every app you launch to the history file below and shows the -# most-recently-opened ones first. This is the default (sort=recent). +# most-recently-opened ones first. This is the default (center_first=1 # the app you used last sits at the top of the arc, and + # distance from the top is how long ago you used it: second + # to the right, third to the left, fourth to the right, and + # so on. Past the end of the history the rest carry on + # outwards in alphabetical order. + # 0 = fill the arc from the left, oldest layout +sort=recent). sort=recent # recent = most-recently-opened first (DEFAULT) | alpha = A-Z launcher=sh # sh = run Exec= ; gtk-launch = launch by desktop id terminal=xterm # used for Terminal=true entries when launcher=sh diff --git a/swas.c b/swas.c index fb9fb1d..d1b688c 100644 --- a/swas.c +++ b/swas.c @@ -136,10 +136,14 @@ typedef struct { char sort[16], launcher[16], terminal[128]; char history[PATH_MAX], dirs[4096], include[8192], exclude[8192]; int drop; /* drag an app onto a workspace: 0 = off */ + int center_first; /* the most recent app at the top, the + rest spreading either side of it */ int overview; /* 1 = swov behind the wheel as the target */ int overview_debug; /* narrate the backdrop conversation */ float drop_shrink; /* how small the wheel gets while dragging */ int drop_ms, drop_px, drop_focus; + int drop_assign; /* assign the pid before the app opens, + or let it open here and move it */ int drop_close_here; /* dropped on the workspace we are on: close, or the app opens behind us */ char drop_corner[16]; /* where the wheel sits while dragging: @@ -175,8 +179,8 @@ static void config_defaults(Config*c){ 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; - c->drop=1; c->drop_shrink=0.55f; c->drop_ms=130; c->drop_px=10; c->drop_focus=0; - c->overview=0; c->overview_debug=0; c->drop_close_here=0; + c->drop=1; c->drop_shrink=0.55f; c->drop_ms=130; c->drop_px=10; c->drop_assign=1; c->drop_focus=0; + c->center_first=1; c->overview=0; c->overview_debug=0; c->drop_close_here=0; strcpy(c->drop_corner,"center"); c->drop_size=66; strcpy(c->swov,"swov"); strcpy(c->sort,"recent"); strcpy(c->launcher,"sh"); @@ -218,6 +222,7 @@ static void config_set(Config*c,const char*k,const char*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,"drop"))c->drop=atoi(v); + else if(!strcmp(k,"center_first"))c->center_first=atoi(v); else if(!strcmp(k,"overview"))c->overview=atoi(v); else if(!strcmp(k,"overview_debug"))c->overview_debug=atoi(v); else if(!strcmp(k,"drop_close_here"))c->drop_close_here=atoi(v); @@ -227,6 +232,7 @@ static void config_set(Config*c,const char*k,const char*v){ else if(!strcmp(k,"drop_shrink"))c->drop_shrink=(float)atof(v); else if(!strcmp(k,"drop_ms"))c->drop_ms=atoi(v); else if(!strcmp(k,"drop_px"))c->drop_px=atoi(v); + else if(!strcmp(k,"drop_assign"))c->drop_assign=atoi(v); else if(!strcmp(k,"drop_focus"))c->drop_focus=atoi(v); else if(!strcmp(k,"swov"))snprintf(c->swov,sizeof c->swov,"%s",v); else if(!strcmp(k,"font"))snprintf(c->font,sizeof c->font,"%s",v); @@ -588,11 +594,41 @@ static int match_cmp(const void*a,const void*b){ 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){ +static int build_filter(AppList*apps,const char*query,int*filt,int slots,int recent_first,int center_first,int*p_off,int*p_selslot){ int fn=0; - if(!query[0]){ /* no query: all apps, in order */ + if(!query[0]){ + /* No query: the same shape a search gives, with recency standing in + for the ranking. The one you used last sits at the top, the next + few to its right, and the rest carry on round to its left — so the + further from the top an app is, the longer since you opened it. */ for(int i=0;i<apps->n;i++) filt[fn++]=i; - *p_off=0; *p_selslot=0; return fn; + if(fn<1){ *p_off=0; *p_selslot=0; return 0; } + if(!center_first){ *p_off=0; *p_selslot=0; return fn; } + + /* Distance from the top is how long ago you used it: second to the + right, third to the left, fourth to the right, and so on. The list + is already history first and then everything else by name, so once + the history runs out the alphabet simply carries on outwards. When + one side fills up the rest goes on the other. */ + int c=(fn-1)/2, rank=1; + int *tmp=malloc((size_t)fn*sizeof(int)); + if(!tmp){ *p_off=0; *p_selslot=0; return fn; } + tmp[c]=filt[0]; + for(int r=c+1, l=c-1; rank<fn; ){ + if(r<fn && rank<fn) tmp[r++]=filt[rank++]; + if(l>=0 && rank<fn) tmp[l--]=filt[rank++]; + if(r>=fn && l<0) break; + } + memcpy(filt,tmp,(size_t)fn*sizeof(int)); + free(tmp); + + int vis = fn<slots?fn:slots; if(vis<1)vis=1; + if(vis>=4 && (vis&1)==0) vis--; /* odd: dead centre */ + 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; } size_t ql=strlen(query); Match *m=malloc(apps->n*sizeof(Match)); int nm=0; @@ -837,6 +873,60 @@ static void ov_send(const char*fmt,...){ } } +/* Take over from a wheel that is already up. + * + * Two of these on top of each other is confusing and the second one grabs the + * keyboard, so the binding should just open the one that is there. Same + * approach as swbr: clear the way rather than race a pkill. */ +static void replace_running(void){ + DIR*d=opendir("/proc"); if(!d) return; + pid_t me=getpid(), victims[32]; int n=0; + struct dirent*e; + while((e=readdir(d))&&n<(int)(sizeof victims/sizeof*victims)){ + bool digits=e->d_name[0]!=0; + for(const char*q=e->d_name;*q;q++) if(*q<'0'||*q>'9'){digits=false;break;} + if(!digits) continue; + pid_t pid=(pid_t)atoi(e->d_name); + if(pid==me||pid<=1) continue; + char path[64],comm[64]=""; + snprintf(path,sizeof path,"/proc/%d/comm",(int)pid); + FILE*f=fopen(path,"r"); if(!f) continue; + if(fgets(comm,sizeof comm,f)){ char*nl=strchr(comm,'\n'); if(nl)*nl=0; } + fclose(f); + if(strcmp(comm,APP_ID)) continue; + struct stat st; snprintf(path,sizeof path,"/proc/%d",(int)pid); + if(stat(path,&st)!=0||st.st_uid!=getuid()) continue; + victims[n++]=pid; + } + closedir(d); + for(int i=0;i<n;i++) kill(victims[i],SIGTERM); + for(int w=0;w<40&&n;w++){ + usleep(50000); + int left=0; + for(int i=0;i<n;i++) if(kill(victims[i],0)==0) victims[left++]=victims[i]; + n=left; + } + for(int i=0;i<n;i++) kill(victims[i],SIGKILL); +} + +static int OV_ONCE=0; /* launched by an overview: close after one drop */ + +/* Started by an overview that is already up: it hands us two pipes and stays + where it is, so the wheel appears in front of the overview instead of + replacing it. One drag, one drop, and we are gone again. */ +static int ov_adopt(void){ + const char*i=getenv("SWAS_OV_IN"), *o=getenv("SWAS_OV_OUT"); + if(!i||!*i||!o||!*o) return 0; + int rd=atoi(i), wr=atoi(o); /* what swov gave us: read, write */ + if(rd<=0||wr<=0) return 0; + signal(SIGPIPE,SIG_IGN); + OV_OUT=rd; /* we read its replies here */ + OV_IN =wr; /* and send it commands here */ + fcntl(OV_OUT,F_SETFL,O_NONBLOCK); + OV_ONCE=1; + return 1; +} + static void ov_spawn(Config*c){ OV_LOG=c->overview_debug; int to[2], from[2]; @@ -926,8 +1016,14 @@ static void adopt_to(Config*c,pid_t pid,const char*target, if(p==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*av[12]; int n=0; + const char*av[14]; int n=0; av[n++]=c->swov; av[n++]="--adopt"; av[n++]=spid; av[n++]=target; + /* Assigning the app to a workspace before it opens means it maps on + one that is not on screen. A toolkit that picks its scale from the + output it lands on has no output to look at, takes 1, and comes out + the wrong size on a scaled screen. Without the rule it opens here, + at the right scale, and is moved after. */ + if(!c->drop_assign) av[n++]="--no-assign"; if(edge&&*edge){ if(beside&&*beside){ av[n++]="--beside"; av[n++]=beside; } av[n++]="--edge"; av[n++]=edge; @@ -948,6 +1044,38 @@ static void wsp_adopt(Config*c,pid_t pid,const Wsp*w){ adopt_to(c,pid,target,NULL,NULL); } +/* Something to say when an app does not appear. + * + * A launcher that silently does nothing is the worst kind: you press, nothing + * happens, and there is no way to tell a typo in a .desktop file from a slow + * program. The pid is watched for a moment after launching, and if it is + * gone by then the app never really started. */ +static char ERR_TEXT[192]; +static double ERR_UNTIL; +static pid_t WATCH_PID; +static double WATCH_AT; +static char WATCH_NAME[64]; + +static double now_secs_(void){ return (double)SDL_GetTicks()/1000.0; } + +static void err_say(const char*fmt,...){ + va_list ap; va_start(ap,fmt); + vsnprintf(ERR_TEXT,sizeof ERR_TEXT,fmt,ap); + va_end(ap); + ERR_UNTIL = now_secs_() + 6.0; +} + +static void watch_tick(void){ + if(!WATCH_PID) return; + if(now_secs_() - WATCH_AT < 1.5) return; + + char path[64]; + snprintf(path,sizeof path,"/proc/%d",(int)WATCH_PID); + if(access(path,F_OK)!=0) + err_say("%s did not start", WATCH_NAME); + WATCH_PID=0; +} + static pid_t launch(App*a,Config*c){ char cmd[8192]; if(!strcmp(c->launcher,"gtk-launch")) snprintf(cmd,sizeof cmd,"gtk-launch %s.desktop",a->id); @@ -977,7 +1105,10 @@ static pid_t launch(App*a,Config*c){ if(pfd[1]>=0){ pid_t me=getpid(); if(write(pfd[1],&me,sizeof me)<0){} - close(pfd[1]); + /* Kept open across the exec, so that if the exec fails this end + closes and the reader learns the app never started. On success + it is closed by the exec itself. */ + fcntl(pfd[1],F_SETFD,FD_CLOEXEC); } int fd=open("/dev/null",O_RDWR); if(fd>=0){ dup2(fd,0); dup2(fd,1); dup2(fd,2); if(fd>2) close(fd); } @@ -993,6 +1124,14 @@ static pid_t launch(App*a,Config*c){ if(read(pfd[0],&app,sizeof app)!=(ssize_t)sizeof app) app=-1; close(pfd[0]); } + + if(app<=0){ + err_say("could not start %s", (a->name&&a->name[0])?a->name:a->id); + } else { + WATCH_PID = app; + WATCH_AT = now_secs_(); + snprintf(WATCH_NAME,sizeof WATCH_NAME,"%s", (a->name&&a->name[0])?a->name:a->id); + } return app; } @@ -1133,6 +1272,10 @@ static void dump_config(void){ " # not get lost over the overview. Alpha 0 for none\n" "drop_ms=130 # how long that takes\n" "drop_px=10 # movement before a press turns into a drag\n" +"drop_assign=1 # assign the app to its workspace before it opens. 0 =\n" +" # let it open on the workspace you are on and move it\n" +" # after, which is what a toolkit that reads its scale from\n" +" # the output it lands on needs\n" "drop_focus=0 # 1 = also switch to that workspace\n" "drop_close_here=0 # dropping on the workspace you are on keeps the wheel\n" " # open too; 1 = close, so the new window is not behind it\n" @@ -1141,6 +1284,9 @@ static void dump_config(void){ "# --- launching / ordering ---\n" "# swas 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" +"center_first=1 # the app you used last at the top of the arc, second to\n" +" # its right, third to its left, and so on outwards; then\n" +" # the rest alphabetically. 0 = fill from the left\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" @@ -1185,9 +1331,12 @@ static void usage(const char*a0){ " --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" +" one to stdout (a dmenu/bemenu/wofi-style picker). dmenu's\n" +" own flags (-l N, -p PROMPT, -i, ...) imply it and are\n" +" otherwise ignored, so MENU=swas works in dmenu scripts\n" " -h, --help show this help and exit\n" " -v, --version print the version and build id, and exit\n" +" --replace close a wheel that is already open, then start\n" "\n" "LAYOUT / INTERACTION\n" " slots=11 wide, easy-to-hit app slots across the top arc\n" @@ -1276,6 +1425,8 @@ static void load_apps(AppList*apps, Config*cfg, int dmenu){ while(fgets(line,sizeof line,stdin)){ size_t L=strlen(line); while(L && (line[L-1]=='\n'||line[L-1]=='\r')) line[--L]=0; + if(!L) continue; /* a blank line is a gap in a list, + not an empty slice on the wheel */ App a; memset(&a,0,sizeof a); a.id=xstrdup(line); a.name=xstrdup(line); applist_push(apps,a); /* input order preserved */ @@ -1290,29 +1441,48 @@ static void load_apps(AppList*apps, Config*cfg, int dmenu){ if(!dmenu){ apply_config_order(apps,cfg); apply_recency(apps,cfg); } } +/* dmenu's own flags. A script written for dmenu or bemenu calls the menu with + -l 10 -p "" and the like, and used to get the app wheel: the flags were + ignored, the pick was launched instead of printed, and the script got an + empty answer. Any of these now means dmenu mode. Returns how many argv + entries the flag takes, or 0 if it is not one of them. */ +static int dmenu_flag(const char*a){ + static const char*with_arg[]={"-l","-p","-P","-fn","-nb","-nf","-sb","-sf", + "-m","-w","-W","--prompt","--lines","--fn",0}; + static const char*bare[]={"-i","-b","-f","-n","--ignorecase",0}; + for(int k=0;with_arg[k];k++) if(!strcmp(a,with_arg[k])) return 2; + for(int k=0;bare[k];k++) if(!strcmp(a,bare[k])) return 1; + return 0; +} + int main(int argc,char**argv){ Config cfg; config_defaults(&cfg); char cfgpath[PATH_MAX]; xdg_path(cfgpath,sizeof cfgpath,"XDG_CONFIG_HOME",".config","config"); - int want_list=0, dmenu=0; + int want_list=0, dmenu=0, do_replace=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],"--replace")){ do_replace=1; } else if(!strcmp(argv[i],"-v")||!strcmp(argv[i],"--version")){ printf("swas %s (build %s)\n",SWAS_VERSION,SWAS_BUILD); 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; + else if(dmenu_flag(argv[i])){ dmenu=1; i+=dmenu_flag(argv[i])-1; } } { char tmp[PATH_MAX]; expand_tilde(cfgpath,tmp,sizeof tmp); snprintf(cfgpath,sizeof cfgpath,"%s",tmp); } + if(do_replace) replace_running(); /* one wheel at a time */ + sw_shared_apply("swas",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")||!strcmp(argv[i],"-v")||!strcmp(argv[i],"--version")) 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")||!strcmp(argv[i],"-v")||!strcmp(argv[i],"--version")||!strcmp(argv[i],"--replace")) continue; + if(dmenu_flag(argv[i])){ i+=dmenu_flag(argv[i])-1; continue; } /* and its value */ 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); } @@ -1398,7 +1568,7 @@ int main(int argc,char**argv){ #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) + fn=build_filter(&apps,query,filt,cfg.slots,cfg.recent_first,cfg.center_first,&off,&selslot); anim0=SDL_GetTicks(); }while(0) REBUILD(); /* --- drag an app out of the wheel and onto a workspace --- @@ -1558,7 +1728,13 @@ int main(int argc,char**argv){ if(press_down && !dragging && cfg.drop && press_item>=0){ float dx=mx-press_x, dy=my-press_y; if(dx*dx+dy*dy > (float)(cfg.drop_px*cfg.drop_px)){ - if(OV_READY){ dragging=1; ov_send("drag on"); } + if(OV_READY){ + dragging=1; + ov_send("drag on"); + /* the app on the pointer is drawn on this + surface, so it has to be the one in front */ + ov_send("raise " APP_ID); + } else { wsp_load(&cfg); /* first drag pays for the list */ if(NWSP>0) dragging=1; else press_item=-1; @@ -1649,7 +1825,8 @@ int main(int argc,char**argv){ adopt_to(&cfg,p,OV_TARGET,OV_BESIDE,OV_EDGE); /* the new window takes the focus as it opens; the wheel is meant to stay in front of it */ - if(OV_HERE && cfg.drop_close_here) running=0; + if(OV_ONCE) running=0; /* one go */ + else if(OV_HERE && cfg.drop_close_here) running=0; else ov_send("raise " APP_ID); } ov_send("drag off"); @@ -1866,6 +2043,26 @@ int main(int argc,char**argv){ text_centered(ren,&font,mx,my+isz*0.72f,cfg.label_px*ui,cfg.text,cut); } + /* Whatever went wrong, said plainly under the wheel. */ + watch_tick(); + if(ERR_TEXT[0] && now_secs_() < ERR_UNTIL){ + float a=(float)(ERR_UNTIL-now_secs_()); + if(a>1.0f) a=1.0f; + Col ec=cfg.hl; ec.a=(Uint8)(255*a); + Col bgc=cfg.center; bgc.a=(Uint8)(bgc.a*a); + char cut[200]; + fit_label(&font,cfg.label_px*ui,ERR_TEXT,(float)w*0.7f,cut,sizeof cut); + float tw=text_width(&font,cfg.label_px*ui,cut); + float pad=14.0f*ui, bh=cfg.label_px*ui+pad; + SDL_FRect box={(float)w*0.5f-tw*0.5f-pad, cy+R+40.0f*ui, tw+2*pad, bh}; + SDL_SetRenderDrawColor(ren,bgc.r,bgc.g,bgc.b,bgc.a); + SDL_RenderFillRect(ren,&box); + text_centered(ren,&font,(float)w*0.5f,box.y+bh*0.5f+cfg.label_px*ui*0.35f, + cfg.label_px*ui,ec,cut); + } else if(ERR_TEXT[0] && now_secs_() >= ERR_UNTIL) { + ERR_TEXT[0]=0; + } + if(ss>1 && target){ SDL_SetRenderScale(ren,1,1); SDL_SetRenderTarget(ren,NULL); @@ -1875,9 +2072,11 @@ int main(int argc,char**argv){ } SDL_RenderPresent(ren); - if(!ov_started && cfg.overview && cfg.drop){ + if(!ov_started && cfg.drop){ ov_started=1; /* only now: the wheel is already up */ - ov_spawn(&cfg); + /* an overview that started us is already there; otherwise bring + one up ourselves, if we were asked to */ + if(!ov_adopt() && cfg.overview) ov_spawn(&cfg); } SDL_Delay(16); }