Nimbin[12]?Sway / SWOv / commits / 4247f49

SWOv git · main

SWOv - window overview for sway

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

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

Fix toggle function

Christian Immanuel · 2026-08-26 09:31 · 4247f495ade22e95a8c271532181f53f03516b8a

 README.md      |  14 ++-
 config.example |   7 +-
 swov.c         | 376 +++++++++++++++++++++++++++++++++++++++++++--------------
 3 files changed, 304 insertions(+), 93 deletions(-)

diff --git a/README.md b/README.md
index 8bd224b..4fce1ef 100644
--- a/README.md
+++ b/README.md
@@ -86,12 +86,21 @@ workspace from a single window.
 ```sh
 swov -g 3        # switch to workspace 3 and exit
 swov -g 3:code   # by name works too
-swov -b          # switch to the workspace you came from
+swov -b          # back to the last workspace you used *on this monitor*
 ```
 
 Both talk to the IPC socket and quit — no window, no font, about 5 ms. Good for
 keybindings.
 
+`-b` goes to the workspace activated most recently that is not the current one,
+on the monitor you are on. It switches *by number*, so a workspace renamed since
+is still the same workspace, and every swov run first checks where sway actually
+is — workspaces you reached with a sway keybinding count too. Press it twice and
+you are back where you started.
+
+sway's own `back_and_forth` is only the fallback when there is no history yet:
+it is global, so with two screens it throws focus onto the other monitor.
+
 ## Workspace usage
 
 Every switch goes through swov, so it stamps the time as it goes: the workspace
@@ -148,6 +157,9 @@ swov --shot /tmp/o.png     # one frame to a PNG, for tuning colours
 - Reordering workspaces renames them, which is all sway offers. Swaps go through
   a temporary name.
 - Scratchpad windows are not shown.
+- The overlay opens on the focused output, found by matching sway's output
+  geometry against the display list — SDL often reports a monitor model where
+  sway reports the connector, so names alone are not enough.
 - Tabbed and stacked containers are drawn the way sway draws them: a tab strip
   across the top, the visible window's contents below. Each tab is clickable.
 - Only the name plate of a floating or fullscreen window takes the mouse; clicks
diff --git a/config.example b/config.example
index 6c2a686..aaaa9b2 100644
--- a/config.example
+++ b/config.example
@@ -12,9 +12,10 @@ icons=1            # show .desktop icons (PNG/SVG/XPM); 0 = text only
 icon_px=40         # icon edge length; shrinks by itself on small cards
 shadow=1           # drop shadows under tiles and floating windows
 shadow_layers=3    # fewer layers = cheaper, harder edged
-track=1            # remember how long each workspace is used. swov performs
-                   # every switch itself, so it simply stamps the time on each
-                   # one — no background process. 0 = do not record.
+track=1            # remember how long each workspace is used, and which one
+                   # you were on per monitor (that is what -b goes back to).
+                   # swov performs every switch itself, so it stamps the time
+                   # as it goes — no background process. 0 = do not record.
 usage_dots=1       # dot scale down the left edge of a tile: how much that
                    # workspace is used, relative to the busiest one. Fills from
                    # the bottom.                              alias: usage_bar
diff --git a/swov.c b/swov.c
index 5037f19..40fe9da 100644
--- a/swov.c
+++ b/swov.c
@@ -41,7 +41,7 @@
 #include <unistd.h>
 
 #define APP_ID          "swov"
-#define SWOV_VERSION    "1.0"
+#define SWOV_VERSION    "1.2"
 #define MAX_WINDOWS     512
 #define MAX_WORKSPACES  64
 #define MAX_DESKTOPS    4096
@@ -96,6 +96,15 @@ static char *fmt_alloc(const char *fmt, ...)
     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)
@@ -262,8 +271,8 @@ static Cfg cfg_defaults(void)
     c.dot_px     = 5.0f;
     c.anim_ms = 160.0f;
     c.start_selection = 1;
