Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions docs/adrs/022.client.canvas-fill.md
Original file line number Diff line number Diff line change
Expand Up @@ -250,6 +250,8 @@ floor-rounding gap, but the 15px offset disappears).

## Related Decisions

- [ADR 033 — fit without the scrollbar reserve](033.client.padding-fit.md): drops the
reserve locally, ahead of the upstream fix above, and adds `theme.padding`.
- [ADR 017 — SGR mouse scroll sequences](017.client.mouse-scroll.md): same
pattern — a ghostty-web internal behaviour gap worked around at the webtty
client layer, with an upstream fix path documented.
Expand Down
77 changes: 77 additions & 0 deletions docs/adrs/033.client.padding-fit.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,77 @@
# ADR 033: Client — fit without the scrollbar reserve, and a `theme.padding` colour

**SPEC:** [client](../specs/client.md)
**Status:** Accepted
**Date:** 2026-09-21

---

## Context

[ADR 022](022.client.canvas-fill.md) centres the canvas by measuring the gap between the
container and the canvas and distributing it as padding on `#terminal`. It also records
that ghostty-web's `FitAddon` always subtracts a fixed 15px scrollbar reserve, even
though the container has no scrollbar, and that the fix belongs upstream.

The scrollbar in ghostty-web is drawn as an overlay inside the canvas, so the reserve
buys nothing, but it does leave a visible strip: about 15px plus the sub-cell remainder,
split across both sides. Against a full-screen TUI that paints its own edge cells, such
as a multiplexer with a darker sidebar, that strip is a band in the default background
colour between the browser chrome and the app. It shows at all four edges. Only
`theme.background` was available to colour it, and that is also the terminal's default
cell colour.

## Decision

Two changes, both aimed at the visible strip:

1. **Fit from the full container.** The client no longer uses `FitAddon`. A small
`computeGrid()` in `src/client/fit.ts` derives cols and rows from the container size
and the renderer's cell metrics, with no scrollbar reserve. The fit also runs again after the font is remeasured (`document.fonts.ready`), since that changes the cell size. What is left over, less
than one cell, is still distributed as padding by the existing measured-gap code, so
the canvas stays centred.
2. **An optional `theme.padding` colour.** The `#terminal` container, whose background
shows only in that padding, takes `theme.padding` when set and `theme.background`
otherwise, so existing configs render as before. (Superseded in part by [ADR 035](035.client.edge-extend.md): the leftover strip is now
extended from the edge cells, and `theme.padding` colours only the space set by `padding`.) `padding` is a webtty setting, not a
terminal colour, so the client removes it from the theme object before handing the
theme to ghostty-web.

## Considered Options

### Option A: Local fit without the reserve, plus `theme.padding` (chosen)
Removes almost all of the strip, and lets the rest be coloured to match the app. Costs a
small local replacement for `FitAddon` and one theme key.

### Option B: Wait for the upstream `FitAddon` option
ADR 022 already lists this contribution. It is the long-term fix, but it does not help
until released and adopted. The local fit is small and easy to delete once upstream
supports `scrollbarWidth: 0`.

### Option C: Only add the colour
Hides the strip instead of shrinking it, and the strip stays about 16px wide. Rejected as
the sole change; kept as the second half of this one.

### Option D: Colour the padding from the edge cells automatically
Needs per-frame pixel reads and still shows a seam where the edge colour varies. Rejected.

## Consequences

**Positive**
- The strip at the edges shrinks from about 16px to under one cell (typically a few px).
- The remainder can be coloured to match a full-screen app's edge colour.
- No change for existing configs, other than a slightly larger grid.

**Negative**
- The grid is up to one column wider than before (the reserve is gone), so a program that
hard-codes widths may reflow once.
- ghostty-web's overlay scrollbar, shown only while scrolling back, can overlap the last
column, since that strip is no longer left empty.
- Users choose one padding colour for all four edges, which is exact only if the app's
edge cells share it.

## Related Decisions

- [ADR 022](022.client.canvas-fill.md): the padding this builds on, and the upstream fix
path it documents.
- [ADR 008](008.webtty.config.md): theme keys.
66 changes: 66 additions & 0 deletions docs/adrs/034.client.padding-config.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,66 @@
# ADR 034: Client — configurable `padding` around the terminal canvas

**SPEC:** [client](../specs/client.md)
**Status:** Accepted
**Date:** 2026-09-21

---

## Context

[ADR 033](033.client.padding-fit.md) removed the 15px scrollbar reserve so the strip
around the canvas is only the sub-cell remainder, and added `theme.padding` to colour it.
That gives no way to ask for breathing room: the canvas always runs to the edge of the
browser tab, which some users prefer to avoid. Terminal emulators such as Ghostty and
Windows Terminal expose a padding setting for this.

