/* swov — a fast window/workspace overview for Sway (SDL3) * * Build: * cc -std=c11 -O2 -Wall -Wextra -o swov swov.c \ * $(pkg-config --cflags --libs sdl3 sdl3-image sdl3-ttf) * * 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 the desktop font, otherwise a few well known * font paths are tried. * * See README.md for configuration and key bindings. */ #define _POSIX_C_SOURCE 200809L #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #ifdef __linux__ #include #endif #include #include #include #include #include #include #include "sw_theme.h" #define APP_ID "swov" #define SWOV_VERSION "1.2" #ifndef SWOV_BUILD /* set by the Makefile: md5 of this file */ #define SWOV_BUILD "unknown" #endif #define MAX_WINDOWS 512 #define MAX_WORKSPACES 64 #define MAX_DESKTOPS 4096 /* ------------------------------------------------------------------ util */ static void die(const char *fmt, ...) { va_list ap; va_start(ap, fmt); fputs("swov: ", 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 char *xstrdup(const char *s) { size_t n = strlen(s ? s : "") + 1; char *p = (char *)xmalloc(n); memcpy(p, s ? s : "", n); return p; } static char *fmt_alloc(const char *fmt, ...) { if (!fmt) return xstrdup(""); va_list ap; va_start(ap, fmt); int n = vsnprintf(NULL, 0, fmt, ap); va_end(ap); if (n < 0) return xstrdup(""); char *buf = (char *)xmalloc((size_t)n + 1); va_start(ap, fmt); vsnprintf(buf, (size_t)n + 1, fmt, ap); va_end(ap); return buf; } /* snprintf(dst, sizeof dst, "%s", src) makes newer gcc warn: it cannot prove * src is shorter than dst, since a char field inside a struct array could in * principle run to the end of that array. The precision settles it. */ static void str_set(char *dst, size_t cap, const char *src) { if (!cap) return; snprintf(dst, cap, "%.*s", (int)cap - 1, src ? src : ""); } static bool file_readable(const char *p) { return p && access(p, R_OK) == 0; } static bool is_dir(const char *p) { struct stat st; return p && stat(p, &st) == 0 && S_ISDIR(st.st_mode); } static char *str_trim(char *s) { while (*s == ' ' || *s == '\t') s++; size_t n = strlen(s); while (n && (s[n-1]==' '||s[n-1]=='\t'||s[n-1]=='\r'||s[n-1]=='\n')) s[--n] = 0; return s; } static int ci_cmp(const char *a, const char *b) { while (*a && *b) { int ca = tolower((unsigned char)*a++); int cb = tolower((unsigned char)*b++); if (ca != cb) return ca - cb; } return (unsigned char)*a - (unsigned char)*b; } /* case-insensitive substring search (used by the filter) */ static bool ci_contains(const char *hay, const char *needle) { if (!needle || !*needle) return true; if (!hay) return false; size_t nl = strlen(needle); for (const char *p = hay; *p; ++p) { size_t i = 0; while (i < nl && p[i] && tolower((unsigned char)p[i]) == tolower((unsigned char)needle[i])) i++; if (i == nl) return true; } return false; } static bool str_all_digits(const char *s) { if (!s || !*s) return false; for (const char *p = s; *p; ++p) if (!isdigit((unsigned char)*p)) return false; return true; } /* ---------------------------------------------------------------- colors */ static SDL_FColor rgba(uint32_t v) { return (SDL_FColor){ ((v >> 24) & 0xff) / 255.0f, ((v >> 16) & 0xff) / 255.0f, ((v >> 8) & 0xff) / 255.0f, ((v ) & 0xff) / 255.0f }; } static bool parse_color(const char *s, SDL_FColor *out) { if (!s) return false; while (*s == '#' || *s == ' ') s++; size_t n = strlen(s); if (n != 6 && n != 8) return false; char buf[9]; memcpy(buf, s, n); buf[n] = 0; for (size_t i = 0; i < n; ++i) if (!isxdigit((unsigned char)buf[i])) return false; unsigned long v = strtoul(buf, NULL, 16); if (n == 6) v = (v << 8) | 0xff; *out = rgba((uint32_t)v); return true; } /* c blended towards `to` by t (0..1); alpha kept from c */ static SDL_FColor mix(SDL_FColor c, SDL_FColor to, float t) { return (SDL_FColor){ c.r + (to.r - c.r) * t, c.g + (to.g - c.g) * t, c.b + (to.b - c.b) * t, c.a + (to.a - c.a) * t }; } static SDL_FColor with_alpha(SDL_FColor c, float a) { c.a = a; return c; } /* ---------------------------------------------------------------- config */ typedef struct { /* rendering */ int ssaa; /* 1..4 full scene supersampling */ int icons; /* show .desktop icons */ int icon_px; /* icon edge length (logical px, before ui_scale) */ int shadow; int vsync; /* text */ float ui_scale; int ws_px; /* workspace number badge */ int label_px; /* app name on a window card */ int title_px; /* window title (the long text) */ int hint_px; /* footer / header text */ char font[512]; char font_bold[512]; /* layout */ int cols, rows; /* 0 = automatic */ float margin, gap, pad, radius, border; float win_gap; /* space between two window cards */ float badge_top; /* space above the workspace number */ float float_alpha; /* opacity of floating / fullscreen cards */ float screen_pad; /* border between mini screen and the cards */ int shadow_layers; int usage_dots; /* dot scale down the left edge of a tile */ int track; /* record how long each workspace is used */ 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 snap_ms; /* hold a floating window over another this long to put it into the layout instead */ int focus_self; /* ask sway for the keyboard after mapping */ int show_pid; /* the process id on every card */ int ghost_click; /* clicking an empty number switches to it */ int rename_icon; /* a pencil in the header starts a rename; 0 = the old invisible middle of the name */ int slots_lo, slots_hi;/* numbers that always have a tile, empty or not; 0,0 = only the ones that exist */ 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 */ int dot_count; /* how many dots the scale has */ float dot_px; /* dot diameter */ float anim_ms; /* tile glide duration, 0 disables */ int start_selection; /* 0 none, 1 workspace, 2 first window */ char header_pos[16]; /* none|top-left|top-center|...|bottom-right */ char hints_pos[16]; 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 */ char tab[16]; /* what tab walks: workspaces, or last used */ int show_header; int show_hints; int quit_on_focus_loss; int quit_after_action; /* colors */ SDL_FColor bg, tile, tile_sel, tile_hover, mini_bg; SDL_FColor card, card_hover, card_focus; SDL_FColor hl, text, subtext, dim, accent, hltext, shadow_col, outline, urgent, hint; SDL_FColor current; /* what sway is showing right now */ SDL_FColor match; /* windows the search found */ } Cfg; static Cfg cfg_defaults(void) { Cfg c; memset(&c, 0, sizeof(c)); c.ssaa = 2; c.icons = 1; c.icon_px = 40; c.shadow = 1; c.vsync = 1; c.ui_scale = 1.0f; c.ws_px = 26; c.label_px = 16; c.title_px = 13; c.hint_px = 14; c.cols = c.rows = 0; c.margin = 26.0f; c.win_gap = 5.0f; c.badge_top = 7.0f; c.float_alpha = 0.62f; c.screen_pad = 6.0f; c.shadow_layers = 3; c.usage_dots = 1; c.track = 1; c.back_scope = 0; /* global: go back to where I just was */ c.blur = 2; c.cpu = 1; c.drop_ghosts = 1; c.drop_outputs = 1; c.over_fullscreen = 1; c.snap_ms = 1000; c.focus_self = 1; c.show_pid = 1; c.ghost_click = 1; c.rename_icon = 1; c.slots_lo = 1; /* all ten numbers, always there */ c.slots_hi = 10; 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; c.dot_count = 14; c.dot_px = 5.0f; c.anim_ms = 160.0f; c.start_selection = 1; str_set(c.header_pos, sizeof(c.header_pos), "top-right"); str_set(c.hints_pos, sizeof(c.hints_pos), "bottom-center"); c.gap = 14.0f; c.pad = 10.0f; c.radius = 14.0f; c.border = 3.0f; c.show_empty = 1; 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"); str_set(c.tab, sizeof(c.tab), "recent"); c.show_header = 1; c.show_hints = 1; c.quit_on_focus_loss = 1; c.quit_after_action = 0; /* palette: same family as the swas config */ c.bg = rgba(0x0d1117cc); /* dims whatever is behind the overlay*/ c.tile = rgba(0x1e2733f2); /* workspace tile */ c.tile_sel = rgba(0x26313ff2); /* selected workspace tile */ c.tile_hover = rgba(0x2b3644f2); /* hovered workspace tile */ c.mini_bg = rgba(0x11171f9e); /* inset "screen" inside a tile */ c.card = rgba(0x33404ff7); /* window card */ c.card_hover = rgba(0x46566af7); /* hovered window card */ c.card_focus = rgba(0x3b4a5bf7); /* card of the window sway focuses */ c.hl = rgba(0xcb9b00ff); /* the orange */ c.text = rgba(0xe8e8e8ff); c.subtext = rgba(0xb3c0cdff); c.dim = rgba(0x5a6b7aff); c.accent = rgba(0x89afc4ff); c.hltext = rgba(0x141414ff); c.shadow_col = rgba(0x00000073); c.outline = rgba(0x0a0e1499); c.urgent = rgba(0xe0533cff); c.hint = rgba(0xa7b5c4ff); /* header line and the key hints */ c.current = rgba(0x4fb3a5ff); /* the live workspace / focused window */ c.match = rgba(0xb58ae0ff); /* search hits */ return c; } static bool key_is(const char *k, const char *a) { return ci_cmp(k, a) == 0; } static void cfg_set(Cfg *c, const char *k, const char *v) { /* rendering */ if (key_is(k,"ssaa") || key_is(k,"aa")) { c->ssaa = atoi(v); if (c->ssaa < 1) c->ssaa = 1; if (c->ssaa > 4) c->ssaa = 4; } else if (key_is(k,"icons")) c->icons = atoi(v) != 0; else if (key_is(k,"icon_px")) c->icon_px = atoi(v); else if (key_is(k,"shadow")) c->shadow = atoi(v) != 0; else if (key_is(k,"vsync")) c->vsync = atoi(v) != 0; /* text */ else if (key_is(k,"ui_scale") || key_is(k,"font_scale") || key_is(k,"text_scale")) c->ui_scale = (float)atof(v); else if (key_is(k,"ws_px") || key_is(k,"workspace_px")) c->ws_px = atoi(v); else if (key_is(k,"label_px") || key_is(k,"app_px")) c->label_px = atoi(v); else if (key_is(k,"title_px")) c->title_px = atoi(v); else if (key_is(k,"hint_px") || key_is(k,"count_px")) c->hint_px = atoi(v); else if (key_is(k,"font")) str_set(c->font, sizeof(c->font), v); else if (key_is(k,"font_bold")) str_set(c->font_bold, sizeof(c->font_bold), v); /* layout */ else if (key_is(k,"cols")) c->cols = atoi(v); else if (key_is(k,"rows")) c->rows = atoi(v); else if (key_is(k,"margin")) c->margin = (float)atof(v); else if (key_is(k,"gap")) c->gap = (float)atof(v); else if (key_is(k,"pad")) c->pad = (float)atof(v); else if (key_is(k,"radius") || key_is(k,"corner")) c->radius = (float)atof(v); else if (key_is(k,"border")) c->border = (float)atof(v); else if (key_is(k,"win_gap") || key_is(k,"window_gap")) c->win_gap = (float)atof(v); else if (key_is(k,"screen_pad")) c->screen_pad = (float)atof(v); else if (key_is(k,"badge_top") || key_is(k,"badge_scale")) c->badge_top = (float)atof(v); else if (key_is(k,"float_alpha") || key_is(k,"floating_alpha")) c->float_alpha = (float)atof(v); else if (key_is(k,"shadow_layers")) c->shadow_layers = atoi(v); else if (key_is(k,"usage_dots") || key_is(k,"usage_bar")) c->usage_dots = atoi(v) != 0; else if (key_is(k,"dot_count")) c->dot_count = atoi(v); else if (key_is(k,"dot_px")) c->dot_px = (float)atof(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,"snap_ms")) c->snap_ms = atoi(v); else if (key_is(k,"focus_self")) c->focus_self = atoi(v) != 0; else if (key_is(k,"show_pid")) c->show_pid = atoi(v) != 0; else if (key_is(k,"ghost_click")) c->ghost_click = atoi(v) != 0; else if (key_is(k,"rename_icon")) c->rename_icon = atoi(v) != 0; else if (key_is(k,"ws_slots")) { /* "1-10", or "0" for none */ int lo = 0, hi = 0; if (sscanf(v, "%d-%d", &lo, &hi) != 2) { lo = 0; hi = atoi(v); } if (hi < lo) { int t = lo; lo = hi; hi = t; } c->slots_lo = lo; c->slots_hi = hi; } 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); else if (key_is(k,"blur")) { c->blur = atoi(v); if (c->blur < 0) c->blur = 0; if (c->blur > 3) c->blur = 3; } else if (key_is(k,"back") || key_is(k,"back_scope")) c->back_scope = key_is(v,"output") ? 1 : key_is(v,"sway") ? 2 : 0; else if (key_is(k,"anim_ms") || key_is(k,"animation")) c->anim_ms = (float)atof(v); else if (key_is(k,"start_selection")) { c->start_selection = key_is(v,"none") ? 0 : key_is(v,"window") ? 2 : 1; } else if (key_is(k,"header_pos")) str_set(c->header_pos, sizeof(c->header_pos), v); else if (key_is(k,"hints_pos")) str_set(c->hints_pos, sizeof(c->hints_pos), v); else if (key_is(k,"show_empty")) c->show_empty = atoi(v) != 0; 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,"tab")) str_set(c->tab, sizeof(c->tab), 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; else if (key_is(k,"quit_after_action")) c->quit_after_action = atoi(v) != 0; /* colors */ else if (key_is(k,"bg")) parse_color(v, &c->bg); else if (key_is(k,"tile") || key_is(k,"ring")) parse_color(v, &c->tile); else if (key_is(k,"tile_sel") || key_is(k,"ring2")) parse_color(v, &c->tile_sel); else if (key_is(k,"tile_hover")) parse_color(v, &c->tile_hover); else if (key_is(k,"mini_bg") || key_is(k,"center")) parse_color(v, &c->mini_bg); else if (key_is(k,"card")) parse_color(v, &c->card); else if (key_is(k,"card_hover") || key_is(k,"hover")) parse_color(v, &c->card_hover); else if (key_is(k,"card_focus")) parse_color(v, &c->card_focus); else if (key_is(k,"hl")) parse_color(v, &c->hl); else if (key_is(k,"text")) parse_color(v, &c->text); else if (key_is(k,"subtext")) parse_color(v, &c->subtext); else if (key_is(k,"dim")) parse_color(v, &c->dim); else if (key_is(k,"accent")) parse_color(v, &c->accent); else if (key_is(k,"hltext")) parse_color(v, &c->hltext); else if (key_is(k,"shadow_color")) parse_color(v, &c->shadow_col); else if (key_is(k,"outline")) parse_color(v, &c->outline); else if (key_is(k,"urgent")) parse_color(v, &c->urgent); else if (key_is(k,"hint")) parse_color(v, &c->hint); else if (key_is(k,"current")) parse_color(v, &c->current); else if (key_is(k,"match")) parse_color(v, &c->match); else fprintf(stderr, "swov: unknown config key '%s' (ignored)\n", k); } /* the shared ~/.config/sw/config, translated into swov's own keys */ static void cfg_set_shared(void *ud, const char *k, const char *v) { cfg_set((Cfg *)ud, k, v); } static char *expand_tilde(const char *p) { if (p && p[0] == '~' && (p[1] == '/' || p[1] == 0)) { const char *home = getenv("HOME"); if (home) return fmt_alloc("%s%s", home, p + 1); } return xstrdup(p ? p : ""); } static bool cfg_load_file(Cfg *c, const char *path) { FILE *f = fopen(path, "r"); if (!f) return false; char line[1024]; while (fgets(line, sizeof(line), f)) { char *s = str_trim(line); if (!*s || *s == '#' || *s == ';') continue; char *eq = strchr(s, '='); if (!eq) continue; *eq = 0; char *k = str_trim(s); char *v = str_trim(eq + 1); char *hash = strchr(v, '#'); /* trailing comment */ if (hash && hash > v && hash[-1] == ' ') { *hash = 0; v = str_trim(v); } if (*k) cfg_set(c, k, v); } fclose(f); return true; } static char CFG_PATH[512]; static bool CFG_LOADED; static bool SHARED_LOADED; static char *default_config_path(void) { const char *xdg = getenv("XDG_CONFIG_HOME"); if (xdg && *xdg) return fmt_alloc("%s/swov/config", xdg); const char *home = getenv("HOME"); if (home) return fmt_alloc("%s/.config/swov/config", home); return NULL; } /* ----------------------------------------------------------- json parser * Just enough JSON to read sway's IPC replies: no streaming, no comments, * numbers as double, strings unescaped to UTF-8. */ typedef enum { J_NULL, J_BOOL, J_NUM, J_STR, J_ARR, J_OBJ } JType; typedef struct JV JV; struct JV { JType type; bool b; double num; char *str; /* J_STR */ JV **items; /* J_ARR / J_OBJ values */ char **keys; /* J_OBJ keys */ int count, cap; }; static void jfree(JV *v) { if (!v) return; switch (v->type) { case J_STR: free(v->str); break; case J_ARR: for (int i = 0; i < v->count; ++i) jfree(v->items[i]); free(v->items); break; case J_OBJ: for (int i = 0; i < v->count; ++i) { free(v->keys[i]); jfree(v->items[i]); } free(v->items); free(v->keys); break; default: break; } free(v); } static JV *jnew(JType t) { JV *v = (JV *)xmalloc(sizeof(JV)); memset(v, 0, sizeof(*v)); v->type = t; return v; } static void jpush(JV *v, char *key, JV *val) { 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 *)); if (v->type == J_OBJ) v->keys = (char **)xrealloc(v->keys, (size_t)v->cap * sizeof(char *)); } if (v->type == J_OBJ) v->keys[v->count] = key; v->items[v->count++] = val; } static void utf8_append(char **dst, size_t *len, size_t *cap, uint32_t cp) { char tmp[4]; int n = 0; if (cp < 0x80) { tmp[0] = (char)cp; n = 1; } else if (cp < 0x800) { tmp[0] = (char)(0xc0 | (cp >> 6)); tmp[1] = (char)(0x80 | (cp & 0x3f)); n = 2; } else if (cp < 0x10000) { tmp[0] = (char)(0xe0 | (cp >> 12)); tmp[1] = (char)(0x80 | ((cp >> 6) & 0x3f)); tmp[2] = (char)(0x80 | (cp & 0x3f)); n = 3; } else { tmp[0] = (char)(0xf0 | (cp >> 18)); tmp[1] = (char)(0x80 | ((cp >> 12) & 0x3f)); tmp[2] = (char)(0x80 | ((cp >> 6) & 0x3f)); tmp[3] = (char)(0x80 | (cp & 0x3f)); n = 4; } if (*len + (size_t)n + 1 > *cap) { *cap = (*cap ? *cap * 2 : 32) + (size_t)n; *dst = (char *)xrealloc(*dst, *cap); } memcpy(*dst + *len, tmp, (size_t)n); *len += (size_t)n; (*dst)[*len] = 0; } static const char *jskip_ws(const char *p) { while (*p == ' ' || *p == '\t' || *p == '\n' || *p == '\r') p++; return p; } static const char *jparse_string(const char *p, char **out) { /* p points at the opening quote */ size_t len = 0, cap = 32; char *s = (char *)xmalloc(cap); s[0] = 0; p++; while (*p && *p != '"') { if (*p == '\\') { p++; uint32_t cp = 0; switch (*p) { case 'n': cp = '\n'; p++; break; case 't': cp = '\t'; p++; break; case 'r': cp = '\r'; p++; break; case 'b': cp = '\b'; p++; break; case 'f': cp = '\f'; p++; break; case '/': cp = '/'; p++; break; case '"': cp = '"'; p++; break; case '\\': cp = '\\'; p++; break; case 'u': { p++; char hex[5] = {0}; for (int i = 0; i < 4 && p[i]; ++i) hex[i] = p[i]; cp = (uint32_t)strtoul(hex, NULL, 16); p += 4; if (cp >= 0xd800 && cp <= 0xdbff && p[0] == '\\' && p[1] == 'u') { char hex2[5] = {0}; for (int i = 0; i < 4 && p[2 + i]; ++i) hex2[i] = p[2 + i]; uint32_t lo = (uint32_t)strtoul(hex2, NULL, 16); if (lo >= 0xdc00 && lo <= 0xdfff) { cp = 0x10000 + ((cp - 0xd800) << 10) + (lo - 0xdc00); p += 6; } } break; } default: if (!*p) { free(s); *out = NULL; return NULL; } cp = (unsigned char)*p++; break; } utf8_append(&s, &len, &cap, cp); } else { if (len + 2 > cap) { cap *= 2; s = (char *)xrealloc(s, 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_GET_OUTPUTS = 3, IPC_RUN_COMMAND = 0, IPC_GET_WORKSPACES = 1, IPC_GET_TREE = 4, IPC_SUBSCRIBE = 2 }; 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, ...) { if (!fmt) return false; 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; } /* Sway answers a command as soon as it is queued, but the tree only carries * the new geometry once the transaction has committed. Rather than sleeping * and hoping, we subscribe to window and workspace events on a second socket * and reload when sway tells us it is done. That also keeps the overview * correct while it is open. */ static bool sway_subscribe_events(void) { sway_evt_fd = sway_connect(); if (sway_evt_fd < 0) return false; const char *payload = "[\"window\",\"workspace\"]"; 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 */ 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)) { /* rest of the header */ if (!read_all(sway_evt_fd, hdr + r, sizeof(hdr) - (size_t)r)) break; } uint32_t rlen = 0; memcpy(&rlen, hdr + 6, 4); if (rlen > (16u << 20)) break; if (rlen) { char *body = (char *)xmalloc(rlen + 1); bool ok = read_all(sway_evt_fd, body, rlen); free(body); if (!ok) break; } any = true; } return any; } /* ------------------------------------------------------------ usage store * How long the user has had each workspace in front of them. Every workspace * switch goes through swov, so no daemon is needed: on each switch we credit * the workspace we are leaving with the time since the last switch, and write * down where we are going and when. A workspace that falls empty loses its * count — the number describes the workspace as it is now. * * File: one "> " line saying where we are, then " * " per workspace. */ typedef struct { char name[64]; char output[64]; /* which monitor it lives on */ int num; /* its number, or -1 for a named workspace */ double secs; double last; /* when we were last there (epoch) */ } Usage; static Usage USAGE[128]; static int NUSAGE; static double USAGE_MAX = 0.0; /* the busiest workspace, for the scale */ static char USAGE_CUR[64]; /* the workspace we are on */ static char USAGE_CUR_OUT[64]; /* ... on this output */ static double USAGE_SINCE; /* ... since this moment (epoch seconds) */ static double now_secs(void) { struct timespec ts; clock_gettime(CLOCK_REALTIME, &ts); return (double)ts.tv_sec + (double)ts.tv_nsec / 1e9; } static char *usage_path(void) { const char *xdg = getenv("XDG_CACHE_HOME"); if (xdg && *xdg) return fmt_alloc("%s/swov/usage", xdg); const char *home = getenv("HOME"); return home ? fmt_alloc("%s/.cache/swov/usage", home) : NULL; } static Usage *usage_find(const char *name, bool create) { for (int i = 0; i < NUSAGE; ++i) if (strcmp(USAGE[i].name, name) == 0) return &USAGE[i]; if (!create || NUSAGE >= (int)SDL_arraysize(USAGE)) return NULL; Usage *u = &USAGE[NUSAGE++]; memset(u, 0, sizeof(*u)); u->num = -1; str_set(u->name, sizeof(u->name), name); return u; } static void usage_load(void) { NUSAGE = 0; char *path = usage_path(); if (!path) return; FILE *f = fopen(path, "r"); free(path); if (!f) return; USAGE_CUR[0] = 0; USAGE_CUR_OUT[0] = 0; USAGE_SINCE = 0.0; char line[320]; while (fgets(line, sizeof(line), f) && NUSAGE < (int)SDL_arraysize(USAGE)) { double v = 0.0, last = 0.0; char name[64] = {0}, out[64] = {0}; if (line[0] == '>') { if (sscanf(line + 1, "%lf %63s %63[^\n]", &v, out, name) == 3) { USAGE_SINCE = v; str_set(USAGE_CUR, sizeof(USAGE_CUR), name); str_set(USAGE_CUR_OUT, sizeof(USAGE_CUR_OUT), out); } else if (sscanf(line + 1, "%lf %63[^\n]", &v, name) == 2) { USAGE_SINCE = v; /* a file from before outputs */ str_set(USAGE_CUR, sizeof(USAGE_CUR), name); } continue; } int num = -1; if (sscanf(line, "%lf %lf %d %63s %63[^\n]", &v, &last, &num, out, name) == 5 && name[0]) { Usage *u = usage_find(name, true); if (u) { u->secs = v; u->last = last; u->num = num; if (strcmp(out, "-") != 0) str_set(u->output, sizeof(u->output), out); } } else if (sscanf(line, "%lf %lf %63s %63[^\n]", &v, &last, out, name) == 4 && name[0]) { Usage *u = usage_find(name, true); /* a file from before numbers */ if (u) { u->secs = v; u->last = last; if (strcmp(out, "-") != 0) str_set(u->output, sizeof(u->output), out); } } else if (sscanf(line, "%lf %63[^\n]", &v, name) == 2 && name[0]) { Usage *u = usage_find(name, true); if (u) u->secs = v; } } fclose(f); } /* time spent on the workspace we are on but not yet written down */ static double usage_pending(void) { if (!USAGE_CUR[0] || USAGE_SINCE <= 0.0) return 0.0; double d = now_secs() - USAGE_SINCE; if (d < 0.0 || d > 12.0 * 3600.0) return 0.0; /* clock jump, or a nap */ return d; } static void usage_save(void) { char *path = usage_path(); if (!path) return; char *slash = strrchr(path, '/'); if (slash) { /* mkdir -p on the parent, one level */ *slash = 0; char *up = strrchr(path, '/'); if (up) { *up = 0; mkdir(path, 0755); *up = '/'; } mkdir(path, 0755); *slash = '/'; } char *tmp = fmt_alloc("%s.tmp", path); FILE *f = fopen(tmp, "w"); if (f) { if (USAGE_CUR[0]) fprintf(f, "> %.0f %s %s\n", USAGE_SINCE, USAGE_CUR_OUT[0] ? USAGE_CUR_OUT : "-", USAGE_CUR); for (int i = 0; i < NUSAGE; ++i) if (USAGE[i].secs > 0.0 || USAGE[i].last > 0.0) fprintf(f, "%.0f %.3f %d %s %s\n", USAGE[i].secs, USAGE[i].last, USAGE[i].num, USAGE[i].output[0] ? USAGE[i].output : "-", USAGE[i].name); fclose(f); rename(tmp, path); } free(tmp); free(path); } /* Credit the workspace we are leaving, then note where we are going. This is * the whole of the time tracking: swov performs every switch itself. */ static void usage_switch(const char *to, const char *out, int num) { if (!to || !*to) return; usage_load(); double now = now_secs(); double pending = usage_pending(); if (USAGE_CUR[0]) { Usage *u = usage_find(USAGE_CUR, true); if (u) { if (pending > 0.0) u->secs += pending; u->last = now; /* we were just there */ if (USAGE_CUR_OUT[0]) str_set(u->output, sizeof(u->output), USAGE_CUR_OUT); } } Usage *dst = usage_find(to, true); if (dst) { if (out && *out) str_set(dst->output, sizeof(dst->output), out); dst->num = num; dst->last = now; /* arriving counts as a visit, so the entry (and * with it the number) survives the save filter */ } str_set(USAGE_CUR, sizeof(USAGE_CUR), to); str_set(USAGE_CUR_OUT, sizeof(USAGE_CUR_OUT), out ? out : ""); USAGE_SINCE = now; usage_save(); } /* The workspace we were on before this one. With `output` set, only the ones * on that monitor count (back=output); with NULL, the most recent anywhere * wins (back=global, the default), so leaving a monitor and coming back is a * toggle instead of a jump into that monitor's own history. */ static bool last_workspace_here(const char *output, const char *current, char *out, size_t cap, int *num) { usage_load(); const Usage *best = NULL; for (int i = 0; i < NUSAGE; ++i) { const Usage *u = &USAGE[i]; if (u->last <= 0.0) continue; if (current && strcmp(u->name, current) == 0) continue; if (output && *output && u->output[0] && strcmp(u->output, output) != 0) continue; if (!best || u->last > best->last) best = u; } if (!best) return false; str_set(out, cap, best->name); if (num) *num = best->num; return true; } /* the workspace sway is showing at this moment */ static void focused_workspace(char *name, size_t ncap, char *output, size_t ocap, int *num) { name[0] = 0; if (output) output[0] = 0; if (num) *num = -1; JV *r = sway_query(IPC_GET_WORKSPACES); if (r && r->type == J_ARR) for (int i = 0; i < r->count; ++i) if (jbool(r->items[i], "focused", false)) { str_set(name, ncap, jstr(r->items[i], "name", "")); if (output) str_set(output, ocap, jstr(r->items[i], "output", "")); if (num) *num = jint(r->items[i], "num", -1); } jfree(r); } static bool node_is_view(const JV *n); static char *escape_arg(const char *s); static bool ADOPT_LOG; /* --adopt-debug: say what is happening */ static bool TIMING; /* --timing: where startup goes */ static double T0, T_LAST; /* Milliseconds since the last mark, on stderr. Cheap enough to leave in and * the only honest way to answer "why was that slow this time". */ static void mark(const char *what) { if (!TIMING) return; double now = now_secs(); if (T_LAST == 0.0) T_LAST = T0; fprintf(stderr, "swov: %-18s %6.1f ms (%6.1f total)\n", what, (now - T_LAST) * 1000.0, (now - T0) * 1000.0); T_LAST = now; } /* ------------------------------------------------- workspaces / adopt * Two one-shot modes that need no window, no font and no renderer. They are * what a launcher talks to: --workspaces to know where a window could go, * --adopt to put one there once it shows up. */ /* `num name output flags`, tab separated, one workspace per line. */ static int print_workspaces(void) { JV *r = sway_query(IPC_GET_WORKSPACES); if (!r || r->type != J_ARR) { jfree(r); return 1; } for (int i = 0; i < r->count; ++i) { const JV *w = r->items[i]; char flags[64] = ""; if (jbool(w, "focused", false)) strcat(flags, "focused,"); if (jbool(w, "visible", false)) strcat(flags, "visible,"); if (jbool(w, "urgent", false)) strcat(flags, "urgent,"); size_t n = strlen(flags); if (n) flags[n - 1] = 0; else str_set(flags, sizeof(flags), "-"); printf("%d\t%s\t%s\t%s\n", jint(w, "num", -1), jstr(w, "name", ""), jstr(w, "output", ""), flags); } jfree(r); return 0; } /* Is `pid` the process we launched, or something it started? A launcher that * goes through a shell, or an app that re-execs itself, would not match on * the pid alone. */ static bool pid_descends_from(int pid, int ancestor) { for (int i = 0; i < 8 && pid > 1; ++i) { if (pid == ancestor) return true; char path[64]; snprintf(path, sizeof(path), "/proc/%d/stat", pid); FILE *f = fopen(path, "r"); if (!f) return false; /* comm can hold spaces and brackets, so start after the last ')' */ char buf[512]; size_t got = fread(buf, 1, sizeof(buf) - 1, f); fclose(f); buf[got] = 0; char *close = strrchr(buf, ')'); int ppid = 0; if (!close || sscanf(close + 1, " %*c %d", &ppid) != 1) return false; pid = ppid; } return false; } /* the container id of the first view belonging to `pid`, or -1 */ static int find_view_of_pid(const JV *node, int pid) { if (!node || node->type != J_OBJ) return -1; if (node_is_view(node)) { int p = jint(node, "pid", -1); if (p > 0 && pid_descends_from(p, pid)) return jint(node, "id", -1); } for (int i = 0; i < node->count; ++i) { const JV *kid = node->items[i]; if (!kid) continue; if (kid->type == J_ARR) { for (int j = 0; j < kid->count; ++j) { int id = find_view_of_pid(kid->items[j], pid); if (id >= 0) return id; } } else if (kid->type == J_OBJ) { int id = find_view_of_pid(kid, pid); if (id >= 0) return id; } } return -1; } /* Tell sway where the window this process is about to open belongs, before it * opens. sway consults `assign` in select_workspace(), which runs before the * container is put anywhere, so the window never appears on the workspace you * are looking at and nothing there is rearranged. Without this the window * lands here first, the layout shuffles, and only then does it move. * * The rule stays for the rest of the sway session — there is no unassign — * but it is tied to one pid, so it does nothing after that process is gone. */ static void assign_pid_to_ws(int pid, const char *ws) { if (str_all_digits(ws)) { sway_cmd("assign [pid=%d] workspace number %s", pid, ws); } else { char *e = escape_arg(ws); sway_cmd("assign [pid=%d] workspace \"%s\"", pid, e); free(e); } } /* Every process descending from `root`, so the rule can be put in front of * whichever one ends up owning the window. A launcher, a wrapper script or an * app that forks and lets the parent go leaves the window belonging to a pid * we were never told about, and an assign rule for the pid we started is then * matched by nothing. */ static int sweep_descendants(int root, int *out, int cap) { DIR *d = opendir("/proc"); if (!d) return 0; int n = 0; struct dirent *e; while ((e = readdir(d)) && n < cap) { if (!str_all_digits(e->d_name)) continue; int pid = atoi(e->d_name); if (pid <= 1 || pid == root) continue; if (pid_descends_from(pid, root)) out[n++] = pid; } closedir(d); return n; } static void move_to_ws_edge_named(int con_id, const char *ws, const char *edge); /* every view in the tree, so a window that turns up later can be spotted */ static void collect_view_ids(const JV *node, int *ids, int *n, int cap) { if (!node || node->type != J_OBJ || *n >= cap) return; if (node_is_view(node)) { int id = jint(node, "id", -1); if (id >= 0) ids[(*n)++] = id; } for (int i = 0; i < node->count; ++i) { const JV *kid = node->items[i]; if (!kid) continue; if (kid->type == J_ARR) for (int j = 0; j < kid->count; ++j) collect_view_ids(kid->items[j], ids, n, cap); else if (kid->type == J_OBJ) collect_view_ids(kid, ids, n, cap); } } static int find_new_view(const JV *node, const int *known, int nknown) { int ids[512], n = 0; collect_view_ids(node, ids, &n, (int)SDL_arraysize(ids)); for (int i = 0; i < n; ++i) { bool seen = false; for (int j = 0; j < nknown; ++j) if (known[j] == ids[i]) { seen = true; break; } if (!seen) return ids[i]; } return -1; } /* Wait for the window `pid` opens, then put it where it was dropped. sway has * no "run this on workspace N", so this is the only way to land an app * somewhere else without switching there first and switching back. * * The pid is the first thing we look for, but it is not reliable on its own: * a launcher, a wrapper script or an app that re-execs leaves the window * belonging to a process whose parent chain no longer leads back to us. So we * also watch for a view that simply was not there when we started — the one * the drop just asked for. */ static int adopt_window(int pid, const char *ws, double timeout, bool focus, int beside, const char *edge, bool no_assign) { double deadline = now_secs() + timeout; int con_id = -1; /* 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; } int known[512], nknown = 0; JV *first = sway_query(IPC_GET_TREE); if (first) { collect_view_ids(first, known, &nknown, (int)SDL_arraysize(known)); jfree(first); } if (ADOPT_LOG) fprintf(stderr, "swov: adopt pid %d -> ws %s (%d views now)\n", pid, ws, nknown); double started = now_secs(); int ticks = 0; while (now_secs() < deadline) { struct timespec ts = { 0, 40 * 1000000L }; nanosleep(&ts, NULL); ticks++; /* Keep putting the rule in front of anything the app starts, for the * first few seconds. Every one of these is a pid that could be the * one sway ends up seeing. */ if (!no_assign && nassigned < (int)SDL_arraysize(assigned) && now_secs() - started < 4.0) { int kids[64]; int nk = sweep_descendants(pid, kids, (int)SDL_arraysize(kids)); for (int i = 0; i < nk && nassigned < (int)SDL_arraysize(assigned); ++i) { bool seen = false; for (int j = 0; j < nassigned; ++j) if (assigned[j] == kids[i]) { seen = true; break; } if (seen) continue; assign_pid_to_ws(kids[i], ws); assigned[nassigned++] = kids[i]; if (ADOPT_LOG) fprintf(stderr, "swov: also assigned pid %d\n", kids[i]); } } if (ticks % 3) continue; /* look at the tree at 8 Hz */ JV *tree = sway_query(IPC_GET_TREE); if (!tree) continue; con_id = find_view_of_pid(tree, pid); if (con_id < 0) con_id = find_new_view(tree, known, nknown); jfree(tree); if (con_id >= 0) break; if (kill(pid, 0) != 0 && errno == ESRCH && now_secs() - started > 3.0) { if (ADOPT_LOG) fprintf(stderr, "swov: adopt gave up, pid %d is gone\n", pid); break; } } if (con_id < 0) { if (ADOPT_LOG) fprintf(stderr, "swov: adopt found no window for pid %d\n", pid); return 1; } if (ADOPT_LOG) fprintf(stderr, "swov: adopt window con_id %d\n", con_id); /* If it did land here after all, float it first: a tiled window leaving a * workspace makes everything left behind reflow, and that is the flash * the assign rule is there to avoid. Floating, moving and tiling again * touches only the workspace it ends up on. */ bool ok; sway_cmd("[con_id=%d] floating enable", con_id); if (str_all_digits(ws)) { ok = sway_cmd("[con_id=%d] move container to workspace number %s", con_id, ws); } else { char *e = escape_arg(ws); ok = sway_cmd("[con_id=%d] move container to workspace \"%s\"", con_id, e); free(e); } sway_cmd("[con_id=%d] floating disable", con_id); /* Dropped against the edge of a workspace whose windows are stacked: one * move across the stack lifts the new window out of it and puts it beside * the whole column, which is what "left of both" means. */ if (ok && beside <= 0 && edge) { move_to_ws_edge_named(con_id, ws, edge); if (ADOPT_LOG) fprintf(stderr, "swov: moved to the %s of everything there\n", edge); } /* and then next to the window it was dropped on, splitting the way the * pointer said — the same three commands a drag inside swov uses */ if (ok && beside > 0 && edge) { bool horiz = !strcmp(edge, "left") || !strcmp(edge, "right"); sway_cmd("[con_id=%d] mark --add _swov_drop", beside); sway_cmd("[con_id=%d] split %s", beside, horiz ? "h" : "v"); sway_cmd("[con_id=%d] move container to mark _swov_drop", con_id); if (!strcmp(edge, "left") || !strcmp(edge, "top")) sway_cmd("[con_id=%d] move %s", con_id, horiz ? "left" : "up"); sway_cmd("unmark _swov_drop"); if (ADOPT_LOG) fprintf(stderr, "swov: placed %s of con_id %d\n", edge, beside); } if (ok && focus) { if (str_all_digits(ws)) sway_cmd("workspace number %s", ws); else { char *e = escape_arg(ws); sway_cmd("workspace --no-auto-back-and-forth \"%s\"", e); free(e); } } return ok ? 0 : 1; } /* --------------------------------------------------------------- globals */ static Cfg C; /* ------------------------------------------------------- cpu per workspace * swbr measures it — a rate needs two samples seconds apart, and swov is only * on screen for a moment — and leaves the answer here. If swbr is not running * the file is stale or missing and nothing is drawn. */ typedef struct { char name[64]; float cores; } CpuWs; static CpuWs CPU[64]; static int NCPU; static time_t CPU_MTIME; static void cpu_load(void) { NCPU = 0; if (!C.cpu) return; char path[512]; const char *rt = getenv("XDG_RUNTIME_DIR"); if (rt && *rt) snprintf(path, sizeof(path), "%s/swbr-cpu", rt); else { const char *home = getenv("HOME"); if (!home) return; snprintf(path, sizeof(path), "%s/.cache/swbr-cpu", home); } struct stat st; if (stat(path, &st) != 0) return; CPU_MTIME = st.st_mtime; if (time(NULL) - st.st_mtime > 30) return; /* nobody is measuring */ FILE *f = fopen(path, "r"); if (!f) return; char line[256]; while (NCPU < (int)SDL_arraysize(CPU) && fgets(line, sizeof(line), f)) { char *tab = strchr(line, '\t'); if (!tab) continue; *tab = 0; str_set(CPU[NCPU].name, sizeof(CPU[0].name), line); CPU[NCPU].cores = (float)atof(tab + 1); NCPU++; } fclose(f); } /* how busy, 0..1, or -1 when there is nothing worth drawing. * The number swbr leaves behind is in cores: 1.0 is one core kept busy. */ static float cpu_frac(const char *ws_name) { for (int i = 0; i < NCPU; ++i) { if (strcmp(CPU[i].name, ws_name)) continue; if (CPU[i].cores < C.cpu_idle) return -1.0f; /* genuinely nothing */ if (CPU[i].cores < C.cpu_min) return 0.0f; /* a hint, one dot */ float span = C.cpu_full - C.cpu_min; float f = span > 0.0f ? (CPU[i].cores - C.cpu_min) / span : 1.0f; return SDL_clamp(f, 0.0f, 1.0f); } return -1.0f; } static SDL_Renderer *REN; static TTF_Font *F_BADGE; /* workspace number */ static TTF_Font *F_LABEL; /* app name on a window card */ static TTF_Font *F_TITLE; /* window title (small) */ static TTF_Font *F_HINT; /* header / footer */ static TTF_Font *F_PID; /* the pid in a card's corner */ static float SC = 1.0f; /* supersampling scale factor */ static int px(float logical) { return (int)(logical * SC + 0.5f); } /* ----------------------------------------------------------------- text */ typedef struct { SDL_Texture *t; int w, h; } Tex; static void tex_free(Tex *x) { if (x->t) SDL_DestroyTexture(x->t); x->t = NULL; x->w = x->h = 0; } /* Text is always rendered white and tinted at draw time, so one texture can * be reused for normal / hovered / selected states. */ static Tex text_make(TTF_Font *f, const char *s) { Tex out = {0}; if (!f || !s || !*s) return out; SDL_Color white = { 255, 255, 255, 255 }; SDL_Surface *surf = TTF_RenderText_Blended(f, s, strlen(s), white); if (!surf) return out; out.t = SDL_CreateTextureFromSurface(REN, surf); out.w = surf->w; out.h = surf->h; SDL_DestroySurface(surf); if (out.t) SDL_SetTextureScaleMode(out.t, SDL_SCALEMODE_LINEAR); return out; } /* Render `s`, shortened with an ellipsis so that it fits into max_w pixels. */ static Tex text_make_fit(TTF_Font *f, const char *s, int max_w) { Tex out = {0}; if (!f || !s || !*s || max_w <= 0) return out; int w = 0, h = 0; if (TTF_GetStringSize(f, s, strlen(s), &w, &h) && w <= max_w) return text_make(f, s); const char *ell = "\xe2\x80\xa6"; /* … */ int ew = 0, eh = 0; TTF_GetStringSize(f, ell, strlen(ell), &ew, &eh); int avail = max_w - ew; if (avail <= 0) return out; int mw = 0; size_t fit = 0; if (!TTF_MeasureString(f, s, strlen(s), avail, &mw, &fit) || fit == 0) return out; char *buf = (char *)xmalloc(fit + strlen(ell) + 1); memcpy(buf, s, fit); memcpy(buf + fit, ell, strlen(ell) + 1); out = text_make(f, buf); free(buf); return out; } static void tex_draw(Tex x, float dx, float dy, SDL_FColor col) { if (!x.t) return; SDL_SetTextureColorModFloat(x.t, col.r, col.g, col.b); SDL_SetTextureAlphaModFloat(x.t, col.a); SDL_FRect dst = { dx, dy, (float)x.w, (float)x.h }; SDL_RenderTexture(REN, x.t, NULL, &dst); } static void tex_draw_center(Tex x, float cx, float dy, SDL_FColor col) { tex_draw(x, cx - x.w * 0.5f, dy, col); } /* Centre a line inside a box. A text texture is a full line box: it reserves * room for descenders even when the string has none, so plain centring makes * digits and capitals look too high. Half the descent puts them right. */ static void tex_draw_in_box(Tex x, SDL_FRect box, TTF_Font *f, SDL_FColor col) { if (!x.t) return; float desc = f ? (float)-TTF_GetFontDescent(f) : 0.0f; float y = box.y + (box.h - (float)x.h) * 0.5f - desc * 0.5f; tex_draw(x, box.x + (box.w - (float)x.w) * 0.5f, y, col); } /* ------------------------------------------------------------- primitives */ static void set_col(SDL_FColor c) { SDL_SetRenderDrawColorFloat(REN, c.r, c.g, c.b, c.a); } static void fill_round_rect(SDL_FRect r, float rad, SDL_FColor c) { if (r.w <= 0.0f || r.h <= 0.0f) return; rad = SDL_min(rad, SDL_min(r.w, r.h) * 0.5f); set_col(c); if (rad <= 0.5f) { SDL_RenderFillRect(REN, &r); return; } SDL_FRect mid = { r.x, r.y + rad, r.w, r.h - 2.0f * rad }; 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; SDL_FRect top = { x0, r.y + (float)i, w, 1.0f }; SDL_FRect bot = { x0, r.y + r.h - (float)i - 1.0f, w, 1.0f }; SDL_RenderFillRect(REN, &top); SDL_RenderFillRect(REN, &bot); } } /* 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) { if (t <= 0.0f || r.w <= 0.0f || r.h <= 0.0f) return; rad = SDL_min(rad, SDL_min(r.w, r.h) * 0.5f); t = SDL_min(t, SDL_min(r.w, r.h) * 0.5f); set_col(c); if (rad <= 0.5f) { SDL_FRect e[4] = { { r.x, r.y, r.w, t }, { r.x, r.y + r.h - t, r.w, t }, { r.x, r.y + t, t, r.h - 2.0f * t }, { r.x + r.w - t, r.y + t, t, r.h - 2.0f * t } }; SDL_RenderFillRects(REN, e, 4); return; } SDL_FRect edges[4] = { { r.x + rad, r.y, r.w - 2.0f * rad, t }, /* top */ { r.x + rad, r.y + r.h - t, r.w - 2.0f * rad, t }, /* bottom */ { r.x, r.y + rad, t, r.h - 2.0f * rad }, /* left */ { r.x + r.w - t, r.y + rad, t, r.h - 2.0f * rad } /* right */ }; SDL_RenderFillRects(REN, edges, 4); float ri = rad - t; int steps = (int)SDL_ceilf(rad); for (int i = 0; i < steps; ++i) { float dy = rad - (float)i - 0.5f; float dxo = SDL_sqrtf(SDL_max(0.0f, rad * rad - dy * dy)); float dxi = (ri > 0.0f && SDL_fabsf(dy) < ri) ? SDL_sqrtf(SDL_max(0.0f, ri * ri - dy * dy)) : 0.0f; float seg = dxo - dxi; if (seg <= 0.0f) continue; float lx = r.x + rad - dxo; float rx = r.x + r.w - rad + dxi; float ty = r.y + (float)i; float by = r.y + r.h - (float)i - 1.0f; SDL_FRect q[4] = { { lx, ty, seg, 1.0f }, { rx, ty, seg, 1.0f }, { lx, by, seg, 1.0f }, { rx, by, seg, 1.0f } }; SDL_RenderFillRects(REN, q, 4); } } /* soft drop shadow: a few expanded, fading rounded rects */ static void drop_shadow(SDL_FRect r, float rad, float size, SDL_FColor c) { if (!C.shadow || size <= 0.0f) return; int layers = C.shadow_layers > 0 ? C.shadow_layers : 3; for (int i = layers; i >= 1; --i) { float g = size * (float)i / (float)layers; SDL_FRect s = { r.x - g, r.y - g, r.w + 2.0f * g, r.h + 2.0f * g }; SDL_FColor col = with_alpha(c, c.a / (float)(layers * 2)); fill_round_rect(s, rad + g, col); } } /* ------------------------------------------------------------ icon lookup * app_id (or WM_CLASS) -> .desktop file -> Icon= -> a file on disk. * Everything is cached: the .desktop index is built once and lazily, the * resulting textures are shared between windows of the same application. */ typedef struct { char *id; /* desktop file basename without .desktop */ char *path; char *wmclass; /* StartupWMClass, may be NULL */ char *icon; /* Icon=, may be NULL */ char *name; /* Name=, may be NULL */ } DesktopEntry; static DesktopEntry *g_desktops = NULL; static int g_desktop_count = 0; static bool g_desktop_indexed = false; static void desktop_scan_dir(const char *dir) { DIR *d = opendir(dir); if (!d) return; struct dirent *de; while ((de = readdir(d)) && g_desktop_count < MAX_DESKTOPS) { const char *n = de->d_name; size_t len = strlen(n); if (len < 9 || strcmp(n + len - 8, ".desktop") != 0) continue; char *path = fmt_alloc("%s/%s", dir, n); FILE *f = fopen(path, "r"); if (!f) { free(path); continue; } char *icon = NULL, *wmclass = NULL, *dname = NULL; char line[1024]; while (fgets(line, sizeof(line), f)) { if (!icon && strncmp(line, "Icon=", 5) == 0) { char *v = str_trim(line + 5); if (*v) icon = xstrdup(v); } else if (!wmclass && strncmp(line, "StartupWMClass=", 15) == 0) { char *v = str_trim(line + 15); if (*v) wmclass = xstrdup(v); } else if (!dname && strncmp(line, "Name=", 5) == 0) { char *v = str_trim(line + 5); if (*v) dname = xstrdup(v); } if (icon && wmclass && dname) break; } fclose(f); if (!icon && !dname) { free(wmclass); free(path); continue; } DesktopEntry *e = &g_desktops[g_desktop_count++]; e->path = path; e->icon = icon; e->wmclass = wmclass; e->name = dname; e->id = xstrdup(n); e->id[len - 8] = 0; } closedir(d); } static void desktop_index_build(void) { if (g_desktop_indexed) return; g_desktop_indexed = true; g_desktops = (DesktopEntry *)xmalloc(sizeof(DesktopEntry) * MAX_DESKTOPS); const char *home = getenv("HOME"); const char *xdg_data_home = getenv("XDG_DATA_HOME"); const char *xdg_data_dirs = getenv("XDG_DATA_DIRS"); char *dirs[16]; int nd = 0; if (xdg_data_home && *xdg_data_home) dirs[nd++] = fmt_alloc("%s/applications", xdg_data_home); else if (home) dirs[nd++] = fmt_alloc("%s/.local/share/applications", home); if (home) dirs[nd++] = fmt_alloc("%s/.local/share/flatpak/exports/share/applications", home); if (xdg_data_dirs && *xdg_data_dirs) { char *copy = xstrdup(xdg_data_dirs); char *save = NULL; for (char *tok = strtok_r(copy, ":", &save); tok && nd < 14; tok = strtok_r(NULL, ":", &save)) if (*tok) dirs[nd++] = fmt_alloc("%s/applications", tok); free(copy); } else { dirs[nd++] = xstrdup("/usr/local/share/applications"); dirs[nd++] = xstrdup("/usr/share/applications"); } if (nd < 15) dirs[nd++] = xstrdup("/var/lib/flatpak/exports/share/applications"); for (int i = 0; i < nd; ++i) { if (is_dir(dirs[i])) desktop_scan_dir(dirs[i]); free(dirs[i]); } } /* org.qutebrowser.qutebrowser -> qutebrowser, org.telegram.desktop -> telegram */ static const char *short_app_name(const char *app_id) { static const char *generic[] = { "desktop", "app", "App", "gui", "GUI", "client", "Client", "bin", "Bin", "gtk", "qt", "Desktop" }; if (!app_id || !*app_id) return "unknown"; const char *dot = strrchr(app_id, '.'); if (!dot || !dot[1] || strchr(app_id, '.') == dot) return app_id; for (size_t i = 0; i < SDL_arraysize(generic); ++i) { if (ci_cmp(dot + 1, generic[i]) != 0) continue; /* the last part says nothing: use the one before it */ static char buf[128]; const char *p = dot - 1; while (p > app_id && *p != '.') p--; if (*p == '.') p++; size_t n = (size_t)(dot - p); if (n == 0 || n >= sizeof(buf)) break; memcpy(buf, p, n); buf[n] = 0; return buf; } return dot + 1; } static const DesktopEntry *desktop_find(const char *app_id) { if (!app_id || !*app_id || !C.icons) return NULL; desktop_index_build(); for (int i = 0; i < g_desktop_count; ++i) /* exact id */ if (ci_cmp(g_desktops[i].id, app_id) == 0) return &g_desktops[i]; for (int i = 0; i < g_desktop_count; ++i) /* WM_CLASS */ if (g_desktops[i].wmclass && ci_cmp(g_desktops[i].wmclass, app_id) == 0) return &g_desktops[i]; const char *shrt = short_app_name(app_id); for (int i = 0; i < g_desktop_count; ++i) /* short name */ if (ci_cmp(g_desktops[i].id, shrt) == 0) return &g_desktops[i]; for (int i = 0; i < g_desktop_count; ++i) { /* id suffix */ const char *dot = strrchr(g_desktops[i].id, '.'); if (dot && ci_cmp(dot + 1, shrt) == 0) return &g_desktops[i]; } return NULL; } /* Some toolkits hand sway an app_id that identifies nothing: GTK apps * launched without one report "GTK Application", and several Electron * builds are no better. In that case the process behind the window knows * more than the window does. */ static bool app_id_is_useless(const char *id) { static const char *bad[] = { "", "gtk application", "gtk-application", "unknown", "wayland", "xwayland", "electron", "toplevel", "window", "main", "app" }; if (!id) return true; for (size_t i = 0; i < SDL_arraysize(bad); ++i) if (ci_cmp(id, bad[i]) == 0) return true; return false; } static bool is_interpreter(const char *name) { static const char *interp[] = { "electron", "node", "python", "python3", "java", "sh", "bash", "zsh", "wine", "wine64", "wine-preloader", "flatpak", "snap", "env", "steam" }; for (size_t i = 0; i < SDL_arraysize(interp); ++i) if (ci_cmp(name, interp[i]) == 0) return true; return false; } static const char *base_name(const char *path) { const char *slash = strrchr(path, '/'); return slash ? slash + 1 : path; } /* argv[0] of the process, or its comm name; empty when nothing usable */ static void proc_app_name(int pid, char *out, size_t cap) { out[0] = 0; if (pid <= 1) return; char path[64]; snprintf(path, sizeof(path), "/proc/%d/cmdline", pid); FILE *f = fopen(path, "r"); if (f) { char buf[1024]; size_t n = fread(buf, 1, sizeof(buf) - 1, f); fclose(f); buf[n] = 0; if (n) { const char *arg0 = base_name(buf); if (*arg0 && !is_interpreter(arg0)) { str_set(out, cap, arg0); return; } /* an interpreter: the script or the app directory names it */ size_t off = strlen(buf) + 1; while (off < n && out[0] == 0) { const char *arg = buf + off; if (*arg != '-') { const char *b = base_name(arg); if (*b && !is_interpreter(b) && !strchr(b, '=')) { str_set(out, cap, b); break; } } off += strlen(arg) + 1; } if (out[0]) return; } } snprintf(path, sizeof(path), "/proc/%d/comm", pid); f = fopen(path, "r"); if (!f) return; char comm[128] = {0}; if (fgets(comm, sizeof(comm), f)) { str_trim(comm); if (comm[0] && !is_interpreter(comm)) str_set(out, cap, comm); } fclose(f); } /* what we print on a card: the .desktop Name= if we have one */ static const char *app_display_name(const char *app_id) { const DesktopEntry *de = desktop_find(app_id); if (de && de->name && *de->name) return de->name; return short_app_name(app_id); } static char *icon_theme_name(void) { const char *home = getenv("HOME"); if (home) { char *p = fmt_alloc("%s/.config/gtk-3.0/settings.ini", home); FILE *f = fopen(p, "r"); free(p); if (f) { char line[512]; while (fgets(line, sizeof(line), f)) { if (strncmp(line, "gtk-icon-theme-name", 19) == 0) { char *eq = strchr(line, '='); if (eq) { char *v = str_trim(eq + 1); if (*v) { fclose(f); return xstrdup(v); } } } } fclose(f); } } return xstrdup("hicolor"); } /* The theme directories are scanned once, in preference order. Looking an * icon up afterwards is a handful of access() calls instead of the thousands * a blind size/extension sweep would cost. */ typedef struct { char *path; int size; } IconDir; static IconDir *g_icon_dirs = NULL; static int g_icon_dir_count = 0, g_icon_dir_cap = 0; static bool g_icon_dirs_built = false; static void icon_dir_add(const char *path, int size) { if (!is_dir(path)) return; if (g_icon_dir_count == g_icon_dir_cap) { g_icon_dir_cap = g_icon_dir_cap ? g_icon_dir_cap * 2 : 32; g_icon_dirs = (IconDir *)xrealloc(g_icon_dirs, (size_t)g_icon_dir_cap * sizeof(IconDir)); } g_icon_dirs[g_icon_dir_count].path = xstrdup(path); g_icon_dirs[g_icon_dir_count].size = size; g_icon_dir_count++; } static int cmp_icon_dir(const void *A, const void *B) { const IconDir *a = (const IconDir *)A, *b = (const IconDir *)B; if (a->size != b->size) { if (a->size == 0) return -1; /* scalable wins: crisp at any size */ if (b->size == 0) return 1; return b->size - a->size; /* then big before small */ } return 0; } static void icon_dirs_build(void) { if (g_icon_dirs_built) return; g_icon_dirs_built = true; static char *theme = NULL; if (!theme) theme = icon_theme_name(); const char *home = getenv("HOME"); char *roots[4]; int nr = 0; if (home) { roots[nr++] = fmt_alloc("%s/.local/share/icons", home); roots[nr++] = fmt_alloc("%s/.icons", home); } roots[nr++] = xstrdup("/usr/share/icons"); roots[nr++] = xstrdup("/usr/local/share/icons"); const char *themes[] = { theme, "Adwaita", "breeze", "Papirus", "hicolor" }; const char *cats[] = { "apps", "applications" }; for (int r = 0; r < nr; ++r) { for (size_t t = 0; t < SDL_arraysize(themes); ++t) { if (!themes[t] || !*themes[t]) continue; char *tdir = fmt_alloc("%s/%s", roots[r], themes[t]); DIR *d = opendir(tdir); if (!d) { free(tdir); continue; } struct dirent *de; while ((de = readdir(d))) { const char *n = de->d_name; if (n[0] == '.') continue; int size = -1; if (strcmp(n, "scalable") == 0) size = 0; else if (isdigit((unsigned char)n[0])) size = atoi(n); /* 48x48 */ if (size < 0) continue; if (strstr(n, "@2x")) continue; /* skip hidpi */ for (size_t c = 0; c < SDL_arraysize(cats); ++c) { char *p = fmt_alloc("%s/%s/%s", tdir, n, cats[c]); icon_dir_add(p, size); free(p); } } closedir(d); free(tdir); } } for (int i = 0; i < nr; ++i) free(roots[i]); qsort(g_icon_dirs, (size_t)g_icon_dir_count, sizeof(IconDir), cmp_icon_dir); icon_dir_add("/usr/share/pixmaps", 0); } static char *icon_lookup(const char *name, int want_px) { (void)want_px; if (!name || !*name) return NULL; if (name[0] == '/') return file_readable(name) ? xstrdup(name) : NULL; icon_dirs_build(); const char *exts[] = { ".svg", ".png", ".xpm" }; bool has_ext = strstr(name, ".png") || strstr(name, ".svg") || strstr(name, ".xpm"); for (int d = 0; d < g_icon_dir_count; ++d) { for (size_t e = 0; e < SDL_arraysize(exts); ++e) { char *p = fmt_alloc("%s/%s%s", g_icon_dirs[d].path, name, has_ext ? "" : exts[e]); if (file_readable(p)) return p; free(p); if (has_ext) break; } } return NULL; } static SDL_Texture *icon_load_file(const char *path, int want_px) { SDL_Surface *surf = NULL; size_t n = strlen(path); if (n > 4 && ci_cmp(path + n - 4, ".svg") == 0) { SDL_IOStream *io = SDL_IOFromFile(path, "rb"); if (io) { surf = IMG_LoadSizedSVG_IO(io, want_px, want_px); SDL_CloseIO(io); } } if (!surf) surf = IMG_Load(path); if (!surf) return NULL; SDL_Texture *t = SDL_CreateTextureFromSurface(REN, surf); SDL_DestroySurface(surf); if (t) { SDL_SetTextureScaleMode(t, SDL_SCALEMODE_LINEAR); SDL_SetTextureBlendMode(t, SDL_BLENDMODE_BLEND); } return t; } /* a rounded tile with the first letter, used when no icon is found */ static SDL_Texture *icon_make_letter(const char *app_id, int want_px) { const char *nm = short_app_name(app_id); char letter[8] = {0}; /* copy one UTF-8 code point, upper-cased if ASCII */ unsigned char c0 = (unsigned char)nm[0]; int clen = (c0 < 0x80) ? 1 : (c0 < 0xe0) ? 2 : (c0 < 0xf0) ? 3 : 4; memcpy(letter, nm, (size_t)clen); if (clen == 1) letter[0] = (char)toupper(c0); SDL_Texture *t = SDL_CreateTexture(REN, SDL_PIXELFORMAT_RGBA32, SDL_TEXTUREACCESS_TARGET, want_px, want_px); if (!t) return NULL; SDL_SetTextureBlendMode(t, SDL_BLENDMODE_BLEND); SDL_SetTextureScaleMode(t, SDL_SCALEMODE_LINEAR); SDL_Texture *prev = SDL_GetRenderTarget(REN); SDL_SetRenderTarget(REN, t); SDL_SetRenderDrawBlendMode(REN, SDL_BLENDMODE_NONE); set_col((SDL_FColor){0, 0, 0, 0}); SDL_RenderClear(REN); SDL_SetRenderDrawBlendMode(REN, SDL_BLENDMODE_BLEND); SDL_FRect box = { 0, 0, (float)want_px, (float)want_px }; fill_round_rect(box, want_px * 0.24f, mix(C.hl, C.tile, 0.55f)); stroke_round_rect(box, want_px * 0.24f, SDL_max(1.0f, want_px * 0.05f), with_alpha(C.hl, 0.85f)); Tex g = text_make(F_LABEL, letter); if (g.t) { float s = SDL_min((float)want_px * 0.62f / SDL_max(1, g.h), 1.6f); SDL_FRect dst = { (want_px - g.w * s) * 0.5f, (want_px - g.h * s) * 0.5f, g.w * s, g.h * s }; SDL_SetTextureColorModFloat(g.t, C.text.r, C.text.g, C.text.b); SDL_RenderTexture(REN, g.t, NULL, &dst); tex_free(&g); } SDL_SetRenderTarget(REN, prev); return t; } typedef struct { char key[192]; SDL_Texture *tex; } IconCacheEntry; static IconCacheEntry g_icons[256]; static int g_icon_count = 0; static SDL_Texture *icon_for_app(const char *app_id, int want_px) { if (!C.icons) return NULL; const char *key = (app_id && *app_id) ? app_id : "unknown"; for (int i = 0; i < g_icon_count; ++i) if (strcmp(g_icons[i].key, key) == 0) return g_icons[i].tex; SDL_Texture *tex = NULL; const DesktopEntry *de = desktop_find(key); if (de && de->icon) { char *path = icon_lookup(de->icon, want_px); if (path) { tex = icon_load_file(path, want_px); free(path); } } if (!tex) { /* app_id often *is* the icon */ char *path = icon_lookup(key, want_px); if (!path) path = icon_lookup(short_app_name(key), want_px); if (path) { tex = icon_load_file(path, want_px); free(path); } } if (!tex) tex = icon_make_letter(key, want_px); if (g_icon_count < (int)SDL_arraysize(g_icons)) { str_set(g_icons[g_icon_count].key, sizeof(g_icons[0].key), key); g_icons[g_icon_count].tex = tex; g_icon_count++; } return tex; } static void icons_free(void) { for (int i = 0; i < g_icon_count; ++i) if (g_icons[i].tex) SDL_DestroyTexture(g_icons[i].tex); g_icon_count = 0; for (int i = 0; i < g_desktop_count; ++i) { free(g_desktops[i].id); free(g_desktops[i].path); free(g_desktops[i].icon); free(g_desktops[i].wmclass); free(g_desktops[i].name); } for (int i = 0; i < g_icon_dir_count; ++i) free(g_icon_dirs[i].path); free(g_icon_dirs); g_icon_dirs = NULL; g_icon_dir_count = g_icon_dir_cap = 0; g_icon_dirs_built = false; free(g_desktops); g_desktops = NULL; g_desktop_count = 0; g_desktop_indexed = false; } /* ----------------------------------------------------------------- model */ typedef struct { int con_id; int pid; char app_id[128]; /* what sway reports */ char app_key[128]; /* what we look the icon and the name up with */ char title[512]; char ws_name[64]; char output[64]; 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 */ SDL_FRect tab; /* its slot in the container's tab strip */ bool has_tab; int ws; /* index into WSS */ SDL_FRect card; /* where it is drawn (render pixels) */ SDL_FRect hit; /* what the mouse acts on: the whole card, or * just the name plate of a floating window */ int group; /* head index of a tabbed/stacked group, or -1 */ int lay_mode; /* how the card contents are arranged */ float lay_icon; /* icon edge length on this card */ bool lay_sub; /* is there room for the title */ SDL_Texture *icon; /* borrowed from the icon cache */ Tex label, subtitle, pidtex; } Win; typedef struct { char name[64]; /* what sway calls it: "3" or "3:code" */ char label[64]; /* just the name part, "" when it is only a number */ char output[64]; double usage; /* seconds spent here, from the tracker */ int num; bool focused, visible, urgent; int rx, ry, rw, rh; /* workspace rect */ int first, count; /* range in WINS */ int sel; /* selected window, relative to first; -1 = the * workspace itself is selected */ int top[64]; /* top level containers, in order */ int ntop; SDL_FRect tile, screen; /* layout (render pixels) */ bool marked; /* the workspace itself, not its windows */ SDL_FRect title_box; /* where the name is drawn in the header */ SDL_FRect title_hit; /* clicking there renames: the pencil at the * right end of the name, or with * rename_icon=0 the middle half of it */ SDL_FRect tile_from; /* where this tile animates from */ Tex badge, sub, title; } Ws; static Win WINS[MAX_WINDOWS]; 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 */ /* Every monitor, in the arrangement sway has them in, so the overview can * draw a little map of the desk and let you step onto another screen. */ typedef struct { char name[64]; int x, y, w, h; bool focused; SDL_FRect box; /* where it sits in the map */ } Out; static Out OUTS[8]; static int NOUTS; 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 int drop_out = -1; /* the monitor a drag is over, in the map */ static int press_slot = -1; /* an empty number the press landed on */ static int OUT_PRESS = -1; /* a monitor being dragged in the map */ static float OUT_PRESS_X, OUT_PRESS_Y; static int OUT_TARGET = -1; /* ...and the one it is being put against */ static int OUT_EDGE; static int SNAP_WIN = -1; /* the card a floating drag is resting on */ static double SNAP_AT; /* ...and since when */ static float SNAP_X, SNAP_Y; /* where the pointer was when it settled */ static bool SNAP_DONE; /* the hold finished: now pick an edge */ static double MAP_HOVER_SINCE; static float MAP_HOVER_X, MAP_HOVER_Y; static void outputs_reload(void) { NOUTS = 0; JV *outs = sway_query(IPC_GET_OUTPUTS); if (!outs || outs->type != J_ARR) { jfree(outs); return; } for (int i = 0; i < outs->count && NOUTS < (int)SDL_arraysize(OUTS); ++i) { const JV *o = outs->items[i]; if (!jbool(o, "active", true)) continue; const JV *r = jget(o, "rect"); Out *d = &OUTS[NOUTS++]; str_set(d->name, sizeof(d->name), jstr(o, "name", "")); d->x = jint(r, "x", 0); d->y = jint(r, "y", 0); d->w = jint(r, "width", 0); d->h = jint(r, "height", 0); d->focused = jbool(o, "focused", false); if (d->focused) str_set(HOME_OUTPUT, sizeof(HOME_OUTPUT), d->name); d->box = (SDL_FRect){ 0, 0, 0, 0 }; } jfree(outs); } static char CUR_OUTPUT[64] = {0}; /* output the tree walk is inside */ static void model_free(void) { for (int i = 0; i < NWIN; ++i) { tex_free(&WINS[i].label); tex_free(&WINS[i].subtitle); } for (int i = 0; i < NWS; ++i) { tex_free(&WSS[i].badge); tex_free(&WSS[i].sub); tex_free(&WSS[i].title); } NWIN = NWS = 0; } static bool node_is_view(const JV *n) { if (jget(n, "app_id") && jget(n, "app_id")->type == J_STR) return true; const JV *wp = jget(n, "window_properties"); return wp && jget(wp, "class") && jget(wp, "class")->type == J_STR; } static void collect_views(const JV *node, Ws *ws, bool floating) { if (!node || node->type != J_OBJ) return; if (node_is_view(node) && NWIN < MAX_WINDOWS) { Win *w = &WINS[NWIN]; memset(w, 0, sizeof(*w)); const char *app = jstr(node, "app_id", NULL); if (!app) { const JV *wp = jget(node, "window_properties"); app = wp ? jstr(wp, "class", NULL) : NULL; } str_set(w->app_id, sizeof(w->app_id), app ? app : "unknown"); memcpy(w->app_key, w->app_id, sizeof(w->app_key)); str_set(w->title, sizeof(w->title), jstr(node, "name", "")); str_set(w->ws_name, sizeof(w->ws_name), ws->name); str_set(w->output, sizeof(w->output), ws->output); 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); w->sticky = jbool(node, "sticky", false); w->floating = floating; w->fullscreen = jint(node, "fullscreen_mode", 0) != 0; w->match = true; w->ws = (int)(ws - WSS); const JV *r = jget(node, "rect"); w->x = jint(r, "x", ws->rx); w->y = jint(r, "y", ws->ry); w->w = jint(r, "width", 100); w->h = jint(r, "height", 100); if (w->w <= 0) w->w = 100; if (w->h <= 0) w->h = 100; NWIN++; ws->count++; return; /* views have no children */ } const JV *nodes = jget(node, "nodes"); if (nodes && nodes->type == J_ARR) for (int i = 0; i < nodes->count; ++i) collect_views(nodes->items[i], ws, floating); const JV *fl = jget(node, "floating_nodes"); if (fl && fl->type == J_ARR) for (int i = 0; i < fl->count; ++i) collect_views(fl->items[i], ws, true); } static int cmp_win_pos(const void *A, const void *B) { const Win *a = (const Win *)A, *b = (const Win *)B; bool af = a->floating || a->fullscreen; /* both are drawn on top */ bool bf = b->floating || b->fullscreen; if (af != bf) return af ? 1 : -1; if (a->y != b->y) return a->y < b->y ? -1 : 1; if (a->x != b->x) return a->x < b->x ? -1 : 1; return a->con_id - b->con_id; } static void walk_outputs(const JV *node) { if (!node || node->type != J_OBJ) return; const char *type = jstr(node, "type", ""); if (strcmp(type, "workspace") == 0) { const char *name = jstr(node, "name", "?"); if (strcmp(name, "__i3_scratchpad") == 0) return; if (NWS >= MAX_WORKSPACES) return; Ws *ws = &WSS[NWS]; memset(ws, 0, sizeof(*ws)); str_set(ws->name, sizeof(ws->name), name); str_set(ws->output, sizeof(ws->output), CUR_OUTPUT); ws->num = jint(node, "num", str_all_digits(name) ? atoi(name) : -1); const char *colon = strchr(name, ':'); /* "3:code" -> "code" */ if (colon) str_set(ws->label, sizeof(ws->label), colon + 1); else if (!str_all_digits(name)) str_set(ws->label, sizeof(ws->label), name); ws->focused = jbool(node, "focused", false); ws->urgent = jbool(node, "urgent", false); const JV *r = jget(node, "rect"); ws->rx = jint(r, "x", 0); ws->ry = jint(r, "y", 0); ws->rw = jint(r, "width", 1920); ws->rh = jint(r, "height", 1080); ws->first = NWIN; ws->count = 0; ws->sel = -1; ws->ntop = 0; NWS++; /* remember the direct children: moving those keeps their layout */ const JV *kids[2] = { jget(node, "nodes"), jget(node, "floating_nodes") }; for (int a = 0; a < 2; ++a) if (kids[a] && kids[a]->type == J_ARR) for (int b = 0; b < kids[a]->count && ws->ntop < 64; ++b) { int id = jint(kids[a]->items[b], "id", 0); if (id) ws->top[ws->ntop++] = id; } collect_views(node, ws, false); if (ws->count > 1) qsort(&WINS[ws->first], (size_t)ws->count, sizeof(Win), cmp_win_pos); for (int i = 0; i < ws->count; ++i) WINS[ws->first + i].ws = (int)(ws - WSS); return; } if (strcmp(type, "output") == 0) { const char *name = jstr(node, "name", ""); if (strcmp(name, "__i3") == 0) return; /* Asking for a fixed set of numbers only makes sense if the * workspaces behind them are real wherever they live: otherwise 3 * and 5 are on the other screen, so the model has never heard of * them, and their tiles come out as empty numbered placeholders — * the grid looks right and the windows are missing. */ bool want_all = C.all_outputs || DRAG_ALL_OUTPUTS || C.slots_hi > 0; if (!want_all && FOCUSED_OUTPUT[0] && strcmp(name, FOCUSED_OUTPUT) != 0) return; str_set(CUR_OUTPUT, sizeof(CUR_OUTPUT), name); } const JV *nodes = jget(node, "nodes"); if (nodes && nodes->type == J_ARR) for (int i = 0; i < nodes->count; ++i) walk_outputs(nodes->items[i]); } static int cmp_ws(const void *A, const void *B) { const Ws *a = (const Ws *)A, *b = (const Ws *)B; int o = strcmp(a->output, b->output); if (o) return o; if (a->num >= 0 && b->num >= 0 && a->num != b->num) return a->num < b->num ? -1 : 1; if ((a->num >= 0) != (b->num >= 0)) return a->num >= 0 ? -1 : 1; return strcmp(a->name, b->name); } /* Fetch workspaces + tree and rebuild the model. Returns false on failure. */ static bool model_reload(void) { model_free(); char focused[64] = {0}; JV *wsr = sway_query(IPC_GET_WORKSPACES); if (wsr && wsr->type == J_ARR) { for (int i = 0; i < wsr->count; ++i) if (jbool(wsr->items[i], "focused", false)) str_set(focused, sizeof(focused), jstr(wsr->items[i], "output", "")); } /* sway reports no focused workspace for a moment after a rename; keeping * the previous output beats falling back to "every output" */ if (focused[0] && !PINNED_OUTPUT) str_set(FOCUSED_OUTPUT, sizeof(FOCUSED_OUTPUT), focused); outputs_reload(); JV *tree = sway_query(IPC_GET_TREE); if (!tree) { jfree(wsr); return false; } CUR_OUTPUT[0] = 0; walk_outputs(tree); /* per-output names + visible flags from get_workspaces */ if (wsr && wsr->type == J_ARR) { for (int i = 0; i < NWS; ++i) { for (int j = 0; j < wsr->count; ++j) { const JV *o = wsr->items[j]; if (strcmp(jstr(o, "name", ""), WSS[i].name) != 0) continue; str_set(WSS[i].output, sizeof(WSS[i].output), jstr(o, "output", "")); WSS[i].visible = jbool(o, "visible", false); WSS[i].focused = jbool(o, "focused", WSS[i].focused); WSS[i].urgent = jbool(o, "urgent", WSS[i].urgent); WSS[i].num = jint(o, "num", WSS[i].num); break; } } } jfree(wsr); jfree(tree); /* drop empty workspaces if the user does not want them */ if (!C.show_empty) { int k = 0; for (int i = 0; i < NWS; ++i) if (WSS[i].count > 0) WSS[k++] = WSS[i]; NWS = k; } /* sorting workspaces moves them around; fix up window -> workspace links */ if (NWS > 1) qsort(WSS, (size_t)NWS, sizeof(Ws), cmp_ws); for (int i = 0; i < NWS; ++i) for (int j = 0; j < WSS[i].count; ++j) WINS[WSS[i].first + j].ws = i; usage_load(); double pending = usage_pending(); bool changed = false; for (int i = 0; i < NUSAGE; ) { /* forget what is gone */ bool alive = false; for (int j = 0; j < NWS; ++j) if (strcmp(WSS[j].name, USAGE[i].name) == 0 && WSS[j].count > 0) alive = true; if (alive) { ++i; continue; } USAGE[i] = USAGE[--NUSAGE]; /* an empty workspace resets */ changed = true; } if (changed) usage_save(); USAGE_MAX = 0.0; for (int i = 0; i < NWS; ++i) { Usage *u = usage_find(WSS[i].name, false); WSS[i].usage = u ? u->secs : 0.0; if (strcmp(WSS[i].name, USAGE_CUR) == 0) WSS[i].usage += pending; if (WSS[i].count == 0) WSS[i].usage = 0.0; /* empty means start over */ if (WSS[i].usage > USAGE_MAX) USAGE_MAX = WSS[i].usage; } for (int i = 0; i < NWIN; ++i) { Win *w = &WINS[i]; if (app_id_is_useless(w->app_id)) { /* ask the process instead */ char nm[128]; proc_app_name(w->pid, nm, sizeof(nm)); if (nm[0]) str_set(w->app_key, sizeof(w->app_key), nm); } w->icon = icon_for_app(w->app_key, px((float)C.icon_px * 1.6f)); } return true; } /* ---------------------------------------------------------------- layout */ typedef struct { int ws; int num; SDL_FRect tile, from; } Slot; /* ws < 0 = ghost */ static Slot SLOTS[MAX_WORKSPACES + 16]; static int NSLOTS; static bool drag_ws_mode; /* a workspace is being dragged right now */ static int ghost_lo = -1, ghost_hi = -1; /* free numbers currently offered */ static Slot DYING[16]; /* ghosts on their way out */ static int NDYING; static Uint64 dying_start; static bool laid_out_once; static bool shown_once; /* a frame has been presented */ /* Nothing glides while swov is still settling. * * Opening it lays the grid out several times over: with the model half * built, again once the fonts are in, again when sway gives the window its * real size, and again for the events our own focus request provokes. Any of * those counted as "where the tiles were a moment ago", so they slid into * position instead of simply being there. A frame on screen is not enough of * a test — the resize comes after it. */ #define SETTLE_MS 500 static bool settled(void) { return shown_once && SDL_GetTicks() > SETTLE_MS; } static Uint64 anim_start; /* tiles glide when the grid changes */ static bool anim_running(void) { if (C.anim_ms <= 0.0f) return false; if (anim_start && (float)(SDL_GetTicks() - anim_start) < C.anim_ms) return true; return NDYING > 0 && (float)(SDL_GetTicks() - dying_start) < C.anim_ms; } static float phase_since(Uint64 start) { if (!start || C.anim_ms <= 0.0f) return 1.0f; float t = (float)(SDL_GetTicks() - start) / C.anim_ms; if (t >= 1.0f) return 1.0f; if (t <= 0.0f) return 0.0f; float inv = 1.0f - t; return 1.0f - inv * inv * inv; } /* 0..1, ease-out so the movement settles instead of stopping dead */ static float anim_phase(void) { if (!anim_start || C.anim_ms <= 0.0f) return 1.0f; float t = (float)(SDL_GetTicks() - anim_start) / C.anim_ms; if (t >= 1.0f) return 1.0f; if (t <= 0.0f) return 0.0f; float inv = 1.0f - t; return 1.0f - inv * inv * inv; } static SDL_FRect rect_lerp(SDL_FRect a, SDL_FRect b, float t) { return (SDL_FRect){ a.x + (b.x - a.x) * t, a.y + (b.y - a.y) * t, a.w + (b.w - a.w) * t, a.h + (b.h - a.h) * t }; } static SDL_FRect rect_shrink(SDL_FRect r, float f) { float w = r.w * f, h = r.h * f; return (SDL_FRect){ r.x + (r.w - w) * 0.5f, r.y + (r.h - h) * 0.5f, w, h }; } static int GRID_COLS = 1; /* columns the tiles are arranged in */ static int RW, RH; /* size of the render target, in pixels */ static float HEADER_H, FOOTER_H; /* position strings: none | top-left | top-center | top-right | * bottom-left | bottom-center | bottom-right */ static bool pos_is_none(const char *p) { return !p || !*p || ci_cmp(p, "none") == 0 || ci_cmp(p, "off") == 0 || ci_cmp(p, "hidden") == 0; } static bool pos_is_top(const char *p) { return !pos_is_none(p) && strncmp(p, "top", 3) == 0; } static bool pos_is_bottom(const char *p) { return !pos_is_none(p) && !pos_is_top(p); } /* x of a text of width w for the horizontal half of the position string */ static float pos_x(const char *p, float w, float margin) { const char *dash = strchr(p ? p : "", '-'); const char *h = dash ? dash + 1 : "right"; if (ci_cmp(h, "left") == 0) return margin; if (ci_cmp(h, "center") == 0 || ci_cmp(h, "centre") == 0) return ((float)RW - w) * 0.5f; return (float)RW - margin - w; } 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; static bool pencil_hot; /* the pointer is on a rename pencil */ static void draw_pencil(SDL_FRect r, SDL_FColor c); /* 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 */ static bool searching = false; /* "s" — highlights what matches, hides nothing */ static bool query_active(void) { return filtering || searching; } /* The key press that opens a mode is followed by its own text input event — * "s" would otherwise be the first character of the search. */ static bool swallow_next_text = false; static bool confirm_kill = false; /* renaming a workspace in place, from a click on its title */ static bool editing = false; static int edit_ws = -1; static char edit_buf[64]; static int edit_len = 0; /* NULL means "the workspace itself is selected, not a window in it" */ static Win *ws_sel_win(Ws *w) { if (w->count <= 0 || w->sel < 0) return NULL; if (w->sel >= w->count) w->sel = w->count - 1; return &WINS[w->first + w->sel]; } static bool rect_overlap_same(const Win *a, const Win *b) { int tol = 6; return SDL_abs(a->x - b->x) <= tol && SDL_abs(a->y - b->y) <= tol && SDL_abs(a->w - b->w) <= tol && SDL_abs(a->h - b->h) <= tol; } static void layout_cards(Ws *ws) { if (ws->count <= 0) return; /* the windows live in an area that keeps an equal border on all four * sides of the mini screen, so nothing ever touches the tile frame */ float sp = C.screen_pad * SC; SDL_FRect area = { ws->screen.x + sp, ws->screen.y + sp, ws->screen.w - 2.0f * sp, ws->screen.h - 2.0f * sp }; if (area.w < 8.0f || area.h < 8.0f) area = ws->screen; float sx = area.w / (float)SDL_max(1, ws->rw); float sy = area.h / (float)SDL_max(1, ws->rh); float inset = C.win_gap * SC * 0.5f; float min_w = 74.0f * SC, min_h = 52.0f * SC; min_w = SDL_min(min_w, area.w); min_h = SDL_min(min_h, area.h); for (int i = 0; i < ws->count; ++i) { Win *w = &WINS[ws->first + i]; SDL_FRect r = { area.x + (float)(w->x - ws->rx) * sx + inset, area.y + (float)(w->y - ws->ry) * sy + inset, (float)w->w * sx - 2.0f * inset, (float)w->h * sy - 2.0f * inset }; if (r.w < min_w) { r.x -= (min_w - r.w) * 0.5f; r.w = min_w; } if (r.h < min_h) { r.y -= (min_h - r.h) * 0.5f; r.h = min_h; } w->card = r; } /* Tabbed / stacked containers report identical geometry for every child. * Split such a group into side by side slices so all of them stay * visible and clickable. */ bool done[MAX_WINDOWS]; memset(done, 0, sizeof(bool) * (size_t)ws->count); for (int i = 0; i < ws->count; ++i) { WINS[ws->first + i].group = -1; WINS[ws->first + i].has_tab = false; } for (int i = 0; i < ws->count; ++i) { if (done[i]) continue; int group[64]; int n = 0; group[n++] = i; for (int j = i + 1; j < ws->count && n < 64; ++j) { if (done[j]) continue; if (WINS[ws->first + i].floating != WINS[ws->first + j].floating) continue; if (rect_overlap_same(&WINS[ws->first + i], &WINS[ws->first + j])) { group[n++] = j; done[j] = true; } } done[i] = true; if (n < 2) continue; float ge = 3.0f * SC; /* room for the group outline */ SDL_FRect base = WINS[ws->first + i].card; base.x += ge; base.y += ge; base.w -= 2.0f * ge; base.h -= 2.0f * ge; if (base.w < 4.0f || base.h < 4.0f) base = WINS[ws->first + i].card; for (int k = 0; k < n; ++k) WINS[ws->first + group[k]].group = i; /* Which child is on top: sway marks it visible, otherwise the focused * one, otherwise the first. */ int active = 0; for (int k = 0; k < n; ++k) { Win *cw = &WINS[ws->first + group[k]]; if (cw->focused) { active = k; break; } if (cw->visible) active = k; } float strip = SDL_min(32.0f * SC, base.h * 0.36f); 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. 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 + 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 + pad, base.y + pad + strip + inset * 0.5f, inner, base.h - 2.0f * pad - strip - inset * 0.5f } : cw->tab; } } else { bool horiz = base.w >= base.h; /* too small: plain slices */ for (int k = 0; k < n; ++k) { SDL_FRect r = base; if (horiz) { r.w = (base.w - (float)(n - 1) * inset * 2.0f) / (float)n; r.x = base.x + (float)k * (r.w + inset * 2.0f); } else { r.h = (base.h - (float)(n - 1) * inset * 2.0f) / (float)n; r.y = base.y + (float)k * (r.h + inset * 2.0f); } WINS[ws->first + group[k]].card = r; WINS[ws->first + group[k]].has_tab = false; } } } /* keep everything inside the tile */ for (int i = 0; i < ws->count; ++i) { SDL_FRect *r = &WINS[ws->first + i].card; if (r->w > area.w) r->w = area.w; if (r->h > area.h) r->h = area.h; if (r->x < area.x) r->x = area.x; if (r->y < area.y) r->y = area.y; if (r->x + r->w > area.x + area.w) r->x = area.x + area.w - r->w; if (r->y + r->h > area.y + area.h) r->y = area.y + area.h - r->h; } } /* shallow cards (a tab, say) cannot spare 7px top and bottom */ static float card_pad(const Win *w) { return SDL_min(7.0f * SC, w->card.h * 0.13f); } enum { CL_ICON, CL_STACK, CL_ROW, CL_TEXT }; /* Decide how much fits on a card: icon + app name + title, icon + name, * a row (icon left, text right) on wide flat cards, or just an icon. */ static void compute_card_layout(Win *w) { float pad = card_pad(w); float availw = w->card.w - 2.0f * pad; float availh = w->card.h - 2.0f * pad; float lab_h = (float)TTF_GetFontHeight(F_LABEL); float sub_h = (float)TTF_GetFontHeight(F_TITLE); float g1 = 4.0f * SC, g2 = 1.0f * SC; float icon_max = C.icons ? SDL_min((float)C.icon_px * SC, SDL_min(availw * 0.72f, availh * 0.72f)) : 0.0f; float icon_min = 16.0f * SC; w->lay_mode = CL_ICON; w->lay_icon = icon_max; w->lay_sub = false; if (availw <= 4.0f || availh <= 4.0f) return; if (availh >= icon_max + g1 + lab_h + g2 + sub_h) { /* everything */ w->lay_mode = CL_STACK; w->lay_icon = icon_max; w->lay_sub = true; return; } if (availw >= availh * 1.15f && availw >= lab_h * 5.0f && availh >= lab_h + g2 + sub_h) { /* wide and flat */ w->lay_mode = CL_ROW; w->lay_icon = C.icons ? SDL_min(icon_max, SDL_min(availh * 0.9f, availw * 0.3f)) : 0.0f; if (w->lay_icon < icon_min) w->lay_icon = 0.0f; w->lay_sub = true; return; } if (availh - (lab_h + g1 + g2 + sub_h) >= icon_min) { /* shrink the icon */ w->lay_mode = CL_STACK; w->lay_icon = availh - (lab_h + g1 + g2 + sub_h); w->lay_sub = true; return; } if (availh >= icon_min + g1 + lab_h) { /* icon + name */ w->lay_mode = CL_STACK; w->lay_icon = SDL_min(icon_max, availh - lab_h - g1); w->lay_sub = false; return; } if (availh >= lab_h) { /* name only */ w->lay_mode = CL_TEXT; w->lay_icon = 0.0f; w->lay_sub = false; return; } w->lay_icon = SDL_min(availw, availh); /* icon only */ } /* Floating and fullscreen windows are drawn as see-through frames with a name * plate at the top. The plate is the only part that takes the mouse, so the * windows underneath stay clickable. */ static bool card_is_overlay(const Win *w) { return w->floating || w->fullscreen; } static bool overlay_plate(const Win *w, SDL_FRect r, SDL_FRect *plate, float *icon_w) { if (!w->label.t) return false; float ip = 6.0f * SC; float ih = SDL_min(22.0f * SC, (float)C.icon_px * SC * 0.6f); bool has_icon = C.icons && w->icon && w->lay_icon >= 10.0f * SC && ih >= 10.0f * SC; float iw = has_icon ? ih + 5.0f * SC : 0.0f; float pw = iw + (float)w->label.w + 2.0f * ip; float ph = SDL_max((float)w->label.h, ih) + ip; if (pw > r.w - 4.0f * SC) { /* tight: drop the icon first */ iw = 0.0f; pw = (float)w->label.w + 2.0f * ip; ph = (float)w->label.h + ip; } if (pw > r.w - 4.0f * SC || ph > r.h * 0.6f) return false; *plate = (SDL_FRect){ r.x + (r.w - pw) * 0.5f, r.y + 7.0f * SC, pw, ph }; if (icon_w) *icon_w = iw; return true; } static float card_text_width(const Win *w) { float pad = card_pad(w); float availw = w->card.w - 2.0f * pad; if (w->lay_mode == CL_ROW && w->lay_icon > 0.0f) availw -= w->lay_icon + 6.0f * SC; return availw; } /* The name printed in bold on a card. Sway's app_id is used when it says * something; otherwise the process behind the window has already been asked * (app_key), and failing that the tail of the title is the best guess: * "document.txt - Some Editor". */ static const char *card_label_name(const Win *w) { /* sway knew the app: that is the best answer */ if (!app_id_is_useless(w->app_id)) return app_display_name(w->app_id); /* the process behind it matched a .desktop file: also solid */ const DesktopEntry *de = desktop_find(w->app_key); if (de && de->name && *de->name) return de->name; /* otherwise the tail of the title names the app more often than a binary * does: "document.txt - Some Editor", or plainly "Claude" */ if (w->title[0]) { const char *tail = NULL; for (const char *p = strstr(w->title, " - "); p; p = strstr(p + 3, " - ")) tail = p + 3; for (const char *p = strstr(w->title, " \xe2\x80\x94 "); p; p = strstr(p + 5, " \xe2\x80\x94 ")) tail = p + 5; if (tail && *tail) return tail; /* no separator at all: a short title is usually the app name itself * ("Claude"), a long one is a document and the binary is the better * answer */ if (strlen(w->title) <= 24) return w->title; } /* last resort: the executable name, without its extension */ if (!app_id_is_useless(w->app_key)) { static char buf[128]; str_set(buf, sizeof(buf), w->app_key); char *dot = strrchr(buf, '.'); if (dot && (ci_cmp(dot, ".py") == 0 || ci_cmp(dot, ".sh") == 0 || ci_cmp(dot, ".js") == 0 || ci_cmp(dot, ".bin") == 0 || ci_cmp(dot, ".exe") == 0)) *dot = 0; if (buf[0]) return buf; } return w->title[0] ? w->title : "unknown"; } static void build_texts(void) { for (int i = 0; i < NWS; ++i) { Ws *ws = &WSS[i]; tex_free(&ws->badge); tex_free(&ws->sub); char num[16]; if (ws->num >= 0) snprintf(num, sizeof(num), "%d", ws->num); else str_set(num, sizeof(num), ws->name); ws->badge = text_make(F_BADGE, num); ws->title = ws->label[0] ? text_make(F_LABEL, ws->label) : (Tex){0}; /* "DP-1 · 3 windows · 1h 20m", assembled from bounded pieces */ char count[32], time[32] = "", out[80] = ""; if (ws->count == 0) snprintf(count, sizeof(count), "empty"); else if (ws->count == 1) snprintf(count, sizeof(count), "1 window"); else snprintf(count, sizeof(count), "%d windows", ws->count); if (C.usage_dots && ws->usage >= 60.0) { /* say what the dots mean */ int mins = (int)(ws->usage / 60.0); if (mins < 60) snprintf(time, sizeof(time), " \xc2\xb7 %dm", mins); else snprintf(time, sizeof(time), " \xc2\xb7 %dh %dm", mins / 60, mins % 60); } if (C.all_outputs && ws->output[0]) snprintf(out, sizeof(out), "%.63s \xc2\xb7 ", ws->output); char sub[192]; snprintf(sub, sizeof(sub), "%.79s%.31s%.31s", out, count, time); ws->sub = text_make(F_HINT, sub); } for (int i = 0; i < NWIN; ++i) { Win *w = &WINS[i]; tex_free(&w->label); tex_free(&w->subtitle); tex_free(&w->pidtex); w->hit = w->card; /* the whole card, unless a plate * takes over below */ compute_card_layout(w); int maxw = (int)card_text_width(w); if (maxw < 12 || w->lay_mode == CL_ICON) continue; const char *name = card_label_name(w); w->label = text_make_fit(F_LABEL, name, maxw); if (w->lay_sub && strcmp(name, w->title) != 0) w->subtitle = text_make_fit(F_TITLE, w->title, maxw); /* The pid, small and out of the way. It is the one thing about a * window you cannot get at from the title, and the thing you want * when something has to be killed or traced. */ if (C.show_pid && w->pid > 0) { char p[24]; snprintf(p, sizeof(p), "%d", w->pid); w->pidtex = text_make_fit(F_PID ? F_PID : F_HINT, p, maxw); } SDL_FRect plate; if (card_is_overlay(w) && overlay_plate(w, w->card, &plate, NULL)) w->hit = plate; } } static void layout(void) { float m = C.margin * SC; float gap = C.gap * SC; bool head_on = C.show_header && !pos_is_none(C.header_pos); bool hints_on = C.show_hints && !pos_is_none(C.hints_pos); float line = (float)C.hint_px * SC * C.ui_scale * 2.4f; bool top = (head_on && pos_is_top(C.header_pos)) || (hints_on && pos_is_top(C.hints_pos)); bool bottom = (head_on && pos_is_bottom(C.header_pos)) || (hints_on && pos_is_bottom(C.hints_pos)); HEADER_H = top ? line : m; FOOTER_H = bottom ? line : 0.0f; float ax = m; float ay = HEADER_H; float aw = (float)RW - 2.0f * m; float ah = (float)RH - HEADER_H - FOOTER_H - m; if (aw < 40.0f || ah < 40.0f || NWS == 0) return; /* The monitor map keeps a fixed place in the bottom left, always the * same one whichever screen is being shown. Handing it whatever cell the * 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; for (int i = 1; i < NOUTS; ++i) { if (OUTS[i].x < minx) minx = OUTS[i].x; if (OUTS[i].y < miny) miny = OUTS[i].y; if (OUTS[i].x + OUTS[i].w > maxx) maxx = OUTS[i].x + OUTS[i].w; if (OUTS[i].y + OUTS[i].h > maxy) maxy = OUTS[i].y + OUTS[i].h; } float dw = (float)(maxx - minx), dh = (float)(maxy - miny); if (dw > 1.0f && dh > 1.0f) { float mw = aw * SDL_clamp(C.outputs_map_w, 0.05f, 0.45f); float mh = mw * dh / dw; 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; } } } /* aspect of the screen we are mirroring */ float aspect = 16.0f / 9.0f; for (int i = 0; i < NWS; ++i) if (WSS[i].rw > 0 && WSS[i].rh > 0) { aspect = (float)WSS[i].rw / (float)WSS[i].rh; break; } int best_rows = 1, best_cols = NWS; float best_area = -1.0f; if (C.rows > 0 || C.cols > 0) { best_rows = C.rows > 0 ? C.rows : (NWS + C.cols - 1) / C.cols; best_cols = C.cols > 0 ? C.cols : (NWS + C.rows - 1) / C.rows; if (best_rows * best_cols < NWS) best_cols = (NWS + best_rows - 1) / best_rows; } else { for (int rows = 1; rows <= NWS; ++rows) { int cols = (NWS + rows - 1) / rows; float tw = (aw - gap * (float)(cols - 1)) / (float)cols; float th = (ah - gap * (float)(rows - 1)) / (float)rows; if (tw <= 0.0f || th <= 0.0f) continue; /* tiles keep the screen aspect (plus room for the tile header) */ float ch = tw / aspect; if (ch > th) tw = th * aspect; else th = ch; float area = tw * th; if (area > best_area) { best_area = area; best_rows = rows; best_cols = cols; } } } /* While a workspace is dragged every free number 0..10 becomes a small * ghost slot, so it can be dropped on a workspace that does not exist * yet. Otherwise the slots are just the workspaces themselves. */ /* Slots are the workspaces themselves. Only while something is dragged * into a gap do the free numbers of that gap join them, as small ghost * tiles to drop on — they are not shown before they mean anything. */ /* Where everything is *right now*, mid-glide included: a relayout during * an animation has to continue from what the eye sees, otherwise the * movement snaps. */ Slot old[MAX_WORKSPACES + 16]; int nold = SDL_min(NSLOTS, (int)SDL_arraysize(SLOTS)); float ph_now = anim_phase(); for (int i = 0; i < nold; ++i) { old[i] = SLOTS[i]; old[i].tile = rect_lerp(SLOTS[i].from, SLOTS[i].tile, ph_now); } NSLOTS = 0; for (int i = 0; i < NWS && NSLOTS < (int)SDL_arraysize(SLOTS); ++i) { if (WSS[i].num < 0) continue; SLOTS[NSLOTS].ws = i; SLOTS[NSLOTS].num = WSS[i].num; NSLOTS++; } for (int n = ghost_lo; ghost_lo >= 0 && n <= ghost_hi && NSLOTS < (int)SDL_arraysize(SLOTS); ++n) { bool taken = false; for (int i = 0; i < NWS; ++i) if (WSS[i].num == n) { taken = true; break; } if (taken) continue; SLOTS[NSLOTS].ws = -1; SLOTS[NSLOTS].num = n; NSLOTS++; } for (int i = 0; i + 1 < NSLOTS; ++i) /* order by number */ for (int j = i + 1; j < NSLOTS; ++j) if (SLOTS[j].num < SLOTS[i].num) { Slot t = SLOTS[i]; SLOTS[i] = SLOTS[j]; SLOTS[j] = t; } for (int i = 0; i < NWS && NSLOTS < (int)SDL_arraysize(SLOTS); ++i) if (WSS[i].num < 0) { /* named workspaces last */ SLOTS[NSLOTS].ws = i; SLOTS[NSLOTS].num = -1; NSLOTS++; } int n_tiles = NSLOTS; if (C.rows <= 0 && C.cols <= 0 && n_tiles != NWS) { /* redo the grid search */ best_area = -1.0f; for (int rows = 1; rows <= n_tiles; ++rows) { int cols = (n_tiles + rows - 1) / rows; float tw = (aw - gap * (float)(cols - 1)) / (float)cols; float th = (ah - gap * (float)(rows - 1)) / (float)rows; if (tw <= 0.0f || th <= 0.0f) continue; float ch = tw / aspect; if (ch > th) tw = th * aspect; else th = ch; float area = tw * th; if (area > best_area) { best_area = area; best_rows = rows; best_cols = cols; } } } int cols = best_cols, rows = best_rows; if (cols * rows < n_tiles) cols = (n_tiles + rows - 1) / rows; GRID_COLS = SDL_max(1, cols); float tw = (aw - gap * (float)(cols - 1)) / (float)cols; float th = (ah - gap * (float)(rows - 1)) / (float)rows; float head_h = (float)C.ws_px * SC * 1.55f; float body_aspect_h = tw / aspect + head_h; if (body_aspect_h < th) th = body_aspect_h; float used_w = (float)cols * tw + (float)(cols - 1) * gap; float used_h = (float)rows * th + (float)(rows - 1) * gap; float ox = ax + (aw - used_w) * 0.5f; float oy = ay + (ah - used_h) * 0.5f; for (int sidx = 0; sidx < NSLOTS; ++sidx) { int r = sidx / cols, c = sidx % cols; SLOTS[sidx].tile = (SDL_FRect){ ox + (float)c * (tw + gap), oy + (float)r * (th + gap), tw, th }; if (SLOTS[sidx].ws < 0) continue; /* ghost: nothing to lay out */ Ws *ws = &WSS[SLOTS[sidx].ws]; ws->tile = SLOTS[sidx].tile; float p = C.pad * SC; float gutter = C.usage_dots ? C.dot_px * SC * 2.0f : 0.0f; SDL_FRect body = { ws->tile.x + p + gutter, ws->tile.y + head_h, ws->tile.w - 2.0f * p - gutter, ws->tile.h - head_h - p }; if (body.h < 8.0f) body.h = 8.0f; /* letterbox the body to the real screen aspect so the mini layout * has exactly the proportions of the monitor */ float wa = (ws->rw > 0 && ws->rh > 0) ? (float)ws->rw / (float)ws->rh : aspect; float sw = body.w, sh = body.w / wa; if (sh > body.h) { sh = body.h; sw = body.h * wa; } ws->screen = (SDL_FRect){ body.x + (body.w - sw) * 0.5f, body.y + (body.h - sh) * 0.5f, sw, sh }; layout_cards(ws); } /* match every slot with where it was a moment ago; things that were not * there yet grow out of their own centre */ bool moved = false; for (int i = 0; i < NSLOTS; ++i) { SLOTS[i].from = rect_shrink(SLOTS[i].tile, 0.55f); bool matched = false; for (int j = 0; j < nold; ++j) { bool same = (SLOTS[i].ws >= 0) ? (old[j].ws == SLOTS[i].ws) : (old[j].ws < 0 && old[j].num == SLOTS[i].num); if (same && old[j].tile.w > 0.0f) { SLOTS[i].from = old[j].tile; matched = true; break; } } if (!settled()) SLOTS[i].from = SLOTS[i].tile; /* no intro */ if (!matched || SDL_fabsf(SLOTS[i].from.x - SLOTS[i].tile.x) > 0.5f || SDL_fabsf(SLOTS[i].from.y - SLOTS[i].tile.y) > 0.5f || SDL_fabsf(SLOTS[i].from.w - SLOTS[i].tile.w) > 0.5f) moved = true; if (SLOTS[i].ws >= 0) WSS[SLOTS[i].ws].tile_from = SLOTS[i].from; } /* ghosts that are no longer part of the grid shrink away instead of * blinking out; they are drawn from this list until they are done */ int dying = 0; for (int j = 0; j < nold && dying < (int)SDL_arraysize(DYING); ++j) { if (old[j].ws >= 0) continue; bool still_there = false; for (int i = 0; i < NSLOTS; ++i) if (SLOTS[i].ws < 0 && SLOTS[i].num == old[j].num) { still_there = true; break; } if (still_there) continue; DYING[dying].ws = -1; DYING[dying].num = old[j].num; DYING[dying].from = old[j].tile; DYING[dying].tile = rect_shrink(old[j].tile, 0.55f); dying++; } if (dying > 0) { /* a later relayout must not eat them */ NDYING = dying; dying_start = SDL_GetTicks(); moved = true; } if (moved && settled()) anim_start = SDL_GetTicks(); laid_out_once = true; build_texts(); /* the clickable name area, between the count and the number */ for (int i = 0; i < NWS; ++i) { Ws *ws = &WSS[i]; float p = C.pad * SC; float head_h = (float)C.ws_px * SC * 1.55f; float left = ws->tile.x + p + (ws->sub.t ? (float)ws->sub.w : 0.0f) + p; float right = ws->tile.x + ws->tile.w - p - (ws->badge.t ? (float)ws->badge.w : 0.0f) - p; float w = SDL_max(right - left, 0.0f); if (C.rename_icon) { /* The middle of the name used to be an invisible button, and * clicking a header to grab the workspace opened the editor by * accident. Now it is a pencil you can see, at the right end of * the name, and the name itself is part of the header like * everywhere else. The name is centred in what is left. */ float icon = SDL_min(head_h * 0.72f, SDL_max(w, 0.0f)); ws->title_box = (SDL_FRect){ left, ws->tile.y, SDL_max(w - icon, 0.0f), head_h }; ws->title_hit = (SDL_FRect){ left + w - icon, ws->tile.y + (head_h - icon) * 0.5f, icon, icon }; } else { ws->title_box = (SDL_FRect){ left, ws->tile.y, w, head_h }; ws->title_hit = (SDL_FRect){ left + w * 0.25f, ws->tile.y, w * 0.5f, head_h }; } } } /* ------------------------------------------------------------- navigation */ static bool win_visible(const Win *w) { return !filtering || w->match; } /* first window of the workspace that matches the query, if any */ static int ws_first_visible_match(const Ws *ws) { for (int i = 0; i < ws->count; ++i) if (WINS[ws->first + i].match) return i; return -1; } static int ws_first_visible(const Ws *ws) { for (int i = 0; i < ws->count; ++i) if (win_visible(&WINS[ws->first + i])) return i; return -1; } /* select a workspace; keep_window=false drops down to workspace level */ static void select_ws(int idx, bool keep_window); static void select_ws(int idx, bool keep_window) { if (NWS == 0) return; sel_ws = (idx % NWS + NWS) % NWS; Ws *ws = &WSS[sel_ws]; if (!keep_window || ws->count == 0) { ws->sel = -1; return; } if (ws->sel >= ws->count || (ws->sel >= 0 && !win_visible(&WINS[ws->first + ws->sel]))) { int v = ws_first_visible(ws); ws->sel = v; } } /* A workspace is worth stepping to while a query is up if something on it * matched, or if its own name did — an empty workspace called "mail" is a * hit for "mail" even though it has no windows to mark. */ static bool ws_is_hit(const Ws *ws) { if (qlen == 0) return true; if (ci_contains(ws->name, query)) return true; return ws_first_visible_match(ws) >= 0; } /* The focus order swbr keeps, most recent first. swov opens and closes in a * moment, so it can never watch focus itself; swbr is running all day and * leaves the list where this can pick it up. */ static int FOCUS[32]; static int NFOCUS; static void focus_load(void) { NFOCUS = 0; const char *rt = getenv("XDG_RUNTIME_DIR"); if (!rt || !*rt) return; char path[512]; snprintf(path, sizeof(path), "%s/swbr-focus", rt); FILE *f = fopen(path, "r"); if (!f) return; char line[64]; while (NFOCUS < (int)SDL_arraysize(FOCUS) && fgets(line, sizeof(line), f)) { int id = atoi(line); if (id > 0) FOCUS[NFOCUS++] = id; } fclose(f); } /* where a window sits in that order, or a big number if it is not in it */ static int focus_rank(int con_id) { for (int i = 0; i < NFOCUS; ++i) if (FOCUS[i] == con_id) return i; return 1000; } /* Tab through windows in the order they were last used, rather than through * workspaces. The first press lands on the one you were in before this one — * which is what the key means everywhere else — and holding shift walks back. * Returns false when there is no order to walk, so tab falls back to * workspaces rather than doing nothing. */ static bool step_recent(int dir) { if (NFOCUS == 0 || NWIN == 0) return false; /* every window we can see, in the order they were last focused */ int order[MAX_WINDOWS], n = 0; for (int i = 0; i < NWIN && n < (int)SDL_arraysize(order); ++i) if (win_visible(&WINS[i]) && !WINS[i].is_self) order[n++] = i; if (n == 0) return false; for (int i = 1; i < n; ++i) /* small n, plain sort */ for (int j = i; j > 0 && focus_rank(WINS[order[j]].con_id) < focus_rank(WINS[order[j-1]].con_id); --j) { int t = order[j]; order[j] = order[j-1]; order[j-1] = t; } int at = -1; Win *cur = NWS > 0 ? ws_sel_win(&WSS[sel_ws]) : NULL; for (int i = 0; i < n; ++i) if (cur ? (&WINS[order[i]] == cur) : WINS[order[i]].focused) { at = i; break; } int next = at < 0 ? 0 : (at + dir + n) % n; if (at < 0 && dir < 0) next = n - 1; Win *w = &WINS[order[next]]; if (w->ws < 0 || w->ws >= NWS) return false; sel_ws = w->ws; sel_active = true; WSS[sel_ws].sel = (int)(w - &WINS[WSS[sel_ws].first]); dirty = true; return true; } static void step_ws(int dir) { for (int i = 1; i <= NWS; ++i) { int cand = (((sel_ws + dir * i) % NWS) + NWS) % NWS; if (ws_is_hit(&WSS[cand])) { select_ws(cand, false); return; } } } /* one row up or down in the grid, wrapping through the list */ static void step_ws_row(int dir) { if (NWS == 0) return; int step = SDL_max(1, GRID_COLS) * dir; int cand = ((sel_ws + step) % NWS + NWS) % NWS; select_ws(cand, false); } static float rect_cx(SDL_FRect r) { return r.x + r.w * 0.5f; } static float rect_cy(SDL_FRect r) { return r.y + r.h * 0.5f; } /* move the selection one step into direction (dx, dy); crosses tiles */ static void navigate(int dx, int dy) { if (NWS == 0) return; Ws *ws = &WSS[sel_ws]; if (ws->sel < 0 && ws->count > 0) { /* step into the workspace */ int v = ws_first_visible(ws); if (v >= 0) { ws->sel = v; return; } } Win *cur = ws->count ? ws_sel_win(ws) : NULL; float fx = cur ? rect_cx(cur->card) : rect_cx(ws->tile); float fy = cur ? rect_cy(cur->card) : rect_cy(ws->tile); int best = -1; float best_score = 1e30f; for (int i = 0; i < ws->count; ++i) { Win *w = &WINS[ws->first + i]; if (w == cur || !win_visible(w)) continue; float cx = rect_cx(w->card) - fx, cy = rect_cy(w->card) - fy; float along = cx * (float)dx + cy * (float)dy; float perp = SDL_fabsf(cx * (float)dy - cy * (float)dx); if (along <= 4.0f * SC) continue; float score = along + perp * 2.0f; if (score < best_score) { best_score = score; best = i; } } if (best >= 0) { ws->sel = best; return; } /* nothing in that direction inside the tile: jump to the next tile */ int best_ws = -1; best_score = 1e30f; for (int i = 0; i < NWS; ++i) { if (i == sel_ws) continue; float cx = rect_cx(WSS[i].tile) - rect_cx(ws->tile); float cy = rect_cy(WSS[i].tile) - rect_cy(ws->tile); float along = cx * (float)dx + cy * (float)dy; float perp = SDL_fabsf(cx * (float)dy - cy * (float)dx); if (along <= 1.0f) continue; float score = along + perp * 3.0f; if (score < best_score) { best_score = score; best_ws = i; } } if (best_ws < 0) { /* nothing further in that direction: wrap around to the far side, * staying as close to the current row or column as possible */ float bs = 1e30f; for (int i = 0; i < NWS; ++i) { if (i == sel_ws) continue; float cx = rect_cx(WSS[i].tile), cy = rect_cy(WSS[i].tile); float along = cx * (float)dx + cy * (float)dy; /* far side */ float perp = SDL_fabsf((cx - rect_cx(ws->tile)) * (float)dy - (cy - rect_cy(ws->tile)) * (float)dx); float score = along + perp * 3.0f; if (score < bs) { bs = score; best_ws = i; } } } if (best_ws >= 0) { select_ws(best_ws, false); Ws *nw = &WSS[sel_ws]; if (nw->count > 0) { /* land on the closest card */ int bi = -1; float bs = 1e30f; for (int i = 0; i < nw->count; ++i) { Win *w = &WINS[nw->first + i]; if (!win_visible(w)) continue; float d = SDL_fabsf(rect_cx(w->card) - fx) + SDL_fabsf(rect_cy(w->card) - fy); if (d < bs) { bs = d; bi = i; } } if (bi >= 0) nw->sel = bi; } } } /* How good a hit is, lowest first. * * What you typed is nearly always the start of a window's title, so that wins * outright; then the title anywhere, then the application, then the workspace * it happens to sit on. Ties go alphabetically, so the same query always * lands on the same window. */ static int match_score(const Win *w) { if (qlen == 0) return 100; if (!strncasecmp(w->title, query, (size_t)qlen)) return 0; if (ci_contains(w->title, query)) return 1; if (!strncasecmp(w->app_id, query, (size_t)qlen)) return 2; if (ci_contains(w->app_id, query)) return 3; if (ci_contains(w->app_key, query)) return 4; if (ci_contains(w->ws_name, query)) return 5; return 100; } static void apply_filter(void) { for (int i = 0; i < NWIN; ++i) { Win *w = &WINS[i]; w->match = (qlen == 0) || ci_contains(w->title, query) || ci_contains(w->app_id, query) || ci_contains(w->app_key, query) || ci_contains(w->ws_name, query); } if (qlen > 0) { /* jump to the best hit, not the first */ int best = -1, best_score = 100; for (int i = 0; i < NWIN; ++i) { Win *w = &WINS[i]; if (!w->match || w->is_self || !win_visible(w)) continue; int sc = match_score(w); if (sc > 5) continue; if (best < 0 || sc < best_score || (sc == best_score && strcasecmp(w->title, WINS[best].title) < 0)) { best = i; best_score = sc; } } if (best >= 0 && WINS[best].ws >= 0 && WINS[best].ws < NWS) { sel_ws = WINS[best].ws; WSS[sel_ws].sel = best - WSS[sel_ws].first; sel_active = true; return; } /* Nothing matched. Leaving the previous selection lit says something * was found and points at the wrong thing; enter would then act on * it. Nothing is selected until the query matches again. */ for (int i = 0; i < NWS; ++i) WSS[i].sel = -1; sel_active = false; } else { select_ws(sel_ws, true); } } static int hit_test(float mx, float my, int *out_ws) { *out_ws = -1; for (int i = 0; i < NWS; ++i) { Ws *ws = &WSS[i]; if (mx < ws->tile.x || mx >= ws->tile.x + ws->tile.w || my < ws->tile.y || my >= ws->tile.y + ws->tile.h) continue; *out_ws = i; for (int j = ws->count - 1; j >= 0; --j) { /* floats are last = on top */ Win *w = &WINS[ws->first + j]; 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; } return -1; } return -1; } static char *escape_arg(const char *s) { size_t n = strlen(s); char *out = (char *)xmalloc(n * 2 + 1); char *o = out; for (const char *p = s; *p; ++p) { if (*p == '"' || *p == '\\') *o++ = '\\'; *o++ = *p; } *o = 0; return out; } /* Bring the history in line with reality. Workspaces also get switched by * sway keybindings, which swov never sees; without this, "go back" bounces * between the last two workspaces swov itself visited and can never return to * one the user reached another way. */ static void usage_sync(void) { if (!C.track) return; char name[64] = {0}, out[64] = {0}; int num = -1; focused_workspace(name, sizeof(name), out, sizeof(out), &num); if (!name[0]) return; usage_load(); if (strcmp(name, USAGE_CUR) == 0) return; /* already up to date */ usage_switch(name, out, num); /* credits the old one */ } /* ------------------------------------------------------------ drag & drop * A press only arms a drag. If the pointer stays put, the release focuses * (that is the "press and release on the same thing" rule); if it moves, we * are dragging either a window or a whole workspace. */ typedef enum { DROP_NONE, DROP_WIN_WS, /* window onto a workspace tile */ DROP_WIN_NEAR, /* window next to another window, splitting h or v */ 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_CANCEL, /* the ✕ beside the monitors: let go and nothing happens */ DROP_OUTPUT /* onto a monitor in the map: send it to that screen */ } DropKind; enum { EDGE_LEFT, EDGE_RIGHT, EDGE_TOP, EDGE_BOTTOM }; static bool press_down; static float press_x, press_y; static int press_win = -1, press_ws = -1; static bool drag_active; static float drag_x, drag_y; static SDL_FRect drag_src_tile; /* where the dragged window's workspace was * when the drag began — a fixed rectangle, * so the reflow cannot move the threshold */ static DropKind drop_kind; static int drop_ws = -1, drop_win = -1, drop_num = -1, drop_edge = EDGE_RIGHT; static bool drop_insert; /* WS_SWAP position means "insert here" */ static void layout(void); /* the workspace sway is showing right now */ static void select_current_workspace(void) { sel_ws = 0; for (int i = 0; i < NWS; ++i) if (WSS[i].focused) { sel_ws = i; return; } for (int i = 0; i < NWS; ++i) if (WSS[i].visible) { sel_ws = i; return; } } static void reload_model(void); static int slot_at(float x, float y) { for (int i = 0; i < NSLOTS; ++i) { SDL_FRect t = SLOTS[i].tile; if (x >= t.x && x < t.x + t.w && y >= t.y && y < t.y + t.h) return i; } return -1; } /* which side of a card the pointer is closest to */ static int edge_at(SDL_FRect r, float x, float y) { float dx = (x - (r.x + r.w * 0.5f)) / SDL_max(1.0f, r.w); float dy = (y - (r.y + r.h * 0.5f)) / SDL_max(1.0f, r.h); if (SDL_fabsf(dx) >= SDL_fabsf(dy)) return dx < 0.0f ? EDGE_LEFT : EDGE_RIGHT; return dy < 0.0f ? EDGE_TOP : EDGE_BOTTOM; } 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); /* The numbers on show. Outside a drag it is whatever ws_slots asks for, so * 1..10 are always there whether or not sway has them yet; during a drag it * reaches one further either side, which is how you get to 0 and to 11. */ static void ghosts_default(void) { if (C.slots_hi > 0) set_ghosts(C.slots_lo, C.slots_hi); else set_ghosts(-1, -1); } static void ghosts_dragging(void) { if (C.slots_hi > 0) { int lo = C.slots_lo - 1; if (lo < 0) lo = 0; set_ghosts(lo, C.slots_hi + 1); } else if (C.drop_ghosts) { set_ghosts(0, 10); } } static void set_ghosts(int lo, int hi) { if (lo == ghost_lo && hi == ghost_hi) return; ghost_lo = lo; ghost_hi = hi; layout(); /* the ghosts take their place, layout() starts the glide */ } /* A workspace being dragged always wants the free numbers on screen. A window * asks for them once it leaves the workspace it lives on — and then keeps * them for the rest of the drag. Toggling them back off would move that very * workspace back under the pointer, which asks for them again: the tiles would * shudder along the border instead of settling. */ static void update_ghosts(float x, float y) { if (ghost_lo >= 0) return; /* already out, leave them */ if (anim_running()) return; /* never decide mid-glide */ bool want = false; if (drag_ws_mode) { want = true; } else if (press_win >= 0) { want = !point_in(drag_src_tile, x, y); } if (want) { if (C.drop_outputs) show_all_outputs(true); ghosts_dragging(); } else { ghosts_default(); } } /* 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; if (on >= 0) { drop_kind = DROP_OUTPUT; drop_out = on; } else if (drop_kind == DROP_OUTPUT) { drop_kind = DROP_NONE; drop_out = -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_ws = drop_win = drop_num = -1; return; /* map_dwell_hover set the kind */ } drop_kind = DROP_NONE; drop_ws = drop_win = drop_num = -1; drop_insert = false; update_ghosts(x, y); int gs = slot_at(x, y); if (gs >= 0 && SLOTS[gs].ws < 0) { /* a ghost: a free number */ drop_kind = drag_ws_mode ? DROP_WS_NUM : DROP_WIN_NEWWS; drop_num = SLOTS[gs].num; return; } if (drag_ws_mode) { int si = gs; if (si < 0) return; if (SLOTS[si].ws == press_ws) return; /* itself */ SDL_FRect t = SLOTS[si].tile; float rel = (x - t.x) / SDL_max(1.0f, t.w); drop_ws = SLOTS[si].ws; drop_num = SLOTS[si].num; if (rel < 0.22f || rel > 0.78f) { /* the edges insert */ drop_kind = DROP_WS_SWAP; drop_insert = true; drop_edge = rel < 0.22f ? EDGE_LEFT : EDGE_RIGHT; } else { drop_kind = DROP_WS_SWAP; /* the middle swaps */ } return; } int ws_idx = -1; int win_idx = hit_test(x, y, &ws_idx); if (ws_idx < 0) return; /* a band along the inside of the tile: beside everything on it */ { SDL_FRect t = WSS[ws_idx].screen; float bx = SDL_clamp(t.w * 0.09f, 6.0f * SC, t.w * 0.18f); float by = SDL_clamp(t.h * 0.09f, 6.0f * SC, t.h * 0.18f); int e = -1; if (x - t.x < bx) e = EDGE_LEFT; else if (t.x + t.w - x < bx) e = EDGE_RIGHT; else if (y - t.y < by) e = EDGE_TOP; else if (t.y + t.h - y < by) e = EDGE_BOTTOM; if (e >= 0 && WSS[ws_idx].count > 0) { drop_kind = DROP_WS_EDGE; drop_ws = ws_idx; drop_edge = e; return; } } if (win_idx >= 0 && win_idx != press_win) { /* A floating window is being moved, not filed away. Passing over * other windows on the way somewhere should not tile it — so it * simply lands on that workspace, still floating, unless you hold * still over one. Hold, and it snaps into the layout beside it: the * card fills up while you wait, and the edge bar appears when it is * ready. A tiled window has no such doubt and snaps at once. */ bool floaty = press_win >= 0 && WINS[press_win].floating && C.snap_ms > 0; if (floaty) { if (SNAP_WIN != win_idx) { SNAP_WIN = win_idx; SNAP_AT = now_secs(); SNAP_X = x; SNAP_Y = y; SNAP_DONE = false; } else if (!SNAP_DONE && SDL_fabsf(x - SNAP_X) + SDL_fabsf(y - SNAP_Y) > 4.0f * SC) { SNAP_AT = now_secs(); /* still moving: not deliberate */ SNAP_X = x; SNAP_Y = y; } if (!SNAP_DONE && (now_secs() - SNAP_AT) * 1000.0 < (double)C.snap_ms) { drop_kind = DROP_WIN_WS; /* move it there, keep it floating */ drop_ws = ws_idx; drop_win = -1; return; } /* Once it has snapped, moving about picks the edge rather than * starting the wait again — the decision has been made. */ SNAP_DONE = true; } else { SNAP_WIN = -1; } drop_kind = DROP_WIN_NEAR; drop_win = win_idx; drop_ws = ws_idx; drop_edge = edge_at(WINS[win_idx].card, x, y); return; } SNAP_WIN = -1; if (win_idx < 0) { drop_kind = DROP_WIN_WS; drop_ws = ws_idx; } } /* Dropping against the edge of a tile means "next to everything here", not * "next to whichever window happens to be under the pointer". Two windows * stacked one above the other are a column, and left of the column is not the * same as left of its top window — which is all sway can be told about a * single view. * * Whether the column has to be broken out of depends on how the workspace is * split. If the windows sit side by side, putting the new one beside the * outermost of them is already right. If they are stacked, the new window has * to be moved out of that stack, which is what a perpendicular `move` does. * * Returns the window to work from, and sets *pop when the stack has to be * escaped. -1 when the workspace is empty and a plain move will do. */ static int ws_edge_ref(const Ws *ws, int edge, bool *pop) { *pop = false; if (ws->count <= 0) return -1; bool horiz = (edge == EDGE_LEFT || edge == EDGE_RIGHT); int best = -1; float best_v = 0.0f; int aligned = 0; for (int i = 0; i < ws->count; ++i) { const Win *w = &WINS[ws->first + i]; if (!win_visible(w)) continue; float v = horiz ? w->card.x : w->card.y; if (edge == EDGE_RIGHT) v = w->card.x + w->card.w; if (edge == EDGE_BOTTOM) v = w->card.y + w->card.h; bool better = best < 0 || ((edge == EDGE_LEFT || edge == EDGE_TOP) ? v < best_v : v > best_v); if (better) { best = ws->first + i; best_v = v; } } if (best < 0) return -1; /* how many share the outermost one's line: more than one means they are * stacked across the direction we are dropping from */ float ref = horiz ? WINS[best].card.x : WINS[best].card.y; float tol = horiz ? WINS[best].card.w * 0.25f : WINS[best].card.h * 0.25f; for (int i = 0; i < ws->count; ++i) { const Win *w = &WINS[ws->first + i]; if (!win_visible(w)) continue; float v = horiz ? w->card.x : w->card.y; if (SDL_fabsf(v - ref) <= tol) aligned++; } *pop = aligned > 1; return best; } /* "7:chat" keeps its label when it becomes workspace 8 */ static char *ws_name_with_num(const Ws *ws, int num) { const char *colon = strchr(ws->name, ':'); if (colon) return fmt_alloc("%d%s", num, colon); return fmt_alloc("%d", num); } static void ws_rename(const Ws *ws, const char *to) { char *from = escape_arg(ws->name); char *dst = escape_arg(to); sway_cmd("rename workspace \"%s\" to \"%s\"", from, dst); free(from); free(dst); } static void act_ws_assign(int ws_idx, int num) { if (ws_idx < 0 || ws_idx >= NWS) return; char *to = ws_name_with_num(&WSS[ws_idx], num); ws_rename(&WSS[ws_idx], to); free(to); } static void act_ws_swap(int a, int b) { if (a < 0 || b < 0 || a == b) return; int na = WSS[a].num, nb = WSS[b].num; if (na < 0 || nb < 0) return; char *tmp = fmt_alloc("swov_tmp_%d", na); ws_rename(&WSS[a], tmp); act_ws_assign(b, na); char *to = ws_name_with_num(&WSS[a], nb); /* a is called tmp now */ char *from = escape_arg(tmp); char *dst = escape_arg(to); sway_cmd("rename workspace \"%s\" to \"%s\"", from, dst); free(from); free(dst); free(to); free(tmp); } /* Insert the dragged workspace at `num`, pushing the occupied run up by one. * With 1, 2, 5 on screen, inserting at 2 moves 2 to 3 and stops there. */ static void act_ws_insert(int ws_idx, int num) { if (ws_idx < 0 || num < 0) return; if (WSS[ws_idx].num == num) return; int run[MAX_WORKSPACES]; int n = 0; for (int want = num; n < MAX_WORKSPACES; ++want) { int found = -1; for (int i = 0; i < NWS; ++i) if (i != ws_idx && WSS[i].num == want) { found = i; break; } if (found < 0) break; run[n++] = found; } for (int i = n - 1; i >= 0; --i) /* top down, no clashes */ act_ws_assign(run[i], WSS[run[i]].num + 1); act_ws_assign(ws_idx, num); } /* Put the dragged window next to `target`, splitting the target the right * way first. A mark is the only reliable way to say "there" to sway. */ /* A floating window dropped next to a tiled one has to stop floating, or * sway puts it where it likes and the split you asked for never happens. * Dropped on the open part of a workspace it stays floating: that is the * difference between "put this in the layout" and "move this over there". */ static void unfloat_for_layout(Win *drag) { if (!drag || !drag->floating) return; sway_cmd("[con_id=%d] floating disable", drag->con_id); drag->floating = false; } static void act_win_drop_near(Win *drag, Win *target, int edge) { unfloat_for_layout(drag); if (!drag || !target || drag == target) return; bool horiz = (edge == EDGE_LEFT || edge == EDGE_RIGHT); sway_cmd("[con_id=%d] mark --add _swov_drop", target->con_id); sway_cmd("[con_id=%d] split %s", target->con_id, horiz ? "h" : "v"); sway_cmd("[con_id=%d] move container to mark _swov_drop", drag->con_id); if (edge == EDGE_LEFT || edge == EDGE_TOP) sway_cmd("[con_id=%d] move %s", drag->con_id, horiz ? "left" : "up"); sway_cmd("unmark _swov_drop"); } static const JV *jfind_workspace(const JV *node, const char *name) { if (!node || node->type != J_OBJ) return NULL; if (!strcmp(jstr(node, "type", ""), "workspace") && !strcmp(jstr(node, "name", ""), name)) return node; for (int i = 0; i < node->count; ++i) { const JV *kid = node->items[i]; if (!kid) continue; if (kid->type == J_ARR) { for (int j = 0; j < kid->count; ++j) { const JV *r = jfind_workspace(kid->items[j], name); if (r) return r; } } else if (kid->type == J_OBJ) { const JV *r = jfind_workspace(kid, name); if (r) return r; } } return NULL; } static bool subtree_has_id(const JV *node, int id) { if (!node || node->type != J_OBJ) return false; if (jint(node, "id", -1) == id) return true; for (int i = 0; i < node->count; ++i) { const JV *kid = node->items[i]; if (!kid) continue; if (kid->type == J_ARR) { for (int j = 0; j < kid->count; ++j) if (subtree_has_id(kid->items[j], id)) return true; } else if (kid->type == J_OBJ) { if (subtree_has_id(kid, id)) return true; } } return false; } /* Is `con_id` the first (or last) direct child of that workspace? * 1 = yes, 0 = on the workspace but not at that end, -1 = not there at all */ static int ws_edge_state(const char *ws_name, int con_id, bool want_first) { JV *tree = sway_query(IPC_GET_TREE); if (!tree) return -1; const JV *ws = jfind_workspace(tree, ws_name); int state = -1; if (ws) { const JV *kids = jget(ws, "nodes"); if (kids && kids->type == J_ARR && kids->count > 0) { const JV *end = kids->items[want_first ? 0 : kids->count - 1]; if (end && jint(end, "id", -1) == con_id) state = 1; else state = subtree_has_id(ws, con_id) ? 0 : -1; } else { state = subtree_has_id(ws, con_id) ? 0 : -1; } } jfree(tree); return state; } /* Move it to one end of the workspace and check, rather than guessing how * many moves it takes. One `move left` out of a stack lifts the window clear * of the whole column; one inside a row only swaps it with a neighbour. Both * end at the same place if you keep going until sway says it is the first * child, and stopping on that condition means never one move too many — * which would push it onto the next monitor. */ static void move_to_ws_edge(int con_id, const char *ws_name, int edge) { static const char *DIR[] = { "left", "right", "up", "down" }; bool first = (edge == EDGE_LEFT || edge == EDGE_TOP); for (int i = 0; i < 10; ++i) { int st = ws_edge_state(ws_name, con_id, first); if (st == 1) return; /* where it was asked for */ if (st < 0) return; /* not there: leave it be */ sway_cmd("[con_id=%d] move %s", con_id, DIR[edge]); } } static void move_to_ws_edge_named(int con_id, const char *ws, const char *edge) { int e = !strcmp(edge, "left") ? EDGE_LEFT : !strcmp(edge, "right") ? EDGE_RIGHT : !strcmp(edge, "top") ? EDGE_TOP : EDGE_BOTTOM; move_to_ws_edge(con_id, ws, e); } /* Beside everything on a workspace. Side by side already means the outermost * window is the whole edge, so that is the ordinary case. Stacked means the * new window has to leave the stack: a move across the stack's direction * lifts it out and puts it beside the whole thing, which is the one shape * sway can be asked for directly. */ static void act_win_drop_edge(Win *drag, const Ws *ws, int edge) { unfloat_for_layout(drag); bool pop = false; int ref = ws_edge_ref(ws, edge, &pop); if (ref >= 0 && !pop) { act_win_drop_near(drag, &WINS[ref], edge); return; } if (drag->ws != (int)(ws - WSS)) { if (ws->num >= 0) sway_cmd("[con_id=%d] move container to workspace number %d", drag->con_id, ws->num); else { char *e = escape_arg(ws->name); sway_cmd("[con_id=%d] move container to workspace \"%s\"", drag->con_id, e); free(e); } } if (ref < 0) return; /* nothing there to be beside */ move_to_ws_edge(drag->con_id, ws->name, edge); } static void drag_finish(void) { switch (drop_kind) { case DROP_WIN_WS: if (press_win >= 0 && drop_ws >= 0 && WINS[press_win].ws != drop_ws) { Ws *ws = &WSS[drop_ws]; if (ws->num >= 0) sway_cmd("[con_id=%d] move container to workspace number %d", WINS[press_win].con_id, ws->num); else { char *e = escape_arg(ws->name); sway_cmd("[con_id=%d] move container to workspace \"%s\"", WINS[press_win].con_id, e); free(e); } } break; case DROP_WIN_NEAR: if (press_win >= 0 && drop_win >= 0) act_win_drop_near(&WINS[press_win], &WINS[drop_win], drop_edge); break; case DROP_WS_EDGE: if (press_win >= 0 && drop_ws >= 0) act_win_drop_edge(&WINS[press_win], &WSS[drop_ws], drop_edge); break; case DROP_WS_SWAP: if (drop_insert) { int num = drop_num + (drop_edge == EDGE_RIGHT ? 1 : 0); act_ws_insert(press_ws, num); } else { act_ws_swap(press_ws, drop_ws); } break; case DROP_WS_NUM: act_ws_assign(press_ws, drop_num); break; case DROP_WIN_NEWWS: /* sway creates the workspace */ if (press_win >= 0 && drop_num >= 0) sway_cmd("[con_id=%d] move container to workspace number %d", WINS[press_win].con_id, drop_num); break; case DROP_OUTPUT: /* Onto a screen in the map: send it there. A workspace moves whole, * a window goes to whatever workspace is showing on that screen. */ if (drop_out >= 0 && drop_out < NOUTS) { char *o = escape_arg(OUTS[drop_out].name); int moved = 0; if (drag_ws_mode) /* every marked one, or the one */ for (int i = 0; i < NWS; ++i) if (WSS[i].marked && WSS[i].num >= 0) { sway_cmd("workspace number %d; move workspace to output \"%s\"", WSS[i].num, o); moved++; } if (!moved && drag_ws_mode && press_ws >= 0 && WSS[press_ws].num >= 0) sway_cmd("workspace number %d; move workspace to output \"%s\"", WSS[press_ws].num, o); else if (press_win >= 0) sway_cmd("[con_id=%d] move container to output \"%s\"", WINS[press_win].con_id, o); free(o); } break; case DROP_CANCEL: /* put down where it started */ case DROP_NONE: default: break; } show_all_outputs(false); SNAP_WIN = -1; SNAP_DONE = false; DRAG_CON = 0; drag_active = drag_ws_mode = false; drop_kind = DROP_NONE; press_win = press_ws = -1; ghost_lo = ghost_hi = -1; ghosts_default(); layout(); reload_model(); /* the event socket refreshes again once sway settles */ } static void drag_cancel(void) { bool had_ghosts = ghost_lo >= 0; drag_active = drag_ws_mode = false; drop_kind = DROP_NONE; press_down = false; press_win = press_ws = -1; ghost_lo = ghost_hi = -1; ghosts_default(); if (had_ghosts) layout(); } /* ---------------------------------------------------------------- chrome */ static Tex T_HEADER, T_HINTS, T_QUERY; static double QUERY_FLASH; /* enter on a query that found nothing */ static void rebuild_chrome(void) { tex_free(&T_HEADER); tex_free(&T_HINTS); tex_free(&T_QUERY); if (C.show_header) { char buf[256]; /* Looking at a screen you are not on is worth saying outright: every * key and every drop from here lands over there. */ snprintf(buf, sizeof(buf), "%s%.63s %d window%s on %d workspace%s", PINNED_OUTPUT ? "viewing " : "", FOCUSED_OUTPUT[0] ? FOCUSED_OUTPUT : "sway", NWIN, NWIN == 1 ? "" : "s", NWS, NWS == 1 ? "" : "s"); T_HEADER = text_make(F_HINT, buf); } if (C.show_hints) { /* when both lines share a band the hints get whatever width is left */ int avail = RW - (int)(C.margin * SC * 2.0f); bool same_band = C.show_header && !pos_is_none(C.header_pos) && !pos_is_none(C.hints_pos) && pos_is_top(C.header_pos) == pos_is_top(C.hints_pos); if (same_band && T_HEADER.t) avail -= T_HEADER.w + (int)(C.margin * SC); T_HINTS = text_make_fit(F_HINT, "\xe2\x86\xb5 focus drag to move tab workspace 0-9 go to " "ctrl+0-9 move space mark x close f find / filter " "esc quit", avail); } if (confirm_kill) { int n = 0; for (int i = 0; i < NWIN; ++i) if (WINS[i].marked) n++; if (n == 0 && NWS > 0) { Win *w = ws_sel_win(&WSS[sel_ws]); n = w ? 1 : WSS[sel_ws].count; } char buf[192]; snprintf(buf, sizeof(buf), "close %d window%s? enter to confirm", n, n == 1 ? "" : "s"); T_QUERY = text_make(F_HINT, buf); } else if (query_active()) { char buf[192]; snprintf(buf, sizeof(buf), "%s %.127s\xe2\x96\x8f", filtering ? "filter:" : "search:", query); T_QUERY = text_make(F_HINT, buf); } } /* --------------------------------------------------------------- drawing */ /* While the grid is settling, a workspace is drawn where it currently is on * its way, and everything inside it rides along on the same transform. */ static float XF_SX = 1.0f, XF_SY = 1.0f, XF_X0, XF_Y0, XF_DX, XF_DY; static bool XF_ON; static void xf_set(SDL_FRect from, SDL_FRect to) { XF_ON = true; XF_SX = (from.w > 0.0f) ? to.w / from.w : 1.0f; XF_SY = (from.h > 0.0f) ? to.h / from.h : 1.0f; XF_X0 = from.x; XF_Y0 = from.y; XF_DX = to.x; XF_DY = to.y; } static void xf_off(void) { XF_ON = false; XF_SX = XF_SY = 1.0f; } static SDL_FRect xf(SDL_FRect r) { if (!XF_ON) return r; return (SDL_FRect){ XF_DX + (r.x - XF_X0) * XF_SX, XF_DY + (r.y - XF_Y0) * XF_SY, r.w * XF_SX, r.h * XF_SY }; } /* how solid an icon is drawn: a workspace on another screen is faded */ static float ICON_FADE = 1.0f; static void draw_icon(SDL_Texture *icon, float cx, float top, float size) { if (!icon) return; float w = size, h = size; float tw = 0.0f, th = 0.0f; if (SDL_GetTextureSize(icon, &tw, &th) && tw > 0.0f && th > 0.0f) { if (tw > th) h = size * (th / tw); else if (th > tw) w = size * (tw / th); } SDL_FRect dst = { cx - w * 0.5f, top + (size - h) * 0.5f, w, h }; SDL_SetTextureAlphaModFloat(icon, ICON_FADE); SDL_RenderTexture(REN, icon, NULL, &dst); SDL_SetTextureAlphaModFloat(icon, 1.0f); } static void draw_card(Win *w, bool tile_selected) { SDL_FRect r = xf(w->card); if (r.w < 4.0f || r.h < 4.0f) return; bool selected = tile_selected && sel_active && ws_sel_win(&WSS[w->ws]) == w; bool is_hovered = selected; /* pointer and keyboard share a cursor */ bool dimmed = filtering && !w->match; bool is_hit = searching && qlen > 0 && w->match; float rad = SDL_min(C.radius * SC * 0.62f, SDL_min(r.w, r.h) * 0.32f); SDL_FColor fill = w->focused ? C.card_focus : C.card; if (is_hovered) fill = C.card_hover; if (is_hit) fill = mix(fill, C.match, 0.22f); if (confirm_kill && (w->marked || selected)) fill = mix(fill, C.urgent, 0.25f); 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; /* A workspace that lives on another screen is shown in full, windows and * all — you want to see what is over there — but washed out, so a glance * is enough to know it is not here. Blur would be truer to the idea; * this renderer has no shader to do it with, and fading everything * uniformly reads the same way at a glance. */ float away = 1.0f; if (w->ws >= 0 && w->ws < NWS && WSS[w->ws].output[0] && FOCUSED_OUTPUT[0] && strcmp(WSS[w->ws].output, FOCUSED_OUTPUT) != 0) away = 0.45f; ICON_FADE = away; float over = away, cover = 0.0f; bool on_top = w->floating || w->fullscreen; 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; cover = SDL_clamp(cover, 0.0f, 1.0f); over = C.float_alpha * (1.0f - 0.55f * cover) * away; fill = with_alpha(fill, fill.a * over); } /* 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 && !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; /* The one on top shows its contents below, so its own tab was never * drawn: with two windows you saw a single tab and could not tell a pair * from one window with a title. It gets a tab like the others, filled * rather than outlined because it is the one you are looking at. */ if (is_panel) { SDL_FRect tb = xf(w->tab); float trad = SDL_min(rad, SDL_min(tb.w, tb.h) * 0.4f); fill_round_side(tb, trad, true, false, w->focused ? C.card_focus : mix(C.card, C.bg, 0.10f)); stroke_round_rect(tb, trad, SDL_max(1.0f, C.border * SC * 0.5f), with_alpha(C.accent, 0.45f)); if (w->label.t) { float lx = tb.x + (tb.w - (float)w->label.w) * 0.5f; float ly = tb.y + (tb.h - (float)w->label.h) * 0.5f; if ((float)w->label.w < tb.w - 6.0f * SC) tex_draw(w->label, lx, ly, C.text); } } if (w->floating) drop_shadow(r, rad, 9.0f * SC, with_alpha(C.shadow_col, C.shadow_col.a * over)); if (away < 1.0f && !on_top) fill = with_alpha(fill, fill.a * away); 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: what is being dragged beats everything, then selection, hover, mark, plain */ float bw = SDL_max(1.0f, C.border * SC * 0.75f); /* the hold that turns a move into a snap, filling from the bottom */ if (drag_active && !SNAP_DONE && SNAP_WIN >= 0 && SNAP_WIN < NWIN && &WINS[SNAP_WIN] == w && C.snap_ms > 0) { float k = (float)((now_secs() - SNAP_AT) * 1000.0 / (double)C.snap_ms); k = SDL_clamp(k, 0.0f, 1.0f); if (k < 1.0f) { SDL_FRect fillr = { r.x, r.y + r.h * (1.0f - k), r.w, r.h * k }; fill_round_rect(fillr, rad * 0.5f, with_alpha(C.accent, 0.22f)); stroke_round_rect(r, rad, SDL_max(1.0f, 2.0f * SC), with_alpha(C.accent, 0.35f + 0.5f * k)); } } 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); else if (is_hovered) stroke_round_rect(r, rad, bw, C.accent); else if (w->marked) stroke_round_rect(r, rad, bw, with_alpha(C.hl, 0.85f)); else if (w->urgent) stroke_round_rect(r, rad, bw, C.urgent); else if (on_top) stroke_round_rect(r, rad, SDL_max(1.0f, bw * 0.7f), with_alpha(C.accent, dimmed ? 0.25f : 0.5f)); else stroke_round_rect(r, rad, SDL_max(1.0f, bw * 0.4f), with_alpha(C.outline, dimmed ? 0.3f : 0.7f)); /* the window sway has focused: marked as current, not as selected */ if (w->focused && !dimmed) { SDL_FRect bar = { r.x + bw * 0.6f, r.y + rad * 0.7f, SDL_max(2.0f, 3.0f * SC), r.h - rad * 1.4f }; fill_round_rect(bar, bar.w * 0.5f, C.current); } /* multi selection marker */ if (w->marked) { float d = SDL_max(6.0f, 9.0f * SC); SDL_FRect dot = { r.x + r.w - d - 5.0f * SC, r.y + 5.0f * SC, d, d }; fill_round_rect(dot, d * 0.5f, C.hl); } float pad = card_pad(w); float a = dimmed ? 0.45f : 1.0f; SDL_FColor lab_col = (selected || is_hovered) ? C.text : C.subtext; if (w->urgent) lab_col = C.urgent; SDL_FColor sub_col = mix(C.dim, C.text, (selected || is_hovered) ? 0.55f : 0.32f); lab_col = with_alpha(lab_col, lab_col.a * a * away); sub_col = with_alpha(sub_col, sub_col.a * a * away); bool has_icon = C.icons && w->icon && w->lay_icon >= 10.0f * SC; if (has_icon) SDL_SetTextureAlphaModFloat(w->icon, a); SDL_FRect plate; float plate_iw = 0.0f; if (as_overlay && overlay_plate(w, r, &plate, &plate_iw)) { /* Centre would land on top of the cards showing through, so the name * rides in a small plate at the top edge instead. That plate is also * the only part of this window the mouse can grab. */ float ip = 6.0f * SC; fill_round_rect(plate, plate.h * 0.32f, with_alpha(mix(C.card, C.bg, 0.25f), 0.94f)); stroke_round_rect(plate, plate.h * 0.32f, SDL_max(1.0f, 1.4f * SC), is_hovered ? C.accent : with_alpha(C.accent, 0.55f)); float tx = plate.x + ip; if (plate_iw > 0.0f && has_icon) { float ih = plate_iw - 5.0f * SC; draw_icon(w->icon, tx + ih * 0.5f, plate.y + (plate.h - ih) * 0.5f, ih); tx += plate_iw; } tex_draw(w->label, tx, plate.y + (plate.h - (float)w->label.h) * 0.5f, with_alpha(C.text, a)); if (has_icon) SDL_SetTextureAlphaModFloat(w->icon, 1.0f); return; } float lab_h = w->label.t ? (float)w->label.h : 0.0f; float sub_h = w->subtitle.t ? (float)w->subtitle.h : 0.0f; float g1 = 4.0f * SC, g2 = 1.0f * SC; if (w->pidtex.t && r.w > (float)w->pidtex.w + 12.0f * SC && r.h > (float)w->pidtex.h + 12.0f * SC) { tex_draw(w->pidtex, r.x + r.w - (float)w->pidtex.w - pad * 0.6f, r.y + pad * 0.4f, /* a step quieter than a card's subtitle, which sits at 0.32 of the way from dim to text when it is not selected */ with_alpha(mix(C.dim, C.text, 0.22f), 0.9f * away)); } switch (w->lay_mode) { case CL_ROW: { float tx = r.x + pad; if (has_icon) { draw_icon(w->icon, tx + w->lay_icon * 0.5f, r.y + (r.h - w->lay_icon) * 0.5f, w->lay_icon); tx += w->lay_icon + 6.0f * SC; } float block = lab_h + (sub_h > 0.0f ? g2 + sub_h : 0.0f); float y = r.y + (r.h - block) * 0.5f; tex_draw(w->label, tx, y, lab_col); if (sub_h > 0.0f) tex_draw(w->subtitle, tx, y + lab_h + g2, sub_col); break; } case CL_TEXT: { float y = r.y + (r.h - lab_h) * 0.5f; tex_draw_center(w->label, r.x + r.w * 0.5f, y, lab_col); break; } case CL_ICON: { if (has_icon) draw_icon(w->icon, r.x + r.w * 0.5f, r.y + (r.h - w->lay_icon) * 0.5f, w->lay_icon); break; } default: { /* CL_STACK */ float icon = has_icon ? w->lay_icon : 0.0f; float block = icon + (lab_h > 0.0f ? g1 + lab_h : 0.0f) + (sub_h > 0.0f ? g2 + sub_h : 0.0f); float cx = r.x + r.w * 0.5f; /* a window drawn on top keeps to the upper edge, where it is not * sitting on the label of whatever shows through beneath it */ float y = as_overlay ? r.y + pad : r.y + (r.h - block) * 0.5f; if (has_icon) { draw_icon(w->icon, cx, y, icon); y += icon; } if (lab_h > 0.0f) { y += g1; tex_draw_center(w->label, cx, y, lab_col); y += lab_h; } if (sub_h > 0.0f) { y += g2; tex_draw_center(w->subtitle, cx, y, sub_col); } break; } } 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]; bool selected = sel_active && (idx == sel_ws); bool hovered = false; /* the selection is the only cursor */ float rad = C.radius * SC; float t = anim_phase(); if (t < 1.0f && ws->tile_from.w > 0.0f) xf_set(ws->tile, rect_lerp(ws->tile_from, ws->tile, t)); else xf_off(); SDL_FRect tile = xf(ws->tile); 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); SDL_FColor bc = C.outline; float bw = C.border * SC; if (selected) bc = C.hl; else if (hovered) bc = with_alpha(C.accent, 0.9f); else if (ws->urgent) bc = C.urgent; else if (searching && qlen > 0 && ws_first_visible_match(ws) >= 0) 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); } if (ws->marked) { /* picked out to be moved together */ bc = C.hl; bw = SDL_max(2.0f, C.border * SC * 1.8f); } stroke_round_rect(tile, rad, bw, bc); /* ---- tile header: count left, name centred, number right ---- */ float p = C.pad * SC; float top = tile.y + C.badge_top * SC; bool live = ws->visible || ws->focused; SDL_FColor num_col = selected ? C.hl : live ? C.current : mix(C.subtext, C.tile, 0.25f); if (ws->urgent && !selected) num_col = C.urgent; tex_draw(ws->badge, tile.x + tile.w - p - (float)ws->badge.w, top, num_col); float count_right = tile.x + p; if (ws->sub.t) { float base = top + ((float)ws->badge.h - (float)ws->sub.h) * 0.62f; SDL_FColor col = ws->count ? mix(C.dim, C.text, selected ? 0.45f : 0.25f) : with_alpha(C.dim, 0.85f); tex_draw(ws->sub, tile.x + p, base, col); count_right += (float)ws->sub.w; } /* the name sits between the two; clicking it starts a rename */ (void)count_right; SDL_FRect tb = xf(ws->title_box); float tb_x = tb.x, tb_w = tb.w; bool editing_this = editing && edit_ws == idx; if (editing_this) { SDL_FRect box = { tb_x, top - p * 0.35f, SDL_max(tb_w, 40.0f * SC), (float)ws->badge.h + p * 0.7f }; fill_round_rect(box, box.h * 0.3f, mix(C.tile, C.bg, 0.2f)); stroke_round_rect(box, box.h * 0.3f, SDL_max(1.0f, 1.5f * SC), C.hl); char buf[80]; snprintf(buf, sizeof(buf), "%.63s\xe2\x96\x8f", edit_buf); 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; if ((float)t.w > tb_w) x = tb_x; SDL_FColor col = selected ? C.text : mix(C.subtext, C.tile, 0.15f); tex_draw(t, x, top + ((float)ws->badge.h - (float)t.h) * 0.55f, col); } /* The rename pencil: faint until the pointer is on it, so it says "this * can be named" without competing with the name. */ if (C.rename_icon && !editing_this && ws->title_hit.w > 6.0f * SC) { SDL_FRect ir = xf(ws->title_hit); bool hot = hov_ws == idx && pencil_hot; draw_pencil(ir, hot ? C.hl : with_alpha(C.subtext, selected ? 0.55f : 0.32f)); } /* How much this workspace gets used, as a column of dots down the left * edge: the more of them are lit, the more time is spent here. The scale * is relative to the busiest workspace. */ if (C.usage_dots && C.dot_count > 0) { float d = C.dot_px * SC; float gap = d * 0.62f; SDL_FRect screen_now = xf(ws->screen); int fits = (int)((screen_now.h + gap) / (d + gap)); int n = SDL_clamp(SDL_min(C.dot_count, fits), 0, 40); if (n < 2) n = 0; float total = (float)n * d + (float)(n - 1) * gap; float cx = tile.x + C.pad * SC + d * 0.55f; float y0 = screen_now.y + (screen_now.h - total) * 0.5f; float frac = (USAGE_MAX > 0.0) ? (float)(ws->usage / USAGE_MAX) : 0.0f; int lit = (ws->usage > 0.0) ? (int)SDL_ceilf(frac * (float)n) : 0; lit = SDL_clamp(lit, 0, n); for (int i = 0; i < n; ++i) { /* fills from the bottom like a level gauge */ bool on = (n - i) <= lit; SDL_FRect dot = { cx - d * 0.5f, y0 + (float)i * (d + gap), d, d }; fill_round_rect(dot, d * 0.5f, on ? C.current : with_alpha(C.dim, 0.38f)); } /* the same scale down the other edge, for what the workspace is doing * now rather than how long you have spent there — swbr measures it, * and draws the identical dots on its own workspace buttons */ float cpu = cpu_frac(ws->name); if (cpu >= 0.0f) { float cxr = tile.x + tile.w - C.pad * SC - d * 0.55f; int cl = SDL_clamp((int)SDL_ceilf(cpu * (float)n), 1, n); SDL_FColor hot = cpu < 0.85f ? C.accent : mix(C.accent, C.urgent, (cpu - 0.85f) / 0.15f); /* below the scale but not asleep: one dot, faint */ if (cpu <= 0.0f) hot = with_alpha(mix(C.dim, C.accent, 0.4f), 0.55f); for (int i = 0; i < n; ++i) { bool on = (n - i) <= cl; SDL_FRect dot = { cxr - d * 0.5f, y0 + (float)i * (d + gap), d, d }; fill_round_rect(dot, d * 0.5f, on ? hot : with_alpha(C.dim, 0.30f)); } } } /* ---- the mini screen ---- */ SDL_FRect screen = xf(ws->screen); float srad = SDL_min(rad * 0.7f, SDL_min(screen.w, screen.h) * 0.5f); fill_round_rect(screen, srad, C.mini_bg); stroke_round_rect(screen, srad, SDL_max(1.0f, 1.2f * SC), with_alpha(C.outline, 0.55f)); if (ws->count == 0) { /* nothing here: keep the tile obviously "a screen, but empty" */ xf_off(); return; } /* a container whose children share one rectangle (tabbed or stacked) is * drawn as slices; a shared outline shows that they belong together */ for (int i = 0; i < ws->count; ++i) { if (WINS[ws->first + i].group != i) continue; SDL_FRect u = WINS[ws->first + i].card; for (int j = i + 1; j < ws->count; ++j) { if (WINS[ws->first + j].group != i) continue; SDL_FRect c = WINS[ws->first + j].card; float x1 = SDL_max(u.x + u.w, c.x + c.w), y1 = SDL_max(u.y + u.h, c.y + c.h); u.x = SDL_min(u.x, c.x); u.y = SDL_min(u.y, c.y); u.w = x1 - u.x; u.h = y1 - u.y; } u = xf(u); float e = 3.0f * SC; SDL_FRect box = { u.x - e, u.y - e, u.w + 2.0f * e, u.h + 2.0f * e }; stroke_round_rect(box, C.radius * SC * 0.7f, SDL_max(1.0f, 1.6f * SC), with_alpha(C.accent, 0.5f)); if (WINS[ws->first + i].has_tab) { /* The strip the tabs sit on, exactly as wide as the tabs. It used * to span the group's whole box, which left a dark band sticking * out either side of the tabs once they were inset — the shadow * that looked like the background bleeding over the edges. */ float x0 = 1e9f, x1 = -1e9f, ty = 0.0f, th = 0.0f; for (int j = 0; j < ws->count; ++j) { Win *tw = &WINS[ws->first + j]; if (tw->group != i || !tw->has_tab) continue; SDL_FRect t = xf(tw->tab); if (t.x < x0) x0 = t.x; if (t.x + t.w > x1) x1 = t.x + t.w; ty = t.y; th = t.h; } if (x1 > x0) { SDL_FRect strip = { x0 - e * 0.5f, ty - e * 0.5f, x1 - x0 + e, th + e }; fill_round_rect(strip, C.radius * SC * 0.5f, with_alpha(C.mini_bg, 0.8f)); } } } for (int i = 0; i < ws->count; ++i) { int gi = ws->first + i; draw_card(&WINS[gi], selected); } xf_off(); } /* ---- what the drop would do, shown while the button is still held ---- */ static void draw_drop_indicator(void) { float thick = SDL_max(2.0f, C.border * SC * 1.2f); switch (drop_kind) { case DROP_WIN_WS: if (drop_ws >= 0) { SDL_FRect r = WSS[drop_ws].screen; fill_round_rect(r, C.radius * SC * 0.7f, with_alpha(C.hl, 0.13f)); stroke_round_rect(r, C.radius * SC * 0.7f, thick, C.hl); } break; case DROP_WIN_NEAR: if (drop_win >= 0) { SDL_FRect r = WINS[drop_win].card; stroke_round_rect(r, C.radius * SC * 0.6f, SDL_max(1.0f, thick * 0.6f), with_alpha(C.accent, 0.8f)); /* the bar shows which side the window lands on, and therefore * whether the container ends up split horizontally or vertically */ float b = thick * 1.6f; SDL_FRect bar = r; if (drop_edge == EDGE_LEFT) bar = (SDL_FRect){ r.x - b * 0.5f, r.y, b, r.h }; else if (drop_edge == EDGE_RIGHT) bar = (SDL_FRect){ r.x + r.w - b * 0.5f, r.y, b, r.h }; else if (drop_edge == EDGE_TOP) bar = (SDL_FRect){ r.x, r.y - b * 0.5f, r.w, b }; else bar = (SDL_FRect){ r.x, r.y + r.h - b * 0.5f, r.w, b }; fill_round_rect(bar, b * 0.5f, C.hl); } break; case DROP_WS_EDGE: if (drop_ws >= 0) { SDL_FRect r = WSS[drop_ws].screen; stroke_round_rect(r, C.radius * SC * 0.7f, SDL_max(1.0f, thick * 0.6f), with_alpha(C.accent, 0.8f)); float b = thick * 1.8f; SDL_FRect bar; if (drop_edge == EDGE_LEFT) bar = (SDL_FRect){ r.x, r.y, b, r.h }; else if (drop_edge == EDGE_RIGHT) bar = (SDL_FRect){ r.x + r.w - b, r.y, b, r.h }; else if (drop_edge == EDGE_TOP) bar = (SDL_FRect){ r.x, r.y, r.w, b }; else bar = (SDL_FRect){ r.x, r.y + r.h - b, r.w, b }; fill_round_rect(bar, b * 0.5f, C.hl); } break; case DROP_WS_SWAP: if (drop_ws >= 0) { SDL_FRect t = WSS[drop_ws].tile; if (drop_insert) { float b = thick * 1.8f; float x = (drop_edge == EDGE_LEFT) ? t.x - b : t.x + t.w; SDL_FRect bar = { x, t.y, b, t.h }; fill_round_rect(bar, b * 0.5f, C.hl); } else { fill_round_rect(t, C.radius * SC, with_alpha(C.hl, 0.16f)); stroke_round_rect(t, C.radius * SC, thick, C.hl); } } break; default: break; } /* the thing being dragged, held under the pointer */ if (drag_ws_mode && press_ws >= 0) { SDL_FRect t = WSS[press_ws].tile; float w = t.w * 0.34f, h = t.h * 0.34f; SDL_FRect g = { drag_x - w * 0.5f, drag_y - h * 0.5f, w, h }; fill_round_rect(g, C.radius * SC * 0.7f, with_alpha(C.tile_sel, 0.92f)); stroke_round_rect(g, C.radius * SC * 0.7f, thick, C.hl); tex_draw_in_box(WSS[press_ws].badge, g, F_BADGE, C.text); } else if (press_win >= 0) { Win *w = &WINS[press_win]; SDL_FRect r = w->card; SDL_FRect g = { drag_x - r.w * 0.5f, drag_y - r.h * 0.5f, r.w, r.h }; fill_round_rect(g, C.radius * SC * 0.6f, with_alpha(C.card_hover, 0.9f)); stroke_round_rect(g, C.radius * SC * 0.6f, thick, C.hl); if (w->icon && C.icons) { float sz = SDL_min(SDL_min(g.w, g.h) * 0.5f, (float)C.icon_px * SC); draw_icon(w->icon, g.x + g.w * 0.5f, g.y + (g.h - sz) * 0.5f, sz); } } } /* The monitors as they sit on the desk, scaled into whatever corner of the * grid was left over. Click one to look at its workspaces instead; the one * you are looking at is filled, the one sway is really on keeps a ring. */ static void draw_outputs_map(void) { if (MAP_RECT.w < 16.0f || NOUTS < 2) return; int minx = OUTS[0].x, miny = OUTS[0].y; int maxx = OUTS[0].x + OUTS[0].w, maxy = OUTS[0].y + OUTS[0].h; for (int i = 1; i < NOUTS; ++i) { if (OUTS[i].x < minx) minx = OUTS[i].x; if (OUTS[i].y < miny) miny = OUTS[i].y; if (OUTS[i].x + OUTS[i].w > maxx) maxx = OUTS[i].x + OUTS[i].w; if (OUTS[i].y + OUTS[i].h > maxy) maxy = OUTS[i].y + OUTS[i].h; } float dw = (float)(maxx - minx), dh = (float)(maxy - miny); if (dw < 1.0f || dh < 1.0f) return; /* fit the whole desk inside its box, aspect kept */ float pad = C.pad * SC * 0.5f; float aw = MAP_RECT.w - 2.0f * pad, ah = MAP_RECT.h - 2.0f * pad; float sc = SDL_min(aw / dw, ah / dh); float bw = dw * sc, bh = dh * sc; float bx = MAP_RECT.x + (MAP_RECT.w - bw) * 0.5f; float by = MAP_RECT.y + (MAP_RECT.h - bh) * 0.5f; float rad = SDL_max(2.0f, 4.0f * SC); for (int i = 0; i < NOUTS; ++i) { Out *d = &OUTS[i]; SDL_FRect r = { bx + (float)(d->x - minx) * sc + 1.0f * SC, by + (float)(d->y - miny) * sc + 1.0f * SC, (float)d->w * sc - 2.0f * SC, (float)d->h * sc - 2.0f * SC }; d->box = r; if (r.w < 4.0f || r.h < 4.0f) continue; bool showing = strcmp(d->name, FOCUSED_OUTPUT) == 0; /* 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 && !drag_active) 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; } /* One border, in the same colour its workspaces wear while dragging. Two rings and two fills competing for the same plate was what made this look muddled: which screen is being shown is said by how strongly it is filled, and which one sway is on by a dot. */ SDL_FColor oc = output_colour(d->name); fill_round_rect(r, rad, with_alpha(oc, showing ? 0.55f : 0.18f)); stroke_round_rect(r, rad, SDL_max(1.0f, (showing ? 2.0f : 1.2f) * SC), with_alpha(oc, showing ? 1.0f : 0.55f)); 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 (OUT_PRESS == i) { /* the one on the pointer */ stroke_round_rect(r, rad, SDL_max(2.0f, 2.5f * SC), with_alpha(C.hl, 0.9f)); } if (OUT_TARGET == i) { /* and where it will go */ float b2 = SDL_max(3.0f, 4.0f * SC); SDL_FRect bar; if (OUT_EDGE == EDGE_LEFT) bar = (SDL_FRect){ r.x - b2 * 0.5f, r.y, b2, r.h }; else if (OUT_EDGE == EDGE_RIGHT) bar = (SDL_FRect){ r.x + r.w - b2 * 0.5f, r.y, b2, r.h }; else if (OUT_EDGE == EDGE_TOP) bar = (SDL_FRect){ r.x, r.y - b2 * 0.5f, r.w, b2 }; else bar = (SDL_FRect){ r.x, r.y + r.h - b2 * 0.5f, r.w, b2 }; fill_round_rect(bar, b2 * 0.5f, C.hl); } if (drag_active && drop_kind == DROP_OUTPUT && drop_out == i) { fill_round_rect(r, rad, with_alpha(oc, 0.75f)); stroke_round_rect(r, rad, SDL_max(2.0f, 3.0f * SC), C.hl); } if (d->focused) { /* where sway actually is */ float dot = SDL_min(6.0f * SC, SDL_min(r.w, r.h) * 0.22f); SDL_FRect p = { r.x + dot * 0.7f, r.y + dot * 0.7f, dot, dot }; fill_round_rect(p, dot * 0.5f, C.current); } Tex t = text_make_fit(F_HINT, d->name, (int)(r.w - 4.0f * SC)); 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.bg : with_alpha(C.text, 0.9f)); 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); } } /* A pencil at 45 degrees, point down-left: a body, a band where the wood * starts, and the tip. Drawn as geometry so it stays crisp at any scale and * does not depend on the font having a glyph for it. */ static void draw_pencil(SDL_FRect r, SDL_FColor c) { float s = SDL_min(r.w, r.h) * 0.70f; float cx = r.x + r.w * 0.5f, cy = r.y + r.h * 0.5f; const float k = 0.70710678f; float dx = -k, dy = k, nx = k, ny = k; /* along it, and across it */ float L = s * 0.5f, W = s * 0.15f, cone = s * 0.30f; float tx = cx + dx * L, ty = cy + dy * L; /* tip */ float bx = cx + dx * (L - cone), by = cy + dy * (L - cone); /* wood */ float ex = cx - dx * L, ey = cy - dy * L; /* end */ SDL_FColor wood = { c.r, c.g, c.b, c.a * 0.55f }; SDL_Vertex v[9] = { { { ex + nx * W, ey + ny * W }, c, { 0, 0 } }, { { ex - nx * W, ey - ny * W }, c, { 0, 0 } }, { { bx + nx * W, by + ny * W }, c, { 0, 0 } }, { { bx - nx * W, by - ny * W }, c, { 0, 0 } }, { { bx + nx * W, by + ny * W }, wood, { 0, 0 } }, { { bx - nx * W, by - ny * W }, wood, { 0, 0 } }, { { tx, ty }, c, { 0, 0 } }, }; int idx[9] = { 0, 1, 2, 1, 3, 2, 4, 5, 6 }; SDL_RenderGeometry(REN, NULL, v, 7, idx, 9); } static void render(void) { SDL_SetRenderDrawBlendMode(REN, SDL_BLENDMODE_NONE); set_col(C.bg); SDL_RenderClear(REN); SDL_SetRenderDrawBlendMode(REN, SDL_BLENDMODE_BLEND); if (NWS == 0) { Tex t = text_make(F_LABEL, "no windows found on this output"); tex_draw_center(t, (float)RW * 0.5f, (float)RH * 0.5f, C.subtext); tex_free(&t); return; } if (ghost_lo >= 0) /* free numbers to drop on */ for (int i = 0; i < NSLOTS; ++i) { if (SLOTS[i].ws >= 0) continue; float ph = anim_phase(); SDL_FRect t = rect_lerp(SLOTS[i].from, SLOTS[i].tile, ph); float ix = t.w * 0.22f, iy = t.h * 0.22f; SDL_FRect g = { t.x + ix, t.y + iy, t.w - 2.0f * ix, t.h - 2.0f * iy }; bool hot = (drop_kind == DROP_WS_NUM || drop_kind == DROP_WIN_NEWWS) && drop_num == SLOTS[i].num; fill_round_rect(g, C.radius * SC * 0.8f, with_alpha(hot ? C.hl : C.tile, (hot ? 0.30f : 0.55f) * ph)); stroke_round_rect(g, C.radius * SC * 0.8f, SDL_max(1.0f, 2.0f * SC), with_alpha(hot ? C.hl : C.accent, (hot ? 1.0f : 0.35f) * ph)); char num[16]; snprintf(num, sizeof(num), "%d", SLOTS[i].num); Tex t2 = text_make(F_BADGE, num); tex_draw_in_box(t2, g, F_BADGE, with_alpha(hot ? C.hl : C.dim, 0.9f * ph)); tex_free(&t2); } if (NDYING > 0) { /* ghosts fading away */ float ph = phase_since(dying_start); if (ph >= 1.0f) NDYING = 0; for (int i = 0; i < NDYING; ++i) { SDL_FRect t = rect_lerp(DYING[i].from, DYING[i].tile, ph); float ix = t.w * 0.22f, iy = t.h * 0.22f; SDL_FRect g = { t.x + ix, t.y + iy, t.w - 2.0f * ix, t.h - 2.0f * iy }; float a = 1.0f - ph; fill_round_rect(g, C.radius * SC * 0.8f, with_alpha(C.tile, 0.55f * a)); stroke_round_rect(g, C.radius * SC * 0.8f, SDL_max(1.0f, 2.0f * SC), with_alpha(C.accent, 0.35f * a)); char num[16]; snprintf(num, sizeof(num), "%d", DYING[i].num); Tex t2 = text_make(F_BADGE, num); tex_draw_in_box(t2, g, F_BADGE, with_alpha(C.dim, 0.9f * a)); tex_free(&t2); } } for (int i = 0; i < NWS; ++i) if (i != sel_ws) draw_workspace(i); draw_workspace(sel_ws); /* selection is drawn last */ 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; bool head_top = pos_is_top(C.header_pos); if (T_HEADER.t && !pos_is_none(C.header_pos)) { float y = head_top ? (HEADER_H - (float)T_HEADER.h) * 0.5f : (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 = 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, pin); } tex_draw(T_HEADER, head_x0, y, PINNED_OUTPUT ? C.bg : C.hint); } if (T_QUERY.t) { float p = 10.0f * SC; SDL_FRect box = { (float)RW * 0.5f - ((float)T_QUERY.w * 0.5f + p), (HEADER_H - (float)T_QUERY.h) * 0.55f - p * 0.5f, (float)T_QUERY.w + 2.0f * p, (float)T_QUERY.h + p }; SDL_FColor qc = confirm_kill ? C.urgent : (filtering ? C.accent : C.match); /* pressed enter on a query that matched nothing: the field answers */ float flash = 0.0f; if (QUERY_FLASH > 0.0) { flash = 1.0f - (float)((now_secs() - QUERY_FLASH) / 0.6); if (flash <= 0.0f) { QUERY_FLASH = 0.0; flash = 0.0f; } else { qc = mix(qc, C.urgent, flash); dirty = true; } } fill_round_rect(box, box.h * 0.35f, mix(mix(C.tile, C.bg, 0.15f), C.urgent, flash * 0.30f)); stroke_round_rect(box, box.h * 0.35f, SDL_max(1.0f, (1.5f + 1.5f * flash) * SC), with_alpha(qc, 0.8f + 0.2f * flash)); tex_draw(T_QUERY, box.x + p, box.y + p * 0.5f, qc); } if (T_HINTS.t && !pos_is_none(C.hints_pos)) { bool hint_top = pos_is_top(C.hints_pos); float y = hint_top ? (HEADER_H - (float)T_HINTS.h) * 0.5f : (float)RH - FOOTER_H * 0.5f - (float)T_HINTS.h * 0.5f; float x = pos_x(C.hints_pos, (float)T_HINTS.w, m); /* both lines in the same band: keep the hints clear of the header */ if (T_HEADER.t && head_top == hint_top && x + (float)T_HINTS.w > head_x0 - m * 0.5f) { if (head_x0 > (float)RW * 0.5f) x = SDL_min(x, head_x0 - m * 0.5f - (float)T_HINTS.w); else x = SDL_max(x, head_x1 + m * 0.5f); x = SDL_max(x, m * 0.5f); } tex_draw(T_HINTS, x, y, with_alpha(C.hint, 0.8f)); } } /* --------------------------------------------------------------- actions */ static bool running = true; static bool BACKDROP; /* drawn behind another program's window */ /* swbr writes a new set of numbers every few seconds; the overview sits open * for longer than that, so it watches the file rather than reading it once. */ static void cpu_poll(void) { if (!C.cpu) return; static Uint64 next; Uint64 now = SDL_GetTicks(); if (now < next) return; next = now + 1000; char path[512]; const char *rt = getenv("XDG_RUNTIME_DIR"); if (rt && *rt) snprintf(path, sizeof(path), "%s/swbr-cpu", rt); else { const char *home = getenv("HOME"); if (!home) return; snprintf(path, sizeof(path), "%s/.cache/swbr-cpu", home); } struct stat st; if (stat(path, &st) != 0 || st.st_mtime == CPU_MTIME) return; cpu_load(); 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; } /* set from the loop, where the backdrop flags are in scope */ static void focus_self_soon(void); static void reload_model(void) { /* remember what was selected so a reload does not lose the cursor */ int keep_con = -1; char keep_ws[64] = {0}; if (NWS > 0) { str_set(keep_ws, sizeof(keep_ws), WSS[sel_ws].name); Win *w = ws_sel_win(&WSS[sel_ws]); if (w) keep_con = w->con_id; } int marks[MAX_WINDOWS]; int nmarks = 0; for (int i = 0; i < NWIN && nmarks < MAX_WINDOWS; ++i) if (WINS[i].marked) marks[nmarks++] = WINS[i].con_id; if (!model_reload()) { /* The tree is being read while sway is busy starting something; a * miss is not a reason to disappear. The overlay quits over it, the * backdrop keeps what it had and tries again on the next event. */ if (!BACKDROP) running = false; return; } for (int i = 0; i < NWIN; ++i) for (int j = 0; j < nmarks; ++j) if (WINS[i].con_id == marks[j]) WINS[i].marked = true; select_current_workspace(); for (int i = 0; i < NWS; ++i) { WSS[i].sel = -1; if (keep_ws[0] && strcmp(WSS[i].name, keep_ws) == 0) sel_ws = i; for (int j = 0; j < WSS[i].count; ++j) if (WINS[WSS[i].first + j].con_id == keep_con) { sel_ws = i; WSS[i].sel = j; } } cpu_load(); /* whatever swbr last measured */ focus_load(); /* and the order things were used in */ mark("cpu numbers"); if (!FS_RESTORE) fullscreen_step_aside(); /* Whether a new window is given the keyboard is the compositor's * business, and an overlay without it does nothing at all — every key * goes to whatever is underneath. So it asks, a few times over half a * second, rather than assuming. Harmless when it already has it. */ focus_self_soon(); if (!drag_active) ghosts_default(); layout(); apply_filter(); rebuild_chrome(); hov_ws = hov_win = -1; } static void act_focus_window(Win *w) { if (!w) return; sway_cmd("[con_id=%d] focus", w->con_id); running = false; } static void act_goto_workspace(const Ws *ws) { if (!ws) return; char *e = escape_arg(ws->name); sway_cmd("workspace --no-auto-back-and-forth \"%s\"", e); free(e); if (C.track) usage_switch(ws->name, ws->output, ws->num); running = false; } /* An empty number: sway has no such workspace yet, so it is asked for by * number and creates it on the way there. */ static void act_goto_number(int num) { if (num < 0) return; sway_cmd("workspace number %d", num); running = false; } /* windows the next action applies to: all marked ones, else the selected one. * Returns 0 when the selection is a whole workspace rather than windows. */ static int gather_targets(int *out, int cap) { int n = 0; for (int i = 0; i < NWIN && n < cap; ++i) if (WINS[i].marked) out[n++] = WINS[i].con_id; if (n == 0 && NWS > 0) { Win *w = ws_sel_win(&WSS[sel_ws]); if (w) out[n++] = w->con_id; } return n; } static void act_move_to_workspace(int number) { int ids[MAX_WINDOWS]; int n = gather_targets(ids, MAX_WINDOWS); if (n > 0) { for (int i = 0; i < n; ++i) sway_cmd("[con_id=%d] move container to workspace number %d", ids[i], number); } else if (NWS > 0 && WSS[sel_ws].ntop > 0) { /* Whole workspace: move its top level containers, not the single * windows. A container carries its own split/tab layout along, so * the arrangement survives the move. */ Ws *ws = &WSS[sel_ws]; if (ws->num == number) return; for (int i = 0; i < ws->ntop; ++i) sway_cmd("[con_id=%d] move container to workspace number %d", ws->top[i], number); } else return; for (int i = 0; i < NWIN; ++i) WINS[i].marked = false; if (C.quit_after_action) { running = false; return; } reload_model(); } static void act_close(void) { int ids[MAX_WINDOWS]; int n = gather_targets(ids, MAX_WINDOWS); if (n == 0 && NWS > 0 && WSS[sel_ws].sel < 0) { /* the whole workspace */ Ws *ws = &WSS[sel_ws]; for (int i = 0; i < ws->count && n < MAX_WINDOWS; ++i) ids[n++] = WINS[ws->first + i].con_id; } for (int i = 0; i < n; ++i) sway_cmd("[con_id=%d] kill", ids[i]); for (int i = 0; i < NWIN; ++i) WINS[i].marked = false; reload_model(); } static void act_toggle_mark(Win *w) { if (w) w->marked = !w->marked; } /* mark every window of the workspace, or clear them all if they already are */ static void toggle_ws_marks(Ws *ws) { bool all = ws->count > 0; for (int i = 0; i < ws->count; ++i) if (!WINS[ws->first + i].marked) { all = false; break; } for (int i = 0; i < ws->count; ++i) WINS[ws->first + i].marked = !all; } /* ----------------------------------------------------------------- fonts */ /* One fc-match run resolves the regular and the bold cut together; spawning * a process twice is the most expensive thing startup would otherwise do. */ static char *FC_REGULAR, *FC_BOLD; static void fontconfig_resolve(const char *family) { /* the family ends up in a shell command, so keep it to harmless characters */ char fam[128] = "sans"; if (family && *family) { size_t n = 0; for (const char *p = family; *p && n < sizeof(fam) - 1; ++p) if (isalnum((unsigned char)*p) || *p == ' ' || *p == '-' || *p == '_' || *p == '.' || *p == ',') fam[n++] = *p; fam[n] = 0; if (!n) snprintf(fam, sizeof(fam), "sans"); } /* The answer never changes between runs, and a shell plus two fc-match * processes is the most expensive thing startup does — tens of * milliseconds when the fontconfig cache is warm, far worse when it is * not. Keep it next to the usage file and the whole thing becomes two * reads. */ char cache[512] = ""; const char *xdg = getenv("XDG_CACHE_HOME"), *home = getenv("HOME"); if (xdg && *xdg) snprintf(cache, sizeof(cache), "%s/swov/fonts", xdg); else if (home && *home) snprintf(cache, sizeof(cache), "%s/.cache/swov/fonts", home); char l1[1024] = {0}, l2[1024] = {0}, want[160] = {0}; snprintf(want, sizeof(want), "%s", fam); if (cache[0]) { /* family, then the two paths */ FILE *c = fopen(cache, "r"); if (c) { char had[160] = {0}; if (fgets(had, sizeof(had), c)) str_trim(had); if (fgets(l1, sizeof(l1), c)) str_trim(l1); if (fgets(l2, sizeof(l2), c)) str_trim(l2); fclose(c); if (strcmp(had, want) || !file_readable(l1)) l1[0] = l2[0] = 0; } } if (!l1[0]) { char cmd[512]; snprintf(cmd, sizeof(cmd), "fc-match -f '%%{file}\n' '%s' 2>/dev/null; " "fc-match -f '%%{file}\n' '%s:bold' 2>/dev/null", fam, fam); FILE *p = popen(cmd, "r"); if (!p) return; if (fgets(l1, sizeof(l1), p)) str_trim(l1); if (fgets(l2, sizeof(l2), p)) str_trim(l2); pclose(p); if (cache[0] && l1[0]) { char *slash = strrchr(cache, '/'); /* mkdir -p, one level */ if (slash) { *slash = 0; char *up = strrchr(cache, '/'); if (up) { *up = 0; mkdir(cache, 0755); *up = '/'; } mkdir(cache, 0755); *slash = '/'; } FILE *c = fopen(cache, "w"); if (c) { fprintf(c, "%s\n%s\n%s\n", want, l1, l2); fclose(c); } } } if (l1[0] && file_readable(l1)) FC_REGULAR = xstrdup(l1); if (l2[0] && file_readable(l2)) FC_BOLD = xstrdup(l2); } static char *pick_font(const char *spec, bool bold) { if (spec && *spec && strchr(spec, '/') && file_readable(spec)) return xstrdup(spec); const char *hit = bold ? FC_BOLD : FC_REGULAR; if (hit) return xstrdup(hit); const char *fallback[] = { "/usr/share/fonts/truetype/dejavu/DejaVuSans.ttf", "/usr/share/fonts/TTF/DejaVuSans.ttf", "/usr/share/fonts/truetype/noto/NotoSans-Regular.ttf", "/usr/share/fonts/liberation/LiberationSans-Regular.ttf" }; for (size_t i = 0; i < SDL_arraysize(fallback); ++i) if (file_readable(fallback[i])) return xstrdup(fallback[i]); return NULL; } static TTF_Font *open_font(const char *path, float size, bool bold, bool synth_bold) { TTF_Font *f = TTF_OpenFont(path, size); if (!f) return NULL; if (bold && synth_bold) TTF_SetFontStyle(f, TTF_STYLE_BOLD); TTF_SetFontHinting(f, TTF_HINTING_LIGHT); return f; } static void load_fonts(void) { /* a configured path needs no fontconfig at all */ bool have_paths = C.font[0] && strchr(C.font, '/') && (!C.font_bold[0] || strchr(C.font_bold, '/')); if (!have_paths) fontconfig_resolve(C.font); mark("font lookup"); char *regular = pick_font(C.font, false); if (!regular) die("no usable font found (install fontconfig and a TTF font)"); char *bold = C.font_bold[0] ? pick_font(C.font_bold, true) : pick_font(C.font, true); bool synth = false; if (!bold) { bold = xstrdup(regular); synth = true; } else if (strcmp(bold, regular) == 0) synth = true; float u = C.ui_scale * SC; F_BADGE = open_font(bold, (float)C.ws_px * u, true, synth); F_LABEL = open_font(bold, (float)C.label_px * u, true, synth); F_TITLE = open_font(regular, (float)C.title_px * u, false, false); F_HINT = open_font(regular, (float)C.hint_px * u, false, false); F_PID = open_font(regular, (float)C.hint_px * u * 0.82f, false, false); free(regular); free(bold); free(FC_REGULAR); free(FC_BOLD); FC_REGULAR = FC_BOLD = NULL; if (!F_BADGE || !F_LABEL || !F_TITLE || !F_HINT) die("could not open fonts: %s", SDL_GetError()); mark("font open"); } /* ------------------------------------------------------------------ main */ static SDL_Window *WIN_HANDLE; static const char *SHOT_PATH; static float SHOT_MX = -1.0f, SHOT_MY = -1.0f; static SDL_Texture *TARGET; static SDL_Cursor *CUR_ARROW, *CUR_HAND; static float MOUSE_SCALE = 1.0f; /* --- backdrop: swov drawn behind another program's window --------------- * No input of its own; a launcher in front sends pointer positions on stdin * 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; static bool BACKDROP_LOG; /* --backdrop-debug: narrate to stderr */ static char RAISE_APP[64]; /* who to put back on top of us */ static int RAISE_LEFT; static double RAISE_AT; static float BLUR_NOW = 1.0f; /* 1 = as soft as `blur` says, 0 = sharp */ static float BLUR_WANT = 1.0f; /* a drag sharpens it, so you can aim */ static SDL_Texture *BLUR_HALF, *BLUR_SMALL; static int BLUR_W, BLUR_H; static void usage(void) { printf( "swov — window and workspace overview for sway\n" "\n" "usage: swov [options] [key=value ...]\n" " -g, --go N|NAME switch to that workspace and exit, without a window\n" " -b, --back switch to the previously used workspace and exit\n" " --workspaces print the workspaces as num/name/output/flags, and exit\n" " --adopt PID WS wait for the window PID opens, move it to workspace WS\n" " and exit. It tells sway where that window belongs\n" " before it opens, so nothing flashes onto the workspace\n" " you are on; --no-assign leaves that out.\n" " --beside CON_ID --edge left|right|top|bottom\n" " puts it next to that window instead of at the end,\n" " --adopt-debug says what it is doing\n" " and exit. Runs in the background; --adopt-wait keeps it\n" " in the foreground, --adopt-focus also switches there,\n" " --adopt-timeout SECS gives up after that (default 20)\n" " --timing print how long each part of startup took, to stderr\n" " --backdrop-debug the same, narrating the conversation to stderr\n" " --backdrop draw the overview behind another window, taking no\n" " input of its own: it fades in, and reads commands on\n" " stdin (hover FX FY, drag on|off, fade in|out, quit).\n" " This is what swas puts behind its wheel\n" " --usage print how long each workspace has been used, and exit\n" " --info print every path and setting swov is using, and exit\n" " -c, --config PATH read this config file instead of the default\n" " -n, --no-config ignore the config file\n" " --shot PATH render one frame to a PNG and exit (handy for tuning)\n" " -h, --help this text\n" " -v, --version print the version and exit\n" "\n" "Every config key is also a command line option, in three spellings:\n" " swov --ui_scale=1.2 hl=ff8800 -s ssaa=1\n" "\n" "default config: ${XDG_CONFIG_HOME:-~/.config}/swov/config\n" "keys: ssaa icons icon_px shadow shadow_layers vsync ui_scale ws_px label_px\n" " title_px hint_px font font_bold cols rows margin gap pad win_gap\n" " screen_pad radius border show_empty all_outputs header_pos hints_pos\n" " start_selection quit_on_focus_loss quit_after_action\n" " colors: bg tile tile_sel tile_hover mini_bg card card_hover card_focus\n" " hl text subtext dim accent hltext hint urgent outline\n" "\n" "keys:\n" " enter / click focus the window under the cursor and leave\n" " click on a tile switch to that workspace\n" " arrows or hjkl move the selection; it walks through tile borders\n" " tab / shift+tab previous / next workspace\n" " ctrl+tab one row down in the grid, with shift one row up\n" " w switch between window and whole-workspace selection\n" " space / right click mark or unmark the window under the cursor\n" " shift+space, a mark or unmark every window of the workspace\n" " c clear all marks\n" " 0-9 switch to that workspace and leave\n" " ctrl+0-9 move marked or selected windows to that workspace;\n" " with a whole workspace selected it moves everything\n" " there, keeping the container layout intact\n" " x / delete close marked or selected windows, enter confirms\n" " f find windows by app id, title or workspace name\n" " / filter: same search, but hides everything else\n" " r reload the tree\n" " esc / q quit\n"); } static bool create_target(void) { if (TARGET) { SDL_DestroyTexture(TARGET); TARGET = NULL; } int pw = 0, ph = 0; SDL_GetWindowSizeInPixels(WIN_HANDLE, &pw, &ph); if (pw <= 0 || ph <= 0) return false; int ssaa = C.ssaa; while (ssaa > 1 && (long long)pw * ph * ssaa * ssaa > 34000000LL) ssaa--; if (ssaa > 1 || BACKDROP) { TARGET = SDL_CreateTexture(REN, SDL_PIXELFORMAT_RGBA32, SDL_TEXTUREACCESS_TARGET, pw * ssaa, ph * ssaa); if (TARGET) { SDL_SetTextureScaleMode(TARGET, SDL_SCALEMODE_LINEAR); SDL_SetTextureBlendMode(TARGET, SDL_BLENDMODE_BLEND); } } SC = TARGET ? (float)ssaa : 1.0f; RW = TARGET ? pw * ssaa : pw; RH = TARGET ? ph * ssaa : ph; int ww = 0, wh = 0; SDL_GetWindowSize(WIN_HANDLE, &ww, &wh); MOUSE_SCALE = (ww > 0) ? (float)RW / (float)ww : 1.0f; return true; } static void goto_workspace_number(int num) { for (int i = 0; i < NWS; ++i) if (WSS[i].num == num) { act_goto_workspace(&WSS[i]); return; } sway_cmd("workspace number %d", num); /* it does not exist yet */ if (C.track) { char name[16]; snprintf(name, sizeof(name), "%d", num); usage_switch(name, FOCUSED_OUTPUT, num); } running = false; } static int digit_from_scancode(SDL_Scancode sc) { if (sc >= SDL_SCANCODE_1 && sc <= SDL_SCANCODE_9) return (int)(sc - SDL_SCANCODE_1) + 1; if (sc == SDL_SCANCODE_0) return 0; return -1; } 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; ghosts_default(); 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; bool was_active = sel_active; sel_active = true; SDL_Keycode key = k->key; if (editing) { switch (key) { case SDLK_RETURN: case SDLK_KP_ENTER: end_edit(true); reload_model(); return; case SDLK_ESCAPE: end_edit(false); return; case SDLK_BACKSPACE: while (edit_len > 0) { unsigned char c = (unsigned char)edit_buf[--edit_len]; edit_buf[edit_len] = 0; if ((c & 0xc0) != 0x80) break; } return; default: return; } } if (editing) { switch (key) { case SDLK_RETURN: case SDLK_KP_ENTER: end_edit(true); reload_model(); return; case SDLK_ESCAPE: end_edit(false); return; case SDLK_BACKSPACE: while (edit_len > 0) { unsigned char c = (unsigned char)edit_buf[--edit_len]; edit_buf[edit_len] = 0; if ((c & 0xc0) != 0x80) break; } return; default: return; } } if (confirm_kill) { /* enter confirms, anything * else calls it off */ confirm_kill = false; if (key == SDLK_RETURN || key == SDLK_KP_ENTER) act_close(); rebuild_chrome(); return; } if (query_active()) { switch (key) { case SDLK_ESCAPE: filtering = searching = false; query[0] = 0; qlen = 0; apply_filter(); rebuild_chrome(); return; case SDLK_RETURN: case SDLK_KP_ENTER: /* the same rule as outside a search: a window if one is picked, * otherwise the workspace itself. Tab leaves no window picked, * and enter has to keep working after it. */ if (NWS == 0) { running = false; return; } if (ws_sel_win(&WSS[sel_ws])) act_focus_window(ws_sel_win(&WSS[sel_ws])); else act_goto_workspace(&WSS[sel_ws]); return; case SDLK_BACKSPACE: while (qlen > 0) { /* drop one code point */ unsigned char c = (unsigned char)query[--qlen]; query[qlen] = 0; if ((c & 0xc0) != 0x80) break; } apply_filter(); rebuild_chrome(); return; case SDLK_TAB: if (k->mod & SDL_KMOD_CTRL) step_ws_row(shift ? -1 : 1); else step_ws(shift ? -1 : 1); return; case SDLK_LEFT: navigate(-1, 0); return; case SDLK_RIGHT: navigate( 1, 0); return; case SDLK_UP: navigate(0, -1); return; case SDLK_DOWN: navigate(0, 1); return; default: return; } } /* A digit only counts when it is pressed bare, or with ctrl. With shift * held the layout may well be producing something else entirely — on a * German keyboard shift+7 is "/" — and that has to reach text input. */ int digit = digit_from_scancode(k->scancode); if (digit >= 0 && !shift && !(k->mod & SDL_KMOD_ALT)) { if (k->mod & SDL_KMOD_CTRL) { if (was_active) act_move_to_workspace(digit); /* ctrl: take it there */ } else { goto_workspace_number(digit); /* bare: go there */ } return; } if (digit >= 0) return; /* shifted digit: let text input have it */ switch (key) { case SDLK_ESCAPE: if (drag_active) { drag_cancel(); break; } running = false; break; case SDLK_Q: running = false; break; case SDLK_RETURN: case SDLK_KP_ENTER: if (NWS == 0) { running = false; break; } /* A query that found nothing has no selection to act on. Leaving and * switching to whatever happened to be selected before is the worst * of the possible answers; the field says no instead. */ if (qlen > 0 && !sel_active) { QUERY_FLASH = now_secs(); dirty = true; break; } if (ws_sel_win(&WSS[sel_ws])) act_focus_window(ws_sel_win(&WSS[sel_ws])); else act_goto_workspace(&WSS[sel_ws]); break; case SDLK_TAB: if (k->mod & SDL_KMOD_CTRL) step_ws_row(shift ? -1 : 1); else if (!strcmp(C.tab, "recent") && step_recent(shift ? -1 : 1)) break; 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]; if (ws->sel >= 0) ws->sel = -1; else ws->sel = ws_first_visible(ws); } break; case SDLK_LEFT: case SDLK_H: navigate(-1, 0); break; case SDLK_RIGHT: case SDLK_L: navigate( 1, 0); break; case SDLK_UP: case SDLK_K: navigate(0, -1); break; case SDLK_DOWN: case SDLK_J: navigate(0, 1); break; case SDLK_SPACE: if (NWS > 0) { Ws *ws = &WSS[sel_ws]; /* On a window, space marks that window. On a workspace with no * window picked, it marks the workspace — several of those can * then be dragged to another screen in one go. shift+space still * marks everything inside one. */ if (!shift && ws->sel >= 0) act_toggle_mark(ws_sel_win(ws)); else if (!shift) ws->marked = !ws->marked; else toggle_ws_marks(ws); } break; case SDLK_A: if (NWS > 0) toggle_ws_marks(&WSS[sel_ws]); break; case SDLK_C: for (int i = 0; i < NWIN; ++i) WINS[i].marked = false; for (int i = 0; i < NWS; ++i) WSS[i].marked = false; break; case SDLK_X: case SDLK_DELETE: if (NWS > 0) { /* ask before killing */ int ids[MAX_WINDOWS]; if (gather_targets(ids, MAX_WINDOWS) > 0 || (WSS[sel_ws].sel < 0 && WSS[sel_ws].count > 0)) { confirm_kill = true; rebuild_chrome(); } } break; case SDLK_R: reload_model(); break; case SDLK_SLASH: filtering = true; searching = false; query[0] = 0; qlen = 0; swallow_next_text = true; rebuild_chrome(); break; case SDLK_F: /* find, without hiding */ searching = true; filtering = false; query[0] = 0; qlen = 0; swallow_next_text = true; rebuild_chrome(); break; default: break; } } static void begin_edit(int idx) { editing = true; edit_ws = idx; str_set(edit_buf, sizeof(edit_buf), WSS[idx].label); edit_len = (int)strlen(edit_buf); swallow_next_text = false; } static void end_edit(bool commit) { if (commit && edit_ws >= 0 && edit_ws < NWS) { Ws *ws = &WSS[edit_ws]; char *to = edit_buf[0] ? (ws->num >= 0 ? fmt_alloc("%d:%s", ws->num, edit_buf) : xstrdup(edit_buf)) : (ws->num >= 0 ? fmt_alloc("%d", ws->num) : xstrdup(ws->name)); if (strcmp(to, ws->name) != 0) { ws_rename(ws, to); if (C.track) { /* keep its history */ usage_load(); Usage *u = usage_find(ws->name, false); if (u) str_set(u->name, sizeof(u->name), to); if (strcmp(USAGE_CUR, ws->name) == 0) str_set(USAGE_CUR, sizeof(USAGE_CUR), to); usage_save(); } } free(to); } editing = false; edit_ws = -1; edit_buf[0] = 0; edit_len = 0; } static void handle_mouse_press(const SDL_MouseButtonEvent *b) { float mx = b->x * MOUSE_SCALE, my = b->y * MOUSE_SCALE; int ws_idx = -1; int win_idx = hit_test(mx, my, &ws_idx); if (editing) end_edit(true); /* a click elsewhere commits */ /* a monitor picked up out of the map: it can be put down against another one to rearrange the desk */ if (b->button == SDL_BUTTON_LEFT && MAP_RECT.w > 0.0f) { for (int i = 0; i < NOUTS; ++i) { SDL_FRect r = OUTS[i].box; if (r.w <= 0.0f || mx < r.x || mx >= r.x + r.w || my < r.y || my >= r.y + r.h) continue; OUT_PRESS = i; OUT_PRESS_X = mx; OUT_PRESS_Y = my; break; } } /* the map of the monitors: step onto one and look at its workspaces */ if (b->button == SDL_BUTTON_LEFT && MAP_RECT.w > 0.0f) { for (int i = 0; i < NOUTS; ++i) { SDL_FRect r = OUTS[i].box; if (r.w <= 0.0f || mx < r.x || mx >= r.x + r.w || my < r.y || my >= r.y + r.h) continue; if (strcmp(OUTS[i].name, FOCUSED_OUTPUT) == 0) { PINNED_OUTPUT = false; /* back to following sway */ str_set(FOCUSED_OUTPUT, sizeof(FOCUSED_OUTPUT), HOME_OUTPUT); } else { PINNED_OUTPUT = strcmp(OUTS[i].name, HOME_OUTPUT) != 0; str_set(FOCUSED_OUTPUT, sizeof(FOCUSED_OUTPUT), OUTS[i].name); } reload_model(); dirty = true; return; } } press_slot = -1; if (ws_idx < 0) { /* maybe an empty number */ int si = slot_at(mx, my); if (si >= 0 && SLOTS[si].ws < 0) press_slot = si; } if (b->button == SDL_BUTTON_LEFT && ws_idx >= 0 && win_idx < 0) { SDL_FRect tb = WSS[ws_idx].title_hit; if (mx >= tb.x && mx < tb.x + tb.w && my >= tb.y && my < tb.y + tb.h) { sel_active = true; sel_ws = ws_idx; WSS[ws_idx].sel = -1; begin_edit(ws_idx); return; } } if (ws_idx < 0) { /* the empty background */ if (b->button == SDL_BUTTON_LEFT) running = false; return; } sel_active = true; sel_ws = ws_idx; WSS[ws_idx].sel = (win_idx >= 0) ? win_idx - WSS[ws_idx].first : -1; switch (b->button) { case SDL_BUTTON_LEFT: /* arm a drag, act on release */ press_down = true; press_x = mx; press_y = my; press_win = win_idx; press_ws = ws_idx; break; case SDL_BUTTON_RIGHT: if (win_idx >= 0) act_toggle_mark(&WINS[win_idx]); break; case SDL_BUTTON_MIDDLE: if (win_idx >= 0) { sway_cmd("[con_id=%d] kill", WINS[win_idx].con_id); reload_model(); } break; default: break; } } /* Rearranging the desk. sway places outputs by absolute pixel position, so * putting one against another's edge is arithmetic: the same top, and an x * one width to the left, or whatever the chosen side asks for. */ static void outputs_rearrange(int moved, int anchor, int edge) { if (moved < 0 || anchor < 0 || moved == anchor || moved >= NOUTS || anchor >= NOUTS) return; Out *m = &OUTS[moved], *a = &OUTS[anchor]; int x = a->x, y = a->y; switch (edge) { case EDGE_LEFT: x = a->x - m->w; y = a->y; break; case EDGE_RIGHT: x = a->x + a->w; y = a->y; break; case EDGE_TOP: x = a->x; y = a->y - m->h; break; default: x = a->x; y = a->y + a->h; break; } char *n = escape_arg(m->name); sway_cmd("output \"%s\" position %d %d", n, x, y); free(n); reload_model(); } static void handle_mouse_release(const SDL_MouseButtonEvent *b) { if (b->button != SDL_BUTTON_LEFT) return; float mx = b->x * MOUSE_SCALE, my = b->y * MOUSE_SCALE; if (OUT_PRESS >= 0) { if (OUT_TARGET >= 0) outputs_rearrange(OUT_PRESS, OUT_TARGET, OUT_EDGE); OUT_PRESS = OUT_TARGET = -1; dirty = true; return; } if (drag_active) { drag_update_target(mx, my); drag_finish(); press_down = false; return; } if (!press_down) return; press_down = false; /* An empty number is a workspace that does not exist yet. Clicking one * goes there and sway creates it — the hit test only knows about * workspaces sway has, so the slot has to be asked directly. */ { int si = slot_at(mx, my); if (si >= 0 && SLOTS[si].ws < 0) { int num = SLOTS[si].num; press_win = press_ws = -1; if (C.ghost_click && si == press_slot) act_goto_number(num); press_slot = -1; return; } } /* a plain click: only acts when press and release landed on the same * window, or on the same workspace */ int ws_idx = -1; int win_idx = hit_test(mx, my, &ws_idx); if (ws_idx < 0 || ws_idx != press_ws) { press_win = press_ws = -1; return; } if (win_idx >= 0 && win_idx == press_win) act_focus_window(&WINS[win_idx]); else if (win_idx < 0 && press_win < 0) act_goto_workspace(&WSS[ws_idx]); press_win = press_ws = -1; } static void handle_mouse_motion(const SDL_MouseMotionEvent *mo) { float mx = mo->x * MOUSE_SCALE, my = mo->y * MOUSE_SCALE; drag_x = mx; drag_y = my; if (press_down && !drag_active) { float dx = mx - press_x, dy = my - press_y; if (dx * dx + dy * dy > (7.0f * SC) * (7.0f * SC)) { drag_active = true; hov_ws = hov_win = -1; /* hover is meaningless now */ drag_src_tile = (SDL_FRect){ 0, 0, 0, 0 }; if (press_win >= 0 && WINS[press_win].ws >= 0 && WINS[press_win].ws < NWS) drag_src_tile = WSS[WINS[press_win].ws].tile; if (press_win < 0 && press_ws >= 0 && WSS[press_ws].num >= 0) drag_ws_mode = true; /* dragging the whole workspace */ update_ghosts(mx, my); SDL_SetCursor(CUR_HAND); } } /* dragging a monitor about in the map */ if (OUT_PRESS >= 0) { OUT_TARGET = -1; if (SDL_fabsf(mx - OUT_PRESS_X) + SDL_fabsf(my - OUT_PRESS_Y) > 6.0f * SC) for (int i = 0; i < NOUTS; ++i) { if (i == OUT_PRESS) continue; SDL_FRect r = OUTS[i].box; if (r.w <= 0.0f || mx < r.x || mx >= r.x + r.w || my < r.y || my >= r.y + r.h) continue; OUT_TARGET = i; OUT_EDGE = edge_at(r, mx, my); break; } dirty = true; return; } if (drag_active) { int dws = -1; lift_update(mx, my, dws, hit_test(mx, my, &dws)); drag_update_target(mx, my); dirty = true; return; } int ws_idx = -1; int win_idx = hit_test(mx, my, &ws_idx); lift_update(mx, my, ws_idx, win_idx); bool ph = false; if (C.rename_icon && ws_idx >= 0 && win_idx < 0) { SDL_FRect tb = WSS[ws_idx].title_hit; ph = mx >= tb.x && mx < tb.x + tb.w && my >= tb.y && my < tb.y + tb.h; } if (ph != pencil_hot) { pencil_hot = ph; dirty = true; } if (ws_idx == hov_ws && win_idx == hov_win) return; hov_ws = ws_idx; hov_win = win_idx; SDL_SetCursor(ws_idx >= 0 ? CUR_HAND : CUR_ARROW); dirty = true; /* Pointing at something selects it, so space, x, ctrl+digit and the rest * act on whatever is under the pointer, exactly as they do on whatever * the arrow keys picked. */ if (ws_idx >= 0) { sel_active = true; sel_ws = ws_idx; WSS[ws_idx].sel = (win_idx >= 0) ? win_idx - WSS[ws_idx].first : -1; } } static void handle_event(const SDL_Event *e) { /* Mouse motion fires constantly; it only earns a redraw when it changes * what is under the cursor. Everything else is a real state change. */ if (e->type != SDL_EVENT_MOUSE_MOTION) dirty = true; if (BACKDROP) { /* the window in front owns the pointer and the keyboard; all we do is * follow the size of the screen and the commands on stdin */ switch (e->type) { case SDL_EVENT_QUIT: running = false; break; case SDL_EVENT_WINDOW_PIXEL_SIZE_CHANGED: case SDL_EVENT_WINDOW_RESIZED: if (create_target()) { layout(); rebuild_chrome(); } break; default: break; } return; } switch (e->type) { case SDL_EVENT_QUIT: running = false; break; case SDL_EVENT_KEY_DOWN: handle_key(&e->key); break; case SDL_EVENT_TEXT_INPUT: /* the key that opened a mode sends its own character right after */ if (swallow_next_text) { swallow_next_text = false; break; } if (editing) { /* renaming a workspace */ size_t add = strlen(e->text.text); if (edit_len + (int)add < (int)sizeof(edit_buf) - 1) { memcpy(edit_buf + edit_len, e->text.text, add + 1); edit_len += (int)add; } break; } if (!query_active()) { /* "/" opens the filter */ if (strcmp(e->text.text, "/") == 0) { filtering = true; query[0] = 0; qlen = 0; rebuild_chrome(); } break; } { size_t add = strlen(e->text.text); if (qlen + (int)add < (int)sizeof(query) - 1) { memcpy(query + qlen, e->text.text, add + 1); qlen += (int)add; apply_filter(); rebuild_chrome(); } } break; case SDL_EVENT_MOUSE_MOTION: handle_mouse_motion(&e->motion); break; case SDL_EVENT_MOUSE_BUTTON_DOWN: handle_mouse_press(&e->button); break; case SDL_EVENT_MOUSE_BUTTON_UP: handle_mouse_release(&e->button); break; case SDL_EVENT_MOUSE_WHEEL: if (e->wheel.y != 0.0f) step_ws(e->wheel.y > 0.0f ? -1 : 1); break; case SDL_EVENT_WINDOW_PIXEL_SIZE_CHANGED: case SDL_EVENT_WINDOW_RESIZED: if (create_target()) { layout(); rebuild_chrome(); } break; case SDL_EVENT_WINDOW_FOCUS_LOST: /* 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: break; } } /* ------------------------------------------------------- backdrop commands * One line in, one line out, on stdin and stdout. Coordinates are fractions * of the window, so the program in front never has to know how big we are or * which monitor we picked. * * drag on|off make the free numbers appear as tiles, or put them away * hover FX FY -> "target N", "target NAME" or "target none" * fade in|out blend the overview in or out * quit fade out and leave * (end of input) the same as quit: the parent is gone */ static void backdrop_reply(const char *fmt, ...) { char line[512]; va_list ap; va_start(ap, fmt); int n = vsnprintf(line, sizeof(line) - 2, fmt, ap); va_end(ap); 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) { float x = fx * (float)RW, y = fy * (float)RH; int si = slot_at(x, y); if (si >= 0 && SLOTS[si].ws < 0) { /* a free number */ drop_kind = DROP_WIN_NEWWS; /* the ghost lights up */ drop_num = SLOTS[si].num; drag_active = true; dirty = true; backdrop_reply("target %d", SLOTS[si].num); 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) { dirty = true; backdrop_reply("target none"); return; } sel_active = true; /* the selection cursor */ sel_ws = ws_idx; WSS[ws_idx].sel = -1; dirty = true; /* Where inside the tile decides what happens, exactly as dragging a * window inside swov does. Against an edge: beside everything on the * workspace. Over a window: beside that one. The bar shows which. */ static const char *EDGE_NAME[] = { "left", "right", "top", "bottom" }; char place[80] = ""; SDL_FRect t = WSS[ws_idx].screen; float bx = SDL_clamp(t.w * 0.09f, 6.0f * SC, t.w * 0.18f); float by = SDL_clamp(t.h * 0.09f, 6.0f * SC, t.h * 0.18f); int edge = -1; if (x - t.x < bx) edge = EDGE_LEFT; else if (t.x + t.w - x < bx) edge = EDGE_RIGHT; else if (y - t.y < by) edge = EDGE_TOP; else if (t.y + t.h - y < by) edge = EDGE_BOTTOM; if (edge >= 0 && WSS[ws_idx].count > 0) { bool pop = false; int ref = ws_edge_ref(&WSS[ws_idx], edge, &pop); drag_active = true; drop_kind = DROP_WS_EDGE; drop_ws = ws_idx; drop_edge = edge; if (ref >= 0 && !pop) snprintf(place, sizeof(place), " beside %d %s", WINS[ref].con_id, EDGE_NAME[edge]); else if (ref >= 0) snprintf(place, sizeof(place), " outer %s", EDGE_NAME[edge]); } else if (win_idx >= 0) { int e = edge_at(WINS[win_idx].card, x, y); drag_active = true; drop_kind = DROP_WIN_NEAR; drop_win = win_idx; drop_ws = ws_idx; drop_edge = e; snprintf(place, sizeof(place), " beside %d %s", WINS[win_idx].con_id, EDGE_NAME[e]); } const char *here = WSS[ws_idx].focused ? " current" : ""; if (WSS[ws_idx].num >= 0) backdrop_reply("target %d%s%s", WSS[ws_idx].num, here, place); else backdrop_reply("target %s%s%s", WSS[ws_idx].name, here, place); } /* returns false when the channel is done with */ static bool backdrop_command(char *line) { char *nl = strchr(line, '\n'); if (nl) *nl = 0; if (BACKDROP_LOG) fprintf(stderr, "swov: <- %s\n", line); if (!strncmp(line, "hover ", 6)) { float fx = 0.0f, fy = 0.0f; if (sscanf(line + 6, "%f %f", &fx, &fy) == 2) backdrop_hover(fx, fy); return true; } if (!strncmp(line, "assign ", 7)) { int pid = 0; char ws[64] = ""; if (sscanf(line + 7, "%d %63s", &pid, ws) == 2 && pid > 0 && ws[0]) assign_pid_to_ws(pid, ws); 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. * They do cost the real tiles some size; drop_ghosts=0 keeps them * out of a drag that comes from swas. */ ghosts_dragging(); BLUR_WANT = 0.0f; return true; } if (!strcmp(line, "drag off")) { MAP_HOVER = -1; show_all_outputs(false); ghosts_default(); BLUR_WANT = 1.0f; drag_active = false; drop_kind = DROP_NONE; sel_active = false; dirty = true; return true; } if (!strncmp(line, "raise ", 6) && line[6]) { str_set(RAISE_APP, sizeof(RAISE_APP), line + 6); RAISE_LEFT = 3; /* now, and twice more */ RAISE_AT = 0.0; /* Our window mapped on top of the one that asked for us, and on * Wayland that one cannot lift itself back. sway can. * * Focusing it directly is not enough: it usually still *is* the * focused window, thanks to `no_focus`, so sway has nothing to do and * the stacking never changes. Focusing us first and it second makes * the second a real change, which restacks. That is also why this * waits for our own window to exist in the tree — asking before sway * knows about it is the other half of the race. */ return true; } if (!strcmp(line, "fade in")) { BACKDROP_WANT = 1.0f; return true; } if (!strcmp(line, "fade out")) { BACKDROP_WANT = 0.0f; return true; } if (!strcmp(line, "quit")) return false; return true; } /* read whatever is waiting, one line at a time */ static void backdrop_poll(void) { static char buf[512]; static int len; struct pollfd p = { OV_IN, POLLIN, 0 }; while (poll(&p, 1, 0) > 0 && (p.revents & (POLLIN | POLLHUP))) { 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; char *start = buf, *nl; while ((nl = strchr(start, '\n')) != NULL) { *nl = 0; if (!backdrop_command(start)) { BACKDROP_LEAVING = true; return; } start = nl + 1; } len = (int)strlen(start); memmove(buf, start, (size_t)len + 1); } } /* Put the window that spawned us back on top. Ours mapped over it and on * Wayland it cannot lift itself; sway's `focus` on a floating container always * raises it, so one command does the job — but only once sway knows our window * exists. Rather than guess when that is, say it three times over half a * second. Repeating is harmless: the wheel is already the focused window. */ static void focus_self_soon(void) { if (BACKDROP || SERVING || !C.focus_self || RAISE_APP[0]) return; str_set(RAISE_APP, sizeof(RAISE_APP), APP_ID); RAISE_LEFT = 3; RAISE_AT = 0.0; } static void backdrop_raise_tick(void) { if (RAISE_LEFT <= 0) return; double now = now_secs(); if (now < RAISE_AT) return; char *e = escape_arg(RAISE_APP); bool ok = sway_cmd("[app_id=\"%s\"] focus", e); free(e); if (BACKDROP_LOG) fprintf(stderr, "swov: raise [app_id=\"%s\"] focus -> %s\n", RAISE_APP, ok ? "ok" : "no match"); RAISE_AT = now + 0.18; RAISE_LEFT--; } /* 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) { /* Nought means off, not "at once". Read as a duration it came out as a * millisecond, so dragging a workspace over a monitor switched the view * the instant the pointer arrived — and the drop then had nothing left * to do. The switch is off by default: dropping is the way to move * something to another screen. */ if (C.map_dwell_ms <= 0) { MAP_HOVER = -1; return; } if (MAP_HOVER < 0) return; dirty = true; /* the fill is growing */ if (now_secs() - MAP_HOVER_SINCE < C.map_dwell_ms / 1000.0) 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; float rate = C.anim_ms > 1.0f ? ms / C.anim_ms : 1.0f; if (BACKDROP_A < BACKDROP_WANT) { BACKDROP_A += rate; if (BACKDROP_A > BACKDROP_WANT) BACKDROP_A = BACKDROP_WANT; dirty = true; } else if (BACKDROP_A > BACKDROP_WANT) { BACKDROP_A -= rate; if (BACKDROP_A < BACKDROP_WANT) BACKDROP_A = BACKDROP_WANT; dirty = true; } if (BLUR_NOW != BLUR_WANT) { /* sharpen / soften again */ float step = rate * 1.6f; if (BLUR_NOW < BLUR_WANT) { BLUR_NOW += step; if (BLUR_NOW > BLUR_WANT) BLUR_NOW = BLUR_WANT; } else { BLUR_NOW -= step; if (BLUR_NOW < BLUR_WANT) BLUR_NOW = BLUR_WANT; } dirty = true; } if (BACKDROP_LEAVING && BACKDROP_A <= 0.0f) running = false; } /* Two scaled copies of the frame we already rendered: full -> half -> small, * then that small one stretched back over the screen. The card does the * filtering, so it is three quad blits and no per-pixel work of our own. The * sharp frame is then drawn over it with whatever alpha is left, which is how * it sharpens while you drag. */ static void backdrop_blur(float alpha) { static const int FACTOR[4] = { 0, 5, 9, 15 }; int f = FACTOR[C.blur < 0 ? 0 : (C.blur > 3 ? 3 : C.blur)]; int w1 = SDL_max(2, RW / 2), h1 = SDL_max(2, RH / 2); int w2 = SDL_max(2, RW / f), h2 = SDL_max(2, RH / f); if (!BLUR_HALF || BLUR_W != w2 || BLUR_H != h2) { if (BLUR_HALF) SDL_DestroyTexture(BLUR_HALF); if (BLUR_SMALL) SDL_DestroyTexture(BLUR_SMALL); BLUR_HALF = SDL_CreateTexture(REN, SDL_PIXELFORMAT_RGBA32, SDL_TEXTUREACCESS_TARGET, w1, h1); BLUR_SMALL = SDL_CreateTexture(REN, SDL_PIXELFORMAT_RGBA32, SDL_TEXTUREACCESS_TARGET, w2, h2); if (!BLUR_HALF || !BLUR_SMALL) { BLUR_W = BLUR_H = 0; return; } SDL_SetTextureScaleMode(BLUR_HALF, SDL_SCALEMODE_LINEAR); SDL_SetTextureScaleMode(BLUR_SMALL, SDL_SCALEMODE_LINEAR); BLUR_W = w2; BLUR_H = h2; } /* copy, do not blend: the alpha of the overlay has to survive the trip */ SDL_SetTextureBlendMode(TARGET, SDL_BLENDMODE_NONE); SDL_SetTextureBlendMode(BLUR_HALF, SDL_BLENDMODE_NONE); SDL_SetRenderTarget(REN, BLUR_HALF); SDL_RenderTexture(REN, TARGET, NULL, NULL); SDL_SetRenderTarget(REN, BLUR_SMALL); SDL_RenderTexture(REN, BLUR_HALF, NULL, NULL); SDL_SetRenderTarget(REN, NULL); SDL_SetTextureBlendMode(TARGET, SDL_BLENDMODE_BLEND); SDL_SetTextureBlendMode(BLUR_SMALL, SDL_BLENDMODE_BLEND); SDL_SetTextureAlphaModFloat(BLUR_SMALL, alpha); SDL_RenderTexture(REN, BLUR_SMALL, NULL, NULL); } static void present(void) { if (TARGET) SDL_SetRenderTarget(REN, TARGET); render(); if (TARGET) { SDL_SetRenderTarget(REN, NULL); SDL_SetRenderDrawBlendMode(REN, SDL_BLENDMODE_NONE); SDL_SetRenderDrawColorFloat(REN, 0, 0, 0, 0); SDL_RenderClear(REN); SDL_SetRenderDrawBlendMode(REN, SDL_BLENDMODE_BLEND); float sharp = 1.0f; if (BACKDROP && C.blur > 0 && BLUR_NOW > 0.0f) { backdrop_blur(BACKDROP_A); sharp = 1.0f - BLUR_NOW; } if (BACKDROP) SDL_SetTextureAlphaModFloat(TARGET, BACKDROP_A * sharp); if (sharp > 0.0f) SDL_RenderTexture(REN, TARGET, NULL, NULL); } if (SHOT_PATH) { SDL_Surface *shot = SDL_RenderReadPixels(REN, NULL); if (shot) { if (!IMG_SavePNG(shot, SHOT_PATH)) fprintf(stderr, "swov: cannot write %s: %s\n", SHOT_PATH, SDL_GetError()); SDL_DestroySurface(shot); } running = false; } SDL_RenderPresent(REN); shown_once = true; } /* Place the overlay on the output sway is using. Matching by name only works * when SDL reports the connector ("DP-1"); on wayland it often reports the * monitor model instead, which is why the geometry is the better key. */ static void pick_bounds(SDL_Rect *out) { *out = (SDL_Rect){ 0, 0, 1280, 720 }; int count = 0; SDL_DisplayID *ids = SDL_GetDisplays(&count); if (!ids || count <= 0) { SDL_free(ids); return; } SDL_DisplayID chosen = 0; /* what sway says the focused output covers */ int ox = 0, oy = 0, ow = 0, oh = 0; JV *outs = sway_query(IPC_GET_OUTPUTS); if (outs && outs->type == J_ARR) { for (int i = 0; i < outs->count; ++i) { const JV *o = outs->items[i]; bool is_it = FOCUSED_OUTPUT[0] ? strcmp(jstr(o, "name", ""), FOCUSED_OUTPUT) == 0 : jbool(o, "focused", false); if (!is_it) continue; const JV *r = jget(o, "rect"); ox = jint(r, "x", 0); oy = jint(r, "y", 0); ow = jint(r, "width", 0); oh = jint(r, "height", 0); break; } } jfree(outs); if (ow > 0 && oh > 0) { /* the display holding it */ float cx = (float)ox + (float)ow * 0.5f; float cy = (float)oy + (float)oh * 0.5f; for (int i = 0; i < count; ++i) { SDL_Rect b; if (!SDL_GetDisplayBounds(ids[i], &b)) continue; if (cx >= (float)b.x && cx < (float)(b.x + b.w) && cy >= (float)b.y && cy < (float)(b.y + b.h)) { chosen = ids[i]; break; } } } if (!chosen && FOCUSED_OUTPUT[0]) { for (int i = 0; i < count; ++i) { const char *n = SDL_GetDisplayName(ids[i]); if (n && strcmp(n, FOCUSED_OUTPUT) == 0) { chosen = ids[i]; break; } } } if (!chosen) { float mx = 0.0f, my = 0.0f; SDL_GetGlobalMouseState(&mx, &my); chosen = ids[0]; for (int i = 0; i < count; ++i) { SDL_Rect b; if (!SDL_GetDisplayBounds(ids[i], &b)) continue; if (mx >= (float)b.x && mx < (float)(b.x + b.w) && my >= (float)b.y && my < (float)(b.y + b.h)) { chosen = ids[i]; break; } } } SDL_GetDisplayBounds(chosen, out); SDL_free(ids); } int main(int argc, char **argv) { C = cfg_defaults(); char *cfg_path = NULL; bool use_cfg = true; const char *go_name = NULL; bool go_back = false; bool show_usage_stats = false; bool show_info = false; bool list_ws = false; bool adopt_focus = false, adopt_wait = false; int adopt_pid = -1; const char *adopt_ws = NULL; double adopt_timeout = 20.0; int adopt_beside = -1; bool adopt_no_assign = false; const char *adopt_edge = NULL; const char *sets[64]; int nsets = 0; for (int i = 1; i < argc; ++i) { const char *a = argv[i]; if (!strcmp(a, "-h") || !strcmp(a, "--help")) { usage(); return 0; } else if (!strcmp(a, "-v") || !strcmp(a, "--version")) { printf("swov %s (build %s)\n", SWOV_VERSION, SWOV_BUILD); return 0; } else if ((!strcmp(a, "-g") || !strcmp(a, "--go")) && i + 1 < argc) go_name = argv[++i]; else if (!strcmp(a, "-b") || !strcmp(a, "--back")) go_back = true; else if (!strcmp(a, "--workspaces")) list_ws = true; else if (!strcmp(a, "--backdrop")) BACKDROP = true; else if (!strcmp(a, "--backdrop-debug")) { BACKDROP = true; BACKDROP_LOG = true; } else if (!strcmp(a, "--adopt") && i + 2 < argc) { adopt_pid = atoi(argv[++i]); adopt_ws = argv[++i]; } 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]; else if (!strcmp(a, "--adopt-wait")) adopt_wait = true; else if (!strcmp(a, "--adopt-timeout") && i + 1 < argc) adopt_timeout = atof(argv[++i]); else if (!strcmp(a, "--usage")) show_usage_stats = true; else if (!strcmp(a, "--info")) show_info = true; else if ((!strcmp(a, "-c") || !strcmp(a, "--config")) && i + 1 < argc) cfg_path = expand_tilde(argv[++i]); else if (!strcmp(a, "-n") || !strcmp(a, "--no-config")) use_cfg = false; else if (!strcmp(a, "--shot") && i + 1 < argc) SHOT_PATH = argv[++i]; else if (!strcmp(a, "--mouse") && i + 1 < argc) { float fx = 0.0f, fy = 0.0f; if (sscanf(argv[++i], "%f,%f", &fx, &fy) == 2) { SHOT_MX = fx; SHOT_MY = fy; } } else if ((!strcmp(a, "-s") || !strcmp(a, "--set")) && i + 1 < argc && nsets < 64) sets[nsets++] = argv[++i]; else if (strncmp(a, "--", 2) == 0 && strchr(a, '=')) { if (nsets < 64) sets[nsets++] = a + 2; /* --ui_scale=1.2 */ } else if (a[0] != '-' && strchr(a, '=')) { if (nsets < 64) sets[nsets++] = a; /* ui_scale=1.2 */ } else { fprintf(stderr, "swov: unknown argument '%s'\n", a); usage(); return 2; } } if (use_cfg) { SHARED_LOADED = sw_shared_apply("swov", cfg_set_shared, &C) != 0; bool named = cfg_path != NULL; if (!cfg_path) cfg_path = default_config_path(); if (cfg_path) { str_set(CFG_PATH, sizeof(CFG_PATH), cfg_path); CFG_LOADED = cfg_load_file(&C, cfg_path); if (!CFG_LOADED && named) fprintf(stderr, "swov: no config at %s\n", cfg_path); } } free(cfg_path); for (int i = 0; i < nsets; ++i) { /* -s key=value */ char *copy = xstrdup(sets[i]); char *eq = strchr(copy, '='); if (eq) { *eq = 0; cfg_set(&C, str_trim(copy), str_trim(eq + 1)); } free(copy); } if (show_info) { char exe[PATH_MAX] = {0}; ssize_t n = readlink("/proc/self/exe", exe, sizeof(exe) - 1); if (n > 0) exe[n] = 0; char *upath = usage_path(); usage_load(); printf("swov %s (build %s)\n\n", SWOV_VERSION, SWOV_BUILD); printf("binary %s\n", exe[0] ? exe : "(unknown)"); printf("shared config %s%s\n", sw_shared_path() ? sw_shared_path() : "(none)", SHARED_LOADED ? " [loaded]" : " [not present]"); printf("config %s%s\n", CFG_PATH[0] ? CFG_PATH : "(none)", CFG_PATH[0] ? (CFG_LOADED ? " [loaded]" : " [missing, defaults in use]") : ""); printf("usage file %s\n", upath ? upath : "(none)"); printf("sway socket %s\n", getenv("SWAYSOCK") ? getenv("SWAYSOCK") : "(unset)"); char *theme = icon_theme_name(); printf("icon theme %s\n", theme); free(theme); fontconfig_resolve(C.font); printf("font %s\n", FC_REGULAR ? FC_REGULAR : (C.font[0] ? C.font : "(none)")); printf("font bold %s\n", FC_BOLD ? FC_BOLD : "(synthetic)"); free(FC_REGULAR); free(FC_BOLD); FC_REGULAR = FC_BOLD = NULL; printf("\nrecorded usage\n"); if (USAGE_CUR[0]) printf(" on %s (%s), for %.0fs\n", USAGE_CUR, USAGE_CUR_OUT[0] ? USAGE_CUR_OUT : "?", usage_pending()); if (NUSAGE == 0) printf(" (nothing yet)\n"); for (int i = 0; i < NUSAGE; ++i) { double secs = USAGE[i].secs + (strcmp(USAGE[i].name, USAGE_CUR) == 0 ? usage_pending() : 0.0); printf(" %-16s %-10s %7.0fs\n", USAGE[i].name, USAGE[i].output[0] ? USAGE[i].output : "-", secs); } printf("\nsettings\n"); printf(" ssaa %d icons %d icon_px %d ui_scale %.2f\n", C.ssaa, C.icons, C.icon_px, (double)C.ui_scale); printf(" ws_px %d label_px %d title_px %d hint_px %d\n", C.ws_px, C.label_px, C.title_px, C.hint_px); printf(" cols %d rows %d margin %.0f gap %.0f pad %.0f\n", C.cols, C.rows, (double)C.margin, (double)C.gap, (double)C.pad); printf(" win_gap %.0f screen_pad %.0f radius %.0f border %.0f\n", (double)C.win_gap, (double)C.screen_pad, (double)C.radius, (double)C.border); printf(" show_empty %d all_outputs %d start_selection %d\n", C.show_empty, C.all_outputs, C.start_selection); printf(" header_pos %s hints_pos %s\n", C.header_pos, C.hints_pos); printf(" blur %d (backdrop only) cpu %d (min %g, full %g)\n", C.blur, C.cpu, (double)C.cpu_min, (double)C.cpu_full); printf(" back %s\n", C.back_scope == 1 ? "output" : C.back_scope == 2 ? "sway" : "global"); printf(" track %d usage_dots %d dot_count %d dot_px %.0f\n", C.track, C.usage_dots, C.dot_count, (double)C.dot_px); printf(" anim_ms %.0f shadow %d float_alpha %.2f vsync %d\n", (double)C.anim_ms, C.shadow, (double)C.float_alpha, C.vsync); printf("\ncolours\n"); struct { const char *k; SDL_FColor c; } cols[] = { { "bg", C.bg }, { "tile", C.tile }, { "tile_sel", C.tile_sel }, { "mini_bg", C.mini_bg }, { "card", C.card }, { "card_hover", C.card_hover }, { "card_focus", C.card_focus }, { "hl", C.hl }, { "current", C.current }, { "match", C.match }, { "text", C.text }, { "subtext", C.subtext }, { "dim", C.dim }, { "accent", C.accent }, { "hint", C.hint }, { "hltext", C.hltext }, { "urgent", C.urgent }, { "outline", C.outline } }; for (size_t i = 0; i < SDL_arraysize(cols); ++i) printf(" %-12s %02x%02x%02x%02x\n", cols[i].k, (unsigned)(cols[i].c.r * 255.0f + 0.5f), (unsigned)(cols[i].c.g * 255.0f + 0.5f), (unsigned)(cols[i].c.b * 255.0f + 0.5f), (unsigned)(cols[i].c.a * 255.0f + 0.5f)); free(upath); return 0; } if (show_usage_stats) { char *path = usage_path(); usage_load(); printf("usage file: %s\n", path ? path : "(none)"); if (USAGE_CUR[0]) printf("on: %s on %s, for %.0fs\n", USAGE_CUR, USAGE_CUR_OUT[0] ? USAGE_CUR_OUT : "?", usage_pending()); else printf("on: nothing recorded yet\n"); for (int i = 0; i < NUSAGE; ++i) { double secs = USAGE[i].secs + (strcmp(USAGE[i].name, USAGE_CUR) == 0 ? usage_pending() : 0.0); int mins = (int)(secs / 60.0); double ago = USAGE[i].last > 0.0 ? now_secs() - USAGE[i].last : -1.0; printf(" %-16s %-10s %6.0fs (%dh %02dm)", USAGE[i].name, USAGE[i].output[0] ? USAGE[i].output : "-", secs, mins / 60, mins % 60); if (ago >= 0.0) printf(" last seen %.0fs ago", ago); printf("\n"); } free(path); return 0; } if (adopt_pid > 0 && !adopt_wait) { /* Step into the background before touching the socket: whoever called * us wants to carry on, and the window may be seconds away. */ pid_t p = fork(); if (p < 0) die("fork: %s", strerror(errno)); if (p > 0) return 0; setsid(); int devnull = open("/dev/null", O_RDWR); if (devnull >= 0) { dup2(devnull, 0); dup2(devnull, 1); if (devnull > 2) close(devnull); } } if (TIMING) T0 = now_secs(); sway_fd = sway_connect(); if (sway_fd < 0) die("cannot reach sway (is SWAYSOCK set?)"); mark("sway socket"); if (list_ws) { /* the model as swov sees it, which is the only way to tell whether a workspace on another screen brought its windows with it */ if (model_reload()) { for (int i = 0; i < NWS; ++i) fprintf(stderr, "swov: %-8s %-8s %2d window(s)\n", WSS[i].name, WSS[i].output, WSS[i].count); } int rc = print_workspaces(); close(sway_fd); return rc; } if (adopt_pid > 0 && adopt_ws) { int rc = adopt_window(adopt_pid, adopt_ws, adopt_timeout, adopt_focus, adopt_beside, adopt_edge, adopt_no_assign); close(sway_fd); return rc; } /* Switching does not need a window, a renderer or a font: connect, say it, * leave. This is the path a keybinding should use. */ if (go_back || go_name) { usage_sync(); /* the user may have moved without us */ bool ok; if (go_back) { /* the last workspace we used on this monitor, not sway's global * back_and_forth, which would jump to the other screen */ char here[64] = {0}, out[64] = {0}, target[64] = {0}; int here_num = -1, target_num = -1; focused_workspace(here, sizeof(here), out, sizeof(out), &here_num); if (C.track && C.back_scope != 2 && last_workspace_here(C.back_scope == 1 ? out : NULL, here, target, sizeof(target), &target_num)) { /* By number where there is one: the workspace may have been * renamed since we saw it, and asking sway for a name that no * longer exists makes it create a second workspace with the * same number. */ if (target_num >= 0) { ok = sway_cmd("workspace number %d", target_num); } else { char *e = escape_arg(target); ok = sway_cmd("workspace --no-auto-back-and-forth \"%s\"", e); free(e); } } else { ok = sway_cmd("workspace back_and_forth"); } } else if (str_all_digits(go_name)) { ok = sway_cmd("workspace number %d", atoi(go_name)); } else { char *e = escape_arg(go_name); /* a name, "code" or "3:code" */ ok = sway_cmd("workspace --no-auto-back-and-forth \"%s\"", e); free(e); } if (ok && C.track) { /* stamp the new workspace */ char now[64], nowout[64]; int nownum = -1; focused_workspace(now, sizeof(now), nowout, sizeof(nowout), &nownum); usage_switch(now, nowout, nownum); } close(sway_fd); return ok ? 0 : 1; } /* the focused output is needed before the window is created */ JV *wsr = sway_query(IPC_GET_WORKSPACES); if (wsr && wsr->type == J_ARR) for (int i = 0; i < wsr->count; ++i) if (jbool(wsr->items[i], "focused", false)) str_set(FOCUSED_OUTPUT, sizeof(FOCUSED_OUTPUT), jstr(wsr->items[i], "output", "")); jfree(wsr); /* A different app id, because a backdrop must not take the keyboard from * the window in front of it: * for_window [app_id="swov-backdrop"] no_focus */ SDL_SetHint(SDL_HINT_APP_ID, BACKDROP ? APP_ID "-backdrop" : APP_ID); SDL_SetAppMetadata(APP_ID, SWOV_VERSION, "org.swov.overview"); if (!SDL_Init(SDL_INIT_VIDEO)) die("SDL_Init: %s", SDL_GetError()); mark("SDL_Init"); if (!TTF_Init()) die("TTF_Init: %s", SDL_GetError()); SDL_Rect bounds; pick_bounds(&bounds); SDL_WindowFlags flags = SDL_WINDOW_BORDERLESS | SDL_WINDOW_TRANSPARENT | SDL_WINDOW_ALWAYS_ON_TOP | SDL_WINDOW_HIGH_PIXEL_DENSITY; WIN_HANDLE = SDL_CreateWindow(BACKDROP ? APP_ID "-backdrop" : APP_ID, bounds.w, bounds.h, flags); if (!WIN_HANDLE) die("SDL_CreateWindow: %s", SDL_GetError()); SDL_SetWindowPosition(WIN_HANDLE, bounds.x, bounds.y); REN = SDL_CreateRenderer(WIN_HANDLE, NULL); if (!REN) die("SDL_CreateRenderer: %s", SDL_GetError()); mark("window+renderer"); SDL_SetRenderDrawBlendMode(REN, SDL_BLENDMODE_BLEND); SDL_SetRenderVSync(REN, C.vsync ? 1 : 0); if (!create_target()) die("cannot size the window"); mark("render target"); load_fonts(); CUR_ARROW = SDL_CreateSystemCursor(SDL_SYSTEM_CURSOR_DEFAULT); CUR_HAND = SDL_CreateSystemCursor(SDL_SYSTEM_CURSOR_POINTER); usage_sync(); sway_subscribe_events(); if (C.output[0]) { /* asked to look at another screen */ str_set(FOCUSED_OUTPUT, sizeof(FOCUSED_OUTPUT), C.output); PINNED_OUTPUT = true; } if (!model_reload()) die("could not read the sway tree"); if (C.output[0] && HOME_OUTPUT[0] && !strcmp(C.output, HOME_OUTPUT)) PINNED_OUTPUT = false; mark("sway tree"); cpu_load(); /* whatever swbr last measured */ select_current_workspace(); sel_active = BACKDROP ? false : (C.start_selection != 0); if (!BACKDROP && C.start_selection == 2 && NWS > 0) WSS[sel_ws].sel = ws_first_visible(&WSS[sel_ws]); layout(); apply_filter(); rebuild_chrome(); if (SHOT_MX >= 0.0f) { /* pretend the pointer is there */ hov_win = hit_test(SHOT_MX * MOUSE_SCALE, SHOT_MY * MOUSE_SCALE, &hov_ws); } Uint64 last_reload = 0; bool pending_reload = false; if (!BACKDROP) SDL_StartTextInput(WIN_HANDLE); SDL_RaiseWindow(WIN_HANDLE); if (BACKDROP && SHOT_PATH) BACKDROP_A = 1.0f; /* a still has nothing to fade */ present(); if (BACKDROP) { /* the window is up; whoever spawned us can put itself back on top */ printf("ready\n"); fflush(stdout); } Uint64 last_tick = SDL_GetTicks(); while (running) { SDL_Event e; if (anim_running()) dirty = true; cpu_poll(); if (drag_active) { map_dwell_tick(); if (SNAP_WIN >= 0) { /* the hold is running: keep drawing */ float mx, my; SDL_GetMouseState(&mx, &my); drag_update_target(mx * MOUSE_SCALE, my * MOUSE_SCALE); dirty = true; } } if (RAISE_LEFT > 0) backdrop_raise_tick(); if (BACKDROP || SERVING) { backdrop_poll(); Uint64 now = SDL_GetTicks(); if (BACKDROP) backdrop_step((float)(now - last_tick)); last_tick = now; if (!running) break; } 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 */ } /* Sway acknowledges a command before the layout transaction has * committed, so the tree is only trustworthy once the events for it * have arrived. This also keeps the overview live. A window that * rewrites its title in a loop would otherwise have us rebuilding the * model every few milliseconds, so events are coalesced. */ if (running && !drag_active && sway_events_pending()) pending_reload = true; if (running && pending_reload) { Uint64 now = SDL_GetTicks(); if (now - last_reload >= 120) { pending_reload = false; last_reload = now; reload_model(); dirty = true; } } if (running && dirty) { present(); dirty = false; } } /* cleanup */ tex_free(&T_HEADER); tex_free(&T_HINTS); tex_free(&T_QUERY); model_free(); icons_free(); if (CUR_ARROW) SDL_DestroyCursor(CUR_ARROW); if (CUR_HAND) SDL_DestroyCursor(CUR_HAND); if (BLUR_HALF) SDL_DestroyTexture(BLUR_HALF); if (BLUR_SMALL) SDL_DestroyTexture(BLUR_SMALL); if (TARGET) SDL_DestroyTexture(TARGET); if (F_BADGE) TTF_CloseFont(F_BADGE); if (F_LABEL) TTF_CloseFont(F_LABEL); if (F_TITLE) TTF_CloseFont(F_TITLE); if (F_HINT) TTF_CloseFont(F_HINT); 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); return 0; }