Generated 2026-04-02 from possibilities--prise branch arthack-prod
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.
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
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)
},
},
}){
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
},
}Either a pane (leaf) or a split (branch):
{
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
}{
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)
}apply_layout(name)is called (from keybind or command palette)- Layout definition is looked up in
config.layouts[name] - Validation: must have tabs, tabs must have roots, total pane count > 0
- A pending layout state object is created (
state.pending_layout) - PTYs are spawned for every leaf pane + floating pane (via
prise.spawn()) - Each spawned PTY triggers a callback; PTYs are queued in order
- Once all PTYs are received (
panes_received == panes_needed), finalization begins
- Validates pane count matches (spawned PTYs == expected)
- Builds new tab tree by assigning queued PTYs to layout nodes depth-first
- Closes all existing tabs (kills all old PTYs)
- Replaces
state.tabswith the new tab array - Sets active tab, resets split IDs, requests frame redraw
~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)
- 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
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)Columnwith title +Listwidget showing layout names- Each item shows: name, tab count, total pane count
- Navigation:
j/k/arrows to move,Enterto apply,Escapeto close - Mouse click support on list items
Rendering: build_layout_picker() at line 3784 constructs the widget tree.
| 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 |
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:
- Collects all current PTY references from the existing split tree
- Builds a new split tree geometry (the desired layout algorithm)
- Reassigns PTY references to the new tree positions
- Replaces the tab's
rootwithout callingpty: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.
layouts = {
["dev"] = {
name = "dev",
tabs = {
{
title = "editor",
root = { type = "pane", cwd = "~/code/project", cmd = "nvim" },
},
{
title = "term",
root = { type = "pane", cwd = "~/code/project" },
},
},
},
}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" },
},
},
},
},
},
},
},
}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,
},
},
},
},
}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| 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).
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.