WorkspaceForge Window-layout solver and re-fitter Techlosoft - Workspace Line WHAT IT IS ========== WorkspaceForge answers one question the rest of the Workspace Line does not: "this layout was saved on three 1440p screens - what should it look like on the laptop I am actually holding?" You describe your monitors in one JSON file (a topology) and your workspace in another (a layout: nested rows and columns with weights, whose leaves are named zones, plus rules that say which app goes in which zone). WorkspaceForge solves that into exact integer pixel rectangles, and re-fits a layout computed for one topology onto a completely different one, telling you precisely which window moved, which was resized, and which was folded in from a screen that no longer exists. The solver is integer-only. Weighted splits distribute leftover pixels by the largest-remainder method, so a column of five zones on a 1439 px screen sums to exactly 1439 px - no window is ever off by an accumulating rounding error, and the zones tile the work area with zero gaps and zero overlaps. It does not touch your windows. See SCOPE below, which is blunt about it. INSTALL ======= Pre-built binaries are in dist/. There is nothing to install - copy the one for your platform anywhere on your PATH and run it. dist/workspaceforge-linux-amd64 Linux, x86-64 dist/workspaceforge-darwin-arm64 macOS, Apple Silicon dist/workspaceforge-darwin-amd64 macOS, Intel dist/workspaceforge-windows-amd64.exe Windows, x86-64 On macOS and Linux you may need to mark it executable: chmod +x workspaceforge-linux-amd64 To build from source you need Go 1.24 or newer. There are no dependencies of any kind, so no network access is required: go build -o workspaceforge . COMMANDS ======== workspaceforge plan --topology [--windows ] [--json] workspaceforge check [--topology ] [--windows ] [--json] workspaceforge refit --from --to [--windows ] [--json] workspaceforge render --topology --out [--json] workspaceforge topo [--json] workspaceforge help | -h | --help plan Solves the layout against the topology and prints the exact rectangle of every zone, in device pixels of the virtual desktop, plus the logical (scale-independent) size and the windows that land there. check Validates a layout without needing a topology: structure, duplicate zone names, weights, matchers, unmatched windows, and unreachable rules (both kinds - shadowed by an earlier rule, and matching nothing at all). Given --topology it also solves the geometry and re-verifies the result independently of the solver: overlapping zones, out-of-bounds rectangles, tiling drift and minimum-size violations. refit Maps a layout authored on one topology onto another. See RE-FIT below for the whole algorithm. Reports the monitor mapping, every window that moved, resized or was folded, and every zone. render Writes a real SVG diagram of the solved layout, drawn to scale: one SVG user unit is one device pixel, and the rectangles in the file are literally the rectangles the solver computed. Open it in any browser. topo Validates and describes a topology: bounds, insets, work areas, scale, logical sizes, canonical rank order and which monitors share an edge. FLAGS --topology Display topology JSON (plan, check, render). --from Source topology the layout was authored on (refit). --to Destination topology to map it onto (refit). --windows Window list JSON, replacing the layout's own "windows" block. A bare JSON array or an object with a "windows" key are both accepted. --out SVG file to write (render). --json Machine-readable output. Available on every command. Short forms -t, -l, -w and -o are accepted for --topology, --layout, --windows and --out. Flags may appear before or after positional arguments; either order works. QUICK START =========== # is my desk description sane? workspaceforge topo fixtures/desk3.topology.json # does the layout hold together, before any geometry is involved? workspaceforge check fixtures/desk3.layout.json # what are the actual rectangles? workspaceforge plan fixtures/desk3.layout.json -t fixtures/desk3.topology.json # I am on the road today workspaceforge refit fixtures/desk3.layout.json \ --from fixtures/desk3.topology.json --to fixtures/laptop.topology.json # show me workspaceforge render fixtures/desk3.layout.json \ -t fixtures/desk3.topology.json --out map.svg # feed a script workspaceforge plan fixtures/desk3.layout.json -t fixtures/desk3.topology.json --json THE TOPOLOGY FILE ================= { "name": "desk-3x1440p", "monitors": [ { "id": "left", "label": "Dell U2723QE (left wing)", "bounds": { "x": -2560, "y": 0, "w": 2560, "h": 1440 }, "scale": 1.0, "insets": { "top": 0, "right": 0, "bottom": 0, "left": 0 } }, { "id": "center", "primary": true, "bounds": { "x": 0, "y": 0, "w": 2560, "h": 1440 }, "insets": { "bottom": 48 } } ] } id Unique, non-empty. This is what a layout binds to. label Free text, shown in reports and the SVG. Optional. primary At most ONE monitor may declare it. If none does, WorkspaceForge picks the rank-0 monitor and says so. bounds Position and size in the virtual desktop, device pixels. The origin may be negative; a monitor to the left of the primary usually has a negative x. scale HiDPI factor, default 1.0, allowed range 0.25 to 8. It is parsed ONCE into an integer per-mille value (1.5 becomes 1500) and never touches a floating point number again. insets Pixels claimed by the menu bar, taskbar, dock or panel. The WORK AREA is bounds minus insets, and zones are solved inside the work area, never inside the raw bounds. Unknown fields are rejected, so a typo is an error rather than a silently ignored setting. TOPOLOGIES THAT ARE REFUSED overlapping bounds Two monitors claiming the same pixels is reported with both ids and the overlapping rectangle. No display server produces this; a file that does is a mistake. a disjoint desktop Every monitor must share an EDGE SEGMENT of positive length with the rest of the desktop. A one-pixel gap, or two monitors touching only at a corner, is reported as "N disjoint groups" and refused - a window dragged into that hole would be on no screen at all. zero or negative size, negative insets, insets that consume the whole screen, duplicate ids, two primaries, a scale outside 0.25-8. THE LAYOUT FILE =============== { "name": "deep-work", "monitors": [ { "monitor": "center", "gap": 8, "padding": 12, "root": { "split": "columns", "children": [ { "zone": "editor", "weight": 3, "minWidth": 900 }, { "split": "rows", "weight": 2, "children": [ { "zone": "terminal", "weight": 1, "minHeight": 240 }, { "zone": "browser", "weight": 1 } ] } ] } } ], "rules": [ { "zone": "editor", "app": "exact:com.microsoft.VSCode" }, { "zone": "terminal", "app": "glob:*.terminal" }, { "zone": "browser", "app": "prefix:org.mozilla." }, { "zone": "logs", "app": "prefix:io.lnav", "title": "glob:*production*" } ], "windows": [ { "id": "w-code", "app": "com.microsoft.VSCode", "title": "solve.go" } ] } NODES A node is either a SPLIT (has "split" and "children") or a ZONE LEAF (has "zone"). Never both. Leaves carry the names; splits carry the structure. split "rows" or "columns". weight Share of the parent along the split axis. Default 1, minimum 1. gap Pixels reserved BETWEEN this split's children. Defaults to the monitor's "gap". padding Pixels shaved off the inside of this node before its children (or, on a leaf, before the window rectangle). minWidth minHeight Minimum size in LOGICAL pixels. Multiplied by the monitor's scale, rounding UP, so a stated minimum is never quietly shaved. Zone names must be unique across the WHOLE layout, not just per monitor. That is what lets a zone - and the windows in it - follow you from one monitor to another during a re-fit. RULES Rules are tried in order and the FIRST match wins. A rule may test the app id, the window title, or both; when both are given, both must match. exact:VALUE the whole string equals VALUE prefix:VALUE the string starts with VALUE glob:PATTERN wildcards: * is any run of characters (possibly empty), ? is exactly one character. Nothing else is special - '.' and '/' are ordinary characters. VALUE no prefix at all means exact Matching is case sensitive. Several windows may land in one zone. They all get the same rectangle, and the report says so; WorkspaceForge does not invent a cascade or a stack offset. HOW THE SOLVER WORKS ==================== Each monitor's work area is shrunk by the monitor's padding, and the root node is solved into that rectangle. For a split of n children along an axis of E pixels with gap G: available = E - G * (n - 1) each child's exact share is available * weight_i / sum_of_weights Each child takes the integer FLOOR of its share. That leaves a remainder of strictly fewer than n pixels. Those pixels are handed out one each to the children with the largest fractional remainder, ties broken by ascending index - the Hare/Hamilton largest-remainder method. The consequences are the point of the whole program: - The child sizes plus the gap strips sum to EXACTLY the parent extent. The last zone in a row ends precisely on the parent's right edge. - No child is ever more than one pixel away from its exact share. - Error does not accumulate. Truncating each share instead would lose up to n-1 pixels at every level of the tree, and those losses compound down a nested layout until the right-hand column is visibly short. - The result is deterministic. The same inputs always produce the same pixels, on every platform, forever. Every node produces two rectangles: cell the slice of the parent this node owns frame the cell shrunk by this node's padding - the rectangle a window would actually get With gap 0 and padding 0 the cells tile the work area exactly: they are all inside it, none overlaps another, and their areas sum to its area. That is asserted as a property over hundreds of random weight vectors and tree shapes in the test suite, and re-verified at run time by `check`. WHEN IT CANNOT BE DONE Nothing is fudged. If a split cannot give every child at least one pixel, or a zone ends up below its stated minimum, the exact constraint is named: [min-width] zone "editor" on "builtin" is 563 px wide but requires minWidth 900 logical px (= 1800 device px at 2x); short by 1237 px [split-too-small] node root on "m" cannot fit 5 children: 20 px of width minus 4 gaps of 10 px leaves -20 px, under 1 px each plan and check exit 1 when any error-severity issue is found. They still print everything they did compute, so you can see what went wrong rather than being handed a bare failure. RE-FIT ====== Given a layout solved for topology A and a different topology B, the mapping is completely deterministic. There are no heuristics and no history. 1. Rank the monitors of BOTH topologies with the same canonical order: a. the primary monitor first; b. then by descending work-area area; c. then by ascending x, then ascending y; d. then by ascending id. Step (d) makes the order total, so it never depends on the order monitors happen to appear in the file. 2. Pair rank i of the source with rank i of the destination. The source monitor's tree is re-solved against the destination monitor's WORK AREA, so every zone keeps its proportions and its neighbours and gets destination-sized rectangles. 3. Source monitors ranked beyond the last destination rank have disappeared. Each is FOLDED into the fallback monitor, which is always destination rank 0 - the primary. The fallback's tree becomes a new split along the long axis of its work area (columns if it is wider than tall, rows otherwise), with the fallback's own tree first and each folded tree after it in source rank order, every child weighted 1. Three screens folded onto a laptop therefore become three equal columns: the old centre layout, then the old left, then the old right. 4. Destination monitors beyond the last source rank get no layout. They are reported as left empty, not filled with something invented. 5. Every window is compared against its source rectangle and classified: unchanged, moved, resized, moved+resized, or unplaced - and separately flagged "folded" when the monitor it used to live on is gone. Re-fit does not rewrite your layout file. It computes and reports; the file on disk is untouched. EXAMPLE OUTPUT ============== WorkspaceForge plan layout : deep-work (fixtures/desk3.layout.json) topology : desk-3x1440p (fixtures/desk3.topology.json) MONITORS (* = primary; rank order is primary, then descending area, ...) RANK MONITOR BOUNDS WORK AREA SCALE LOGICAL ZONES ---- -------- ----------------- ----------------- ----- --------- ----- 0 center * 2560x1440+0+0 2560x1392+0+0 1x 2560x1392 3 1 left 2560x1440+-2560+0 2560x1440+-2560+0 1x 2560x1440 2 2 right 2560x1440+2560+0 2560x1440+2560+0 1x 2560x1440 2 ZONES (7; X/Y/W/H are device pixels in the virtual desktop) MONITOR ZONE X Y W H LOGICAL PATH ------- ---------- ----- --- ---- ---- --------- ----------------------- center editor 12 12 1517 1368 1517x1368 root/columns[0] center terminal 1537 12 1011 680 1011x680 root/columns[1]/rows[0] center browser 1537 700 1011 680 1011x680 root/columns[1]/rows[1] Rectangles are printed as WxH+X+Y, the same notation X11 has used for decades. EXIT CODES ========== 0 Success. Warnings - an unmatched window, an unreachable rule, a zone nobody lives in, a destination monitor left empty - are legitimate findings, not failures, and do not change the exit code. 1 Bad invocation (unknown command, missing required flag), an I/O or parse error (missing file, invalid JSON, unknown field), an invalid topology or layout, or any error-severity finding: a minimum size that cannot be met, a split that cannot fit, an overlap, an out-of-bounds rectangle, a layout naming a monitor the topology does not have. Explicit help exits 0. Usage for `help`, `-h` and `--help` goes to STDOUT. Usage printed because you got the arguments wrong goes to STDERR. SCOPE / WHAT THIS DOES NOT DO ============================= Read this before you file a bug. WorkspaceForge IS A CALCULATOR. It reads JSON files you wrote and writes a table, a JSON document, or an SVG. That is the entire surface. - It does NOT enumerate the displays actually attached to this machine. There is no call to Xrandr, Wayland, CoreGraphics, EnumDisplayMonitors or anything else. The topology is a file YOU write. If it says you have three 4K screens, the program believes you. - It does NOT move, resize, focus, raise or in any way touch a real window. It computes where a window WOULD go. Nothing is applied. - It does NOT enumerate the windows currently open. The window list is a file you write, matched against rules you write, so `plan` can tell you where each of them would land. - There is NO window-manager integration of any kind: no yabai, no i3, no bspwm, no komorebi, no AeroSpace, no Rectangle, no PowerToys FancyZones, no AppleScript, no wmctrl, no DBus. Nothing is generated for them either. - It does NOT run in the background, watch for display changes, hot-plug events, or apply a layout when you dock. It runs once and exits. - It does NOT read or write any other program's configuration. - It has NO network access, no telemetry, no auto-update, and no cgo. If you want the rectangles applied to real windows, take `--json` and feed it to whatever tool on your platform can actually do that. Producing correct, exactly-tiling, deterministic rectangles is the job WorkspaceForge does; moving windows is a different job, and pretending otherwise here would mean shipping a per-OS pile of privileged code behind a promise this binary cannot keep. ALSO NOT IMPLEMENTED - HONESTLY ABSENT, NOT STUBBED - Rotated monitors. A portrait screen is fine (bounds taller than wide), but there is no rotation transform: bounds are axis-aligned rectangles. - Fractional-scale rounding policies per platform. Scale is used for minimum sizes and for reporting logical sizes; it does not reproduce any specific OS's own DPI-rounding quirks. - Floating, always-on-top, pinned or tabbed windows, and per-window stacking. Several windows in one zone all get the same rectangle. - Overlapping or free-form (non-tiling) zone layouts, percentage-of-screen zones, and absolute pixel zones. Everything is a weighted split. - Constraint solving. Minimum sizes are CHECKED and reported; they do not push other zones around to make room. There is no relaxation pass. - Aspect-ratio and maximum-size constraints. - Multiple workspaces/virtual desktops per monitor, and per-workspace rules. - Writing a re-fitted layout back out as a new layout file. `refit` reports; it does not save. - Any raster output. `render` writes SVG only - no PNG, no PDF. FILE FORMATS AT A GLANCE ======================== Both input files are ordinary JSON, UTF-8, and reject unknown fields. topology { "name", "monitors": [ { "id", "label", "primary", "bounds": {x,y,w,h}, "scale", "insets": {top,right,bottom,left} } ] } layout { "name", "topology", "monitors": [ { "monitor", "gap", "padding", "root": } ], "rules": [ { "zone", "app", "title" } ], "windows": [ { "id", "app", "title" } ] } node { "split": "rows"|"columns", "gap", "padding", "weight", "children": [ ... ] } | { "zone": "name", "weight", "padding", "minWidth", "minHeight" } The --json output of every command is a superset of what the tables print, including the "cell" rectangle behind each "frame", the per-monitor rank, and every issue with a stable machine-readable "code" ("min-width", "overlap", "split-too-small", "rule-shadowed", "window-unmatched", and so on).