summaryrefslogtreecommitdiffstats
path: root/docs/design.md
blob: ed37c1370ef3355d96d035798bc5d5e15e73dbab (plain) (blame)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
# How it works

Two decisions do most of the work. The rest (twenty-two numbered decisions,
each with what it cost to get wrong) is in
`DESIGN.md`.

## One geometry

**Everything drawn registers what it is, as it is drawn**, and a click is a
lookup in that list. Drawing and hit-testing cannot disagree, because there is
only one of them. The dashed guide that shows where a split would land is drawn
from the same rect the click will resolve against, in the same pass, so a
preview that lies is not a bug you can have.

The same rule applies to state that could be derived: the layout is a pure
function of the tree and the terminal's size, recomputed every frame. Nothing
stores where a pane is. That is why a small terminal can collapse a tab to a list
of headers and come back to exactly the layout you had, and why a pane cannot be
left greyed out by the one code path that forgot to clear a flag.

Sizes are **weights**, so an even split is equal weights and resizing is not a
special case of anything.

## It runs headless

`slosh --script` is the whole thing without a terminal: commands in, the
composited screen out as JSON. The interactive client and the test suite drive
the same object through the same commands, so there is exactly one implementation
of what a key does.

That is what makes the tests real: they type keys and assert on the screen a
client would have seen, rather than on the internals. 1,500-odd checks in about
thirteen seconds, plus 260 more from unit tests on the parts that are pure
functions (the KDL parser, the shader pass, the expression compiler).

## The shape of it

```
src/app.c      the session core: pane and tab lifecycle, input routing
src/app_layout.c    the layout: splits, tree edits, resizing, focus
src/app_draw.c      drawing the chrome, the shader passes, graphics
src/app_ui.c        the finder, the palette, modals, renaming
src/app_session.c   layout files, workspaces, dumping a session out
src/pane.c     one pty + one libghostty-vt terminal, composed into the screen
src/screen.c   our cell buffer, and the minimal byte stream to update a terminal
src/server.c   the session process: socket, poll loop, config watcher
src/client.c   the attached terminal: bytes in, bytes out
src/config.c   the config, the keymap, the cheatsheet's names
src/shader.c   the colour passes; src/expr.c compiles their strengths
src/kdl.c      a hand-rolled KDL subset, for configs and layouts
```

About 15k lines of C. The vendored terminal core does VT parsing, scrollback,
selection, images and key encoding; everything above is ours.

## How it is formatted

`clang-format` for the C (`.clang-format`) and `ruff` for the Python
(`ruff.toml`), imports sorted and then laid out. `make fmt` applies both,
`make fmt-check` only reports, and `make hooks` installs a pre-commit hook that
formats what you staged, so what lands in git is already formatted.

Getting the two tools:

```
make tools            # a venv in .venv with the pinned ruff
apt install clang-format          # ...or brew, or your package manager
```

clang-format is a system package everywhere. ruff usually is not, and
`pip install --user ruff` is refused outright on a distro python (PEP 668), so
`make tools` builds a venv in `.venv/` and everything looks there **before**
`$PATH`. That order matters: `ruff.toml` pins the version with ruff's own
`required-version`, so a system ruff of the wrong one refuses to run, and
preferring `$PATH` would strand somebody whose `.venv` has exactly the version the
repo asked for. The venv survives `make clean`; `rm -rf .venv` starts over.

The pin is there because a formatter whose output moves between releases turns
"formatted" into "formatted by whoever committed last", and the diff lands on the
next person. A mismatch is refused with both versions named rather than quietly
reformatting the tree. Bumping it is one line in `ruff.toml`, one in the Makefile,
and a reformat commit, which is the honest cost of a new version.

The base is LLVM, because that is what this code was written in by hand: two
spaces, 80 columns, `char *p`, braces attached. The settings that differ from it
were chosen by measuring the reformat rather than by taste: LLVM alone rewrote 38%
of the lines, mostly by splitting `if (!p) return false;` in two and reflowing
comments that are prose. Keeping those two and leaving include order alone brings
it to 17%, which is alignment and table packing.

Comments are not reflowed. They are prose here, wrapped by hand, and sometimes the
line breaks carry meaning: a paragraph, a list, a diagram of a pane. A comment
that runs long is a thing for a person to fix.

The Python is the opposite: left entirely at ruff's defaults. The C here was
hand-formatted into a house style worth arguing for; the Python was not, and a
formatter of that kind is only worth having when there is nothing left to argue
about. Every knob turned would be an argument to have again later. It cost a 60%
reformat of the test suite, once.

**Python imports are sorted and C includes are not**, which is not an
inconsistency. A C include order can be load-bearing (`_GNU_SOURCE` ahead of the
system headers, a grouping somebody wrote by hand) while Python import order
is not. Where it genuinely is, a statement between the imports ends the block ruff
will sort, and the tests lean on exactly that: `sys.path.insert(...)` sits between
the standard library and `from harness import ...`, so the harness import cannot be
hoisted above the line that makes it importable.

The vendored terminal core is never formatted: it is pinned by commit (D12), and
reformatting it would turn every re-vendor into a merge.