Add custom layout creation.

This commit is contained in:
Greyson Parrelli
2026-08-11 13:57:54 -04:00
parent a7a9429d09
commit 3026bdcd4c
14 changed files with 1964 additions and 20 deletions
+91 -2
View File
@@ -112,6 +112,9 @@ Pane.zig content plus its header, drag source, and drop target
Terminal.zig GtkDrawingArea: Cairo/Pango renderer + input handling
Browser.zig WebKitWebView plus a back/forward/reload/address bar
webkit.zig hand-written bindings for the WebKitGTK calls we make
Layouts.zig saved tab templates: model, JSON on disk, {{substitution}}
OpenLayoutDialog.zig prompts for a layout's parameters
SaveLayoutDialog.zig turns the current tab into a saved layout
Session.zig libghostty-vt Terminal + parser, fed by the PTY
Pty.zig openpt/fork/exec, controlling terminal setup
key.zig GDK keyval -> libghostty-vt key mapping
@@ -154,6 +157,88 @@ preview. Cancelling a drag puts the pane back: only the dragged pane ever
moves, so the rest of the tree is unchanged and re-inserting it beside its
original sibling restores the original shape.
## Layouts
A **layout** is a saved tab: an arrangement of panes, each with a directory and
a script, that opens in one go. Layouts take **parameters**, so one layout
serves any number of projects.
The layout button in the sidebar lists them. Picking one asks for its
parameters — prefilled with their defaults — and opens a new tab. A layout with
no parameters skips the prompt. The plain new-tab button is untouched: it still
opens one shell, immediately.
**You author layouts by arranging a tab.** Split it, drag panes around, drag the
dividers, then *Save tab as layout…*. That captures the tree, the split
orientations and the ratios exactly as they are on screen, and asks only for
what it can't infer: a name, the parameters, and each pane's script. There is
deliberately no separate layout builder — the split tree already is one.
The save dialog prefills what it can read off the live tab: each terminal's
current directory, straight out of `/proc/<pid>/cwd`, and each web pane's
current page. So the usual flow is to get a tab set up the way you like,
save it, and replace the literal paths with `{{parameters}}`.
**Editing** a saved layout opens the same dialog on the stored one, so its
name, parameters and per-pane scripts can be changed without opening it.
Renaming moves the layout rather than copying it, and renaming onto a name
another layout already has is refused instead of quietly replacing it. Nothing
is written until you confirm, so cancelling leaves the layout untouched. To
change the *shape* of a layout, open it, rearrange the tab, and save over it
under the same name — the same tools you used to build it in the first place.
Every `cwd`, `command` and `url` goes through `{{name}}` substitution, and a
leading `~` is expanded afterwards. An unknown `{{name}}` is left as written
rather than blanked, so a typo shows up in the pane instead of silently
producing an empty path.
**Scripts are typed into the shell, not run instead of it.** A pane starts your
login shell as usual, and the script is fed to it once it is ready. The shell
is still there when the script finishes, with your environment loaded and the
command in history. The wait matters: writing at spawn time loses the input,
because shells that set up line editing discard whatever was buffered while
they were initializing. The shell's first output is the signal that it is
reading, so that is when the script goes in.
Layouts live in `~/.config/vtabs/layouts.json` (or `$XDG_CONFIG_HOME`), and the
file is meant to be edited by hand as well — *Reload from disk* picks up
changes. It is JSON because the app writes it too, and a format that
round-trips without a hand-written emitter is worth more here than a prettier
one.
```json
{
"version": 1,
"layouts": [
{
"name": "Project",
"parameters": [
{ "name": "path", "description": "Project directory", "default": "~" }
],
"root": {
"split": "horizontal",
"ratio": 0.55,
"first": {
"kind": "terminal",
"cwd": "{{path}}",
"command": "git status"
},
"second": {
"split": "vertical",
"ratio": 0.5,
"first": { "kind": "terminal", "cwd": "{{path}}", "command": "nvim ." },
"second": { "kind": "web", "url": "https://github.com" }
}
}
}
]
}
```
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.
Two design choices worth calling out:
**No IO thread.** The PTY is read on the GLib main loop through a unix fd
@@ -187,6 +272,9 @@ in principle, but a terminal grid is small.
dragging around exactly like they do: back/forward/reload, an address bar
that takes a URL or falls back to a search, a load-progress indicator in the
entry, and the page title feeding the pane header and tab label
- **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)
### Shortcuts
@@ -225,8 +313,9 @@ 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.
- **Saved layouts.** A view's arrangement lives only as long as the tab.
- **Kitty graphics, hyperlinks, tab reordering, config file.**
- **Restoring a session.** Layouts save arrangements you open deliberately;
nothing restores the tabs you happened to have open when the app closed.
- **Kitty graphics, hyperlinks, tab reordering.**
- **Custom terminfo.** `TERM` is reported as `xterm-256color` rather than
`ghostty`, since we don't install a terminfo entry.