turbo-editors/turbo-corepublic Fork 0
28d59854361aeda8541d853093e732126f3d7bff
Commits
Clone
git clone https://git.rickub.com/turbo-editors/turbo-core.git
git clone ssh://git@rickub.com/turbo-editors/turbo-core.git

Host key fingerprint (ed25519): SHA256:iycHnxEyq0Q7uyVpB7JlznP0G7JrTPXLYRcAU5CSLhc — verify it before your first connect.

README.md · 103 lines · 9.1 KBmarkdown Blame HistoryRaw
🛟 Updated. 28d5985 k33g 21h ago1# terminal
2
3Runs a shell in a window: a pseudo-terminal, a VT/ANSI emulator, and the widget that draws it.
4
5Depends on `ui` for its place on screen and `theme` for its colours, and on nothing above it. `app` opens and owns the windows; this package knows nothing about menus, files or language servers.
6
7## The three layers
8
9**`Session`** — the pseudo-terminal and the process on the other end of it. On Unix it opens `/dev/ptmx`, puts the child in a session of its own with the slave as its controlling terminal, and resizes it with `TIOCSWINSZ`; on Windows it creates a pseudo-console (ConPTY) wired to two pipes and starts the child attached to it. A real controlling terminal is what buys job control, `isatty`, `SIGWINCH` and colours; a captured pipe buys none of them. Both sit behind one small `child` interface, so the two layers above know nothing about which one is running.
10
11**`Parser` + `Screen`** — bytes in, a grid out. The parser is a state machine over CSI, OSC and the one-byte escapes; the screen is the cell grid, the cursor, the scroll region, the scrollback and the alternate-screen aside.
12
13**`View`** — a `ui` widget: reads the session on a goroutine, feeds the parser, draws the grid through the theme, and encodes key presses back.
14
15The split is what makes the emulator testable without a shell. `Parser` and `Screen` take bytes and return a grid — no process, no timing, no screen — and that is where most of this package's tests are.
16
17## Emulation is deliberately partial
18
19What a shell, `go test`, `git`, `less`, `htop` and `vim` need is a bounded list: cursor movement, erase and insert-delete, a scroll region, SGR in all three colour depths, the alternate screen, auto-wrap, cursor visibility, and application cursor keys. That is what is here.
20
21Mouse reporting, bracketed paste, character sets, sixel and the DEC status reports are not. A program that asks for one gets silence rather than corruption — the failure mode worth having. The exact boundary is documented, in both languages, under `docs/*/reference/terminal.md`.
22
23Two details of the emulation are easy to get wrong and are worth naming:
24
25- **A character written in the last column does not scroll by itself.** It sets `wrapPending`, and only the *next* character wraps the line. Getting this wrong makes every full-width line scroll one line too early.
26- **Scrollback is only fed from a full-height region on the primary screen.** A program using a scroll region is managing its own window; a full-screen program on the alternate screen should leave no history behind.
27
28## Platforms
29
30`startChild` and `defaultShell` are the whole platform surface. The Unix side is `session_unix.go` over the two `openPTY` files; Windows is `pty_windows.go`; the pure parts Windows needs are in `windows.go`, with no build tag, so they are tested everywhere:
31
32| File | Platform | What it does |
33| --- | --- | --- |
34| `session_unix.go` | linux, darwin | `exec.Cmd` with the slave as its controlling terminal; `$SHELL`, then `/bin/sh` |
35| `pty_linux.go` | linux | `TIOCSPTLCK`, `TIOCGPTN``/dev/pts/N` |
36| `pty_darwin.go` | darwin | `TIOCPTYGRANT`, `TIOCPTYUNLK`, `TIOCPTYGNAME` |
37| `pty_windows.go` | windows | Two pipes, `CreatePseudoConsole`, `CreateProcess` with the pseudo-console attribute, `ResizePseudoConsole`; `%COMSPEC%`, then `cmd.exe` |
38| `windows.go` | all | The environment block, the command line cmd.exe wants, C-runtime quoting for anything else |
39| `pty_other.go` | everything else | Returns `ErrUnsupported` |
40
41Callers check `errors.Is(err, ErrUnsupported)` to tell "this platform cannot" from "this attempt failed".
42
43Three things about the Windows side are worth knowing before touching it:
44
45- **The process is created by hand.** Attaching a process to a pseudo-console takes an extended `STARTUPINFO` with the `PROC_THREAD_ATTRIBUTE_PSEUDOCONSOLE` attribute, which `exec.Cmd` cannot carry. `UpdateProcThreadAttribute` is called through a `LazyProc` rather than x/sys's wrapper because the attribute's value is the handle *itself*, and the wrapper takes an `unsafe.Pointer` — the conversion `go vet` exists to flag.
46- **conhost holds the output pipe open until the console is closed**, whatever the shell does. A goroutine waits for the process and then closes the pseudo-console; that is what turns a shell exiting into the `io.EOF` the view relies on. Without it a command run in a window would never be seen to finish.
47- **cmd.exe does not read its command line the way the C runtime does.** `windowsCommandLine` writes `"cmd.exe" /S /C "<command>"` with the command verbatim, so a quote inside it reaches cmd.exe as a quote; every other program gets the escaping `syscall.EscapeArg` would produce, reimplemented so it can be tested on Linux.
48
49> The darwin path compiles and passes `go vet` but has never been run — this project is developed on Linux. **The Windows path is the same**: cross-compiled, vetted and unit-tested in its pure parts, never started against a real conhost. The first run on a Windows machine is the test that matters, and `docs/*/how-to/use-a-terminal.md` in each editor says what to try.
50
51## Concurrency
52
53The shell writes from a goroutine of its own while the editor draws from the main one, so `View.mu` guards the parser and its screen. Every exported method of `View` takes it.
54
55Redraws are **on a clock, not per chunk**. `consume` sets a flag; a 16 ms ticker turns the flag into one `OnChange` call. Waking the event loop per chunk would waste most of the redraws, and worse, tcell's `PostEvent` drops events when its queue is full — so the burst that most needs a redraw is the one whose wake-up gets discarded. A dropped tick cannot strand anything; the next one is 16 ms away.
56
57`OnChange` and `OnExit` are called from the reading goroutine. They must only wake the event loop, never touch the desktop.
58
59**They are `ViewOptions` fields rather than assignable ones, and that is a fix rather than a style.** `NewView` starts the goroutine that reads them, so a caller assigning them afterwards is a data race. It hid for a whole feature because a shell takes longer to produce its first output than an assignment takes to run, and only surfaced once a command finished immediately — `sh -c "echo x"`. Taking them as options makes the race impossible rather than unlikely.
60
61**A finished view stops taking keys**, except the scrolling ones. It used to consume every key and write it to the shell; once the shell had gone the write failed silently and the key was consumed anyway, so `Ctrl-W` could never close a window whose command had ended and the mouse was the only way out.
62
63## Public API
64
65| Name | What it does |
66| --- | --- |
67| `Start(Options) (*Session, error)` | Opens a pseudo-terminal and starts the shell in it |
68| `Options{Shell, Args, Dir, Width, Height, Env}` | Empty `Shell` means `$SHELL`, then `/bin/sh` (Windows: `%COMSPEC%`, then `cmd.exe`); `TERM` is always replaced. `Args` runs one command — `["-c", "go test ./..."]`, or `["/S", "/C", …]` for cmd.exe — instead of an interactive shell. |
69| `ErrUnsupported` | Returned by `Start` on a platform with no pseudo-terminals |
70| `(*Session) Read/Write/Resize/Close/Command` | The pseudo-terminal, and the name of the program in it |
71| `NewView(ViewOptions) (*View, error)` | Starts a session and returns the widget showing it |
72| `ViewOptions{Options, Name, OnChange, OnExit}` | Everything the view needs, given **before** the goroutines start |
73| `(*View) Title() string` | The title the program asked for, else the shell's name |
74| `(*View) Draw/SetBounds/HandleKey/HandleMouse/Close` | The `ui` widget contract |
75| `(*View) ScrollBy/ScrollToBottom/ScrollOffset` | Reading back through the history |
76| `(*View) Exited() bool` | Whether the program has gone |
77| `NewScreen(w, h) *Screen` | The cell grid on its own |
78| `NewParser(*Screen) *Parser` | An `io.Writer` that never fails and always consumes |
79| `(*Parser) Title/TakeBell` | What the program set, and whether it rang |
80| `Encode(*tcell.EventKey, bool) []byte` | A key press as the bytes a terminal program expects |
81
82```go
83view, err := terminal.NewView(terminal.ViewOptions{
84 Options: terminal.Options{Dir: "."},
85 OnChange: wakeTheEventLoop,
86})
87if err != nil {
88 if errors.Is(err, terminal.ErrUnsupported) {
89 return // this platform has no pseudo-terminals
90 }
91 return err
92}
93window := ui.NewWindow(view.Title(), view)
94```
95
96## Tests
97
98`go test ./terminal/` — the emulator tests need nothing; the session and view tests drive `/bin/sh` and `stty`, so they skip themselves off Linux and macOS, or without `/dev/ptmx`. The Windows pure parts — `windows_test.go` — run everywhere; `GOOS=windows go vet ./terminal && GOOS=windows go test -c ./terminal` is how the rest is checked here.
99
100Two traps this package's own tests fell into, both worth remembering:
101
102- **A pseudo-terminal echoes the command line.** Waiting for `red` after typing `printf '\033[31mred\033[0m'` passes before the command has run. Wait for something only the output can produce — a colour, or a string the typed line spells differently.
103- **Drawing tests must not race the shell's startup output.** `newOfflineView` builds a `View` with no session behind it, which is enough for `Draw` and is deterministic.