Multiple terminals per tab, rearrangeable by drag or keyboard

A tab is now a view holding an ordered list of panes rather than a single
terminal. New panes open side by side; moving one against a top or bottom
edge restacks the view, and against a side edge returns it to a row.

Layout is a flat list with one orientation per view rather than a split
tree. That covers a shell beside an agent without the structure a tree
needs, and it can be replaced once mixed layouts actually matter.

Panes live in a GtkBox and are reordered in place, so rearranging never
unparents a terminal and running processes and scrollback survive the move.

Dragging a pane uses its header as the handle so it never competes with the
terminal's own mouse handling, and drops go through the same moveRelative
path as the Ctrl+Shift+arrow shortcuts.

Also fixes an ordering bug this surfaced: View.create used to add its first
pane immediately, firing the title callback into a Tab that had not been
initialized yet. The view is now created empty and the caller adds the pane
once it has finished wiring itself up.
This commit is contained in:
Greyson Parrelli
2026-08-11 10:59:21 -04:00
parent 74bc9e8fd3
commit 5b00c98e03
6 changed files with 768 additions and 63 deletions
+35 -6
View File
@@ -77,7 +77,9 @@ just the VT core.
```
main.zig AdwApplication, CSS loading
Window.zig sidebar + GtkStack of terminals, tab management, shortcuts
Window.zig sidebar + GtkStack of views, tab management, shortcuts
View.zig one tab's content: an ordered list of panes + their layout
Pane.zig a terminal plus its header, drag source, and drop target
Terminal.zig GtkDrawingArea: Cairo/Pango renderer + input handling
Session.zig libghostty-vt Terminal + parser, fed by the PTY
Pty.zig openpt/fork/exec, controlling terminal setup
@@ -85,6 +87,20 @@ key.zig GDK keyval -> libghostty-vt key mapping
theme.zig colors libghostty-vt has no opinion about
```
A tab is a **view**, and a view holds one or more terminal **panes**. New panes
open side by side; moving a pane to a top or bottom edge restacks the view, and
moving it to a side edge puts it back in a row.
**Layout is a flat list, not a split tree.** A view has one orientation shared
by all its panes, so it can express "three side by side" or "three stacked" but
not "two beside a stack of three". A tree would handle the general case and is
where this goes if mixed layouts turn out to matter; it just brings a lot of
structure that a shell-next-to-an-agent view doesn't need yet.
Panes are held in a `GtkBox`, which can reorder its children in place. Nothing
is ever unparented, so rearranging a view never disturbs the running
terminals — their scrollback and processes carry straight through the move.
Two design choices worth calling out:
**No IO thread.** The PTY is read on the GLib main loop through a unix fd
@@ -108,20 +124,29 @@ in principle, but a terminal grid is small.
- Block / bar / underline / hollow cursor styles
- Scrollback via mouse wheel; typing snaps back to the prompt
- Resize reflows the grid and notifies the child
- Window title (OSC 0/2) becomes the tab label
- Tabs: create, close, switch; closing the last one closes the window;
a child exiting closes its own tab
- Window title (OSC 0/2) becomes the pane header and tab label; the tab shows
its pane count once a view holds more than one
- Tabs: create, close, switch; closing the last one closes the window
- Multiple terminals per tab, rearranged by keyboard or by dragging a pane's
header; closing the last pane in a view closes its tab
### Shortcuts
| | |
|---|---|
| `Ctrl+Shift+T` | new tab |
| `Ctrl+Shift+W` | close tab |
| `Ctrl+Shift+E` | new terminal in the current tab |
| `Ctrl+Shift+W` | close the focused terminal (closes the tab with its last one) |
| `Ctrl+Shift+←/→/↑/↓` | move the focused terminal within its view |
| `Ctrl+Shift+V` | paste (bracketed-paste aware, refuses unsafe pastes) |
| `Ctrl+PageUp/PageDown` | previous / next tab |
| `Alt+1`..`Alt+8` | jump to tab N, `Alt+9` jumps to the last |
Dragging a pane by its header does the same thing as the arrow shortcuts: the
edge you drop against decides both the order and the view's orientation. The
header is the drag handle rather than the whole pane so that dragging never
competes with the terminal's own mouse handling.
## Not implemented
This is a proof of concept, and the following are deliberately absent:
@@ -132,7 +157,11 @@ This is a proof of concept, and the following are deliberately absent:
- **Ligatures and complex shaping.** Each run is drawn independently at a
fixed grid offset, so text that needs shaping across cell boundaries won't
look right.
- **Kitty graphics, hyperlinks, tab reordering, split panes, config file.**
- **Resizable splits.** Panes in a view divide the space evenly; there are no
draggable dividers. Moving to `GtkPaned` would add them.
- **Mixed layouts and moving panes between tabs.** See the flat-list note
above; panes can only be rearranged within their own view.
- **Kitty graphics, hyperlinks, tab reordering, config file.**
- **Custom terminfo.** `TERM` is reported as `xterm-256color` rather than
`ghostty`, since we don't install a terminfo entry.