Skip to content

Instantly share code, notes, and snippets.

@possibilities
Created April 3, 2026 01:42
Show Gist options
  • Select an option

  • Save possibilities/aca61d52b3349e4e20e5c1fd503f58d9 to your computer and use it in GitHub Desktop.

Select an option

Save possibilities/aca61d52b3349e4e20e5c1fd503f58d9 to your computer and use it in GitHub Desktop.

Prise Layout System — Full Report

Generated 2026-04-02 from possibilities--prise branch arthack-prod

What It Is

Prise has a workspace launcher system called "layouts." You define named presets — each one describes a complete session: tabs, pane splits, working directories, and startup commands. When you pick a layout, prise destroys your current session and rebuilds it from the definition.

This is not tmux's select-layout (main-vertical, tiled, etc.) which rearranges existing panes in place. Prise layouts are destructive — all running processes are killed and PTYs are respawned.

Think tmuxinator/teamocil, but in Lua, native to the multiplexer.

Source Location

All layout logic lives in src/lua/tiling.lua in the prise fork (~/src/possibilities--prise).

Key sections:

  • Type definitions: lines 70-98
  • Pane counting: lines 1319-1344
  • Pending layout state machine: lines 1349-1358
  • Node tree builder: lines 1365-1416
  • Tab builder: lines 1469-1508
  • Finalization (close old, apply new): lines 1512-1575
  • Path expansion (~ support, existence validation): lines 1580-1613
  • PTY spawning: lines 1617-1626
  • apply_layout() entry point: lines 1631-1673
  • Layout picker UI: lines 1687-1716, 3784-3880
  • Action handler: line 2481

Schema

Top Level (in ui.setup())

ui.setup({
    layouts = {
        ["layout-name"] = {
            name = "layout-name",          -- string, layout identifier
            active_tab = 1,                -- integer, optional, which tab to focus (default: 1)
            tabs = { ... },                -- array of PriseLayoutTab (required, at least one)
        },
    },
})

PriseLayoutTab

{
    title = "Tab Name",            -- string, optional tab title
    root = <PriseLayoutNode>,      -- required, the pane/split tree
    floating = {                   -- optional, floating pane overlay for this tab
        pane = <PriseLayoutPane>,  -- the floating pane definition
        visible = true,            -- boolean, start visible? (default: true)
        width = 80,                -- number, override default floating width
        height = 24,               -- number, override default floating height
    },
}

PriseLayoutNode (union type)

Either a pane (leaf) or a split (branch):

PriseLayoutPane

{
    type = "pane",
    cwd = "~/code/project",    -- string, optional, working directory (~ expanded)
    cmd = "nvim",              -- string, optional, command to run after shell starts
    ratio = 0.6,               -- number 0-1, optional, relative size within parent split
}

PriseLayoutSplit

{
    type = "split",
    direction = "row",         -- "row" (side by side) or "col" (stacked vertically)
    ratio = 0.3,               -- number 0-1, optional, this split's size within parent
    children = { ... },        -- array of PriseLayoutPane | PriseLayoutSplit (nestable)
}

How It Works Internally

Lifecycle

  1. apply_layout(name) is called (from keybind or command palette)
  2. Layout definition is looked up in config.layouts[name]
  3. Validation: must have tabs, tabs must have roots, total pane count > 0
  4. A pending layout state object is created (state.pending_layout)
  5. PTYs are spawned for every leaf pane + floating pane (via prise.spawn())
  6. Each spawned PTY triggers a callback; PTYs are queued in order
  7. Once all PTYs are received (panes_received == panes_needed), finalization begins

Finalization (finalize_layout)

  1. Validates pane count matches (spawned PTYs == expected)
  2. Builds new tab tree by assigning queued PTYs to layout nodes depth-first
  3. Closes all existing tabs (kills all old PTYs)
  4. Replaces state.tabs with the new tab array
  5. Sets active tab, resets split IDs, requests frame redraw

Path Handling

  • ~ is expanded to $HOME
  • Paths are validated for existence (file or directory probe via io.open)
  • Invalid paths log a warning and return nil (pane spawns with default cwd)

Error Handling

  • If a PTY fails to arrive, cleanup closes any already-spawned new PTYs
  • If pane count mismatches, the layout is aborted and pending state cleared
  • If a layout is already pending, the new request is rejected with a warning

The Layout Picker UI

Triggered by the layout_picker action (or <leader>o in default keybinds).