-    snprintf(c.header_pos, sizeof(c.header_pos), "%s", "top-right");
-    snprintf(c.hints_pos,  sizeof(c.hints_pos),  "%s", "bottom-center");
+    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;
@@ -320,8 +329,8 @@ static void cfg_set(Cfg *c, const char *k, const char *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"))       snprintf(c->font, sizeof(c->font), "%s", v);
-    else if (key_is(k,"font_bold"))  snprintf(c->font_bold, sizeof(c->font_bold), "%s", 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);
@@ -346,8 +355,8 @@ static void cfg_set(Cfg *c, const char *k, const char *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")) snprintf(c->header_pos, sizeof(c->header_pos), "%s", v);
-    else if (key_is(k,"hints_pos"))  snprintf(c->hints_pos,  sizeof(c->hints_pos),  "%s", v);
+    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,"show_header"))   c->show_header = atoi(v) != 0;
@@ -674,6 +683,7 @@ static bool jbool(const JV *o, const char *key, bool def)
 /* -------------------------------------------------------------- sway ipc */
 
 enum {
+    IPC_GET_OUTPUTS    = 3,
     IPC_RUN_COMMAND    = 0,
     IPC_GET_WORKSPACES = 1,
     IPC_GET_TREE       = 4,
@@ -695,7 +705,7 @@ static int sway_connect(void)
     struct sockaddr_un addr;
     memset(&addr, 0, sizeof(addr));
     addr.sun_family = AF_UNIX;
-    snprintf(addr.sun_path, sizeof(addr.sun_path), "%s", path);
+    str_set(addr.sun_path, sizeof(addr.sun_path), path);
     if (connect(fd, (struct sockaddr *)&addr, sizeof(addr)) < 0) {
         close(fd);
         return -1;
@@ -872,12 +882,19 @@ static bool sway_events_pending(void)
  * <name>" per workspace.
  */
 
-typedef struct { char name[64]; double secs; } Usage;
+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)
@@ -902,8 +919,9 @@ static Usage *usage_find(const char *name, bool create)
     if (!create || NUSAGE >= (int)SDL_arraysize(USAGE)) return NULL;
 
     Usage *u = &USAGE[NUSAGE++];
-    snprintf(u->name, sizeof(u->name), "%s", name);
-    u->secs = 0.0;
+    memset(u, 0, sizeof(*u));
+    u->num = -1;
+    str_set(u->name, sizeof(u->name), name);
     return u;
 }
 
@@ -918,16 +936,41 @@ static void usage_load(void)
     if (!f) return;
 
     USAGE_CUR[0] = 0;
+    USAGE_CUR_OUT[0] = 0;
     USAGE_SINCE = 0.0;
 
-    char line[256];
+    char line[320];
     while (fgets(line, sizeof(line), f) && NUSAGE < (int)SDL_arraysize(USAGE)) {
-        double v = 0.0;
-        char name[64] = {0};
+        double v = 0.0, last = 0.0;
+        char name[64] = {0}, out[64] = {0};
+
         if (line[0] == '>') {
-            if (sscanf(line + 1, "%lf %63[^\n]", &v, name) == 2) {
+            if (sscanf(line + 1, "%lf %63s %63[^\n]", &v, out, name) == 3) {
                 USAGE_SINCE = v;
-                snprintf(USAGE_CUR, sizeof(USAGE_CUR), "%s", name);
+                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);
@@ -963,9 +1006,13 @@ static void usage_save(void)
     char *tmp = fmt_alloc("%s.tmp", path);
     FILE *f = fopen(tmp, "w");
     if (f) {
-        if (USAGE_CUR[0]) fprintf(f, "> %.0f %s\n", USAGE_SINCE, USAGE_CUR);
+        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) fprintf(f, "%.0f %s\n", USAGE[i].secs, USAGE[i].name);
+            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);
     }
@@ -975,30 +1022,73 @@ static void usage_save(void)
 
 /* 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)
+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 (pending > 0.0 && USAGE_CUR[0]) {
+    if (USAGE_CUR[0]) {
         Usage *u = usage_find(USAGE_CUR, true);
-        if (u) u->secs += pending;
+        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);
+        }
     }
-    snprintf(USAGE_CUR, sizeof(USAGE_CUR), "%s", to);
-    USAGE_SINCE = now_secs();
+
+    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, on this monitor. sway's own
+ * back_and_forth is global: with two screens it happily throws focus onto the
+ * other one, which is never what "go back" means while working on this one. */
+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 *out, size_t cap)
+static void focused_workspace(char *name, size_t ncap, char *output, size_t ocap, int *num)
 {
-    out[0] = 0;
+    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))
-                snprintf(out, cap, "%s", jstr(r->items[i], "name", ""));
+            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);
 }
 