## Decision

Add a top-level config key, `padding`: a non-negative number of CSS pixels reserved on
all four sides of the canvas, default `0` (no change for existing configs). Invalid values
(negative, non-numeric) fall back to `0`. It is served to the client through
`GET /api/config`.

The client subtracts twice the padding from each container dimension before computing
cols and rows, so the padding is reserved out of the same space the sub-cell remainder
comes from. The leftover space is then distributed as follows: the top and left edges are exactly
`padding`, and the leftover, less than one cell, goes to the right and bottom. So `padding`
alone decides the gap at the top and left, and `0` puts the terminal flush against the
browser toolbar and the left edge. Centring the leftover left a thin strip at the top and
left in a different colour from a full-screen app's edge cells, where it was very visible. The space set by `padding` is coloured by the container
background, `theme.padding` if set and `theme.background` otherwise, so the colour needs no new
setting. The leftover strip is not coloured by it: [ADR 035](035.client.edge-extend.md)
extends the edge cells' own background into it.

## Considered Options

### Option A: One `padding` number for all sides (chosen)
Simple and predictable, and matches the way the strip is already coloured and centred.

### Option B: Separate horizontal and vertical (or per-side) padding
More control, but more keys and validation for a case nobody has asked for yet. Can be
added later if needed.

### Option C: Percent or cell-based padding
Cells would keep the grid aligned but make the size depend on the font. Rejected in favour
of plain pixels.

## Consequences

**Positive**
- Users can add breathing room, and colour it to match their theme or app.
- Default `0` keeps existing behaviour.

**Negative**
- `theme.padding` (a colour) and `padding` (a size) share a word. They are in different
places (inside and outside `theme`), and the colour key is documented as the colour of
the padding, but they are easy to confuse.
- The leftover, up to one cell, now sits on the right and at the bottom instead of being
split around the canvas. `theme.padding` does not colour it: [ADR 035](035.client.edge-extend.md)
fills it from the edge cells' own background, so it is invisible apart from the bottom-right
corner, where the two strips meet and the container background shows.

## Related Decisions

- [ADR 033](033.client.padding-fit.md): the fit and `theme.padding` this builds on.
- [ADR 022](022.client.canvas-fill.md): the centring of the canvas.
73 changes: 73 additions & 0 deletions docs/adrs/035.client.edge-extend.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,73 @@
# ADR 035: Client — extend the edge colours into the leftover strip

**SPEC:** [client](../specs/client.md)
**Status:** Accepted
**Date:** 2026-09-21

---

## Context

The canvas is fitted to whole cells, so a strip of less than one cell is always left at the
right and bottom ([ADR 033](033.client.padding-fit.md), [ADR 034](034.client.padding-config.md)).
It cannot be removed: filling it would need rows of a fractional pixel height, and ghostty-web
draws cell backgrounds and text at those fractional positions, which shows as seams and
blur. A single colour hides the strip only where the app's edge cells share that colour. A
multiplexer with a dark sidebar next to a light main area has two edge colours along the
bottom, so any one colour leaves a visible patch.

Terminal emulators solve this by stretching the edge cells' own background into the
padding. Ghostty calls it `window-padding-color = extend`.

## Decision

The client always extends the edge colours into the leftover strip, with no setting. It
adds two overlay strips, one under the canvas and one to its right, and keeps them painted
from the canvas' last pixel row and last pixel column, stretched across the leftover. The
strip therefore takes on the background of the edge cells in every column and row.

Only the leftover is extended. The space set by `padding` keeps the container background
colour, `theme.padding` or `theme.background`, so `theme.padding` stays a plain colour and
only matters when `padding` is above `0` (and for the bottom-right corner, where the two
strips meet).

It is not an option because there is no reason to prefer a visible seam: extending only
affects less than one cell of leftover that otherwise shows a colour that matches nothing.

## Considered Options

### Option A: Always stretch the edge pixel row and column into the leftover (chosen)
Seamless for any app at any window size, no config, the same result as Ghostty's `extend`.
Costs two small overlay canvases repainted each frame, one `drawImage` each.

### Option B: Make it a setting, off by default
Keeps the old look for anyone who wants it, but adds a key for a behaviour nobody needs to
avoid, and leaves the seam by default. Rejected. Can be added if a real case appears.

### Option C: One colour for the leftover
Hides the strip only where the edge cells share the colour. This was the earlier behaviour.

### Option D: Stretch rows to fill the height
Removes the strip, but fractional row heights give seams between backgrounds and uneven
text. Rejected.

### Option E: Reflect the canvas with `-webkit-box-reflect`
CSS only, but it mirrors the last few pixels including any text descenders, and is
Chromium-only. Rejected.

