TUIOSTUIOS

BSP Tiling

Advanced window tiling with Binary Space Partitioning

TUIOS ships three tiling layouts. BSP is the default and the one with manual split control, preselection, and edge resizing. This page covers BSP in depth and tells you how to reach the other two.

Layout Modes

Tiling is a toggle (on or off), and the tiling layout is a separate setting that TUIOS remembers while tiling is off.

ModeBehavior
bspBinary space partitioning. A tree of splits, any number of panes, each independently resizable. The default.
master-stackFixed arrangements by window count: 1 full screen, 2 side by side, 3 master plus stacked pair, 4 a 2x2 grid, 5 or more a grid (2 columns up to 6 windows, 3 columns beyond).
scrollingNiri-style horizontal columns. You scroll the workspace instead of shrinking every pane.
floatingNot a layout mode. This is what you get when tiling is off, and the mode you were in comes back when you turn tiling on again.

Switching modes

There is no keybinding for layout mode. Open the command palette with Ctrl+B P and run one of:

  • Layout: BSP Tiling
  • Layout: Master-Stack
  • Layout: Scrolling (niri-style)
  • Layout: Disable Tiling

There is no "cycle layout" key. Next Layout and Previous Layout in the palette cycle your saved layout templates, not layout modes. A ToggleLayoutMode function exists in the code that cycles bsp to master-stack to scrolling, but nothing calls it outside tests, so it is not reachable from the UI.

Enable Tiling

KeyWhere
tWindow management mode
Ctrl+B SpaceAny mode
Ctrl+B t tWindow prefix menu

Tiling state is per workspace. Switching workspaces can land you somewhere tiling is off.

The dock shows the tiling icon followed by V or H, which is the direction the next split will take, not the current mode. There is no TILING text label.

Auto-Insertion Schemes

When a new window is added without a preselection, the tree picks a split direction using its auto scheme. New trees default to spiral.

SchemeRule
longest_sideSplit along the longer dimension of the target pane.
alternateAlternate V, H, V, H based on the total number of splits in the tree.
spiralbspwm-style. Alternate on the depth of the pane being split, so repeatedly splitting the newest pane rotates V, H, V, H. Default.
smart_splitPane wider than twice its height splits vertically; taller than it is wide splits horizontally; otherwise falls back to depth parity.

The scheme has no config key and no keybinding. It travels in session state and in saved layout templates (tiling_scheme), so in practice the only way to change it is to load a template that carries one. A fresh workspace is always spiral.

The Smart Split command in the palette forces one insertion through smart_split regardless of the tree's scheme. It has no keybinding.

Creating Splits

A split command creates a window. SplitFocusedHorizontal and SplitFocusedVertical set a preselection and immediately call AddWindow. Pressing n after a split adds a third window, it does not fill a reserved gap. There is no empty-slot state in TUIOS.

These are bound both directly in window management mode and behind the prefix:

KeyPrefix formAction
-Ctrl+B -Split horizontal, new window below
| or \Ctrl+B |Split vertical, new window right
RCtrl+B RRotate the split containing the focused window
=Ctrl+B =Equalize every split ratio to 0.5

Rotating flips a vertical split to horizontal and back, so side-by-side panes become stacked.

Preselection

Preselection tells the tree where the next window goes relative to the focused one.

KeyNext window appears
Alt+hLeft
Alt+lRight
Alt+kAbove
Alt+jBelow

On macOS these are Opt+h/j/k/l.

Preselection only applies when tiling is on. It is consumed by the next window creation and then resets.

There is no key that cancels a preselection. ClearPreselection exists but its only caller is the tape executor. Once set, a preselection stays until a window consumes it, or until a split command overwrites it with its own direction.

Swapping Windows

Swaps move windows between leaves without changing the split structure.

KeyAction
Shift+H or Ctrl+Swap with the pane to the left
Shift+L or Ctrl+Swap with the pane to the right
Shift+K or Ctrl+Swap with the pane above
Shift+J or Ctrl+Swap with the pane below

Lowercase h/j/k/l move focus; the shifted forms swap. Dragging one window onto another with the mouse also swaps them.

Resizing

Each key moves one edge of the focused window by a fixed number of cells. These are split-line moves, not ratio adjustments, and every window sharing the moved divider moves with it.

