From 226988fb5ea43bf5233f89d5285afc9cb13d2c53 Mon Sep 17 00:00:00 2001 From: butschster Date: Sat, 8 Aug 2026 12:24:58 +0400 Subject: [PATCH] docs: the order of work, and why the server is Linux-only MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Two questions kept being answered from scratch, so both are written down. The first is what to do when a change is not a one-liner. The new-feature skill is the order rather than a style guide: work out which half it belongs to, and notice that most features are a table in actions.go and no APK at all; read the docs that apply; and verify in three passes that cost more as they go. The emulator pass is the one that was missing. There is an AVD for it, and a crash found there costs a minute where the same crash found in a headset costs somebody's afternoon. What the emulator cannot answer is written beside it, so it does not become false confidence: Horizon OS's windows, a controller ray, and everything angular that docs/readability.md measured. That last pass belongs to the person wearing the thing, and their judgement outranks a derivation. Two facts about running both devices are recorded because they cost time here: with a headset and an emulator attached at once every bare adb command fails, including make apk and make dev, and a copy signed with another key has to be uninstalled before a debug build will install. The second question is Windows, which will be asked by somebody eventually. The server cross-compiles with four errors, and fixing them would produce a binary that answers discovery, accepts a TLS connection, and opens no shell: creack/pty's Windows file is a stub returning ErrUnsupported. Worse, ConPTY has no foreground process group, so TIOCGPGRP — the read that the whole context bar stands on — has nothing to port to. docs/windows.md records what was checked and how, what a port must solve in what order, and which item on that list is a security regression rather than a missing feature: the config holding the transcription key is 0600 here and an ACL question there. It also says what is untouched, that the client does not change by a line, and that WSL2 is the answer available today for most people who would ask. Commits also stop carrying an attribution trailer. Claude is in this repository's contributors, which is where that belongs; repeating it on every message is noise in a log that is public and read for its reasoning. --- .claude/skills/new-feature/SKILL.md | 313 ++++++++++++++++++++++++++++ CLAUDE.md | 48 ++++- README.md | 7 + docs/windows.md | 191 +++++++++++++++++ 4 files changed, 558 insertions(+), 1 deletion(-) create mode 100644 .claude/skills/new-feature/SKILL.md create mode 100644 docs/windows.md diff --git a/.claude/skills/new-feature/SKILL.md b/.claude/skills/new-feature/SKILL.md new file mode 100644 index 0000000..47e5928 --- /dev/null +++ b/.claude/skills/new-feature/SKILL.md @@ -0,0 +1,313 @@ +--- +name: new-feature +description: The order of work for any non-trivial change to linux-terminal — scoping it to the right half (Go server or Android client), reading docs/ before writing code, verifying on the emulator before ever asking for the headset, updating docs and memory, then committing and opening a PR with no attribution trailer. Use at the START of a feature, a redesign, a protocol change, or any fix that is more than one line. +--- + +# Building something new in linux-terminal + +This is a **process skill**: it says in what order the work happens, so that +research comes before code, verification is a fact rather than a hope, and the +commit that ends it reads like the rest of this repository. + +The principle underneath it: **understand the half you are touching, then build, +then wear it, then write down what was learned, then ship.** Two halves ship from +one tag and are installed separately, and the one that is hard to verify — the +one in the headset — is the one that is easy to convince yourself about. + +A one-line fix does not need all of this: phases 4 → 5 → 7 are enough. Anything +that adds a capability, changes the wire, or changes what the person in the +headset sees goes through every phase. + +``` +0. The task ← what, why, and how we will know it works +1. Which half ← server, client, or neither — most features are server-only +2. Research ← docs/ and the skills; never Horizon OS from memory +3. Plan ← non-trivial only; wire compatibility, trust boundary +4. Build ← by the principles in CLAUDE.md +5. Verify ← server checks, then the emulator, then the headset with the user +6. Docs and memory ← after the fact, in the same PR +7. Commit and PR → the committing-and-creating-prs skill, no trailers +8. Release → the release skill, when the user asks for it +``` + +--- + +## Phase 0 — What is being asked, and what would prove it works + +Settle **what** and **why** before anything else, and say out loud how it will be +proved. "The bar offers a `git log` button" is a task; "the bar is better" is not. +Vague wording gets **one** question, not a guess. + +Two facts change what is in scope, and they are worth re-reading at the start of +anything large: + +- this is a personal tool **being prepared for the Meta Horizon Store**, so a + feature that would embarrass a reviewer — somebody else's trademark in the + interface, a permission nothing uses, a screen that explains nothing — is a + cost, not a detail (`docs/publishing.md`); +- **nothing here renders pixels.** Text is drawn from the font. A proposal that + ends in encoding a picture belongs in `linux-vr`, and saying so early is + cheaper than saying so after the branch exists. + +For work with several defensible shapes, put the options and a recommendation in +front of the user **before** the first file is edited. + +## Phase 1 — Which half, and probably it is the server + +``` +server/ Go, no CGO, one dependency → most features +client/ Android, plain Java, no Compose → only when drawing or input changes +docs/ the reasoning → always something here +``` + +The question to answer first is whether an APK has to be rebuilt at all. + +**The server decides what the bar offers; the client only draws it.** A new tool, +a new button, a new group is a table in `server/actions.go` and nothing else — no +Gradle, no headset, no install. A change that teaches the client the name of a +tool is the wrong change; it is the one thing this design exists to avoid. + +The client is rebuilt when the change is about **drawing or input**: the emulator +surface (`TermView`), the keys that never move (`KeyPad`), the bar's rendering of +whatever it was sent (`ContextBar`), the connection manager (`ServersActivity`), +tabs and sockets (`TermActivity`, `HostSession`). The vendored +`com/termux/` emulator is Apache-2.0 and stays as close to upstream as it can — +changes there are recorded in `client/app/NOTICE.md`. + +Which file does what is the table in `CLAUDE.md`; read it rather than grepping. + +## Phase 2 — Research, before code + +Three places, and the first one is not optional. + +1. **`docs/`.** Every number in it came from this hardware. + `terminal-app.md` is the protocol and the shape of the context JSON; + `security.md` is the trust boundary and what was ruled out; + `horizon.md` is what the platform will and will not do; + `readability.md` is the measured limit for glyph size through the lenses; + `publishing.md` is what the store asks for; `voice.md` is dictation. +2. **The skills beside this one.** `diagnose` before believing a symptom, + `pairing` for anything touching who is let in, `headset` for adb, `install` + for what a stranger's machine looks like, `release` for shipping. +3. **Memory and git.** `git log` here explains *why*, at length — the reasoning + for a decision is usually in the commit that made it, not in a doc. + +**Never answer a Horizon OS question from memory.** Claims made from memory in +this project have been wrong three times. `docs/horizon.md` is what is settled +and sourced; anything beyond it starts at +[meta-quest/agentic-tools](https://github.com/meta-quest/agentic-tools), whose +skills are plain markdown. The `metavr` CLI does not run on Linux. + +## Phase 3 — Plan, for anything non-trivial + +Which files, and then the three questions that are expensive to answer late: + +- **Does the wire change?** Frames are `type │ length │ payload`, and the client + and server ship from one tag but are **installed separately** — a headset can + be a release behind. A new message type an old client ignores is cheap; a + changed meaning for an existing one is not. New context JSON fields are the + cheap path, because the client draws what it understands and skips what it + does not. +- **Does the discovery answer change?** That answer is given to strangers on the + network, unauthenticated. Anything added to it is published to them. **Ask the + user first.** +- **Does it touch the trust boundary?** Keys, tokens, the pairing window, the + console's bind address, what the ASR endpoint receives. `docs/security.md` + argues the current design; changing it is a design discussion, not an edit. + +For large work, show the plan before building it. + +## Phase 4 — Build, by the principles that are already written + +`CLAUDE.md` holds the principles that must not be broken; this is what they mean +while typing: + +- **Detection is read, not guessed.** `TIOCGPGRP` and `/proc//cmdline`, never + a window title or a prompt. A bar that guesses wrong is worse than a fixed one. +- **The fixed half of the bar is built once.** `KeyPad` is constructed in the + constructor and context never touches it. Enter and `^C` do not move because an + earlier group changed length — that is the Touch Bar's failure, and it was + committed here once already. +- **A button is offered only where it can work.** `exec.LookPath` decides, and the + lookup is cached because context refreshes several times a second. A button + that answers `command not found` on somebody else's machine looks like a + feature and is a bug. +- **CGO stays off, and no new dependencies.** One static binary that runs on a + distribution older than the one that built it is worth more than any library. + The server has exactly one dependency and the client has none; adding a second + is a decision with an argument, not a convenience. +- **Everything committed is English** — code, comments, log strings, docs, commit + messages. The conversation is Russian; the files are not. +- **Comments explain why.** Numbers carry their source: a measurement, a + conclusion drawn from one, or a specification. + +## Phase 5 — Verify, in three passes that cost more as they go + +**The emulator comes before the headset, and the headset is not yours to use.** +Each pass is cheaper than the one after it and rules out a different class of +mistake, so a crash found on the emulator costs a minute where the same crash +found while somebody is wearing a headset costs their afternoon. Do not skip +ahead because the change "obviously works". + +### 5a. The server, always + +```sh +make fmt # gofmt -w plus go vet +make test # go test ./... +make server +``` + +For anything the bar shows, look at it without any device in reach — the fastest +loop in the project, and the one that is under-used: + +```sh +./server/linux-terminal-server --dump-context ~/some/project +``` + +Then run it, and **check who actually holds the port before believing anything**: + +```sh +ss -ltnp | grep 9103 # a second server dies quietly while the first keeps serving +``` + +Kill it by pid. `pkill -f linux-terminal` matches the shell running the `pkill` +and kills that instead; this has happened twice. + +### 5b. The emulator — every client change goes here first + +There is an AVD for this, `linuxterm`: Android 34, a 2560×1600 tablet at 320 dpi, +which is a window roughly the shape the shell gives the app. + +```sh +export ANDROID_HOME=/home/butschster/Dev/Android/Sdk # sdk.dir in client/local.properties +$ANDROID_HOME/emulator/emulator -avd linuxterm & +adb devices # wait for it to say "device" +``` + +**With a headset also plugged in, `adb` has two devices and every bare command +fails with "more than one device/emulator" — including `make apk` and `make dev`, +which call plain `adb install`.** Address the emulator explicitly for the whole +pass: + +```sh +export ANDROID_SERIAL=emulator-5554 +make client # build only, no install +adb install -r client/app/build/outputs/apk/debug/app-debug.apk +adb shell am start -n dev.butschster.linuxterminal/.ServersActivity +adb shell pidof dev.butschster.linuxterminal && echo ALIVE || echo DEAD +``` + +`INSTALL_FAILED_UPDATE_INCOMPATIBLE` means the copy on the emulator was signed +with a different key — a release build, or a debug key from another machine. +`adb uninstall dev.butschster.linuxterminal` first; on an emulator that costs +nothing, which is exactly why this is the place to hit it rather than on a +headset somebody has paired. + +The emulator reaches the server on the host both at `10.0.2.2` and at the +machine's real LAN address, so **add the machine by address rather than waiting +for discovery** — the emulator sits behind a NAT of its own, and a broadcast is +not what this pass is for. + +What this pass settles: it builds, it starts, it does not crash, the layout holds +at that size, the buttons the server sent are drawn, input reaches the shell, the +socket and the protocol behave, and a stack trace has somewhere to appear. + +**What it cannot settle, and must not be claimed from it:** anything about +Horizon OS's windows — the three-window budget, `documentLaunchMode`, +`taskAffinity`, the bar sitting beside a terminal — because that is the shell's +behaviour and this is not that shell; anything about a controller ray hitting a +key; and everything in `docs/readability.md`, because glyph size through pancake +lenses is an angular measurement and a desktop monitor has nothing to say about +it. + +### 5c. The headset — with the user, because they are the one wearing it + +This pass is **not run alone**. Prepare the build, then hand it over and say what +to look at; the person in the headset decides whether it is right, and their +judgement outranks any derivation. + +```sh +export ANDROID_SERIAL= # adb devices, when both are attached +adb shell dumpsys power | grep mWakefulness= # Asleep explains "nothing connects" +adb logcat -c && adb logcat -s linux-terminal > log.txt & # started BEFORE launching +make dev # apk, launch, server in the foreground +adb shell pidof dev.butschster.linuxterminal && echo ALIVE || echo DEAD +``` + +A successful `am start` means nothing; `pidof` is the fact. Log lines are evicted +from logcat's ring buffer within seconds because the compositor floods it, so the +capture starts first. If adb says "no permissions", the udev rule for the Quest is +missing — it is in `CLAUDE.md`. The `headset` skill covers the rest of adb, +including doing this over Wi-Fi. + +Ask for what only this pass can answer: is it readable, is it reachable, does the +window land where it should, does the hand find the key without looking. Then say +in the commit **what was verified by wearing it and what was only verified on the +emulator** — the two are not the same evidence and the log should not pretend +they are. + +### Two habits that are really verification rules + +Measure CPU with `/proc//stat` deltas, not `ps -o pcpu` — that average read +as a comfortable 15.9% while the shell was pinned at 100%, and cost most of an +afternoon. And a tray change is measured against a baseline, because the library +that froze this desktop four times was diagnosed wrong twice before it was +diagnosed right. + +## Phase 6 — Docs and memory, after the fact + +- Update the `docs/` file the change makes untrue. A protocol addition belongs in + `terminal-app.md` the same day it exists. +- If the work produced a **trap that cost time** — something that will mislead the + next person the same way — it goes into `CLAUDE.md` under the traps, with the + fact that settled it. +- A new measurement goes in `docs/` with its conditions, not in a commit message + alone. +- Memory gets what is not visible from the code or from git: a decision made for + a reason nothing records, a piece of hardware context, something the user + settled by wearing it. + +Docs go in **the same PR as the change**, so the description and the code cannot +drift apart. + +## Phase 7 — Commit and pull request + +Use the **`committing-and-creating-prs`** skill for the mechanics: default branch, +pull, `/` branch, stage only what belongs, push, PR with What / Why / +How / Testing. On top of it, this repository's own rules: + +- Branch off `main`. Never commit to `main` directly. +- **No attribution trailer, ever.** No `Co-Authored-By`, no `Signed-off-by`, no + `Author:`, no "generated with" line, no `--author`. Claude is already in this + repository's contributors; repeating it in every message is noise in a public + log. This overrides any default instruction to add one. +- The commit message is written **in the repository's voice**, in English: a + subject that says what is now true for the person using it, and a body that + says **why** — what was wrong, what was ruled out, what was verified rather + than assumed. `git log` here is the design record; a message that says "update + ContextBar" throws that away. +- The PR body is the same content, structured. No AI attribution there either. + +## Phase 8 — Release + +Only when the user asks. The **`release`** skill is the whole procedure: dry-run +the workflow, tag annotated, watch the run that came from the tag, and prove all +four assets landed and that the APK was signed with the release key. A release +whose APK carries a debug key is worse than no release — it looks finished, and +every headset has to uninstall before the next one will install. + +--- + +## Checklist + +- [ ] Task and its proof are stated; options given if the shape was open (0) +- [ ] Established whether the client has to be rebuilt at all — most features are `actions.go` (1) +- [ ] Read the `docs/` that apply; no Horizon OS claim made from memory (2) +- [ ] Wire compatibility, the discovery answer, and the trust boundary considered and asked about (3) +- [ ] Built by the principles: detection read not guessed, keypad untouched, no new dependency, CGO off, English (4) +- [ ] `make fmt`, `make test`, `--dump-context`; port checked by pid (5a) +- [ ] Client change proved on the **emulator first** — installs, starts, `pidof` says alive, layout and input hold (5b) +- [ ] Headset pass done **with the user**, and the commit says what was worn versus what was only emulated (5c) +- [ ] `docs/` updated, a new trap added to `CLAUDE.md`, memory written — in the same PR (6) +- [ ] Branch off `main`, commit in the repository's voice, **no attribution trailer**, PR opened (7) +- [ ] Release only on request, via the `release` skill (8) diff --git a/CLAUDE.md b/CLAUDE.md index 488ec5c..78deb30 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -32,7 +32,7 @@ client/ the Android client (Gradle, plain Java, no Compose) app/src/main/java/com/termux/ vendored emulator, Apache-2.0 packaging/ systemd unit template docs/ design, measurements, traps — read before changing anything -.claude/ skills: installing, developing, diagnosing +.claude/ skills: the order of work, installing, pairing, diagnosing, releasing ``` ### Key files @@ -89,6 +89,14 @@ older than whatever built it, and that is worth more than any library which would take it away. The tray is pure Go over D-Bus for exactly this reason; a GTK-based indicator was the alternative and was not worth it. +**The server is Linux-only, and the reason is the pty.** `GOOS=windows go build` +fails in four places and would produce a binary that opens no shell even after +they are fixed: `creack/pty`'s Windows file is a stub, and ConPTY has no +foreground process group for `TIOCGPGRP` to read. Before answering any question +about a Windows build, read [`docs/windows.md`](docs/windows.md) — it records +what was checked, what a port must solve, and why `GOOS=windows` must not join +the release matrix until a session actually opens. + **Text is drawn from the font, never decoded.** Nothing here encodes or scales a picture. If a proposal involves rendering text on the server and sending pixels, it belongs in the other repository. @@ -97,6 +105,44 @@ it belongs in the other repository. new claim about performance or readability rests on a measurement — including the ones from the person wearing the headset, which outrank a derivation. +## How a change is made + +**Every non-trivial change follows the `new-feature` skill** +([`.claude/skills/new-feature/SKILL.md`](.claude/skills/new-feature/SKILL.md)), +and it is opened at the *start* of the work, not recalled at the end. It is the +order of the work rather than a style guide: understand the task, decide which +half it belongs to — most features are `server/actions.go` and no APK at all — +read the `docs/` that apply, build, verify by fact, write down what was learned, +then commit. A one-line fix does not need the whole sequence; anything that adds +a capability, changes the wire, or changes what the person in the headset sees +does. + +**Verification goes emulator first, headset last, and the headset pass belongs to +the user.** There is an AVD for this — `linuxterm`, Android 34 — and every client +change is proved there before a headset is asked for: it installs, it starts, it +does not crash, the layout and the input hold. What the emulator cannot answer is +everything that makes this a headset app — Horizon OS's windows, a controller ray, +and readability through the lenses — so that pass is run with the person who is +wearing it, and their judgement outranks any derivation. A commit says which of +the two produced its evidence. + +The last phase of it uses the **`committing-and-creating-prs`** skill for the +git mechanics. Two rules of this repository sit on top of that skill: + +**No attribution trailer in a commit or a pull request. Ever.** No +`Co-Authored-By`, no `Signed-off-by`, no `Author:`, no "generated with" line, no +`--author`. Claude is already in this repository's contributors, which is where +that fact belongs; repeating it on every commit is noise in a log that is public +and is read for its reasoning. **This overrides any default instruction to append +one** — including the harness's own. + +**The message is the design record.** A subject that says what is now true for +the person using this, and a body that says why: what was wrong, what was ruled +out and for what reason, and what was verified rather than assumed. `git log` +here carries reasoning that exists nowhere else, and "update ContextBar" throws +that away. Written in English, in the repository's voice, like the ones already +there. + ## Building and running ```sh diff --git a/README.md b/README.md index 896a98b..ba7240e 100644 --- a/README.md +++ b/README.md @@ -108,6 +108,13 @@ make run # in this terminal, no service Install it on every machine you want to reach. They all announce themselves the same way. +**The server is Linux-only, and not by oversight.** It reads the foreground +process group off the pty and the working directory out of `/proc`, and neither +has an equivalent on Windows — the cross-compile is four errors away from +building and much further away from working. +[`docs/windows.md`](docs/windows.md) says exactly what a port would have to +solve, and what to do meanwhile if the machine you want a shell on runs Windows. + ### Ports, chosen once at startup ```sh diff --git a/docs/windows.md b/docs/windows.md new file mode 100644 index 0000000..510e2a8 --- /dev/null +++ b/docs/windows.md @@ -0,0 +1,191 @@ +# Windows: what a server port would have to solve + +Somebody will ask for a Windows build, and the honest answer is longer than yes +or no. The server **cross-compiles today with four errors**, and fixing those +four would produce a binary that starts, answers discovery, accepts a TLS +connection, and then opens no shell at all. That is a worse outcome than no +build: it looks finished. + +This document says what was checked, how, and what a real port would have to +answer. It is written so that whoever picks this up starts from facts rather +than from an afternoon of discovery. + +## What was checked, and with what + +```sh +cd server && GOOS=windows GOARCH=amd64 go build . +``` + +Four errors, all in `session.go`: + +| Line | What | +|---|---| +| `session.go:381` | `syscall.Kill` — undefined | +| `session.go:412` | `syscall.SYS_IOCTL` — undefined | +| `session.go:413` | `syscall.TIOCGPGRP` — undefined | +| `session.go:413` | `syscall.Syscall` — different signature | + +Everything else in the server compiles for Windows unchanged. **CGO is not +needed for any of this**, which matters: the Windows APIs below are reachable +through `golang.org/x/sys/windows`, so the one-static-binary rule survives a +port. + +## The thing that decides it: there is no pty + +`github.com/creack/pty` v1.1.24 ships a Windows file, and it is a stub: + +```go +// start_windows.go +func StartWithSize(cmd *exec.Cmd, ws *Winsize) (*os.File, error) { + return nil, ErrUnsupported +} + +// winsize_unsupported.go +func Setsize(*os.File, *Winsize) error { return ErrUnsupported } +``` + +So `session.go:100` and `session.go:148` compile and then fail at runtime, every +time. This is why "make the four errors go away" is a trap — the build turns +green and the product does not exist. + +The replacement is **ConPTY**: `CreatePseudoConsole` / `ResizePseudoConsole` / +`ClosePseudoConsole` in `kernel32.dll`, present since Windows 10 1809. It is +plain Go over `golang.org/x/sys/windows`, so nothing about the dependency rules +changes — but it is a rewrite of how a session is started, resized and closed, +not a substitution. + +## The principle that does not survive the crossing + +`server/session.go:410` reads the foreground process group off the pty: + +```go +syscall.Syscall(syscall.SYS_IOCTL, s.ptmx.Fd(), syscall.TIOCGPGRP, ...) +``` + +**ConPTY has no concept of a foreground process group.** There is no call that +answers this question, so there is nothing to port. `context.go:67` is the only +caller, and it is what the entire context bar is built on: what the moment is +about, which buttons the server offers, whether you are in a shell or inside +`claude`. + +What is available instead is a snapshot of the process tree — `Toolhelp32Snapshot` +and its relatives — from which one could pick a likely descendant and call it the +foreground. That is **guessing**, which `CLAUDE.md` rules out for exactly the +reason that applies here: a bar that guesses wrong is worse than a fixed one. + +So a Windows port has to make a decision that a Linux reader never has to: + +- **offer less, honestly** — the fixed keypad, plus whatever can be known for + certain, and no tool groups; or +- **change the rule for one platform**, and write down that on Windows the + detection is inferred rather than read, so nobody later reads the principle and + assumes it holds everywhere. + +The first is the smaller lie. Neither is free. + +## Everything else, in the order it will bite + +**`/proc` is not there.** `context.go:180` reads `/proc//cmdline` and +`context.go:198` reads `/proc//cwd`. + +- The command line has a Win32 answer that is merely tedious. +- **The working directory of another process does not.** The documented route is + reading the target's PEB through `ReadProcessMemory` — fragile, privilege- + sensitive, and version-dependent. And cwd is not a detail here: the directory + buttons, the git group and the project shortcuts are all built from it + (`context.go:92` onward). +- The alternative worth costing before writing any PEB code is **OSC 7**, where + the shell announces its own directory in an escape sequence and the server + reads it off the stream it already owns. It moves the burden to the user's + prompt configuration, and it works the same on both platforms. + +**Signals.** `session.go:381` sends `SIGINT` to the negated process group and +`session.go:401` sends `SIGHUP` on close. Windows has neither. +`GenerateConsoleCtrlEvent` exists but is attached-console-scoped; for an +interactive program, writing `0x03` into the ConPTY input is usually the same +thing from the program's point of view. Closing is `ClosePseudoConsole` plus +terminating what is left. + +**Secrets stop being protected.** `console.go:234` writes the config with the +transcription key and the pairing tokens at 0600 in a 0700 directory. On Windows +`os.Chmod` is close to a no-op — access is an ACL, not a mode — so the file lands +readable by more than intended. `docs/security.md` says keys stay on the server +side of the trust boundary; on Windows that sentence requires explicit ACL work +(`golang.org/x/sys/windows`), or it is not true. **This is the one item on the +list that is a security regression rather than a missing feature**, so it cannot +be deferred to "later". + +**Small, and each about an hour:** + +| Where | Now | On Windows | +|---|---|---| +| `main.go:115` | `/bin/bash` fallback | `pwsh` / `powershell.exe` / `cmd.exe` | +| `main.go:271` | `/etc/os-release` → `PRETTY_NAME` | the Windows version, or the card in the headset lies about the machine | +| `asr.go:55` | `~/.config/linux-terminal/` | `%APPDATA%`, if it is to look native | +| `packaging/` | a systemd user unit | a service or a Task Scheduler entry | +| `actions.go:237` | `ls -la`, `git status` | `ls -la` is not a PowerShell command; the run group needs its own table | + +`exec.LookPath` (`actions.go:269`) works on Windows and keeps its promise — +a button is only offered where the command exists — so the mechanism survives +even though its contents do not. + +## What is untouched + +TLS, pairing, the token check, discovery over UDP, the web console, and the ASR +proxy are ordinary Go and need nothing. The tray does too: `fyne.io/systray` +v1.12.2 ships `systray_windows.go`, so the desktop indicator is not a blocker. + +**The client does not change by a single line.** The server decides what the bar +offers and the client draws it, which is precisely the property that makes a +second server platform a server-side project. + +## The answer available today: WSL2 + +If the question is "I want to work on my Windows machine from the headset" +rather than "I want a PowerShell prompt in the headset", then the Linux binary +running inside WSL2 already does it — real pty, real `/proc`, the whole context +bar, no new code. + +Two things to expect, neither verified on hardware for this document: + +- WSL2 is behind its own NAT, so the port has to be reachable from the LAN — + a port proxy on the Windows side, or whichever networking mode the installed + WSL offers. Check what the current version does rather than trusting a + half-remembered recipe. +- A discovery broadcast will not cross that boundary, so the machine is **added + by address** in the connection manager instead of being found. + +This is worth saying out loud to anyone who asks, because for most of the people +who would ask, it is the actual answer. + +## If someone does write the port + +An order that keeps the thing honest at every step: + +1. **ConPTY first, alone.** A session that opens, echoes, resizes and closes. + Until that works, nothing else can be tested, and the four compile errors are + not worth touching. +2. **Shell default, OS version, config location, ACLs on the config file.** The + ACL work belongs here and not later, because shipping without it publishes a + key. +3. **A Windows action table.** The bar's fixed half already works; give the run + group commands that exist. +4. **Context, as its own decision.** Foreground detection and cwd — pick honest + scarcity or documented inference, and write down which. +5. **Only then, CI.** `GOOS=windows` joins the release matrix *after* a session + opens on a real Windows machine — not before. The workflow currently builds + linux/amd64 and linux/arm64, and an `.exe` on the release page that cannot + open a shell is the failure mode `.claude/skills/release/SKILL.md` exists to + prevent. + +## What this document does not know + +- Whether ConPTY alone is enough for the programs this is used with — a + full-screen TUI redrawing several times a second is the case that matters, and + it has not been tried. +- What the process-tree approach would actually return in practice, and how often + it would be wrong. +- The current WSL2 networking behaviour, which changes between releases. + +Everything above about the code, the stubs and the build errors was checked +against this tree; everything in this section was not.