DeskScene Degrading multi-monitor scenes onto the screens you actually have Techlosoft - Workspace Center WHAT IT IS ========== DeskScene answers one question the rest of the Workspace Center line does not: "my scene does not fit - what should I give up, and why?" A SCENE is a named set of panes: "six charts, an order ticket, a DOM ladder, positions, risk, news and a terminal". Each pane carries a priority, a minimum useful size, an ideal area, whether it may share a tab group with its siblings, and whether it is required at all costs. You hand DeskScene a scene and a display topology - monitors with work areas and scale factors, from JSON - and it decides the best REALISABLE variant: what stays at full size, what shrinks, what gets stacked behind tabs, and what is dropped. It reports a fit score, a per-pane outcome and a plain-English reason for every degradation. Its sibling WorkspaceForge computes exact rectangles from explicit rules and always succeeds because you told it precisely what to do. DeskScene does the opposite job: DEGRADATION UNDER CONSTRAINT. Its core is a selection and packing optimiser, not a splitter. If your scene fits comfortably, DeskScene has very little to say; it earns its keep on the laptop screen in the hotel room. 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/deskscene-linux-amd64 Linux, x86-64 dist/deskscene-darwin-arm64 macOS, Apple Silicon dist/deskscene-darwin-amd64 macOS, Intel dist/deskscene-windows-amd64.exe Windows, x86-64 On macOS and Linux you may need to mark it executable: chmod +x deskscene-linux-amd64 To build from source you need Go 1.24 or newer. There are no dependencies of any kind, no cgo and no network access required: go build -o deskscene . go test ./... COMMANDS ======== deskscene fit --scene --displays [--out ] [--json] deskscene compare --displays [--json] deskscene advise --scene --displays [--json] deskscene render --scene --displays --out [--json] deskscene presets [--json] deskscene help | -h | --help fit Runs the optimiser and prints the winning variant: every pane's outcome (full / shrunk / stacked / dropped), its rectangle in logical pixels, the fit score, per-monitor occupancy, and a reason for each degradation. With --out it also writes the SVG. compare Scores two scenes on the same hardware and says which one degrades less. Useful for "should I run the trading layout or the research layout on this machine?". Each scene is scored against ITS OWN ideal, so the percentages are comparable even when the scenes have different numbers of panes. advise States the smallest hardware change that would let the scene fit at full size - "one more 1920x1080 monitor", or "240 more px of height on monitor 2". This is DERIVED, not guessed: a catalogue of candidate topologies is generated, sorted by how many physical pixels each one adds, and the fit is RE-RUN against each candidate in that order. The first candidate that genuinely produces a full fit is the recommendation. render Draws the computed placement to scale as a real SVG you can open in any browser. Monitors are drawn in physical device pixels at their virtual desktop positions; each pane's logical rectangle is converted back through its monitor's scale factor, so a 200% HiDPI panel correctly shows half as many logical pixels in the same physical space. presets Lists the three built-in scenes. Any preset name may be used wherever a scene file is expected. FLAGS --scene Scene JSON file, or the name of a built-in preset. Bare positional arguments to fit/advise/render are also treated as the scene reference. --displays Display topology JSON file. --out Where to write the SVG (fit, render). --json Machine-readable output. Available on every reporting subcommand: fit, compare, advise, render, presets. Short forms -s, -d and -o are accepted for --scene, --displays and --out. Flags may appear before or after positional arguments; either order works. SCENE FILE FORMAT ================= { "name": "Trading desk", "notes": "optional free text", "panes": [ {"id": "order-ticket", "label": "Order ticket", "priority": 100, "minWidth": 380, "minHeight": 520, "idealArea": 243200, "required": true}, {"id": "chart-es", "label": "ES chart", "priority": 90, "minWidth": 640, "minHeight": 420, "idealArea": 540000, "stackable": true, "stackGroup": "charts"} ] } id Unique, non-empty. Used in output and as the deterministic tie-break key. label Human name for reports and the SVG. Defaults to the id. priority Positive integer weight. Bigger means more important. The scale is yours; only ratios matter. minWidth The size below which the pane is USELESS, in logical pixels. minHeight A pane that cannot be given at least this much is dropped, not squeezed. Below the minimum it contributes zero to the score. idealArea The area the pane wants, in square logical pixels. DeskScene derives concrete integer width and height from this (see HOW THE FIT IS COMPUTED). An idealArea below minWidth*minHeight is silently raised to minWidth*minHeight. stackable May share one rectangle with other panes of the same group as a tab stack. Requires stackGroup. stackGroup Group name. Only panes in the SAME group ever stack together. required A scene that cannot place this pane is reported INFEASIBLE rather than quietly degraded. All geometry is in LOGICAL pixels - the units a user perceives, already divided by the display scale factor. There are no floats anywhere in the file format or in the engine. DISPLAY FILE FORMAT =================== { "name": "Generous 3-monitor trading rig", "monitors": [ {"id": "left", "width": 2560, "height": 1440, "scale": 100, "workArea": {"x": 0, "y": 0, "width": 2560, "height": 1400}}, {"id": "centre", "width": 3840, "height": 2160, "scale": 100, "workArea": {"x": 0, "y": 40, "width": 3840, "height": 2080}}, {"id": "right", "width": 2560, "height": 1440, "scale": 100} ] } id Unique. Defaults to monitor1, monitor2, ... if omitted. width Panel size in PHYSICAL device pixels. height scale Display scale factor as an integer percentage. 100 = 1x, 200 = 2x HiDPI, 125/150 for the fractional Windows settings. Range 25..400. Defaults to 100. workArea The usable sub-rectangle, in physical pixels, relative to the monitor's own top-left corner - subtract your taskbar, dock, menu bar and panels here. Defaults to the whole panel. x, y Optional position of the monitor in the virtual desktop, in physical pixels, used only for rendering. When omitted the monitors are laid out left to right in the order given. Logical work area = physical work area * 100 / scale, truncated. Truncation deliberately UNDER-reports the room available by at most one pixel per axis, so DeskScene never claims a pane fits when it is a pixel short. BUILT-IN PRESETS ================ trading-desk 12 panes. Six charts in one stack group, a DOM ladder, a positions blotter, risk, news and a terminal. The order ticket is required - a trading layout you cannot send an order from is not a trading layout. developer 8 panes. Editor (required), terminal and a live preview in front; tests, logs and git share a tab stack when space runs out. video-editor 8 panes. A wide timeline and a program monitor are both required; scopes, effects, the audio mixer and the media browser degrade into one tabbed panel column. These are embedded in the binary as real JSON. `deskscene presets --json` prints them in full, so they double as worked examples of the scene format - pipe one to a file, edit it, and feed it back in. QUICK START =========== # what is built in deskscene presets # the good desk deskscene fit --scene trading-desk --displays examples/rig-3mon.json # the hotel room deskscene fit --scene trading-desk --displays examples/laptop.json # what would it take to fix that deskscene advise --scene trading-desk --displays examples/laptop.json # which of my two layouts survives this machine better deskscene compare trading-desk developer --displays examples/laptop.json # look at it deskscene render --scene trading-desk --displays examples/laptop.json \ --out laptop.svg # feed a provisioning script deskscene fit --scene trading-desk --displays laptop.json --json EXAMPLE OUTPUT ============== DeskScene fit scene : Trading desk (12 panes) [preset:trading-desk] displays : Single 14-inch laptop screen (1 monitor) internal 2880x1700 physical @ 200% = 1440x850 logical algorithm: guillotine best-area-fit packing + greedy descent with priority repair and local search search : 21 stack configurations, exhaustive fit score: 436446 / 932000 (46.8% of the priority-weighted ideal) outcome : 0 full, 3 shrunk, 8 stacked, 1 dropped shrink : uniform schedule level 70% of ideal linear size PANES (highest priority first) * order-ticket p100 shrunk internal 380x520 at (960,0) shrunk to 90% of ideal width (380x520 instead of 421x577, 81% of ideal area) so that all 5 placed rectangles fit; still at or above its 380x520 minimum chart-es p90 stacked internal 640x420 at (0,0) front tab of a 6-pane stack in group "charts" shared with NQ chart, CL chart, GC chart, ZN chart, 6E chart: the topology had room for one 640x420 rectangle here, not 6 of them positions p84 dropped - dropped: at priority 84 it was the cheapest thing to lose, and after the kept panes were packed no free rectangle of 720x300 (its minimum) remained MONITORS internal 1440x850 logical used 88% free 143200 px^2 (5 rectangles) * = required pane HOW THE FIT IS COMPUTED ======================= Everything below is exact integer arithmetic. There is no floating point anywhere in the engine; percentages are stored per-mille as integers and only formatted for display. 1. LOGICAL PROJECTION Each monitor's physical work area is divided by its scale percentage to give a logical work area. That rectangle is the packer's whole world for that monitor; monitors do not share coordinate space and a pane never straddles two of them. 2. IDEAL DIMENSIONS FROM AN IDEAL AREA A pane states an AREA, not a size, because what a chart wants is "about this much room", not a fixed rectangle. Its preferred aspect ratio is taken to be its minimum aspect ratio, so a wide blotter stays wide and a tall DOM ladder stays tall as the area grows: w / h = minW / minH and w * h = idealArea => w = isqrt(idealArea * minW / minH), h = idealArea / w isqrt is an integer square root by Newton's method. Both results are then clamped up to the stated minimum. Because the division truncates, the realised ideal area can be a row and a column short of the requested one - this is why a perfect layout typically scores 99.8% rather than 100.0%. 3. UNITS AND STACK CONFIGURATIONS The packer places UNITS, not panes. A unit is either a single pane or a TAB STACK: two or more panes of the same stackGroup sharing one rectangle. A stack's minimum is the componentwise maximum of its members' minimums and its ideal area is the largest member's. For a group of k stackable panes, DeskScene enumerates k+1 configurations: the top s panes by priority get their own rectangles and the remaining k-s share one stack, for every s in 0..k. The full search is the cartesian product over groups, capped at 4096 configurations; past that it falls back to the two extremes per group (all-stacked / all-separate) and then to all-stacked alone. The chosen mode is printed as `search:`. 4. THE SHRINK SCHEDULE Sizes are tried at 20 discrete levels: 100, 95, 90, ... 10 percent of ideal linear size, then a final level meaning "everything at its stated minimum". The level is uniform across the layout; a pane whose minimum bites simply stops shrinking, so the effective per-pane percentage varies and is reported individually. The largest level at which the chosen set packs is used. 5. GUILLOTINE PACKING Each monitor's work area starts as one free rectangle. Units are placed largest-area-first. For each unit the packer takes the SMALLEST free rectangle that can hold it (best area fit), anchors it to that rectangle's top-left corner, and divides what is left with a single straight cut running the full width or the full height of that free rectangle - the cut is chosen along whichever axis leaves the shorter leftover strip. Every region is therefore reachable by a sequence of edge-to-edge cuts, which is exactly the structure a tiling window manager can realise with nested splits. Because free rectangles are produced by disjoint cuts and are never merged, placed rectangles provably never overlap and never leave the work area. The test suite asserts both as properties over randomised scenes. Best-area-fit deliberately fills the tightest space that works. On a rig with one very large monitor and two smaller ones, a scene that fits comfortably in the smaller two will leave the large monitor completely empty. That is the packer conserving large contiguous regions, not a bug. 6. SCORING A pane placed at area A against ideal area I delivers satisfaction sat = min(1000, 1000 * A / I) per-mille and contributes priority * sat, times 700/1000 if it is sitting behind a tab rather than being visible at a glance. A pane below its minimum is never placed and contributes nothing at all. The fit score is the sum over all panes; the maximum is sum(priority) * 1000. A stacked pane is therefore always worth more than a dropped one and always worth less than a visible one, which is the ordering that makes "stack it rather than lose it" fall out of the arithmetic instead of being special-cased. 7. THE OPTIMISER For each stack configuration, the selection problem is solved three times from three different greedy seeds, and the best answer is kept. The seeds differ only in which unit they sacrifice first when the set does not pack: least total value, least value per pixel of minimum footprint, or largest footprint. Each seed then runs: a. Greedy descent. While the active set does not pack at any shrink level, drop the non-required unit the strategy likes least. b. Priority repair, to a fixpoint. If a dropped unit d outranks a placed unit p and DOMINATES it in size (d's minimum and ideal are both no larger than p's in each axis), swap them. Domination guarantees the swap is feasible - d fits wherever p fitted, at every shrink level - and the swap can never lower the score, because d has both the higher priority and the higher satisfaction in the same rectangle. This makes "a higher-priority pane is never dropped in favour of a lower-priority one of equal size" a property of the algorithm rather than a hope. c. Local search. Try adding each dropped unit back; try every drop/add swap that moves value upward. Accept only strict score increases. Iterate to a fixpoint, bounded at 200 rounds. Configurations are compared by score, then by number of placed units, then by shrink level, then by a sorted signature of unit keys. Every ordering in the engine ends in a total tie-break on unit key, and no map is ever iterated without sorting first, so the same input always produces byte- identical output. The test suite asserts this under random permutation of the pane order. 8. REQUIRED PANES Required units are never dropped. If a required pane's minimum exceeds every monitor's logical work area, the scene is declared INFEASIBLE immediately with that pane named. If the required units cannot be packed together at their minimums under any configuration, the scene is declared INFEASIBLE with that reason. DeskScene never silently degrades a required pane. 9. ADVISE Candidate topologies are: growing one monitor's height by 60..1080px in 60px steps; growing one monitor's width by 80..1920px in 80px steps; and adding one panel from a seven-model catalogue (1366x768 through 3840x2160). Growing a monitor grows its work area by the same amount. Candidates are sorted by added physical pixel count and the full fit is re-run on each in that order. The first that produces a genuine full fit is recommended, and up to three further working options are listed. SCOPE / WHAT THIS DOES NOT DO ============================= Read this section before you trust anything above. DESKSCENE DOES NOT ENUMERATE YOUR REAL DISPLAYS. It has no idea what monitors you own. The display topology is a JSON file that YOU write. There is no EDID parsing, no xrandr, no Quartz Display Services, no EnumDisplayMonitors, no Wayland output protocol. Nothing in this binary touches a graphics API, and a portable standard-library-only program cannot. If your JSON says you have a 40-inch monitor, DeskScene believes you. DESKSCENE DOES NOT LAUNCH, MOVE OR RESIZE ANY REAL APPLICATION. It computes rectangles and prints them. It never starts a process, never calls a window manager, never sends an X/Wayland/Win32/AppKit request, and has no concept of which program a pane corresponds to. The output is a plan. Feeding that plan to something that can actually move windows - i3/sway, yabai, komorebi, PowerToys FancyZones, a shell script - is your job, and --json exists for exactly that. THE OPTIMISER IS A DOCUMENTED HEURISTIC. IT IS NOT PROVEN GLOBALLY OPTIMAL. Two-dimensional rectangle packing with selection is NP-hard; nothing here dodges that. Specifically: - The feasibility test is guillotine best-area-fit, which is INCOMPLETE. A set of rectangles that this packer fails to place may still be placeable by some other arrangement. When that happens DeskScene degrades a scene that did not strictly need degrading. It never does the reverse - a reported placement is always genuinely realisable - so the answer errs towards pessimism, never towards a layout that cannot exist. - Free rectangles are never merged after a pane is removed from consideration, so the free-space representation is more fragmented than a perfect one would be. - The shrink schedule is uniform and has 20 discrete levels. There is no per-pane continuous sizing and no attempt to grow a pane into leftover space. Panes never exceed their ideal size, so leftover pixels stay leftover. - Stack configurations are restricted to PREFIX splits by priority: within a group, the top s panes are separate and the rest form one stack. A group is never split into two or more independent stacks, and panes from different groups never share a stack. - The selection local search explores only 1-add and 1-swap neighbourhoods from three greedy seeds. It finds a local optimum of that neighbourhood, not a global one. Seeding from three strategies measurably improves the answer on real scenes but proves nothing. - The tab-visibility factor (700/1000) and the shrink levels are chosen constants that encode an opinion about how annoying a tab is. They are not measured from anything. In short: the fit score is a good comparative number and a poor absolute one. Use it to compare scenes and topologies, not as a claim of optimality. OTHER THINGS THAT ARE HONESTLY ABSENT - No pane-to-pane adjacency, alignment or grouping constraints ("keep the ticket next to the DOM", "never put the terminal on the primary"). Panes have no preferred monitor and no relative-position rules at all. - No aspect-ratio locking. A pane's aspect follows from its minimum; you cannot pin it. - No overlap, floating or picture-in-picture layouts. Everything is tiled. - No multi-desktop / virtual-workspace modelling. One scene, one screenful. - No monitor rotation, mirroring, or per-monitor DPI beyond the single integer scale percentage. - No persistence: DeskScene stores nothing between runs and has no state file, cache or config file. - No named exit-code thresholds ("fail if anything is dropped"). Parse --json and decide for yourself. - The SVG is a diagram, not a screenshot. It has no application chrome, no real window decorations and no fonts beyond generic sans-serif. JSON OUTPUT =========== --json is available on fit, compare, advise, render and presets. All geometry is integral; there is not a single floating point number anywhere in the output. Percentages appear as integers, and the fit score additionally as `fit_per_mille` (0..1000). fit emits the whole FitResult: scene, topology, feasible, infeasible_reason, full_fit, score, max_score, fit_per_mille, the four outcome counts, the shrink level, the number of configurations searched, then `panes` (one object per pane with outcome, monitor, rect, min/ideal dimensions, scale_percent, area_percent, stack_group, stacked_with, satisfaction_per_mille, contribution and reason) and `monitors` (logical size, used and free area, and which units landed there). advise emits the current summary plus `recommended` and `alternatives`, each carrying the COMPLETE candidate topology in the same format the --displays flag accepts. You can lift it straight out of the JSON and feed it back in. EXIT CODES ========== 0 Success. This includes answers you may not like: a scene that had to drop half its panes, and a scene reported INFEASIBLE because a required pane cannot be placed, are both legitimate answers, not failures. Explicit help also exits 0, on stdout. 1 Bad invocation (unknown command, missing required flag, unknown preset, two scenes not supplied to compare) or an I/O error (unreadable or malformed scene or displays file, unwritable SVG path). Usage in this case goes to stderr.