KeyEdge movedStep
>Right edge, outward4 columns
<Right edge, inward4 columns
.Left edge, outward4 columns
,Left edge, inward4 columns (see below)
}Bottom edge, downward2 rows
{Bottom edge, upward2 rows
]Top edge, upward2 rows
[Top edge, downward2 rows

, does not resize. Window management mode intercepts it to open the settings page before the keybind registry ever sees it, so the resize_master_shrink_left binding is unreachable. . works normally.

A resize is a no-op when the edge you are moving is already at the screen boundary. Growing the right edge of a pane that is already flush with the right side of the screen does nothing; move the left edge instead.

How resizing actually works

Resize in BSP is geometry-first. The tree is reconciled afterwards.

Move the geometry

adjustTilingNeighborsGeneric takes the moved edge's old pixel coordinate, finds every window whose corresponding edge sits within 1 cell of it, and splits them into the two sides of that divider.

Constrain

The requested new divider position is clamped so that no window on either side falls below the minimum size (20 columns, 5 rows).

Rewrite rectangles

Window X, Y, Width, and Height are written directly. The tree is not consulted at all in this step.

Re-derive ratios

SyncRatiosFromGeometry walks the tree and recomputes every split ratio from the resulting rectangles.

The last step is not optional. From the code comment on MarkBSPSyncPending:

the tree ratios, not the window rectangles, are what survives a retile, so a drag that ended without a final sync would have its result discarded the next time the layout was applied.

For mouse drags the sync is deferred to the next composed frame (its only job mid-drag is to keep the separator overlay under the pointer), and it always runs unconditionally on mouse release.

Resize invariants

These are pinned by tests in internal/layout/bsp_resize_axis_test.go, exercised across both shared-border settings, five layouts (two panes, tall-left, tall-right, 2x2 grid, and a three-level nest), every pane, and all four edges.

InvariantMeaning
Axis isolationA horizontal resize may change only X and W. A vertical resize may change only Y and H.
Round-trip axis isolationDrag any edge, sync the geometry back into the tree, re-apply the layout: the perpendicular axis of every pane is unchanged.
No driftMany small drags land where one large drag lands, and the perpendicular axis accumulates no drift over a long sequence.
Sync stabilitySyncing an unmodified layout and re-applying it is a no-op. Any movement here would show up as panes creeping on every mouse motion event.

Split ratios in BSP are stored as a plain float64 and are not clamped to a band. The 0.3 to 0.7 clamp you may have read about applies only to the master-stack layout's master ratio.

Shared Borders

By default each tiled pane draws its own border, so adjacent panes show two border lines between them. Shared borders collapses that into one.

SettingValue
Config keyappearance.shared_borders
Defaultfalse
CLI flag--shared-borders
In-appSettings page (, in window management mode)
[appearance]
shared_borders = true

This applies to BSP only. The scrolling layout ignores it.

Panes are asymmetric under shared borders

A split reserves one cell for the separator, and that cell is taken from the right or bottom pane. The two children of a 0.5 split are not the same size.

Bounds X=0, W=160, one vertical split at ratio 0.5:

Shared bordersLeft paneSeparatorRight pane
offX=0, W=80noneX=80, W=80
onX=0, W=80column x=80X=81, W=79

The same one-cell deduction applies to horizontal splits, taken from the bottom pane. Ratio syncing uses the identical model; if it did not, every ratio in the tree would be re-derived one cell off on each sync and a resize on one axis would walk the dividers on the other.

Mouse affordance

With shared borders on, hovering the pointer over a separator changes the cursor shape: horizontal resize over a vertical divider, vertical resize over a horizontal one. Drag from there to move the divider.

With shared borders off there is no separator to hover, so tiled panes fall back to their own border edges and corners for resize handles.

Layout Templates

Save the current arrangement and restore it later.

KeyAction
Ctrl+B L sSave layout (prompts for a name)
Ctrl+B L lLoad layout (searchable picker)

The same two are in the command palette as Save Layout and Load Layout, and Next Layout / Previous Layout cycle through saved templates in order. A template records the tiling scheme alongside the geometry.

From a script or a tape, SaveLayoutExec and LoadLayoutExec take a template name directly.

Persistence

Each workspace owns its own BSP tree, kept in WorkspaceTrees keyed by workspace number. Trees are serialized into session state, so they survive detach and reattach, and are written to disk by the daemon for resurrection. See Sessions for what resurrection does and does not restore.

The tree is rebuilt from scratch when it goes stale, for example when it still references a window that has closed. A rebuild re-inserts the current windows in order and then syncs ratios back from their live positions, so a rebuild preserves proportions rather than resetting to 50/50.

Known Gaps

  • Layout mode has no keybinding. Command palette only.
  • The auto-insertion scheme is not configurable. No config key, no key, no palette entry that sets it directly.
  • , is dead in window management mode. It opens settings instead of resizing.
  • Stacked splits are unreachable. SplitStacked is a real split type, and both layout application and ratio syncing handle it (the inactive child collapses to a one-row title bar). Nothing in the app ever constructs such a node, so you cannot produce one.
  • Layout application is not batched. ApplyBSPLayout loops over windows and creates a per-window snap animation. There is no single atomic geometry commit.

Troubleshooting

Windows are not tiling

Check that tiling is on for this workspace: the dock shows the tiling icon plus V or H when it is. Press t to toggle. Tiling state does not follow you across workspaces.

If windows are arranging themselves but not the way BSP would, you may be in master-stack or scrolling mode. Run Layout: BSP Tiling from the palette.

A resize key does nothing

Either the edge you are moving is already at the screen boundary, or the move would push a pane below 20 columns or 5 rows and was clamped away. Try the opposite edge key: . instead of >, ] instead of }.

If it is specifically , that does nothing, that key opens settings and cannot resize.

The layout looks lopsided after resizing

Press = to reset every split ratio to 0.5. Note that under shared borders this still leaves each right/bottom pane one cell smaller than its sibling, by design.

Swap does nothing

Swaps are directional. If there is no pane in that direction, nothing happens. Mouse drag onto another window always swaps.

A preselection fired in the wrong direction

Preselection is one-shot and cannot be cancelled. A split command sets its own preselection, so pressing - after Alt+l overwrites "right" with "below".

On this page