Nimbin[12]?Sway / SWOv / commits / 1bd6eca

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 open layouted

Christian Immanuel · 2026-08-28 10:19 · 1bd6eca18f24feccd321f8afc3f6207d9dbc6611

 README.md |  17 +++-
 swov.c    | 291 +++++++++++++++++++++++++++++++++++++++++++++++++++++++-------
 2 files changed, 273 insertions(+), 35 deletions(-)

diff --git a/README.md b/README.md
index 7305b3e..5013474 100644
--- a/README.md
+++ b/README.md
@@ -73,6 +73,14 @@ click-through, so the windows underneath stay selectable.
 window it lands beside it, splitting horizontally; top or bottom splits
 vertically. A bar shows the edge before you let go.
 
+Against the **edge of the tile itself** it lands beside *everything* on that
+workspace, not beside whichever window is nearest. Two windows stacked one
+above the other are a column, and left of the column is a different place from
+left of its top window — the bar spans the whole edge to say which one you are
+getting. Where the windows already sit side by side the two are the same
+thing, so swov works that out from the layout and only breaks out of a stack
+when there is one.
+
 **A workspace** (grab the header strip) onto another swaps the two. Onto the
 left or right quarter of another it inserts there, pushing the occupied run up
 by one.
@@ -207,10 +215,11 @@ swov --info      # every path and setting in use: binary, config, usage file,
 
 ## Load per workspace
 
-A small moon next to each workspace number says how busy that workspace is: a
-sliver when it is idling, whole when something is working, running from teal
-through orange into red and held back towards `dim`, since it is secondary to
-everything else on the tile. swbr measures it — a rate needs two samples
+A second dot scale down the *right* edge of each tile says how busy that
+workspace is, mirroring the usage dots on the left: the same shape in a
+different colour, one for the time you have spent there and one for the work
+happening there now. It stays teal until a workspace is really pinned, and
+only then tips towards red. swbr measures it — a rate needs two samples
 seconds apart and swov is only on screen for a moment — and leaves the answer
 in `$XDG_RUNTIME_DIR/swbr-cpu`. Without swbr running the file is stale or
 missing and nothing is drawn. `cpu=0` turns it off.
diff --git a/swov.c b/swov.c
index a774692..22a8093 100644
--- a/swov.c
+++ b/swov.c
@@ -1251,6 +1251,8 @@ static int sweep_descendants(int root, int *out, int cap)
     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)
 {
@@ -1369,6 +1371,14 @@ static int adopt_window(int pid, const char *ws, double timeout, bool focus,
     }
     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) {
@@ -3293,7 +3303,8 @@ typedef enum {
     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_WIN_NEWWS,  /* window onto a free number: sway creates it          */
+    DROP_WS_EDGE     /* window along one edge of a tile: beside the lot      */
 } DropKind;
 
 enum { EDGE_LEFT, EDGE_RIGHT, EDGE_TOP, EDGE_BOTTOM };
@@ -3412,6 +3423,24 @@ static void drag_update_target(float x, float y)
     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) {
         drop_kind = DROP_WIN_NEAR;
         drop_win  = win_idx;
@@ -3425,6 +3454,56 @@ static void drag_update_target(float x, float y)
     }
 }
 
+/* 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)
 {
@@ -3506,6 +3585,121 @@ static void act_win_drop_near(Win *drag, Win *target, int edge)
     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)
+{
+    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) {
@@ -3529,6 +3723,11 @@ static void drag_finish(void)
             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);
@@ -3856,30 +4055,6 @@ static void draw_workspace(int idx)
 
     tex_draw(ws->badge, tile.x + tile.w - p - (float)ws->badge.w, top, num_col);
 
-    /* How busy that workspace is, as a moon filling from the bottom: barely
-     * there when it is idle, whole when something is working. Held back
-     * towards the tile colour, because it is not what the tile is about. */
-    float load = cpu_frac(ws->name);
-    if (load >= 0.0f) {
-        SDL_FColor hot = load < 0.5f ? mix(C.current, C.hl, load * 2.0f)
-                                     : mix(C.hl, C.urgent, (load - 0.5f) * 2.0f);
-        SDL_FColor lc = with_alpha(mix(C.dim, hot, 0.45f + 0.4f * load),
-                                   0.55f + 0.35f * load);
-        float d  = SDL_max(4.0f * SC, (float)ws->badge.h * 0.26f);
-        float dx = tile.x + tile.w - p - (float)ws->badge.w - d - 5.0f * SC;
-        float dy = top + (float)ws->badge.h * 0.5f - d * 0.5f;
-
-        fill_round_rect((SDL_FRect){ dx, dy, d, d }, d * 0.5f,
-                        with_alpha(lc, lc.a * 0.4f));      /* the whole disc */
-
-        float lit = d * (0.15f + 0.85f * load);            /* and how full   */
-        SDL_Rect clip = { (int)dx, (int)(dy + d - lit), (int)(d + 1.0f), (int)(lit + 1.0f) };
-        SDL_Rect prev;
-        bool had = SDL_GetRenderClipRect(REN, &prev) && SDL_RenderClipEnabled(REN);
-        SDL_SetRenderClipRect(REN, &clip);
-        fill_round_rect((SDL_FRect){ dx, dy, d, d }, d * 0.5f, lc);
-        SDL_SetRenderClipRect(REN, had ? &prev : NULL);
-    }
 
     float count_right = tile.x + p;
     if (ws->sub.t) {
@@ -3941,6 +4116,23 @@ static void draw_workspace(int idx)
             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);
+            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 ---- */
@@ -4025,6 +4217,21 @@ static void draw_drop_indicator(void)
         }
         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;
@@ -5031,12 +5238,34 @@ static void backdrop_hover(float fx, float fy)
     WSS[ws_idx].sel = -1;
     dirty = true;
 
-    /* Over a window, the pointer picks a side of it, exactly as dragging a
-     * window inside swov does: the bar shows where it will land, and the app
-     * is placed there once it opens. */
-    char place[64] = "";
-    if (win_idx >= 0) {
-        static const char *EDGE_NAME[] = { "left", "right", "top", "bottom" };
+    /* 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;