@@ -1389,7 +1479,7 @@ static void proc_app_name(int pid, char *out, size_t cap)
         if (n) {
             const char *arg0 = base_name(buf);
             if (*arg0 && !is_interpreter(arg0)) {
-                snprintf(out, cap, "%.*s", (int)cap - 1, arg0);
+                str_set(out, cap, arg0);
                 return;
             }
             /* an interpreter: the script or the app directory names it */
@@ -1399,7 +1489,7 @@ static void proc_app_name(int pid, char *out, size_t cap)
                 if (*arg != '-') {
                     const char *b = base_name(arg);
                     if (*b && !is_interpreter(b) && !strchr(b, '=')) {
-                        snprintf(out, cap, "%.*s", (int)cap - 1, b);
+                        str_set(out, cap, b);
                         break;
                     }
                 }
@@ -1415,7 +1505,7 @@ static void proc_app_name(int pid, char *out, size_t cap)
     char comm[128] = {0};
     if (fgets(comm, sizeof(comm), f)) {
         str_trim(comm);
-        if (comm[0] && !is_interpreter(comm)) snprintf(out, cap, "%.*s", (int)cap - 1, comm);
+        if (comm[0] && !is_interpreter(comm)) str_set(out, cap, comm);
     }
     fclose(f);
 }
@@ -1650,7 +1740,7 @@ static SDL_Texture *icon_for_app(const char *app_id, int want_px)
     if (!tex) tex = icon_make_letter(key, want_px);
 
     if (g_icon_count < (int)SDL_arraysize(g_icons)) {
-        snprintf(g_icons[g_icon_count].key, sizeof(g_icons[0].key), "%s", key);
+        str_set(g_icons[g_icon_count].key, sizeof(g_icons[0].key), key);
         g_icons[g_icon_count].tex = tex;
         g_icon_count++;
     }
@@ -1775,11 +1865,11 @@ static void collect_views(const JV *node, Ws *ws, bool floating)
             const JV *wp = jget(node, "window_properties");
             app = wp ? jstr(wp, "class", NULL) : NULL;
         }
-        snprintf(w->app_id, sizeof(w->app_id), "%s", app ? app : "unknown");
+        str_set(w->app_id, sizeof(w->app_id), app ? app : "unknown");
         memcpy(w->app_key, w->app_id, sizeof(w->app_key));
-        snprintf(w->title,  sizeof(w->title),  "%s", jstr(node, "name", ""));
-        snprintf(w->ws_name, sizeof(w->ws_name), "%s", ws->name);
-        snprintf(w->output,  sizeof(w->output),  "%s", ws->output);
+        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);
@@ -1837,13 +1927,13 @@ static void walk_outputs(const JV *node)
 
         Ws *ws = &WSS[NWS];
         memset(ws, 0, sizeof(*ws));
-        snprintf(ws->name, sizeof(ws->name), "%s", name);
-        snprintf(ws->output, sizeof(ws->output), "%s", CUR_OUTPUT);
+        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) snprintf(ws->label, sizeof(ws->label), "%s", colon + 1);
-        else if (!str_all_digits(name)) snprintf(ws->label, sizeof(ws->label), "%s", name);
+        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);
 