## Consequences

**Positive**
- The strip is invisible at any window size, for any app, with no setting.
- `theme.padding` stays a plain colour with one job.

**Negative**
- Two extra canvases repainted every frame while a strip is present.
- The bottom-right corner, where both strips meet, stays the container background.
- If the edge cells' last pixel row is not their background, for example a full-width
line drawn on the very last row, that colour is stretched.

## Related Decisions

- [ADR 034](034.client.padding-config.md): `padding`, `theme.padding`, and where the leftover goes.
- [ADR 033](033.client.padding-fit.md): the fit that leaves the strip.
8 changes: 6 additions & 2 deletions docs/specs/client.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
# SPEC: Client

**Last Updated:** 2026-03-31
**Last Updated:** 2026-09-21

---

Expand Down Expand Up @@ -43,7 +43,7 @@ src/client/
3. Sets `document.title = sessionId + ' | webtty'`
4. Initialises a `ghostty-web` `Terminal` with config values (cols, rows, fontSize, fontFamily, cursorStyle, cursorStyleBlink, scrollback, theme, copyOnSelect, rightClickBehavior)
5. Connects to `ws://<host>/ws/:id?cols=<cols>&rows=<rows>` over WebSocket
6. Fits the terminal to the viewport and observes resize events via `FitAddon`
6. Fits the terminal to the viewport from the full container size (no scrollbar reserve, see ADR 033) and observes resize events
7. Sends a `{ type: 'resize', cols, rows }` JSON message on open and on every terminal resize
8. Forwards all keystrokes to the PTY via `term.onData`