Guard: Only shows if config.layouts is non-empty (next(config.layouts) ~= nil). If no layouts are defined, the action silently does nothing.

UI: A floating modal built with prise widgets:

  • Positioned (top center, y=5)
  • Box (rounded border, max width 50)
  • Column with title + List widget showing layout names
  • Each item shows: name, tab count, total pane count
  • Navigation: j/k/arrows to move, Enter to apply, Escape to close
  • Mouse click support on list items

Rendering: build_layout_picker() at line 3784 constructs the widget tree.

Comparison with tmux Layout Systems

Feature tmux select-layout tmux tmuxinator Prise layouts
Rearranges existing panes Yes No No
Preserves running processes Yes No No
Adapts to current pane count Yes (dynamic) No No
Spawns new processes No Yes Yes
Runs startup commands No Yes Yes
Nested split trees N/A Yes (YAML) Yes (Lua tables)
Floating pane support No No Yes
Per-tab floating config No No Yes
Dynamic/computed layouts No ERB templates Full Lua
Tab titles N/A Yes Yes
Active tab selection N/A Yes Yes

What's Missing (vs. tmux)

No in-place rearrangement. tmux's main-vertical, main-horizontal, even-horizontal, even-vertical, and tiled layouts take your existing panes and rearrange them without killing processes. Prise has no equivalent. This would require a new function that:

  1. Collects all current PTY references from the existing split tree
  2. Builds a new split tree geometry (the desired layout algorithm)
  3. Reassigns PTY references to the new tree positions
  4. Replaces the tab's root without calling pty:close() on anything

The data structures support this — the split tree is just Lua tables and PTY userdata references — but no such function exists today. It would be a fork-level change in tiling.lua.

No layout cycling. tmux's next-layout (C-b Space) cycles through built-in layouts. Even if prise had in-place rearrangement, there's no built-in set of layout algorithms to cycle through.

No per-pane preserve. You can't say "keep pane 1's PTY but replace pane 2." It's all-or-nothing.

Examples

Simple: Editor + Terminal

layouts = {
    ["dev"] = {
        name = "dev",
        tabs = {
            {
                title = "editor",
                root = { type = "pane", cwd = "~/code/project", cmd = "nvim" },
            },
            {
                title = "term",
                root = { type = "pane", cwd = "~/code/project" },
            },
        },
    },
}

Main-Vertical Style (Static)

layouts = {
    ["main-v"] = {
        name = "main-v",
        tabs = {
            {
                title = "work",
                root = {
                    type = "split",
                    direction = "row",
                    children = {
                        { type = "pane", cwd = "~/code/project", cmd = "nvim", ratio = 0.6 },
                        {
                            type = "split",
                            direction = "col",
                            ratio = 0.4,
                            children = {
                                { type = "pane", cwd = "~/code/project" },
                                { type = "pane", cwd = "~/code/project" },
                            },
                        },
                    },
                },
            },
        },
    },
}

With Floating Pane

layouts = {
    ["monitor"] = {
        name = "monitor",
        tabs = {
            {
                title = "main",
                root = { type = "pane", cwd = "~/code/project" },
                floating = {
                    pane = { type = "pane", cwd = "~/code/project", cmd = "btop" },
                    visible = false,   -- hidden by default, toggle with floating_toggle
                    width = 120,
                    height = 35,
                },
            },
        },
    },
}

Dynamic (Lua Power)

Since it's all Lua, you can generate layouts programmatically:

local projects = { "dotfiles", "webapp", "api" }
local layouts = {}

for _, name in ipairs(projects) do
    layouts[name] = {
        name = name,
        tabs = {
            {
                title = "code",
                root = { type = "pane", cwd = "~/code/" .. name, cmd = "nvim" },
            },
            {
                title = "shell",
                root = { type = "pane", cwd = "~/code/" .. name },
            },
        },
    }
end

Available Actions

Action What it does
layout_picker Opens the picker modal (requires layouts to be defined)

There is no apply_layout action exposed directly through the keybind system — the picker is the only entry point. To apply a layout programmatically, you'd need to call apply_layout(name) from within tiling.lua (it's a local function, not exported).

Summary

Prise layouts are a workspace bootstrapping system, not a pane rearrangement system. They're powerful for project-specific setups (especially with Lua's dynamism), but they don't fill the gap left by tmux's select-layout family. Adding in-place rearrangement would be a meaningful enhancement to the fork — the architecture supports it, it just hasn't been built.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment