Add ability to set boot layout.

This commit is contained in:
Greyson Parrelli
2026-08-13 10:52:58 -04:00
parent 6c7c3bfa63
commit 4e622b15af
9 changed files with 1330 additions and 90 deletions
+65 -9
View File
@@ -4,7 +4,7 @@ A proof-of-concept workspace built on
[libghostty-vt](https://github.com/ghostty-org/ghostty) and GTK4/libadwaita:
**vertical tabs**, each holding a split tree of terminal and web panes, with
**saved layouts** that open a whole arrangement — directories, scripts and all —
in one go.
in one go, and a **startup list** that opens the ones you always want.
The sidebar holds the window controls, a new-tab button, one row per tab, and a
settings gear at its foot — the layout Zen Browser uses for vertical tabs —
@@ -124,8 +124,8 @@ Pty.zig openpt/fork/exec, controlling terminal setup
script.zig runs a command for `$(...)` in a layout's directory
key.zig GDK keyval -> libghostty-vt key mapping
theme.zig colors libghostty-vt has no opinion about, per scheme
Settings.zig preferences: model, JSON on disk
SettingsDialog.zig the settings page
Settings.zig preferences: theme and startup tabs, JSON on disk
SettingsDialog.zig the settings page, including the startup list editor
TabSettingsDialog.zig one tab's own settings, and the emoji picker
emoji.zig generated: every emoji the picker offers, and the search over them
```
@@ -288,6 +288,55 @@ A node is a split if it has a `split` key and a leaf otherwise. Saves are
atomic — written to a temporary and renamed — so an interrupted write leaves
the previous layouts intact rather than a file that won't parse.
## Startup tabs
Opening the same three layouts against the same three worktrees every morning is
the kind of thing the app should be doing for you. **Settings → Startup** is a
list of tabs to open at launch: one row per tab, each naming a saved layout, the
values to fill its parameters in with, and optionally a name and an emoji to pin
on the row.
The fastest way to fill it in is *Use current tabs*: arrange the window you want,
then say that this is the window you always want. It writes down one entry per
open tab, in sidebar order, with whatever each was opened with.
What is saved is the **recipe, not the session**. An entry is the layout's name
and a handful of values, so it is a few lines you can read, edit and keep — and
the layout itself stays the thing you maintain. Nothing here restores a
scrollback, a running command, or a shell that has since wandered into another
directory; launching re-runs the arrangement, and that was the tedious part.
Rows in the list can be reordered, and the first one is the tab you land in. An
entry with no layout picked opens a plain shell, which is worth having — a
startup list is often two configured tabs and one ordinary shell to work in. An
entry naming a layout that has since been renamed or deleted is skipped with a
warning rather than silently costing you a tab, and the settings page keeps
showing it — with *This layout is no longer saved* under it — so the values you
typed for it survive until you deal with it.
Parameters that the entry doesn't mention open at the layout's own default, which
is the same rule the parameter prompt follows. So an entry only has to carry the
values that differ from what the layout already suggests.
The list lives in `settings.json` beside the theme:
```json
{
"version": 1,
"theme": "dark",
"startup": [
{ "layout": "Project", "name": "signal", "emoji": "🚀",
"parameters": { "path": "~/src/signal" } },
{ "layout": "Project", "parameters": { "path": "~/src/playpen" } },
{ "name": "scratch" }
]
}
```
With no `startup` list the app opens a single shell, exactly as it did before
this existed — and if a list opens nothing at all, that is what you get too,
since a window you can't type in is not an outcome worth being faithful for.
Two design choices worth calling out:
**No IO thread.** The PTY is read on the GLib main loop through a unix fd
@@ -324,6 +373,9 @@ in principle, but a terminal grid is small.
- **Saved layouts**: whole tabs — panes, splits, ratios, per-pane directories
and scripts — opened in one go, parameterised by `{{name}}`, authored by
arranging a tab and saving it. See [Layouts](#layouts)
- **A startup list**: the tabs to open at launch, each a layout with its
parameters filled in, fillable from the tabs you have open now. See
[Startup tabs](#startup-tabs)
- **Pane status in the tab strip**, driven by OSC 9;4, so a tab can say whether
it is working, waiting on you, or finished and still unanswered. See
[Agent status](#agent-status)
@@ -566,16 +618,18 @@ the tab simply keeps the name it had.
Deep navy surfaces with Signal's ultramarine (`#3a76f0`) as the accent, in a
light and a dark scheme. `Ctrl+,` or the gear at the foot of the sidebar opens
**Settings**, which currently holds one choice: light, dark, or **system**,
which is the default and follows the desktop.
**Settings**, whose Appearance section holds the choice: light, dark, or
**system**, which is the default and follows the desktop.
The preference lives in `~/.config/playpen/settings.json` (or
`$XDG_CONFIG_HOME`), beside `layouts.json`:
`$XDG_CONFIG_HOME`), beside `layouts.json`, alongside the
[startup tabs](#startup-tabs):
```json
{
"version": 1,
"theme": "system"
"theme": "system",
"startup": []
}
```
@@ -632,8 +686,10 @@ This is a proof of concept, and the following are deliberately absent:
nothing else: no bookmarks, history, downloads, devtools, or find-in-page,
and each pane uses WebKit's default context, so nothing is persisted between
runs. Links that ask for a new window are ignored rather than opening a pane.
- **Restoring a session.** Layouts save arrangements you open deliberately;
nothing restores the tabs you happened to have open when the app closed.
- **Restoring a session.** [Startup tabs](#startup-tabs) reopen the
arrangement you asked for, but nothing captures the state a session got itself
into: no scrollback, no running commands, no directories a shell wandered off
to. A tab comes back as its layout, not as you left it.
- **Kitty graphics, hyperlinks, tab reordering.**
- **Custom terminfo.** `TERM` is reported as `xterm-256color` rather than
`ghostty`, since we don't install a terminfo entry.