Expand All @@ -56,6 +56,7 @@ src/client/
cols, rows, fontSize, fontFamily, cursorStyle, cursorStyleBlink, scrollback,
theme, copyOnSelect, rightClickBehavior,
mouseScrollSpeed, // used by the custom wheel handler, not passed to Terminal constructor
padding, // px around the canvas, used by fit(); coloured by theme.padding or theme.background
keyboardBindings // used by the keydown capture handler, not passed to Terminal constructor
}
```
Expand Down Expand Up @@ -167,3 +168,6 @@ When a session ends (shell exits → WS close code `4001`) or the server stops (
| Canvas gap fill | After each fit, distribute the gap between the container and canvas as symmetric padding so the canvas is centred at the new size | [ADR 022](../adrs/022.client.canvas-fill.md) | ✅ |
| Font-size zoom | `Ctrl/Cmd` + `=`/`-`/`0` adjust terminal font size in-session; not configurable; same shortcuts as VS Code | [ADR 023](../adrs/023.client.font-size-zoom.md) | ✅ |
| Kitty graphics | Client intercepts kitty graphics APC sequences and draws images on an overlay canvas; enables terminal-browser and `icat`-style tools | [ADR 032](../adrs/032.client.kitty-graphics-overlay.md) | ✅ |
| Tighter fit | Grid sized from the full container without `FitAddon`'s 15px scrollbar reserve, shrinking the strip at the edges to under one cell; what is left over is handled by the padding and edge-extension rows below | [ADR 033](../adrs/033.client.padding-fit.md) | ✅ |
| Configurable padding | Top-level `padding` (px, default `0`) reserves space around the canvas on all sides; the grid is fitted to what remains; the top and left gap is exactly `padding` and the leftover goes to the right and bottom; optional `theme.padding` colours the space `padding` adds (default: `theme.background`) but not the leftover | [ADR 034](../adrs/034.client.padding-config.md) | ✅ |
| Edge extension | The leftover strip at the right and bottom takes the edge cells' own background, by repainting the canvas' last pixel row and column across it; always on, no setting | [ADR 035](../adrs/035.client.edge-extend.md) | ✅ |
5 changes: 4 additions & 1 deletion docs/specs/config.md
Original file line number Diff line number Diff line change
Expand Up @@ -91,7 +91,7 @@ spawn PTY with fresh: shell, term, colorTerm, scrollback
- **Env overrides**: `PORT` overrides `config.port` at runtime. Applied after file load, never written back.
- **Hot config reload**:
- `port` / `host` — locked at startup (server socket already bound; restart required).
- `cols`, `rows`, `fontSize`, `fontFamily`, `cursorStyle`, `cursorStyleBlink`, `scrollback`, `theme`, `copyOnSelect`, `rightClickBehavior`, `mouseScrollSpeed`, `keyboardBindings` — re-read on every tab reload. `cursorStyle` and `cursorStyleBlink` set the startup defaults; apps override them at runtime via DECSCUSR.
- `cols`, `rows`, `fontSize`, `fontFamily`, `cursorStyle`, `cursorStyleBlink`, `scrollback`, `theme`, `copyOnSelect`, `rightClickBehavior`, `mouseScrollSpeed`, `padding`, `keyboardBindings` — re-read on every tab reload. `cursorStyle` and `cursorStyleBlink` set the startup defaults; apps override them at runtime via DECSCUSR.
- `shell`, `term`, `colorTerm`, `scrollback` — re-read when a new PTY is spawned (i.e. first connection to a session that has no running shell).
- An already-running session is never affected mid-flight.
- Historical note: ADR 008/009/012 describe an earlier config flow that used a `cursorBlink` key and different HTML injection mechanics. Those ADRs are considered historical; this spec's `cursorStyle` / `cursorStyleBlink` behavior is authoritative.
Expand All @@ -115,6 +115,7 @@ All keys are optional — omit any key to use the default value.
| `copyOnSelect` | boolean | `true` | Auto-copy selection to clipboard on mouseup (kitty / Windows Terminal style) |
| `rightClickBehavior` | string | `"default"` | Right-click behavior: `"copyPaste"` copies selection + clears it if selection exists, otherwise native menu; `"default"` always shows native context menu. Invalid values fall back to `"default"` |
| `mouseScrollSpeed` | number | `1` | Mouse wheel scroll speed multiplier for apps with mouse tracking (e.g. vim `set mouse=a`). `1` = one SGR event per wheel tick (default). Values `< 1` reduce rate (e.g. `0.5` fires every other tick); values `> 1` send multiple SGRs per tick. Must be `> 0`. |
| `padding` | number | `0` | Padding in px around the terminal canvas, on all four sides. The canvas is fitted to whole cells, so a few extra pixels can remain on top of this: on the right and at the bottom. The top and left gap is exactly `padding`. The padding is coloured by `theme.padding`, or `theme.background` if that is unset. |
| `logs` | boolean | `false` | Write server stdout/stderr to `~/.config/webtty/server.log`. Appends on each start. Default `false` — server runs silently. |
| `fontSize` | number | `13` | Font size in px |
| `fontFamily` | string | `"Menlo, Consolas, 'DejaVu Sans Mono', monospace"` | CSS font-family stack |
Expand All @@ -131,6 +132,7 @@ All theme keys are optional; omitted keys fall back to the Campbell (Windows Ter
| `foreground` | `#CCCCCC` | Default text color |
| `cursor` | `#FFFFFF` | Cursor color |
| `selection` | `#FFFFFF` | Selection highlight |
| `padding` | *(same as `background`)* | Colour of the padding set by the top-level `padding` setting. A colour only. The few pixels left when the canvas is fitted to whole cells are not coloured by this: the client always extends the edge cells' own background into them (like Ghostty's `window-padding-color = extend`). Set it to a full-screen app's edge colour (for example a sidebar) to hide the strip. A webtty setting, not a terminal colour, so it is not sent to the terminal. |
| `black` | `#0C0C0C` | ANSI 0 |
| `red` | `#C50F1F` | ANSI 1 |
| `green` | `#13A10E` | ANSI 2 |
Expand Down Expand Up @@ -208,6 +210,7 @@ All theme keys are optional; omitted keys fall back to the Campbell (Windows Ter
| Copy behavior | `copyOnSelect` + `rightClickBehavior` — configurable clipboard copy matching VS Code / kitty conventions | [ADR 011](../adrs/011.cli.config-and-help.md) | ✅ |
| Server logs | `logs: true` appends server stdout/stderr to `~/.config/webtty/server.log` | [ADR 011](../adrs/011.cli.config-and-help.md) | ✅ |
| Cursor style | `cursorStyle` sets the default cursor shape; DECSCUSR sequences from apps override at runtime | [ADR 013](../adrs/013.client.cursor-style.md) | ✅ |
| Padding | `padding` (px, default `0`) around the terminal canvas, coloured by `theme.padding` or `theme.background` | [ADR 034](../adrs/034.client.padding-config.md) | ✅ |
| Mouse scroll speed | `mouseScrollSpeed` scales SGR events per wheel tick for apps with mouse tracking; default `1` | [ADR 017](../adrs/017.client.mouse-scroll.md) | ✅ |
| Keyboard bindings | `keyboardBindings` — configurable key-to-sequence mappings sent to PTY; defaults to `[]` | [ADR 018](../adrs/018.key-bindings.config-support.md), [key-bindings spec](key-bindings.md) | ✅ |
| Env injection | `env` — flat `Record<string,string>` merged over `process.env` and injected into PTY shells and execute commands; same contract as `~/.claude/settings.json` | [ADR 030](../adrs/030.config.env-inject.md) | ✅ |
Loading
Loading