@@ -1880,7 +1970,7 @@ static void walk_outputs(const JV *node)
         const char *name = jstr(node, "name", "");
         if (strcmp(name, "__i3") == 0) return;
         if (!C.all_outputs && FOCUSED_OUTPUT[0] && strcmp(name, FOCUSED_OUTPUT) != 0) return;
-        snprintf(CUR_OUTPUT, sizeof(CUR_OUTPUT), "%s", name);
+        str_set(CUR_OUTPUT, sizeof(CUR_OUTPUT), name);
     }
 
     const JV *nodes = jget(node, "nodes");
@@ -1908,12 +1998,12 @@ static bool model_reload(void)
     if (wsr && wsr->type == J_ARR) {
         for (int i = 0; i < wsr->count; ++i)
             if (jbool(wsr->items[i], "focused", false))
-                snprintf(focused, sizeof(focused), "%s", jstr(wsr->items[i], "output", ""));
+                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]) snprintf(FOCUSED_OUTPUT, sizeof(FOCUSED_OUTPUT), "%s", focused);
+    if (focused[0]) str_set(FOCUSED_OUTPUT, sizeof(FOCUSED_OUTPUT), focused);
 
     JV *tree = sway_query(IPC_GET_TREE);
     if (!tree) { jfree(wsr); return false; }
@@ -1927,7 +2017,7 @@ static bool model_reload(void)
             for (int j = 0; j < wsr->count; ++j) {
                 const JV *o = wsr->items[j];
                 if (strcmp(jstr(o, "name", ""), WSS[i].name) != 0) continue;
-                snprintf(WSS[i].output, sizeof(WSS[i].output), "%s", jstr(o, "output", ""));
+                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);
@@ -1981,7 +2071,7 @@ static bool model_reload(void)
         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]) snprintf(w->app_key, sizeof(w->app_key), "%s", 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));
     }
@@ -2350,7 +2440,7 @@ static const char *card_label_name(const Win *w)
     /* last resort: the executable name, without its extension */
     if (!app_id_is_useless(w->app_key)) {
         static char buf[128];
-        snprintf(buf, sizeof(buf), "%s", w->app_key);
+        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 ||
@@ -2369,27 +2459,26 @@ static void build_texts(void)
 
         char num[16];
         if (ws->num >= 0) snprintf(num, sizeof(num), "%d", ws->num);
-        else              snprintf(num, sizeof(num), "%s", ws->name);
+        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};
 
