turbo-editors/turbo-jspublic Fork 0
v1.0.2
Commits
Clone
git clone https://git.rickub.com/turbo-editors/turbo-js.git
git clone ssh://git@rickub.com/turbo-editors/turbo-js.git

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

terminal-windows.md · 72 lines · 8.6 KBmarkdown Blame HistoryRaw
📦 Turbo JS 91999d1 k33g 12h ago1# Terminal windows — explanation
2
3## What is this about?
4
5`F8` opens a window with a shell in it. That sentence hides most of the work: to put a shell in a window, an editor has to become a terminal emulator, and this page is about what that involved and which of the cheaper alternatives were turned down on the way.
6
7## Why a real pseudo-terminal
8
9The obvious cheap version is to run a command with `exec.Command`, capture its output, and show it in a read-only pane. Many editors ship exactly that, and it fails on the things people actually want a terminal for.
10
11A program behaves differently when its output is a pipe rather than a terminal. Coloured output goes plain. `git log` does not page. `ls` prints one name per line. Nothing interactive works at all: no `vim`, no `ssh`, no `git rebase -i`, no answering a prompt, and no `Ctrl-C`, because with no controlling terminal there is no signal to send. For Node the case is sharper still: `node` with no file is a read-eval-print loop, `npm start` is very often a server that runs until `Ctrl-C` stops it, and a script that reads `process.stdin` waits for the keyboard — none of them is usable on a captured pipe, which is why the starter tools file sends **Run** and **Start** to a terminal window rather than a popup.
12
13So the shell gets a real pseudo-terminal: `/dev/ptmx` on both supported platforms, the child in a session of its own with the slave as its controlling terminal, and `TIOCSWINSZ` whenever the window is resized. That buys job control, `isatty`, `SIGWINCH` and colour, all for free, because they are the same mechanisms every other terminal uses.
14
15The cost is that the editor must then read back what a terminal is expected to understand — which is the emulator.
16
17## Why write the emulator rather than borrow one
18
19Go has terminal emulator libraries. Taking one would have meant a third dependency, against a project that has exactly two and a stated reluctance to add a third.
20
21The thing being weighed is not "emulator" against "no emulator" but against *how much* emulator. What a shell, `node`, `git`, `less`, `htop` and `vim` need is a well-bounded list: cursor movement, the erase and insert-delete family, a scroll region, SGR in all three colour depths, the alternate screen, auto-wrap, cursor visibility and application cursor keys. That is about six hundred lines, it is written down in ECMA-48, and it is testable by writing bytes in and reading a grid out — no shell, no timing, no screen.
22
23Compare that with what a general-purpose library brings: character sets, mouse reporting protocols, sixel, bracketed paste, DEC status reports. All real, none of it needed here, and all of it surface to keep working.
24
25So the emulator is hand-written and deliberately partial, and the [reference](../reference/terminal.md) says exactly where it stops. A program that asks for something absent gets silence rather than corruption, which is the failure mode worth having: `htop` renders, `sixel` output simply does not appear.
26
27## Who gets the key press
28
29This is the decision with the most consequence for how the editor feels, and the first version got it wrong.
30
31The editor's global shortcuts are checked before the window in front sees anything. That is right for an editor and wrong the moment the window in front is a shell, because the two disagree about the same keys. `Ctrl-W` closes a window in Turbo C and deletes a word in every shell. `Ctrl-F` is Find here and forward-a-character in readline. `Ctrl-C` is copy, and also the only way to stop a runaway command.
32
33The rule chosen inverts the usual order, but only for the keys that are genuinely contested:
34
35**A focused terminal gets everything except the function keys, `Alt-X`, and `Alt-0`…`Alt-9`.**
36
37Those exceptions are not a compromise between the two claims — they are the way *out*. A full-screen program like `vim` covers the window and takes the mouse; without a reserved key there would be no way to reach the menu bar, switch windows or leave the editor short of quitting the program inside. Function keys are the natural reservation because a terminal user reaches for them least, and `Alt-X` because leaving an editor should never be in doubt.
38
39What this costs is real and worth naming: `Alt-B` and `Alt-F` reach the shell, so readline's word movement works, but a program inside a terminal window can never see `F1``F12`. `htop`'s function-key menu is unreachable. That is the trade, and it was made in favour of always being able to get out.
40
41## Why closing a terminal asks nothing
42
43Closing a modified file asks whether to save it. Closing a terminal does not ask anything at all, and that asymmetry is deliberate.
44
45A window with unsaved work holds something that would be *lost*. A terminal holds a running process, and closing the window is the ordinary way to say you are done with it — the same as closing a terminal emulator's tab. Asking "are you sure?" every time would train the answer out of anyone, which is the general problem with confirmations that fire on the common case.
46
47Leaving the editor closes every terminal for the same reason in reverse: a window is the only handle on those shells, so letting them outlive the editor would strand the processes with nothing able to reach them.
48
49## Why the redraws are on a clock
50
51The shell writes on a goroutine of its own; the editor draws on the main one. Waking the event loop per chunk of output looked obvious and was wrong twice over.
52
53A build writes far faster than a screen can usefully be repainted, so most of those redraws are wasted. Worse, the mechanism for waking the loop from another goroutine is tcell's `PostEvent`, which **drops** events when its queue is full — so the burst that most needs a redraw is the one whose final wake-up gets discarded, and the window freezes mid-build showing stale text. That exact bug had already been found once elsewhere in this editor, over the language server.
54
55So the view sets a flag and a ticker asks for a redraw sixty times a second while the flag is set. A dropped wake-up cannot strand anything, because the next tick is sixteen milliseconds away.
56
57## Windows: a pseudo-console, and why it is a file of its own
58
59Pseudo-terminals are the one part of this that is not portable. Linux and macOS both go through `/dev/ptmx` and differ only in which `ioctl` grants the slave. Windows has no such device: it has **pseudo-consoles** — ConPTY, since Windows 10 version 1809 — an object owned by `conhost.exe` and wired to two pipes of the editor's. What the shell prints arrives on one pipe as the same VT sequences a Unix shell writes to a pty, which is why the emulator on this side needed no Windows code at all; what the editor writes to the other pipe reaches the shell as keystrokes.
60
61Three things made it a file of its own rather than a variant of the Unix one. The process has to be created by hand, because attaching it to a pseudo-console takes an extended startup record that Go's `os/exec` cannot carry. The shell is `%COMSPEC%` — cmd.exe — rather than `$SHELL`, and cmd.exe reads its command line by rules of its own, so the line that runs a menu command is composed for it verbatim, the command inside one pair of quotes, rather than escaped the way every other program expects. And `conhost.exe` holds the output pipe open until the console is closed, whatever the shell does, so a goroutine waits for the shell to exit and then closes the console — that is what turns a command finishing into the end of input the window relies on to say so. Job control is cmd.exe's rather than the kernel's: `Ctrl-C` interrupts the running program as it would in a console window.
62
63The platform files stay split so that each platform has one honest implementation behind one small interface, and a platform with neither — the BSDs, today — gets `ErrUnsupported`, `F8` says so plainly, and nothing else in the editor is affected.
64
65**The Windows path has been built and vetted, not run.** turbo-core is developed on Linux and its author works on macOS. The pure parts — the environment block, the command line cmd.exe wants — are unit-tested on every platform, and the API calls compile and pass `go vet` under `GOOS=windows`; nobody has yet pressed `F8` on a Windows machine. [The how-to](../how-to/use-a-terminal.md) says what to try first.
66
67## How it relates to the rest
68
69- The exact list of what is implemented: [Terminal windows reference](../reference/terminal.md)
70- Using one: [How to run shell commands without leaving the editor](../how-to/use-a-terminal.md)
71- Where `terminal` sits among the packages, and why the graph runs one way: [Architecture](architecture.md)
72- The dependency count this page keeps invoking: [Design decisions](design-decisions.md)