Nimbin[12]?Sway / SWOv / commits / 5a83e50

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

Tab back to last focused - rely on swbr

Christian Immanuel · 2026-09-05 21:37 · 5a83e5010f62a2b8bedbf41e388265f0cc7239d1

 README.md      | 16 +++++++++++-
 config.example |  3 +++
 swov.c         | 78 ++++++++++++++++++++++++++++++++++++++++++++++++++++++++--
 3 files changed, 94 insertions(+), 3 deletions(-)

diff --git a/README.md b/README.md
index dfb4ad7..8795d42 100644
--- a/README.md
+++ b/README.md
@@ -38,7 +38,7 @@ for_window [app_id="swov"] floating enable, border none
 | `0`–`9` | switch to that workspace |
 | `ctrl+0`–`9` | move the selection there |
 | arrows, `hjkl` | move the selection; it walks through tile borders and wraps around the grid |
-| `tab` / `shift+tab` | previous / next workspace; while searching, only the ones with a hit |
+| `tab` / `shift+tab` | the app you were in before this one, then the one before that (`tab=workspace` walks workspaces instead) |
 | `ctrl+tab` (`+shift`) | one row down / up in the grid |
 | `w` | window selection ⇄ whole-workspace selection |
 | `enter`, click | focus |
@@ -278,6 +278,19 @@ overlay that is not on the layer shell cannot be drawn over it. swov takes it
 off fullscreen while it is up and puts it back exactly as it was on the way
 out. `over_fullscreen=0` leaves it alone, and swov opens behind it.
 
+## Last used
+
+`tab=recent` (the default) makes tab walk windows in the order they were last
+focused, so one press lands on what you were in before this — what the key
+means everywhere else. `shift+tab` goes the other way, and `ctrl+tab` still
+moves a row in the grid.
+
+swov opens and closes in a moment, so it cannot watch focus itself. swbr is
+running all day and does: it keeps the last thirty-two windows in
+`$XDG_RUNTIME_DIR/swbr-focus`, most recent first, and swov reads that when it
+starts. Without swbr there is no order to walk, so tab quietly falls back to
+stepping through workspaces — which is also what `tab=workspace` does.
+
 ## Workspace usage
 
 Every switch goes through swov, so it stamps the time as it goes: the workspace
@@ -360,6 +373,7 @@ lists them. The file above is read after it, so swov's own config always wins.
 | `over_fullscreen` | un-fullscreen whatever is in the way, and restore it on exit |
 | `map_dwell_ms` | hold a drag over a monitor this long to switch to it; `0` is off |
 | `launcher` | what `d` opens; `swas --replace overview=1` by default |
+| `tab` | `recent` walks the last used apps, `workspace` walks workspaces |
 | `cancel_drop` | the ✕ beside the monitor map: let a drag go there and nothing happens |
 | `track`, `usage_dots`, `dot_count`, `dot_px` | usage recording and its dot scale |
 | `start_selection` | `workspace`, `none` or `window` |
diff --git a/config.example b/config.example
index b5f0605..fbe0301 100644
--- a/config.example
+++ b/config.example
@@ -44,6 +44,9 @@ outputs_map=1      # with a second screen: a map of the monitors, bottom left,
                    # workspaces. It keeps that place to itself, so it is
                    # always in the same spot
 outputs_map_w=0.18 # how wide, as a share of the tile area
+tab=recent         # what tab walks: recent = the apps you used last, in that
+                   # order (needs swbr running, which is what keeps the
+                   # order). workspace = previous/next workspace
 launcher=swas --replace overview=1
                    # what `d` opens. swas draws its own backdrop, so swov
                    # steps aside rather than stacking two overlays
diff --git a/swov.c b/swov.c
index 32a3198..f3f0ff2 100644
--- a/swov.c
+++ b/swov.c
@@ -250,6 +250,7 @@ typedef struct {
     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;
@@ -315,6 +316,7 @@ static Cfg cfg_defaults(void)
     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;
@@ -411,6 +413,7 @@ static void cfg_set(Cfg *c, const char *k, const char *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;
@@ -3345,6 +3348,75 @@ static bool ws_is_hit(const Ws *ws)
     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) {
@@ -5027,6 +5099,7 @@ static void reload_model(void)
     }
 
     cpu_load();                  /* whatever swbr last measured */
+    focus_load();                /* and the order things were used in */
     mark("cpu numbers");
     if (!FS_RESTORE) fullscreen_step_aside();
     layout();
@@ -5598,8 +5671,9 @@ static void handle_key(const SDL_KeyboardEvent *k)
         break;
 
     case SDLK_TAB:
-        if (k->mod & SDL_KMOD_CTRL) step_ws_row(shift ? -1 : 1);
-        else                        step_ws(shift ? -1 : 1);
+        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 */