-        char sub[192];
-        if (ws->count == 0)          snprintf(sub, sizeof(sub), "empty");
-        else if (ws->count == 1)     snprintf(sub, sizeof(sub), "1 window");
-        else                         snprintf(sub, sizeof(sub), "%d windows", ws->count);
+        /* "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);
-            char tmp[224];
-            if (mins < 60) snprintf(tmp, sizeof(tmp), "%s \xc2\xb7 %dm", sub, mins);
-            else snprintf(tmp, sizeof(tmp), "%s \xc2\xb7 %dh %dm", sub, mins / 60, mins % 60);
-            snprintf(sub, sizeof(sub), "%.191s", tmp);
-        }
-        if (C.all_outputs && ws->output[0]) {
-            char tmp[256];
-            snprintf(tmp, sizeof(tmp), "%s \xc2\xb7 %s", ws->output, sub);
-            snprintf(sub, sizeof(sub), "%.191s", tmp);
+            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);
     }
 
@@ -2805,6 +2894,25 @@ static char *escape_arg(const char *s)
     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
@@ -3108,7 +3216,7 @@ static void rebuild_chrome(void)
 
     if (C.show_header) {
         char buf[256];
-        snprintf(buf, sizeof(buf), "%s   %d window%s on %d workspace%s",
+        snprintf(buf, sizeof(buf), "%.63s   %d window%s on %d workspace%s",
                  FOCUSED_OUTPUT[0] ? FOCUSED_OUTPUT : "sway",
                  NWIN, NWIN == 1 ? "" : "s", NWS, NWS == 1 ? "" : "s");
         T_HEADER = text_make(F_HINT, buf);
@@ -3140,7 +3248,7 @@ static void rebuild_chrome(void)
         T_QUERY = text_make(F_HINT, buf);
     } else if (query_active()) {
         char buf[192];
-        snprintf(buf, sizeof(buf), "%s  %s\xe2\x96\x8f",
+        snprintf(buf, sizeof(buf), "%s  %.127s\xe2\x96\x8f",
                  filtering ? "filter:" : "search:", query);
         T_QUERY = text_make(F_HINT, buf);
     }
@@ -3402,7 +3510,7 @@ static void draw_workspace(int idx)
         stroke_round_rect(box, box.h * 0.3f, SDL_max(1.0f, 1.5f * SC), C.hl);
 
         char buf[80];
-        snprintf(buf, sizeof(buf), "%s\xe2\x96\x8f", edit_buf);
+        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);
@@ -3593,7 +3701,7 @@ static void render(void)
             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[8];
+            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));
@@ -3613,7 +3721,7 @@ static void render(void)
             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[8];
+            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));
@@ -3678,7 +3786,7 @@ static void reload_model(void)
     int keep_con = -1;
     char keep_ws[64] = {0};
     if (NWS > 0) {
-        snprintf(keep_ws, sizeof(keep_ws), "%s", WSS[sel_ws].name);
+        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;
     }
@@ -3721,7 +3829,7 @@ static void act_goto_workspace(const Ws *ws)
     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);
+    if (C.track) usage_switch(ws->name, ws->output, ws->num);
     running = false;
 }
 
@@ -3904,6 +4012,7 @@ static void usage(void)
 "  -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"
@@ -3977,7 +4086,7 @@ static void goto_workspace_number(int num)
     if (C.track) {
         char name[16];
         snprintf(name, sizeof(name), "%d", num);
-        usage_switch(name);
+        usage_switch(name, FOCUSED_OUTPUT, num);
     }
     running = false;
 }
@@ -4181,7 +4290,7 @@ static void begin_edit(int idx)
 {
     editing = true;
     edit_ws = idx;
-    snprintf(edit_buf, sizeof(edit_buf), "%s", WSS[idx].label);
+    str_set(edit_buf, sizeof(edit_buf), WSS[idx].label);
     edit_len = (int)strlen(edit_buf);
     swallow_next_text = false;
 }
@@ -4194,7 +4303,16 @@ static void end_edit(bool commit)
                                                : 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 (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;
@@ -4429,7 +4547,9 @@ static void present(void)
     SDL_RenderPresent(REN);
 }
 
-/* place the overlay on the output sway is using, fall back to the pointer */
+/* 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 };
@@ -4439,7 +4559,39 @@ static void pick_bounds(SDL_Rect *out)
     if (!ids || count <= 0) { SDL_free(ids); return; }
 
     SDL_DisplayID chosen = 0;
-    if (FOCUSED_OUTPUT[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; }
@@ -4476,6 +4628,10 @@ int main(int argc, char **argv)
     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 (built %s)\n", SWOV_VERSION, __DATE__);
+            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, "--usage")) show_usage_stats = true;
@@ -4503,7 +4659,7 @@ int main(int argc, char **argv)
         bool named = cfg_path != NULL;
         if (!cfg_path) cfg_path = default_config_path();
         if (cfg_path) {
-            snprintf(CFG_PATH, sizeof(CFG_PATH), "%s", 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);
         }
@@ -4525,7 +4681,7 @@ int main(int argc, char **argv)
         char *upath = usage_path();
         usage_load();
 
-        printf("swov %s\n\n", SWOV_VERSION);
+        printf("swov %s (built %s)\n\n", SWOV_VERSION, __DATE__);
         printf("binary        %s\n", exe[0] ? exe : "(unknown)");
         printf("config        %s%s\n", CFG_PATH[0] ? CFG_PATH : "(none)",
                CFG_PATH[0] ? (CFG_LOADED ? "  [loaded]" : "  [missing, defaults in use]") : "");
@@ -4545,12 +4701,14 @@ int main(int argc, char **argv)
 
         printf("\nrecorded usage\n");
         if (USAGE_CUR[0])
-            printf("  on %s, for %.0fs\n", USAGE_CUR, usage_pending());
+            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("  %-20s %7.0fs\n", USAGE[i].name, secs);
+            printf("  %-16s %-10s %7.0fs\n", USAGE[i].name,
+                   USAGE[i].output[0] ? USAGE[i].output : "-", secs);
         }
 
         printf("\nsettings\n");
@@ -4592,13 +4750,19 @@ int main(int argc, char **argv)
         char *path = usage_path();
         usage_load();
         printf("usage file: %s\n", path ? path : "(none)");
-        if (USAGE_CUR[0]) printf("on:         %s, for %.0fs\n", USAGE_CUR, usage_pending());
+        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);
-            printf("  %-20s %6.0fs  (%dh %02dm)\n", USAGE[i].name, secs, mins / 60, mins % 60);
+            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;
@@ -4610,9 +4774,30 @@ int main(int argc, char **argv)
     /* 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) {
-            ok = sway_cmd("workspace back_and_forth");
+            /* 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 && last_workspace_here(out, 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 {
@@ -4621,9 +4806,10 @@ int main(int argc, char **argv)
             free(e);
         }
         if (ok && C.track) {                       /* stamp the new workspace */
