SWOv git · main
SWOv - window overview for sway
C 91.5% Markdown 7.6%git clone https://git.christianimmanuel.de/sway/SWOv.gitwget https://git.christianimmanuel.de/sway/SWOv/archive/SWOv.tar.gzswov.c raw
/* 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 <SDL3/SDL.h>
#include <SDL3_image/SDL_image.h>
#include <SDL3_ttf/SDL_ttf.h>
#include <ctype.h>
#include <dirent.h>
#include <errno.h>
#include <signal.h>
#include <stdarg.h>
#include <time.h>
#include <stdbool.h>
#include <stdint.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <strings.h>
#include <fcntl.h>
#include <poll.h>
#include <sys/file.h>
#ifdef __linux__
#include <sys/prctl.h>
#endif
#include <sys/socket.h>
#include <sys/stat.h>
#include <sys/un.h>
#include <limits.h>
#include <sys/wait.h>
#include <unistd.h>
#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 "> <epoch> <name>" line saying where we are, then "<seconds>
* <name>" 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;
}