-            char now[64];
-            focused_workspace(now, sizeof(now));
-            usage_switch(now);
+            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;
@@ -4634,8 +4820,7 @@ int main(int argc, char **argv)
     if (wsr && wsr->type == J_ARR)
         for (int i = 0; i < wsr->count; ++i)
             if (jbool(wsr->items[i], "focused", false))
-                snprintf(FOCUSED_OUTPUT, sizeof(FOCUSED_OUTPUT), "%s",
-                         jstr(wsr->items[i], "output", ""));
+                str_set(FOCUSED_OUTPUT, sizeof(FOCUSED_OUTPUT), jstr(wsr->items[i], "output", ""));
     jfree(wsr);
 
     SDL_SetHint(SDL_HINT_APP_ID, APP_ID);
@@ -4664,6 +4849,7 @@ int main(int argc, char **argv)
     CUR_ARROW = SDL_CreateSystemCursor(SDL_SYSTEM_CURSOR_DEFAULT);
     CUR_HAND  = SDL_CreateSystemCursor(SDL_SYSTEM_CURSOR_POINTER);
 
+    usage_sync();
     sway_subscribe_events();
     if (!model_reload()) die("could not read the sway tree");
     select_current_workspace();
@@ -4678,6 +4864,9 @@ int main(int argc, char **argv)
         hov_win = hit_test(SHOT_MX * MOUSE_SCALE, SHOT_MY * MOUSE_SCALE, &hov_ws);
     }
 
+    Uint64 last_reload = 0;
+    bool   pending_reload = false;
+
     SDL_StartTextInput(WIN_HANDLE);
     SDL_RaiseWindow(WIN_HANDLE);
     present();
@@ -4685,17 +4874,26 @@ int main(int argc, char **argv)
     while (running) {
         SDL_Event e;
         if (anim_running()) dirty = true;
-        if (SDL_WaitEventTimeout(&e, anim_running() ? 8 : 60)) {
+        if (SDL_WaitEventTimeout(&e, (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
+        /* 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. */
-        if (running && !drag_active && sway_events_pending()) {
-            reload_model();
-            dirty = true;
+         * 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; }
     }